save development progress

This commit is contained in:
StellaOps Bot
2025-12-25 23:09:58 +02:00
parent d71853ad7e
commit aa70af062e
351 changed files with 37683 additions and 150156 deletions

View File

@@ -0,0 +1,388 @@
// -----------------------------------------------------------------------------
// quiet-triage-a11y.spec.ts
// Sprint: SPRINT_9200_0001_0004_FE_quiet_triage_ui
// Description: Accessibility tests for the Quiet-by-Design triage workflow.
// Tests WCAG 2.0 AA compliance for gated buckets, VEX trust, replay command.
// -----------------------------------------------------------------------------
import { test, expect, Page } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
import fs from 'node:fs';
import path from 'node:path';
import { policyAuthorSession } from '../../src/app/testing';
const shouldFail = process.env.FAIL_ON_A11Y === '1';
const reportDir = path.join(process.cwd(), 'test-results');
const mockConfig = {
authority: {
issuer: 'https://authority.local',
clientId: 'stellaops-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 authority:tenants.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read orch:read vuln:view vuln:investigate vuln:operate vuln:audit',
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,
};
// Mock responses for quiet triage components
const mockGatedBuckets = {
unreachableCount: 42,
policyDismissedCount: 15,
backportedCount: 8,
vexNotAffectedCount: 23,
supersededCount: 3,
userMutedCount: 5,
totalHiddenCount: 96,
actionableCount: 12,
};
const mockVexTrust = {
status: 'not_affected',
justification: 'vulnerable_code_not_in_execute_path',
issuedBy: 'vendor.example',
issuedAt: '2025-12-15T10:00:00Z',
trustScore: 0.85,
policyTrustThreshold: 0.80,
meetsPolicyThreshold: true,
trustBreakdown: {
authority: 0.90,
accuracy: 0.85,
timeliness: 0.80,
verification: 0.85,
},
};
const mockReplayCommand = {
findingId: 'f-abc123',
scanId: 'scan-xyz789',
fullCommand: {
type: 'full',
command: 'stella scan replay --artifact sha256:a1b2c3... --manifest sha256:def456...',
shell: 'bash',
requiresNetwork: false,
},
shortCommand: {
type: 'short',
command: 'stella replay snapshot --verdict V-12345',
shell: 'bash',
requiresNetwork: false,
},
generatedAt: '2025-12-15T10:30:00Z',
expectedVerdictHash: 'sha256:verdict123...',
};
async function writeReport(filename: string, data: unknown) {
fs.mkdirSync(reportDir, { recursive: true });
fs.writeFileSync(path.join(reportDir, filename), JSON.stringify(data, null, 2));
}
async function runA11y(page: Page, selector?: string) {
let builder = new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']);
if (selector) {
builder = builder.include(selector);
}
const results = await builder.analyze();
const violations = [...results.violations].sort((a, b) => a.id.localeCompare(b.id));
return violations;
}
test.describe('quiet-triage-a11y', () => {
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());
// Mock gating API endpoints
await page.route('**/api/v1/triage/findings/*/gated-buckets', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockGatedBuckets),
})
);
await page.route('**/api/v1/triage/findings/*/vex-trust', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockVexTrust),
})
);
await page.route('**/api/v1/triage/findings/*/replay-command', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockReplayCommand),
})
);
});
test('gated buckets component: WCAG 2.0 AA compliance', async ({ page }, testInfo) => {
await page.goto('/triage/artifacts/asset-web-prod');
await expect(page.getByRole('heading', { name: 'Artifact triage' })).toBeVisible({ timeout: 10000 });
const violations = await runA11y(page, '.gated-buckets');
await writeReport('a11y-quiet_triage_gated_buckets.json', { url: page.url(), violations });
if (shouldFail) {
expect(violations).toEqual([]);
}
testInfo.annotations.push({
type: 'a11y',
description: `${violations.length} violations (gated buckets component)`,
});
// Specific checks for gated buckets
const gatedBuckets = page.locator('.gated-buckets');
// Check role and aria-label
await expect(gatedBuckets).toHaveAttribute('role', 'group');
await expect(gatedBuckets).toHaveAttribute('aria-label', 'Gated findings summary');
// Check bucket chips have aria-expanded
const chips = gatedBuckets.locator('.bucket-chip');
const chipCount = await chips.count();
for (let i = 0; i < chipCount; i++) {
await expect(chips.nth(i)).toHaveAttribute('aria-expanded');
await expect(chips.nth(i)).toHaveAttribute('aria-label');
}
// Check show all toggle has aria-pressed
const showAllToggle = gatedBuckets.locator('.show-all-toggle');
await expect(showAllToggle).toHaveAttribute('aria-pressed');
});
test('VEX trust display: WCAG 2.0 AA compliance', async ({ page }, testInfo) => {
await page.goto('/triage/artifacts/asset-web-prod/findings/f-abc123');
await expect(page.getByRole('heading', { name: 'Finding detail' })).toBeVisible({ timeout: 10000 });
const violations = await runA11y(page, '.vex-trust-display');
await writeReport('a11y-quiet_triage_vex_trust.json', { url: page.url(), violations });
if (shouldFail) {
expect(violations).toEqual([]);
}
testInfo.annotations.push({
type: 'a11y',
description: `${violations.length} violations (VEX trust display)`,
});
// Check color contrast for trust score
const trustScore = page.locator('.vex-trust-display .trust-score');
await expect(trustScore).toBeVisible();
// Check expand button has proper labeling
const expandBtn = page.locator('.vex-trust-display').getByRole('button');
await expect(expandBtn).toHaveAttribute('aria-expanded');
});
test('replay command component: WCAG 2.0 AA compliance', async ({ page }, testInfo) => {
await page.goto('/triage/artifacts/asset-web-prod/findings/f-abc123');
await expect(page.getByRole('heading', { name: 'Finding detail' })).toBeVisible({ timeout: 10000 });
const violations = await runA11y(page, '.replay-command');
await writeReport('a11y-quiet_triage_replay_command.json', { url: page.url(), violations });
if (shouldFail) {
expect(violations).toEqual([]);
}
testInfo.annotations.push({
type: 'a11y',
description: `${violations.length} violations (replay command)`,
});
// Check tabs have proper ARIA roles
const tabs = page.locator('.replay-command .command-tabs');
await expect(tabs).toHaveAttribute('role', 'tablist');
const tabButtons = tabs.locator('.tab');
const tabCount = await tabButtons.count();
for (let i = 0; i < tabCount; i++) {
await expect(tabButtons.nth(i)).toHaveAttribute('role', 'tab');
await expect(tabButtons.nth(i)).toHaveAttribute('aria-selected');
}
});
test('gating explainer modal: WCAG 2.0 AA compliance', async ({ page }, testInfo) => {
await page.goto('/triage/artifacts/asset-web-prod/findings/f-abc123');
await expect(page.getByRole('heading', { name: 'Finding detail' })).toBeVisible({ timeout: 10000 });
// Open the modal
const whyHiddenLink = page.getByRole('button', { name: /Why hidden/ });
await whyHiddenLink.click();
const modal = page.getByRole('dialog', { name: 'Gating explanation' });
await expect(modal).toBeVisible();
const violations = await runA11y(page, '[role="dialog"]');
await writeReport('a11y-quiet_triage_gating_modal.json', { url: page.url(), violations });
if (shouldFail) {
expect(violations).toEqual([]);
}
testInfo.annotations.push({
type: 'a11y',
description: `${violations.length} violations (gating explainer modal)`,
});
// Check modal has proper ARIA attributes
await expect(modal).toHaveAttribute('role', 'dialog');
await expect(modal).toHaveAttribute('aria-modal', 'true');
// Check close button is accessible
const closeBtn = modal.getByRole('button', { name: 'Close' });
await expect(closeBtn).toBeVisible();
await expect(closeBtn).toBeFocused();
});
test('keyboard navigation: focus management', async ({ page }, testInfo) => {
await page.goto('/triage/artifacts/asset-web-prod');
await expect(page.getByRole('heading', { name: 'Artifact triage' })).toBeVisible({ timeout: 10000 });
// Tab through gated buckets
const gatedBuckets = page.locator('.gated-buckets');
const chips = gatedBuckets.locator('.bucket-chip');
// Focus first chip
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
const firstChip = chips.first();
await expect(firstChip).toBeFocused();
// Check focus indicator is visible (outline)
const focusStyles = await firstChip.evaluate((el) => {
const styles = window.getComputedStyle(el);
return {
outline: styles.outline,
outlineOffset: styles.outlineOffset,
};
});
// Focus should have visible outline
expect(focusStyles.outline).not.toBe('none');
testInfo.annotations.push({
type: 'a11y',
description: 'Focus indicators verified',
});
});
test('keyboard navigation: enter activates bucket chip', async ({ page }) => {
await page.goto('/triage/artifacts/asset-web-prod');
await expect(page.getByRole('heading', { name: 'Artifact triage' })).toBeVisible({ timeout: 10000 });
const unreachableChip = page.getByRole('button', { name: /Show 42 unreachable findings/ });
// Focus the chip
await unreachableChip.focus();
await expect(unreachableChip).toBeFocused();
// Activate with Enter
await page.keyboard.press('Enter');
await expect(unreachableChip).toHaveAttribute('aria-expanded', 'true');
// Deactivate with Enter
await page.keyboard.press('Enter');
await expect(unreachableChip).toHaveAttribute('aria-expanded', 'false');
});
test('keyboard navigation: space activates bucket chip', async ({ page }) => {
await page.goto('/triage/artifacts/asset-web-prod');
await expect(page.getByRole('heading', { name: 'Artifact triage' })).toBeVisible({ timeout: 10000 });
const policyChip = page.getByRole('button', { name: /policy-dismissed/ });
// Focus the chip
await policyChip.focus();
await expect(policyChip).toBeFocused();
// Activate with Space
await page.keyboard.press('Space');
await expect(policyChip).toHaveAttribute('aria-expanded', 'true');
});
test('screen reader: live region announces changes', async ({ page }, testInfo) => {
await page.goto('/triage/artifacts/asset-web-prod');
await expect(page.getByRole('heading', { name: 'Artifact triage' })).toBeVisible({ timeout: 10000 });
// Check for aria-live region for dynamic updates
const liveRegion = page.locator('[aria-live]');
const liveRegionCount = await liveRegion.count();
testInfo.annotations.push({
type: 'a11y',
description: `${liveRegionCount} aria-live regions found`,
});
// At minimum, status updates should have aria-live
expect(liveRegionCount).toBeGreaterThanOrEqual(0);
});
test('color contrast: bucket chips meet WCAG AA', async ({ page }) => {
await page.goto('/triage/artifacts/asset-web-prod');
await expect(page.getByRole('heading', { name: 'Artifact triage' })).toBeVisible({ timeout: 10000 });
const violations = await runA11y(page, '.bucket-chips');
// Filter for color-contrast violations
const contrastViolations = violations.filter((v) => v.id === 'color-contrast');
// No contrast violations should exist in bucket chips
expect(contrastViolations.length).toBe(0);
});
test('reduced motion: respects prefers-reduced-motion', async ({ page }) => {
// Emulate reduced motion preference
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.goto('/triage/artifacts/asset-web-prod');
await expect(page.getByRole('heading', { name: 'Artifact triage' })).toBeVisible({ timeout: 10000 });
const chip = page.locator('.bucket-chip').first();
// Check transitions are instant or disabled
const transitionDuration = await chip.evaluate((el) => {
return window.getComputedStyle(el).transitionDuration;
});
// With reduced motion, transitions should be instant (0s) or very fast
const duration = parseFloat(transitionDuration);
expect(duration).toBeLessThanOrEqual(0.01);
});
});

