mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-08 23:28:51 +00:00
76e9380389
The "Frontend E2E (informational)" workflow has been red on every push since Phase 8 (commita9e229b) shipped TEST-H1+H2. The workflow's own header acknowledges this is non-blocking: "The job is intentionally NOT in the merge gate. It runs on every push to surface flakiness early; merge eligibility comes from ci.yml's existing gates (Vitest, lint, build, the 34 CI guards)." But the red badge on every commit is noise. Two ground-truthed root causes (NOT regressions from any recent commit): (1) NO BACKEND IN CI. playwright.config.ts:48-53 only spins up `npm run dev` (Vite frontend). The Vite dev-server proxy forwards /api/v1/* and /health to a backend that doesn't exist in the CI environment → ECONNREFUSED flood throughout the run log. 6 specs need backend data to drive AuthGate bootstrap / lazy palette mount / settings reload: - 01-login-redirect (3 tests): all 3 depend on AuthGate deciding to redirect to /login, which requires /api/v1/auth/info to resolve - 02-dashboard-shell (2 of 4): the palette tests need the Dashboard page to hydrate past loading state → React.lazy palette chunk only mounts after backend data lands - 03-settings-timestamp-pref (1 of 3): the reload+persist test calls page.reload() which re-runs AuthProvider's 4-endpoint bootstrap (2) NO VISUAL-REGRESSION BASELINES COMMITTED. 04-visual- regression.spec.ts uses Playwright `toHaveScreenshot()` against PNG baselines that don't exist (`find web/src/__tests__/e2e -name '*.png'` returns 0). First-run = "snapshot doesn't exist, writing actual" = expected fail. The e2e.yml workflow exposes an `update_snapshots` dispatch input for the controlled first-run pass, but on default push runs that flag is false → tests fail. Operator choice (2026-05-14): "skip backend-dependent specs" over spinning up backend in CI (1-2 days of CI engineering, premature per the e2e.yml comment's "do not promote to required-for-merge in this phase" guidance) or dropping the e2e job from push triggers entirely (loses early-flakiness signal). ═══════════════════════════ CHANGES ═══════════════════════════════ web/src/__tests__/e2e/01-login-redirect.spec.ts: describe-level test.skip(NEEDS_BACKEND, '...') guard. All 3 tests in this file depend on AuthGate. web/src/__tests__/e2e/02-dashboard-shell.spec.ts: Per-test test.skip(NEEDS_BACKEND, '...') on the 2 palette tests (47, 59). Sidebar IA test (31) and breadcrumb test (70) stay ungated — both passed in CI today because they don't depend on Dashboard data resolving. web/src/__tests__/e2e/03-settings-timestamp-pref.spec.ts: Per-test test.skip(NEEDS_BACKEND, '...') on the reload+persist test (39). Card-render (28) and invalid-IANA-fallback (54) tests stay ungated — both passed. web/src/__tests__/e2e/04-visual-regression.spec.ts: describe-level skip guard. All 5 tests need both backend AND committed baselines; neither exists in CI today. The workflow_ dispatch update_snapshots input is the controlled-update path when both prereqs land. Skip condition is `!process.env.CERTCTL_E2E_BACKEND_URL && !!process.env.CI`: • In CI without a backend → skip • Locally where operator runs `make demo` + `npm run e2e` → no CI env var, so skip evaluates false → all tests run • In CI WITH a backend set via CERTCTL_E2E_BACKEND_URL env → tests run; this is the path the e2e.yml's "next steps" will use when backend-in-CI infra lands ═══════════════════════════ AUDIT FRAMING ════════════════════════ This is honest signal, not test deletion: • 11 tests don't run in CI today; they're SKIPPED with a clear operator-facing reason and an env-var unlock path. • The 5 tests that DO run in CI today (sidebar IA, breadcrumb, timestamp card render, invalid-IANA fallback, smoke "login renders brand") continue to run and protect the no-backend- needed surface. • The "1-2 weeks of green runs" promotion criterion in e2e.yml's header is now achievable for the no-backend subset. ═══════════════════════════ VERIFICATION ═══════════════════════════ • npx tsc --noEmit — exit 0 • Visual diff of skip-guard patterns across 4 files — consistent NEEDS_BACKEND const + test.skip(...) + operator-facing reason • Falsifiable proof: the next push's e2e workflow run should show 5 passing + 11 skipped + 0 failed; exit 0; informational job goes from RED to GREEN. Ground-truth: origin/master tip7268d12(FE-M6 just pushed) verified via GitHub API BEFORE commit.
93 lines
4.6 KiB
TypeScript
93 lines
4.6 KiB
TypeScript
// 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 <Breadcrumbs /> which derives
|
|
// the trail from useLocation(). Top-level pages get "Home / <Label>".
|
|
const nav = page.getByRole('navigation', { name: /breadcrumb/i });
|
|
await expect(nav).toBeVisible();
|
|
await expect(nav).toContainText(/Home/);
|
|
await expect(nav).toContainText(/Issuers/);
|
|
});
|
|
});
|