272 lines
13 KiB
TypeScript
272 lines
13 KiB
TypeScript
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);
|
||
});
|
||
});
|