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

Dust Laravel Package

pestphp/dust

A Pest PHP plugin that surfaces code gathering dust, flagging unused classes, methods, and other dead code in your Laravel or PHP project so you can safely clean it up right from your test suite.

View on GitHub
Deep Wiki
Context7

Getting Started

First, confirm what you actually have. The tagline says Pest plugin for Laravel Dusk; some internal write-ups in circulation describe a dead-code scanner. Open composer.json, src/, and tests/ in the archived repo before you write a line of config — it takes ten minutes and prevents an entire sprint of confusion. Everything below assumes the Dusk-bridge interpretation.

Preconditions. This package is only worth the keystrokes if both halves are already true: Pest is your mandated runner, and Dusk is your chosen browser tool. If either is missing, you are making two large decisions to justify a small convenience. The plugin itself adds no capability — Dusk already drives Chrome, Pest already runs tests. It is a syntax-unification shim.

Minimal install (throwaway branch, timeboxed):

composer require --dev laravel/dusk pestphp/dust
php artisan dusk:install          # scaffolds tests/DuskTestCase.php, tests/Browser/, .env.dusk.local
php artisan dusk:chrome-driver --detect

Where to look first, in this order:

  1. tests/DuskTestCase.php — the generated base test case and the Chrome driver options. Everything browser-related hangs off this class.
  2. tests/Pest.php — where you bind Dusk's base test case to Pest. This one line is the entire integration.
  3. .env.dusk.local / .env.dusk.testing — Dusk boots a separate application instance with its own env file. Point APP_URL at something reachable and give it a dedicated database.
  4. phpunit.dusk.xml (or phpunit.xml group config) — the suite definition used when running browser tests.

First use case — a login smoke test (this is the one you should port, because login breaks when auth, layout, CSRF, JS, or sessions break):

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

// tests/Browser/LoginTest.php
it('lets a user log in', function () {
    $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('@login-button')
            ->waitForLocation('/dashboard')
            ->assertSee('Dashboard');
    });
});

Run it:

./vendor/bin/pest tests/Browser          # via Pest
php artisan dusk tests/Browser/LoginTest.php   # via Artisan (useful for the Dusk-specific flags)

Note the @login-button selector — that's a Dusk dusk="login-button" attribute on the HTML element. Adopt that convention on day one; CSS-class selectors are the single biggest source of false red builds.

Implementation Patterns

Exclude browser tests from the fast loop. This is the pattern that determines whether the team accepts or resents the tool. Browser tests take seconds each; your TDD loop takes milliseconds. Tag the whole directory and exclude it by default:

// tests/Pest.php
uses(Tests\DuskTestCase::class)->group('browser')->in('Browser');
./vendor/bin/pest --exclude-group=browser    # what engineers run 95% of the time
./vendor/bin/pest --group=browser            # pre-push hook / CI nightly

Reuse one browser session per file with beforeEach. Dusk's expensive part is booting Chrome. Sharing the browse session across tests in a file cuts minutes:

beforeEach(function () {
    $this->browse(fn (Browser $b) => $b->loginAs(User::factory()->create()));
});

Page objects, not inline chains. Long fluent chains become unreadable at ~10 steps and unmaintainable at ~5 tests. Put selectors in page objects and keep the Pest test as a narrative:

it('completes checkout', function () {
    $this->browse(function (Browser $browser) {
        $browser->visit(new CheckoutPage)
            ->fillShipping($this->address)
            ->selectShippingMethod('express')
            ->assertSee('Order total: $42.00');
    });
});

This is also your handoff artifact — page objects map to user journeys, so product and design can read the test and argue about scope.

Pest datasets work, and are the best reason to be here. One browser journey × five role/plan combinations is exactly the shape of the E2E suite you want, and Pest expresses it in three lines instead of a loop inside a PHPUnit method:

it('shows the right dashboard', function (string $role, string $expected) {
    $this->browse(function (Browser $b) use ($role, $expected) {
        $b->loginAs(User::factory()->create(['role' => $role]))
          ->visit('/dashboard')
          ->assertSee($expected);
    });
})->with([
    ['admin', 'Team management'],
    ['member', 'My tasks'],
]);

Let the plugin handle only Pest↔Dusk binding; register behaviour in Dusk itself. Custom browser macros belong in a service provider, not in tests/Pest.php, so feature-test helpers and browser tests don't grow tangled:

// app/Providers/DuskServiceProvider.php
Browser::macro('submitLogin', function (string $email) {
    return $this->type('email', $email)->type('password', 'secret')->press('@login-button');
});

