save progress

This commit is contained in:
master
2026-01-09 18:27:36 +02:00
parent e608752924
commit a21d3dbc1f
361 changed files with 63068 additions and 1192 deletions

View File

@@ -0,0 +1,584 @@
// -----------------------------------------------------------------------------
// diff-runtime-tabs.e2e.spec.ts
// Sprint: SPRINT_20260107_006_002_FE_diff_runtime_tabs
// Task: DR-013 — E2E tests for Diff and Runtime tabs
// -----------------------------------------------------------------------------
import { test, expect, Page } from '@playwright/test';
test.describe('Diff Tab', () => {
let page: Page;
test.beforeEach(async ({ browser }) => {
page = await browser.newPage();
// Navigate to a finding detail page with diff evidence
await page.goto('/triage/findings/test-finding-001');
// Wait for evidence panel to load
await page.waitForSelector('[data-testid="evidence-panel"]');
});
test.afterEach(async () => {
await page.close();
});
test('should display Diff tab in evidence panel', async () => {
const diffTab = page.locator('[data-testid="tab-diff"]');
await expect(diffTab).toBeVisible();
await expect(diffTab).toHaveText(/Diff/i);
});
test('should switch to Diff tab on click', async () => {
const diffTab = page.locator('[data-testid="tab-diff"]');
await diffTab.click();
const diffPanel = page.locator('[data-testid="panel-diff"]');
await expect(diffPanel).toBeVisible();
});
test('should switch to Diff tab with keyboard shortcut', async () => {
// Press '4' to switch to Diff tab (assuming it's the 4th tab)
await page.keyboard.press('4');
const diffPanel = page.locator('[data-testid="panel-diff"]');
await expect(diffPanel).toBeVisible();
});
test('should display backport verdict badge', async () => {
await page.locator('[data-testid="tab-diff"]').click();
await page.waitForSelector('[data-testid="backport-verdict-badge"]');
const badge = page.locator('[data-testid="backport-verdict-badge"]');
await expect(badge).toBeVisible();
// Should show one of the verdict states
await expect(badge).toHaveAttribute('data-status', /verified|unverified|unknown|partial/);
});
test('should display confidence percentage', async () => {
await page.locator('[data-testid="tab-diff"]').click();
await page.waitForSelector('[data-testid="backport-verdict-badge"]');
const confidence = page.locator('[data-testid="verdict-confidence"]');
await expect(confidence).toBeVisible();
// Should show percentage
await expect(confidence).toHaveText(/%/);
});
test('should show tooltip with tier info on badge hover', async () => {
await page.locator('[data-testid="tab-diff"]').click();
await page.waitForSelector('[data-testid="backport-verdict-badge"]');
const badge = page.locator('[data-testid="backport-verdict-badge"]');
await badge.hover();
const tooltip = page.locator('[data-testid="verdict-tooltip"]');
await expect(tooltip).toBeVisible();
await expect(tooltip).toHaveText(/Tier/i);
});
test('should display version comparison section', async () => {
await page.locator('[data-testid="tab-diff"]').click();
await page.waitForSelector('[data-testid="version-compare"]');
const upstream = page.locator('[data-testid="version-upstream"]');
const distro = page.locator('[data-testid="version-distro"]');
await expect(upstream).toBeVisible();
await expect(distro).toBeVisible();
});
test('should display upstream commit link', async () => {
await page.locator('[data-testid="tab-diff"]').click();
await page.waitForSelector('[data-testid="commit-link"]');
const commitLink = page.locator('[data-testid="commit-link"]');
await expect(commitLink).toBeVisible();
await expect(commitLink).toHaveAttribute('href', /github\.com|gitlab\.com/);
await expect(commitLink).toHaveAttribute('target', '_blank');
});
test('should display patch diff viewer', async () => {
await page.locator('[data-testid="tab-diff"]').click();
await page.waitForSelector('[data-testid="patch-diff-viewer"]');
const diffViewer = page.locator('[data-testid="patch-diff-viewer"]');
await expect(diffViewer).toBeVisible();
});
test('should expand and collapse diff hunks', async () => {
await page.locator('[data-testid="tab-diff"]').click();
await page.waitForSelector('[data-testid="patch-diff-viewer"]');
const hunkHeader = page.locator('[data-testid="hunk-header"]').first();
const hunkContent = page.locator('[data-testid="hunk-content"]').first();
// Initially expanded
await expect(hunkContent).toBeVisible();
// Click to collapse
await hunkHeader.click();
await expect(hunkContent).not.toBeVisible();
// Click to expand again
await hunkHeader.click();
await expect(hunkContent).toBeVisible();
});
test('should display line numbers in diff', async () => {
await page.locator('[data-testid="tab-diff"]').click();
await page.waitForSelector('[data-testid="patch-diff-viewer"]');
const lineNumbers = page.locator('[data-testid="line-number"]');
await expect(lineNumbers.first()).toBeVisible();
});
test('should highlight additions in green', async () => {
await page.locator('[data-testid="tab-diff"]').click();
await page.waitForSelector('[data-testid="patch-diff-viewer"]');
const additionLine = page.locator('[data-testid="diff-line-addition"]').first();
await expect(additionLine).toBeVisible();
// Check for green background styling
await expect(additionLine).toHaveClass(/addition/);
});
test('should highlight deletions in red', async () => {
await page.locator('[data-testid="tab-diff"]').click();
await page.waitForSelector('[data-testid="patch-diff-viewer"]');
const deletionLine = page.locator('[data-testid="diff-line-deletion"]').first();
await expect(deletionLine).toBeVisible();
await expect(deletionLine).toHaveClass(/deletion/);
});
test('should copy diff to clipboard', async () => {
await page.locator('[data-testid="tab-diff"]').click();
await page.waitForSelector('[data-testid="patch-diff-viewer"]');
const copyBtn = page.locator('[data-testid="copy-diff-btn"]');
await copyBtn.click();
// Check clipboard content (requires clipboard permissions)
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardText).toContain('@@');
});
test('should copy hunk signature', async () => {
await page.locator('[data-testid="tab-diff"]').click();
await page.waitForSelector('[data-testid="hunk-signature"]');
const copyHashBtn = page.locator('[data-testid="copy-hash-btn"]');
await copyHashBtn.click();
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardText).toMatch(/sha256:/);
});
test('should show empty state when no diff evidence', async () => {
// Navigate to finding without diff evidence
await page.goto('/triage/findings/no-diff-finding');
await page.waitForSelector('[data-testid="evidence-panel"]');
await page.locator('[data-testid="tab-diff"]').click();
const emptyState = page.locator('[data-testid="diff-empty-state"]');
await expect(emptyState).toBeVisible();
await expect(emptyState).toHaveText(/No backport evidence/i);
});
test('should handle loading state', async () => {
// Slow down network to observe loading state
await page.route('**/api/v1/findings/**/backport', async (route) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
await route.continue();
});
await page.locator('[data-testid="tab-diff"]').click();
const loadingState = page.locator('[data-testid="diff-loading"]');
await expect(loadingState).toBeVisible();
});
test('should handle error state with retry', async () => {
// Mock API error
await page.route('**/api/v1/findings/**/backport', (route) => {
route.fulfill({ status: 500, body: 'Server Error' });
});
await page.locator('[data-testid="tab-diff"]').click();
const errorState = page.locator('[data-testid="diff-error"]');
await expect(errorState).toBeVisible();
const retryBtn = page.locator('[data-testid="diff-retry-btn"]');
await expect(retryBtn).toBeVisible();
});
});
test.describe('Runtime Tab', () => {
let page: Page;
test.beforeEach(async ({ browser }) => {
page = await browser.newPage();
await page.goto('/triage/findings/test-finding-001');
await page.waitForSelector('[data-testid="evidence-panel"]');
});
test.afterEach(async () => {
await page.close();
});
test('should display Runtime tab in evidence panel', async () => {
const runtimeTab = page.locator('[data-testid="tab-runtime"]');
await expect(runtimeTab).toBeVisible();
await expect(runtimeTab).toHaveText(/Runtime/i);
});
test('should switch to Runtime tab on click', async () => {
const runtimeTab = page.locator('[data-testid="tab-runtime"]');
await runtimeTab.click();
const runtimePanel = page.locator('[data-testid="panel-runtime"]');
await expect(runtimePanel).toBeVisible();
});
test('should switch to Runtime tab with keyboard shortcut', async () => {
await page.keyboard.press('5');
const runtimePanel = page.locator('[data-testid="panel-runtime"]');
await expect(runtimePanel).toBeVisible();
});
test('should display live indicator when collection active', async () => {
// Mock active collection response
await page.route('**/api/v1/findings/**/runtime/traces', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
findingId: 'test-finding-001',
collectionActive: true,
collectionStarted: new Date().toISOString(),
summary: { totalHits: 100, uniquePaths: 3 },
traces: [],
}),
});
});
await page.locator('[data-testid="tab-runtime"]').click();
await page.waitForSelector('[data-testid="live-indicator"]');
const liveIndicator = page.locator('[data-testid="live-indicator"]');
await expect(liveIndicator).toBeVisible();
await expect(liveIndicator).toHaveClass(/active/);
await expect(liveIndicator).toHaveText(/Live/i);
});
test('should show offline indicator when collection stopped', async () => {
await page.route('**/api/v1/findings/**/runtime/traces', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
findingId: 'test-finding-001',
collectionActive: false,
summary: { totalHits: 100, uniquePaths: 3 },
traces: [],
}),
});
});
await page.locator('[data-testid="tab-runtime"]').click();
await page.waitForSelector('[data-testid="live-indicator"]');
const liveIndicator = page.locator('[data-testid="live-indicator"]');
await expect(liveIndicator).toHaveClass(/inactive/);
await expect(liveIndicator).toHaveText(/Offline/i);
});
test('should show tooltip on live indicator hover', async () => {
await page.locator('[data-testid="tab-runtime"]').click();
await page.waitForSelector('[data-testid="live-indicator"]');
const indicator = page.locator('[data-testid="live-indicator"]');
await indicator.hover();
const tooltip = page.locator('[data-testid="live-tooltip"]');
await expect(tooltip).toBeVisible();
});
test('should display summary stats', async () => {
await page.locator('[data-testid="tab-runtime"]').click();
await page.waitForSelector('[data-testid="summary-stats"]');
const totalHits = page.locator('[data-testid="stat-total-hits"]');
const uniquePaths = page.locator('[data-testid="stat-unique-paths"]');
const containers = page.locator('[data-testid="stat-containers"]');
await expect(totalHits).toBeVisible();
await expect(uniquePaths).toBeVisible();
await expect(containers).toBeVisible();
});
test('should display RTS score', async () => {
await page.locator('[data-testid="tab-runtime"]').click();
await page.waitForSelector('[data-testid="rts-score-display"]');
const rtsScore = page.locator('[data-testid="rts-score-display"]');
await expect(rtsScore).toBeVisible();
const scoreValue = page.locator('[data-testid="rts-value"]');
await expect(scoreValue).toHaveText(/%/);
});
test('should display posture badge', async () => {
await page.locator('[data-testid="tab-runtime"]').click();
await page.waitForSelector('[data-testid="posture-badge"]');
const postureBadge = page.locator('[data-testid="posture-badge"]');
await expect(postureBadge).toBeVisible();
await expect(postureBadge).toHaveText(/Excellent|Good|Limited|None/i);
});
test('should expand RTS score breakdown', async () => {
await page.locator('[data-testid="tab-runtime"]').click();
await page.waitForSelector('[data-testid="rts-score-display"]');
const breakdownToggle = page.locator('[data-testid="breakdown-toggle"]');
await breakdownToggle.click();
const breakdownContent = page.locator('[data-testid="breakdown-content"]');
await expect(breakdownContent).toBeVisible();
});
test('should display function traces list', async () => {
await page.locator('[data-testid="tab-runtime"]').click();
await page.waitForSelector('[data-testid="traces-list"]');
const tracesList = page.locator('[data-testid="traces-list"]');
await expect(tracesList).toBeVisible();
const traceItems = page.locator('[data-testid="function-trace"]');
await expect(traceItems.first()).toBeVisible();
});
test('should expand function trace to show call stack', async () => {
await page.locator('[data-testid="tab-runtime"]').click();
await page.waitForSelector('[data-testid="function-trace"]');
const traceHeader = page.locator('[data-testid="trace-header"]').first();
await traceHeader.click();
const callPath = page.locator('[data-testid="call-path"]').first();
await expect(callPath).toBeVisible();
});
test('should display stack frames with file:line links', async () => {
await page.locator('[data-testid="tab-runtime"]').click();
await page.waitForSelector('[data-testid="function-trace"]');
// Expand first trace
await page.locator('[data-testid="trace-header"]').first().click();
const frameLocation = page.locator('[data-testid="frame-location"]').first();
await expect(frameLocation).toBeVisible();
await expect(frameLocation).toHaveText(/:\d+/); // file:line format
});
test('should highlight vulnerable function in stack', async () => {
await page.locator('[data-testid="tab-runtime"]').click();
await page.waitForSelector('[data-testid="function-trace"]');
await page.locator('[data-testid="trace-header"]').first().click();
const vulnFrame = page.locator('[data-testid="stack-frame-vulnerable"]');
await expect(vulnFrame).toBeVisible();
await expect(vulnFrame).toHaveClass(/vulnerable/);
});
test('should copy stack trace', async () => {
await page.locator('[data-testid="tab-runtime"]').click();
await page.waitForSelector('[data-testid="function-trace"]');
await page.locator('[data-testid="trace-header"]').first().click();
const copyBtn = page.locator('[data-testid="copy-stack-btn"]').first();
await copyBtn.click();
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardText).toContain('at ');
});
test('should sort traces by hits', async () => {
await page.locator('[data-testid="tab-runtime"]').click();
await page.waitForSelector('[data-testid="traces-list"]');
const sortByHits = page.locator('[data-testid="sort-by-hits"]');
await sortByHits.click();
// First trace should have highest hit count
const firstHitCount = await page.locator('[data-testid="hit-count"]').first().textContent();
const secondHitCount = await page.locator('[data-testid="hit-count"]').nth(1).textContent();
// Parse hit counts (handle K/M suffixes)
const parseCount = (str: string | null) => {
if (!str) return 0;
const num = parseFloat(str.replace(/[KM]/g, ''));
if (str.includes('K')) return num * 1000;
if (str.includes('M')) return num * 1000000;
return num;
};
expect(parseCount(firstHitCount)).toBeGreaterThanOrEqual(parseCount(secondHitCount));
});
test('should sort traces by recency', async () => {
await page.locator('[data-testid="tab-runtime"]').click();
await page.waitForSelector('[data-testid="traces-list"]');
const sortByRecent = page.locator('[data-testid="sort-by-recent"]');
await sortByRecent.click();
// Verify sorting changed
await expect(sortByRecent).toHaveClass(/active/);
});
test('should load more traces on button click', async () => {
await page.locator('[data-testid="tab-runtime"]').click();
await page.waitForSelector('[data-testid="traces-list"]');
const initialCount = await page.locator('[data-testid="function-trace"]').count();
const loadMoreBtn = page.locator('[data-testid="load-more-btn"]');
if (await loadMoreBtn.isVisible()) {
await loadMoreBtn.click();
const newCount = await page.locator('[data-testid="function-trace"]').count();
expect(newCount).toBeGreaterThan(initialCount);
}
});
test('should show direct path indicator', async () => {
await page.locator('[data-testid="tab-runtime"]').click();
await page.waitForSelector('[data-testid="indicator-direct-path"]');
const directPathIndicator = page.locator('[data-testid="indicator-direct-path"]');
await expect(directPathIndicator).toBeVisible();
});
test('should show production traffic indicator', async () => {
await page.locator('[data-testid="tab-runtime"]').click();
await page.waitForSelector('[data-testid="indicator-production"]');
const prodIndicator = page.locator('[data-testid="indicator-production"]');
await expect(prodIndicator).toBeVisible();
});
test('should show empty state when no runtime data', async () => {
await page.goto('/triage/findings/no-runtime-finding');
await page.waitForSelector('[data-testid="evidence-panel"]');
await page.locator('[data-testid="tab-runtime"]').click();
const emptyState = page.locator('[data-testid="runtime-empty-state"]');
await expect(emptyState).toBeVisible();
await expect(emptyState).toHaveText(/No runtime observations/i);
});
test('should poll for updates when live', async () => {
let requestCount = 0;
await page.route('**/api/v1/findings/**/runtime/traces', (route) => {
requestCount++;
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
findingId: 'test-finding-001',
collectionActive: true,
summary: { totalHits: 100 + requestCount * 10, uniquePaths: 3 },
traces: [],
}),
});
});
await page.locator('[data-testid="tab-runtime"]').click();
// Wait for initial request
await page.waitForTimeout(100);
const initialCount = requestCount;
// Wait for poll interval (assuming 30s, we'll use a shorter timeout in test)
await page.waitForTimeout(35000);
expect(requestCount).toBeGreaterThan(initialCount);
});
});
test.describe('Tab Persistence', () => {
test('should persist selected tab in URL', async ({ page }) => {
await page.goto('/triage/findings/test-finding-001');
await page.waitForSelector('[data-testid="evidence-panel"]');
await page.locator('[data-testid="tab-diff"]').click();
// Check URL contains tab parameter
expect(page.url()).toContain('tab=diff');
});
test('should restore tab from URL on page load', async ({ page }) => {
await page.goto('/triage/findings/test-finding-001?tab=runtime');
await page.waitForSelector('[data-testid="evidence-panel"]');
const runtimePanel = page.locator('[data-testid="panel-runtime"]');
await expect(runtimePanel).toBeVisible();
});
});
test.describe('Accessibility', () => {
test('should have proper ARIA roles for Diff tab', async ({ page }) => {
await page.goto('/triage/findings/test-finding-001');
await page.waitForSelector('[data-testid="evidence-panel"]');
await page.locator('[data-testid="tab-diff"]').click();
const diffPanel = page.locator('[data-testid="panel-diff"]');
await expect(diffPanel).toHaveAttribute('role', 'tabpanel');
const badge = page.locator('[data-testid="backport-verdict-badge"]');
await expect(badge).toHaveAttribute('role', 'status');
});
test('should have proper ARIA roles for Runtime tab', async ({ page }) => {
await page.goto('/triage/findings/test-finding-001');
await page.waitForSelector('[data-testid="evidence-panel"]');
await page.locator('[data-testid="tab-runtime"]').click();
const runtimePanel = page.locator('[data-testid="panel-runtime"]');
await expect(runtimePanel).toHaveAttribute('role', 'tabpanel');
const indicator = page.locator('[data-testid="live-indicator"]');
await expect(indicator).toHaveAttribute('role', 'status');
});
test('should be keyboard navigable', async ({ page }) => {
await page.goto('/triage/findings/test-finding-001');
await page.waitForSelector('[data-testid="evidence-panel"]');
// Tab to diff tab
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
// Check diff tab is focused
const focusedElement = await page.locator(':focus');
await expect(focusedElement).toHaveAttribute('data-testid', 'tab-diff');
// Press Enter to activate
await page.keyboard.press('Enter');
const diffPanel = page.locator('[data-testid="panel-diff"]');
await expect(diffPanel).toBeVisible();
});
});

