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); }); test('11. Connect new infrastructure server through onboarding wizard', async ({ page }) => { await page.goto('/control/infrastructure'); await page.waitForLoadState('domcontentloaded'); // Click "+ Подключить сервер" const addBtn = page.locator('[data-testid="btn-open-connect-server"]:visible').first(); await expect(addBtn).toBeVisible(); await addBtn.click(); // Verify modal is open const modal = page.locator('[data-testid="connect-server-modal"]'); await expect(modal).toBeVisible(); // Fill Step 1 await page.locator('[data-testid="input-server-name"]').fill('vps-sel-e2e-99'); await page.locator('[data-testid="input-server-ip"]').fill('178.212.13.240'); await page.locator('[data-testid="btn-connect-next"]').click(); // Step 2: SSH config await expect(page.locator('*:has-text("Настройка SSH"):visible').first()).toBeVisible(); await page.locator('[data-testid="btn-connect-next"]').click(); // Step 3: Test connection & Finish await expect(page.locator('*:has-text("Проверка связи"):visible').first()).toBeVisible(); await page.locator('[data-testid="btn-test-ssh"]').click(); await expect(page.locator('*:has-text("SSH-соединение успешно установлено"):visible').first()).toBeVisible(); await page.locator('[data-testid="btn-connect-finish"]').click(); // Verify modal closed and server is present in the list await expect(modal).not.toBeVisible(); await expect(page.locator('*:has-text("vps-sel-e2e-99"):visible').first()).toBeVisible(); }); test('12. Open interactive terminal and execute diagnostic commands', async ({ page }) => { await page.goto('/control/infrastructure'); await page.waitForLoadState('domcontentloaded'); // Click terminal button on first server card const termBtn = page.locator('[data-testid^="terminal-server-"]:visible').first(); await expect(termBtn).toBeVisible(); await termBtn.click(); // Verify terminal modal is open const termModal = page.locator('[data-testid="server-terminal-modal"]'); await expect(termModal).toBeVisible(); // Enter bash command const termInput = page.locator('[data-testid="terminal-input"]'); await termInput.fill('uptime'); await termInput.press('Enter'); // Verify command output await expect(termModal.locator('*:has-text("load average:"):visible').first()).toBeVisible(); // Use quick action chip 'docker ps' const dockerChip = termModal.locator('button:has-text("docker ps"):visible').first(); await dockerChip.click(); await expect(termModal.locator('*:has-text("krona-reverse-proxy"):visible').first()).toBeVisible(); // Close terminal modal await termModal.locator('button[title="Закрыть терминал"]').click(); await expect(termModal).not.toBeVisible(); }); });