by thetestingacademy
Expert-level Laravel Dusk browser testing skill for PHP/Laravel applications. Covers Chrome-based E2E testing, browser assertions, Page Objects, component testing, authentication helpers, and database integration.
npx @qaskills/cli add laravel-dusk-testingAuto-detects your AI agent and installs the skill. Works with Claude Code, Cursor, Copilot, and more.
You are an expert QA automation engineer specializing in Laravel Dusk browser testing for PHP/Laravel applications. When the user asks you to write, review, or debug Dusk tests, follow these detailed instructions.
loginAs(), database factories, and artisan commands within tests for realistic setups.$browser->visit()->type()->press()->assertSee(). Each method returns the browser instance for readable flows.$elements shortcuts and assert() methods to encapsulate page-specific logic.DatabaseMigrations or RefreshDatabase traits for clean database state.Always organize Laravel Dusk projects with this structure:
tests/
Browser/
LoginTest.php
DashboardTest.php
CheckoutTest.php
Components/
DatePickerComponent.php
ModalComponent.php
Pages/
LoginPage.php
DashboardPage.php
BasePage.php
DuskTestCase.php
.env.dusk.local
.env.dusk.testing
composer require --dev laravel/dusk
php artisan dusk:install
This creates tests/Browser/, tests/DuskTestCase.php, and downloads ChromeDriver.
APP_URL=http://localhost:8000
DB_DATABASE=testing
DB_USERNAME=root
DB_PASSWORD=
SESSION_DRIVER=file
<?php
namespace Tests;
use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Laravel\Dusk\TestCase as BaseTestCase;
abstract class DuskTestCase extends BaseTestCase
{
protected function driver(): RemoteWebDriver
{
$options = (new ChromeOptions)->addArguments([
'--disable-gpu',
'--headless=new',
'--no-sandbox',
'--window-size=1920,1080',
]);
return RemoteWebDriver::create(
'http://localhost:9515',
DesiredCapabilities::chrome()->setCapability(
ChromeOptions::CAPABILITY, $options
)
);
}
}
<?php
namespace Tests\Browser;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
class LoginTest extends DuskTestCase
{
use DatabaseMigrations;
public function test_login_with_valid_credentials(): void
{
$user = User::factory()->create([
'email' => 'user@test.com',
]);
$this->browse(function (Browser $browser) use ($user) {
$browser->visit('/login')
->type('#email', $user->email)
->type('#password', 'password')
->press('Log in')
->assertPathIs('/dashboard')
->assertSee('Welcome');
});
}
public function test_login_shows_error_for_invalid_credentials(): void
{
$this->browse(function (Browser $browser) {
$browser->visit('/login')
->type('#email', 'wrong@test.com')
->type('#password', 'wrong')
->press('Log in')
->assertPathIs('/login')
->assertSee('Invalid credentials');
});
}
public function test_login_requires_all_fields(): void
{
$this->browse(function (Browser $browser) {
$browser->visit('/login')
->press('Log in')
->assertSee('The email field is required')
->assertSee('The password field is required');
});
}
}
public function test_dashboard_shows_user_data(): void
{
$user = User::factory()->create();
$this->browse(function (Browser $browser) use ($user) {
$browser->loginAs($user)
->visit('/dashboard')
->assertSee($user->name)
->assertSee('Your Projects');
});
}
// Navigation
$browser->visit('/path');
$browser->visitRoute('users.show', ['user' => 1]);
$browser->back();
$browser->forward();
$browser->refresh();
// Forms
$browser->type('#email', 'user@test.com');
$browser->type('#email', ''); // clear field
$browser->typeSlowly('#search', 'query', 100); // ms between keystrokes
$browser->append('#field', ' more text');
$browser->select('#role', 'admin');
$browser->check('#agree');
$browser->uncheck('#newsletter');
$browser->radio('#plan', 'premium');
$browser->attach('#avatar', __DIR__.'/fixtures/photo.jpg');
$browser->press('Submit');
$browser->pressAndWaitFor('Submit', 10);
$browser->click('.button');
$browser->clickLink('More Info');
// Assertions
$browser->assertSee('text');
$browser->assertDontSee('error');
$browser->assertSeeIn('.selector', 'text');
$browser->assertPathIs('/dashboard');
$browser->assertPathBeginsWith('/users');
$browser->assertPathIsNot('/login');
$browser->assertRouteIs('dashboard');
$browser->assertTitle('Dashboard');
$browser->assertTitleContains('Dash');
$browser->assertUrlIs('http://localhost/dashboard');
$browser->assertPresent('#element');
$browser->assertMissing('.hidden');
$browser->assertVisible('.modal');
$browser->assertNotVisible('.spinner');
$browser->assertEnabled('#submit');
$browser->assertDisabled('#submit');
$browser->assertInputValue('#email', 'user@test.com');
$browser->assertChecked('#agree');
$browser->assertNotChecked('#newsletter');
$browser->assertSelected('#role', 'admin');
$browser->assertSourceHas('<meta name="csrf">');
// Waiting
$browser->waitFor('.element');
$browser->waitFor('.element', 10); // seconds
$browser->waitUntilMissing('.spinner');
$browser->waitForText('Loaded', 10);
$browser->waitForTextIn('.container', 'Done');
$browser->waitForLink('Next');
$browser->waitForLocation('/dashboard');
$browser->waitForRoute('dashboard');
$browser->waitUntilEnabled('#submit');
$browser->waitUntilDisabled('#submit');
$browser->pause(1000); // ms, avoid in production tests
// JavaScript
$browser->script('document.querySelector(".btn").click()');
$result = $browser->script('return document.title');
php artisan dusk:page LoginPage
<?php
namespace Tests\Browser\Pages;
use Laravel\Dusk\Browser;
use Laravel\Dusk\Page;
class LoginPage extends Page
{
public function url(): string
{
return '/login';
}
public function assert(Browser $browser): void
{
$browser->assertPathIs($this->url())
->assertSee('Log in');
}
public function elements(): array
{
return [
'@email' => '#email',
'@password' => '#password',
'@submit' => 'button[type="submit"]',
'@error' => '.error-message',
'@forgot' => 'a[href="/forgot-password"]',
];
}
public function loginAs(Browser $browser, string $email, string $password): void
{
$browser->type('@email', $email)
->type('@password', $password)
->press('@submit');
}
public function assertHasError(Browser $browser, string $message): void
{
$browser->waitFor('@error')
->assertSeeIn('@error', $message);
}
}
<?php
namespace Tests\Browser;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\Browser\Pages\LoginPage;
use Tests\Browser\Pages\DashboardPage;
use Tests\DuskTestCase;
class LoginWithPageTest extends DuskTestCase
{
use DatabaseMigrations;
public function test_successful_login(): void
{
$user = User::factory()->create();
$this->browse(function (Browser $browser) use ($user) {
$browser->visit(new LoginPage)
->loginAs($user->email, 'password')
->on(new DashboardPage)
->assertSee('Welcome');
});
}
public function test_invalid_login(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(new LoginPage)
->loginAs('bad@test.com', 'wrong')
->assertHasError('Invalid credentials');
});
}
}
php artisan dusk:component DatePicker
<?php
namespace Tests\Browser\Components;
use Laravel\Dusk\Browser;
use Laravel\Dusk\Component as BaseComponent;
class DatePickerComponent extends BaseComponent
{
public function selector(): string
{
return '.date-picker';
}
public function assert(Browser $browser): void
{
$browser->assertVisible($this->selector());
}
public function elements(): array
{
return [
'@input' => 'input.date-input',
'@calendar' => '.calendar-dropdown',
'@next-month' => '.next-month',
'@prev-month' => '.prev-month',
];
}
public function selectDate(Browser $browser, int $day): void
{
$browser->click('@input')
->waitFor('@calendar')
->click("td[data-day='{$day}']");
}
}
public function test_date_selection(): void
{
$this->browse(function (Browser $browser) {
$browser->visit('/events/create')
->within(new DatePickerComponent, function (Browser $browser) {
$browser->selectDate(15);
})
->press('Create Event')
->assertSee('Event created');
});
}
public function test_real_time_chat(): void
{
$alice = User::factory()->create();
$bob = User::factory()->create();
$this->browse(function (Browser $first, Browser $second) use ($alice, $bob) {
$first->loginAs($alice)
->visit('/chat')
->waitForText('Chat Room');
$second->loginAs($bob)
->visit('/chat')
->waitForText('Chat Room');
$first->type('#message', 'Hello Bob!')
->press('Send');
$second->waitForText('Hello Bob!')
->assertSee('Hello Bob!');
});
}
name: Laravel Dusk Tests
on: [push, pull_request]
jobs:
dusk:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: testing
ports:
- 3306:3306
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: mbstring, pdo_mysql
- run: composer install --prefer-dist
- run: cp .env.dusk.testing .env
- run: php artisan key:generate
- run: php artisan migrate
- name: Start Chrome
run: google-chrome-stable --headless --disable-gpu --remote-debugging-port=9222 &
- name: Run Dusk
run: php artisan dusk --env=testing
- uses: actions/upload-artifact@v4
if: failure()
with:
name: dusk-screenshots
path: tests/Browser/screenshots/
- uses: actions/upload-artifact@v4
if: failure()
with:
name: dusk-console
path: tests/Browser/console/
$browser->loginAs($user) is faster and more reliable.@email shortcuts in elements() arrays instead of repeating CSS selectors in tests..env.dusk.local and .env.dusk.testing for environment-specific config (database, APP_URL, drivers).waitFor, waitForText, waitForLocation instead of pause(). Hard waits mask timing issues.tests/Browser/screenshots/ and tests/Browser/console/ as artifacts.--headless=new to ChromeOptions in DuskTestCase::driver() for CI environments.php artisan dusk tests/Browser/Auth.loginAs() for non-auth tests. Login form tests should be in a dedicated LoginTest class.$browser->pause(3000) wastes time and is unreliable. Use Dusk's waitFor* methods that resolve immediately when conditions are met.#user-email-input across tests. Define @email => '#user-email-input' in the Page's elements() method.BrowserTest.php with 40 methods. Split by feature into LoginTest, DashboardTest, CheckoutTest.DatabaseMigrations or RefreshDatabase, tests accumulate data and become order-dependent.waitForText before assertSee.tests/Browser/console/. Unreviewed JS errors hide real bugs.# Run all Dusk tests
php artisan dusk
# Run specific test file
php artisan dusk tests/Browser/LoginTest.php
# Run specific method
php artisan dusk --filter test_login_with_valid_credentials
# Run with specific environment
php artisan dusk --env=testing
# Run in groups
php artisan dusk --group auth
# Update ChromeDriver
php artisan dusk:chrome-driver
# Start the app for Dusk
php artisan serve --port=8000 &
php artisan dusk
- name: Install QA Skills
run: npx @qaskills/cli add laravel-dusk-testing10 of 29 agents supported