feat: add bulk triage view component and related stories
- Exported BulkTriageViewComponent and its related types from findings module. - Created a new accessibility test suite for score components using axe-core. - Introduced design tokens for score components to standardize styling. - Enhanced score breakdown popover for mobile responsiveness with drag handle. - Added date range selector functionality to score history chart component. - Implemented unit tests for date range selector in score history chart. - Created Storybook stories for bulk triage view and score history chart with date range selector.
This commit is contained in:
536
src/Web/StellaOps.Web/tests/e2e/score-features.spec.ts
Normal file
536
src/Web/StellaOps.Web/tests/e2e/score-features.spec.ts
Normal file
@@ -0,0 +1,536 @@
|
||||
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,
|
||||
};
|
||||
|
||||
const mockFindings = [
|
||||
{
|
||||
id: 'CVE-2024-1234@pkg:npm/lodash@4.17.20',
|
||||
advisoryId: 'CVE-2024-1234',
|
||||
packageName: 'lodash',
|
||||
packageVersion: '4.17.20',
|
||||
severity: 'critical',
|
||||
status: 'open',
|
||||
},
|
||||
{
|
||||
id: 'CVE-2024-5678@pkg:npm/express@4.18.0',
|
||||
advisoryId: 'CVE-2024-5678',
|
||||
packageName: 'express',
|
||||
packageVersion: '4.18.0',
|
||||
severity: 'high',
|
||||
status: 'open',
|
||||
},
|
||||
{
|
||||
id: 'GHSA-abc123@pkg:pypi/requests@2.25.0',
|
||||
advisoryId: 'GHSA-abc123',
|
||||
packageName: 'requests',
|
||||
packageVersion: '2.25.0',
|
||||
severity: 'medium',
|
||||
status: 'open',
|
||||
},
|
||||
];
|
||||
|
||||
const mockScoreResults = [
|
||||
{
|
||||
findingId: 'CVE-2024-1234@pkg:npm/lodash@4.17.20',
|
||||
score: 92,
|
||||
bucket: 'ActNow',
|
||||
inputs: { rch: 0.9, rts: 0.8, bkp: 0, xpl: 0.9, src: 0.8, mit: 0.1 },
|
||||
weights: { rch: 0.3, rts: 0.25, bkp: 0.15, xpl: 0.15, src: 0.1, mit: 0.05 },
|
||||
flags: ['live-signal', 'proven-path'],
|
||||
explanations: ['High reachability via static analysis', 'Active runtime signals detected'],
|
||||
caps: { speculativeCap: false, notAffectedCap: false, runtimeFloor: true },
|
||||
policyDigest: 'sha256:abc123',
|
||||
calculatedAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
findingId: 'CVE-2024-5678@pkg:npm/express@4.18.0',
|
||||
score: 78,
|
||||
bucket: 'ScheduleNext',
|
||||
inputs: { rch: 0.7, rts: 0.3, bkp: 0, xpl: 0.6, src: 0.8, mit: 0 },
|
||||
weights: { rch: 0.3, rts: 0.25, bkp: 0.15, xpl: 0.15, src: 0.1, mit: 0.05 },
|
||||
flags: ['proven-path'],
|
||||
explanations: ['Verified call path to vulnerable function'],
|
||||
caps: { speculativeCap: false, notAffectedCap: false, runtimeFloor: false },
|
||||
policyDigest: 'sha256:abc123',
|
||||
calculatedAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
findingId: 'GHSA-abc123@pkg:pypi/requests@2.25.0',
|
||||
score: 45,
|
||||
bucket: 'Investigate',
|
||||
inputs: { rch: 0.4, rts: 0, bkp: 0, xpl: 0.5, src: 0.6, mit: 0 },
|
||||
weights: { rch: 0.3, rts: 0.25, bkp: 0.15, xpl: 0.15, src: 0.1, mit: 0.05 },
|
||||
flags: ['speculative'],
|
||||
explanations: ['Reachability unconfirmed'],
|
||||
caps: { speculativeCap: true, notAffectedCap: false, runtimeFloor: false },
|
||||
policyDigest: 'sha256:abc123',
|
||||
calculatedAt: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
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('**/api/findings**', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ items: mockFindings, total: mockFindings.length }),
|
||||
})
|
||||
);
|
||||
|
||||
await page.route('**/api/scores/batch', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ results: mockScoreResults }),
|
||||
})
|
||||
);
|
||||
|
||||
await page.route('https://authority.local/**', (route) => route.abort());
|
||||
});
|
||||
|
||||
test.describe('Score Pill Component', () => {
|
||||
test('displays score pills with correct bucket colors', async ({ page }) => {
|
||||
await page.goto('/findings');
|
||||
await expect(page.getByRole('heading', { name: /findings/i })).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Wait for scores to load
|
||||
await page.waitForResponse('**/api/scores/batch');
|
||||
|
||||
// Check Act Now score (92) has red styling
|
||||
const actNowPill = page.locator('stella-score-pill').filter({ hasText: '92' });
|
||||
await expect(actNowPill).toBeVisible();
|
||||
await expect(actNowPill).toHaveCSS('background-color', 'rgb(220, 38, 38)'); // #DC2626
|
||||
|
||||
// Check Schedule Next score (78) has amber styling
|
||||
const scheduleNextPill = page.locator('stella-score-pill').filter({ hasText: '78' });
|
||||
await expect(scheduleNextPill).toBeVisible();
|
||||
|
||||
// Check Investigate score (45) has blue styling
|
||||
const investigatePill = page.locator('stella-score-pill').filter({ hasText: '45' });
|
||||
await expect(investigatePill).toBeVisible();
|
||||
});
|
||||
|
||||
test('score pill shows tooltip on hover', async ({ page }) => {
|
||||
await page.goto('/findings');
|
||||
await page.waitForResponse('**/api/scores/batch');
|
||||
|
||||
const scorePill = page.locator('stella-score-pill').first();
|
||||
await scorePill.hover();
|
||||
|
||||
// Tooltip should appear with bucket name
|
||||
await expect(page.getByRole('tooltip')).toBeVisible();
|
||||
await expect(page.getByRole('tooltip')).toContainText(/act now|schedule next|investigate|watchlist/i);
|
||||
});
|
||||
|
||||
test('score pill is keyboard accessible', async ({ page }) => {
|
||||
await page.goto('/findings');
|
||||
await page.waitForResponse('**/api/scores/batch');
|
||||
|
||||
const scorePill = page.locator('stella-score-pill').first();
|
||||
await scorePill.focus();
|
||||
|
||||
// Should have focus ring
|
||||
await expect(scorePill).toBeFocused();
|
||||
|
||||
// Enter key should trigger click
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
// Score breakdown popover should appear
|
||||
await expect(page.locator('stella-score-breakdown-popover')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Score Breakdown Popover', () => {
|
||||
test('opens on score pill click and shows all dimensions', async ({ page }) => {
|
||||
await page.goto('/findings');
|
||||
await page.waitForResponse('**/api/scores/batch');
|
||||
|
||||
// Click on the first score pill
|
||||
await page.locator('stella-score-pill').first().click();
|
||||
|
||||
const popover = page.locator('stella-score-breakdown-popover');
|
||||
await expect(popover).toBeVisible();
|
||||
|
||||
// Should show all 6 dimensions
|
||||
await expect(popover.getByText('Reachability')).toBeVisible();
|
||||
await expect(popover.getByText('Runtime Signals')).toBeVisible();
|
||||
await expect(popover.getByText('Backport')).toBeVisible();
|
||||
await expect(popover.getByText('Exploitability')).toBeVisible();
|
||||
await expect(popover.getByText('Source Trust')).toBeVisible();
|
||||
await expect(popover.getByText('Mitigations')).toBeVisible();
|
||||
});
|
||||
|
||||
test('shows flags in popover', async ({ page }) => {
|
||||
await page.goto('/findings');
|
||||
await page.waitForResponse('**/api/scores/batch');
|
||||
|
||||
// Click on score with live-signal and proven-path flags
|
||||
await page.locator('stella-score-pill').filter({ hasText: '92' }).click();
|
||||
|
||||
const popover = page.locator('stella-score-breakdown-popover');
|
||||
await expect(popover).toBeVisible();
|
||||
|
||||
// Should show flag badges
|
||||
await expect(popover.locator('stella-score-badge[type="live-signal"]')).toBeVisible();
|
||||
await expect(popover.locator('stella-score-badge[type="proven-path"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('shows guardrails when applied', async ({ page }) => {
|
||||
await page.goto('/findings');
|
||||
await page.waitForResponse('**/api/scores/batch');
|
||||
|
||||
// Click on score with runtime floor applied
|
||||
await page.locator('stella-score-pill').filter({ hasText: '92' }).click();
|
||||
|
||||
const popover = page.locator('stella-score-breakdown-popover');
|
||||
await expect(popover).toBeVisible();
|
||||
|
||||
// Should show runtime floor guardrail
|
||||
await expect(popover.getByText(/runtime floor/i)).toBeVisible();
|
||||
});
|
||||
|
||||
test('closes on click outside', async ({ page }) => {
|
||||
await page.goto('/findings');
|
||||
await page.waitForResponse('**/api/scores/batch');
|
||||
|
||||
await page.locator('stella-score-pill').first().click();
|
||||
await expect(page.locator('stella-score-breakdown-popover')).toBeVisible();
|
||||
|
||||
// Click outside the popover
|
||||
await page.locator('body').click({ position: { x: 10, y: 10 } });
|
||||
|
||||
await expect(page.locator('stella-score-breakdown-popover')).toBeHidden();
|
||||
});
|
||||
|
||||
test('closes on Escape key', async ({ page }) => {
|
||||
await page.goto('/findings');
|
||||
await page.waitForResponse('**/api/scores/batch');
|
||||
|
||||
await page.locator('stella-score-pill').first().click();
|
||||
await expect(page.locator('stella-score-breakdown-popover')).toBeVisible();
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await expect(page.locator('stella-score-breakdown-popover')).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Score Badge Component', () => {
|
||||
test('displays all flag types correctly', async ({ page }) => {
|
||||
await page.goto('/findings');
|
||||
await page.waitForResponse('**/api/scores/batch');
|
||||
|
||||
// Check for live-signal badge (green)
|
||||
const liveSignalBadge = page.locator('stella-score-badge[type="live-signal"]').first();
|
||||
await expect(liveSignalBadge).toBeVisible();
|
||||
|
||||
// Check for proven-path badge (blue)
|
||||
const provenPathBadge = page.locator('stella-score-badge[type="proven-path"]').first();
|
||||
await expect(provenPathBadge).toBeVisible();
|
||||
|
||||
// Check for speculative badge (orange)
|
||||
const speculativeBadge = page.locator('stella-score-badge[type="speculative"]').first();
|
||||
await expect(speculativeBadge).toBeVisible();
|
||||
});
|
||||
|
||||
test('shows tooltip on badge hover', async ({ page }) => {
|
||||
await page.goto('/findings');
|
||||
await page.waitForResponse('**/api/scores/batch');
|
||||
|
||||
const badge = page.locator('stella-score-badge[type="live-signal"]').first();
|
||||
await badge.hover();
|
||||
|
||||
await expect(page.getByRole('tooltip')).toBeVisible();
|
||||
await expect(page.getByRole('tooltip')).toContainText(/runtime signals/i);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Findings List Score Integration', () => {
|
||||
test('loads scores automatically when findings load', async ({ page }) => {
|
||||
await page.goto('/findings');
|
||||
|
||||
// Wait for both findings and scores to load
|
||||
await page.waitForResponse('**/api/findings**');
|
||||
const scoresResponse = await page.waitForResponse('**/api/scores/batch');
|
||||
|
||||
expect(scoresResponse.ok()).toBeTruthy();
|
||||
|
||||
// All score pills should be visible
|
||||
const scorePills = page.locator('stella-score-pill');
|
||||
await expect(scorePills).toHaveCount(3);
|
||||
});
|
||||
|
||||
test('filters findings by bucket', async ({ page }) => {
|
||||
await page.goto('/findings');
|
||||
await page.waitForResponse('**/api/scores/batch');
|
||||
|
||||
// Click on Act Now filter chip
|
||||
await page.getByRole('button', { name: /act now/i }).click();
|
||||
|
||||
// Should only show Act Now findings
|
||||
const visiblePills = page.locator('stella-score-pill:visible');
|
||||
await expect(visiblePills).toHaveCount(1);
|
||||
await expect(visiblePills.first()).toContainText('92');
|
||||
});
|
||||
|
||||
test('filters findings by flag', async ({ page }) => {
|
||||
await page.goto('/findings');
|
||||
await page.waitForResponse('**/api/scores/batch');
|
||||
|
||||
// Click on Live Signal filter checkbox
|
||||
await page.getByLabel(/live signal/i).check();
|
||||
|
||||
// Should only show findings with live-signal flag
|
||||
const visibleRows = page.locator('table tbody tr:visible');
|
||||
await expect(visibleRows).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('sorts findings by score', async ({ page }) => {
|
||||
await page.goto('/findings');
|
||||
await page.waitForResponse('**/api/scores/batch');
|
||||
|
||||
// Click on Score column header to sort
|
||||
await page.getByRole('columnheader', { name: /score/i }).click();
|
||||
|
||||
// First row should have highest score
|
||||
const firstPill = page.locator('table tbody tr').first().locator('stella-score-pill');
|
||||
await expect(firstPill).toContainText('92');
|
||||
|
||||
// Click again to reverse sort
|
||||
await page.getByRole('columnheader', { name: /score/i }).click();
|
||||
|
||||
// First row should now have lowest score
|
||||
const firstPillReversed = page.locator('table tbody tr').first().locator('stella-score-pill');
|
||||
await expect(firstPillReversed).toContainText('45');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Bulk Triage View', () => {
|
||||
test('shows bucket summary cards with correct counts', async ({ page }) => {
|
||||
await page.goto('/findings/triage');
|
||||
await page.waitForResponse('**/api/scores/batch');
|
||||
|
||||
// Check bucket cards
|
||||
const actNowCard = page.locator('.bucket-card').filter({ hasText: /act now/i });
|
||||
await expect(actNowCard).toContainText('1');
|
||||
|
||||
const scheduleNextCard = page.locator('.bucket-card').filter({ hasText: /schedule next/i });
|
||||
await expect(scheduleNextCard).toContainText('1');
|
||||
|
||||
const investigateCard = page.locator('.bucket-card').filter({ hasText: /investigate/i });
|
||||
await expect(investigateCard).toContainText('1');
|
||||
});
|
||||
|
||||
test('select all in bucket selects correct findings', async ({ page }) => {
|
||||
await page.goto('/findings/triage');
|
||||
await page.waitForResponse('**/api/scores/batch');
|
||||
|
||||
// Click Select All on Act Now bucket
|
||||
const actNowCard = page.locator('.bucket-card').filter({ hasText: /act now/i });
|
||||
await actNowCard.getByRole('button', { name: /select all/i }).click();
|
||||
|
||||
// Action bar should appear with correct count
|
||||
await expect(page.locator('.action-bar.visible')).toBeVisible();
|
||||
await expect(page.locator('.selection-count')).toContainText('1');
|
||||
});
|
||||
|
||||
test('bulk acknowledge action works', async ({ page }) => {
|
||||
await page.goto('/findings/triage');
|
||||
await page.waitForResponse('**/api/scores/batch');
|
||||
|
||||
// Mock acknowledge endpoint
|
||||
await page.route('**/api/findings/acknowledge', (route) =>
|
||||
route.fulfill({ status: 200, body: JSON.stringify({ success: true }) })
|
||||
);
|
||||
|
||||
// Select a finding
|
||||
const actNowCard = page.locator('.bucket-card').filter({ hasText: /act now/i });
|
||||
await actNowCard.getByRole('button', { name: /select all/i }).click();
|
||||
|
||||
// Click acknowledge
|
||||
await page.getByRole('button', { name: /acknowledge/i }).click();
|
||||
|
||||
// Progress overlay should appear
|
||||
await expect(page.locator('.progress-overlay')).toBeVisible();
|
||||
|
||||
// Wait for completion
|
||||
await expect(page.locator('.progress-overlay')).toBeHidden({ timeout: 5000 });
|
||||
|
||||
// Selection should be cleared
|
||||
await expect(page.locator('.action-bar.visible')).toBeHidden();
|
||||
});
|
||||
|
||||
test('bulk suppress action opens modal', async ({ page }) => {
|
||||
await page.goto('/findings/triage');
|
||||
await page.waitForResponse('**/api/scores/batch');
|
||||
|
||||
// Select a finding
|
||||
const actNowCard = page.locator('.bucket-card').filter({ hasText: /act now/i });
|
||||
await actNowCard.getByRole('button', { name: /select all/i }).click();
|
||||
|
||||
// Click suppress
|
||||
await page.getByRole('button', { name: /suppress/i }).click();
|
||||
|
||||
// Modal should appear
|
||||
const modal = page.locator('.modal').filter({ hasText: /suppress/i });
|
||||
await expect(modal).toBeVisible();
|
||||
await expect(modal.getByLabel(/reason/i)).toBeVisible();
|
||||
});
|
||||
|
||||
test('bulk assign action opens modal', async ({ page }) => {
|
||||
await page.goto('/findings/triage');
|
||||
await page.waitForResponse('**/api/scores/batch');
|
||||
|
||||
// Select a finding
|
||||
const actNowCard = page.locator('.bucket-card').filter({ hasText: /act now/i });
|
||||
await actNowCard.getByRole('button', { name: /select all/i }).click();
|
||||
|
||||
// Click assign
|
||||
await page.getByRole('button', { name: /assign/i }).click();
|
||||
|
||||
// Modal should appear
|
||||
const modal = page.locator('.modal').filter({ hasText: /assign/i });
|
||||
await expect(modal).toBeVisible();
|
||||
await expect(modal.getByLabel(/assignee|email/i)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Score History Chart', () => {
|
||||
const mockHistory = [
|
||||
{ score: 65, bucket: 'Investigate', policyDigest: 'sha256:abc', calculatedAt: '2025-01-01T10:00:00Z', trigger: 'scheduled', changedFactors: [] },
|
||||
{ score: 72, bucket: 'ScheduleNext', policyDigest: 'sha256:abc', calculatedAt: '2025-01-05T10:00:00Z', trigger: 'evidence_update', changedFactors: ['xpl'] },
|
||||
{ score: 85, bucket: 'ScheduleNext', policyDigest: 'sha256:abc', calculatedAt: '2025-01-10T10:00:00Z', trigger: 'evidence_update', changedFactors: ['rts'] },
|
||||
{ score: 92, bucket: 'ActNow', policyDigest: 'sha256:abc', calculatedAt: '2025-01-14T10:00:00Z', trigger: 'evidence_update', changedFactors: ['rch'] },
|
||||
];
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.route('**/api/findings/*/history', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ entries: mockHistory }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('renders chart with data points', async ({ page }) => {
|
||||
await page.goto('/findings/CVE-2024-1234@pkg:npm/lodash@4.17.20');
|
||||
await page.waitForResponse('**/api/findings/*/history');
|
||||
|
||||
const chart = page.locator('stella-score-history-chart');
|
||||
await expect(chart).toBeVisible();
|
||||
|
||||
// Should have data points
|
||||
const dataPoints = chart.locator('.data-point, circle');
|
||||
await expect(dataPoints).toHaveCount(4);
|
||||
});
|
||||
|
||||
test('shows tooltip on data point hover', async ({ page }) => {
|
||||
await page.goto('/findings/CVE-2024-1234@pkg:npm/lodash@4.17.20');
|
||||
await page.waitForResponse('**/api/findings/*/history');
|
||||
|
||||
const chart = page.locator('stella-score-history-chart');
|
||||
const dataPoint = chart.locator('.data-point, circle').first();
|
||||
await dataPoint.hover();
|
||||
|
||||
await expect(page.locator('.chart-tooltip')).toBeVisible();
|
||||
await expect(page.locator('.chart-tooltip')).toContainText(/score/i);
|
||||
});
|
||||
|
||||
test('date range selector filters history', async ({ page }) => {
|
||||
await page.goto('/findings/CVE-2024-1234@pkg:npm/lodash@4.17.20');
|
||||
await page.waitForResponse('**/api/findings/*/history');
|
||||
|
||||
const chart = page.locator('stella-score-history-chart');
|
||||
|
||||
// Select 7 day range
|
||||
await chart.getByRole('button', { name: /7 days/i }).click();
|
||||
|
||||
// Should filter to recent entries
|
||||
const dataPoints = chart.locator('.data-point:visible, circle:visible');
|
||||
const count = await dataPoints.count();
|
||||
expect(count).toBeLessThanOrEqual(4);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Accessibility', () => {
|
||||
test('score pill has correct ARIA attributes', async ({ page }) => {
|
||||
await page.goto('/findings');
|
||||
await page.waitForResponse('**/api/scores/batch');
|
||||
|
||||
const scorePill = page.locator('stella-score-pill').first();
|
||||
await expect(scorePill).toHaveAttribute('role', 'status');
|
||||
await expect(scorePill).toHaveAttribute('aria-label', /score.*92.*act now/i);
|
||||
});
|
||||
|
||||
test('score badge has correct ARIA attributes', async ({ page }) => {
|
||||
await page.goto('/findings');
|
||||
await page.waitForResponse('**/api/scores/batch');
|
||||
|
||||
const badge = page.locator('stella-score-badge').first();
|
||||
await expect(badge).toHaveAttribute('role', 'img');
|
||||
await expect(badge).toHaveAttribute('aria-label', /.+/);
|
||||
});
|
||||
|
||||
test('bucket summary has correct ARIA label', async ({ page }) => {
|
||||
await page.goto('/findings/triage');
|
||||
await page.waitForResponse('**/api/scores/batch');
|
||||
|
||||
const bucketSummary = page.locator('.bucket-summary');
|
||||
await expect(bucketSummary).toHaveAttribute('aria-label', 'Findings by priority');
|
||||
});
|
||||
|
||||
test('action bar has toolbar role', async ({ page }) => {
|
||||
await page.goto('/findings/triage');
|
||||
await page.waitForResponse('**/api/scores/batch');
|
||||
|
||||
// Select a finding to show action bar
|
||||
const actNowCard = page.locator('.bucket-card').filter({ hasText: /act now/i });
|
||||
await actNowCard.getByRole('button', { name: /select all/i }).click();
|
||||
|
||||
const actionBar = page.locator('.action-bar');
|
||||
await expect(actionBar).toHaveAttribute('role', 'toolbar');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user