krona-front/e2e/12-mobile-first.spec.ts

189 lines
7.6 KiB
TypeScript

import { test, expect } from '@playwright/test';
test.describe('Scenario 12: Mobile-First and Tablet Architecture Overhaul', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
});
test('1. Mobile viewport (< 768px) automatically displays bottom navigation', async ({ page, viewport }) => {
const isMobileViewport = viewport && viewport.width < 768;
if (isMobileViewport) {
// Bottom navigation must be visible
const bottomNav = page.locator('[data-testid="bottom-nav"]');
await expect(bottomNav).toBeVisible({ timeout: 10000 });
// Sidebar must NOT be displayed
const sidebar = page.locator('aside');
await expect(sidebar).not.toBeVisible();
// Topbar must have mobile controls (logo + section title + menu button)
await expect(page.locator('[data-testid="mobile-menu-btn"]')).toBeVisible();
await expect(page.locator('[data-testid="mobile-current-title"]')).toBeVisible();
} else {
// Desktop / tablet view: sidebar visible, bottom nav not visible
const sidebar = page.locator('[data-testid="sidebar"]');
await expect(sidebar).toBeVisible({ timeout: 10000 });
const bottomNav = page.locator('[data-testid="bottom-nav"]');
await expect(bottomNav).not.toBeVisible();
}
});
test('2. Mobile bottom nav tabs are functional with >= 44x44px touch targets', async ({ page, viewport }) => {
const isMobileViewport = viewport && viewport.width < 768;
if (!isMobileViewport) return;
// Check touch target heights
const catalogTab = page.locator('[data-testid="bottom-nav-catalog"]');
await expect(catalogTab).toBeVisible();
const box = await catalogTab.boundingBox();
expect(box).not.toBeNull();
if (box) {
expect(box.height).toBeGreaterThanOrEqual(44);
expect(box.width).toBeGreaterThanOrEqual(44);
}
// Navigate to Tasks via bottom nav
const tasksTab = page.locator('[data-testid="bottom-nav-tasks"]');
await tasksTab.click();
await page.waitForURL('**/tasks');
await expect(page.locator('h1, h2').first()).toBeVisible();
// Navigate to Communications via bottom nav
const commsTab = page.locator('[data-testid="bottom-nav-communications"]');
await commsTab.click();
await page.waitForURL('**/communications');
await expect(page.locator('h1, h2').first()).toBeVisible();
// Return to Catalog
await catalogTab.click();
await page.waitForURL('**/');
await expect(page.locator('[data-testid="modules-catalog-container"]')).toBeVisible();
});
test('3. Mobile menu drawer opens and allows switching themes and profiles', async ({ page, viewport }) => {
const isMobileViewport = viewport && viewport.width < 768;
if (!isMobileViewport) return;
// Open via "Ещё" tab in bottom nav
const moreTab = page.locator('[data-testid="bottom-nav-more"]');
await moreTab.click();
// Drawer must be visible
const drawer = page.locator('[data-testid="mobile-menu-drawer"]');
await expect(drawer).toBeVisible();
// Check drawer content: profiles, roles, themes, reset
await expect(drawer).toContainText('Управление платформой');
await expect(drawer).toContainText('Отрасль и профиль организации');
await expect(drawer).toContainText('Тема оформления');
await expect(page.locator('[data-testid="mobile-reset-db-btn"]')).toBeVisible();
// Switch theme to dark
const darkThemeBtn = drawer.getByRole('button', { name: /Тёмная/i });
await darkThemeBtn.click();
const themeAttr = await page.evaluate(() => document.documentElement.getAttribute('data-theme'));
expect(themeAttr).toBe('dark');
// Close drawer
const closeBtn = page.locator('[data-testid="close-mobile-menu"]');
await closeBtn.click();
await expect(drawer).not.toBeVisible();
});
test('4. Zero horizontal overflow across all viewports', async ({ page }) => {
// Check main document scroll width vs client width
const isOverflowing = await page.evaluate(() => {
return document.documentElement.scrollWidth > document.documentElement.clientWidth;
});
expect(isOverflowing).toBe(false);
// Verify on /crm
await page.goto('/crm');
await page.waitForLoadState('domcontentloaded');
const isCrmOverflowing = await page.evaluate(() => {
return document.documentElement.scrollWidth > document.documentElement.clientWidth;
});
expect(isCrmOverflowing).toBe(false);
// Verify on /settings/entities
await page.goto('/settings/entities');
await page.waitForLoadState('domcontentloaded');
const isSettingsOverflowing = await page.evaluate(() => {
return document.documentElement.scrollWidth > document.documentElement.clientWidth;
});
expect(isSettingsOverflowing).toBe(false);
});
test('5. DynamicTable renders responsive card list on mobile screens', async ({ page, viewport }) => {
await page.goto('/crm');
await page.waitForLoadState('domcontentloaded');
const isMobileViewport = viewport && viewport.width < 768;
if (isMobileViewport) {
// Mobile card list must be visible
const mobileCards = page.locator('[data-testid="mobile-table-cards"]');
if (await mobileCards.isVisible()) {
await expect(mobileCards).toBeVisible();
}
}
});
test('6. FormBuilder supports touch-friendly up and down field reordering', async ({ page }) => {
await page.goto('/settings/entities/contact/form-builder');
await page.waitForLoadState('domcontentloaded');
// Locate field move buttons
const moveDownBtn = page.locator('[data-testid^="move-field-down-"]').first();
if (await moveDownBtn.isVisible()) {
await expect(moveDownBtn).toBeVisible();
// Verify button has accessible touch target
const box = await moveDownBtn.boundingBox();
expect(box).not.toBeNull();
if (box) {
expect(box.height).toBeGreaterThanOrEqual(28);
}
}
});
test('7. Full Russian localization check (zero untranslated system labels)', async ({ page }) => {
// Check home page headings and buttons
const bodyText = await page.locator('body').innerText();
// Forbidden English words in visible platform headers
expect(bodyText).not.toContain('Business OS');
expect(bodyText).not.toContain('Role: Owner');
expect(bodyText).not.toContain('Quick Action');
});
test('8. All 3 brand themes (soft-light, light, dark) apply and persist', async ({ page }) => {
// Set light theme
await page.evaluate(() => {
window.localStorage.setItem('krona_theme_mode', 'light');
document.documentElement.setAttribute('data-theme', 'light');
});
await page.reload();
let currentTheme = await page.evaluate(() => document.documentElement.getAttribute('data-theme'));
expect(currentTheme).toBe('light');
// Set dark theme
await page.evaluate(() => {
window.localStorage.setItem('krona_theme_mode', 'dark');
document.documentElement.setAttribute('data-theme', 'dark');
});
await page.reload();
currentTheme = await page.evaluate(() => document.documentElement.getAttribute('data-theme'));
expect(currentTheme).toBe('dark');
// Restore soft-light
await page.evaluate(() => {
window.localStorage.setItem('krona_theme_mode', 'soft-light');
document.documentElement.setAttribute('data-theme', 'soft-light');
});
await page.reload();
currentTheme = await page.evaluate(() => document.documentElement.getAttribute('data-theme'));
expect(currentTheme).toBe('soft-light');
});
});