feat(web): close sprint 006 onboarding ux

This commit is contained in:
master
2026-04-01 03:59:48 +03:00
parent 1d7c8fadbd
commit 07f7cd91b0
60 changed files with 6247 additions and 983 deletions

View File

@@ -0,0 +1,221 @@
import { test, expect } from './fixtures/auth.fixture';
import type { Page, Route } from '@playwright/test';
function collectCriticalErrors(page: Page): string[] {
const errors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') {
errors.push(msg.text());
}
});
page.on('pageerror', (error) => {
errors.push(error.message);
});
return errors;
}
function expectNoCriticalErrors(errors: string[]): void {
const critical = errors.filter((error) => /NG0|TypeError|ReferenceError/.test(error));
expect(critical, critical.join('\n')).toHaveLength(0);
}
async function go(page: Page, path: string): Promise<void> {
await page.goto(path, { waitUntil: 'networkidle', timeout: 60_000 });
await page.waitForLoadState('domcontentloaded');
}
async function stubIntegrationCounts(page: Page): Promise<void> {
await page.route('**/v1/integrations**', async (route: Route) => {
const requestUrl = new URL(route.request().url());
const type = Number.parseInt(requestUrl.searchParams.get('type') ?? '0', 10);
const counts: Record<number, number> = {
1: 2,
2: 1,
3: 0,
4: 1,
5: 4,
6: 3,
};
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
items: [],
totalCount: counts[type] ?? 0,
page: 1,
pageSize: 1,
totalPages: 1,
}),
});
});
}
async function openCommandPalette(page: Page): Promise<void> {
await page.evaluate(() => {
document.dispatchEvent(new KeyboardEvent('keydown', {
key: 'k',
ctrlKey: true,
bubbles: true,
}));
});
await expect(page.locator('.cp__input')).toBeVisible();
}
test.describe('Onboarding UX sprint closure', () => {
test('dashboard shows first-visit guidance, metric hints, status chip tooltips, and sidebar recommendation', async ({ authenticatedPage: page }) => {
const errors = collectCriticalErrors(page);
await go(page, '/');
await page.evaluate(() => {
localStorage.removeItem('stellaops.helper.preferences');
});
await page.reload({ waitUntil: 'networkidle' });
const banner = page.locator('[aria-label="Dashboard welcome"]');
await expect(banner).toBeVisible();
await expect(banner).toContainText('Welcome to Stella Ops');
await expect(banner).toContainText('Severity guide');
await expect(banner).toContainText('Run diagnostics');
await expect(page.getByRole('button', { name: /SBOM: show glossary definition/i })).toBeVisible();
await expect(page.getByRole('button', { name: /Feed: show glossary definition/i })).toBeVisible();
await expect(page.locator('app-live-event-stream-chip a')).toHaveAttribute('title', /Events:/);
await expect(page.locator('app-policy-baseline-chip a')).toHaveAttribute('title', /Policy:/);
await expect(page.locator('app-evidence-mode-chip a')).toHaveAttribute('title', /Evidence:/);
await expect(page.locator('app-feed-snapshot-chip a')).toHaveAttribute('title', /Feed:/);
await expect(page.locator('app-offline-status-chip a')).toHaveAttribute('title', /Offline:/);
const sidebar = page.locator('app-sidebar');
await expect(sidebar).toContainText('Start here');
await expect(sidebar).toContainText('Diagnostics');
await page.getByRole('button', { name: /dismiss dashboard welcome/i }).click();
await page.reload({ waitUntil: 'networkidle' });
await expect(banner).toBeHidden();
expectNoCriticalErrors(errors);
});
test('page help panels explain routes, persist state, and lead into docs routes', async ({ authenticatedPage: page }) => {
const errors = collectCriticalErrors(page);
await go(page, '/security/reachability');
const panel = page.locator('[data-page-help="reachability"]');
await expect(panel).toBeVisible();
await expect(panel).toContainText('About this page');
await expect(panel).toContainText('Reachability');
await expect(panel).toContainText('Key concepts');
await expect(panel).toContainText('Coverage is confidence');
await expect(panel.locator('.page-help__body')).toBeVisible();
await panel.locator('.page-help__toggle').click();
await expect(panel.locator('.page-help__body')).toHaveCount(0);
await page.reload({ waitUntil: 'networkidle' });
await expect(panel.locator('.page-help__toggle')).toHaveAttribute('aria-expanded', 'false');
await panel.locator('.page-help__toggle').click();
await expect(panel.locator('.page-help__body')).toBeVisible();
await panel.getByRole('link', { name: 'Operator guide' }).click();
await expect(page).toHaveURL(/\/docs\/UI_GUIDE\.md$/);
await expect(page.locator('.docs-viewer__header')).toContainText('Documentation');
await expect(page.locator('.docs-viewer__path')).toContainText('UI_GUIDE.md');
const docsPanel = page.locator('[data-page-help="default"]');
await expect(docsPanel).toBeVisible();
await expect(docsPanel).toContainText('Documentation');
await expect(docsPanel).toContainText('Operator guide');
expectNoCriticalErrors(errors);
});
test('vex page exposes glossary-enhanced help content for the hardest concepts', async ({ authenticatedPage: page }) => {
const errors = collectCriticalErrors(page);
await go(page, '/ops/policy/vex');
const panel = page.locator('[data-page-help="vex"]');
await expect(panel).toBeVisible();
await expect(panel).toContainText('VEX and Exceptions');
await expect(panel).toContainText('Quick mental model');
const glossaryButton = page.getByRole('button', { name: /VEX: show glossary definition/i }).first();
await glossaryButton.hover();
await glossaryButton.focus();
const tooltip = page.locator('.glossary-tooltip');
await expect(tooltip).toBeVisible();
await expect(tooltip).toContainText(/VEX|vulnerability/i);
expectNoCriticalErrors(errors);
});
test('decision capsules and findings explorer explain capsules and baselines inline', async ({ authenticatedPage: page }) => {
const errors = collectCriticalErrors(page);
await go(page, '/evidence/capsules');
const capsulesPanel = page.locator('[data-page-help="decision-capsules"]');
await expect(capsulesPanel).toBeVisible();
await expect(capsulesPanel).toContainText('Decision Capsules');
await expect(capsulesPanel).toContainText('What is inside a capsule');
await expect(capsulesPanel).toContainText('Replay and verification');
await go(page, '/security/findings');
const findingsPanel = page.locator('[data-page-help="findings"]');
await expect(findingsPanel).toBeVisible();
await expect(findingsPanel).toContainText('Findings Explorer');
await expect(findingsPanel).toContainText('Baselines reduce noise');
await expect(findingsPanel).toContainText('Baseline workflow');
expectNoCriticalErrors(errors);
});
test('integrations hub shows guided setup order with live done and pending state', async ({ authenticatedPage: page }) => {
const errors = collectCriticalErrors(page);
await stubIntegrationCounts(page);
await go(page, '/setup/integrations');
const setupOrder = page.getByRole('region', { name: 'Suggested Setup Order' });
await expect(page.getByRole('heading', { name: 'Suggested Setup Order' })).toBeVisible();
await expect(page.locator('.hub-summary')).toContainText('14');
await expect(setupOrder.getByRole('link', { name: 'Source Control', exact: true })).toBeVisible();
await expect(setupOrder).toContainText(/Fresh advisory and VEX sources are what make security posture/i);
await expect(page.locator('.setup-step-card__status--done')).toHaveCount(4);
await expect(page.locator('.setup-step-card__status--pending')).toHaveCount(1);
await expect(page.locator('.activity')).toContainText('Use the activity timeline for connector event history');
expectNoCriticalErrors(errors);
});
test('command palette serves glossary help and guided workflows inline', async ({ authenticatedPage: page }) => {
const errors = collectCriticalErrors(page);
await go(page, '/');
await openCommandPalette(page);
const input = page.locator('.cp__input');
const dialog = page.locator('.cp__dialog');
await input.fill('help: sbom');
await expect(dialog).toContainText('Help & Guides');
await expect(dialog).toContainText('Help: SBOM');
await expect(dialog).toContainText('A list of all the parts');
await input.fill('guide: scan image');
await expect(dialog).toContainText('Guide: Scan Image');
await expect(dialog).toContainText('Step 1');
await expect(dialog).toContainText('Step 2');
await input.fill('guide: first setup');
await page.getByRole('button', { name: /Guide: First Setup/i }).click();
await expect(page).toHaveURL(/\/setup-wizard\/wizard$/);
expectNoCriticalErrors(errors);
});
});