test(control-center): add 11 Playwright E2E scenarios across desktop, tablet, and mobile
This commit is contained in:
parent
47dbadafe9
commit
8e0f640673
1 changed files with 272 additions and 0 deletions
272
e2e/11-control-center.spec.ts
Normal file
272
e2e/11-control-center.spec.ts
Normal file
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue