/** * @license * Copyright (c) StellaOps. Licensed under the BUSL-1.1. */ import { test, expect } from '@playwright/test'; test.describe('Timeline Feature', () => { const mockTimelineResponse = { correlationId: 'test-corr-001', events: [ { eventId: 'evt-001', tHlc: '1704067200000:0:node1', tsWall: '2024-01-01T12:00:00Z', correlationId: 'test-corr-001', service: 'Scheduler', kind: 'EXECUTE', payload: '{"jobId": "job-001"}', payloadDigest: 'abc123', engineVersion: { name: 'Test', version: '1.0.0', digest: 'def456' }, schemaVersion: 1, }, { eventId: 'evt-002', tHlc: '1704067201000:0:node1', tsWall: '2024-01-01T12:00:01Z', correlationId: 'test-corr-001', service: 'AirGap', kind: 'IMPORT', payload: '{}', payloadDigest: 'ghi789', engineVersion: { name: 'Test', version: '1.0.0', digest: 'jkl012' }, schemaVersion: 1, }, ], totalCount: 2, hasMore: false, }; const mockCriticalPathResponse = { correlationId: 'test-corr-001', totalDurationMs: 5000, stages: [ { stage: 'ENQUEUE->EXECUTE', service: 'Scheduler', durationMs: 1000, percentage: 20, fromHlc: '1704067200000:0:node1', toHlc: '1704067201000:0:node1', }, ], }; test.beforeEach(async ({ page }) => { // Mock API responses await page.route('**/api/v1/timeline/test-corr-001', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockTimelineResponse), }); }); await page.route('**/api/v1/timeline/test-corr-001/critical-path', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockCriticalPathResponse), }); }); }); test('should display timeline page', async ({ page }) => { await page.goto('/timeline/test-corr-001'); // Check page title await expect(page.locator('h1')).toContainText('Timeline'); // Check correlation ID is displayed await expect(page.locator('.correlation-id')).toContainText('test-corr-001'); }); test('should display causal lanes with events', async ({ page }) => { await page.goto('/timeline/test-corr-001'); // Wait for events to load await expect(page.locator('.lane')).toHaveCount(2); // Check lane names await expect(page.locator('.lane-name').first()).toBeVisible(); }); test('should display critical path', async ({ page }) => { await page.goto('/timeline/test-corr-001'); // Check critical path is rendered await expect(page.locator('.critical-path-container')).toBeVisible(); await expect(page.locator('.stage-bar')).toHaveCount(1); }); test('should select event and show details', async ({ page }) => { await page.goto('/timeline/test-corr-001'); // Wait for events await expect(page.locator('.event-marker').first()).toBeVisible(); // Click on an event await page.locator('.event-marker').first().click(); // Check detail panel shows event info await expect(page.locator('.event-detail-panel')).toBeVisible(); await expect(page.locator('.event-id code')).toContainText('evt-001'); }); test('should filter by service', async ({ page }) => { await page.goto('/timeline/test-corr-001'); // Open service filter await page.locator('mat-select').first().click(); // Select Scheduler await page.locator('mat-option').filter({ hasText: 'Scheduler' }).click(); // Press escape to close await page.keyboard.press('Escape'); // Check URL params updated await expect(page).toHaveURL(/services=Scheduler/); }); test('should support keyboard navigation', async ({ page }) => { await page.goto('/timeline/test-corr-001'); // Wait for events await expect(page.locator('.event-marker').first()).toBeVisible(); // Tab to first event await page.keyboard.press('Tab'); await page.keyboard.press('Tab'); await page.keyboard.press('Tab'); // Focus should be on event marker const focused = page.locator('.event-marker:focus'); await expect(focused).toBeVisible(); // Press enter to select await page.keyboard.press('Enter'); // Detail panel should show await expect(page.locator('.event-detail-panel .event-id')).toBeVisible(); }); test('should export timeline', async ({ page }) => { // Mock export endpoints await page.route('**/api/v1/timeline/test-corr-001/export', async (route) => { await route.fulfill({ status: 202, contentType: 'application/json', body: JSON.stringify({ exportId: 'exp-001', correlationId: 'test-corr-001', format: 'ndjson', signBundle: false, status: 'INITIATED', estimatedEventCount: 2, }), }); }); await page.route('**/api/v1/timeline/export/exp-001', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ exportId: 'exp-001', status: 'COMPLETED', format: 'ndjson', eventCount: 2, fileSizeBytes: 1024, }), }); }); await page.route('**/api/v1/timeline/export/exp-001/download', async (route) => { await route.fulfill({ status: 200, contentType: 'application/x-ndjson', body: '{"event": "test"}\n', }); }); await page.goto('/timeline/test-corr-001'); // Click export button await page.locator('button:has-text("Export")').click(); // Select NDJSON option await page.locator('button:has-text("NDJSON")').first().click(); // Wait for download (or snackbar) await expect(page.locator('text=Export downloaded successfully')).toBeVisible({ timeout: 10000, }); }); test('should handle empty state', async ({ page }) => { await page.goto('/timeline'); // Should show empty state await expect(page.locator('.empty-state')).toBeVisible(); await expect(page.locator('.empty-state p')).toContainText( 'Enter a correlation ID' ); }); test('should handle error state', async ({ page }) => { // Mock error response await page.route('**/api/v1/timeline/error-test', async (route) => { await route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ error: 'Internal server error' }), }); }); await page.goto('/timeline/error-test'); // Should show error state await expect(page.locator('.error-state')).toBeVisible(); }); test('should be accessible', async ({ page }) => { await page.goto('/timeline/test-corr-001'); // Check ARIA labels await expect(page.locator('[role="main"]')).toHaveAttribute( 'aria-label', 'Event Timeline' ); await expect(page.locator('[role="region"]').first()).toBeVisible(); // Check focus indicators await page.keyboard.press('Tab'); const focusedElement = page.locator(':focus'); await expect(focusedElement).toBeVisible(); }); });