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 }) => { test.setTimeout(60000); // 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. Compact header and channel switcher validation', async ({ page }) => { const vp = page.viewportSize(); const isMobile = (vp?.width ?? 1200) < 640; // Check main header await expect(page.locator('h1', { hasText: 'Коммуникации' })).toBeVisible(); await expect(page.getByText('Все сообщения, почта и каналы компании в одном пространстве')).toHaveCount(0); await expect(page.getByText('Больше, чем общение с поддержкой')).toHaveCount(0); await expect(page.getByRole('button', { name: /добавить канал/i })).toHaveCount(0); // 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(); if (isMobile) { await expect(page.getByRole('button', { name: /подключить канал/i })).toBeVisible(); } else { await expect(page.getByRole('button', { name: /подключить/i })).toBeVisible(); } }); test('3. Responsive layout behavior, geometric assertions and state screenshots', 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 }); } const layoutMode = await page.locator('[data-layout-mode]').getAttribute('data-layout-mode'); const isMobile = layoutMode === 'compact'; const isTablet = layoutMode === 'medium'; const isDesktop = layoutMode === 'wide'; if (isMobile) { // 1. Mobile drilldown check: Level 1 (List) -> Level 2 (Chat) -> Level 3 (Contact) const chatList = page.locator('section[aria-label="Список диалогов"]'); await expect(chatList).toBeVisible(); // State Screenshot 1: Mobile List if (projectName === 'mobile-390' || projectName === 'mobile-360') { await page.screenshot({ path: path.join(artifactsDir, 'mobile-list.png'), fullPage: false, }); } // Check single-row tabs with scroll const tabsList = page.locator('div[role="tablist"]'); const tabsBox = await tabsList.boundingBox(); expect(tabsBox).not.toBeNull(); if (tabsBox) { expect(tabsBox.height).toBeLessThanOrEqual(56); } // 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(); // Chat list is NOT visible in compact mode await expect(chatList).toBeHidden(); // State Screenshot 2: Mobile Chat if (projectName === 'mobile-390' || projectName === 'mobile-360') { await page.screenshot({ path: path.join(artifactsDir, 'mobile-chat.png'), fullPage: false, }); } // 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(); // Check message composer is positioned within viewport const composer = activeChat.locator('form'); await expect(composer).toBeVisible(); const composerBox = await composer.boundingBox(); const vp = page.viewportSize(); if (composerBox && vp) { expect(composerBox.y + composerBox.height).toBeLessThanOrEqual(vp.height + 10); } // Click profile teaser to drill down to Level 3 (Contact) await profileTeaserBtn.click(); const mobileContactCard = page.locator('section[aria-label="Карточка контакта"]'); await expect(mobileContactCard).toBeVisible(); await expect(activeChat).toBeHidden(); // State Screenshot 3: Mobile Contact if (projectName === 'mobile-390' || projectName === 'mobile-360') { await page.screenshot({ path: path.join(artifactsDir, 'mobile-contact.png'), fullPage: false, }); } // 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) { // 2. Tablet: 2 columns (Chat list + Active Chat). Contact card opens via slide-over Drawer. const chatList = page.locator('section[aria-label="Список диалогов"]'); await expect(chatList).toBeVisible(); const activeChat = page.locator('section[aria-label="Активный диалог"]'); await expect(activeChat).toBeVisible(); // Verify Chat width >= 320px const chatBox = await activeChat.boundingBox(); expect(chatBox).not.toBeNull(); if (chatBox) { expect(chatBox.width).toBeGreaterThanOrEqual(320); } // Verify no squished under-chat horizontal card exists const squishedCard = page.locator('aside[aria-label="Карточка контакта планшет"]'); await expect(squishedCard).toHaveCount(0); // State Screenshot 4: Tablet Chat if (projectName === 'tablet-768' || projectName === 'tablet-1024') { await page.screenshot({ path: path.join(artifactsDir, 'tablet-chat.png'), fullPage: false, }); } // Open slide-over Drawer via contact button in chat header const openContactBtn = activeChat.locator('button[aria-label="Открыть контакт"]'); await expect(openContactBtn).toBeVisible(); await openContactBtn.click(); // Drawer is open const drawer = page.locator('div[role="dialog"]'); await expect(drawer).toBeVisible(); await expect(drawer.locator('text=Карточка контакта')).toBeVisible(); await expect(drawer.locator('text=Иван Петров').first()).toBeVisible(); await expect(drawer.locator('text=VIP')).toBeVisible(); // State Screenshot 5: Tablet Contact Drawer if (projectName === 'tablet-768' || projectName === 'tablet-1024') { await page.screenshot({ path: path.join(artifactsDir, 'tablet-contact-drawer.png'), fullPage: false, }); } // Close Drawer const closeDrawerBtn = drawer.locator('button[aria-label="Закрыть"]').first(); if (await closeDrawerBtn.isVisible()) { await closeDrawerBtn.click(); } else { await page.keyboard.press('Escape'); } await expect(drawer).toBeHidden(); } else if (isDesktop) { // 3. Desktop: 3 columns (Chat list + Active Chat + Aside Contact card) 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(); // Test tab inside Contact Card await verticalContactCard.getByRole('button', { name: 'AI-сводка' }).click(); await expect(verticalContactCard.locator('text=✦ AI-сводка')).toBeVisible(); // Verify scrollY is 0 const scrollY = await page.evaluate(() => window.scrollY); expect(scrollY).toBe(0); // State Screenshot 6: Desktop without Krona AI if (projectName === 'desktop-1440') { await page.screenshot({ path: path.join(artifactsDir, 'desktop-without-krona-ai.png'), fullPage: false, }); } // Open Krona AI drawer const aiBtn = page.locator('button[title="Krona AI — Бизнес-оркестратор"]'); if (await aiBtn.isVisible()) { await aiBtn.click(); const aiDrawer = page.locator('text=✦ Krona AI — Бизнес-оркестратор'); await expect(aiDrawer).toBeVisible(); // State Screenshot 7: Desktop with Krona AI if (projectName === 'desktop-1440') { await page.screenshot({ path: path.join(artifactsDir, 'desktop-with-krona-ai.png'), fullPage: false, }); } // Close AI drawer await page.keyboard.press('Escape'); await page.waitForTimeout(300); } } }); test('4. Interactive messaging, quick replies, and tab navigation', async ({ page }) => { const layoutMode = await page.locator('[data-layout-mode]').getAttribute('data-layout-mode'); const isMobile = layoutMode === 'compact'; 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' : file.startsWith('parchment-light') ? 'parchment-light' : 'state-capture', 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' ); }); });