fix(test): browser skip path, dialog listener detach, launcher lifecycle and locking - #2934
Conversation
…cle and locking Addresses 8 browser-test-infra findings from the 2026-06-09 framework review (test-infra:1,2,3,5,8,9,10,13): - browserDescribe() aroundEach now skips spec bodies entirely when browserTestSkipped is set, removing the 30 hand-written per-spec 'if (this.browserTestSkipped) return;' guards (a forgotten guard used to hit the UnwiredBrowserGuard sentinel and fail in CI). - $clearDialogListener() now detaches the Consumer<Dialog> listener via page.offDialog() — onDialog() is additive, not one-shot, so a second dialog in the same it block was handled by the stale first listener. Docblock corrected. - Deleted the dead $jarPath/$verifyInstall pair (only callers were their own unit tests; $verifyInstall pointed at the never-existent 'wheels browser:install'). Standardized the remaining install-guidance strings on the canonical 'wheels browser setup'. - Extracted $waitOptions() to deduplicate the WaitForOptions construction triplicated across waitFor/waitForText/waitForUrl; a custom timeout on a launcher-less client now throws Wheels.BrowserTimeoutUnavailable instead of silently falling back to 30s. - onApplicationEnd (public/Application.cfc + app template) now releases the application-scoped BrowserLauncher so applicationStop() reload cycles no longer orphan the headless browser, node driver process, and JAR handles; corrected the false afterAll() scope-clear comment. - acquireBrowser() now mirrors $ensureLauncher's double-check locking and probes isConnected() on cache hits, evicting and relaunching when a crashed browser would otherwise poison the application-scoped cache. - $startBrowserContext() throws Wheels.BrowserTest.NotWired when no Browser was acquired (beforeAll() overridden without super.beforeAll()) instead of the cryptic string-method error. Verified on Lucee 7 + SQLite (worktree docker single-area run): wheelstest area 151 pass / 0 fail / 0 error; new specs confirmed red against the pre-fix framework code; applicationStop() reload exercised post-change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
There was a problem hiding this comment.
Wheels Bot — Reviewer A
TL;DR: This PR fixes eight browser-test-infrastructure findings from the June 2026 framework review. The logic is sound, the cross-engine patterns are correctly applied, and the test coverage is targeted and clear. Approving with two minor nits — neither blocks merge.
Correctness
All eight T-findings are correctly addressed:
-
T1 (
aroundEachskip path): Before the fix,arguments.spec.body()was unconditionally called before the early return, making the "skip" branch a no-op that relied on each spec's hand-written guard. After:aroundEachreturns before touchingspec.body(). The newBrowserDescribeSkipSpecself-test is clever — athrowin the body is the proof of failure; silence (no exception) is the proof of correctness. -
T2 (dialog listener leak):
$clearDialogListener()now callsvariables.page.offDialog(variables.$dialogProxy)with the same proxy instance registered in$registerDialogListener, so successive armed interactions in oneitblock no longer stack stale listeners. -
T5 (
$waitOptionsrefactor): Extracting the triplicated option-building into a single helper and turning the silent-fallback intoWheels.BrowserTimeoutUnavailableis a real improvement. One minor test gap noted below. -
T8/T9/T10/T13: The
onApplicationEndrelease block, double-check locking inacquireBrowser(),Wheels.BrowserTest.NotWiredguard in$startBrowserContext(), andisConnected()liveness probe all look correct.
Cross-engine
BoxLang invariant #11 ("local.X in catch doesn't persist") is correctly handled. The catch-block state in the new BrowserTestNotWiredSpec additions uses the shared-struct pattern:
// vendor/wheels/tests/specs/wheelstest/BrowserTestNotWiredSpec.cfc (added ~lines 475-486)
var state = {message: ""};
try {
spec.$startBrowserContext();
} catch (Wheels.BrowserTest.NotWired e) {
state.message = e.message; // struct field, not local.X — correct for BoxLang
}$liveCachedBrowser assigns alive inside try (not catch), so invariant #11 does not apply there. No inline closures as constructor named args; no obj["key"]() inside closures. The named lock wheelsBrowserLauncherAcquire and its double-check pattern in acquireBrowser() mirror the established $ensureLauncher prior art in BrowserTest.cfc. All good.
Tests
Coverage is strong. One minor gap:
Nit 1 — waitForText not tested for BrowserTimeoutUnavailable.
BrowserIntegrationSpec adds specs for waitFor and waitForUrl with a custom timeout on a launcher-less client, but not waitForText, which routes through the same $waitOptions helper. Not a blocking gap (the path is structurally identical), but a third case would complete T5 coverage:
// vendor/wheels/tests/specs/wheelstest/BrowserIntegrationSpec.cfc — suggested addition
it("waitForText() with a custom timeout but no launcher surfaces BrowserTimeoutUnavailable", () => {
var c = new wheels.wheelstest.BrowserClient()
.init(baseUrl="http://localhost");
expect(() => {
c.waitForText(text="never", seconds=5);
}).toThrow(type="Wheels.BrowserTimeoutUnavailable");
});Conventions
Nit 2 — Inline comment blocks in Application.cfc exceed one line.
CLAUDE.md says "one short line max" for inline comments. The four-line block added to both public/Application.cfc:119 and the template copy is justified (the "why" is genuinely non-obvious — CFML lacks destructors), but it could be condensed to something like:
// Release browser launcher on reload — CFML has no destructors; without this,
// applicationStop() cycles orphan the headless browser, node driver, and JAR handles.Worth a follow-up tightening if the codebase enforces the one-line norm consistently.
Docs
Changelog entry intentionally omitted pending campaign consolidation — acknowledged in the PR body. No user-facing behavior changes (test infrastructure only), so no guides update is needed. The examples/ app divergence is called out explicitly and flagged as a follow-up candidate; acceptable for this PR's scope.
Commits
Single commit: fix(test): browser skip path, dialog listener detach, launcher lifecycle and locking — valid type, valid scope, 84-char header (under 100). Clean.
Wheels Bot — Reviewer B (round 1) |
Wheels Bot — Reviewer B (round 1)A's review is accurate and well-grounded. I re-scanned the diff and the cited lines; no sycophancy, no false positives, no missed issues. Both nits are real findings. Aligning on approve. SycophancyNone detected. The approval cites specific evidence for each of the 8 findings rather than a blanket LGTM. False positivesNone detected.
Missed issuesNone detected. The one gap A identified --
Verdict alignmentAPPROVED is consistent with the findings: two non-blocking nits, browser test infrastructure only, no user-facing behavior changes, cross-engine patterns correctly applied. ConvergenceAligned. A's review is accurate and the approve verdict is correct. The |
Summary
Fixes 8 browser-test-infrastructure findings from the 2026-06-09 framework review (package
test-browser-lifecycle): thebrowserDescribe()skip path now actually skips spec bodies (removing 30 hand-written per-spec guards), dialog listeners are detached from the Playwright Page after each armed action, the application-scopedBrowserLauncheris released ononApplicationEnd(no more orphaned headless browser/driver processes across reloads),acquireBrowser()gains double-check locking plus anisConnected()liveness probe,$startBrowserContext()throws a descriptiveWheels.BrowserTest.NotWirederror, the triplicatedWaitForOptionsconstruction is extracted to$waitOptions()(surfacing the silent custom-timeout fallback), and the dead$jarPath/$verifyInstallpair pointing at a nonexistent CLI command is deleted.Findings addressed
browserDescribeskip path still executes spec bodies, forcing duplicated skip guards in every browser spec @vendor/wheels/wheelstest/BrowserTest.cfc:143—aroundEachnow returns without callingarguments.spec.body()whenbrowserTestSkippedis set; all 30if (this.browserTestSkipped) return;guards removed acrossBrowserLoginSpec,BrowserRouteSpec,BrowserTestLifecycleSpec, andBrowserIntegrationSpec.vendor/wheels/wheelstest/BrowserClient.cfc:1014—$clearDialogListener()now callspage.offDialog()with the identical proxy instance registered by$registerDialogListener(identity-based removal), invoked fromfinallyblocks inclick/press/keys; docblock corrected.$jarPath/$verifyInstallpair tells users to run a CLI command that does not exist @vendor/wheels/wheelstest/BrowserLauncher.cfc(formerly :98-114) — pair deleted (zero callers outside their own unit tests); remaining install-guidance strings standardized on the canonicalwheels browser setup.WaitForOptionsconstruction triplicated acrosswaitFor/waitForText/waitForUrlwith silent timeout fallback @vendor/wheels/wheelstest/BrowserClient.cfc:259— extracted$waitOptions(); a custom timeout on a launcher-less client now throwsWheels.BrowserTimeoutUnavailableinstead of silently falling back to 30s.BrowserLauncher.release()is never called for the application-scoped launcher — browser/driver processes and JAR handles leak across reloads @public/Application.cfc:118andcli/lucli/templates/app/public/Application.cfc:107—onApplicationEndreleasesapplication.$wheelsBrowserLauncher(key matches$ensureLauncher), soapplicationStop()reload cycles no longer orphan the headless browser, node driver, and JAR file handles; the falseafterAll()scope-clear comment corrected.acquireBrowser()check-then-act on shared application-scoped state has no lock @vendor/wheels/wheelstest/BrowserLauncher.cfc:203— now mirrors$ensureLauncher's double-check locking (named lockwheelsBrowserLauncherAcquire).$startBrowserContext()callsnewContext()on an unwired empty-string$browserwhensuper.beforeAll()is skipped @vendor/wheels/wheelstest/BrowserTest.cfc:172— throwsWheels.BrowserTest.NotWiredwith actionable guidance instead of a cryptic string-method error.acquireBrowser()returns the cached Browser without a liveness check — a crashed browser poisons the application-scoped cache @vendor/wheels/wheelstest/BrowserLauncher.cfc:229— cache hits probeisConnected()and evict/relaunch when dead.Findings verified already-fixed
None — all 8 packaged findings reproduced against
origin/developand required fixes. The review report's line references for this package were spot-checked (T1, T3) and confirmed accurate against develop. One report nit: T3's claim thatwheels browser:install"never existed" is slightly off — the legacy CommandBox CLI atcli/src/commands/wheels/browser/install.cfcdefines it — but standardizing on the canonicalwheels browser setup(present incli/lucli/Module.cfc) is correct regardless.Source
Internal multi-agent framework review 2026-06-09, wave 2, package
test-browser-lifecycle.Tests
vendor/wheels/tests/specs/wheelstest/BrowserDescribeSkipSpec.cfc(T1 — fails pre-fix: the old skip branch still calledspec.body()outside try/catch) andvendor/wheels/tests/specs/wheelstest/BrowserTestNotWiredSpec.cfc(T10 — fails pre-fix with the cryptic string-method error).BrowserDialogSpec.cfc(T2 second-dialog-in-same-it coverage),BrowserIntegrationSpec.cfc(T5 launcher-less custom-timeout throw),BrowserLauncherSpec.cfc(T3 dead-pair removal, T9/T13 locking + liveness).wheelstestarea 151 pass / 0 fail / 0 error; new specs confirmed red against pre-fix framework code;applicationStop()reload exercised post-change. Full engine x database matrix deferred to CI (the real gate).Cross-engine notes
local.Xincatchdoes not persist on BoxLang).##escapes correct; no literal CFML tag text in strings.private $launchBrowseris safe —BrowserLauncheris directly instantiated, never mixin-integrated.wheelsBrowserLauncherAcquireis server-global, so independent launcher instances serialize launches — harmless, and portable across Lucee/Adobe/BoxLang.examples/starter-appandexamples/tweetretain the oldonApplicationEndwithout the release block — acceptable drift for demo apps, candidate for a follow-up sync.Changelog
Entry deliberately omitted; consolidated at campaign end.
🤖 Generated with Claude Code