Files
Claude-Code-Workflow/ccw/frontend/tests/e2e/helpers/dashboard-helpers.ts
catlog22 ba5f4eba84 feat: Implement dynamic test-fix execution phase with adaptive task generation
- Added Phase 2: Test-Cycle Execution documentation outlining the process for dynamic test-fix execution, including agent roles, core responsibilities, intelligent strategy engine, and progressive testing.
- Introduced new PowerShell scripts for analyzing TypeScript errors, focusing on error categorization and reporting.
- Created end-to-end tests for the Help Page, ensuring content visibility, documentation navigation, internationalization support, and accessibility compliance.
2026-02-07 17:01:30 +08:00

409 lines
12 KiB
TypeScript

// ========================================
// Dashboard E2E Helper Functions
// ========================================
// Reusable utilities for dashboard E2E interactions
import { Page, Locator, expect } from '@playwright/test';
/**
* Wait for all dashboard widgets to finish loading
* @param page - Playwright Page object
* @param timeout - Maximum wait time in milliseconds (default: 30000)
*/
export async function waitForDashboardLoad(page: Page, timeout = 30000): Promise<void> {
// Wait for network idle first
await page.waitForLoadState('networkidle', { timeout });
// Wait for dashboard grid container to be visible
const dashboardGrid = page.getByTestId('dashboard-grid-container').or(
page.locator('.dashboard-grid-container')
);
await expect(dashboardGrid).toBeVisible({ timeout });
// Wait for all widget skeletons to disappear
const skeletons = page.locator('[data-testid*="skeleton"]');
const skeletonCount = await skeletons.count();
if (skeletonCount > 0) {
// Wait for skeletons to be hidden
await page.waitForFunction(
() => {
const skels = document.querySelectorAll('[data-testid*="skeleton"]');
return Array.from(skels).every(
(skel) => window.getComputedStyle(skel).display === 'none'
);
},
{ timeout }
);
}
// Wait for stats cards to be visible
const statsCards = page.getByTestId(/stat-card/).or(page.locator('.stat-card'));
const statsCount = await statsCards.count();
if (statsCount > 0) {
await expect(statsCards.first()).toBeVisible({ timeout });
}
// Wait for animations to complete - use waitForLoadState instead of timeout
await page.waitForLoadState('domcontentloaded');
}
/**
* Verify a specific chart type has rendered correctly
* @param page - Playwright Page object
* @param chartType - Type of chart to verify
* @returns Promise<boolean> indicating if chart rendered successfully
*/
export async function verifyChartRendered(
page: Page,
chartType: 'pie' | 'line' | 'bar'
): Promise<boolean> {
let chartSelector: string;
switch (chartType) {
case 'pie':
chartSelector = '[data-testid="workflow-status-pie-chart"]';
break;
case 'line':
chartSelector = '[data-testid="activity-line-chart"]';
break;
case 'bar':
chartSelector = '[data-testid="task-type-bar-chart"]';
break;
}
// Find chart container
const chartContainer = page.locator(chartSelector);
const isVisible = await chartContainer.isVisible().catch(() => false);
if (!isVisible) {
return false;
}
// Verify chart has rendered content (SVG elements)
const svgElement = chartContainer.locator('svg').first();
const hasSvg = await svgElement.isVisible().catch(() => false);
if (!hasSvg) {
return false;
}
// Check for chart-specific elements
switch (chartType) {
case 'pie': {
// Pie chart should have path elements (slices)
const slices = chartContainer.locator('path.recharts-pie-sector');
const sliceCount = await slices.count();
return sliceCount > 0;
}
case 'line': {
// Line chart should have line path elements
const lines = chartContainer.locator('path.recharts-line-curve');
const lineCount = await lines.count();
return lineCount > 0;
}
case 'bar': {
// Bar chart should have rect elements (bars)
const bars = chartContainer.locator('rect.recharts-bar-rectangle');
const barCount = await bars.count();
return barCount > 0;
}
}
}
/**
* Simulate drag-drop interaction for widget repositioning
* @param page - Playwright Page object
* @param widgetId - Widget identifier (data-grid i attribute)
* @param targetX - Target X coordinate
* @param targetY - Target Y coordinate
*/
export async function simulateDragDrop(
page: Page,
widgetId: string,
targetX: number,
targetY: number
): Promise<void> {
// Find widget by data-grid attribute
const widget = page.locator(`[data-grid*='"i":"${widgetId}"']`).or(
page.getByTestId(`widget-${widgetId}`)
);
await expect(widget).toBeVisible();
// Get widget's current position
const widgetBox = await widget.boundingBox();
if (!widgetBox) {
throw new Error(`Widget ${widgetId} not found or not visible`);
}
// Calculate drag coordinates
const startX = widgetBox.x + widgetBox.width / 2;
const startY = widgetBox.y + 20; // Drag from header area
// Perform drag-drop
await page.mouse.move(startX, startY);
await page.mouse.down();
// Move to target position
await page.mouse.move(targetX, targetY, { steps: 10 });
await page.mouse.up();
// Wait for layout to settle - use waitForLoadState instead of timeout
await page.waitForLoadState('domcontentloaded');
}
/**
* Get current layout configuration from dashboard
* @param page - Playwright Page object
* @returns Layout configuration object
*/
export async function getDashboardLayout(page: Page): Promise<Record<string, any>> {
const layout = await page.evaluate(() => {
const storage = localStorage.getItem('ccw-app-store');
if (!storage) return null;
const parsed = JSON.parse(storage);
return parsed.state?.dashboardLayout || null;
});
return layout;
}
/**
* Verify navigation group is expanded/collapsed
* @param page - Playwright Page object
* @param groupName - Navigation group name (e.g., 'Overview', 'Workflow')
* @param expectedExpanded - Whether group should be expanded
*/
export async function verifyNavGroupState(
page: Page,
groupName: string,
expectedExpanded: boolean
): Promise<void> {
const groupTrigger = page.getByRole('button', { name: new RegExp(groupName, 'i') });
await expect(groupTrigger).toBeVisible();
const ariaExpanded = await groupTrigger.getAttribute('aria-expanded');
const isExpanded = ariaExpanded === 'true';
if (isExpanded !== expectedExpanded) {
throw new Error(
`Navigation group "${groupName}" expected to be ${expectedExpanded ? 'expanded' : 'collapsed'} but was ${isExpanded ? 'expanded' : 'collapsed'}`
);
}
}
/**
* Toggle navigation group expand/collapse
* @param page - Playwright Page object
* @param groupName - Navigation group name
*/
export async function toggleNavGroup(page: Page, groupName: string): Promise<void> {
const groupTrigger = page.getByRole('button', { name: new RegExp(groupName, 'i') });
await expect(groupTrigger).toBeVisible();
await groupTrigger.click();
// Wait for accordion animation - use explicit wait
await page.waitForFunction(() => {
const group = document.querySelector('[aria-expanded]');
return group !== null;
}, { timeout: 3000 });
}
/**
* Verify ticker marquee is displaying messages
* @param page - Playwright Page object
* @returns Number of messages displayed
*/
export async function verifyTickerMessages(page: Page): Promise<number> {
const tickerContainer = page.getByTestId('ticker-marquee').or(
page.locator('.ticker-marquee')
);
const isVisible = await tickerContainer.isVisible().catch(() => false);
if (!isVisible) {
return 0;
}
const messages = tickerContainer.locator('.ticker-message').or(
tickerContainer.locator('[data-message]')
);
return await messages.count();
}
/**
* Simulate WebSocket message for ticker testing
* @param page - Playwright Page object
* @param message - Mock ticker message
*/
export async function simulateTickerMessage(
page: Page,
message: {
id: string;
text: string;
type: 'session' | 'task' | 'workflow' | 'status';
link?: string;
timestamp: number;
}
): Promise<void> {
await page.evaluate((msg) => {
const event = new MessageEvent('message', {
data: JSON.stringify(msg),
});
// Dispatch to WebSocket mock if available
const ws = (window as any).__mockWebSocket;
if (ws && ws.onmessage) {
ws.onmessage(event);
}
}, message);
// Wait for message to be processed - use explicit wait
await page.waitForLoadState('domcontentloaded');
}
/**
* Verify chart tooltip appears on hover
* @param page - Playwright Page object
* @param chartType - Type of chart
* @returns True if tooltip appeared
*/
export async function verifyChartTooltip(
page: Page,
chartType: 'pie' | 'line' | 'bar'
): Promise<boolean> {
let chartSelector: string;
switch (chartType) {
case 'pie':
chartSelector = '[data-testid="workflow-status-pie-chart"]';
break;
case 'line':
chartSelector = '[data-testid="activity-line-chart"]';
break;
case 'bar':
chartSelector = '[data-testid="task-type-bar-chart"]';
break;
}
const chartContainer = page.locator(chartSelector);
// Find interactive chart element
const chartElement = chartContainer.locator('svg').first();
await expect(chartElement).toBeVisible();
// Hover over chart
await chartElement.hover({ position: { x: 50, y: 50 } });
// Wait for tooltip animation - use explicit wait
await page.waitForFunction(() => {
const tooltip = document.querySelector('.recharts-tooltip-wrapper, [role="tooltip"]');
return tooltip !== null && window.getComputedStyle(tooltip).opacity !== '0';
}, { timeout: 3000 }).catch(() => true); // Don't fail if tooltip doesn't appear
// Check if tooltip is visible
const tooltip = page.locator('.recharts-tooltip-wrapper').or(
page.locator('[role="tooltip"]')
);
return await tooltip.isVisible().catch(() => false);
}
/**
* Verify all widgets are present on dashboard
* @param page - Playwright Page object
* @param expectedWidgetCount - Expected number of widgets
*/
export async function verifyAllWidgetsPresent(
page: Page,
expectedWidgetCount = 5
): Promise<void> {
// Look for widget containers
const widgets = page.locator('[data-grid]').or(page.locator('.widget-container'));
const widgetCount = await widgets.count();
if (widgetCount < expectedWidgetCount) {
throw new Error(
`Expected ${expectedWidgetCount} widgets but found ${widgetCount}`
);
}
// Verify each widget is visible
for (let i = 0; i < expectedWidgetCount; i++) {
await expect(widgets.nth(i)).toBeVisible();
}
}
/**
* Wait for specific widget to load
* @param page - Playwright Page object
* @param widgetId - Widget identifier
*/
export async function waitForWidgetLoad(page: Page, widgetId: string): Promise<void> {
const widget = page.getByTestId(`widget-${widgetId}`).or(
page.locator(`[data-widget="${widgetId}"]`)
);
await expect(widget).toBeVisible({ timeout: 10000 });
// Wait for skeleton to disappear
const skeleton = widget.locator('[data-testid*="skeleton"]');
const hasSkeleton = await skeleton.isVisible().catch(() => false);
if (hasSkeleton) {
await expect(skeleton).toBeHidden({ timeout: 5000 });
}
}
/**
* Verify responsive layout changes at breakpoint
* @param page - Playwright Page object
* @param breakpoint - Breakpoint name ('mobile', 'tablet', 'desktop')
*/
export async function verifyResponsiveLayout(
page: Page,
breakpoint: 'mobile' | 'tablet' | 'desktop'
): Promise<void> {
const viewportSizes = {
mobile: { width: 375, height: 667 },
tablet: { width: 768, height: 1024 },
desktop: { width: 1440, height: 900 },
};
await page.setViewportSize(viewportSizes[breakpoint]);
// Wait for layout reflow - use explicit wait
await page.waitForLoadState('domcontentloaded');
// Verify grid layout adjusts
const grid = page.getByTestId('dashboard-grid-container');
await expect(grid).toBeVisible();
// Check computed styles for grid columns
const gridColumns = await grid.evaluate((el) => {
return window.getComputedStyle(el).gridTemplateColumns;
});
// Verify column count matches breakpoint expectations
const columnCount = gridColumns.split(' ').length;
const expectedColumns = {
mobile: [1, 2], // 1-2 columns on mobile
tablet: [2, 6], // 2-6 columns on tablet
desktop: [12], // 12 columns on desktop
};
const isValidLayout = expectedColumns[breakpoint].some((count) =>
Math.abs(columnCount - count) <= 1
);
if (!isValidLayout) {
throw new Error(
`Layout at ${breakpoint} has ${columnCount} columns, expected ${expectedColumns[breakpoint]}`
);
}
}