krona-front/e2e/13-communications-visual-and-responsive.spec.ts

395 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.getByRole('button', { name: /Открыть.*контакт/i });
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'
);
});
});