Free tool for Laravel teams
Convert Dusk tests to Pest
Paste a Laravel Dusk test and get the Pest 4 browser test back. Methods map one to one where Pest has a match. Everything else gets a TODO.
<?php
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
pest()->use(RefreshDatabase::class);
test('a user can log in', function () {
$user = User::factory()->create();
visit('/login')
->type('email', $user->email)
->type('password', 'password')
->press('Log in')
->assertPathIs('/dashboard')
// TODO: removed ->pause(500). Pest assertions wait on their own
->assertSee('Welcome back');
});
1 thing to check
- line 16removed ->pause(500). Pest assertions wait on their own
Before you run it
composer require pestphp/pest-plugin-browser --dev
npm install playwright@latest
npx playwright installOn pest-plugin-browser 4.x, install playwright@1.61.1 instead.
Watch this test on every PR
- 1Opened /login
- 2Typed into "email"
- 3Typed into "password"
- 4Clicked "Log in"
Diff Stage records your Pest browser tests in GitHub Actions and plays each PR next to main.
How Dusk tests map to Pest
Dusk tests run inside $this->browse() closures on a Browser object. Pest browser tests start with visit() and chain the same kind of actions and assertions. Here is what the converter changes.
- Class-based tests become closures. Each test method becomes a test() or it() call, and setUp() becomes beforeEach().
- Waits become assertions. Pest assertions retry until the timeout, so waitForText('Saved') becomes assertSee('Saved') and waitForLocation() becomes assertPathIs().
- DatabaseMigrations becomes RefreshDatabase. Pest runs your app in the test process, so database transactions work in browser tests.
- loginAs() becomes actingAs(). The converter moves it before the first visit(). Pest logs in one user per test.
- A second visit() becomes navigate(). navigate() keeps the cookies and session of the page you already opened.
- Dusk @selectors keep working. @login becomes [dusk="login"], so it still matches your markup. Pest's own @login shorthand looks for data-testid instead.
- Link text that looks like CSS is protected. Pest reads text like "Next: Pay" as a CSS selector, so the converter uses a Playwright text="Next: Pay" selector.
- Nothing is dropped silently. Calls without a Pest equivalent become a // TODO comment with the original Dusk code underneath.
Dusk to Pest method cheat sheet
These Dusk methods have a different name in pest-plugin-browser.
| Dusk | Pest |
|---|---|
pressAndWaitFor() | press() |
clickLink() | click() |
clickWhenEnabled() | click() |
clickWhenVisible() | click() |
mouseover() | hover() |
assertInputValue() | assertValue() |
assertInputValueIsNot() | assertValueIsNot() |
assertInputPresent() | assertPresent() |
assertInputMissing() | assertNotPresent() |
assertRouteIs() | assertRoute() |
waitFor() | assertVisible() |
waitForInput() | assertVisible() |
waitUntilMissing() | assertMissing() |
waitForText() | assertSee() |
waitUntilMissingText() | assertDontSee() |
waitForTextIn() | assertSeeIn() |
waitForLink() | assertSeeLink() |
waitForLocation() | assertPathIs() |
waitForRoute() | assertRoute() |
waitUntilEnabled() | assertEnabled() |
waitUntilDisabled() | assertDisabled() |
waitUntil() | assertScript() |
Same name in Pest
type(), typeSlowly(), append(), clear(), select(), radio(), check(), uncheck(), attach(), press(), click(), rightClick(), drag(), script(), back(), forward(), refresh(), resize(), screenshotElement(), text(), value(), attribute(), assertTitle(), assertTitleContains(), assertSee(), assertDontSee(), assertSeeIn(), assertDontSeeIn(), assertSeeAnythingIn(), assertSeeNothingIn(), assertCount(), assertScript(), assertSourceHas(), assertSourceMissing(), assertSeeLink(), assertDontSeeLink(), assertChecked(), assertNotChecked(), assertIndeterminate(), assertRadioSelected(), assertRadioNotSelected(), assertSelected(), assertNotSelected(), assertValue(), assertValueIsNot(), assertAttribute(), assertAttributeMissing(), assertAttributeContains(), assertAttributeDoesntContain(), assertAriaAttribute(), assertDataAttribute(), assertVisible(), assertPresent(), assertNotPresent(), assertMissing(), assertEnabled(), assertDisabled(), assertButtonEnabled(), assertButtonDisabled(), assertUrlIs(), assertSchemeIs(), assertSchemeIsNot(), assertHostIs(), assertHostIsNot(), assertPortIs(), assertPortIsNot(), assertPathIs(), assertPathIsNot(), assertPathBeginsWith(), assertPathEndsWith(), assertPathContains(), assertQueryStringHas(), assertQueryStringMissing(), assertFragmentIs(), assertFragmentBeginsWith(), assertFragmentIsNot().
No direct Pest equivalent
These Dusk methods become a TODO in the converted test.
within(): Scope assertions with assertSeeIn() or a CSS selector.with(): Scope assertions with assertSeeIn() or a CSS selector.whenAvailable(): Assert it with assertVisible(), then act on it.maximize(): Use ->resize($width, $height).fitContent(): Use ->resize($width, $height).scrollIntoView(): Playwright scrolls before acting, so this can usually go.scrollTo(): Playwright scrolls before acting, so this can usually go.waitForReload(): Assert what the reloaded page shows.clickAndWaitForReload(): Use ->click(), then assert what the reloaded page shows.waitForEvent(): Assert the content that appears after the event.on(): Copy the page object's assert() checks here.loginAs(): Pest logs in one user per test with $this->actingAs() before the first visit().
Questions
Does it work with Pest 4 and Pest 5?
Yes. pest-plugin-browser has the same browser API in 4.x and 5.x, and every method the converter writes exists in both.
Is my test stored?
No. Your test is converted on our server and sent straight back. We don't save it.
Why does my converted test have TODOs?
Some Dusk features have no Pest equivalent, like within() scopes, dialogs and page objects. The converter keeps those calls as comments so you can rewrite them by hand.
Can I convert a whole test suite?
Paste one file at a time. Class-based Dusk tests and Pest files that call $this->browse() both work.
Moving your browser tests to Pest?
Diff Stage records them on every pull request, so reviewers can watch the change without running your branch.