Skip to content

fix: make the page waits send their request and wait for it - #273

Open
sebastiansulinski wants to merge 2 commits into
pestphp:5.xfrom
sebastiansulinski:fix/page-waits-never-sent
Open

sebastiansulinski wants to merge 2 commits into
pestphp:5.xfrom
sebastiansulinski:fix/page-waits-never-sent

Conversation

@sebastiansulinski

Copy link
Copy Markdown

What was wrong

Page::waitForFunction(), Page::waitForLoadState() and Page::waitForURL() never waited. Each called Client::instance()->execute(...) and discarded the return value — but Client::execute() is a generator, so its body never ran: the request was never written to the WebSocket and the method returned at once with its condition unchecked. Webpage::waitForEvent() and the internal callers in MakesConsoleAssertions, MakesScreenshotAssertions, InteractsWithFrames and InteractsWithTab were no-ops for the same reason.

Reproduction on v4.3.1 and v5.0.1: visit('/')->page()->waitForFunction('() => false') returns in 0 ms instead of failing at the action timeout. We hit it in an application suite whose tests "waited" on an editor's focus this way and passed by timing alone.

The fix

  • waitForFunction() goes through sendMessage() (the frame-level Frame.waitForFunction protocol method) and awaits the response, so it polls until the expression or function returns a truthy value and fails with the action timeout otherwise. The argument is passed as before.
  • The wire protocol has no waitForLoadState or waitForURL method (both are client-side in Playwright's own bindings: FrameWaitForFunctionParams, FrameWaitForSelectorParams and FrameWaitForTimeoutParams are the only frame waits in playwright-core 1.62.1's schema), so both are built on waitForFunction():
    • waitForLoadState() reads document.readyState: domcontentloaded once parsing has finished, load (and networkidle, which this synchronous client cannot observe) once the load event has fired. The docblock says so.
    • waitForURL() matches in the page with the same * wildcard assertUrlIs() accepts: against the path when the pattern starts with /, otherwise against scheme://host[:port]/path, mirroring assertPathIs() and assertUrlIs().
  • The two names are removed from isPageLevelOperation(), where they no longer name a message.

If you would rather keep this pull request to waitForFunction() and take the other two separately, say so and I will split it.

Tests

tests/Browser/Webpage/WaitForFunctionTest.php, WaitForLoadStateTest.php, WaitForEventTest.php and WaitForURLTest.php: a function becoming truthy, an expression becoming truthy, the argument, the timeout failure, the three load states and the default, waitForEvent(), a navigation to a path, a wildcard match and a URL that never matches. Six of the ten fail against the previous implementation (all waitForFunction and waitForURL cases); the load-state cases pass either way because a visited page has already loaded, so they cover the mapping and the fluent return only.

composer test locally on PHP 8.5.10 with Playwright 1.62.1: lint (rector, pint), profanity, type coverage 100 %, phpstan (level max, strict rules) and the unit and browser suites all pass.

sebastiansulinski and others added 2 commits September 28, 2026 17:04
`Page::waitForFunction()`, `waitForLoadState()` and `waitForURL()` never
waited: `Client::execute()` is a generator, and the three methods
discarded its return value without iterating it, so the request was
never even written to the socket and each call returned at once with its
condition unchecked. `Webpage::waitForEvent()` and the internal callers in
the console, screenshot, frame and tab helpers were no-ops for the same
reason.

`waitForFunction()` now goes through `sendMessage()` (the frame-level
`Frame.waitForFunction` protocol method) and awaits the response, so it
polls until the expression or function is truthy and fails with the
action timeout otherwise.

The wire protocol has no `waitForLoadState` or `waitForURL` method — both
are client-side in Playwright's own bindings — so they are built on
`waitForFunction()`: the load state is read from `document.readyState`
(`domcontentloaded` once parsing has finished, `load` and `networkidle`
once the load event has fired; network idleness cannot be observed by
this synchronous client), and the URL is matched in the page with the
same `*` wildcard `assertUrlIs()` accepts, against the path when the
pattern starts with `/` and against `scheme://host[:port]/path`
otherwise. The two names are dropped from the page-level operation list,
where they no longer name a message.

Tests cover a function becoming truthy, an expression becoming truthy,
the argument, the timeout failure, the load states, `waitForEvent()`, a
navigation to a path, a wildcard match and a URL that never matches; six
of the ten fail against the previous implementation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants