From 19e176a32930e5012fad7bbbd3ea32b1a13675c4 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 13 Jul 2026 21:14:52 +0800 Subject: [PATCH 1/3] ci: make production-journey UI regression blocking, isolate mockup specs (PT-05) The advisory UI regression job could stay red while pr-required stayed green, letting production-journey failures normalise. Split Playwright chromium into a production-journey project and a chromium-mockups project (prototype /mockups specs), promote ui-regression into pr-required (same ui_changed gating as ui-critical so merge-queue runs are covered), and keep mockups advisory in a new ui-mockups job. firefox/webkit release-matrix coverage is unchanged. Proof for the promotion: production-journey suite 135/135 locally after hardening the one flake - the cross-mode quick-link click, where the app-router URL only commits once /medications/[slug] responds and a cold dev compile takes ~30s when the @critical warm-up journeys are excluded; the spec now re-clicks inside a 45s toPass window. PT-17 (scheduled live-domain monitoring) is deliberately not included: it targets production and needs explicit approval. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 64 +++++++++++++++++++++++++++++++++++----- package.json | 3 +- playwright.config.ts | 16 ++++++++++ tests/ui-smoke.spec.ts | 12 ++++++-- 4 files changed, 85 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b6ad8f81..1fa57e93a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -246,11 +246,13 @@ jobs: playwright-report/ if-no-files-found: ignore + # Production-journey regression suite. Blocking (PT-05): a red production + # journey must fail pr-required instead of normalising as an advisory red. + # Prototype /mockups specs run separately in ui-mockups below. ui-regression: - name: Advisory UI regression + name: UI regression needs: changes - if: github.event_name == 'pull_request' && needs.changes.outputs.ui_changed == 'true' - continue-on-error: true + if: needs.changes.outputs.ui_changed == 'true' runs-on: ubuntu-24.04 timeout-minutes: 45 steps: @@ -278,14 +280,59 @@ jobs: - name: Install Chromium browser run: npx playwright install --with-deps chromium - - name: Chromium advisory regression + - name: Chromium production-journey regression run: npm run test:e2e:advisory - - name: Upload advisory UI diagnostics + - name: Upload UI regression diagnostics + if: failure() + uses: actions/upload-artifact@v7 + with: + name: ui-regression-diagnostics-${{ github.run_id }} + path: | + test-results/ + playwright-report/ + if-no-files-found: ignore + + ui-mockups: + name: Advisory mockup UI + needs: changes + if: github.event_name == 'pull_request' && needs.changes.outputs.ui_changed == 'true' + continue-on-error: true + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version-file: ".nvmrc" + cache: npm + cache-dependency-path: package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Restore Chromium browser cache + uses: actions/cache@v6 + with: + path: ~/.cache/ms-playwright + key: playwright-chromium-${{ runner.os }}-${{ hashFiles('package-lock.json') }} + + - name: Install Chromium browser + run: npx playwright install --with-deps chromium + + - name: Chromium mockup regression + run: npm run test:e2e:mockups + + - name: Upload mockup UI diagnostics if: failure() uses: actions/upload-artifact@v7 with: - name: advisory-ui-diagnostics-${{ github.run_id }} + name: mockup-ui-diagnostics-${{ github.run_id }} path: | test-results/ playwright-report/ @@ -341,7 +388,7 @@ jobs: pr-required: name: PR required - needs: [changes, static-pr, safety, coverage, build, ui-critical, db-reset-verify] + needs: [changes, static-pr, safety, coverage, build, ui-critical, ui-regression, db-reset-verify] if: always() runs-on: ubuntu-24.04 timeout-minutes: 5 @@ -359,6 +406,7 @@ jobs: COVERAGE_RESULT: ${{ needs.coverage.result }} BUILD_RESULT: ${{ needs.build.result }} UI_RESULT: ${{ needs.ui-critical.result }} + UI_REGRESSION_RESULT: ${{ needs.ui-regression.result }} DB_RESULT: ${{ needs.db-reset-verify.result }} run: | set -euo pipefail @@ -404,8 +452,10 @@ jobs: if [ "$UI_CHANGED" = "true" ]; then require_success "ui-critical" "$UI_RESULT" + require_success "ui-regression" "$UI_REGRESSION_RESULT" else require_skipped_or_success "ui-critical" "$UI_RESULT" + require_skipped_or_success "ui-regression" "$UI_REGRESSION_RESULT" fi if [ "$DB_CHANGED" = "true" ]; then diff --git a/package.json b/package.json index b34baaebf..1da823933 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "test:e2e:accessibility": "node scripts/run-playwright.mjs tests/ui-accessibility.spec.ts --project=chromium", "test:e2e:critical": "node scripts/run-playwright.mjs --project=chromium --grep @critical", "test:e2e:advisory": "node scripts/run-playwright.mjs --project=chromium --grep-invert @critical", - "test:e2e:chromium": "node scripts/run-playwright.mjs --project=chromium", + "test:e2e:mockups": "node scripts/run-playwright.mjs --project=chromium-mockups", + "test:e2e:chromium": "node scripts/run-playwright.mjs --project=chromium --project=chromium-mockups", "test:e2e:visual": "node scripts/run-playwright.mjs --config=playwright.visual.config.ts", "verify:cheap": "npm run check:runtime && npm run check:github-actions && npm run sitemap:check && npm run brand:check && npm run check:type-scale && npm run check:icon-scale && npm run lint && npm run typecheck && npm run test", "verify:pr-local": "node scripts/verify-pr-local.mjs", diff --git a/playwright.config.ts b/playwright.config.ts index 7ac060372..52a8c3965 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -7,6 +7,13 @@ const baseURL = getPlaywrightBaseUrl(); // browser downloads; point this at that binary instead of the managed one. const chromiumExecutablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH; +// Prototype /mockups journeys live in their own advisory project so a red +// mockup can never mask a production-journey regression (PT-05). The two +// patterns must stay disjoint and their union must equal the legacy testMatch +// below (which the firefox/webkit release-matrix projects keep using). +const productionSpecPattern = /.*ui-(smoke|stress|accessibility|tools|overlap|universal-search)\.spec\.ts/; +const mockupSpecPattern = /.*ui-tools-(collapse|task-directory)\.spec\.ts/; + export default defineConfig({ testDir: "./tests", testMatch: @@ -27,6 +34,15 @@ export default defineConfig({ projects: [ { name: "chromium", + testMatch: productionSpecPattern, + use: { + ...devices["Desktop Chrome"], + ...(chromiumExecutablePath ? { launchOptions: { executablePath: chromiumExecutablePath } } : {}), + }, + }, + { + name: "chromium-mockups", + testMatch: mockupSpecPattern, use: { ...devices["Desktop Chrome"], ...(chromiumExecutablePath ? { launchOptions: { executablePath: chromiumExecutablePath } } : {}), diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index e5276e459..b9daacec9 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -1840,8 +1840,16 @@ test.describe("Clinical KB UI smoke coverage", () => { const medicationLink = strip.getByRole("link", { name: "Clozapine", exact: true }); await expect(medicationLink).toHaveAttribute("href", "/medications/clozapine"); - await medicationLink.click(); - await expect(page).toHaveURL(/\/medications\/clozapine/, { timeout: 15_000 }); + // Re-click on each retry: a single click can be swallowed while the answer + // surface is still hydrating (same guard as the launcher mode switches), and + // app-router URLs only commit after the destination responds — a cold dev + // compile of /medications/[slug] can take ~30s when this spec runs without + // the @critical warm-up journeys. + await expect(async () => { + if (/\/medications\/clozapine/.test(page.url())) return; + await medicationLink.click(); + await expect(page).toHaveURL(/\/medications\/clozapine/, { timeout: 5_000 }); + }).toPass({ timeout: 45_000 }); await expectNoPageHorizontalOverflow(page); }); From 99ae1f0c124f4afec0fccf2cca02e347ae0f9577 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 13 Jul 2026 23:57:30 +0800 Subject: [PATCH 2/3] fix(ci): isolate all mockup browser journeys --- playwright.config.ts | 9 ++++++--- tests/ui-smoke.spec.ts | 3 --- tests/ui-tools-collapse.spec.ts | 2 +- tests/ui-tools-task-directory.spec.ts | 2 +- tests/ui-tools.spec.ts | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/playwright.config.ts b/playwright.config.ts index 52a8c3965..94b7f97c5 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -9,10 +9,11 @@ const chromiumExecutablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH; // Prototype /mockups journeys live in their own advisory project so a red // mockup can never mask a production-journey regression (PT-05). The two -// patterns must stay disjoint and their union must equal the legacy testMatch -// below (which the firefox/webkit release-matrix projects keep using). +// Tag-level filters keep production and prototype journeys disjoint even when +// they share a spec file; firefox/webkit retain the legacy full testMatch. const productionSpecPattern = /.*ui-(smoke|stress|accessibility|tools|overlap|universal-search)\.spec\.ts/; -const mockupSpecPattern = /.*ui-tools-(collapse|task-directory)\.spec\.ts/; +const mockupSpecPattern = /.*ui-(tools|tools-collapse|tools-task-directory)\.spec\.ts/; +const mockupTag = /@mockup/; export default defineConfig({ testDir: "./tests", @@ -35,6 +36,7 @@ export default defineConfig({ { name: "chromium", testMatch: productionSpecPattern, + grepInvert: mockupTag, use: { ...devices["Desktop Chrome"], ...(chromiumExecutablePath ? { launchOptions: { executablePath: chromiumExecutablePath } } : {}), @@ -43,6 +45,7 @@ export default defineConfig({ { name: "chromium-mockups", testMatch: mockupSpecPattern, + grep: mockupTag, use: { ...devices["Desktop Chrome"], ...(chromiumExecutablePath ? { launchOptions: { executablePath: chromiumExecutablePath } } : {}), diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index 1c01f8cc9..3c23f3023 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -2405,9 +2405,6 @@ test.describe("Clinical KB UI smoke coverage", () => { await expectSingleMedicationPage(page); await expect(page.getByRole("link", { name: "Back to medication search" })).toBeVisible(); - await gotoApp(page, "/mockups/medication-prescribing"); - await expect(page).toHaveURL(/\/medications\/acamprosate$/); - await expectSingleMedicationPage(page); expect(parentNodeErrors).toEqual([]); }); diff --git a/tests/ui-tools-collapse.spec.ts b/tests/ui-tools-collapse.spec.ts index b174aea09..2799f2df8 100644 --- a/tests/ui-tools-collapse.spec.ts +++ b/tests/ui-tools-collapse.spec.ts @@ -9,7 +9,7 @@ async function goto(page: Page, path: string) { await page.waitForLoadState("networkidle", { timeout: 15_000 }).catch(() => undefined); } -test.describe("Tools mockups collapse the primary region when filtering", () => { +test.describe("Tools mockups collapse the primary region when filtering @mockup", () => { test.describe.configure({ timeout: 60_000 }); test("command center hides Start here and avoids a populated grid over an empty state", async ({ page }) => { diff --git a/tests/ui-tools-task-directory.spec.ts b/tests/ui-tools-task-directory.spec.ts index 774966516..fe01d6731 100644 --- a/tests/ui-tools-task-directory.spec.ts +++ b/tests/ui-tools-task-directory.spec.ts @@ -18,7 +18,7 @@ async function expectNoHorizontalOverflow(page: Page) { expect(overflow).toBeLessThanOrEqual(2); } -test.describe("Tools task directory mockup (Concept 4)", () => { +test.describe("Tools task directory mockup (Concept 4) @mockup", () => { test.describe.configure({ timeout: 60_000 }); test("renders task-grouped rows and is reachable", async ({ page }) => { diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index 55801e9e3..b9747545a 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -1511,7 +1511,7 @@ test.describe("Responsive layout guards", () => { ] as const; for (const route of mockupRoutes) { - test(`${route.name} never overflows horizontally across sizes`, async ({ page }) => { + test(`${route.name} never overflows horizontally across sizes @mockup`, async ({ page }) => { await page.setViewportSize({ width: 1280, height: 900 }); await gotoLauncher(page, route.path); await expect(page.getByRole("heading", { level: 1 }).first()).toBeVisible(); From 0318502a5aa0ef011609c32dc7d7936ad1c6464e Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Tue, 14 Jul 2026 00:22:54 +0800 Subject: [PATCH 3/3] ci: pin mockup workflow actions --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 14c16a5ae..de4ecd4f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,13 +41,13 @@ jobs: changed_files: ${{ steps.scope.outputs.changed_files }} steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@9f698171ed81b15d1823a05fc7211befd50c8ae0 # v6.0.3 with: fetch-depth: 0 persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: node-version-file: ".nvmrc" @@ -228,7 +228,7 @@ jobs: run: npm ci - name: Restore Chromium browser cache - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: ~/.cache/ms-playwright key: playwright-chromium-${{ runner.os }}-${{ hashFiles('package-lock.json') }} @@ -241,7 +241,7 @@ jobs: - name: Upload UI diagnostics if: failure() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: critical-ui-diagnostics-${{ github.run_id }} path: |