From 8e0f6406736d6741caaad6cbd2981e0fade61496 Mon Sep 17 00:00:00 2001 From: ochenstarik Date: Fri, 11 Sep 2026 02:34:45 +0700 Subject: [PATCH] test(control-center): add 11 Playwright E2E scenarios across desktop, tablet, and mobile --- e2e/11-control-center.spec.ts | 272 ++++++++++++++++++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 e2e/11-control-center.spec.ts diff --git a/e2e/11-control-center.spec.ts b/e2e/11-control-center.spec.ts new file mode 100644 index 0000000..e952add --- /dev/null +++ b/e2e/11-control-center.spec.ts @@ -0,0 +1,272 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Krona Control Center - Production UX & Operator Scenarios', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/control'); + await page.waitForLoadState('domcontentloaded'); + await expect(page.locator('text=IndexedDB Synced')).toBeVisible({ timeout: 15000 }); + }); + + test('01. Shell loads with zero horizontal overflow and topbar controls', async ({ page }) => { + await expect(page.locator('text=Krona Control Center').first()).toBeVisible(); + await expect(page.locator('button[aria-label="Сессия оператора"]')).toBeVisible(); + + const isOverflowing = await page.evaluate(() => { + return document.documentElement.scrollWidth > document.documentElement.clientWidth; + }); + expect(isOverflowing).toBe(false); + + await expect(page.locator('text=Сводная операторская панель')).toBeVisible(); + await expect(page.locator('text=Организации').first()).toBeVisible(); + await expect(page.locator('text=Пользователи / Точки').first()).toBeVisible(); + }); + + test('02. Scenario 1: Search organization and open 360 inspector drawer', async ({ page }) => { + await page.click('a[href="/control/organizations"]'); + await page.waitForURL('**/control/organizations'); + await expect(page.locator('h1:has-text("Организации платформы")')).toBeVisible(); + + // Search input + const searchInput = page.locator('input[placeholder*="Поиск"]'); + await expect(searchInput).toBeVisible(); + await searchInput.fill('Ритейл'); + + // Filtered result should show Retail Group + await expect(page.locator('text=Северная Ритейл Группа').first()).toBeVisible(); + + // Click 360 card button + const inspectBtn = page.locator('button:has-text("360° Карточка")').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=Северная Ритейл Группа').first()).toBeVisible(); + await expect(drawer.locator('text=Действия оператора платформы')).toBeVisible(); + + // Verify 360 tabs + await expect(drawer.locator('button:has-text("Обзор")')).toBeVisible(); + await expect(drawer.locator('button:has-text("Модули")').first()).toBeVisible(); + + // Switch to Modules tab + await drawer.locator('button:has-text("Модули")').first().click(); + await expect(drawer.locator('div:has-text("v1."), div:has-text("v2."), div:has-text("v3.")').first()).toBeVisible(); + + // Close drawer + await drawer.locator('button[aria-label="Закрыть инспектор"]').click(); + await expect(drawer).not.toBeVisible(); + }); + + test('03. Scenario 2: Dangerous action requires operator authorization & mandatory reason', async ({ page }) => { + await page.click('a[href="/control/organizations"]'); + await page.waitForURL('**/control/organizations'); + + // Open inspector for first org + await page.locator('button:has-text("360° Карточка")').first().click(); + const drawer = page.locator('div[role="complementary"][aria-label="Контекстный инспектор"]'); + await expect(drawer).toBeVisible(); + + // Click trial extension button + const grantTrialBtn = drawer.locator('button:has-text("Выдать Trial")'); + await expect(grantTrialBtn).toBeVisible(); + await grantTrialBtn.click(); + + // Confirm modal opens + const confirmDialog = page.locator('div[role="dialog"][aria-labelledby="confirm-modal-title"]'); + await expect(confirmDialog).toBeVisible(); + await expect(confirmDialog.locator('text=Требуется авторизация оператора')).toBeVisible(); + + // Attempt submit without reason + const submitBtn = confirmDialog.locator('button:has-text("Подтвердить операцию")'); + await submitBtn.click(); + await expect(confirmDialog.locator('text=Обоснование операции обязательно')).toBeVisible(); + + // Fill valid reason (>= 5 chars) + const reasonInput = confirmDialog.locator('textarea[id="action-reason-input"]'); + await reasonInput.fill('Согласованное продление пилота по заявке #PILOT-1049'); + await submitBtn.click(); + + // Modal closes + await expect(confirmDialog).not.toBeVisible(); + }); + + test('04. Scenario 3: Filter subscriptions expiring in 7 days', async ({ page }) => { + await page.click('a[href="/control/billing"]'); + await page.waitForURL('**/control/billing'); + + await expect(page.locator('h1:has-text("Подписки, лицензии и Entitlements")')).toBeVisible(); + + // Click "Истекает 7 дней" filter + const filter7dBtn = page.locator('button:has-text("Истекает 7 дней")'); + await expect(filter7dBtn).toBeVisible(); + await filter7dBtn.click(); + + // Verify filter active styling and table displays matching rows + await expect(page.locator('tbody tr')).not.toHaveCount(0); + await expect(page.locator('text=Истекает (< 7 дн.)').first()).toBeVisible(); + }); + + test('05. Scenario 4: Monitor installations and identify outdated Android terminals', async ({ page }) => { + await page.click('a[href="/control/installations"]'); + await page.waitForURL('**/control/installations'); + + await expect(page.locator('h1:has-text("Установки и клиентские приложения")')).toBeVisible(); + + // Click quick filter button for outdated Android + const outdatedAndroidBtn = page.locator('button:has-text("Показать только устаревшие Android")'); + await expect(outdatedAndroidBtn).toBeVisible(); + await outdatedAndroidBtn.click(); + + // Outdated Android device should be visible in table + await expect(page.locator('text=Android').first()).toBeVisible(); + await expect(page.locator('text=Устарела').first()).toBeVisible(); + }); + + test('06. Scenario 5: Inspect infrastructure nodes and Server Monitor outage resilience', async ({ page }) => { + await page.click('a[href="/control/infrastructure"]'); + await page.waitForURL('**/control/infrastructure'); + + await expect(page.locator('h1:has-text("Инфраструктурные узлы")')).toBeVisible(); + + // Check circuit breaker banner simulation button + const toggleBannerBtn = page.locator('button[title*="Server Monitor"]'); + await expect(toggleBannerBtn).toBeVisible(); + + // Verify server node metrics + await expect(page.locator('text=CPU:').first()).toBeVisible(); + + // Click first server card to open inspector + const firstServerCard = page.locator('div[class*="cursor-pointer"]:has-text("CPU:")').first(); + await firstServerCard.click(); + + // Context drawer opens with server details + const drawer = page.locator('div[role="complementary"][aria-label="Контекстный инспектор"]'); + await expect(drawer).toBeVisible(); + await expect(drawer.locator('text=Телеметрия узла').first()).toBeVisible(); + await drawer.locator('button[aria-label="Закрыть инспектор"]').click(); + }); + + test('07. Scenario 6: Create incident workflow from alert', async ({ page }) => { + await page.click('a[href="/control/infrastructure"]'); + await page.waitForURL('**/control/infrastructure'); + + // Find a problematic server with "Создать инцидент" + const createIncBtn = page.locator('button:has-text("Создать инцидент")').first(); + await expect(createIncBtn).toBeVisible(); + await createIncBtn.click(); + + // Confirm modal opens + const confirmModal = page.locator('div[role="dialog"][aria-labelledby="confirm-modal-title"]'); + await expect(confirmModal).toBeVisible(); + await confirmModal.locator('textarea[id="action-reason-input"]').fill('Эскалация дежурным инженером NOC по алерту узла'); + await confirmModal.locator('button:has-text("Подтвердить операцию")').click(); + + // Navigate to Incidents desk + await page.click('a[href="/control/incidents"]'); + await page.waitForURL('**/control/incidents'); + + // The incident alert is listed + await expect(page.locator('text=Критический сбой узла').first()).toBeVisible(); + }); + + test('08. Scenario 7: Create targeted tenant announcement', async ({ page }) => { + await page.click('a[href="/control/announcements"]'); + await page.waitForURL('**/control/announcements'); + + await expect(page.locator('h1:has-text("Оповещения и системные сообщения")')).toBeVisible(); + + // Open creation form + await page.locator('button:has-text("Создать объявление")').click(); + + // Fill title + await page.locator('input[placeholder*="Omnichannel"]').fill('Плановое ночное техобслуживание'); + + // Fill message body + await page.locator('textarea[placeholder*="текст объявления"]').fill('12 сентября с 03:00 до 04:00 МСК будут проводиться регламентные работы.'); + + // Submit form + await page.locator('button[type="submit"]:has-text("Опубликовать")').click(); + + // Confirmation + const confirmModal = page.locator('div[role="dialog"][aria-labelledby="confirm-modal-title"]'); + await expect(confirmModal).toBeVisible(); + await confirmModal.locator('textarea[id="action-reason-input"]').fill('Согласованный релизный бюллетень #REL-402'); + await confirmModal.locator('button:has-text("Подтвердить операцию")').click(); + await expect(page.locator('text=Плановое ночное техобслуживание').first()).toBeVisible(); + }); + + test('09. Scenario 8: Manage module rollouts (Pause / Resume)', async ({ page }) => { + await page.click('a[href="/control/modules"]'); + await page.waitForURL('**/control/modules'); + + await expect(page.locator('h1:has-text("Каталог модулей и Rollout конвейер")')).toBeVisible(); + + // Select module with active rollout (Омниканальные коммуникации) + await page.locator('text=Омниканальные коммуникации').first().click(); + + // Verify active rollout badge + const activeRolloutBadge = page.locator('text=Пилот (Pilot)').first(); + await expect(activeRolloutBadge).toBeVisible(); + + // Click Pause + const pauseBtn = page.locator('button:has-text("Поставить на паузу")'); + if (await pauseBtn.isVisible()) { + await pauseBtn.click(); + + const confirmModal = page.locator('div[role="dialog"][aria-labelledby="confirm-modal-title"]'); + await expect(confirmModal).toBeVisible(); + await confirmModal.locator('textarea[id="action-reason-input"]').fill('Временная приостановка раскатки до проверки логов'); + await confirmModal.locator('button:has-text("Подтвердить операцию")').click(); + + // Rollout state should now show "На паузе" + await expect(page.locator('text=На паузе').first()).toBeVisible(); + } + }); + + test('10. Scenario 9: Switch operator role and observe strict RBAC enforcement', async ({ page }) => { + const switcher = page.locator('button[aria-label="Сессия оператора"]'); + await switcher.click(); + + // Switch to security_auditor + const auditorItem = page.locator('button[role="menuitem"]:has-text("Аудитор безопасности")'); + await expect(auditorItem).toBeVisible(); + await auditorItem.click(); + + // Topbar now shows "Аудитор безопасности" + await expect(page.locator('text=Аудитор безопасности').first()).toBeVisible(); + + // Navigate to Modules page: triggering rollouts should be disabled for auditor + await page.click('a[href="/control/modules"]'); + await page.waitForURL('**/control/modules'); + + const rolloutBtn = page.locator('button:has-text("Запустить пилотный Rollout"), button:has-text("Поставить на паузу")').first(); + if (await rolloutBtn.isVisible()) { + await expect(rolloutBtn).toBeDisabled(); + } + + // Switch back to platform_owner + await switcher.click(); + const ownerItem = page.locator('button[role="menuitem"]:has-text("Владелец платформы")'); + await ownerItem.click(); + await expect(page.locator('text=Владелец платформы').first()).toBeVisible(); + }); + + test('11. Scenario 10: State survives browser page reload via IndexedDB', async ({ page }) => { + await page.click('a[href="/control/audit"]'); + await page.waitForURL('**/control/audit'); + await expect(page.locator('h1:has-text("Неизменяемый журнал аудита")')).toBeVisible(); + + // Wait for audit log rows to load from IndexedDB + await expect(page.locator('tbody tr')).toHaveCount(3); + const initialRowsCount = await page.locator('tbody tr').count(); + expect(initialRowsCount).toBe(3); + + // Reload page + await page.reload(); + await page.waitForLoadState('domcontentloaded'); + await expect(page.locator('text=IndexedDB Synced')).toBeVisible({ timeout: 15000 }); + + // Audit entries must still be present from IndexedDB + await expect(page.locator('tbody tr')).toHaveCount(initialRowsCount); + }); +});