250 lines
7.2 KiB
TypeScript
250 lines
7.2 KiB
TypeScript
/**
|
|
* @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();
|
|
});
|
|
});
|