In our org we're looking into migrating from Dusk browser tests to Pest v4 - with the browser plugin. We're experiencing a situation that could be caused by a bug, where the test just stops generating any output, and silently crashes.
Pest Browser Version
v4.x (latest)
PHP Version
8.4
Laravel Version
11.x
Description
Any navigation event (redirect, navigate(), goto(), or JS navigation like window.location.href) causes the PHP process to crash silently when subsequent operations are performed on the page. There is no error message, no exception, no output - the process simply terminates.
This is related to but distinct from #1511 (AwaitableWebpage::click times out) - that issue describes timeouts with "Execution context was destroyed" errors. Our issue is a complete silent crash with zero output.
Root Cause Analysis
Through extensive testing, we've identified:
- Each
$this->visit() creates a NEW browser context - contexts don't share cookies/sessions
- After navigation,
PendingAwaitablePage becomes corrupted
- Calling ANY method on a corrupted page crashes PHP silently
What Works vs What Crashes
| Scenario |
Result |
| Basic visit without navigation |
✅ PASS |
| Login WITHOUT server redirect |
✅ PASS |
JS fetch() after login |
✅ PASS (returns 200) |
Login then navigate() |
❌ CRASHES |
Login then page()->goto() |
❌ CRASHES |
Login then JS window.location.href = ... |
❌ CRASHES |
| Login WITH server-side redirect |
❌ CRASHES |
Two independent visit() calls |
❌ CRASHES |
waitForURL() after navigation |
❌ CRASHES |
waitForLoadState() after navigation |
❌ CRASHES |
Key Finding
Authentication WORKS. The issue is NAVIGATION, not authentication.
We can prove this with a fetch test:
// Login without redirect
$page = $this->visit(route('dusk.auth.login', ['userId' => $user->getKey()]));
$page->assertSee('Logged in');
// Fetch dashboard content via JS - uses session cookies
$result = $page->script("
(async () => {
const response = await fetch('/dashboard', {credentials: 'same-origin'});
return {status: response.status, url: response.url};
})()
");
// Returns: {"status": 200, "url": "http://127.0.0.1:PORT/dashboard"}
// Auth IS working - fetch succeeds with 200!
Steps To Reproduce
it('crashes on navigation', function () {
$user = User::factory()->create();
// Step 1: Login (works fine)
$page = $this->visit(route('dusk.auth.login', ['userId' => $user->getKey()]));
$page->assertSee('Logged in');
// Step 2: Any of these cause silent crash:
// Option A: navigate() method
$page->navigate('/dashboard'); // CRASH - no output
// Option B: Playwright goto
$page->page()->goto('/dashboard'); // CRASH - "Broken pipe"
// Option C: JS navigation
$page->script("window.location.href = '/dashboard'");
$page->waitForLoadState('load'); // CRASH - no output
// Option D: Fresh visit
$this->visit('/dashboard'); // CRASH - no output
// None of these lines are reached
$page->assertSee('Dashboard');
});
Workaround
We've developed a fetch+inject pattern that works for static content tests:
// Step 1: Login without redirect
$page = $this->visit(route('dusk.auth.login', ['userId' => $user->getKey()]));
// Step 2: Fetch page HTML via JS (uses session cookies, no navigation)
$html = $page->script("
(async () => {
const response = await fetch('/dashboard', {credentials: 'same-origin'});
return await response.text();
})()
");
// Step 3: Inject into current DOM
$page->script("document.body.innerHTML = " . json_encode($html));
// Step 4: Use JS assertions (assertSee can also crash!)
$found = $page->script("document.body.innerText.includes('Dashboard')");
expect($found)->toBeTrue();
Limitations: This workaround cannot test real navigation, interactive features, form submissions, or SPA navigation.
Environment
- OS: macOS (also reproduced on Linux CI)
- Playwright: Latest
- Running via
php artisan pest:browser
Additional Context
We have ~140 browser tests that all crash silently due to this issue. The diagnostic test file we used to isolate this bug is available if helpful.
This appears to be a fundamental issue with how PendingAwaitablePage handles navigation events. The page object becomes corrupted after any URL change, and subsequent method calls cause a PHP crash rather than throwing a catchable exception.
In our org we're looking into migrating from Dusk browser tests to Pest v4 - with the browser plugin. We're experiencing a situation that could be caused by a bug, where the test just stops generating any output, and silently crashes.
Pest Browser Version
v4.x (latest)
PHP Version
8.4
Laravel Version
11.x
Description
Any navigation event (redirect,
navigate(),goto(), or JS navigation likewindow.location.href) causes the PHP process to crash silently when subsequent operations are performed on the page. There is no error message, no exception, no output - the process simply terminates.This is related to but distinct from #1511 (AwaitableWebpage::click times out) - that issue describes timeouts with "Execution context was destroyed" errors. Our issue is a complete silent crash with zero output.
Root Cause Analysis
Through extensive testing, we've identified:
$this->visit()creates a NEW browser context - contexts don't share cookies/sessionsPendingAwaitablePagebecomes corruptedWhat Works vs What Crashes
fetch()after loginnavigate()page()->goto()window.location.href = ...visit()callswaitForURL()after navigationwaitForLoadState()after navigationKey Finding
Authentication WORKS. The issue is NAVIGATION, not authentication.
We can prove this with a fetch test:
Steps To Reproduce
Workaround
We've developed a fetch+inject pattern that works for static content tests:
Limitations: This workaround cannot test real navigation, interactive features, form submissions, or SPA navigation.
Environment
php artisan pest:browserAdditional Context
We have ~140 browser tests that all crash silently due to this issue. The diagnostic test file we used to isolate this bug is available if helpful.
This appears to be a fundamental issue with how
PendingAwaitablePagehandles navigation events. The page object becomes corrupted after any URL change, and subsequent method calls cause a PHP crash rather than throwing a catchable exception.