Repository navigation
Match WaitForURLAsync strings against the whole URL - #34
Merged
Merged
Conversation
A plain string passed to WaitForURLAsync matched any URL that contained it, so waiting for a site's root returned at once from every page on the site, and "?page=1" was satisfied by "?page=10". The wait passed silently and the test went on against a page that had not loaded. A string with no wildcard now has to equal the URL, and a string with a wildcard is a glob over the whole URL, using the same glob syntax routes already use. WaitForURLAsync gains Regex and predicate overloads on IPage and IFrame for anything a glob cannot express. ToHaveUrlAsync uses the same matcher, so a wait and an assertion given the same string agree. Routes and the request and response waits keep their documented substring form. Three gaps surfaced while testing the wait and are fixed with it. A frame's wait checked the page's URL instead of the frame's. URL changes that keep the document (history.pushState, fragments) never updated the frame URL, so a single-page application never appeared to move; CDP's Page.navigatedWithinDocument is now handled, and on BiDi it is fed by both browsingContext.fragmentNavigated and browsingContext.historyUpdated. And a timeout surfaced as TaskCanceledException rather than the documented WaitTimeoutException. The wait now listens for navigation events as well as polling, so a URL the page only passes through is not missed. Signed-off-by: Torrey Betts <torrey.betts@gmail.com>
SummarySummary
CoverageMotus - 80.9%
Motus.Abstractions - 87.7%
Motus.Analyzers - 91%
Motus.Cli - 82.2%
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.MSTest.v4 - 22.8%
Motus.Testing.xUnit - 63.6%
|
…form WaitForURLAsync and ToHaveUrlAsync now require a plain string to equal the URL, but the browser reports a URL in its normal form: a bare origin gains a trailing slash and the host is lower case. A base URL as it is usually written, such as http://localhost:5000, therefore never matched the page it names and the wait ran out its timeout. An http or https pattern that doesn't equal the URL as given is now also compared in its normalized form. The match never widens: a path, query or fragment still has to be the same. Signed-off-by: Torrey Betts <torrey.betts@gmail.com>
With the substring match gone, a string without a scheme such as "/dashboard" could no longer match anything, because the browser always reports an absolute URL. A context with a BaseURL navigates with GotoAsync("/dashboard"), and waiting or asserting with the same relative string used to work through the substring match; after the switch to whole-URL matching it timed out every time.
WaitForURLAsync on a page or frame and ToHaveUrlAsync now resolve such a string against the base URL first, the way GotoAsync resolves the address it loads, and then compare the whole URL. Globs, absolute URLs, and any string when no base URL is set are compared as written.
Signed-off-by: Torrey Betts <torrey.betts@gmail.com>
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 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.
WaitForURLAsync(string)treated a plain string as "the URL contains this", soWaitForURLAsync("https://example.test/")returned at once from/account,/orderswas satisfied by/orders/42/edit, and?page=1by?page=10. The wait passed silently and the test went on against a page that hadn't loaded. Playwright waits for that exact URL, or a glob over the whole URL, and that is what Motus does now.While writing the tests, three more problems in the same wait turned up, and they are fixed here because the wait isn't usable without them:
IFrame.WaitForURLAsyncwaited on the page's URL, not the frame's.history.pushState,replaceState, a fragment) never updatedUrlat all, so a single-page application never appeared to move and a wait for its next route always timed out.TaskCanceledExceptionrather than the documentedWaitTimeoutException.What changed
IPage.WaitForURLAsync(string)andIFrame.WaitForURLAsync(string): a string with no*has to equal the whole URL; a string with*is a glob that has to cover the whole URL. The glob syntax is unchanged from routes: each*matches any run of characters including/, so*and**are the same, and nothing else is special.IPageandIFrame:WaitForURLAsync(Regex, NavigationOptions?)andWaitForURLAsync(Func<string, bool>, NavigationOptions?), with XML docs.Expect.That(page).ToHaveUrlAsync(string)uses the same whole-URL matcher, so a wait and an assertion given the same string agree.pushState) is still seen. The predicate is only ever called from the waiting loop, never concurrently.IFrame.Urland raiseIPage.FrameNavigated. On CDP this isPage.navigatedWithinDocument. On BiDi, which reports it as two events,BiDiEventMapcan now hold more than one translation per CDP event andBiDiSessionmerges them:browsingContext.historyUpdatedandbrowsingContext.fragmentNavigated. Firefox accepts a context-scoped subscription tofragmentNavigatedand then never delivers it (checked against Firefox 158 with the protocol log), so that one event is subscribed for all contexts; events are still routed by the context they name.WaitTimeoutExceptionwhose message andLastEvaluatedValuecarry the URL the page was on.getting-started.md, the URL pattern section of the network interception guide, the page assertions table inselectors-and-assertions.md, and the Playwright migration guide.What I deliberately left alone: routes (
IPage.RouteAsync,IBrowserContext.RouteAsync, the MCProute_*tools) andWaitForRequestAsync/WaitForResponseAsynckeep their documented exact, glob, or substring matching. Those are documented as one pattern syntax, and changing them silently would break existing mocks. I also didn't adopt Playwright's narrower*(stops at/) or{a,b}brace lists, since the glob is shared with routes and the docs' own examples (*/api/status.json,*iana.org*) depend on*crossing/. ARegexcovers those cases.Behavior changes
WaitForURLAsync(string)onIPageandIFrameno longer matches a URL that only contains the string. A string with no*has to be the whole URL. To keep matching part of a URL, use a glob that covers the rest, such as"**/orders**", or pass aRegexsuch asnew Regex("/orders").ToHaveUrlAsync(string)changes the same way. Use a glob such as"**/orders**"for a partial match.IFrame.WaitForURLAsyncwaits on the frame's own URL. It used to check the page's URL.IPage.UrlandIFrame.Urlnow followhistory.pushState,history.replaceStateand fragment navigations, andIPage.FrameNavigatedis raised for them (as in Playwright). Code that readUrlafter a client-side route change used to get the old URL.WaitForURLAsynctimeout throwsWaitTimeoutExceptioninstead ofTaskCanceledException.IPageandIFramegain twoWaitForURLAsyncoverloads, so a class implementing either interface outside Motus has to add them.Tests
UrlMatchingTests: exact URL, a site's root against a deeper page, a path prefix,?page=1against?page=10, case sensitivity, globs that cover or only partly cover the URL,?, braces and brackets staying literal, and route matching keeping its substring form.PageWaitForUrlTestsagainst a fake CDP socket: already matching, timeout type and message, string, glob, regex and predicate waits, a predicate that throws, null arguments, a URL passed through between polls,Page.navigatedWithinDocumentfor history and fragment changes, and frame waits that ignore the page's URL (and the page ignoring a child frame's).PageAssertionsTests:ToHaveUrlAsyncno longer passes on a prefix.BiDiEventMapTestsandBiDiSessionTests: both BiDi sources forPage.navigatedWithinDocument, the payload translation, the all-contexts subscription forfragmentNavigated, and delivery surviving one of the two subscriptions being refused.WaitForUrlIntegrationTests(Chromium, 12 tests) over a loopback HTTP server: exact versus containing, already matching, globs, regex, predicate,pushState, fragment, and frame waits by string, regex and predicate including apushStateinside the frame.WaitForUrlFirefoxIntegrationTests(3 tests) covers exact matching,pushStateand a fragment on Firefox.Before the fix, 7 of the 8 string-only Chromium integration tests failed (the contains match,
pushState, fragment, the frame checking the page's URL, andTaskCanceledExceptionon timeout). The Firefox fragment test failed until the all-contexts subscription went in.Results:
TestCategory!=Integration, excluding Samples), net10.0 and net8.0: all passed (Motus.Tests 1101, Mcp 366, Cli 303 with 2 skipped, Recorder 105, Codegen 59, Analyzers 29, Testing.xUnit 8).PageIntegrationTests,OutOfProcessFrameTests,FrameOrderTests,FrameSwapTests,NewPageFrameTreeTests,FrameLocatorTests,AssertionRetryIntegrationTests,BrowserContextRouteIntegrationTests,PageNetworkIntegrationTests,FirefoxIntegrationTestsand the MCPFrameToolsIntegrationTests. net8.0: 99/99 and 11/11. net10.0: 98/99 and 11/11. The one failure wasFrameOrderTests.AfterAFrameDetaches_TheSurvivorsKeepTheirOrdertiming out waiting for frames; it passed when I reran that class and passed on net8.0.Fixes #24