Instruction file imported from HandsomeStrife/heartfelt-dagger (
.cursor/rules/browser-testing.mdc). Copyright stays with the author.
Pest v4 Browser + Laravel + Livewire — Project Guide
This project uses Pest v4 Browser for UI flows, Pest Laravel for HTTP helpers, and Pest Livewire for component tests. Domain logic lives in
domain/*. Never create or referenceapp/Domainorapp/domain. Follow PSR-12 and project conventions: snake_case variables, camelCase methods. Never use Dusk.
Core Rules (read first)
-
Do not import
visit()— it’s provided globally by the Browser plugin. Use directly:$page = visit('/'); -
Never chain
->actingAs()on a page object.actingAs()is a Laravel test helper; call it beforevisit():actingAs($user); $page = visit('/dashboard'); -
Use page assertions & actions on
$page(assertSee,assertUrlIs,click,fill,press,assertNoJavaScriptErrors,assertScreenshotMatches). Do not mix HTTP response assertions like$page->assertStatus(200). -
Fail fast: if a class/view/route/selector isn’t known, don’t guess — confirm first.
-
Current user retrieval in app code must go through
(new UserRepository())->getLoggedInUser()— neverAuth::user().
When to use which test type
| Test Type | Use For | Typical Helpers |
|---|---|---|
| Browser (UI E2E) | Real UI flows, JS/console errors, accessibility/screenshot checks | visit(), .click(), .fill(), .press(), .assertSee(), .assertNoSmoke() |
| Laravel (HTTP) | Endpoint behavior, auth gates, JSON APIs | actingAs(), get(), post(), assertOk() |
| Livewire (unit/feature) | Component state, validation, authorization, emitted events | livewire(), .set(), .call(), .assertHasErrors() |
Core API
- Start a session / navigate:
visit('/path')→ returns a$pageobject. Chain interactions and assertions on it. ([pestphp.com][1]) - Switch browser: CLI
--browser=firefox|safarior default intests/Pest.php:pest()->browser()->inFirefox();. ([pestphp.com][1]) - Devices & color scheme:
visit('/')->on()->mobile(),->on()->iPhone14Pro(),->onDarkMode(). ([pestphp.com][1]) - Navigate within the same context:
$page->navigate('/about'). ([pestphp.com][1]) - Multiple pages at once:
$pages = visit(['/', '/about']); [...] $pages->assertNoSmoke();(also assert no console/JS errors). ([pestphp.com][1]) - Timeouts: default 5s; configure in
tests/Pest.php:pest()->browser()->timeout(10);. ([pestphp.com][1])
Finding & acting on elements
- Locate by text or CSS (also
@data-test):$page->click('Login'); $page->click('.btn-primary'); $page->click('@login'); $page->click('#submit');Common actions:click,type/keys/fill/append/clear,select,check/uncheck/radio,attach(files),press/pressAndWaitFor,drag,submit,wait/waitForKey,script, etc. (see table “Element Interactions”). ([pestphp.com][1])
Assertions (high level)
- Content:
assertSee,assertDontSee,assertSeeIn,assertCount,assertVisible,assertPresent/NotPresent/Missing. - Form state:
assertChecked/NotChecked/Indeterminate,assertRadioSelected/NotSelected,assertSelected/NotSelected,assertEnabled/Disabled,assertValue/ValueIsNot,assertAttribute*,assertAriaAttribute,assertDataAttribute. - URL parts:
assertUrlIs,assertSchemeIs*,assertHostIs*,assertPortIs*,assertPath*,assertQueryStringHas/Missing,assertFragment*. - JS/console/smoke:
assertNoConsoleLogs,assertNoJavaScriptErrors,assertNoSmoke()(shorthand for both). - Visual:
assertScreenshotMatches(screenshot comparison). Full list is on the doc page’s table of contents. ([pestphp.com][1])
Laravel-native goodness inside browser tests
-
You can still use
RefreshDatabase, model factories,Event::fake(),Notification::assertSent(),assertAuthenticated(), etc., in the same browser tests. This is a core selling point of Pest v4's browser runner. ([pestphp.com][2]) -
Auth first, then UI:
actingAs($user);happens beforevisit(). We never writevisit()->actingAs(). -
Domain consistency: Models live in
domain/*/Models; actions follow(new Action())->execute(...). -
Page interactions: Only use browser-page methods for UI (
click,fill,press, assertions likeassertSee,assertNoJavaScriptErrors). -
Selectors: Prefer
data-testhooks (e.g.,@type-text) to avoid brittle text/CSS selectors. -
Persistence: Use
$this->assertDatabaseHas()to verify results — UI asserts alone aren’t enough. -
Reset behavior: Reopening the modal checks for clean state, catching common UI bugs.
Copy-paste templates (adjust selectors/routes only)
Authenticated start + UI:
use function Pest\Laravel\actingAs;
test('NAME', function () {
$user = \Domain\User\Models\User::factory()->create();
actingAs($user);
$page = visit('/path');
$page->assertSee('Some text')->assertNoJavaScriptErrors();
});
UI flow with form + asserts:
it('NAME', function () {
$page = visit('/start')->on()->mobile()->inDarkMode();
$page->click('Open Form')
->fill('email', 'user@example.com')
->fill('password', 'secret')
->press('Submit')
->assertUrlIs('/done')
->assertSee('Success')
->assertNoSmoke();
});
Livewire (logic/state) — no browser:
use function Pest\Livewire\livewire;
it('validates title', function () {
livewire(\App\Livewire\CreatePost::class)
->set('title', '')
->call('save')
->assertHasErrors('title');
});
LLM “Do / Don’t” (for this repo)
Do
- Use
actingAs($user)before anyvisit()calls. - Keep domain work inside
domain/*(Models, Actions, Repos). - Prefer
data-testselectors for stability. - Verify DB side-effects with
$this->assertDatabaseHas(). - Reference https://pestphp.com/docs/browser-testing directly for what functions exist
Don’t
- ❌ Import or wrap
visit()— use globally. - ❌ Call
visit()->actingAs(...). - ❌ Reference Dusk, Dusk classes, or Dusk APIs.
- ❌ Invent unknown routes/components/selectors — confirm first.