pestphp / pestphp/pest

[browser plugin] Client::execute() waitUntil early-break strands responses and desynchronizes subsequent commands

Open
#1,759 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
PHP
Stars
11.7k
Forks
538
Avg merge
4d 11h
Merged PRs (30d)
8

Description

Filed here because pestphp/pest-plugin-browser has issues disabled.

Description

Client::execute() can leave a command's own response stranded in the websocket buffer, after which every later command on the connection consumes shifted responses, producing confusing downstream failures far from the cause.

Version: pest-plugin-browser v4.3.1 (pest v4, PHP 8.5)

Root cause

src/Playwright/Client.php, execute() — the response loop breaks on either the command's own response id or a matching waitUntil lifecycle event:

if (
    (isset($response['id']) && $response['id'] === $requestId)
    || (isset($params['waitUntil']) && isset($response['params']['add']) && $params['waitUntil'] === $response['params']['add'])
) {
    break;
}

Page::goto() always sends waitUntil: 'load'. When the load lifecycle event arrives before the goto command's own response (ordering depends on server/browser timing), the loop breaks early and goto's {id: ...} response is never consumed.

The next command then reads it. processResultResponse() accepts the first message carrying result.value without correlating the response id:

foreach ($response as $message) {
    if (isset($message['result']['value'])) {
        return JavaScriptSerializer::parseValue($message['result']['value']);
    }
}

From that point the stream is desynchronized: each command may receive the previous command's result.

Observed symptoms
  • Page::consoleLogs(): Return value must be of type array, true returned — the evaluate consumed a stale boolean from a previous assertScript.
  • click() returns success but the page never receives the click (failure screenshots show the page fully rendered with pre-click state).
  • Text assertions fail on pages that never received the preceding action.

In our suite (~220 browser tests) this reproduced deterministically on GitHub Actions (sequential run, sqlite, both with --only-shell and full chromium) while passing locally — the event ordering is environment-timing dependent, so victims appear/disappear with machine speed.

Suggested fix

Drop responses whose id belongs to a previous command before processing:

// in execute(), after json_decode:
if (isset($response['id']) && $response['id'] !== $requestId) {
    continue;
}

With this guard our previously-failing suite is green; the stranded response from a waitUntil early-break is simply discarded by the next command instead of being misinterpreted as its result. (Correlating ids in processResultResponse() would work as well.)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in src/Playwright/Client.php at execute() and processResultResponse(), then trace the response handling from Page::goto(), which sends waitUntil: 'load'. Reproduce the ordering where the lifecycle event precedes the command response and verify that later commands cannot consume a stale result; the reported browser suite should remain green without shifted responses.

Written by the indexing model from the issue text.

Assessment

Tech stack
php
Domain
testing-qa
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
74/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.