Qaskills Laravel Dusk Testing
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.
install
source · Clone the upstream repo
git clone https://github.com/PramodDutta/qaskills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/PramodDutta/qaskills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/seed-skills/laravel-dusk-testing" ~/.claude/skills/pramoddutta-qaskills-laravel-dusk-testing && rm -rf "$T"
manifest:
seed-skills/laravel-dusk-testing/SKILL.mdsource content
Laravel Dusk Testing Skill
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.
Core Principles
- Laravel-native testing -- Dusk integrates deeply with Laravel. Use
, database factories, and artisan commands within tests for realistic setups.loginAs() - Chrome-powered reliability -- Dusk uses ChromeDriver directly, not WebDriver protocol layers. This gives fast, stable browser automation with automatic ChromeDriver management.
- Fluent browser API -- Chain browser methods:
. Each method returns the browser instance for readable flows.$browser->visit()->type()->press()->assertSee() - Page Objects for encapsulation -- Use Dusk Page classes with
shortcuts and$elements
methods to encapsulate page-specific logic.assert() - Test isolation -- Each test gets a fresh browser session. Use
orDatabaseMigrations
traits for clean database state.RefreshDatabase
Project Structure
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
Setup
Installation
composer require --dev laravel/dusk php artisan dusk:install
This creates
tests/Browser/, tests/DuskTestCase.php, and downloads ChromeDriver.
Environment (.env.dusk.local)
APP_URL=http://localhost:8000 DB_DATABASE=testing DB_USERNAME=root DB_PASSWORD= SESSION_DRIVER=file
DuskTestCase Configuration
<?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 ) ); } }
Basic Test Patterns
Login Test
<?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'); }); } }
Authentication Shortcut
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'); }); }
Browser Methods Reference
// 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');
Page Object Pattern
Generate Page
php artisan dusk:page LoginPage
Login Page Object
<?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); } }
Test Using Page Object
<?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'); }); } }
Component Testing
Generate Component
php artisan dusk:component DatePicker
DatePicker Component
<?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}']"); } }
Using Components in Tests
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'); }); }
Multiple Browser Testing
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!'); }); }
CI/CD Integration
GitHub Actions
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/
Best Practices
- Use loginAs() for authenticated tests -- Skip the login form when testing non-auth features.
is faster and more reliable.$browser->loginAs($user) - Use Page element shortcuts -- Define
shortcuts in@email
arrays instead of repeating CSS selectors in tests.elements() - Separate .env.dusk files -- Create
and.env.dusk.local
for environment-specific config (database, APP_URL, drivers)..env.dusk.testing - Use DatabaseMigrations trait -- Ensures each test starts with a fresh database. Pair with factories for consistent, readable test data.
- Wait explicitly for dynamic content -- Use
,waitFor
,waitForText
instead ofwaitForLocation
. Hard waits mask timing issues.pause() - Test with multiple browsers -- Dusk supports multiple browser instances for testing real-time features, WebSockets, and collaborative flows.
- Component classes for reusable widgets -- Extract date pickers, modals, and dropdowns into Component classes for reuse across tests.
- Upload screenshots and console on failure -- Configure CI to upload
andtests/Browser/screenshots/
as artifacts.tests/Browser/console/ - Run headless in CI -- Add
to ChromeOptions in--headless=new
for CI environments.DuskTestCase::driver() - Group tests by feature -- Organize tests into subdirectories by feature area. Run subsets with
.php artisan dusk tests/Browser/Auth
Anti-Patterns
- Logging in via the form for every test -- Use
for non-auth tests. Login form tests should be in a dedicated LoginTest class.loginAs() - Hardcoded pause() calls --
wastes time and is unreliable. Use Dusk's$browser->pause(3000)
methods that resolve immediately when conditions are met.waitFor* - Not using element shortcuts -- Repeating
across tests. Define#user-email-input
in the Page's@email => '#user-email-input'
method.elements() - Testing with real external services -- Dusk tests should mock payment providers, email services, and third-party APIs at the application level.
- Single massive test class -- One
with 40 methods. Split by feature intoBrowserTest.php
,LoginTest
,DashboardTest
.CheckoutTest - Not cleaning database between tests -- Without
orDatabaseMigrations
, tests accumulate data and become order-dependent.RefreshDatabase - Asserting on flash messages without waiting -- Flash messages that appear via JavaScript need
beforewaitForText
.assertSee - Ignoring console errors -- Dusk captures browser console output in
. Unreviewed JS errors hide real bugs.tests/Browser/console/ - Using raw ChromeDriver commands -- Bypassing Dusk's API with raw WebDriver calls defeats the purpose of the fluent API and loses Dusk's waiting behavior.
- Not using Dusk Pages for complex flows -- Inline selectors and assertions in test methods for multi-step flows become unmaintainable. Use Page Objects.
Run Commands
# 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