Skip to content

[Bug]: Browser contexts never cleaned up when tests live outside the default tests/ directory #1638

@ovp87

Description

@ovp87

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:

  1. Each visit() call creates a new browser context via Browser::newContext()
  2. Playwright::reset() is never called — contexts are never closed
  3. Contexts accumulate across all tests (we observed 48 contexts across 21 tests)
  4. During Plugin::terminate(), PlaywrightNpmServer::stop() sends SIGTERM to the Node.js process
  5. Playwright attempts to gracefully close all accumulated contexts
  6. 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:

  1. Scan all configured PHPUnit test suite directories, not just testPath
  2. Register the afterEach globally (without path scoping) and check inside the callback whether the current test uses browser features
  3. Allow users to configure additional test directories for the browser plugin

How to Reproduce

  1. Structure browser tests outside tests/, e.g.:

    app/
      Domain/
        Feature1/Tests/SomeBrowserTest.php
        Feature2/Tests/AnotherBrowserTest.php
        ...
    
  2. Register the test directory in Pest.php and phpunit.xml:

    // tests/Pest.php
    pest()->extend(Tests\TestCase::class)->in('app/Domain');
  3. Run 5+ browser test files:

    php vendor/bin/pest --filter='Test1|Test2|Test3|Test4|Test5'
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions