// Copyright 2026 certctl LLC. All rights reserved.
// SPDX-License-Identifier: BUSL-1.1
//
// Phase 8 TEST-H1 closure — Priority Flow 2.
//
// Flow: authenticated operator lands on /dashboard → sidebar renders
// the 7 Phase 3 IA groups → cmd+k opens the command palette → search
// → result navigates → breadcrumb trail updates.
//
// This is the IA contract Phase 3 (UX-H1 + UX-H6 + UX-M5) shipped.
// If a future commit breaks the sidebar grouping, the palette, or
// the breadcrumb rendering, this spec screams.
//
// Happy + error pair:
// (a) happy: open palette → type "issuers" → press Enter → /issuers
// (b) error: open palette → type gibberish that won't match → "No results"
import { test, expect } from '@playwright/test';
test.describe('Priority Flow 2 — dashboard shell + cmd+k palette', () => {
// Bypass the API-key form by setting the operator's preference in
// localStorage before the page boots. Real CI would seed a session
// cookie via API; for the dev-server path, demo-mode auth covers it.
test.beforeEach(async ({ page }) => {
await page.context().addInitScript(() => {
// Demo-mode AuthProvider treats absence of an api key + a 200
// /api/v1/auth/me as the synthetic admin — see CLAUDE.md.
});
});
test('sidebar renders the Phase 3 IA groups in canonical order', async ({ page }) => {
await page.goto('/');
// Phase 3 UX-H1 closure: 7 semantic groups — Inventory / Trust /
// Delivery / People / Notify / Access / Audit. The group headers
// are the visible labels; the test pins their presence + order.
const sidebar = page.locator('aside');
await expect(sidebar).toBeVisible();
// Each group has a header element with the group label. Looser
// assertion than DOM-order so a future row-reshuffle within a
// group doesn't fail — we only pin the group-level structure.
const groups = ['Inventory', 'Trust', 'Delivery', 'People', 'Notify', 'Access', 'Audit'];
for (const g of groups) {
await expect(sidebar.getByRole('button', { name: new RegExp(`^${g}`, 'i') })).toBeVisible();
}
});
// Hotfix #17 (2026-05-14): the cmd+k palette mounts via React.lazy().
// Its chunk only loads after the Dashboard page hydrates past first
// paint, which requires backend data (/api/v1/auth/info,
// /api/v1/stats/summary, etc). With no backend in CI the page stays
// in loading state and the palette never mounts → these two specs
// fail with "combobox not visible." Sidebar + breadcrumb specs in
// this same file PASS in CI because they don't depend on backend
// data resolving. Skip just the palette pair; re-enable once CI
// grows a backend (see e2e.yml header's next-steps block).
const NEEDS_BACKEND = !process.env.CERTCTL_E2E_BACKEND_URL && !!process.env.CI;
test('happy: cmd+k opens palette, search routes to /issuers', async ({ page }) => {
test.skip(NEEDS_BACKEND, 'requires backend in CI (Hotfix #17); palette is lazy-loaded after first dashboard paint');
await page.goto('/');
// Phase 3 UX-H6: meta+k OR ctrl+k opens the palette.
await page.keyboard.press('Control+K');
// The palette mounts via React.lazy(); wait for it to render.
const palette = page.getByRole('combobox', { name: /command palette|search|find/i });
await expect(palette).toBeVisible({ timeout: 5_000 });
await palette.fill('Issuers');
await page.keyboard.press('Enter');
await expect(page).toHaveURL(/\/issuers/, { timeout: 5_000 });
});
test('error: palette with no-match query surfaces "No results"', async ({ page }) => {
test.skip(NEEDS_BACKEND, 'requires backend in CI (Hotfix #17); palette is lazy-loaded after first dashboard paint');
await page.goto('/');
await page.keyboard.press('Control+K');
const palette = page.getByRole('combobox', { name: /command palette|search|find/i });
await expect(palette).toBeVisible({ timeout: 5_000 });
// cmdk's default empty state text — overridable but the Phase 3
// CommandPalette uses the cmdk default.
await palette.fill('zzzzz-no-such-thing-xxxxx');
await expect(page.getByText(/no results/i)).toBeVisible({ timeout: 3_000 });
});
test('breadcrumb trail updates on detail-page navigation (UX-M5)', async ({ page }) => {
await page.goto('/issuers');
// Phase 3 UX-M5: PageHeader renders which derives
// the trail from useLocation(). Top-level pages get "Home /