skip to content

Dusk Browser Automation

Laravel Dusk drives a real Chrome through ChromeDriver with browse(), visit, type and press, waiting on the page. Interviewers ask when it earns its cost and why RefreshDatabase breaks it.

on this pageshow

explore

questions

6

In Laravel 13, how do you set up Laravel Dusk and write a first browser test that signs a new user up?

level: juniorimportance: should knowfreq 30%

answer

  1. dev dependency, then dusk:install
  2. tests/Browser and DuskTestCase
  3. APP_URL must reach a running app
  4. browse() hands you a Browser
  5. php artisan dusk runs the suite

basics

~20 s

Require laravel/dusk as a dev dependency and run php artisan dusk:install, which creates tests/Browser and DuskTestCase and downloads ChromeDriver. Point APP_URL at the running app, chain visit(), type() and press() inside $this->browse(), and run php artisan dusk.

solid answer

~40 s

Install with `composer require laravel/dusk --dev`, then `php artisan dusk:install`: it creates `tests/Browser` with an example test and page classes, writes `tests/DuskTestCase.php`, and downloads a ChromeDriver binary for your OS. Set `APP_URL` to the address where the app is actually served, because Dusk drives a real Chrome against that URL and does not start a web server. A test calls `$this->browse(function (Browser $browser) { ... })` and chains `visit('/register')`, `type('name', 'Ada')`, `type('email', ...)`, `press('Register')`, then `assertPathIs('/dashboard')`. `type()` finds an input or textarea by its `name` when given a plain word; `press()` finds a button by text, name or selector. Run it with `php artisan dusk`, not `php artisan test`. With Pest installed, `dusk:install` wires `tests/Browser` to `DuskTestCase` in `tests/Pest.php`. The Laravel 13 docs now recommend Pest 4's browser testing for new projects.

code

bash · 4 lines
bash
composer require laravel/dusk --dev
php artisan dusk:install
php artisan serve --no-reload &
php artisan dusk

go deeper

for a junior

Recall the install steps, dusk:install, the tests/Browser folder, and the browse() closure with visit, type, press and an assertion.

for a middle

Explain that Dusk drives real Chrome via ChromeDriver against the app at APP_URL, and how type() and press() find elements.

for a senior

Keep Dusk as a dev-only dependency, match ChromeDriver to Chrome, and use a dedicated .env.dusk file so browser runs never touch real data.

for a principal

Decide whether browser tests belong in Dusk or Pest's browser plugin for your stack, weighing existing suites against the docs' current recommendation.

