[Bug]: Browser element assertion hangs forever instead of timing out when the element never appears
What Happened
An element assertion whose selector never matches does not fail at the configured timeout. The test process waits indefinitely. In CI this turned a missing frontend asset into a 20 minute job timeout with no failure message.
Expected
The assertion fails after about 30 seconds (the configured timeout) with an expectation message.
Actual
Nothing is printed and the test never finishes. The outer timeout kills it with exit code 124. In CI the same situation ran until the step's 20 minute bound.
While it hangs, the PHP process, the Playwright server and the Chromium renderer all sit at 0% CPU. PHP is blocked in poll with two sockets open: the Playwright websocket and the database.
Results
Tested on the same project and Playwright version in the range (npm install --save-dev playwright@<version>):
| Playwright | MissingElementTest |
|---|---|
| 1.59.1 | fails after 61s: "Timeout 30000ms exceeded." |
| 1.60.0 | fails after 63s: "Timeout 30000ms exceeded." |
| 1.61.1 | fails after 62s: "Timeout 30000ms exceeded." |
| 1.62.0 | hangs, killed after 180s (exit 124) |
| 1.62.1 | hangs, killed after 180s (exit 124) |
The behaviour changed in Playwright 1.62.0. With 1.61.1 and earlier, the getAttribute timeout reaches the plugin as an error. (The 61s is 30s of waitForExpectation() retries plus one final 30s attempt.)
Where it blocks
Logging in Pest\Browser\Playwright\Client::execute() shows the stuck request. Two temporary lines were added:
$this->websocketConnection->sendText($requestJson);
error_log('SEND '.$requestJson); // added
// inside the while loop:
$response = json_decode($responseJson, true);
error_log('RECV '.substr($responseJson, 0, 300)); // addedOutput (trimmed):
SEND {"id":"6aa7bf5ef169a","guid":"frame@...","method":"goto","params":{"timeout":30000,"url":"http://127.0.0.1:36249/","waitUntil":"load"}}
RECV {"guid":"frame@...","method":"loadstate","params":{"add":"load"}}
SEND {"id":"6aa7bf604db76","guid":"frame@...","method":"getAttribute","params":{"timeout":1000,"selector":"ui-never-rendered > [contenteditable]","strict":true,"name":"contenteditable"}}
RECV {"id":"6aa7bf5ef169a","result":{"response":{"guid":"response@..."}}}The last RECV is the late reply to goto, which execute() returned from early on the load event. After that, no message of any kind arrives, not even an error for getAttribute. We watched for 78 seconds in one run and 4 minutes in another.
The call path is assertAttribute() → Execution::waitForExpectation() → Playwright::usingTimeout(1_000, ...) → Locator::getAttribute() → Client::execute(). The request carries timeout: 1000, so Playwright should answer with a TimeoutError after a second and the retry loop would give up at 30 seconds. With no reply, execute() blocks in WebsocketConnection::receive(), which has no timeout of its own, so the loop's deadline check in waitForExpectation() never runs again.
How to Reproduce
In a Laravel app with Pest 4 and the browser plugin installed (
npm install [email protected],npx playwright install chromium), set a browser timeout intests/Pest.php:pest()->browser()->timeout(30_000);Add a test that asserts an attribute on an element that is never rendered:
// tests/Browser/MissingElementTest.php it('fails instead of hanging', function () { visit('/')->assertAttribute('ui-never-rendered > [contenteditable]', 'contenteditable', 'true'); });Run it with an outer bound so it cannot run forever:
timeout 4m php artisan test tests/Browser/MissingElementTest.php; echo "exit $?"
Sample Repository
No response
Pest Version
Pest 4.7.5, pest-plugin-browser 4.3.1, pest-plugin-laravel 4.1.0
PHP Version
PHP 8.4.12
Operation System
Linux
Source: pestphp/pest