// ----------------------------------------------------------------------------- // ux-components-visual.spec.ts // Sprint: SPRINT_20260117_018_FE_ux_components // Task: UXC-008 - Integration tests with Playwright // Description: Visual regression tests for new UX components // ----------------------------------------------------------------------------- import { expect, test } from '@playwright/test'; import { policyAuthorSession } from '../../src/app/testing'; const mockConfig = { authority: { issuer: 'https://authority.local', clientId: 'stella-ops-ui', authorizeEndpoint: 'https://authority.local/connect/authorize', tokenEndpoint: 'https://authority.local/connect/token', logoutEndpoint: 'https://authority.local/connect/logout', redirectUri: 'http://127.0.0.1:4400/auth/callback', postLogoutRedirectUri: 'http://127.0.0.1:4400/', scope: 'openid profile email ui.read findings:read vuln:view binary:read', audience: 'https://scanner.local', dpopAlgorithms: ['ES256'], refreshLeewaySeconds: 60, }, apiBaseUrls: { authority: 'https://authority.local', scanner: 'https://scanner.local', policy: 'https://scanner.local', concelier: 'https://concelier.local', attestor: 'https://attestor.local', }, quickstartMode: true, }; test.beforeEach(async ({ page }) => { await page.addInitScript((session) => { try { window.sessionStorage.clear(); } catch { // ignore storage errors in restricted contexts } (window as any).__stellaopsTestSession = session; }, policyAuthorSession); await page.route('**/config.json', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockConfig), }) ); await page.route('https://authority.local/**', (route) => route.abort()); }); test.describe.skip('UX Components Visual Regression' /* TODO: Visual regression tests depend on filter-strip, triage-card, binary-diff components that need selector alignment */, () => { test.describe('Triage Card', () => { test('default state screenshot', async ({ page }) => { await page.goto('/triage/artifacts/test-artifact'); await expect(page.locator('.triage-card').first()).toBeVisible({ timeout: 10000 }); // Wait for any animations to complete await page.waitForTimeout(500); // Take screenshot of first triage card await expect(page.locator('.triage-card').first()).toHaveScreenshot('triage-card-default.png', { maxDiffPixelRatio: 0.02, }); }); test('hover state screenshot', async ({ page }) => { await page.goto('/triage/artifacts/test-artifact'); const card = page.locator('.triage-card').first(); await expect(card).toBeVisible({ timeout: 10000 }); // Hover over card await card.hover(); await page.waitForTimeout(300); await expect(card).toHaveScreenshot('triage-card-hover.png', { maxDiffPixelRatio: 0.02, }); }); test('expanded verification state screenshot', async ({ page }) => { await page.goto('/triage/artifacts/test-artifact'); const card = page.locator('.triage-card').first(); await expect(card).toBeVisible({ timeout: 10000 }); // Click Rekor Verify and wait for expansion await page.getByRole('button', { name: /Rekor Verify/ }).first().click(); await page.waitForTimeout(2000); // Screenshot expanded state await expect(card).toHaveScreenshot('triage-card-expanded.png', { maxDiffPixelRatio: 0.05, }); }); test('risk chip variants screenshot', async ({ page }) => { await page.goto('/triage/artifacts/test-artifact'); await expect(page.locator('.risk-chip').first()).toBeVisible({ timeout: 10000 }); // Screenshot all risk chips const riskChips = page.locator('.risk-chip'); for (let i = 0; i < Math.min(4, await riskChips.count()); i++) { await expect(riskChips.nth(i)).toHaveScreenshot(`risk-chip-variant-${i}.png`, { maxDiffPixelRatio: 0.02, }); } }); }); test.describe('Filter Strip', () => { test('default state screenshot', async ({ page }) => { await page.goto('/triage/artifacts/test-artifact'); await expect(page.locator('.filter-strip')).toBeVisible({ timeout: 10000 }); await page.waitForTimeout(500); await expect(page.locator('.filter-strip')).toHaveScreenshot('filter-strip-default.png', { maxDiffPixelRatio: 0.02, }); }); test('with filters active screenshot', async ({ page }) => { await page.goto('/triage/artifacts/test-artifact'); await expect(page.locator('.filter-strip')).toBeVisible({ timeout: 10000 }); // Activate some filters await page.getByLabel(/Only reachable/i).check(); await page.locator('#epss-slider').fill('50'); await page.waitForTimeout(300); await expect(page.locator('.filter-strip')).toHaveScreenshot('filter-strip-active.png', { maxDiffPixelRatio: 0.02, }); }); test('deterministic toggle states screenshot', async ({ page }) => { await page.goto('/triage/artifacts/test-artifact'); await expect(page.locator('.filter-strip')).toBeVisible({ timeout: 10000 }); const toggle = page.locator('.determinism-toggle'); // Active state (default) await expect(toggle).toHaveScreenshot('determinism-toggle-active.png', { maxDiffPixelRatio: 0.02, }); // Inactive state await toggle.click(); await page.waitForTimeout(300); await expect(toggle).toHaveScreenshot('determinism-toggle-inactive.png', { maxDiffPixelRatio: 0.02, }); }); }); test.describe('Binary-Diff Panel', () => { test('default state screenshot', async ({ page }) => { await page.goto('/sbom/diff/sha256:base123/sha256:head456'); await expect(page.locator('.binary-diff-panel')).toBeVisible({ timeout: 10000 }); await page.waitForTimeout(500); await expect(page.locator('.binary-diff-panel')).toHaveScreenshot('binary-diff-panel-default.png', { maxDiffPixelRatio: 0.02, }); }); test('scope selector states screenshot', async ({ page }) => { await page.goto('/sbom/diff/sha256:base123/sha256:head456'); await expect(page.locator('.binary-diff-panel')).toBeVisible({ timeout: 10000 }); const scopeSelector = page.locator('.scope-selector'); // File scope (default) await expect(scopeSelector).toHaveScreenshot('scope-selector-file.png', { maxDiffPixelRatio: 0.02, }); // Section scope await page.getByRole('button', { name: /Section/i }).click(); await page.waitForTimeout(300); await expect(scopeSelector).toHaveScreenshot('scope-selector-section.png', { maxDiffPixelRatio: 0.02, }); // Function scope await page.getByRole('button', { name: /Function/i }).click(); await page.waitForTimeout(300); await expect(scopeSelector).toHaveScreenshot('scope-selector-function.png', { maxDiffPixelRatio: 0.02, }); }); test('tree item change indicators screenshot', async ({ page }) => { await page.goto('/sbom/diff/sha256:base123/sha256:head456'); await expect(page.locator('.binary-diff-panel')).toBeVisible({ timeout: 10000 }); const tree = page.locator('.scope-tree'); await expect(tree).toHaveScreenshot('diff-tree-items.png', { maxDiffPixelRatio: 0.02, }); }); test('diff view lines screenshot', async ({ page }) => { await page.goto('/sbom/diff/sha256:base123/sha256:head456'); await expect(page.locator('.binary-diff-panel')).toBeVisible({ timeout: 10000 }); // Select an entry to show diff await page.locator('.tree-item').first().click(); await page.waitForTimeout(300); const diffView = page.locator('.diff-view'); await expect(diffView).toHaveScreenshot('diff-view-lines.png', { maxDiffPixelRatio: 0.02, }); }); }); test.describe('Dark Mode', () => { test.beforeEach(async ({ page }) => { // Enable dark mode await page.emulateMedia({ colorScheme: 'dark' }); }); test('triage card dark mode screenshot', async ({ page }) => { await page.goto('/triage/artifacts/test-artifact'); await expect(page.locator('.triage-card').first()).toBeVisible({ timeout: 10000 }); await page.waitForTimeout(500); await expect(page.locator('.triage-card').first()).toHaveScreenshot('triage-card-dark.png', { maxDiffPixelRatio: 0.02, }); }); test('filter strip dark mode screenshot', async ({ page }) => { await page.goto('/triage/artifacts/test-artifact'); await expect(page.locator('.filter-strip')).toBeVisible({ timeout: 10000 }); await page.waitForTimeout(500); await expect(page.locator('.filter-strip')).toHaveScreenshot('filter-strip-dark.png', { maxDiffPixelRatio: 0.02, }); }); test('binary diff panel dark mode screenshot', async ({ page }) => { await page.goto('/sbom/diff/sha256:base123/sha256:head456'); await expect(page.locator('.binary-diff-panel')).toBeVisible({ timeout: 10000 }); await page.waitForTimeout(500); await expect(page.locator('.binary-diff-panel')).toHaveScreenshot('binary-diff-panel-dark.png', { maxDiffPixelRatio: 0.02, }); }); }); test.describe('Responsive', () => { test('filter strip mobile viewport', async ({ page }) => { await page.setViewportSize({ width: 375, height: 667 }); await page.goto('/triage/artifacts/test-artifact'); await expect(page.locator('.filter-strip')).toBeVisible({ timeout: 10000 }); await page.waitForTimeout(500); await expect(page.locator('.filter-strip')).toHaveScreenshot('filter-strip-mobile.png', { maxDiffPixelRatio: 0.05, }); }); test('triage card mobile viewport', async ({ page }) => { await page.setViewportSize({ width: 375, height: 667 }); await page.goto('/triage/artifacts/test-artifact'); await expect(page.locator('.triage-card').first()).toBeVisible({ timeout: 10000 }); await page.waitForTimeout(500); await expect(page.locator('.triage-card').first()).toHaveScreenshot('triage-card-mobile.png', { maxDiffPixelRatio: 0.05, }); }); }); });