stabilizaiton work - projects rework for maintenanceability and ui livening
This commit is contained in:
404
src/Web/StellaOps.Web/e2e/workflow-visualizer.visual.spec.ts
Normal file
404
src/Web/StellaOps.Web/e2e/workflow-visualizer.visual.spec.ts
Normal file
@@ -0,0 +1,404 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// workflow-visualizer.visual.spec.ts
|
||||
// Sprint: SPRINT_20260117_032_ReleaseOrchestrator_workflow_visualization
|
||||
// Task: TASK-032-12 - Visual Regression Tests for DAG Visualization
|
||||
// Description: Playwright visual regression tests for workflow visualization
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Workflow DAG Visualization', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Navigate to test workflow page
|
||||
await page.goto('/workflows/test-run-001');
|
||||
await page.waitForSelector('.workflow-visualizer');
|
||||
});
|
||||
|
||||
test.describe('Node Rendering', () => {
|
||||
test('renders nodes at various complexities', async ({ page }) => {
|
||||
// Test with 10 nodes
|
||||
await page.goto('/workflows/test-10-nodes');
|
||||
await page.waitForSelector('.node', { timeout: 5000 });
|
||||
await expect(page.locator('.node')).toHaveCount(10);
|
||||
await expect(page.locator('.dag-canvas')).toHaveScreenshot('dag-10-nodes.png');
|
||||
|
||||
// Test with 50 nodes
|
||||
await page.goto('/workflows/test-50-nodes');
|
||||
await page.waitForSelector('.node', { timeout: 10000 });
|
||||
await expect(page.locator('.node')).toHaveCount(50);
|
||||
await expect(page.locator('.dag-canvas')).toHaveScreenshot('dag-50-nodes.png');
|
||||
});
|
||||
|
||||
test('renders large workflow (100+ nodes) with acceptable performance', async ({ page }) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
await page.goto('/workflows/test-100-nodes');
|
||||
await page.waitForSelector('.node', { timeout: 15000 });
|
||||
|
||||
const loadTime = Date.now() - startTime;
|
||||
expect(loadTime).toBeLessThan(10000); // Should load within 10 seconds
|
||||
|
||||
await expect(page.locator('.node')).toHaveCount(100);
|
||||
await expect(page.locator('.dag-canvas')).toHaveScreenshot('dag-100-nodes.png', {
|
||||
maxDiffPixelRatio: 0.05 // Allow 5% variance for large graphs
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Node Status States', () => {
|
||||
test('displays pending node correctly', async ({ page }) => {
|
||||
await page.goto('/workflows/test-pending');
|
||||
const pendingNode = page.locator('.node-pending').first();
|
||||
await expect(pendingNode).toBeVisible();
|
||||
await expect(pendingNode).toHaveScreenshot('node-pending.png');
|
||||
});
|
||||
|
||||
test('displays running node with animation', async ({ page }) => {
|
||||
await page.goto('/workflows/test-running');
|
||||
const runningNode = page.locator('.node-running').first();
|
||||
await expect(runningNode).toBeVisible();
|
||||
|
||||
// Check for pulse animation
|
||||
const statusIndicator = runningNode.locator('.pulse');
|
||||
await expect(statusIndicator).toBeVisible();
|
||||
|
||||
// Capture animation frame
|
||||
await expect(runningNode).toHaveScreenshot('node-running.png');
|
||||
});
|
||||
|
||||
test('displays succeeded node correctly', async ({ page }) => {
|
||||
await page.goto('/workflows/test-succeeded');
|
||||
const succeededNode = page.locator('.node-succeeded').first();
|
||||
await expect(succeededNode).toBeVisible();
|
||||
await expect(succeededNode).toHaveScreenshot('node-succeeded.png');
|
||||
});
|
||||
|
||||
test('displays failed node correctly', async ({ page }) => {
|
||||
await page.goto('/workflows/test-failed');
|
||||
const failedNode = page.locator('.node-failed').first();
|
||||
await expect(failedNode).toBeVisible();
|
||||
await expect(failedNode).toHaveScreenshot('node-failed.png');
|
||||
});
|
||||
|
||||
test('displays skipped node correctly', async ({ page }) => {
|
||||
await page.goto('/workflows/test-skipped');
|
||||
const skippedNode = page.locator('.node-skipped').first();
|
||||
await expect(skippedNode).toBeVisible();
|
||||
await expect(skippedNode).toHaveScreenshot('node-skipped.png');
|
||||
});
|
||||
|
||||
test('displays cancelled node correctly', async ({ page }) => {
|
||||
await page.goto('/workflows/test-cancelled');
|
||||
const cancelledNode = page.locator('.node-cancelled').first();
|
||||
await expect(cancelledNode).toBeVisible();
|
||||
await expect(cancelledNode).toHaveScreenshot('node-cancelled.png');
|
||||
});
|
||||
|
||||
test('node state transition animation', async ({ page }) => {
|
||||
await page.goto('/workflows/test-transition');
|
||||
|
||||
// Initial state - pending
|
||||
const node = page.locator('[data-step-id="step-1"]');
|
||||
await expect(node).toHaveClass(/node-pending/);
|
||||
|
||||
// Trigger transition
|
||||
await page.click('[data-action="start-workflow"]');
|
||||
|
||||
// Wait for running state
|
||||
await expect(node).toHaveClass(/node-running/, { timeout: 5000 });
|
||||
await expect(node).toHaveScreenshot('node-transition-running.png');
|
||||
|
||||
// Wait for completed state
|
||||
await expect(node).toHaveClass(/node-succeeded/, { timeout: 10000 });
|
||||
await expect(node).toHaveScreenshot('node-transition-succeeded.png');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Edge Rendering', () => {
|
||||
test('renders static edges correctly', async ({ page }) => {
|
||||
await page.goto('/workflows/test-static');
|
||||
const edges = page.locator('.edge-path');
|
||||
await expect(edges.first()).toBeVisible();
|
||||
await expect(page.locator('.edges-layer')).toHaveScreenshot('edges-static.png');
|
||||
});
|
||||
|
||||
test('renders animated edges for in-progress steps', async ({ page }) => {
|
||||
await page.goto('/workflows/test-running');
|
||||
const animatedEdge = page.locator('.edge.animated');
|
||||
await expect(animatedEdge).toBeVisible();
|
||||
|
||||
// Verify animation is present (dash animation)
|
||||
const edgePath = animatedEdge.locator('.edge-path');
|
||||
const strokeDasharray = await edgePath.evaluate(el =>
|
||||
window.getComputedStyle(el).getPropertyValue('stroke-dasharray')
|
||||
);
|
||||
expect(strokeDasharray).not.toBe('none');
|
||||
});
|
||||
|
||||
test('highlights critical path edges', async ({ page }) => {
|
||||
await page.goto('/workflows/test-completed');
|
||||
|
||||
// Enable critical path
|
||||
await page.click('button:has-text("Critical Path")');
|
||||
|
||||
const criticalEdge = page.locator('.edge.critical');
|
||||
await expect(criticalEdge.first()).toBeVisible();
|
||||
await expect(page.locator('.edges-layer')).toHaveScreenshot('edges-critical-path.png');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Layout Algorithms', () => {
|
||||
test('dagre layout renders correctly', async ({ page }) => {
|
||||
await page.goto('/workflows/test-layout');
|
||||
await page.selectOption('.layout-selector select', 'dagre');
|
||||
await page.waitForTimeout(500); // Wait for layout animation
|
||||
await expect(page.locator('.dag-canvas')).toHaveScreenshot('layout-dagre.png');
|
||||
});
|
||||
|
||||
test('elk layout renders correctly', async ({ page }) => {
|
||||
await page.goto('/workflows/test-layout');
|
||||
await page.selectOption('.layout-selector select', 'elk');
|
||||
await page.waitForTimeout(500);
|
||||
await expect(page.locator('.dag-canvas')).toHaveScreenshot('layout-elk.png');
|
||||
});
|
||||
|
||||
test('force-directed layout renders correctly', async ({ page }) => {
|
||||
await page.goto('/workflows/test-layout');
|
||||
await page.selectOption('.layout-selector select', 'force');
|
||||
await page.waitForTimeout(1000); // Force layout needs more time
|
||||
await expect(page.locator('.dag-canvas')).toHaveScreenshot('layout-force.png', {
|
||||
maxDiffPixelRatio: 0.1 // Force layout may have slight variations
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Zoom and Pan', () => {
|
||||
test('zoom controls work correctly', async ({ page }) => {
|
||||
await page.goto('/workflows/test-10-nodes');
|
||||
|
||||
// Zoom in
|
||||
await page.click('button[title="Zoom In"]');
|
||||
await page.click('button[title="Zoom In"]');
|
||||
await expect(page.locator('.zoom-label')).toContainText('150%');
|
||||
await expect(page.locator('.dag-canvas')).toHaveScreenshot('zoom-150.png');
|
||||
|
||||
// Zoom out
|
||||
await page.click('button[title="Zoom Out"]');
|
||||
await page.click('button[title="Zoom Out"]');
|
||||
await page.click('button[title="Zoom Out"]');
|
||||
await expect(page.locator('.zoom-label')).toContainText('75%');
|
||||
await expect(page.locator('.dag-canvas')).toHaveScreenshot('zoom-75.png');
|
||||
});
|
||||
|
||||
test('fit to view resets viewport', async ({ page }) => {
|
||||
await page.goto('/workflows/test-10-nodes');
|
||||
|
||||
// Pan and zoom
|
||||
await page.click('button[title="Zoom In"]');
|
||||
await page.click('button[title="Zoom In"]');
|
||||
|
||||
// Fit to view
|
||||
await page.click('button[title="Fit to View"]');
|
||||
await expect(page.locator('.zoom-label')).toContainText('100%');
|
||||
await expect(page.locator('.dag-canvas')).toHaveScreenshot('zoom-fit.png');
|
||||
});
|
||||
|
||||
test('mouse wheel zooms', async ({ page }) => {
|
||||
await page.goto('/workflows/test-10-nodes');
|
||||
const canvas = page.locator('.canvas-container');
|
||||
|
||||
// Scroll to zoom
|
||||
await canvas.hover();
|
||||
await page.mouse.wheel(0, -100); // Zoom in
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
const zoomLabel = await page.locator('.zoom-label').textContent();
|
||||
expect(parseInt(zoomLabel || '100')).toBeGreaterThan(100);
|
||||
});
|
||||
|
||||
test('drag to pan', async ({ page }) => {
|
||||
await page.goto('/workflows/test-50-nodes');
|
||||
const canvas = page.locator('.canvas-container');
|
||||
|
||||
// Get initial viewbox
|
||||
const initialViewBox = await page.locator('.dag-canvas').getAttribute('viewBox');
|
||||
|
||||
// Drag to pan
|
||||
const box = await canvas.boundingBox();
|
||||
if (box) {
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + box.width / 2 + 100, box.y + box.height / 2 + 50);
|
||||
await page.mouse.up();
|
||||
}
|
||||
|
||||
// Viewbox should have changed
|
||||
const newViewBox = await page.locator('.dag-canvas').getAttribute('viewBox');
|
||||
expect(newViewBox).not.toBe(initialViewBox);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Node Selection', () => {
|
||||
test('clicking node selects it', async ({ page }) => {
|
||||
await page.goto('/workflows/test-10-nodes');
|
||||
const node = page.locator('.node').first();
|
||||
|
||||
await node.click();
|
||||
await expect(node).toHaveClass(/selected/);
|
||||
await expect(node).toHaveScreenshot('node-selected.png');
|
||||
});
|
||||
|
||||
test('double-clicking node opens details', async ({ page }) => {
|
||||
await page.goto('/workflows/test-10-nodes');
|
||||
const node = page.locator('.node').first();
|
||||
|
||||
await node.dblclick();
|
||||
await expect(page.locator('.step-detail-panel')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Minimap', () => {
|
||||
test('minimap renders for large workflows', async ({ page }) => {
|
||||
await page.goto('/workflows/test-50-nodes');
|
||||
await expect(page.locator('.minimap')).toBeVisible();
|
||||
await expect(page.locator('.minimap')).toHaveScreenshot('minimap.png');
|
||||
});
|
||||
|
||||
test('minimap shows viewport indicator', async ({ page }) => {
|
||||
await page.goto('/workflows/test-50-nodes');
|
||||
await expect(page.locator('.viewport-indicator')).toBeVisible();
|
||||
});
|
||||
|
||||
test('minimap hidden for small workflows', async ({ page }) => {
|
||||
await page.goto('/workflows/test-5-nodes');
|
||||
await expect(page.locator('.minimap')).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Responsive Layout', () => {
|
||||
test('mobile viewport adjusts layout', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await page.goto('/workflows/test-10-nodes');
|
||||
|
||||
// Toolbar should wrap
|
||||
await expect(page.locator('.visualizer-toolbar')).toHaveScreenshot('toolbar-mobile.png');
|
||||
|
||||
// Minimap should be hidden
|
||||
await expect(page.locator('.minimap')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('tablet viewport renders correctly', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 768, height: 1024 });
|
||||
await page.goto('/workflows/test-10-nodes');
|
||||
await expect(page.locator('.workflow-visualizer')).toHaveScreenshot('visualizer-tablet.png');
|
||||
});
|
||||
|
||||
test('desktop viewport renders correctly', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1920, height: 1080 });
|
||||
await page.goto('/workflows/test-10-nodes');
|
||||
await expect(page.locator('.workflow-visualizer')).toHaveScreenshot('visualizer-desktop.png');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Dark Mode', () => {
|
||||
test('dark mode renders correctly', async ({ page }) => {
|
||||
await page.goto('/workflows/test-10-nodes?theme=dark');
|
||||
await expect(page.locator('.workflow-visualizer')).toHaveClass(/dark-mode/);
|
||||
await expect(page.locator('.dag-canvas')).toHaveScreenshot('dag-dark-mode.png');
|
||||
});
|
||||
|
||||
test('node states in dark mode', async ({ page }) => {
|
||||
await page.goto('/workflows/test-all-states?theme=dark');
|
||||
await expect(page.locator('.dag-canvas')).toHaveScreenshot('nodes-dark-mode.png');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Legend', () => {
|
||||
test('legend displays all states', async ({ page }) => {
|
||||
await page.goto('/workflows/test-10-nodes');
|
||||
const legend = page.locator('.legend');
|
||||
await expect(legend).toBeVisible();
|
||||
await expect(legend.locator('.legend-item')).toHaveCount(5);
|
||||
await expect(legend).toHaveScreenshot('legend.png');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Loading and Error States', () => {
|
||||
test('loading overlay displays correctly', async ({ page }) => {
|
||||
// Intercept API to delay response
|
||||
await page.route('**/api/v1/workflows/*/graph', async route => {
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await route.continue();
|
||||
});
|
||||
|
||||
await page.goto('/workflows/test-10-nodes');
|
||||
await expect(page.locator('.loading-overlay')).toBeVisible();
|
||||
await expect(page.locator('.loading-overlay')).toHaveScreenshot('loading-state.png');
|
||||
});
|
||||
|
||||
test('error overlay displays correctly', async ({ page }) => {
|
||||
// Mock API error
|
||||
await page.route('**/api/v1/workflows/*/graph', async route => {
|
||||
await route.fulfill({
|
||||
status: 500,
|
||||
body: JSON.stringify({ error: 'Internal Server Error' })
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/workflows/test-10-nodes');
|
||||
await expect(page.locator('.error-overlay')).toBeVisible();
|
||||
await expect(page.locator('.error-overlay')).toHaveScreenshot('error-state.png');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Time-Travel Controls', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/workflows/test-completed/debug');
|
||||
await page.waitForSelector('.time-travel-controls');
|
||||
});
|
||||
|
||||
test('controls render correctly', async ({ page }) => {
|
||||
await expect(page.locator('.time-travel-controls')).toHaveScreenshot('time-travel-controls.png');
|
||||
});
|
||||
|
||||
test('timeline with markers', async ({ page }) => {
|
||||
await expect(page.locator('.timeline-container')).toHaveScreenshot('timeline-markers.png');
|
||||
});
|
||||
|
||||
test('playhead position updates', async ({ page }) => {
|
||||
// Step forward
|
||||
await page.click('button[title*="Step Forward"]');
|
||||
await page.click('button[title*="Step Forward"]');
|
||||
|
||||
await expect(page.locator('.timeline')).toHaveScreenshot('timeline-stepped.png');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Step Detail Panel', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/workflows/test-completed');
|
||||
await page.locator('.node').first().click();
|
||||
await page.waitForSelector('.step-detail-panel');
|
||||
});
|
||||
|
||||
test('panel renders correctly', async ({ page }) => {
|
||||
await expect(page.locator('.step-detail-panel')).toHaveScreenshot('step-panel.png');
|
||||
});
|
||||
|
||||
test('logs tab renders correctly', async ({ page }) => {
|
||||
await page.click('.tab:has-text("Logs")');
|
||||
await expect(page.locator('.logs-tab')).toHaveScreenshot('logs-tab.png');
|
||||
});
|
||||
|
||||
test('timing tab renders correctly', async ({ page }) => {
|
||||
await page.click('.tab:has-text("Timing")');
|
||||
await expect(page.locator('.timing-tab')).toHaveScreenshot('timing-tab.png');
|
||||
});
|
||||
|
||||
test('error state panel', async ({ page }) => {
|
||||
await page.goto('/workflows/test-failed');
|
||||
await page.locator('.node-failed').first().click();
|
||||
await expect(page.locator('.step-detail-panel')).toHaveScreenshot('step-panel-error.png');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user