View File

@@ -0,0 +1,335 @@
// -----------------------------------------------------------------------------
// quiet-triage.spec.ts
// Sprint: SPRINT_9200_0001_0004_FE_quiet_triage_ui
// Description: E2E tests for the Quiet-by-Design triage workflow.
// Tests gated bucket chips, VEX trust display, replay command, and gating explainer.
// -----------------------------------------------------------------------------
import { expect, test } from '@playwright/test';
import { policyAuthorSession } from '../../src/app/testing';
const mockConfig = {
authority: {
issuer: 'https://authority.local',
clientId: 'stellaops-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 authority:tenants.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read orch:read vuln:view vuln:investigate vuln:operate vuln:audit',
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,
};
// Mock gated buckets response
const mockGatedBuckets = {
unreachableCount: 42,
policyDismissedCount: 15,
backportedCount: 8,
vexNotAffectedCount: 23,
supersededCount: 3,
userMutedCount: 5,
totalHiddenCount: 96,
actionableCount: 12,
};
// Mock VEX trust response
const mockVexTrust = {
status: 'not_affected',
justification: 'vulnerable_code_not_in_execute_path',
issuedBy: 'vendor.example',
issuedAt: '2025-12-15T10:00:00Z',
trustScore: 0.85,
policyTrustThreshold: 0.80,
meetsPolicyThreshold: true,
trustBreakdown: {
authority: 0.90,
accuracy: 0.85,
timeliness: 0.80,
verification: 0.85,
},
};
// Mock replay command response
const mockReplayCommand = {
findingId: 'f-abc123',
scanId: 'scan-xyz789',
fullCommand: {
type: 'full',
command: 'stella scan replay --artifact sha256:a1b2c3... --manifest sha256:def456...',
shell: 'bash',
requiresNetwork: false,
},
shortCommand: {
type: 'short',
command: 'stella replay snapshot --verdict V-12345',
shell: 'bash',
requiresNetwork: false,
},
generatedAt: '2025-12-15T10:30:00Z',
expectedVerdictHash: 'sha256:verdict123...',
};
test.describe('quiet-triage', () => {
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());
// Mock gating API endpoints
await page.route('**/api/v1/triage/findings/*/gated-buckets', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockGatedBuckets),
})
);
await page.route('**/api/v1/triage/findings/*/vex-trust', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockVexTrust),
})
);
await page.route('**/api/v1/triage/findings/*/replay-command', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockReplayCommand),
})
);
});
test('gated buckets: displays actionable count and hidden summary', async ({ page }) => {
await page.goto('/triage/artifacts/asset-web-prod');
await expect(page.getByRole('heading', { name: 'Artifact triage' })).toBeVisible({ timeout: 10000 });
// Check gated buckets component
const gatedBuckets = page.locator('[role="group"][aria-label="Gated findings summary"]');
await expect(gatedBuckets).toBeVisible();
// Check actionable count
await expect(gatedBuckets.locator('.actionable-count')).toContainText('12');
await expect(gatedBuckets.locator('.hidden-hint')).toContainText('96 hidden');
});
test('gated buckets: bucket chips show correct counts', async ({ page }) => {
await page.goto('/triage/artifacts/asset-web-prod');
await expect(page.getByRole('heading', { name: 'Artifact triage' })).toBeVisible({ timeout: 10000 });
// Check unreachable chip
const unreachableChip = page.getByRole('button', { name: /Show 42 unreachable findings/ });
await expect(unreachableChip).toBeVisible();
await expect(unreachableChip).toContainText('+42');
await expect(unreachableChip).toContainText('unreachable');
// Check VEX chip
const vexChip = page.getByRole('button', { name: /Show 23 VEX not-affected findings/ });
await expect(vexChip).toBeVisible();
await expect(vexChip).toContainText('+23');
});
test('gated buckets: clicking chip expands bucket', async ({ page }) => {
await page.goto('/triage/artifacts/asset-web-prod');
await expect(page.getByRole('heading', { name: 'Artifact triage' })).toBeVisible({ timeout: 10000 });
const unreachableChip = page.getByRole('button', { name: /Show 42 unreachable findings/ });
// Click to expand
await unreachableChip.click();
await expect(unreachableChip).toHaveAttribute('aria-expanded', 'true');
await expect(unreachableChip).toHaveClass(/expanded/);
// Click again to collapse
await unreachableChip.click();
await expect(unreachableChip).toHaveAttribute('aria-expanded', 'false');
await expect(unreachableChip).not.toHaveClass(/expanded/);
});
test('gated buckets: show all toggle reveals hidden findings', async ({ page }) => {
await page.goto('/triage/artifacts/asset-web-prod');
await expect(page.getByRole('heading', { name: 'Artifact triage' })).toBeVisible({ timeout: 10000 });
const showAllToggle = page.getByRole('button', { name: 'Show all' });
await expect(showAllToggle).toBeVisible();
await expect(showAllToggle).toHaveAttribute('aria-pressed', 'false');
// Click to show all
await showAllToggle.click();
await expect(showAllToggle).toHaveAttribute('aria-pressed', 'true');
await expect(showAllToggle).toContainText('Hide gated');
// Click to hide again
await showAllToggle.click();
await expect(showAllToggle).toHaveAttribute('aria-pressed', 'false');
await expect(showAllToggle).toContainText('Show all');
});
test('VEX trust display: shows trust score and threshold', async ({ page }) => {
await page.goto('/triage/artifacts/asset-web-prod/findings/f-abc123');
await expect(page.getByRole('heading', { name: 'Finding detail' })).toBeVisible({ timeout: 10000 });
// Check VEX trust display
const vexTrust = page.locator('.vex-trust-display');
await expect(vexTrust).toBeVisible();
// Check trust score
await expect(vexTrust.locator('.trust-score')).toContainText('0.85');
await expect(vexTrust.locator('.threshold')).toContainText('0.80');
await expect(vexTrust.locator('.meets-threshold')).toBeVisible();
});
test('VEX trust display: shows trust breakdown on expand', async ({ page }) => {
await page.goto('/triage/artifacts/asset-web-prod/findings/f-abc123');
await expect(page.getByRole('heading', { name: 'Finding detail' })).toBeVisible({ timeout: 10000 });
const expandBtn = page.getByRole('button', { name: 'Show trust breakdown' });
await expandBtn.click();
const breakdown = page.locator('.trust-breakdown');
await expect(breakdown).toBeVisible();
await expect(breakdown).toContainText('Authority');
await expect(breakdown).toContainText('0.90');
await expect(breakdown).toContainText('Accuracy');
await expect(breakdown).toContainText('0.85');
});
test('replay command: displays command and copy button', async ({ page }) => {
await page.goto('/triage/artifacts/asset-web-prod/findings/f-abc123');
await expect(page.getByRole('heading', { name: 'Finding detail' })).toBeVisible({ timeout: 10000 });
const replaySection = page.locator('.replay-command');
await expect(replaySection).toBeVisible();
// Check command text
await expect(replaySection.locator('.command-text')).toContainText('stella scan replay');
// Check copy button
const copyBtn = replaySection.getByRole('button', { name: /Copy/ });
await expect(copyBtn).toBeVisible();
await expect(copyBtn).toBeEnabled();
});
test('replay command: tab switching between full and short', async ({ page }) => {
await page.goto('/triage/artifacts/asset-web-prod/findings/f-abc123');
await expect(page.getByRole('heading', { name: 'Finding detail' })).toBeVisible({ timeout: 10000 });
const replaySection = page.locator('.replay-command');
// Full tab active by default
const fullTab = replaySection.getByRole('tab', { name: 'Full' });
await expect(fullTab).toHaveAttribute('aria-selected', 'true');
await expect(replaySection.locator('.command-text')).toContainText('--artifact sha256');
// Switch to short tab
const shortTab = replaySection.getByRole('tab', { name: 'Short' });
await shortTab.click();
await expect(shortTab).toHaveAttribute('aria-selected', 'true');
await expect(replaySection.locator('.command-text')).toContainText('--verdict V-12345');
});
test('replay command: copy to clipboard', async ({ page, context }) => {
// Grant clipboard permissions
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
await page.goto('/triage/artifacts/asset-web-prod/findings/f-abc123');
await expect(page.getByRole('heading', { name: 'Finding detail' })).toBeVisible({ timeout: 10000 });
const replaySection = page.locator('.replay-command');
const copyBtn = replaySection.getByRole('button', { name: /Copy/ });
await copyBtn.click();
// Button should show copied state
await expect(copyBtn).toContainText('Copied!');
// Verify clipboard content
const clipboardContent = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardContent).toContain('stella scan replay');
});
test('gating explainer: opens modal with explanation', async ({ page }) => {
await page.goto('/triage/artifacts/asset-web-prod/findings/f-abc123');
await expect(page.getByRole('heading', { name: 'Finding detail' })).toBeVisible({ timeout: 10000 });
// Click "Why hidden?" link
const whyHiddenLink = page.getByRole('button', { name: /Why hidden/ });
await whyHiddenLink.click();
// Modal should open
const modal = page.getByRole('dialog', { name: 'Gating explanation' });
await expect(modal).toBeVisible();
// Check content
await expect(modal).toContainText('This finding is hidden because');
await expect(modal.getByRole('button', { name: 'Close' })).toBeVisible();
});
test('keyboard navigation: bucket chips focusable', async ({ page }) => {
await page.goto('/triage/artifacts/asset-web-prod');
await expect(page.getByRole('heading', { name: 'Artifact triage' })).toBeVisible({ timeout: 10000 });
// Tab to first bucket chip
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
const unreachableChip = page.getByRole('button', { name: /Show 42 unreachable findings/ });
await expect(unreachableChip).toBeFocused();
// Enter to activate
await page.keyboard.press('Enter');
await expect(unreachableChip).toHaveAttribute('aria-expanded', 'true');
// Tab to next chip
await page.keyboard.press('Tab');
const policyChip = page.getByRole('button', { name: /policy-dismissed/ });
await expect(policyChip).toBeFocused();
});
test('screen reader: proper ARIA labels on components', async ({ page }) => {
await page.goto('/triage/artifacts/asset-web-prod');
await expect(page.getByRole('heading', { name: 'Artifact triage' })).toBeVisible({ timeout: 10000 });
// Check gated buckets group label
const gatedBuckets = page.locator('[role="group"][aria-label="Gated findings summary"]');
await expect(gatedBuckets).toHaveAttribute('aria-label', 'Gated findings summary');
// Check bucket chip labels
const unreachableChip = page.getByRole('button', { name: /Show 42 unreachable findings/ });
await expect(unreachableChip).toHaveAttribute('aria-label', /Show 42 unreachable findings/);
// Check show all toggle
const showAllToggle = page.getByRole('button', { name: 'Show all' });
await expect(showAllToggle).toHaveAttribute('aria-pressed', 'false');
});
});