Skip to content

Harden e2e setup/teardown cleanup and scope the BCV nav helper to the top toolbar - #167

Merged
alex-rawlings-yyc merged 11 commits into
mainfrom
e2e-page-error-diagnostics
Jul 29, 2026
Merged

Harden e2e setup/teardown cleanup and scope the BCV nav helper to the top toolbar#167
alex-rawlings-yyc merged 11 commits into
mainfrom
e2e-page-error-diagnostics

Conversation

@alex-rawlings-yyc

@alex-rawlings-yyc alex-rawlings-yyc commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Three follow-ups to #169, plus one diagnostics improvement. E2e-harness only; no
production/extension code changes. None of this changes pass/fail behavior on a healthy run.

Scope the BCV nav helper to the top toolbar's control

#169 forces platform.interfaceMode: 'power' for every e2e launch, and Power mode is exactly what
brings a second kind of BCV control into existence: paranext-core renders a per-tab
BookChapterControl in each web view's own tab toolbar (web-view.component.tsx, gated on
isPowerMode) alongside the top toolbar's, and every instance carries the same
aria-label="book-chapter-trigger". navigateToScriptureRef looked the trigger up page-wide and
took .first(), so which control it drives now depends on the toolbar happening to render before
the dock layout in document order.

The two are not interchangeable. Only the toolbar's control is passed a disabled prop (bound to
whether BCV navigation has a resolved target web view); the per-tab controls receive no such prop
and are therefore always enabled. If the lookup ever resolved to one of those, the helper's
toBeEnabled wait would become vacuous — passing even with no navigation target resolved.

The trigger is now scoped to the toolbar's own toolbar-reserved-space-wrapper root, with
.first() kept only as a strict-mode guard within that single scope. The helper's JSDoc and its
failure message are corrected too: both still described simple mode as the default, which #169 made
false.

Make setup and teardown cleanup failure-safe

Playwright skips globalTeardown when globalSetup throws. #169 added CDP setup failure-path
cleanup, but its try/catch begins at the readiness waits, so everything acquired before them still
leaked on failure.

  • The settings seed, the spawn, and the PID-marker write now sit inside that same try, with the
    cleanup extracted as cleanUpFailedLaunch() — callable at any point after the seed, since an
    absent PID simply skips the kill.
  • The app log fd is closed in a finally, so a failed spawn leaves no leaked descriptor.
  • A failed user-data marker write removes the temp dir it just created — otherwise teardown has no
    reference to find it by — and stops the dev server the bootstrap started, since that point is past
    bootstrapRendererDevServer's own cleanup but before the main try/catch.
  • The app-exit signal is registered before the PID-marker write, so a write failure still reaches
    cleanup with a usable exit signal instead of silently skipping the post-SIGKILL wait.
  • bootstrapRendererDevServer kills a dev server it started that never became ready, rather than
    leaving it to survive the failed run, hold port 1212, and silently serve a stale bundle to every
    later run.
  • killProcessFromPidFile removes the PID marker independently of the kill (removePidFile), so an
    fs error there can no longer report a successful kill as killed: false — which would skip the
    caller's wait for that PID to exit and walk straight into a live SingletonLock.
  • preConfigureSettings rolls back a partial seed when backupAndSeedSettings throws, so a mid-seed
    filesystem failure can't strand a developer's real settings in a seeded state.

Describe page errors in full

newPlatformError returns a plain object literal, not an Error. When one surfaces as an unhandled
rejection, Playwright's pageerror hands us an Error whose .message has collapsed to a useless
husk — which is why the CI log showed Page error: Object with the actual cause nowhere in it.

describePageError() combines an error's message, its stack (which often carries the true text for a
wrapped throwable), and a JSON dump of own-enumerable properties, skipping the empty cases a real
Error produces. The smoke fixture's pageerror logging uses it.

withFatalStartupTripwire matches against that full description rather than .message. To be clear
about what this is worth: it is defensive, not a fix for a known miss. The theme-settle error the
tripwire targets is rejected by AsyncVariable with a plain string, so its text does reach
.message today and the tripwire already matches it. The change costs nothing and covers a future
fatal signal that arrives wrapped.

