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