## What Dusk is **Laravel Dusk** is Laravel's first-party browser automation package. A Dusk test starts a real Chrome through **ChromeDriver** (a small server that speaks the WebDriver protocol), points it at your running application, and clicks and types like a user. Because the page runs its own JavaScript, Dusk can test what HTTP feature tests cannot: date pickers, modals, Alpine or Livewire behaviour, redirects done in the browser. ## Installing it 1. `composer require laravel/dusk --dev`. It must stay a **dev** dependency; the package registers login helper routes in every non-production environment. 2. `php artisan dusk:install`. This creates `tests/Browser/` with an `ExampleTest.php` and `Pages/` classes, writes `tests/DuskTestCase.php`, and installs a ChromeDriver binary for your operating system. 3. Set `APP_URL` in `.env` to the URL the app is served on, for example `http://127.0.0.1:8000` with `php artisan serve`. Dusk **does not start your application**; it sends the browser to that URL. 4. Optionally create `.env.dusk.local`: when `php artisan dusk` runs in the `local` environment, it backs up `.env`, swaps in that file for the run, and restores it afterwards. If Chrome on the machine is newer than the downloaded driver, `php artisan dusk:chrome-driver --detect` installs the matching version. ## The generated base class `tests/DuskTestCase.php` extends `Laravel\Dusk\TestCase`. Its `prepare()` method, marked with PHPUnit's `#[BeforeClass]` attribute, starts ChromeDriver on port 9515 (unless running in Sail), and its `driver()` method builds Chrome options: a 1920x1080 window, and `--headless=new` plus `--disable-gpu` unless headless mode is disabled. `DUSK_DRIVER_URL` can point it at a different WebDriver server. ## The first test: a sign-up flow ```php $this->browse(function (Browser $browser) { $browser->visit('/register') ->type('name', 'Ada Lovelace') ->type('email', '[email protected]') ->type('password', 'secret-password') ->type('password_confirmation', 'secret-password') ->press('Register') ->assertPathIs('/dashboard'); }); ``` - `browse()` takes a closure and passes it one `Browser` per parameter, so a two-parameter closure gets two browsers. - `visit()` is relative to `APP_URL`. - `type('email', ...)` clears the field and sends keystrokes. A plain word is matched against an `input` or `textarea` with that `name`; an `#id` or any other CSS or `@` selector is used as given. - `press('Register')` finds a button by selector, name, value or visible text and clicks it. - Assertions such as `assertPathIs()`, `assertSee()` and `assertInputValue()` read the live page. ## Page classes `dusk:install` also creates `tests/Browser/Pages/Page.php` and `HomePage.php`, and `php artisan dusk:page SignupPage` generates more. A page class declares its `url()`, an `assert()` method that checks the browser really is on that page, and an `elements()` map of shorthand selectors such as `'@email' => 'input[name=email]'`. `$browser->visit(new SignupPage)` navigates to the URL and runs the page's assertions, so a sign-up flow reads as steps on named pages rather than raw URLs and selectors. ## Running | Command | Purpose | |---|---| | `php artisan dusk` | run the browser suite (accepts the usual PHPUnit or Pest arguments) | | `php artisan dusk --browse` | open a visible browser instead of headless mode, outside CI | | `php artisan dusk:fails` | re-run only the tests that failed last time | | `php artisan dusk:make SignupTest` | generate a new test in `tests/Browser` | ## Dusk with Pest When Pest is installed, `dusk:install` writes a Pest-style example test and adds `pest()->extend(Tests\DuskTestCase::class)->in('Browser')` to `tests/Pest.php`, so `test('...', function () { $this->browse(...); })` works. That is Dusk running under Pest. Separately, the Laravel 13.x documentation now carries a warning that **Pest 4 includes its own browser testing** and recommends it over Dusk for new projects; Dusk remains supported and is what many existing suites use. ## What to say in an interview - Dusk drives real Chrome via ChromeDriver; no Selenium or JDK is required by default. - The app must be running at `APP_URL`. - `browse()`, `visit()`, `type()`, `press()`, then an assertion. - `php artisan dusk`, not `php artisan test`.

  • The first Dusk run fails with a connection error before any page loads. What do you check first?
    That the app is actually being served at `APP_URL` (Dusk never starts it; run `php artisan serve` or use your local server), and that ChromeDriver matches the installed Chrome. `php artisan dusk:chrome-driver --detect` installs the driver version that matches the detected Chrome.
  • Should a brand-new Laravel 13 project start its browser tests with Dusk?
    The 13.x Dusk docs recommend Pest 4's built-in browser testing for new projects, citing performance and usability. Dusk is still maintained and suits teams with existing Dusk suites or a PHPUnit-only codebase; a new project already on Pest has a reason to start there.

saying these in an interview costs you the question

  • Dusk starts its own web server, so APP_URL does not matter
  • Dusk tests run with php artisan test like feature tests
  • Dusk needs a Selenium server and a JDK installed
  • laravel/dusk belongs in require so production can run browser tests
  • Dusk simulates HTTP requests in-process like the feature test client
open as a page

Why should a Laravel Dusk test never use RefreshDatabase or an in-memory SQLite database, and which trait do you use instead?

level: middleimportance: should knowfreq 32%

basics

~20 s

RefreshDatabase keeps each test's writes in an uncommitted transaction, but Dusk's browser hits a separate server process with its own connection, which cannot see them; :memory: SQLite is per-process too. Use DatabaseTruncation or DatabaseMigrations on a shared database.

open as a page

A Laravel Dusk sign-up test passes locally but fails intermittently in CI after press('Sign up'). How do you make it reliable?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Dusk's press() and assertions never wait, so on a slower CI machine they run before the page updates. Replace pause() with condition waits such as waitForLocation() or waitForText(), and target elements through dusk attributes like @signup-button.

open as a page

When a Laravel Dusk test fails only in CI, which artefacts does Dusk leave behind and how do you use them to find the cause?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

On failure Dusk saves a screenshot per browser to tests/Browser/screenshots; after every browse() it writes non-empty console logs to tests/Browser/console, plus page source after source assertions. Keep them as CI artefacts and read them before changing code.

open as a page

In Laravel Dusk, how does $browser->loginAs($user) authenticate the browser, and what must you watch for when using it?

level: middleimportance: nice to knowfreq 18%

basics

~20 s

loginAs() makes the browser visit Dusk's /_dusk/login/{userId} route, whose controller logs the user in and sets the session cookie. Those routes exist in every non-production environment, and the session persists into later tests in the same file.

open as a page

In a Laravel Dusk sign-up test, how do you fill a birth-date field driven by a JavaScript date picker with a read-only input?

level: seniorimportance: nice to knowfreq 15%

basics

~20 s

Operate the widget: click the input, waitFor() the calendar, click the day via dusk selectors, then assert. type() cannot fill a read-only input, and value() writes the DOM without input or change events, so bound JavaScript state misses it.

open as a page