orchestra/testbench-dusk
Laravel Dusk testing helper for package development. Orchestra Testbench Dusk lets you write browser tests for your Laravel packages, bootstrapping a test application with Dusk support so you can verify real user interactions end to end.
Testbench Dusk lets you run real browser tests against your package without spinning up a full Laravel app. It boots the orchestra/testbench-dusk skeleton application under PHP's built-in server and points Dusk's Chrome driver at it.
Minimal setup
Install the version that matches your orchestra/testbench major (they are lock-stepped with Laravel: testbench-dusk ^9 for Laravel 11, ^10 for Laravel 12, ^11 for Laravel 13):
composer require --dev orchestra/testbench-dusk
Make sure Chrome/Chromium and a matching chromedriver are on the machine (in CI, install them before the test run; Dusk's driver installer or a manual download both work). Mismatched Chrome/chromedriver versions are the single most common "it says nothing works" failure.
Create tests/Browser/ExampleTest.php extending Orchestra\Testbench\Dusk\TestCase (not Laravel\Dusk\TestCase):
use Laravel\Dusk\Browser;
use Orchestra\Testbench\Dusk\TestCase;
class ExampleTest extends TestCase
{
protected function defineRoutes($router)
{
$router->get('/demo', fn () => view('demo'));
}
protected function beforeServingApplication($app)
{
$app->register(MyPackageServiceProvider::class);
}
public function test_demo_page_renders()
{
$this->browse(function (Browser $browser) {
$browser->visit('/demo')
->assertSee('Hello')
->click('@save')
->waitForText('Saved');
});
}
}
Run with vendor/bin/phpunit tests/Browser (keep these out of your fast unit suite; put them in a Browser testsuite or a dusk group).
Where to look first: the docs at packages.tools/testbench-dusk.html, then the Orchestra\Testbench\Dusk\TestCase and Orchestra\Testbench\Dusk\DuskServer sources — those two classes define everything you'll override.
First use case: a Blade component, Livewire/Alpine widget, or JS-driven flow in your package that you cannot meaningfully assert with $this->get(). If it doesn't need a browser, use plain Testbench feature tests — they're orders of magnitude faster.
Declare routes, don't rely on the package's routes. The served app is the skeleton, so your package's controllers/routes are only available if the provider registers them. defineRoutes($router) is the cheapest way to expose a fixture page:
protected function defineRoutes($router)
{
$router->get('/forms', fn () => view('forms.index'));
$router->post('/forms', [FormController::class, 'store']);
}
For anything substantial, prefer Testbench's workbench/ application: routes in workbench/routes/web.php, views in workbench/resources/views, and your provider loaded via getPackageProviders(). Both Dusk tests and normal feature tests then exercise the same fixtures.
Use beforeServingApplication() / afterServingApplication() for app-level setup. These run inside the skeleton app before and after the server serves requests — register providers, point the view finder at your test fixtures, swap bindings:
protected function beforeServingApplication($app)
{
$app['config']->set('app.key', 'base64:...');
$app['view']->addLocation(__DIR__.'/views');
$app->register(MyPackageServiceProvider::class);
}
(tweakApplication() / removeApplicationTweaks() are deprecated aliases — migrate if you have older tests.)
Authenticate through the UI, not actingAs(). The browser talks to a separate PHP process, so PHPUnit's session and $this->app state are not shared. Either create a login route and fill the form like a user, or add a dedicated test-only route in beforeServingApplication() that logs in the user and redirects.
Database state: use DatabaseMigrations or migrate:fresh, not RefreshDatabase. Transactions opened in PHPUnit are invisible to the served app's requests. Seed data in beforeServingApplication() or via a test-only route before $this->browse().
Selector strategy. Add dusk="save" attributes to your package's Blade output and use @save selectors; prefer waitForText / waitFor / waitUntilMissing over assertSee for anything rendered by JS. Chain ->screenshot('name') (Dusk writes to tests/Browser/screenshots by default) on failure paths — it's the fastest way to debug a red build.
Console error assertions. $browser->assertConsoleLogHasNoErrors() is a cheap guard for JS regressions in package assets.
CI setup. Run Chrome headless (the package passes --disable-search-engine-choice-screen, --disable-smooth-scrolling, etc. by default so first-run dialogs don't hang the driver). Keep the browser suite as a separate job or workflow — it's slow and needs a display stack.
composer update orchestra/testbench orchestra/testbench-dusk laravel/dusk) rather than chasing Dusk alone.@vite manifest in the skeleton — avoid @vite directives (and asset helpers that require a build) in Dusk views; use inline CSS/JS or a published asset path.DuskServer in your base test case.php -S pipe hangs on older releases. Several releases fixed DuskServer hanging when the built-in server's output pipe fills; if your suite randomly freezes on the first request, upgrade to the latest patch in your major before debugging anything else.tweakApplication() → beforeServingApplication(), removeApplicationTweaks() → afterServingApplication(). Old code still works but will break in a future major.package:dusk-purge cleans generated skeleton/state left behind by Dusk runs; use --pretend to preview what it will delete. Useful in CI between jobs and locally when the skeleton gets dirty.->screenshot(), $browser->dump() / dd(), ->assertConsoleLogHasNoErrors(), then the DuskServer output (it echoes the PHP server's stdout into the test run). If Chrome can't connect, the error is almost always the chromedriver/Chrome version pair, not your test.driver() for custom Chrome flags, defineRoutes() for fixtures, beforeServingApplication() for app bootstrapping, and substitute a custom DuskServer if you need a different host/port/served app. Subclass TestCase once into tests/Browser/DuskTestCase.php so future package-wide tweaks live in one place.How can I help you explore Laravel packages today?