Description
When browser tests are located outside the default tests/ directory (e.g. app/Domain/*/Tests/), the plugin's afterEach hook that calls Playwright::reset() never fires. This causes browser contexts to accumulate across tests, and the Playwright Node.js process hangs indefinitely during shutdown.
Environment
- Pest v4.x
- pest-plugin-browser v4.2.x
- macOS / Linux
Root Cause
In Plugin.php, the afterEach hook that calls Playwright::reset() (which closes browser contexts between tests) is scoped using $this->in():
// Plugin.php boot()
$this->afterEach(function (): void {
Playwright::reset();
})->in($this->in());
The in() method returns:
private function in(): string
{
return TestSuite::getInstance()->rootPath
. DIRECTORY_SEPARATOR
. TestSuite::getInstance()->testPath;
}
testPath defaults to 'tests' (set in vendor/pestphp/pest/bin/pest). This means the afterEach hook only registers for test files inside the tests/ directory.
For projects with browser tests in other directories (like app/Domain/), the hook never fires. As a result:
- Each
visit() call creates a new browser context via Browser::newContext()
Playwright::reset() is never called — contexts are never closed
- Contexts accumulate across all tests (we observed 48 contexts across 21 tests)
- During
Plugin::terminate(), PlaywrightNpmServer::stop() sends SIGTERM to the Node.js process
- Playwright attempts to gracefully close all accumulated contexts
- The Node.js process hangs,
Symfony\Process::stop() blocks, and the PHP process never exits
Workaround
Add an explicit afterEach in tests/Pest.php with an absolute path:
uses()->afterEach(function () {
if (isset($this->page)) {
$this->page->page()->close();
}
if (class_exists(\Pest\Browser\Playwright\Playwright::class, false)) {
\Pest\Browser\ServerManager::instance()->http()->flush();
\Pest\Browser\Playwright\Playwright::reset();
}
})->in(dirname(__DIR__) . '/app/Domain');
Note: using a relative path like ->in('app/Domain') does not work because UsesCall resolves it relative to the file's directory (tests/), resulting in the non-existent path tests/app/Domain/.
Suggested Fix
Plugin::in() should account for all directories that contain test files, not just the default testPath. Possible approaches:
- Scan all configured PHPUnit test suite directories, not just
testPath
- Register the
afterEach globally (without path scoping) and check inside the callback whether the current test uses browser features
- Allow users to configure additional test directories for the browser plugin
How to Reproduce
-
Structure browser tests outside tests/, e.g.:
app/
Domain/
Feature1/Tests/SomeBrowserTest.php
Feature2/Tests/AnotherBrowserTest.php
...
-
Register the test directory in Pest.php and phpunit.xml:
// tests/Pest.php
pest()->extend(Tests\TestCase::class)->in('app/Domain');
-
Run 5+ browser test files:
php vendor/bin/pest --filter='Test1|Test2|Test3|Test4|Test5'
-
The process prints all test results but never terminates.
Running 4 or fewer test files works fine — the threshold depends on how many browser contexts accumulate (we observed 48 contexts across 21 tests) before the Playwright server can no longer shut down gracefully.
Sample Repository
No response
Pest Version
4.3.2
PHP Version
8.3
Operation System
macOS
Notes
No response
Description
When browser tests are located outside the default
tests/directory (e.g.app/Domain/*/Tests/), the plugin'safterEachhook that callsPlaywright::reset()never fires. This causes browser contexts to accumulate across tests, and the Playwright Node.js process hangs indefinitely during shutdown.Environment
Root Cause
In
Plugin.php, theafterEachhook that callsPlaywright::reset()(which closes browser contexts between tests) is scoped using$this->in():The
in()method returns:testPathdefaults to'tests'(set invendor/pestphp/pest/bin/pest). This means theafterEachhook only registers for test files inside thetests/directory.For projects with browser tests in other directories (like
app/Domain/), the hook never fires. As a result:visit()call creates a new browser context viaBrowser::newContext()Playwright::reset()is never called — contexts are never closedPlugin::terminate(),PlaywrightNpmServer::stop()sends SIGTERM to the Node.js processSymfony\Process::stop()blocks, and the PHP process never exitsWorkaround
Add an explicit
afterEachintests/Pest.phpwith an absolute path:Note: using a relative path like
->in('app/Domain')does not work becauseUsesCallresolves it relative to the file's directory (tests/), resulting in the non-existent pathtests/app/Domain/.Suggested Fix
Plugin::in()should account for all directories that contain test files, not just the defaulttestPath. Possible approaches:testPathafterEachglobally (without path scoping) and check inside the callback whether the current test uses browser featuresHow to Reproduce
Structure browser tests outside
tests/, e.g.:Register the test directory in
Pest.phpandphpunit.xml:Run 5+ browser test files:
php vendor/bin/pest --filter='Test1|Test2|Test3|Test4|Test5'The process prints all test results but never terminates.
Running 4 or fewer test files works fine — the threshold depends on how many browser contexts accumulate (we observed 48 contexts across 21 tests) before the Playwright server can no longer shut down gracefully.
Sample Repository
No response
Pest Version
4.3.2
PHP Version
8.3
Operation System
macOS
Notes
No response