Documentation

  • withFatalStartupTripwire records why the boot-time PlatformError rejections (e.g. the
    platform.getOSPlatform -32601 race) must stay log-only: promoting them to fatal signals was tried
    and reverted after CI showed that launches emitting them usually recover, so tripping on a
    per-launch-recurring signal exhausted the retry budget and turned flaky-but-green runs into hard
    failures.
  • backupAndSeedSettings documents its failure conditions, including that a partial seed may need
    rolling back by the caller.
  • SETTINGS_BACKUP_FILE records why one backup file shared across both tiers is safe (workers: 1,
    fullyParallel: false, and the tiers run sequentially) and what would break it.

This change is Reviewable

Summary by CodeRabbit

  • Tests
    • Improved automated Electron E2E startup diagnostics with clearer page-error reporting and better failure fast-paths.
    • Strengthened readiness checks for CDP and the renderer dev server to fail with more actionable logs.
    • Enhanced cleanup on setup/teardown failures to reduce leaked processes, marker files, and stale runtime artifacts between runs.
    • Added stronger safeguards to seed and restore test settings, preventing configuration leakage across runs.
    • Improved navigation verification by aligning toolbar controls with the expected layout.

@alex-rawlings-yyc alex-rawlings-yyc added 🟩Low Low-priority PR up next Auto adds an issue to the PT Lexical Extensions project labels Jul 16, 2026
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

The E2E harness now isolates settings around Electron launches, improves page-error diagnostics and readiness checks, and coordinates renderer, Electron, and user-data cleanup through shared process utilities.

Changes

E2E harness lifecycle

Layer / File(s) Summary
Harness primitives and settings isolation
.gitignore, e2e-tests/process-utils.ts, e2e-tests/fixtures/helpers.ts
Adds settings backup/restore, richer page-error descriptions, project metadata polling, process-exit waiting, PID polling, and retryable directory removal.
Fixture lifecycle and readiness flow
e2e-tests/fixtures/app.fixture.ts, e2e-tests/fixtures/helpers.ts
Applies and restores E2E settings around fixtures, improves startup diagnostics, waits for project metadata, and supports Home-based layouts.
CDP startup and readiness gates
e2e-tests/global-setup-cdp.ts
Seeds settings before launch, waits for interlinearizer activation and project metadata, and cleans up failed or warm-instance setup state.
PID tracking and teardown integration
e2e-tests/global-setup.ts, e2e-tests/global-teardown.ts, e2e-tests/global-teardown-cdp.ts
Centralizes the renderer PID path and uses structured kill results, exit polling, retryable removal, and settings restoration during teardown.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TestHarness
  participant Electron
  participant ProjectLookupService
  participant Renderer
  participant Cleanup
  TestHarness->>Electron: seed settings and launch
  Electron->>Renderer: activate extension and settle layout
  TestHarness->>ProjectLookupService: poll project metadata
  ProjectLookupService-->>TestHarness: return installed projects
  TestHarness->>Cleanup: restore settings and remove artifacts
  Cleanup->>Renderer: wait for process exit
  Cleanup->>Electron: remove isolated user-data directory
Loading

Possibly related PRs

Suggested reviewers: imnasnainaec

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main changes: harder e2e setup/teardown cleanup and scoping the BCV navigation helper to the top toolbar.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch e2e-page-error-diagnostics

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@alex-rawlings-yyc alex-rawlings-yyc self-assigned this Jul 27, 2026
@alex-rawlings-yyc alex-rawlings-yyc changed the title Log real e2e renderer page errors instead of the flattened "Object" husk Match the e2e startup tripwire against the full page-error description Jul 27, 2026
@alex-rawlings-yyc
alex-rawlings-yyc force-pushed the e2e-page-error-diagnostics branch from 7f22b73 to 7ed5e0b Compare July 27, 2026 16:35
coderabbitai[bot]

