// ----------------------------------------------------------------------------- // 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(); }); });