Files
Claude-Code-Workflow/ccw/frontend/tests/e2e/dashboard-redesign.spec.ts

406 lines
13 KiB
TypeScript

// ========================================
// E2E Tests: Dashboard Redesign
// ========================================
// E2E tests for navigation grouping, dashboard loading, drag-drop persistence, and ticker updates
import { test, expect } from '@playwright/test';
import { setupEnhancedMonitoring } from './helpers/i18n-helpers';
import {
waitForDashboardLoad,
verifyNavGroupState,
toggleNavGroup,
simulateDragDrop,
getDashboardLayout,
verifyTickerMessages,
simulateTickerMessage,
verifyAllWidgetsPresent,
verifyResponsiveLayout,
} from './helpers/dashboard-helpers';
test.describe('[Dashboard Redesign] - Navigation & Layout Tests', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/', { waitUntil: 'networkidle' as const });
await waitForDashboardLoad(page);
});
describe('Navigation Grouping', () => {
test('DR-1.1 - should display all 6 navigation groups', async ({ page }) => {
const monitoring = setupEnhancedMonitoring(page);
// Define expected navigation groups
const expectedGroups = [
'Overview',
'Workflow',
'Knowledge',
'Issues',
'Tools',
'Configuration',
];
// Verify each group is present
for (const groupName of expectedGroups) {
const groupTrigger = page.getByRole('button', { name: new RegExp(groupName, 'i') });
await expect(groupTrigger).toBeVisible();
}
monitoring.assertClean({ allowWarnings: true });
monitoring.stop();
});
test('DR-1.2 - should expand and collapse navigation groups', async ({ page }) => {
const monitoring = setupEnhancedMonitoring(page);
// Find first navigation group
const firstGroup = page.getByRole('button', { name: /overview|workflow/i }).first();
await expect(firstGroup).toBeVisible();
// Get initial state
const initialExpanded = (await firstGroup.getAttribute('aria-expanded')) === 'true';
// Toggle group
await firstGroup.click();
await page.waitForTimeout(300); // Wait for accordion animation
// Verify state changed
const afterToggle = (await firstGroup.getAttribute('aria-expanded')) === 'true';
expect(afterToggle).toBe(!initialExpanded);
// Toggle back
await firstGroup.click();
await page.waitForTimeout(300);
const finalState = (await firstGroup.getAttribute('aria-expanded')) === 'true';
expect(finalState).toBe(initialExpanded);
monitoring.assertClean({ allowWarnings: true });
monitoring.stop();
});
test('DR-1.3 - should persist navigation group state across reloads', async ({ page }) => {
const monitoring = setupEnhancedMonitoring(page);
// Expand a group
const workflowGroup = page.getByRole('button', { name: /workflow/i });
const isExpanded = (await workflowGroup.getAttribute('aria-expanded')) === 'true';
if (!isExpanded) {
await workflowGroup.click();
await page.waitForTimeout(300);
}
// Reload page
await page.reload({ waitUntil: 'networkidle' as const });
await waitForDashboardLoad(page);
// Verify group is still expanded
const afterReload = (await workflowGroup.getAttribute('aria-expanded')) === 'true';
expect(afterReload).toBe(true);
monitoring.assertClean({ allowWarnings: true });
monitoring.stop();
});
test('DR-1.4 - should highlight active route within expanded group', async ({ page }) => {
const monitoring = setupEnhancedMonitoring(page);
// Navigate to home (active by default)
const homeLink = page.getByRole('link', { name: /home|dashboard/i });
await expect(homeLink).toBeVisible();
// Check if link has active class or aria-current
const ariaCurrent = await homeLink.getAttribute('aria-current');
const hasActiveClass = await homeLink.evaluate((el) =>
el.classList.contains('active') || el.classList.contains('bg-accent')
);
expect(ariaCurrent === 'page' || hasActiveClass).toBe(true);
monitoring.assertClean({ allowWarnings: true });
monitoring.stop();
});
test('DR-1.5 - should support keyboard navigation for groups', async ({ page }) => {
const monitoring = setupEnhancedMonitoring(page);
// Focus first navigation group
const firstGroup = page.getByRole('button', { name: /overview|workflow/i }).first();
await firstGroup.focus();
// Press Enter to toggle
await page.keyboard.press('Enter');
await page.waitForTimeout(300);
// Verify state changed
const expanded = (await firstGroup.getAttribute('aria-expanded')) === 'true';
expect(expanded).toBeDefined();
// Press Tab to move to next element
await page.keyboard.press('Tab');
// Verify focus moved
const focusedElement = await page.evaluate(() => document.activeElement?.tagName);
expect(focusedElement).toBeTruthy();
monitoring.assertClean({ allowWarnings: true });
monitoring.stop();
});
});
describe('Dashboard Loading', () => {
test('DR-2.1 - should load all 5 widgets successfully', async ({ page }) => {
const monitoring = setupEnhancedMonitoring(page);
await verifyAllWidgetsPresent(page, 5);
monitoring.assertClean({ allowWarnings: true });
monitoring.stop();
});
test('DR-2.2 - should display loading states before data loads', async ({ page }) => {
const monitoring = setupEnhancedMonitoring(page);
// Navigate to fresh page
await page.goto('/', { waitUntil: 'domcontentloaded' as const });
// Check for loading skeletons
const skeletons = page.locator('[data-testid*="skeleton"]');
const skeletonCount = await skeletons.count();
// Should have some loading indicators
expect(skeletonCount).toBeGreaterThanOrEqual(0);
// Wait for page to fully load
await waitForDashboardLoad(page);
// Skeletons should be gone
const remainingSkeletons = await page
.locator('[data-testid*="skeleton"]:visible')
.count();
expect(remainingSkeletons).toBe(0);
monitoring.assertClean({ allowWarnings: true });
monitoring.stop();
});
test('DR-2.3 - should handle widget load errors gracefully', async ({ page }) => {
const monitoring = setupEnhancedMonitoring(page);
// Mock API failure
await page.route('**/api/data', (route) => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal Server Error' }),
});
});
await page.reload({ waitUntil: 'networkidle' as const });
// Should display error state or fallback content
const errorIndicator = page.getByText(/error|failed|unable/i).or(
page.getByTestId('error-state')
);
const hasError = await errorIndicator.isVisible().catch(() => false);
const pageHasContent = (await page.content()).length > 1000;
expect(hasError || pageHasContent).toBe(true);
await page.unroute('**/api/data');
monitoring.assertClean({ ignoreAPIPatterns: ['/api/data'], allowWarnings: true });
monitoring.stop();
});
});
describe('Drag-Drop Persistence', () => {
test('DR-3.1 - should allow dragging widgets to new positions', async ({ page }) => {
const monitoring = setupEnhancedMonitoring(page);
// Get initial layout
const initialLayout = await getDashboardLayout(page);
// Find a widget to drag
const widget = page.locator('[data-grid]').first();
const isVisible = await widget.isVisible().catch(() => false);
if (isVisible) {
const widgetBox = await widget.boundingBox();
if (widgetBox) {
// Simulate drag
const startX = widgetBox.x + widgetBox.width / 2;
const startY = widgetBox.y + 20;
const targetX = startX + 100;
const targetY = startY + 50;
await page.mouse.move(startX, startY);
await page.mouse.down();
await page.waitForTimeout(100);
await page.mouse.move(targetX, targetY, { steps: 5 });
await page.mouse.up();
await page.waitForTimeout(500);
// Get new layout
const newLayout = await getDashboardLayout(page);
// Layout should have changed
expect(JSON.stringify(newLayout)).not.toBe(JSON.stringify(initialLayout));
}
}
monitoring.assertClean({ allowWarnings: true });
monitoring.stop();
});
test('DR-3.2 - should persist layout changes after page reload', async ({ page }) => {
const monitoring = setupEnhancedMonitoring(page);
// Get current layout
const beforeLayout = await getDashboardLayout(page);
// Reload page
await page.reload({ waitUntil: 'networkidle' as const });
await waitForDashboardLoad(page);
// Get layout after reload
const afterLayout = await getDashboardLayout(page);
// Layout should be the same
expect(JSON.stringify(afterLayout)).toBe(JSON.stringify(beforeLayout));
monitoring.assertClean({ allowWarnings: true });
monitoring.stop();
});
test('DR-3.3 - should restore default layout on reset button click', async ({ page }) => {
const monitoring = setupEnhancedMonitoring(page);
// Look for reset button
const resetButton = page.getByRole('button', { name: /reset|default/i });
const hasResetButton = await resetButton.isVisible().catch(() => false);
if (hasResetButton) {
await resetButton.click();
await page.waitForTimeout(500);
// Verify layout was reset (widgets in default positions)
const layout = await getDashboardLayout(page);
expect(layout).toBeDefined();
}
monitoring.assertClean({ allowWarnings: true });
monitoring.stop();
});
});
describe('Ticker Real-time Updates', () => {
test('DR-4.1 - should display ticker marquee component', async ({ page }) => {
const monitoring = setupEnhancedMonitoring(page);
const tickerContainer = page.getByTestId('ticker-marquee').or(
page.locator('.ticker-marquee')
);
const isVisible = await tickerContainer.isVisible().catch(() => false);
expect(isVisible).toBe(true);
monitoring.assertClean({ allowWarnings: true });
monitoring.stop();
});
test('DR-4.2 - should display ticker messages with animation', async ({ page }) => {
const monitoring = setupEnhancedMonitoring(page);
const messageCount = await verifyTickerMessages(page);
// Should have messages (or be waiting for messages)
expect(messageCount).toBeGreaterThanOrEqual(0);
monitoring.assertClean({ allowWarnings: true });
monitoring.stop();
});
test('DR-4.3 - should pause animation on hover', async ({ page }) => {
const monitoring = setupEnhancedMonitoring(page);
const tickerContainer = page.getByTestId('ticker-marquee').or(
page.locator('.ticker-marquee')
);
const isVisible = await tickerContainer.isVisible().catch(() => false);
if (isVisible) {
// Hover over ticker
await tickerContainer.hover();
await page.waitForTimeout(200);
// Check if animation is paused (has paused class or style)
const isPaused = await tickerContainer.evaluate((el) => {
const style = window.getComputedStyle(el);
return (
style.animationPlayState === 'paused' ||
el.classList.contains('paused') ||
el.querySelector('.paused') !== null
);
});
expect(isPaused).toBeDefined();
}
monitoring.assertClean({ allowWarnings: true });
monitoring.stop();
});
test('DR-4.4 - should display connection status indicator', async ({ page }) => {
const monitoring = setupEnhancedMonitoring(page);
// Look for connection status indicator
const statusIndicator = page.getByTestId('ticker-status').or(
page.locator('.connection-status')
);
const hasIndicator = await statusIndicator.isVisible().catch(() => false);
// Either has indicator or ticker is working
const tickerVisible = await page
.getByTestId('ticker-marquee')
.isVisible()
.catch(() => false);
expect(hasIndicator || tickerVisible).toBe(true);
monitoring.assertClean({ allowWarnings: true });
monitoring.stop();
});
});
describe('Responsive Layout', () => {
test('DR-5.1 - should adapt layout for mobile viewport', async ({ page }) => {
const monitoring = setupEnhancedMonitoring(page);
await verifyResponsiveLayout(page, 'mobile');
monitoring.assertClean({ allowWarnings: true });
monitoring.stop();
});
test('DR-5.2 - should adapt layout for tablet viewport', async ({ page }) => {
const monitoring = setupEnhancedMonitoring(page);
await verifyResponsiveLayout(page, 'tablet');
monitoring.assertClean({ allowWarnings: true });
monitoring.stop();
});
test('DR-5.3 - should adapt layout for desktop viewport', async ({ page }) => {
const monitoring = setupEnhancedMonitoring(page);
await verifyResponsiveLayout(page, 'desktop');
monitoring.assertClean({ allowWarnings: true });
monitoring.stop();
});
});
});