This comment was marked as outdated.

@alex-rawlings-yyc
alex-rawlings-yyc marked this pull request as ready for review July 27, 2026 17:27
@alex-rawlings-yyc

This comment was marked as outdated.

@alex-rawlings-yyc alex-rawlings-yyc left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alex-rawlings-yyc resolved 2 discussions.
Reviewable status: 0 of 8 files reviewed, all discussions resolved (waiting on alex-rawlings-yyc).

@coderabbitai

This comment was marked as outdated.

@alex-rawlings-yyc
alex-rawlings-yyc force-pushed the e2e-page-error-diagnostics branch 3 times, most recently from 48d9e06 to c917ddb Compare July 27, 2026 19:57
@alex-rawlings-yyc
alex-rawlings-yyc force-pushed the e2e-page-error-diagnostics branch from d7ccbe3 to c5d7f45 Compare July 27, 2026 22:00

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
e2e-tests/global-setup.ts (1)

219-266: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

PID-file write happens before the failure-cleanup try, leaking the dev server on write failure.

fs.writeFileSync(DEV_SERVER_PID_FILE, ...) (Lines 231-233) runs before the try block that calls killSpawnedDevServer on failure. writeFileSync throws synchronously on error (e.g. EACCES/ENOENT), so if it fails, the spawn already succeeded but the function exits without ever reaching the cleanup path — leaking exactly the orphaned dev-server process this PR is meant to prevent.

The sibling globalSetupCdp handles this same class of failure explicitly by wrapping its analogous write in its own try/catch with cleanup, so there's established precedent for guarding it here too.