View File

@@ -0,0 +1,268 @@
// -----------------------------------------------------------------------------
// evidence-panel.e2e.spec.ts
// Sprint: SPRINT_20260107_006_001_FE_tabbed_evidence_panel
// Task: EP-013 — E2E Tests: Test tab switching, evidence loading, copy JSON, URL persistence
// -----------------------------------------------------------------------------
import { test, expect } from '@playwright/test';
test.describe('Evidence Panel E2E', () => {
test.beforeEach(async ({ page }) => {
// Navigate to triage view with a finding
await page.goto('/triage/findings/test-finding-123');
// Wait for evidence panel to load
await page.waitForSelector('.evidence-panel');
});
test.describe('Tab switching', () => {
test('should display all 5 tabs', async ({ page }) => {
const tabs = page.locator('.tab-btn');
await expect(tabs).toHaveCount(5);
await expect(tabs.nth(0)).toContainText('Provenance');
await expect(tabs.nth(1)).toContainText('Reachability');
await expect(tabs.nth(2)).toContainText('Diff');
await expect(tabs.nth(3)).toContainText('Runtime');
await expect(tabs.nth(4)).toContainText('Policy');
});
test('should default to Provenance tab', async ({ page }) => {
const activeTab = page.locator('.tab-btn--active');
await expect(activeTab).toContainText('Provenance');
});
test('should switch to Reachability tab on click', async ({ page }) => {
await page.click('[aria-controls="panel-reachability"]');
const activeTab = page.locator('.tab-btn--active');
await expect(activeTab).toContainText('Reachability');
const panel = page.locator('#panel-reachability');
await expect(panel).toBeVisible();
});
test('should switch tabs with keyboard shortcuts 1-5', async ({ page }) => {
// Press 2 for Reachability
await page.keyboard.press('2');
await expect(page.locator('.tab-btn--active')).toContainText('Reachability');
// Press 4 for Runtime
await page.keyboard.press('4');
await expect(page.locator('.tab-btn--active')).toContainText('Runtime');
// Press 1 for Provenance
await page.keyboard.press('1');
await expect(page.locator('.tab-btn--active')).toContainText('Provenance');
});
test('should navigate tabs with arrow keys', async ({ page }) => {
// Focus the tab nav
await page.locator('.tab-btn--active').focus();
// Arrow right to Reachability
await page.keyboard.press('ArrowRight');
await expect(page.locator('.tab-btn--active')).toContainText('Reachability');
// Arrow left back to Provenance
await page.keyboard.press('ArrowLeft');
await expect(page.locator('.tab-btn--active')).toContainText('Provenance');
});
test('should wrap tabs on arrow navigation', async ({ page }) => {
await page.locator('.tab-btn--active').focus();
// Arrow left from first should go to last
await page.keyboard.press('ArrowLeft');
await expect(page.locator('.tab-btn--active')).toContainText('Policy');
// Arrow right from last should go to first
await page.keyboard.press('ArrowRight');
await expect(page.locator('.tab-btn--active')).toContainText('Provenance');
});
});
test.describe('URL persistence', () => {
test('should update URL on tab change', async ({ page }) => {
await page.click('[aria-controls="panel-diff"]');
await expect(page).toHaveURL(/[?&]tab=diff/);
});
test('should restore tab from URL on page load', async ({ page }) => {
await page.goto('/triage/findings/test-finding-123?tab=runtime');
await expect(page.locator('.tab-btn--active')).toContainText('Runtime');
});
test('should handle invalid tab in URL gracefully', async ({ page }) => {
await page.goto('/triage/findings/test-finding-123?tab=invalid');
// Should default to provenance
await expect(page.locator('.tab-btn--active')).toContainText('Provenance');
});
});
test.describe('Evidence loading', () => {
test('should show loading spinner while fetching', async ({ page }) => {
// Slow down network to catch spinner
await page.route('**/api/evidence/**', async (route) => {
await new Promise((resolve) => setTimeout(resolve, 500));
await route.continue();
});
await page.click('[aria-controls="panel-reachability"]');
const spinner = page.locator('#panel-reachability .loading-state .spinner');
await expect(spinner).toBeVisible();
});
test('should show error state on fetch failure', async ({ page }) => {
await page.route('**/api/evidence/reachability/**', (route) => {
route.fulfill({
status: 500,
body: JSON.stringify({ error: 'Internal server error' }),
});
});
await page.click('[aria-controls="panel-reachability"]');
const error = page.locator('#panel-reachability .error-state');
await expect(error).toBeVisible();
});
test('should show retry button on error', async ({ page }) => {
await page.route('**/api/evidence/reachability/**', (route) => {
route.fulfill({
status: 500,
body: JSON.stringify({ error: 'Internal server error' }),
});
});
await page.click('[aria-controls="panel-reachability"]');
const retryBtn = page.locator('#panel-reachability .retry-btn');
await expect(retryBtn).toBeVisible();
});
test('should reload evidence on retry', async ({ page }) => {
let requestCount = 0;
await page.route('**/api/evidence/reachability/**', (route) => {
requestCount++;
if (requestCount === 1) {
route.fulfill({ status: 500 });
} else {
route.fulfill({ status: 200, body: JSON.stringify({}) });
}
});
await page.click('[aria-controls="panel-reachability"]');
await page.click('#panel-reachability .retry-btn');
expect(requestCount).toBe(2);
});
test('should not reload already loaded tab', async ({ page }) => {
let requestCount = 0;
await page.route('**/api/evidence/provenance/**', () => {
requestCount++;
});
// Tab is loaded on init
await page.waitForTimeout(200);
const initialCount = requestCount;
// Switch away and back
await page.click('[aria-controls="panel-reachability"]');
await page.click('[aria-controls="panel-provenance"]');
expect(requestCount).toBe(initialCount);
});
});
test.describe('Provenance tab', () => {
test('should display DSSE badge', async ({ page }) => {
const badge = page.locator('.dsse-badge');
await expect(badge).toBeVisible();
});
test('should display attestation chain', async ({ page }) => {
const chain = page.locator('.attestation-chain');
await expect(chain).toBeVisible();
});
test('should expand attestation chain node on click', async ({ page }) => {
const node = page.locator('.chain-node').first();
await node.click();
await expect(node).toHaveClass(/chain-node--expanded/);
});
});
test.describe('Copy JSON functionality', () => {
test('should have copy JSON button in provenance tab', async ({ page }) => {
const copyBtn = page.locator('.copy-json-btn');
await expect(copyBtn).toBeVisible();
});
test('should copy JSON to clipboard', async ({ page, context }) => {
// Grant clipboard permissions
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
await page.click('.copy-json-btn');
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardText).toBeTruthy();
// Should be valid JSON
expect(() => JSON.parse(clipboardText)).not.toThrow();
});
});
test.describe('Accessibility', () => {
test('should have correct ARIA attributes on tabs', async ({ page }) => {
const tablist = page.locator('[role="tablist"]');
await expect(tablist).toBeVisible();
const tabs = page.locator('[role="tab"]');
await expect(tabs).toHaveCount(5);
const panels = page.locator('[role="tabpanel"]');
await expect(panels).toHaveCount(5);
});
test('should set aria-selected correctly', async ({ page }) => {
const activeTab = page.locator('[aria-selected="true"]');
await expect(activeTab).toHaveCount(1);
await expect(activeTab).toContainText('Provenance');
await page.click('[aria-controls="panel-diff"]');
await expect(page.locator('[aria-controls="panel-diff"]')).toHaveAttribute('aria-selected', 'true');
await expect(page.locator('[aria-controls="panel-provenance"]')).toHaveAttribute('aria-selected', 'false');
});
test('should announce tab changes to screen readers', async ({ page }) => {
const panel = page.locator('#panel-provenance');
await expect(panel).toHaveAttribute('aria-labelledby', 'tab-provenance');
});
test('should have keyboard hint visible', async ({ page }) => {
const hint = page.locator('.keyboard-hint');
await expect(hint).toBeVisible();
await expect(hint).toContainText('1');
await expect(hint).toContainText('5');
});
});
test.describe('Responsive behavior', () => {
test('should scroll tabs on narrow viewport', async ({ page }) => {
await page.setViewportSize({ width: 320, height: 568 });
const tabNav = page.locator('.tab-nav');
const isScrollable = await tabNav.evaluate((el) => el.scrollWidth > el.clientWidth);
expect(isScrollable).toBe(true);
});
});
});