395 lines
17 KiB
TypeScript
395 lines
17 KiB
TypeScript
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'
|
||
);
|
||
});
|
||
});
|