pestphp / pestphp/pest

[Bug]: Browser element assertion hangs forever instead of timing out when the element never appears

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

Nobody has claimed this yet.

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

Description

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)); // added

Output (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
  1. In a Laravel app with Pest 4 and the browser plugin installed (npm install playwright@1.62.1, npx playwright install chromium), set a browser timeout in tests/Pest.php:

    pest()->browser()->timeout(30_000);
    
  2. 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');
    });
    
  3. 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

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

Reproduce the issue with tests/Browser/MissingElementTest.php and the provided Playwright 1.62.1 command. Trace assertAttribute() through Execution::waitForExpectation(), Locator::getAttribute(), Client::execute(), and WebsocketConnection::receive(), focusing on the missing response timeout. Done means the missing-element assertion terminates near the configured 30-second timeout with an expectation message.

Written by the indexing model from the issue text.

Assessment

Tech stack
php, playwright
Domain
testing
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.