Repository navigation
Track execution contexts across navigations and wait for a frame's next one - #33
Merged
Merged
Conversation
…xt one An evaluation straight after a navigation could fail with "Cannot find context with specified id". The page recorded each frame's main world from Runtime.executionContextCreated only, and nothing dropped it when the browser tore it down, so the old document's id stayed recorded until the new one's event was handled. That event and Page.loadEventFired are read on separate loops, so GotoAsync could return first and the next EvaluateAsync, TitleAsync or ContentAsync named a dead context. Runtime.executionContextDestroyed and Runtime.executionContextsCleared now drop what they name, matched by session as well as id, since ids are numbered per renderer. The three context events are read from one ordered channel per session, so a teardown is never handled after the context that replaces it. Commands that name a context go through one path: a frame other than the main frame with nothing recorded waits for its next context, bounded by the caller's timeout or 30 seconds, and fails at once if the frame is detached or the page closes, instead of leaving the id out and running in the main frame. The main frame never waits, since leaving the id out reaches its current main world. A command refused with the stale-context error is sent once more; the browser refuses it before running anything, so this cannot run a script twice. Evaluations, handles, selector resolution and script and style tags all take this path, and the isolated world is rebuilt and retried the same way. BiDi addresses script by browsing context and has no ids to go stale, so it is left as it was. Signed-off-by: Torrey Betts <torrey.betts@gmail.com>
SummarySummary
CoverageMotus - 80.7%
Motus.Abstractions - 87.7%
Motus.Analyzers - 91%
Motus.Cli - 82.1%
Motus.Codegen - 89.8%
Motus.Mcp - 91.9%
Motus.Mcp.Http - 98.6%
Motus.Recorder - 77.8%
Motus.Testing - 75.4%
Motus.Testing.MSTest - 79%
Motus.Testing.xUnit - 63.6%
|
Evaluating in a subframe now waits for the frame's next execution context instead of running in the main frame. A lazy iframe that has not scrolled into view has no document and never reports a context, so the snapshot's per-frame index lookup waited out the full 30 second evaluation timeout for each one. A page with two lazy embeds below the fold took a minute to snapshot. The collector now asks every child frame at once and reads the answers after a one second allowance. A frame that has not answered by then is treated as one that cannot say where it belongs, which the collector already handles by falling back to the order the engine gives. Signed-off-by: Torrey Betts <torrey.betts@gmail.com>
A cross-origin frame that navigates back to its parent's origin gets its new context from the parent's session while its own target goes away, and the two are read on separate loops. Handling the target's departure dropped whatever context was recorded for the frames it hosted, including one the parent's session had just reported, so the next evaluation in that frame waited out the full context timeout for a context that had already arrived. The departure now drops only the contexts its own session reported, matching how destroyed and cleared notices are already scoped by session. Signed-off-by: Torrey Betts <torrey.betts@gmail.com>
Brings in the locator assertion, role and label, scroll, Firefox key, and installed browser changes. The only conflict was in FirefoxIntegrationTests.cs, where both sides added a test at the end of the class; both are kept. Signed-off-by: Torrey Betts <torrey.betts@gmail.com>
TorreyBetts
added a commit
that referenced
this pull request
Oct 24, 2026
TorreyBetts
added a commit
that referenced
this pull request
Oct 24, 2026
Brings in #31, #32, #33, #35, #36, #37, #38 and #40. Everything merged cleanly; the frame navigation handlers in Page.Events.cs sit next to the execution context tracking from #33 without touching it, and same-document navigations still leave execution contexts alone. Signed-off-by: Torrey Betts <torrey.betts@gmail.com>
This was referenced Oct 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
An evaluation that runs right after a navigation could fail with
MotusProtocolException: Cannot find context with specified id. The issue's loop (fresh context,GotoAsync("about:blank"),EvaluateAsync("1+1")) reproduced it within 40 iterations on Chrome here before this change.The cause is as the issue describes. The page recorded each frame's main world from
Runtime.executionContextCreatedonly, and nothing dropped it when the browser tore it down, so the old document's id stayed recorded until the new one's event had been handled. That event andPage.loadEventFiredare read on separate loops, soGotoAsynccould return first and the nextEvaluateAsync,TitleAsyncorContentAsyncnamed a dead context. One correction to the issue: the isolated-world guard (auxData.isDefault) was already in place, so isolated worlds were not overwriting the main world.The change
Runtime.executionContextDestroyedandRuntime.executionContextsClearednow drop the contexts they name. Entries are matched by session as well as id, because context ids are numbered per renderer and a frame that moved process has its new context recorded from a different session than the one reporting the old one gone.Page.ExecutionContexts.cs:WaitForFunctionAsync) or 30 seconds. It no longer leaves the id out, which ran the script in the main frame and answered for the wrong document.MotusTargetClosedExceptionwhen the frame is detached or the page closes, and it is woken by the detach rather than timing out. Waiters are per frame and removed when they fire.EvaluateAsync,EvaluateHandleAsync,Frame.EvaluateAsync(and soTitleAsync,ContentAsync,SetContentAsync,WaitForFunctionAsync), selector resolution, andAddScriptTagAsync/AddStyleTagAsyncall take this path. An isolated-world evaluation that hits a torn-down world rebuilds the world and retries once the same way.script.evaluatewithtarget.context) and runs in whatever realm it holds at the time, so there is no id to go stale and nothing to wait for. The BiDi path skips the waiting entirely and no longer subscribes to the CDP context events, which have no BiDi translation and only produced a rejectedsession.subscribe.Behavior changes
MotusTargetClosedExceptionstraight away. Before, it silently ran in the main frame and returned the main document's answer.WaitTimeoutException.No public API changes.
Tests
ExecutionContextTrackingTests(12 unit tests, fake CDP socket): destroyed, cleared, isolated world ignored, destroy-then-create ordering, a child frame waiting for its own context, stale retry for a child frame and for the main frame, no second retry, detached frame fails fast, detach and page close while waiting, and context events from one session leaving another session's context alone. 9 of the 12 fail without the fix; the other three guard behavior that already held.ExecutionContextIntegrationTests(Chromium): the issue's loop over 40 fresh contexts withEvaluateAsync,TitleAsyncandContentAsync; evaluation straight after cross-origin navigations; and evaluation and locator resolution in an iframe straight after it navigates. The loop test fails without the fix (Cannot find context with specified id).Firefox_EvaluateRightAfterNavigation_Succeedsrunning the same loop over BiDi.TestCategory!=Integration, excluding samples), net8.0 and net10.0: all pass (Motus.Tests 1070 per framework).ExecutionContextIntegrationTests,OutOfProcessFrameTests,FrameOrderTests,PageIntegrationTests,FrameLocatorTests,RoleSelectorScopeTests,BrowserContextPopupTests,KeyboardIntegrationTests,AssertionRetryIntegrationTests: 70/70 each.TestCategory=Firefox), net8.0 and net10.0: 7/7 each.Motus.Samples: 38 passed, 3 skipped.Fixes #25