pestphp / pestphp/pest

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

Open
#1,638 0 comments 2 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

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

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

Read Plugin.php, especially boot() and in(), and reproduce the shutdown hang with browser tests under app/Domain as described. Compare the hook's test-path scope with the tests/Pest.php workaround, then verify that browser contexts are cleaned up for tests outside tests/ and the process terminates normally.

Written by the indexing model from the issue text.

Assessment

Tech stack
php
Domain
testing-qa
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.