Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Dusk Laravel Package

laravel/dusk

Laravel Dusk provides an expressive browser automation and testing API for Laravel apps, using a standalone Chromedriver with no JDK or Selenium required by default, while supporting other Selenium drivers.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and bootstrap (5 minutes)

composer require laravel/dusk --dev
php artisan dusk:install          # publishes tests/Browser, DuskTestCase, phpunit.dusk.xml, .env.dusk
php artisan dusk:chrome-driver --detect   # matches driver to your installed Chrome
cp .env.dusk.example .env.dusk.local 2>/dev/null || true

dusk:install scaffolds:

Path Purpose
tests/DuskTestCase.php The only file you normally edit — driver config, Chrome flags, headless toggles
tests/Browser/ExampleTest.php Starter test; delete and replace
tests/Browser/Pages/ Page Objects (Home, Login)
phpunit.dusk.xml Separate PHPUnit suite so Dusk never runs under php artisan test
.env.dusk.local Test env: point APP_URL at a real host/port, use a throwaway DB

Where to look first

  1. tests/DuskTestCase.php → driver(). This is where headless mode, window size, --no-sandbox, DUSK_DRIVER_URL, and download paths are decided.
  2. tests/Browser/Pages/Home.php → the assert() method doubles as a per-page smoke test.
  3. vendor/laravel/dusk/src/Browser.php → the authoritative API list; the Concerns/ traits (InteractsWithElements, MakesAssertions, WaitsForElements) tell you what is actually supported better than any blog post.

First realistic use case: the login/render smoke test

// tests/Browser/LoginTest.php
public function test_user_can_log_in(): void
{
    $user = User::factory()->create(['password' => bcrypt('secret')]);

    $this->browse(function (Browser $browser) use ($user) {
        $browser->visit('/login')
            ->type('email', $user->email)
            ->type('password', 'secret')
            ->press('Log in')
            ->waitForLocation('/dashboard')
            ->assertAuthenticatedAs($user)
            ->assertSee('Dashboard');
    });
}

Run it with php artisan dusk (all), php artisan dusk --filter=LoginTest (one), php artisan dusk --browse (watch it happen), php artisan dusk:fails (re-run last failures). On failure, look in tests/Browser/screenshots/ and tests/Browser/console/ before touching the code.

Aim the first week at breadth, not depth: one test per critical journey (register, login, checkout, admin CRUD) plus one per historically broken page. Dusk's ROI comes from catching "the page 500s / the button vanished / the JS broke", not from re-testing validation rules your feature tests already cover.

Implementation Patterns

Page Objects + @dusk selectors — the pattern everything else builds on

// app markup: <button dusk="checkout-submit">
class Checkout extends Page
{
    public function url(): string { return '/checkout'; }

    public function elements(): array
    {
        return [
            '@submit'   => '[dusk=checkout-submit]',
            '@coupon'   => '[dusk=coupon-field]',
            '@total'    => '[dusk=cart-total]',
        ];
    }

    public function applyCoupon(Browser $browser, string $code): self
    {
        $browser->type('@coupon', $code)
                ->clickWhen('@apply-coupon')      // waits for visible+enabled, no sleep()
                ->waitForTextIn('@total', '£0.00');
        return $this;
    }
}

Rules of thumb that keep the suite alive:

  • Every element a test touches gets a dusk="..." attribute. Freeze the contract in markup; refactoring CSS classes then never breaks tests.
  • Selectors resolve through elements(), so @submit is a stable alias, not a CSS path duplicated in 40 tests.
  • Do not put ) inside selector names — resolution has historically choked on closing parentheses (fixed in 8.3.6, but avoid the pattern).
  • If you prefer data-testid (or any other attribute), register it as the custom Dusk selector once and keep using @name everywhere.

Composable flows: traits, within(), and components

// tests/Browser/Concerns/LogsIn.php
trait LogsIn { protected function loginAs(Browser $b, User $u): Browser { return $b->loginAs($u); } }

$browser->within('@cart-row-1', fn (Browser $row) => $row->assertSee('Sneakers')->click('@remove'));
$browser->component(NavBar::class)->assertSee('Cart (1)');   // scoped sub-browser

component() (v7.13+) is the cleanest way to model repeatable widgets — nav bars, modals, tables — that appear on many pages. Define the component's root selector once; child selectors are looked up inside it.

Wait strategy (the single biggest flake killer)

Need Method
Element appears waitFor('@selector') / waitUntilMissing()
Text changes waitForTextIn('@selector', 'Saved')
Navigation waitForLocation('/orders'), waitForRoute('orders.show')
Element becomes usable clickWhen('@selector') — waits for visible + enabled before clicking
Arbitrary JS condition waitUntil(fn ($b) => $b->script('return window.appReady === true'), 10)
Async assertion retries waitForLink, waitForEnabled, waitForInput variants

Never pause() in committed tests — it's a debugging tool only. If you find yourself needing one, the app is not signalling readiness; add a dusk attribute that appears only when the state is settled.

Assertions beyond assertSee

assertPathEndsWith() / assertPathContains() (8.2.0) for URL routing checks; assertCount() (8.3.0) for list lengths; assertAttributeMissing() (7.12.2); assertAttributeDoesntContain(); assertAuthenticatedAs(), assertGuest(); assertVue() / assertVueContains() for Vue state (requires the Dusk JS bindings and the dev Vue build — treat as a bonus, not a foundation). For case-insensitive checks there is an explicit flag — don't regex around it.

Multi-user / multi-browser flows

