krona-front/e2e/11-control-center.spec.ts

285 lines
13 KiB
TypeScript

import { test, expect } from '@playwright/test';
// ---------------------------------------------------------------------------
// 1. ACCESS GATE LOGIN & VALIDATION (Isolated describe without pre-auth)
// ---------------------------------------------------------------------------
test.describe('Krona Control Center - Access Gate', () => {
test('01. Access Gate redirects unauthorized users and authenticates with PIN krona2026', async ({ page }) => {
// Clear any existing localStorage before page loads
await page.addInitScript(() => {
localStorage.clear();
});
await page.goto('/control');
await page.waitForURL('**/control/gate');
await expect(page.locator('h1:has-text("Защита доступа")')).toBeVisible();
// Invalid PIN
const pinInput = page.locator('[data-testid="pin-input"]');
await pinInput.fill('wrong_pin_123');
await page.locator('[data-testid="pin-submit"]').click();
await expect(page.locator('[data-testid="pin-error"]')).toBeVisible();
// Valid PIN: krona2026
await pinInput.fill('krona2026');
await page.locator('[data-testid="pin-submit"]').click();
await page.waitForURL('**/control');
await expect(page.locator('h1:has-text("Сводная операторская панель")')).toBeVisible({ timeout: 15000 });
});
});
// ---------------------------------------------------------------------------
// 2. MAIN PRODUCTION & MOBILE-FIRST SUITE (Pre-authorized via kcc_access_gate)
// ---------------------------------------------------------------------------
test.describe('Krona Control Center - Mobile-First & Production UX Suite', () => {
test.beforeEach(async ({ context }) => {
await context.addInitScript(() => {
localStorage.setItem('kcc_access_gate', '1');
});
});
test('02. Mobile menu (burger) opens drawer and navigates across sections', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/control');
await page.waitForLoadState('domcontentloaded');
await expect(page.locator('h1:has-text("Сводная операторская панель")')).toBeVisible({ timeout: 15000 });
const menuBtn = page.locator('[data-testid="mobile-menu-button"]');
await expect(menuBtn).toBeVisible();
await menuBtn.click();
// Drawer opens
const drawerNavOrg = page.locator('[data-testid="drawer-nav-organizations"]');
await expect(drawerNavOrg).toBeVisible();
await drawerNavOrg.click();
await page.waitForURL('**/control/organizations');
await expect(page.locator('h1:has-text("Организации платформы")')).toBeVisible();
});
test('03. Switch 3 brandbook themes (soft-light, light, dark) and verify data-theme on html', async ({ page }) => {
await page.goto('/control');
await page.waitForLoadState('domcontentloaded');
await expect(page.locator('h1:has-text("Сводная операторская панель")')).toBeVisible({ timeout: 15000 });
const isMobile = await page.locator('[data-testid="mobile-menu-button"]').isVisible();
if (isMobile) {
await page.locator('[data-testid="mobile-menu-button"]').click();
// Soft-light
await page.locator('[data-testid="mobile-theme-soft-light"]').click();
await expect(page.locator('html')).toHaveAttribute('data-theme', 'soft-light');
// Light
await page.locator('[data-testid="mobile-theme-light"]').click();
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light');
// Dark
await page.locator('[data-testid="mobile-theme-dark"]').click();
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
// Back to soft-light
await page.locator('[data-testid="mobile-theme-soft-light"]').click();
await expect(page.locator('html')).toHaveAttribute('data-theme', 'soft-light');
await page.locator('[data-testid="close-mobile-drawer"]').click();
} else {
// Desktop theme switcher
await page.locator('[data-testid="theme-toggle-light"]').click();
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light');
await page.locator('[data-testid="theme-toggle-dark"]').click();
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
await page.locator('[data-testid="theme-toggle-soft-light"]').click();
await expect(page.locator('html')).toHaveAttribute('data-theme', 'soft-light');
}
});
test('04. Zero horizontal overflow on all 7 target screen sizes', async ({ page }) => {
test.setTimeout(60000);
const viewports = [
{ width: 360, height: 800 },
{ width: 390, height: 844 },
{ width: 430, height: 932 },
{ width: 768, height: 1024 },
{ width: 1024, height: 768 },
{ width: 1280, height: 800 },
{ width: 1440, height: 900 },
];
for (const vp of viewports) {
await page.setViewportSize(vp);
await page.goto('/control');
await page.waitForLoadState('domcontentloaded');
const hasOverflow = await page.evaluate(() => {
return document.documentElement.scrollWidth > document.documentElement.clientWidth;
});
expect(hasOverflow, `Horizontal overflow at ${vp.width}x${vp.height} on /control`).toBe(false);
await page.goto('/control/organizations');
await page.waitForLoadState('domcontentloaded');
const hasOrgOverflow = await page.evaluate(() => {
return document.documentElement.scrollWidth > document.documentElement.clientWidth;
});
expect(hasOrgOverflow, `Horizontal overflow at ${vp.width}x${vp.height} on /control/organizations`).toBe(false);
}
});
test('05. 360 inspector drawer opens, switches tabs, and closes cleanly', async ({ page }) => {
await page.goto('/control/organizations');
await page.waitForLoadState('domcontentloaded');
await expect(page.locator('h1:has-text("Организации платформы")')).toBeVisible();
// Click 360 card button (whichever is visible on current viewport)
const inspectBtn = page.locator('button:has-text("360° Карточка"):visible').first();
await inspectBtn.click();
// Contextual Drawer opens
const drawer = page.locator('div[role="complementary"][aria-label="Контекстный инспектор"]');
await expect(drawer).toBeVisible();
await expect(drawer.locator('text=Действия оператора платформы')).toBeVisible();
// Verify 360 tabs
await expect(drawer.locator('button:has-text("Обзор")')).toBeVisible();
const modulesTab = drawer.locator('button:has-text("Модули")').first();
await expect(modulesTab).toBeVisible();
await modulesTab.click();
// Close drawer
await drawer.locator('button[aria-label="Закрыть инспектор"]').click();
await expect(drawer).not.toBeVisible();
});
test('06. Filters, tabs and accordions operate correctly', async ({ page }) => {
// Organizations search
await page.goto('/control/organizations');
await page.waitForLoadState('domcontentloaded');
const searchInput = page.locator('input[placeholder*="Поиск"]');
await searchInput.fill('Ритейл');
await expect(page.locator('*:has-text("Северная Ритейл Группа"):visible').first()).toBeVisible();
// Billing quick filters
await page.goto('/control/billing');
await page.waitForLoadState('domcontentloaded');
const filter7d = page.locator('[data-testid="filter-expiring_7d"], button:has-text("7 дн")').first();
await expect(filter7d).toBeVisible();
await filter7d.click();
await expect(page.locator('*:has-text("Истекает (< 7 дн.)"):visible').first()).toBeVisible();
// Installations filter
await page.goto('/control/installations');
await page.waitForLoadState('domcontentloaded');
const filterOutdated = page.locator('button:has-text("Показать только устаревшие Android")');
await expect(filterOutdated).toBeVisible();
await filterOutdated.click();
await expect(page.locator('*:has-text("Android"):visible').first()).toBeVisible();
// Infrastructure node telemetry
await page.goto('/control/infrastructure');
await page.waitForLoadState('domcontentloaded');
await expect(page.locator('*:has-text("CPU"):visible').first()).toBeVisible();
});
test('07. Operator dangerous action requires authorization & valid reason', async ({ page }) => {
await page.goto('/control/organizations');
await page.waitForLoadState('domcontentloaded');
await page.locator('button:has-text("360° Карточка"):visible').first().click();
const drawer = page.locator('div[role="complementary"][aria-label="Контекстный инспектор"]');
await expect(drawer).toBeVisible();
const trialBtn = drawer.locator('button:has-text("Выдать Trial")');
await expect(trialBtn).toBeVisible();
await trialBtn.click();
const modal = page.locator('div[role="dialog"][aria-labelledby="confirm-modal-title"]');
await expect(modal).toBeVisible();
await expect(modal.locator('text=Требуется авторизация оператора')).toBeVisible();
// Submit empty reason -> validation error
const submitBtn = modal.locator('button:has-text("Подтвердить операцию")');
await submitBtn.click();
await expect(modal.locator('text=Обоснование операции обязательно')).toBeVisible();
// Fill valid reason (>= 5 chars)
await modal.locator('textarea[id="action-reason-input"]').fill('Согласованное продление пилота #PILOT-2026');
await submitBtn.click();
await expect(modal).not.toBeVisible();
});
test('08. Switch operator role and observe RBAC enforcement', async ({ page }) => {
await page.goto('/control');
await page.waitForLoadState('domcontentloaded');
const isMobile = await page.locator('[data-testid="mobile-menu-button"]').isVisible();
if (isMobile) {
await page.locator('[data-testid="mobile-menu-button"]').click();
}
const switcher = page.locator('[data-testid="operator-session-button"]:visible').first();
await switcher.click();
const auditorOption = page.locator('[data-testid="role-option-security_auditor"]:visible').first();
await expect(auditorOption).toBeVisible();
await auditorOption.click();
// Header / profile now indicates auditor
await expect(page.locator('*:has-text("Аудитор"):visible').first()).toBeVisible();
// Navigate to modules: triggering rollout should be disabled for auditor
await page.goto('/control/modules');
await page.waitForLoadState('domcontentloaded');
const rolloutBtn = page.locator('button:has-text("Запустить пилотный Rollout"), button:has-text("Поставить на паузу")').first();
if (await rolloutBtn.isVisible()) {
await expect(rolloutBtn).toBeDisabled();
}
// Switch back to owner
if (await page.locator('[data-testid="mobile-menu-button"]').isVisible()) {
await page.locator('[data-testid="mobile-menu-button"]').click();
}
const switcherBack = page.locator('[data-testid="operator-session-button"]:visible').first();
await switcherBack.click();
await page.locator('[data-testid="role-option-platform_owner"]:visible').first().click();
await expect(page.locator('*:has-text("Владелец"):visible').first()).toBeVisible();
});
test('09. Control Center UI is completely localized in Russian', async ({ page }) => {
const pagesToCheck = [
{ url: '/control', heading: 'Сводная операторская панель' },
{ url: '/control/organizations', heading: 'Организации платформы' },
{ url: '/control/billing', heading: 'Подписки и биллинг платформы' },
{ url: '/control/modules', heading: 'Каталог модулей и Rollout конвейер' },
{ url: '/control/installations', heading: 'Установки и клиентские приложения' },
{ url: '/control/infrastructure', heading: 'Инфраструктурные серверы' },
{ url: '/control/announcements', heading: 'Оповещения и системные сообщения' },
{ url: '/control/incidents', heading: 'Журнал инцидентов платформы' },
{ url: '/control/audit', heading: 'Неизменяемый журнал аудита действий' },
];
for (const p of pagesToCheck) {
await page.goto(p.url);
await page.waitForLoadState('domcontentloaded');
await expect(page.locator(`h1:has-text("${p.heading}"):visible`).first()).toBeVisible();
}
});
test('10. State survives browser page reload via IndexedDB', async ({ page }) => {
await page.goto('/control/audit');
await page.waitForLoadState('domcontentloaded');
await expect(page.locator('h1:has-text("Неизменяемый журнал аудита")')).toBeVisible();
// Verify audit log rows
await expect(page.locator('tbody tr')).not.toHaveCount(0);
const initialRowsCount = await page.locator('tbody tr').count();
// Reload page
await page.reload();
await page.waitForLoadState('domcontentloaded');
await expect(page.locator('h1:has-text("Неизменяемый журнал аудита")')).toBeVisible();
await expect(page.locator('tbody tr')).toHaveCount(initialRowsCount);
});
});