import { test, expect } from '@playwright/test'; import * as fs from 'fs'; import * as path from 'path'; const THEMES = ['parchment-light', 'noble-green', 'graphite-forest-dark'] as const; test.describe('Scenario 13: Communications Visual & Responsive Design System V3 Suite', () => { test.beforeEach(async ({ page }) => { // Navigate directly to Communications module await page.goto('/communications', { waitUntil: 'domcontentloaded' }); await expect(page.locator('h1', { hasText: 'Коммуникации' })).toBeVisible({ timeout: 15000 }); }); test('1. Visual capture and theme validation across all 3 Design System V3 themes', async ({ page, }, testInfo) => { const projectName = testInfo.project.name; const artifactsDir = path.resolve(process.cwd(), 'artifacts/visual-qa'); if (!fs.existsSync(artifactsDir)) { fs.mkdirSync(artifactsDir, { recursive: true }); } for (const theme of THEMES) { // Apply theme to localStorage and DOM await page.evaluate((th) => { window.localStorage.setItem('krona-theme-mode', th); document.documentElement.setAttribute('data-krona-theme', th); const legacy = th === 'graphite-forest-dark' ? 'dark' : th === 'noble-green' ? 'soft-light' : 'light'; document.documentElement.setAttribute('data-theme', legacy); }, theme); // Verify theme attribute on html await expect(page.locator('html')).toHaveAttribute('data-krona-theme', theme); // Brief wait for rendering stability await page.waitForTimeout(300); // Capture screenshot for this theme and viewport const screenshotPath = path.join(artifactsDir, `${theme}-${projectName}.png`); await page.screenshot({ path: screenshotPath, fullPage: false }); expect(fs.existsSync(screenshotPath)).toBe(true); } }); test('2. Header, Slogan Watermark, and Channel Switcher validation', async ({ page }) => { // Check main header await expect(page.locator('h1', { hasText: 'Коммуникации' })).toBeVisible(); await expect( page.locator('text=Все сообщения, почта и каналы компании в одном пространстве') ).toBeVisible(); // Check all 5 tabs await expect(page.getByRole('tab', { name: /мессенджеры/i })).toBeVisible(); await expect(page.getByRole('tab', { name: /email/i })).toBeVisible(); await expect(page.getByRole('tab', { name: /каналы/i })).toBeVisible(); await expect(page.getByRole('tab', { name: /контакты/i })).toBeVisible(); await expect(page.getByRole('tab', { name: /настройки/i })).toBeVisible(); // Check connector pills row await expect(page.getByRole('button', { name: /все.*48/i })).toBeVisible(); await expect(page.getByRole('button', { name: /telegram.*12/i })).toBeVisible(); await expect(page.getByRole('button', { name: /whatsapp.*8/i })).toBeVisible(); await expect(page.getByRole('button', { name: /vk.*6/i })).toBeVisible(); await expect(page.getByRole('button', { name: /max.*4/i })).toBeVisible(); await expect(page.getByRole('button', { name: /email.*10/i })).toBeVisible(); await expect(page.getByRole('button', { name: /подключить/i })).toBeVisible(); }); test('3. Responsive layout behavior and drill-down flow', async ({ page, viewport }) => { const isMobile = viewport && viewport.width < 768; const isTablet = viewport && viewport.width >= 768 && viewport.width < 1280; const isDesktop = viewport && viewport.width >= 1280; if (isMobile) { // Mobile drilldown check: Level 1 (List) -> Level 2 (Chat) -> Level 3 (Contact) const chatList = page.locator('section[aria-label="Список диалогов"]'); await expect(chatList).toBeVisible(); // Click first conversation to drill down to Level 2 (Chat) const firstChat = chatList.locator('button').filter({ hasText: 'Иван Петров' }).first(); await firstChat.click(); // Active chat is now visible const activeChat = page.locator('section[aria-label="Активный диалог"]'); await expect(activeChat).toBeVisible(); // Check Back to list button const backToListBtn = activeChat.locator('button[aria-label="Назад к списку чатов"]'); await expect(backToListBtn).toBeVisible(); // Check bottom contact teaser button const profileTeaserBtn = activeChat.locator('button[aria-label="Открыть карточку контакта"]'); await expect(profileTeaserBtn).toBeVisible(); // Click profile teaser to drill down to Level 3 (Contact) await profileTeaserBtn.click(); const mobileContactCard = page.locator('section[aria-label="Карточка контакта"]'); await expect(mobileContactCard).toBeVisible(); // Check Back to chat button in Level 3 const backToChatBtn = mobileContactCard.locator('button[aria-label="Назад к диалогу"]'); await expect(backToChatBtn).toBeVisible(); await backToChatBtn.click(); // Back in chat Level 2 await expect(activeChat).toBeVisible(); await backToListBtn.click(); // Back in list Level 1 await expect(chatList).toBeVisible(); // Verify no horizontal overflow const hasHorizontalScroll = await page.evaluate(() => { return document.documentElement.scrollWidth > document.documentElement.clientWidth + 1; }); expect(hasHorizontalScroll).toBe(false); } else if (isTablet) { // Tablet (768 vertical and 1024 landscape): 2 columns (Chat list on left, Chat + Horizontal Contact Card on right) const chatList = page.locator('section[aria-label="Список диалогов"]'); await expect(chatList).toBeVisible(); const activeChat = page.locator('section[aria-label="Активный диалог"]'); await expect(activeChat).toBeVisible(); const horizontalContactCard = page.locator('aside[aria-label="Карточка контакта планшет"]'); await expect(horizontalContactCard).toBeVisible(); await expect(horizontalContactCard.locator('text=VIP')).toBeVisible(); } else if (isDesktop) { // Desktop: 3 separate elevated columns side-by-side const chatList = page.locator('section[aria-label="Список диалогов"]'); await expect(chatList).toBeVisible(); const activeChat = page.locator('section[aria-label="Активный диалог"]'); await expect(activeChat).toBeVisible(); const verticalContactCard = page.locator('aside[aria-label="Карточка контакта"]'); await expect(verticalContactCard).toBeVisible(); await expect(verticalContactCard.locator('text=Иван Петров')).toBeVisible(); await expect(verticalContactCard.locator('text=VIP')).toBeVisible(); await expect(verticalContactCard.locator('text=✦ AI-сводка')).toBeVisible(); } }); test('4. Interactive messaging, quick replies, and tab navigation', async ({ page, viewport }) => { const isMobile = viewport && viewport.width < 768; if (isMobile) { // Select chat first on mobile const firstChat = page .locator('section[aria-label="Список диалогов"]') .locator('button') .filter({ hasText: 'Иван Петров' }) .first(); await firstChat.click(); } const activeChat = page.locator('section[aria-label="Активный диалог"]'); await expect(activeChat).toBeVisible(); // Interactive message typing and sending const input = activeChat.locator('textarea[placeholder="Написать сообщение..."]'); await expect(input).toBeVisible(); await input.fill('Отправлено через Playwright тест E2E'); const sendBtn = activeChat.locator('button[aria-label="Отправить сообщение"]'); await expect(sendBtn).toBeEnabled(); await sendBtn.click(); // Verify outgoing bubble appears in active chat stream await expect(activeChat.getByText('Отправлено через Playwright тест E2E')).toBeVisible(); // Interactive Quick Reply chip const quickReplyChip = activeChat.locator('button', { hasText: 'Да, подготовим' }).first(); await expect(quickReplyChip).toBeVisible(); await quickReplyChip.click(); await expect(activeChat.getByText('Да, подготовим').first()).toBeVisible(); // Test tab navigation: Email tab const emailTab = page.getByRole('tab', { name: /email/i }); await emailTab.click({ force: true }); await expect(page.locator('text=Новое письмо')).toBeVisible(); // Channels tab const channelsTab = page.getByRole('tab', { name: /каналы/i }); await channelsTab.click({ force: true }); await expect(page.locator('text=Внутренние каналы')).toBeVisible(); // Contacts tab const contactsTab = page.getByRole('tab', { name: /контакты/i }); await contactsTab.click({ force: true }); await expect(page.locator('input[placeholder="Поиск по контактам..."]')).toBeVisible(); // Settings tab const settingsTab = page.getByRole('tab', { name: /настройки/i }); await settingsTab.click({ force: true }); await expect(page.locator('text=Подключенные мессенджеры и коннекторы')).toBeVisible(); // Return to Messengers const messengersTab = page.getByRole('tab', { name: /мессенджеры/i }); await messengersTab.click({ force: true }); await expect(page.getByRole('button', { name: /все.*48/i })).toBeVisible(); }); test.afterAll(async () => { const artifactsDir = path.resolve(process.cwd(), 'artifacts/visual-qa'); if (!fs.existsSync(artifactsDir)) return; const files = fs.readdirSync(artifactsDir).filter((f) => f.endsWith('.png')); const manifest = { generatedAt: new Date().toISOString(), module: 'communications', version: 'v3.0', referenceMockups: [ 'Коммуникация.png (Desktop 1440/1280)', 'Коммуникация планшет горизонт.png (Tablet landscape 1024)', 'Коммуникация планшет вертикальный.png (Tablet vertical 768)', 'Коммуникация телефон.png (Mobile 360/390/430)', ], themes: ['parchment-light', 'noble-green', 'graphite-forest-dark'], viewports: [ { name: 'mobile-360', width: 360, height: 800, type: 'phone' }, { name: 'mobile-390', width: 390, height: 844, type: 'phone' }, { name: 'mobile-430', width: 430, height: 932, type: 'phone' }, { name: 'tablet-768', width: 768, height: 1024, type: 'tablet-vertical' }, { name: 'tablet-1024', width: 1024, height: 768, type: 'tablet-horizontal' }, { name: 'desktop-1280', width: 1280, height: 800, type: 'desktop' }, { name: 'desktop-1440', width: 1440, height: 900, type: 'desktop' }, { name: 'desktop-1920', width: 1920, height: 1080, type: 'desktop' }, ], screenshots: files.map((file) => ({ filename: file, theme: file.startsWith('graphite-forest-dark') ? 'graphite-forest-dark' : file.startsWith('noble-green') ? 'noble-green' : 'parchment-light', viewport: file.replace(/^(graphite-forest-dark|noble-green|parchment-light)-/, '').replace('.png', ''), capturedAt: fs.statSync(path.join(artifactsDir, file)).mtime.toISOString(), })), }; fs.writeFileSync( path.join(artifactsDir, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf-8' ); }); });