fix(e2e): drive Claude model selector with trusted clicks + assert-closed (#501)#504
Conversation
…osed (#501) selectClaudeModel() opened the model menu and selected a radio with synthetic `page.evaluate(() => el.click())` clicks, then closed it with `keyboard.press("Escape")`. Synthetic clicks dispatch without the microtask checkpoints a trusted click gets, which desyncs Claude's Radix-style dismissable-layer open/close bookkeeping: the menu content closed but its full-page backdrop was stranded over the whole page, blocking the subsequent `page.click("#saypi-callButton")` ("could not click call button") and recording transcript=false/reply=false for a claude.ai run where SayPi itself was never exercised. Re-running with --claude-model=keep (which skips this function) produced a fully clean run — pinning the fault to this step, not the extension. Same class of bug as the #494 voice-menu race. Fix: use Playwright's trusted locator.click() for both the open and the radio select (via getByRole('menuitemradio').filter(...)), and replace the bare Escape with an explicit assert-closed guard — waitForFunction until no [role='menuitemradio'] remain, with a trusted Escape fallback if the menu lingers. The page's interactivity is now a verified post-condition of the function, satisfying the acceptance criterion. Fixes #501 Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_013L2TkKx2c5z9gcuCRpMq5d
|
Real-host verification (Layer 4 CDP, Model selection succeeded and the turn completed end-to-end: |
…enu content (#501) Review follow-up: the assert-closed guard only checked that no [role='menuitemradio'] remained — that verifies the menu *content* is gone, not that the dismissable layer/backdrop is. Those are exactly the two states #501 can desync. Require BOTH content-gone AND the trigger reporting collapsed (aria-expanded !== "true"), which distinguishes a clean close from a stranded-backdrop-but-content-removed page, and correct the comment to describe what's actually verified. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_013L2TkKx2c5z9gcuCRpMq5d
|
Independent reviewer verdict (subagent): APPROVE — the trusted-click change correctly addresses the #501 root cause with no regressions; the API usage (v1.60.0), the null/graceful-degradation contract, and the The reviewer raised one non-blocking finding: the assert-closed guard only checked Addressed in f7b1125: the guard now requires both content-gone and the trigger reporting collapsed ( Re-verified on the real host with the strengthened guard ( The stricter |
Why
On a 2026-07-05 host sweep,
selectClaudeModel()left claude.ai in a stuck state: a full-page gray overlay with an empty floating-menu remnant persisted from model selection through the rest of the run, blockingpage.click("#saypi-callButton")("could not click call button") and recordingtranscript=false reply=falsefor a run where SayPi itself was never exercised. Immediately re-running with--claude-model=keep(which skips this function) produced a fully clean run — pinning the fault to this step, not the extension.Root cause
selectClaudeModel()opened the menu and selected the radio with syntheticpage.evaluate(() => el.click())clicks, then closed withkeyboard.press("Escape"). Synthetic clicks dispatch without the microtask checkpoints a trusted click gets, which desyncs Claude's Radix-style dismissable-layer open/close bookkeeping — the menu content closes but its full-page backdrop is stranded over everything. Same class of bug as the #494 voice-menu race. The distinguishing factor vsselectSaypiVoice(unaffected) is the close path: model used a bareEscape; voice re-clicks its toggle.Fix
locator.click()(getByRole('menuitemradio').filter({ hasText }).filter({ hasNotText: /currently unavailable/i })), never syntheticevaluate(el.click()).Escapewith an explicit assert-closed guard:waitForFunctionuntil no[role='menuitemradio']remain, with a trustedEscapefallback if the menu lingers.The page's interactivity is now a verified post-condition of the function, satisfying the issue's acceptance criterion.
Scope / tests
Test-harness script only (
scripts/e2e-host-sweep.mjs); no product code.node --checkparses; full merge gate green locally (204 files). True verification is a real claude sweep run — see PR comments for result.Fixes #501
🤖 Generated with Claude Code