Keep working on a page behind its searching window

Merged
YO
10d ago
+734-57

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=_blank link) 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, and PageLoad(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, and Observe pass 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/settleDocument take the action's deadline and wait while a window is held. Goto, Act, Replay, and Upload pass start + 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, with settleWait at 200 ms. The snapshot, page text, extract, and act after the search work on the results. On main all four read the window.
  • TestStagehandSearchingWindowWaitEndsWithTimeout: the answer is held back until the test releases it. A press with a 1 s timeout returns within timeout + 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/statement expect → extract → act on the results → fixed expect. On main the when is skipped, the extract reads "", and the act fails.

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 → 検索 → expect holds ("34件 … 千葉市") → extract reads the count and the first three projects.
  • browser.run through a proxy that delays the site's answers by 6 s, so the search answer outlasts the settle. On main it fails as above. With the final commit the 検索 act takes 15.8 s, the fixed expect and the statement expect hold, and the extract returns {"count":"208件","first_projects":["農業大学校機械化研修運転コース擁壁改修工事", …]}.