Integration tip — test at the seam, not the surface. A healthy split for a small team: feature tests cover every route, validation branch, and policy; browser tests cover only journeys that require PHP + JS + session + rendering together. A practical ceiling is 10–25 browser tests. Below ~20 the syntax-consistency benefit doesn't repay the dependency; above ~100 the maintenance exposure dominates.

Artifact capture on failure is non-negotiable if this ever runs in CI — screenshot + console log + page HTML, uploaded as build artifacts:

$this->browse(fn (Browser $b) => $b->visit('/checkout')->fitOnFailure());

The exit-plan pattern, written before you merge. Because the package is archived, the responsible move is to keep the integration shallow enough to inline. The plugin essentially does one thing; you can replace it with this and remove the dependency entirely:

// tests/Pest.php — replaces the plugin
uses(Tests\DuskTestCase::class)->group('browser')->in('Browser');

Write that file, then decide whether the plugin earns its place. Frequently it doesn't.

Gotchas and Tips

The maintenance risk is the headline, not a footnote. The package is archived, has 13 stars, and 0 dependents. It hooks Pest's plugin/extension API and Dusk's test-case bootstrapping — both of which have moved across major versions. A frozen plugin is scheduled breakage, not hypothetical breakage. If you adopt it: pin the exact resolved version, commit composer.lock, record your PHP × Laravel × Pest matrix in the repo README, and name an owner with a fork-and-patch budget. Adopt away from any Pest or Laravel major upgrade.

browse() is undefined — you forgot the uses() line, or pointed it at the wrong class. The error usually surfaces as Call to undefined method Tests\...::browse(). $this inside a Pest test closure is the bound test case; if that case is not Tests\DuskTestCase (which extends Laravel\Dusk\TestCase), you get a plain HTTP-kernel test case with no browser. Check tests/Pest.php first, always.

Database transactions do not cross the process boundary. Dusk boots a separate PHP process, so RefreshDatabase's transaction wrapping is invisible to the browser. Use DatabaseMigrations or DatabaseTruncation on the Dusk base case, and give .env.dusk.* its own database name. Sharing a database with your fast suite causes cross-process flakiness that looks like "random test order failures."

Never sleep() — always waitFor. Hard sleeps are simultaneously too slow (padding every run) and too fast (failing on a loaded CI box). Prefer waitFor('@selector', 10), waitForText, waitUntilMissing, and raise DuskTestCase::$maxWaitTime once, globally, rather than sprinkling magic numbers.

Selectors: use dusk attributes, not CSS classes or visible text. ->press('Login') breaks the moment a designer changes the copy; ->press('@login') survives every redesign. This single convention is the difference between a suite that gets fixed and a suite that gets skipped.

Pest's --parallel and Dusk do not mix well. Parallel processes collide on the Chrome driver port and on a shared database. Keep the browser group serial (--group=browser without --parallel) even if the fast suite runs parallel. It's the pragmatic default; revisit only if the browser suite grows past a few dozen tests and someone owns it.

expect() doesn't reach into the browser. Dusk assertions live inside the browse() closure against the $browser instance. You can absolutely do $result = $browser->text('@total'); expect($result)->toBe('$42.00'); — but only by hoisting a value out of the closure. Don't try to force Pest's expectation API to cover browser state; it produces contorted tests.

Chrome-driver version skew is the most common "it worked yesterday" failure. php artisan dusk:chrome-driver --detect in CI, pinned to the container's Chrome version. In Docker you'll also want --headless --no-sandbox --disable-dev-shm-usage in the driver options, or you'll get opaque SessionNotCreated / tab-crash errors.

Debugging toolkit, in escalating order: ->dump() (dump state to console, keep going), ->pause(2000) (watch it at 2s/step), ->screenshot('name') (written to tests/Browser/screenshots), ->storeConsoleLog('name') (JS errors, which surface nowhere else), and finally check storage/logs/laravel.log — the Dusk app instance logs exceptions there even when the assertion failure message is useless.

Budget for the flake tax before you pitch it. Getting one green test locally is under a day. Getting a stable, non-flaky CI signal is realistically 2–5 engineering days of driver wiring, wait tuning, and selector cleanup — and the porting of 5–10 tests is a further sprint. If the team won't fund that, don't start; a browser suite that everyone has learned to ignore costs CI budget and returns zero signal.

Extension points are Dusk's, not the plugin's. Browser macros (Browser::macro), page objects, components, and the DuskTestCase driver hook are all upstream Laravel Dusk. The plugin contributes nothing you can extend — another reason to keep the dependency at arm's length and treat it as replaceable glue.

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.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core