🐛 Proposed fix: move the PID-file write inside the try block
-    if (devServer.pid) {
-      fs.writeFileSync(DEV_SERVER_PID_FILE, String(devServer.pid));
-    }
-
     // From here on this run owns a spawned, detached dev server. Playwright skips globalTeardown
     // when globalSetup throws, so a readiness failure below must kill it on the spot or it survives
     // the failed run, holds port 1212, and silently serves a stale bundle to every later run.
     // Scoped to this branch only: the already-running branch above didn't start the server, so this
     // run must leave it alone.
     try {
+      if (devServer.pid) {
+        fs.writeFileSync(DEV_SERVER_PID_FILE, String(devServer.pid));
+      }
       console.log(`Waiting for renderer dev server on port ${RENDERER_PORT}...`);
       await waitForPort(RENDERER_PORT, 60_000);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e-tests/global-setup.ts` around lines 219 - 266, Move the
DEV_SERVER_PID_FILE write and related renderer readiness operations into the try
block that owns cleanup, so synchronous fs.writeFileSync failures also invoke
killSpawnedDevServer(devServer.pid) before rethrowing. Keep the existing
detached-server setup and readiness behavior unchanged.
e2e-tests/global-setup-cdp.ts (1)

358-377: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Unconditional settings restore + marker cleanup on port reuse risks clobbering a concurrently-running owner.

clearStaleOwnershipMarkers() now also calls restoreSeededSettings() (new in this PR) whenever globalSetupCdp decides to reuse an already-running instance on CDP_PORT (Line 91-102). The code assumes that instance is always a stale leftover from a crashed prior launched run, but the same code path fires just as readily when the port is held by a developer's active npm run start:cdp session or by another concurrently-executing test run. In either case, restoring settings.json out from under a live owner (or deleting CDP_PID_FILE/CDP_USER_DATA_FILE it still needs) can corrupt its settings state or break its own teardown.

Consider gating this cleanup on actual ownership evidence — e.g. a per-run token recorded alongside the PID marker, or at minimum verifying the PID in CDP_PID_FILE is no longer alive — before restoring settings or removing markers, rather than acting unconditionally whenever the port happens to be occupied.

Based on learnings, "assume cleanup can be triggered by local concurrent runs or stale-marker recovery... implement a setup-established per-run ownership token and require it in teardown to guard: process termination, user-data cleanup, marker deletion, and settings backup restoration—so teardown only performs cleanup when the current run owns the resources/markers."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e-tests/global-setup-cdp.ts` around lines 358 - 377, Make ownership cleanup
conditional in clearStaleOwnershipMarkers and globalTeardownCdp by recording a
setup-established per-run ownership token and validating it before restoring
seeded settings, terminating processes, deleting user data, or removing marker
files. Preserve warm-instance reuse without modifying resources belonging to a
developer session or another concurrent test run, and only perform teardown
cleanup when the current run’s token matches the recorded ownership metadata.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@e2e-tests/global-setup-cdp.ts`:
- Around line 358-377: Make ownership cleanup conditional in
clearStaleOwnershipMarkers and globalTeardownCdp by recording a
setup-established per-run ownership token and validating it before restoring
seeded settings, terminating processes, deleting user data, or removing marker
files. Preserve warm-instance reuse without modifying resources belonging to a
developer session or another concurrent test run, and only perform teardown
cleanup when the current run’s token matches the recorded ownership metadata.

In `@e2e-tests/global-setup.ts`:
- Around line 219-266: Move the DEV_SERVER_PID_FILE write and related renderer
readiness operations into the try block that owns cleanup, so synchronous
fs.writeFileSync failures also invoke killSpawnedDevServer(devServer.pid) before
rethrowing. Keep the existing detached-server setup and readiness behavior
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6e973eff-d82d-45d6-bd5d-06ba69e48d6c

📥 Commits

Reviewing files that changed from the base of the PR and between 7ed5e0b and 26ec4b9.

📒 Files selected for processing (5)
  • e2e-tests/fixtures/app.fixture.ts
  • e2e-tests/fixtures/helpers.ts
  • e2e-tests/global-setup-cdp.ts
  • e2e-tests/global-setup.ts
  • e2e-tests/global-teardown.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • e2e-tests/fixtures/app.fixture.ts

@alex-rawlings-yyc
alex-rawlings-yyc force-pushed the e2e-page-error-diagnostics branch 3 times, most recently from bccfa24 to 7c6823e Compare July 28, 2026 19:59
alex-rawlings-yyc and others added 7 commits July 28, 2026 14:23
Playwright wraps a non-Error value thrown in the renderer into a bare
`Error` whose `.message` collapses to something useless like "Object",
keeping the real text only in the stack. `withFatalStartupTripwire`
matched FATAL_STARTUP_PAGE_ERROR against `.message` alone, so a
theme-settle failure arriving that way slipped past the tripwire and cost
the launch its whole readiness budget instead of fast-failing.

Add `describePageError()`, which combines the message, the stack, and a
JSON dump of own-enumerable properties, and match the tripwire against
that. The smoke fixture's `pageerror` logging uses it too, so the CI log
shows the real error rather than "Page error: Object".

Also record why the boot-time PlatformError rejections must stay
log-only: promoting them to fatal signals was tried and reverted after CI
showed launches that emit them usually recover.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Seeding and launch now run inside the CDP setup's guarded path so a
failure can't leak settings or the user-data dir, PID-marker removal no
longer masks a successful kill, and the CDP teardown's tail always runs.
Also drop stale simple-mode wording now that the suite forces Power
mode.
Playwright skips globalTeardown when globalSetup throws, so a dev server
spawned here but stuck before readiness survived the failed run, holding
port 1212 and serving a stale bundle to later runs.
A throw from the CDP_PID_FILE write left `exited` undefined, so
cleanUpFailedLaunch skipped its post-SIGKILL wait and removed the
user-data dir while the app still held it.
Power mode renders a per-tab BookChapterControl with the same aria-label
and no disabled prop, so a page-wide .first() relied on render order and
would have made the enabled-wait vacuous.
@alex-rawlings-yyc alex-rawlings-yyc changed the title Match the e2e startup tripwire against the full page-error description Harden e2e setup/teardown cleanup and scope the BCV nav helper to the top toolbar Jul 28, 2026
@alex-rawlings-yyc
alex-rawlings-yyc force-pushed the e2e-page-error-diagnostics branch from 7c6823e to 1e1da28 Compare July 28, 2026 20:58
coderabbitai[bot]

This comment was marked as outdated.

alex-rawlings-yyc and others added 2 commits July 28, 2026 15:36
A failed spawn reports itself by emitting 'error' asynchronously, and an
unhandled 'error' on an EventEmitter throws as an uncaughtException — outside
the try/catch, so cleanUpFailedLaunch never ran and the seeded settings were
left in place. Race a listener-backed rejection against the readiness waits in
both setups instead.

Creating the isolated user-data dir also sat outside its cleanup try, so a
failure there left the just-started renderer dev server running.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The marker is only written when a run spawns the server, so one left on
disk here belongs to a prior run — killing by it could hit a process the
OS has since recycled that PID onto.
@imnasnainaec

Copy link
Copy Markdown
Contributor

Ideas (subset of a Devin-inspired, Claude generated list)

1. Give navigateToScriptureRef a clear failure when its scope root is missinge2e-tests/fixtures/helpers.ts:1455

The scoping itself is verified correct: toolbar-reserved-space-wrapper exists on the toolbar root (paranext-core/src/renderer/components/platform-bible-toolbar.tsx:338) and wraps the toolbar's BookChapterControl, and the JSDoc's premise holds — in Power mode web-view.component.tsx:528-540 renders a per-tab BookChapterControl in the renderer main frame (not the iframe) with no disabled prop, so it is always enabled and a page-wide locator really would be both ambiguous and vacuous.

The residual is diagnostics: if that testid is renamed upstream the locator resolves to nothing and the failure reads "the top toolbar's book-chapter control never became enabled … the WEB Scripture Editor is likely closed", pointing at the wrong cause. An expect(page.getByTestId('toolbar-reserved-space-wrapper')).toBeAttached() (or a count check) before the enabled-wait converts that into "toolbar root not found — testid renamed in paranext-core?".

2. Drop the duplicated message inside describePageErrore2e-tests/fixtures/helpers.ts:95-96

For a normal Error, err.stack already begins with the message line, so the description carries the text twice in every CI log line. Pushing err.message only when !err.stack?.startsWith(...) (or when there's no stack) keeps the non-Error-throwable coverage the function exists for without the redundancy. Harmless for the tripwire either way — the regex has no g flag.

3. Enforce the single-backup-file invariant instead of only documenting ite2e-tests/fixtures/helpers.ts:198-213

The branch documents that one shared SETTINGS_BACKUP_FILE across both tiers is race-free only because both configs are workers: 1/fullyParallel: false and run sequentially. Cheap hardening: derive the filename per tier, or assert the worker count at seed time, so a future config change fails loudly rather than interleaving a restore with another tier's startup read.

4. Reconcile the two post-SIGKILL exit-wait budgetse2e-tests/fixtures/helpers.ts:487 (3s) vs e2e-tests/global-setup-cdp.ts:281 (1s)

Both tiers restore settings only after killing the app, and both are correct today because SIGKILL leaves no opportunity for an exit-time settings flush — the ordering is what makes them safe, not the wait length. Worth a shared constant or a one-line note on why the budgets differ, since the two paths guard the same file.

A truncated backup would be restored over the developer's real settings,
and the
self-heal restore reads it unconditionally. Also name a duplicated test
id as a
cause in the toolbar-root failure message.

@imnasnainaec imnasnainaec left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(My approval is conditional on the e2e checks passing.)

@imnasnainaec reviewed 8 files and all commit messages, and made 1 comment.
Reviewable status: :shipit: complete! all files reviewed, all discussions resolved (waiting on alex-rawlings-yyc).

@alex-rawlings-yyc
alex-rawlings-yyc merged commit 401e8be into main Jul 29, 2026
10 checks passed
@alex-rawlings-yyc
alex-rawlings-yyc deleted the e2e-page-error-diagnostics branch July 29, 2026 21:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🟩Low Low-priority PR up next Auto adds an issue to the PT Lexical Extensions project

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants