Skip to content

fix(e2e): green main — retarget stale specs + fix update-preferences flake#116

Merged
eliotlim merged 7 commits into
mainfrom
integrate/green-main-e2e
Jul 9, 2026
Merged

fix(e2e): green main — retarget stale specs + fix update-preferences flake#116
eliotlim merged 7 commits into
mainfrom
integrate/green-main-e2e

Conversation

@eliotlim

@eliotlim eliotlim commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Problem

main CI had been red for 8+ consecutive runs. The Playwright e2e job is a non-required check, so recent PRs (the AI-usage batch #111#115, the island-hydrated viewer rearch, the identity work) merged over it and the failures accumulated. Latest red run: https://github.com/eliotlim/OpenBook/actions/runs/28804748447

Recon verdict: every consistent failure is a stale/mis-scoped test trailing an intentional, correct product change — not a product regression. One was a pre-existing flake. No product code needs to change.

Board: Epic: Green main — fix failing e2e (2026-07) → T1 (stale-locator retargets) + T2 (update-preferences flake).

Solution

Test-only fixes across 6 e2e specs + one shared fixture. Why test-side, not product-side: each failure is a test asserting old behavior after the product deliberately changed (verified against source, commit-attributed):

  • account-switcher — the account email now legitimately renders in more chrome (ProfileMenu + SettingsPanel ProfileChip, since 406d174), so an unscoped getByText hit 3 nodes. Scoped the two email assertions to their [data-account-id] switcher rows (strictly stronger).
  • ai — the server now redacts provider API keys unconditionally (fix(ai): server-only provider API key + write-only settings entry #111); GET /api/ai/status returns apiKeySet:true, not the raw key. Poll asserts the presence flag.
  • block-editor / kit — block-doc "Interactive HTML" export now mounts the vendored OpenBookViewer (.obe-* DOM) instead of the retired bespoke reactive runtime (e177f38). Retargeted the offline-compute assertions to the real hydrated-viewer DOM; all numeric proofs preserved (6→18, 70/30, chart rect count 3, tick 20→5).
  • export (flake hardening) — restore now proven by the name-pinned … (imported) twin instead of a raw count delta; poll windows widened.
  • update-preferences (flake) — the real 100%-repro failure was the General-tab click racing the async shell re-init after page.reload() (not the originally-reported popover intercept). Hardened openGeneralSettings (expect(...).toPass(), reopens on transient remount) and made chooseValue/chooseLabel wait for popover teardown.

Key files: packages/web/e2e/{account-switcher,ai,block-editor,kit,export,update-preferences}.spec.ts, packages/web/e2e/fixtures.ts. New deps: none. No product/runtime code touched.

Before / After

No visual change (test-only PR) — behavioral state deltas below.

Behavior Before After
account-switcher:52 strict-mode violation: email matched 3 nodes → fail email asserted in the correct switcher row → 3 passed
ai:46 polled redacted apiKey → never equals test key → fail asserts apiKeySet:true → 8 passed
block-editor:377 asserted retired .reactive/[data-val] DOM → element not found asserts hydrated .obe-* viewer DOM, 6→18 → 1 passed
kit:236 asserted retired bespoke selectors → element not found asserts viewer DOM (formula/status/chart/number), 70/30 → 1 passed
update-preferences:21 General-tab click raced post-reload shell settle → 30s timeout resilient open + popover-teardown waits → 20/20 under --repeat-each=20
export:48 restore proven by fragile raw count → flaky proven by name-pinned imported twin → passed

Test procedure

Per-spec, on the web e2e harness (pnpm --filter @book.dev/web exec playwright test <spec> --project=chromium); the flaky ones under load:

playwright test e2e/account-switcher.spec.ts   # 3 passed
playwright test e2e/ai.spec.ts                 # 8 passed
playwright test e2e/block-editor.spec.ts:377   # 1 passed
playwright test e2e/kit.spec.ts:236            # 1 passed
playwright test e2e/export.spec.ts:48          # passed
playwright test e2e/update-preferences.spec.ts --repeat-each=5   # 20 passed

The full sharded suite runs in this PR's CI — that is the real proof main goes green (and that the shared fixtures.ts change has no blast radius across other specs).

Operational notes

  • No deploy steps, no env vars, no migrations. Reversible (test-only).
  • The shared chooseValue/chooseLabel fixture change touches every spec that opens a Select; reviewed for blast radius (the added wait is a no-op when no options are open) and proven by the full CI e2e run.
  • Follow-up (owner): once this is green, make the Playwright e2e check required on main so PRs can't merge over red e2e again — the root cause of the 8-run pileup.

Verify: T1 — typecheck/lint/unit green (sdk 171 / ui 948 / server 645), 5 specs green individually. T2 — full pnpm verify exit 0, spec 20/20. Merge is the clean union of both (7 files, 60/30), no conflicts.
Reviews: code (Quinn) — T1 cleared; T2 in review. No security/design/product gates (test-only, no trust boundary, no user-visible change).

eliotlim and others added 7 commits July 9, 2026 14:36
- account-switcher: the email now also renders in ProfileMenu +
  SettingsPanel's ProfileChip (3 nodes), so scope the two email
  assertions to their `[data-account-id]` switcher rows.
- ai: the server redacts provider API keys unconditionally (#111) —
  `GET /api/ai/status` returns `providers.claude.apiKeySet: true`, not
  the raw key. Poll `apiKeySet` → `true` and widen the inline type.

Test-only; product behaviour is intended and correct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KFk2T9k3p7ghCjdzMfkA5w
Block-doc HTML export now mounts the island-hydrated OpenBookViewer
(PresentBlocks, the real block renderer, locked-but-interactive) over
the static body; the bespoke `.reactive`/`[data-val]`/`.slider` runtime
was retired (e177f38). Await `.obe-present-blocks` (present only after
hydration) before asserting, then read the live kit DOM:

- block-editor:377 — `.expr [data-val]` → `.obe-formula-out`;
  `.slider input` → `.obe-kit-slider input[type=range]`.
- kit:236 — `.reactive.expr [data-val]` → `.obe-formula-out`;
  `.kitlight[data-status]` → `.obe-kit-status[data-status]`;
  `[data-chart] svg rect/text` → `.obe-chart-svg rect/text`;
  the number input is `.obe-kit-number .obe-kit-stepper input` (the
  `number` block renders a stepper, not a range). Numeric expectations
  (6→18, 70/30, rect count 3, ticks 20→5) unchanged.

Test-only; product behaviour is intended and correct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KFk2T9k3p7ghCjdzMfkA5w
Prove the restore by the name-suffixed twin ("Backup Spec Page
(imported)") rather than a raw page-count delta, and widen both polls to
20s so a slow whole-workspace restore on a long-lived dev server no
longer flakes the too-tight window.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KFk2T9k3p7ghCjdzMfkA5w
The custom Select opens its listbox side=bottom, directly over whatever
sits below the trigger. A caller that interacts with that control next
(e.g. the "Security updates only" toggle under the Updates cadence
Select) could land on the still-mounted listbox and loop until timeout.
Wait for `[role=option]` to reach count 0 after picking — the Select
closes without an exit animation, so this resolves immediately in the
normal case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KFk2T9k3p7ghCjdzMfkA5w
…store

The updates spec reopens settings after page.reload() and flaked under
load: the shell re-initialises async (NavigationProvider resolves the
page + writes the URL, DocumentArea flips from its loading null-render to
content), and a dialog opened inside that transition is torn back down,
detaching the settings tab buttons mid-click. Reliably reproduced 20/20
under `--repeat-each=20`.

Retry the open with expect(...).toPass() until the General panel is
actually up and stays up. Test-only; the Updates product code path is
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KFk2T9k3p7ghCjdzMfkA5w
@vercel

vercel Bot commented Jul 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
app.book.pub Ready Ready Preview, Comment Jul 9, 2026 6:59am

Request Review

@eliotlim eliotlim merged commit 80ac04f into main Jul 9, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant