Laravel Dusk is Laravel's official browser testing tool. It controls a real Chrome browser through ChromeDriver, allowing your tests to click buttons, fill out forms, and wait for JavaScript just like a real user would. Install it with composer require laravel/dusk --dev, run php artisan dusk:install, and execute your tests with php artisan dusk.

Here's why that matters. Your feature tests can pass even when the checkout button does nothing. Feature tests send HTTP requests directly to your routes, so they won't catch a broken Alpine.js component, a missing Livewire update, or a modal covering the submit button.

This Dusk tutorial covers installation, your first test, stable selectors, waiting for JavaScript, Pages and Components, database handling, running Laravel browser tests in CI, and when Pest 4 browser testing is a better choice.

Key Takeaways

  • Laravel Dusk tests your app in a real Chrome browser, catching JavaScript, Livewire, and layout bugs that feature tests can miss.

  • Use dusk="..." attributes and @ selectors instead of CSS classes, so styling changes don't break tests.

  • Never use RefreshDatabase with Dusk. Use DatabaseTruncation (faster) or DatabaseMigrations instead.

  • Replace pause() with waitFor(), waitForText(), and waitUntilMissing() to reduce flaky tests.

  • For new projects, Laravel's documentation now recommends Pest 4 browser testing. Dusk remains a solid choice for existing PHPUnit suites.

The developers in this article's examples are illustrative composites based on common Laravel scenarios.

What Is Laravel Dusk, and When Do You Need It?

Laravel has three layers of testing:

Test type

What it checks

Speed

Unit tests

One class or function in isolation

Milliseconds

Feature tests

HTTP requests, responses, and the database

Fast

Browser tests (Dusk)

The actual page, including JavaScript, in Chrome

Seconds per test

Feature tests should make up most of your test suite. Laravel end-to-end testing with Dusk is useful for flows where the browser itself matters:

  • Signup, login, and password reset

  • Checkout and payment forms

  • Livewire, Alpine.js, Vue, or React interactions

  • Modals, dropdowns, file uploads, and multi-step wizards

  • Any flow where a broken deployment could cost you money

A good rule is to write a Dusk test for every flow that would trigger an urgent message if it broke in production. For most applications, that's a short list, not hundreds of tests.

If you're still building the rest of your test suite, our Laravel testing with AI guide covers unit and feature tests first.

Installing Laravel Dusk

Dusk controls a real browser, so install Google Chrome on your machine first. Then install Dusk as a development dependency and run the installer:

composer require laravel/dusk --dev
php artisan dusk:install

The dusk:install command creates a tests/Browser directory, an example test, and downloads ChromeDriver for your operating system.

Next, set APP_URL in your .env file to the URL you use to open the app in a browser, such as http://localhost:8000.

Security warning: Install Dusk only as a development dependency, and never register its service provider in production. The Dusk documentation warns that doing so could allow anyone to log in as any user.

Keeping ChromeDriver in Sync

A common setup problem occurs when your ChromeDriver version doesn't match your installed version of Chrome. Fix it with:

# Detect your Chrome version and install the matching driver
php artisan dusk:chrome-driver --detect

On Linux and macOS, if the driver won't start, make the binaries executable with chmod -R 0755 vendor/laravel/dusk/bin/.

A Separate Environment for Browser Tests

Create a .env.dusk.local file for test-specific settings, such as a separate database:

APP_URL=http://127.0.0.1:8000
DB_DATABASE=myapp_dusk

Dusk backs up your .env file, switches to the Dusk environment while tests run, and then restores the original. Use a real database, such as MySQL, PostgreSQL, or a file-based SQLite database. An in-memory SQLite database won't work because your browser and test runner operate in separate processes.

Writing Your First Dusk Test

Generate a test with php artisan dusk:make LoginTest. If you use Pest, make sure tests/Pest.php configures browser tests to extend Dusk's base class. When Pest is already installed, dusk:install adds this configuration for you:

// tests/Pest.php
pest()->extend(Tests\DuskTestCase::class)->in('Browser');

Here's a login test using Pest syntax:

use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTruncation;
use Laravel\Dusk\Browser;

pest()->use(DatabaseTruncation::class);

test('a user can log in and reach the dashboard', function () {
    $user = User::factory()->create([
        'email' => '[email protected]',
    ]);

    $this->browse(function (Browser $browser) use ($user) {
        $browser->visit('/login')
            ->type('email', $user->email)
            ->type('password', 'password') // default UserFactory password
            ->press('Log in')
            ->assertPathIs('/dashboard')
            ->assertSee('Welcome back');
    });
});

Run it with:

php artisan dusk                      # all browser tests
php artisan dusk --filter=LoginTest   # only tests matching "LoginTest"
php artisan dusk:fails                # re-run previous failures first and stop at the first failure

When a test fails, Dusk saves a screenshot to tests/Browser/screenshots and the page source to tests/Browser/source. Browser console logs are saved to tests/Browser/console. Check these files first; they often reveal the problem within seconds.

Want to watch the test run? Use php artisan dusk --browse to open a visible browser window instead of running headlessly.

Skip the Login Form with loginAs

Most tests don't need to test the login process itself. Use loginAs() to authenticate directly:

$browser->loginAs($user)
    ->visit('/settings')
    ->assertSee('Profile');

Stable Selectors: Use dusk Attributes

Tests that target CSS classes can break whenever someone restyles a button. Dusk avoids this problem with a dedicated attribute:

<button dusk="save-invoice" class="btn btn-primary">Save</button>
$browser->click('@save-invoice');

The @ prefix tells Dusk to find the element with [dusk="save-invoice"]. Designers can change CSS classes and other styling details without breaking the test.

Lucas, a developer at a small SaaS company, had 40 Dusk tests targeting Tailwind classes. A redesign in May broke 31 of them in a single deployment, and the team nearly deleted the suite. Instead, he spent an afternoon adding dusk attributes to key buttons and inputs. The next redesign broke none of the tests.

Want a test suite without writing it from scratch? StackMivo generates Laravel apps with unit and feature tests included, giving you a starting point for test coverage. You can then add Dusk tests for your critical browser flows.

Waiting for JavaScript: How to Stop Flaky Tests

Flaky tests often come down to one mistake: checking the page before JavaScript has finished running. A quick fix like pause(2000) makes tests slower and can still fail on a busy CI server.

Use Dusk's wait methods instead. Each waits only as long as necessary, up to five seconds by default:

$browser->press('Generate report')
    ->waitForText('Report ready')           // wait for text to appear
    ->waitFor('@download-link')             // wait for an element
    ->waitUntilMissing('.loading-spinner')  // wait for a spinner to disappear
    ->click('@download-link');

Need more time for a slow job? Pass the number of seconds as the second argument: waitForText('Report ready', 15).

For pages that reload after a form submission, wrap the action:

$browser->waitForReload(function (Browser $browser) {
    $browser->press('Submit');
})->assertSee('Saved');

Rania's team had a Dusk suite that failed roughly one run in five on GitHub Actions, with a different test failing each time. Every failing test used pause(). Replacing 23 pauses with waitFor() and waitForText() calls reduced the suite's runtime by 40 seconds and stopped the random failures.

Pages and Components: Keep Tests Readable

As your Laravel browser test suite grows, you'll find the same selectors repeated across files. Pages and Components help keep them in one place.

Pages

Generate a page with php artisan dusk:page Login:

// tests/Browser/Pages/Login.php
namespace Tests\Browser\Pages;

use Laravel\Dusk\Browser;
use Laravel\Dusk\Page;

class Login extends Page
{
    public function url(): string
    {
        return '/login';
    }

    public function assert(Browser $browser): void
    {
        $browser->assertPathIs($this->url());
    }

    public function elements(): array
    {
        return [
            '@email' => 'input[name=email]',
            '@password' => 'input[name=password]',
            '@submit' => 'button[type=submit]',
        ];
    }
}

Tests can then read like a sequence of straightforward steps:

use Tests\Browser\Pages\Login;

$browser->visit(new Login)
    ->type('@email', $user->email)
    ->type('@password', 'password')
    ->click('@submit');

If the login form changes, you only need to update one file.

Components

Components work similarly for UI elements that appear on multiple pages, such as a date picker or navigation menu. Generate one with php artisan dusk:component DatePicker, add your own helper methods (such as selectDate() below), and use it with within():

use Tests\Browser\Components\DatePicker;

$browser->within(new DatePicker, function (Browser $browser) {
    $browser->selectDate(2026, 10, 15);
});

Handling the Database in Dusk Tests

This catches almost everyone at least once. Don't use RefreshDatabase in Dusk tests. It wraps each test in a database transaction, but the browser accesses your application through separate HTTP requests that can't see uncommitted data.

Use one of these alternatives:

  • DatabaseTruncation migrates on the first test, then truncates tables for subsequent tests. It's usually faster. You can limit truncation with $tablesToTruncate or $exceptTables. In Pest, define these properties on DuskTestCase rather than in the test file.

  • DatabaseMigrations runs migrations before each test. It's simpler but slower for large schemas.

Create test data with factories inside each test, as shown in the login example above. Our Laravel seeders and factories guide explains states and relationships that can simplify test setup.

Running Laravel Dusk in CI

Dusk runs headlessly by default, so it works on CI servers without a display. The official GitHub Actions setup comes down to four Dusk-specific steps:

- name: Upgrade Chrome Driver
  run: php artisan dusk:chrome-driver --detect
- name: Start Chrome Driver
  run: ./vendor/laravel/dusk/bin/chromedriver-linux --port=9515 &
- name: Run Laravel Server
  run: php artisan serve --no-reload &
- name: Run Dusk Tests
  run: php artisan dusk

Set APP_URL: "http://127.0.0.1:8000" in the job's env section. Also, add a step to upload tests/Browser/screenshots and tests/Browser/console as artifacts when a job fails. Without those files, debugging failures that occur only in CI becomes guesswork.

You can find the complete workflow in the Laravel Dusk documentation.

Run Dusk after your faster checks. Formatting and unit tests should run first, so you don't have to wait for a browser test suite to catch a missing semicolon. Our guides to Pint's test and dry-run modes and a full Laravel CI/CD pipeline explain how to organize those stages.

Laravel Dusk vs. Pest 4 Browser Testing

Here's something many Dusk tutorials leave out: the current Laravel Dusk documentation recommends Pest 4 for new projects because Pest now includes browser testing with improved performance and usability.

Laravel Dusk

Pest 4 browser testing

Browser engine

Chrome via ChromeDriver

Playwright

Test style

PHPUnit or Pest with $this->browse()

Pest, starting with visit()

Best for

Existing Dusk suites and PHPUnit teams

New projects already using Pest

Maturity

Years of production use

Newer

Here's how to choose:

  • Already have Dusk tests? Keep them. There's no urgent reason to rewrite a working suite.

  • Starting fresh with Pest? Try Pest's browser testing first.

  • Using PHPUnit and planning to stick with it? Dusk is still the natural fit.

Either way, the practices in this guide still apply: use stable selectors (Pest uses data-test attributes where Dusk uses dusk), explicit waits, database truncation instead of transactions, and a short list of critical flows.

Frequently Asked Questions

What Is Laravel Dusk Used For?

Laravel Dusk is used for end-to-end browser testing. It controls a real Chrome browser to test pages that rely on JavaScript, including login flows, checkout, Livewire components, and modals.

Why Do My Dusk Tests Fail with a ChromeDriver Error?

This usually happens when ChromeDriver doesn't match your installed version of Chrome. Run php artisan dusk:chrome-driver --detect to install the matching driver.

Can I Use RefreshDatabase with Laravel Dusk?

No. Dusk's browser makes separate HTTP requests that can't access data inside a test transaction. Use DatabaseTruncation or DatabaseMigrations instead.

Should I Use Laravel Dusk or Pest Browser Testing?

For new projects using Pest, Laravel's documentation recommends Pest 4 browser testing. For existing Dusk suites or teams using PHPUnit exclusively, Dusk remains a good choice.

How Do I See What Went Wrong in a Failed Dusk Test?

Check the screenshot in tests/Browser/screenshots and the logs in tests/Browser/console. In CI, upload both folders as artifacts when a job fails.

Conclusion

Laravel Dusk catches bugs that feature tests can't see, including broken JavaScript, stuck spinners, and buttons that never submit. Keep your suite small and focused on critical flows, use dusk attributes for selectors, replace every pause() with an appropriate wait, and truncate the database instead of wrapping tests in transactions.

Add your first Dusk test for login or checkout this week, then connect it to CI with screenshot artifacts. If you're starting a new project with Pest, consider Pest 4 browser testing as well.

Want a Laravel app that comes with tests from day one? Try StackMivo free →