248 lines
12 KiB
TypeScript
248 lines
12 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 }) => {
|
||
// 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'
|
||
);
|
||
});
|
||
});
|