-
-
Notifications
You must be signed in to change notification settings - Fork 454
Description
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 viaBrowser::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
afterEachglobally (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.phpandphpunit.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