Keep working on a page behind its searching window
Summary
On a public bid-information site built of framesets, pressing 「検索」 runs top.openSubWindow(); document.frm.submit(): it writes a small 「通信中ダイアログ」 window ("ただいまサーバと通信中です。しばらくお待ち下さい。") and posts the search into a frame; the results' onload calls top.closeSubWindow(). When the answer took longer than the 5 s an action waits for its page to settle, every operation after the search worked on that window:
[5/10] act "Click the 検索 link" → click (completed, 7.9s)
[6/10] expect "The page shows a list of search results of bid projects"
→ The page only displays a "communicating with server" dialog (ただいまサーバと通信中です), not a list of bid project search results. (skipped)
error: browser: do[6] expect failed: expectation not met: The page only displays a "communicating with the server, please wait" dialog, not a list of bid project search results.
(live run on main, with a local proxy delaying the site's answers by 6 s). With a local copy of the page: a when was skipped, an extract returned {"heading":""} / {"projects":[]}, and the next act failed with "no element on the page matches the instruction".
Root cause
The browser runtime's active page is Chrome's focused tab, and a window a page opens takes focus. Every engine call took the page from there (e.page()), and act/observe left the page to the runtime, which does the same. #3432 made the settle wait read the page in front on each check and wait while any frame loads, but only for settleWait (5 s). An answer that outlasts it leaves the "searching" window in front, and the expect, extract, act, and outline that follow read it. The window has a text field (its animated indicator), so "a window without controls" would not tell it apart from a real dialog.
The rule
Compared with the 地域選択 window #3432 handles right (openChiikisentakuSubWindow() → window.open(...EjCCJ000060c.jsp), checkboxes plus 登録, which calls opener.setChikiSentaku(...) and window.close()), the difference is that the searching window comes with a document load in its opener and closes when that load ends. So:
- A window is held when the last action (act, replay, upload, goto) both opened it and made its opener load a document (a frame's or the main frame's, cross-origin frames included). While held, operations work on the opener.
- It stays held, through later actions too, while the opener loads and for
settleQuiet(0.5 s) after. Then it is let go for good, even if the opener later begins another load of its own after a quiet gap. A window still open at that point is a real page, and operations work on it. - The action's settle keeps waiting while a window is held, up to the action's own timeout, not past it.
- A window opened without a load in its opener (地域選択, a
target=_blanklink) is the page in front at once, as before.
No site-specific checks. Window openers and per-page load start/stop times come from the existing FrameLoadWatcher (Target.attachedToTarget's openerId, Page.frameStartedLoading/frameStoppedLoading), attributed to the page through cross-origin frame sessions.
Changes
browserhost.FrameLoadWatcher:Windows()lists open windows whose opener is open, andPageLoad(targetID)reports whether a page's frames load and when the latest load began and ended.stagehandEngine.page()maps a held window to its opener.Act,Replay, andObservepass that page to the runtime instead of letting it pick the active page. A browser that reports no active page returns an error instead of a nil page.settle/settleDocumenttake the action's deadline and wait while a window is held.Goto,Act,Replay, andUploadpassstart + timeout. Bot-check settles pass none.- Spec 072 describes the rule.
Tests
Each fails on main and passes here unless noted:
TestStagehandWorksBehindASearchingWindow: a written-in-place "searching" window plus a post into a frame answered after 1.5 s, withsettleWaitat 200 ms. The snapshot, page text,extract, andactafter the search work on the results. Onmainall four read the window.TestStagehandSearchingWindowWaitEndsWithTimeout: the answer is held back until the test releases it. A press with a 1 s timeout returns withintimeout + callTimeoutSlack. The snapshot after it shows the page that searched, and a second press on that page works while the window is still up. The results show once released.TestStagehandHeldWindowLetGoAfterItsLoad: a details window held by a timed-out press stays open past its load. When the frame later refreshes itself, two reads in a row get the details window, not the opener.TestStagehandWorksInAWindowThatStays(guard, passes on both): a regions window with checkboxes and Register is the page to work on, and Register gives the region back. A details window that opens while its opener loads becomes the page once the load ends.TestFrameLoadWatcherTellsWindows: windows and their openers, page loads including cross-origin frames, began/ended times, and closing.- Conformance
TestBrowserSearchingWindow(searching_window.yaml) uses the real binary and the real 5 s settle, with the answer after 7 s: choose region → tick → Register → Search →when/statementexpect→extract→acton the results → fixedexpect. Onmainthewhenis skipped, theextractreads"", and theactfails.
go test -race ./internal/browserhost/... ./internal/runtime/builtin/browser/..., conformance spec072_browser and spec079_browser_session, make fmt, and make check all pass.
Live check
Built binary against the live site, with DeepSeek V4.1 Flash via OpenRouter:
- Session: 詳細切替 → 地域選択 opens the regions window as the page (60 checkboxes, 登録) → tick 千葉市 → 登録 puts 千葉市 in the search form → 検索 →
expectholds ("34件 … 千葉市") →extractreads the count and the first three projects. browser.runthrough a proxy that delays the site's answers by 6 s, so the search answer outlasts the settle. Onmainit fails as above. With the final commit the 検索 act takes 15.8 s, the fixedexpectand the statementexpecthold, and theextractreturns{"count":"208件","first_projects":["農業大学校機械化研修運転コース擁壁改修工事", …]}.