Files
git.stella-ops.org/src/Web/StellaOps.Web/tests/e2e/ux-components-visual.spec.ts

294 lines
10 KiB
TypeScript

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