$this->browse(function (Browser $admin, Browser $customer) use ($adminUser) {
    $admin->loginAs($adminUser)->visit('/admin/orders');
    $customer->visit('/orders/latest')->assertSee('Pending');
});

Extra Browser parameters get fresh browser instances sharing the same driver — the canonical way to test chat, notifications, and permission boundaries.

Database & environment strategy

use Illuminate\Foundation\Testing\DatabaseTruncation;

class CheckoutTest extends DuskTestCase
{
    use DatabaseTruncation;          // far faster than DatabaseMigrations on a mature schema
}
  • Migrate once in CI (php artisan migrate --force on the Dusk database), then truncate per test.
  • .env.dusk.local must use a separate database from dev — Dusk does not sandbox you.
  • Queue (QUEUE_CONNECTION=sync) and mail (MAIL_MAILER=log or array) should be switched in that env file; anything async becomes an implicit race.
  • Cache config/route only if you also re-clear in CI; stale caches are a top-3 source of "works locally, fails in CI".

CI wiring

- run: php artisan dusk:chrome-driver --detect
- run: php artisan serve --port=8000 &   # or let Dusk start it
- run: php artisan dusk
  env:
    APP_URL: http://127.0.0.1:8000

Keep --headless=new (the default since 7.7.0). On ARM runners or when you need Selenium features, set DUSK_DRIVER_URL=http://selenium:4444/wd/hub and Dusk talks to the grid instead of spawning Chromedriver.

Parallelism

php artisan dusk --parallel gives real speedups on suites over ~20 tests, but every worker needs its own database — a :memory: SQLite DSN will collide, so use distinct database names per worker (DB_DATABASE=${DB_DATABASE}_${TEST_TOKEN}). Data-provider tests already disambiguate screenshot/console filenames.

Gotchas and Tips

  • APP_URL is load-bearing. Dusk visits exactly that URL. A port mismatch, an https:// in a local http:// app, or a localhost vs 127.0.0.1 split will manifest as instant timeouts, not helpful errors. Test php artisan serve + APP_URL by hand once before debugging tests.

  • Chromedriver/Chrome version drift is the #1 "nothing works" cause. Chrome 115+ changed the download endpoints and Dusk has been patched repeatedly (7.9.0, 8.2.2, 8.2.4). Always run php artisan dusk:chrome-driver --detect in CI; on Chrome release day, run it locally too.

  • Headless defaults surprise people. Tests run headless unless you pass --browse, set the DUSK_HEADLESS_DISABLED env, or override hasHeadlessDisabled() in DuskTestCase. Conversely, CI fails without --no-sandbox in containers — add it via ChromeOptions::addArguments() in driver().

  • Debugging toolkit, in order of usefulness: php artisan dusk --browse → $browser->dump() / dd() (separate methods since 8.1.0) → $browser->screenshot('name') and elementScreenshot('@selector') → storeConsoleLog('name') (captures JS console errors, requires Chromedriver) → storeSource('name') (full DOM snapshot). Console/source/screenshot directories are auto-created if missing (8.2.9), and php artisan dusk:fails re-runs only the previous failures — the tightest inner loop available.

  • Flakes come from missing waits, not from Dusk. The changelog is a catalogue of timing fixes: clickWhen() replaced the misnamed clickOnce(), findButtonByText() now prefers exact text over substring (8.5.0), empty strings passed to assertSeeIn() are proxied (8.5.0). If an assertion behaves oddly, upgrade before you work around it — but read the behaviour-change notes (assertAttributeDoesntContain in 8.2.13).

  • Http::preventStrayRequests() interaction (7.9.3): Dusk boots your real app, so a global fake/prevent registered in a service provider can poison browser requests. Keep HTTP faking out of anything reachable from the Dusk provider path.

  • assertVue is the most brittle API in the package. It depends on injected JS bindings and the dev Vue build, breaks with Vue 3 setups, and silently degrades. Prefer dusk attributes rendered from component state and assert on those.

  • Artifacts in git. tests/Browser/screenshots, /console, /source should be gitignored; keep the .gitignore files Dusk creates inside them. Otherwise every failing CI run churns your repo.

  • Extension points worth knowing before you fork anything:

    • DuskTestCase::driver() — full ChromeOptions/DesiredCapabilities control (downloads dir, window size, proxy, mobile emulation).
    • Browser::macro('name', fn (...) => ...) — add domain helpers like ->seedCart([...]) registered in a service provider or your DuskTestCase::setUp().
    • Custom dusk selector attribute — swap [dusk=...] for [data-testid=...] if your front-end conventions demand it (7.5.0).
    • dusk:page generates Page classes and honours either Laravel\Dusk\Page or your own Tests\Browser\Pages\Page base (8.2.10).
    • Static screenshot/store paths (Browser::$storeScreenshotsAt) for CI artifact upload.
  • Scope discipline matters more than coverage. Dusk suites are 10–100× slower than feature tests. A lean, selector-stable suite of ~15 journey tests survives years; a 200-test Dusk suite gets deleted within two quarters. Push anything expressible as an HTTP-level assertion back into tests/Feature.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
vento/laravel-airbrake
wfeller/laravel-batch
maco-studios/mage-os-installer
busarm/armie
siberfx/laravel-logger
kfteam1991/dcat-admin
dashed/dashed-laposta
dashed/dashed-ecommerce-montaportal
dashed/dashed-ecommerce-etsy
neohq/string-blade-compiler
log1x/acf-composer
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager