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

272 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
});
});