# Search Results

## Http Tests Validation Assertions assertInvalid
Http Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md

#### assertInvalid

Assert that the response has validation errors for the given keys. This method may be used for asserting against responses where the validation errors are returned as a JSON structure or where the validation errors have been flashed to the session:

```php
$response->assertInvalid(['name', 'email']);
```

You may also assert that a given key has a particular validation error message. When doing so, you may provide the entire message or only a small portion of the message:

```php
$response->assertInvalid([
    'name' => 'The name field is required.',
    'email' => 'valid email address',
]);
```

---

## Pest V4 Is Here Now With Browser Testing
Pest V4 Is Here Now With Browser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/pest-v4-is-here-now-with-browser-testing.md

---
title: Pest v4 Is Here — Now with Browser Testing
description: Today, we're thrilled to announce Pest v4 — our biggest release yet, featuring powerful new browser testing with parallel support and full Laravel integration.
---

> To get started with Pest v4's new features including browser testing, please refer to the upgrade guide: [Upgrade Guide →](/docs/upgrade-guide).

- [Browser Testing](#content-pest-v4-is-here-now-with-browser-testing)
- [Smoke Testing](#content-smoke-testing)
- [Visual Regression Testing](#content-visual-regression-testing)
- [Test Sharding](#content-test-sharding)
- [Type Coverage Is Much Faster](#content-type-coverage-is-much-faster)
- [Profanity Checking](#content-profanity-checking)
- [On Top of PHPUnit 12](#content-on-top-of-phpunit-12)

# Pest v4 Is Here — Now with Browser Testing

Today, we're thrilled to announce the release of **Pest v4**, bringing the biggest testing upgrade yet: powerful **[Browser Testing](/docs/browser-testing)**. Pest's new browser testing features let you run elegant, maintainable browser tests — with first-class support for Laravel's testing API and the ability to run tests in parallel. For the first time, this is browser testing that feels as good as writing unit tests.

Here is the creator of Pest, [Nuno Maduro](https://twitter.com/enunomaduro), demoing the new browser testing features in Pest v4 at Laracon US:

[![Pest v4](/assets/pest4_play.png)](https://youtu.be/f5gAgwwwwOI?si=LtPpySZe3tf8qMjz&t=52)

Here is an example of Browser Testing using [Laravel](https://laravel.com):

```php
it('may reset the password', function () {
    // access any laravel testing helpers...
    Notification::fake();

    // access to the database — using the RefreshDatabase trait (even sqlite in memory...)
    $this->actingAs(User::factory()->create());

    $page = visit('/sign-in') // visit on a real browser...
        ->on()->mobile() // or ->desktop(), ->tablet(), etc...
        ->inDarkMode(); // or ->inLightMode()

    $page->assertSee('Sign In')
         ->click('Forgot Password?')
         ->type('email', 'nuno@laravel.com')
         ->press('Send Reset Link')
         ->assertSee('We have emailed your password reset link!')
         ->assertNoJavascriptErrors(); // or ->assertNoConsoleLogs()

    Notification::assertSent(ResetPassword::class);
});
```

With Pest v4's browser testing, you can:
- Seamlessly use **Laravel features** like `Event::fake()`, `assertAuthenticated()`, and model factories
- Use `RefreshDatabase`, even with SQLite in-memory databases, to ensure a clean state for each test
- Test on **multiple browsers** (Chrome, Firefox, Safari)
- Test on **different devices** and viewports (like iPhone 14 Pro, tablets, or custom breakpoints)
- Switch **color schemes** (light/dark mode)
- Interact with the page (click, type, scroll, select, submit, drag-and-drop, touch gestures, etc.)
- Run **parallel browser tests** for dramatically faster suites
- Take **screenshots** or pause tests for debugging
- …all with the elegance of Pest syntax
- **Playwright-based** — modern, fast, and reliable

To get started with browser testing in Pest, you need to install the Pest Browser Plugin:

```bash
composer require pestphp/pest-plugin-browser --dev

npm install playwright@latest
npx playwright install
```

After, you may use the `visit()` function anywhere. Finally, running this test is as simple as executing `./vendor/bin/pest` in your terminal. Pest will handle the rest, launching a browser, navigating to the page, and performing the actions you specified.

## Smoke Testing

Smoke testing your application in real browsers has never been easier. With Pest v4, you can literally visit all your application pages, and ensure they don't throw any JavaScript errors, and they don't log any console errors.

```php
$routes = ['/', '/about', '/contact'];

visit($routes)->assertNoSmoke();

// assertNoSmoke() is a shorthand for:
// - assertNoJavascriptErrors()
// - assertNoConsoleLogs()
```

## Visual Regression Testing

Want to ensure your pages look exactly as expected over time? Pest v4 introduces visual regression testing with the `assertScreenshotMatches()` assertion. This allows you to take screenshots of your pages and compare them against baseline images, ensuring that your UI remains consistent across changes.

```php
$pages = visit(['/', '/about', '/contact']);

$pages->assertScreenshotMatches();
```

This is just a glimpse of what Browser Testing in Pest v4 can do. Find out more about the new features below, and check out the [Browser Testing documentation](/docs/browser-testing) for a complete guide on how to get started.

## Test Sharding

Pest v4 introduces **Test Sharding**, allowing you to split your test suite into smaller, manageable chunks. This is particularly useful for large applications (or when running browser tests) where running all tests at once can be time-consuming.

This feature is particularly useful on CI platforms, where on things like GitHub actions you can no longer scale vertical, but rather horizontally. This means you can run your tests in parallel across multiple machines, significantly speeding up your test suite execution.

To get started with Test Sharding, you can use the `--shard` option when running Pest:

```bash
# GitHub Workflow One
./vendor/bin/pest --shard=1/4

# GitHub Workflow Two
./vendor/bin/pest --shard=2/4

# GitHub Workflow Three
./vendor/bin/pest --shard=3/4

# GitHub Workflow Four
./vendor/bin/pest --shard=4/4
```

You may combine this with the `--parallel` option to run your tests in parallel, and this way trully maximize your test suite execution speed:

```bash
./vendor/bin/pest --shard=1/4 --parallel
```

Finally, to easily set up sharding your CI configuration, you just have to ensure each job in your CI configuration runs a different shard of your test suite. For example, in GitHub Actions, you can use the `matrix` strategy to define multiple jobs that run different shards:

```yaml
strategy:
  matrix:
    shard: [1, 2, 3, 4]

name: Tests (Shard ${{ matrix.shard }}/4)

steps:
  - name: Run tests
    run: pest --parallel --shard ${{ matrix.shard }}/4
```

## Type Coverage Is Much Faster

Remember the days when you had to wait for your type coverage to run? Not anymore! Pest v4 introduces a new type coverage engine that is significantly faster than previous versions.

Type coverage is 2x faster on the first run and instant on subsequent runs. This means you can quickly check your type coverage without waiting for long periods, making your development workflow much more efficient.

In addition, Type Coverage now supports **Sharding**. This means you can run type coverage with the `--shard` option, just like you do with your tests.

## Profanity Checking

Pest v4 introduces a new feature that allows you to check for profanity in your test code. This is particularly useful for maintaining a clean and professional codebase, especially in collaborative environments.

You can enable profanity checking by adding the `--profanity` option when running Pest:

```bash
./vendor/bin/pest --profanity
```

To start using Pest's Profanity plugin, you need to require the plugin via Composer.

```bash
composer require pestphp/pest-plugin-profanity --dev
```

After requiring the plugin, you may utilize the `--profanity` option to generate a report of your profanity.

```bash
./vendor/bin/pest --profanity
```

If any of your files contain profanity, they will be highlighted in red and displayed using their respective line
numbers and the profane word(s) that have been found.

As an example, `pr31(f*ck)` means that the word "fuck" was found on line 31.

To learn more about the Profanity plugin and how to configure it, check out the [Profanity documentation](/docs/profanity).

### Skip Locally or On CI

Pest v4 introduces the ability to conditionally skip tests based on the environment. You can use `skipLocally()` to skip tests when running locally, or `skipOnCi` to skip tests when running on a CI server.

```php
it('does not run locally', function () {
    // This test will be skipped when running locally
})->skipLocally();

it('does not run on CI', function () {
    // This test will be skipped when running on a CI server
})->skipOnCi();
```

### Miscellaneous Improvements

- You may now use `skipLocally()` or `skipOnCi` to conditionally skip tests based on the environment.
- The `not->toHaveSuspiciousCharacters()` arch expectation has been added to help you identify potential suspicious characters in your code. This arch expectation is now enabled by default on the `php` arch preset. This expectation requires the `intl` PHP extension.
- The expectation `toBeSlug` has been added to help you validate that a string is a valid slug.

### On Top of PHPUnit 12

Pest v4 is built on top of PHPUnit 12, which means you get all the latest features and improvements from PHPUnit. As such, be sure to check out the [PHPUnit 12 release announcement](https://phpunit.de/announcements/phpunit-12.html).

### Thanks To You, Pest v4 Is Here!

There's never been a better time to dive into testing and start using Pest. If you're ready to get started with Pest v4 right away, check out our [installation guide](/docs/installation) for step-by-step instructions. And if you're currently using an earlier version of Pest, we've got you covered with detailed upgrade instructions in our [upgrade guide](/docs/upgrade-guide).

Thank you for your continued support and feedback. We can't wait to see what you build with Pest v4!

---

Thank you for reading about Pest v4's new features! Want to get started with Pest? You can find the installation guide in the next section of the documentation: [Installation →](/docs/installation)

---

## Browser Testing waitForKey
Browser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md

### waitForKey

The `waitForKey` method opens the current page URL in the default web browser and waits for a key press:

```php
$page->waitForKey(); // Useful for debugging
```

## Debugging tests

Sometimes you may want to debug your browser tests. Pest provides a convenient way to do this by using the `--debug` option, which makes pest to open the browser window and pause the execution of the test when it fails. You can then inspect the page and see what went wrong.

```bash
./vendor/bin/pest --debug
```

Optionally, you can also use the `debug()` method in your test. It will limit execution to this test (like using [`only()`](filtering-tests.md#only)), pause the execution and open the browser window:

```php
$page->debug();
```

You can also take a screenshot of the current page using the `screenshot()` method. This is useful for visual debugging:

NOTE: If you don't pass the filename, it will use the test name as the filename.

```php
$page->screenshot();
$page->screenshot(fullPage: true);
$page->screenshot(filename: 'custom-name');
```

You can also take a screenshot of a specific element using the `screenshotElement()` method:

```php
$page->screenshotElement('#my-element');
```

You can also use the `tinker()` method to open a Tinker session in the context of the current page. This allows you to interact with the page using PHP code:

```php
$page->tinker();
```

After you can run your tests with the `--headed` option to open the browser window:

```bash
./vendor/bin/pest --headed
```

If you wish to run the tests in a headed mode by default, you can set it in your `Pest.php` configuration file:

```php
pest()->browser()->headed();
```

## Continuous Integration

You may refer to Pest's [Continuous Integration](https://pestphp.com/docs/continuous-integration) documentation for more information on how to run your browser tests in a CI environment.

However, if you are using GitHub Actions, you need to add the following steps to your workflow file:

```yaml
    - uses: actions/setup-node@v4
      with:
        node-version: lts/*

    - name: Install dependencies
      run: npm ci

    - name: Install Playwright Browsers
      run: npx playwright install --with-deps
```

---

Now, let's dive into architectural testing and how it can benefit your development process. By performing architectural testing, you can evaluate the overall design of your application and identify potential flaws before they become significant issues: [Arch Testing](/docs/arch-testing)

---

## Test Impact Analysis
Test Impact Analysis | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/test-impact-analysis.md

---
title: Test Impact Analysis (TIA)
description: Test Impact Analysis is a great way to drastically reduce the time it takes to run your test suite by re-running only the tests affected by your latest changes.
---

# Test Impact Analysis (TIA)

Test Impact Analysis is a great way to drastically reduce the time it takes to run your test suite by re-running only the tests affected by your latest changes. The first time you run with `--tia`, Pest records a graph of which tests depend on which files. Every run after that, Pest looks at what you changed, runs only the tests that touched those files, and replays cached results for everything else.

A typical Laravel suite that takes 15 seconds replays in under a second. Edits to a single Blade template re-run a handful of feature tests. Comment-only edits, formatter passes, and README touches re-run nothing at all.

To get started, just add the `--tia` flag to any Pest invocation:

```bash
./vendor/bin/pest --parallel --tia
```

The first run is the **baseline** — Pest enables a coverage driver (PCOV or Xdebug) and records the dependency graph as your tests execute. Expect a small overhead on this run only.

Every subsequent run is a **replay**. Pest compares your working tree against the baseline and re-runs only the tests affected by your changes:

```
Tests:    774 passed (2658 assertions, 7 affected, 767 replayed)
Duration: 0.74s
```

`affected` is the set of tests Pest actually executed. `replayed` is the set whose results were served from cache.

## How Pest Decides What To Run

For each file you've changed, Pest looks for tests that depend on it:

- **PHP source files** — your `app/` classes, controllers, models, helpers — are tracked through the coverage driver. A change to `app/Models/User.php` re-runs only the tests that touched `User`.
- **Migrations** are intersected with the tables each test queried during the baseline. A column rename in `create_users_table.php` re-runs only the tests that queried the `users` table.
- **Inertia pages** under `resources/js/Pages` re-run only the tests that server-side-rendered them.
- **Shared JS components** under `resources/js/Components`, `Layouts`, and friends re-run only the tests whose pages import them — Pest walks Vite's module graph to figure this out.
- **Blade templates** re-run only the tests that rendered them, including renders triggered by browser tests.
- **Anything else** — config files, route files, `.env`, files outside the recorded graph — falls through to a broader pattern. Editing `config/app.php` re-runs every test, because Pest can't statically prove which tests depend on it.

## Cosmetic Edits Don't Run Anything

Pest normalises file content before comparing, so cosmetic changes don't trigger any tests. PHP files have whitespace, line comments, and docblocks stripped before hashing. Blade strips `{{-- … --}}` comments. JS, TS, Vue, and Svelte files have line and block comments removed too.

The result: a comment-only edit, a Prettier reformat, a Pint pass, or a README tweak produces an identical hash, and the file never enters the changed set. Zero tests run.

## Modes

Pest supports a few flags alongside `--tia`:

| Flag | Behaviour |
|---|---|
| `--tia` | Replay if a baseline graph exists, otherwise record. |
| `--tia --fresh` | Discard any existing graph and re-record from scratch. Use this after large refactors or when the graph feels stale. |
| `--tia --refetch` | Force a CI baseline fetch even within the 24-hour cooldown after a previous failed fetch.

## Sharing The Baseline From CI

Recording the baseline locally takes minutes on large suites. Instead, you can have CI record it once per merge to `main`, and every developer downloads the result.

When Pest detects no local graph (or the local graph is out of date), it uses GitHub's CLI to download the latest successful run of a `tia-baseline.yml` workflow's `pest-tia-baseline` artifact. Pest validates the fetched graph against your project state — if it matches, it's adopted. Otherwise it's discarded and a local rebuild proceeds.

Here's a starter workflow you can drop into `.github/workflows/tia-baseline.yml`:

```yaml
name: TIA Baseline
on:
  push: { branches: [main] }
  schedule: [{ cron: '0 3 * * *' }]
  workflow_dispatch:
jobs:
  baseline:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: shivammathur/setup-php@v2
        with: { php-version: '8.5', coverage: xdebug }
      - run: composer install --no-interaction --prefer-dist
      - run: ./vendor/bin/pest --parallel --tia --coverage
      - name: Stage baseline for upload
        shell: bash
        run: |
          mkdir -p .pest-tia-baseline
          cp -R "$HOME/.pest/tia"/*/. .pest-tia-baseline/
      - uses: actions/upload-artifact@v4
        with:
          name: pest-tia-baseline
          path: .pest-tia-baseline/
          retention-days: 30
```

After CI runs, every developer who runs `pest --tia` for the first time on the repo will download this baseline and start replaying immediately, paying no record cost.

## Storage

Pest stores its state at `~/.pest/tia/<project-key>/`, where the project key is derived from your normalised git remote URL — so `git@github.com:foo/bar.git` and `https://github.com/foo/bar` produce the same key. A non-git project falls back to a hash of the project's absolute path.

Sharing state per remote URL means multiple worktrees of the same repository share one cache, while unrelated projects on the same machine stay isolated.

## Custom Watch Patterns

If your project has a directory layout that doesn't match the framework defaults, you can register custom watch patterns in `tests/Pest.php`:

```php
pest()->tia()->watch([
    'config/billing/**/*.php' => ['tests/Feature/Billing'],

    //
]);
```

Each glob maps to a list of test directories; whenever a matching file changes, every test under those directories is invalidated. Use this when you have specific test directories that genuinely depend on a file Pest can't see otherwise.

---

Now that you've learned how to use Test Impact Analysis to speed up your test suite, let's discuss how to integrate Pest with your continuous integration workflow: [Continuous Integration](/docs/continuous-integration)

---

## Dusk Browser Basics Cookies
Dusk | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/dusk.md

### Cookies

You may use the `cookie` method to get or set an encrypted cookie's value. By default, all of the cookies created by Laravel are encrypted:

```php
$browser->cookie('name');

$browser->cookie('name', 'Taylor');
```

You may use the `plainCookie` method to get or set an unencrypted cookie's value:

```php
$browser->plainCookie('name');

$browser->plainCookie('name', 'Taylor');
```

You may use the `deleteCookie` method to delete the given cookie:

```php
$browser->deleteCookie('name');
```

---

## Dusk Browser Basics Authentication
Dusk | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/dusk.md

### Authentication

Often, you will be testing pages that require authentication. You can use Dusk's `loginAs` method in order to avoid interacting with your application's login screen during every test. The `loginAs` method accepts a primary key associated with your authenticatable model or an authenticatable model instance:

```php
use App\Models\User;
use Laravel\Dusk\Browser;

$this->browse(function (Browser $browser) {
    $browser->loginAs(User::find(1))
        ->visit('/home');
});
```

> [!WARNING]
> After using the `loginAs` method, the user session will be maintained for all tests within the file.

---

## Socialite Testing
Socialite | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/socialite.md

## Testing

Laravel Socialite provides a convenient way to test OAuth authentication flows without making actual requests to OAuth providers. The `fake` method allows you to mock the OAuth provider's behavior and define the user data that should be returned.

---

## Browser Testing wait
Browser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md

### wait

The `wait` method pauses for the given number of seconds:

```php
$page->wait(2); // Wait for 2 seconds
```

---

## Filtering Tests `only()`
Filtering Tests | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/filtering-tests.md

### `only()`

During development, you may want to focus on running specific tests while excluding all others. Pest provides two ways to do this: running only tests in a specific file, or running only a specific test within a file.

#### Running Only Tests in a File

When working on a specific feature, you can mark all tests in a file to run exclusively by calling the `pest()->only()` function at the top of your test file:

```php
<?php

pest()->only();

test('first test', function () {
    // This will run
});

test('second test', function () {
    // This will also run
});
```

All tests in files with `pest()->only()` will run, while tests in other files will be skipped.

#### Running a Single Test

To run only a specific test within a file, chain the `->only()` method to the test:

```php
test('sum', function () {
  $result = sum(1, 2);

  expect($result)->toBe(3);
})->only();

test('another test', function () {
  // This will be skipped
});
```

---

As your codebase grows, manually running your tests with filtering can become tedious. That's where skipping tests comes in. Skipping tests is a useful feature that allows developers to exclude specific tests from the test suite temporarily without deleting them entirely: [Skipping Tests →](/docs/skipping-tests)

---

## Browser Testing url
Browser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md

### url

The `url` method gets the page's URL:

```php
$currentUrl = $page->url();
```

---

## Browser Testing check
Browser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md

### check

The `check` method checks the given checkbox:

```php
$page->check('terms');
$page->check('color', 'blue'); // For checkbox with specific value
```

_[Truncated]_


Output:
Wall time: 3.3543 seconds
Output:
[{"type":"text","text":"# Search Results\n\n## Http Tests Validation Assertions assertInvalid\nHttp Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md\n\n\n#### assertInvalid\n\nAssert that the response has validation errors for the given keys. This method may be used for asserting against responses where the validation errors are returned as a JSON structure or where the validation errors have been flashed to the session:\n\n```php\n$response->assertInvalid(['name', 'email']);\n```\n\nYou may also assert that a given key has a particular validation error message. When doing so, you may provide the entire message or only a small portion of the message:\n\n```php\n$response->assertInvalid([\n    'name' => 'The name field is required.',\n    'email' => 'valid email address',\n]);\n```\n\n---\n\n## Pest V4 Is Here Now With Browser Testing\nPest V4 Is Here Now With Browser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/pest-v4-is-here-now-with-browser-testing.md\n\n---\ntitle: Pest v4 Is Here — Now with Browser Testing\ndescription: Today, we're thrilled to announce Pest v4 — our biggest release yet, featuring powerful new browser testing with parallel support and full Laravel integration.\n---\n\n> To get started with Pest v4's new features including browser testing, please refer to the upgrade guide: [Upgrade Guide →](/docs/upgrade-guide).\n\n- [Browser Testing](#content-pest-v4-is-here-now-with-browser-testing)\n- [Smoke Testing](#content-smoke-testing)\n- [Visual Regression Testing](#content-visual-regression-testing)\n- [Test Sharding](#content-test-sharding)\n- [Type Coverage Is Much Faster](#content-type-coverage-is-much-faster)\n- [Profanity Checking](#content-profanity-checking)\n- [On Top of PHPUnit 12](#content-on-top-of-phpunit-12)\n\n# Pest v4 Is Here — Now with Browser Testing\n\nToday, we're thrilled to announce the release of **Pest v4**, bringing the biggest testing upgrade yet: powerful **[Browser Testing](/docs/browser-testing)**. Pest's new browser testing features let you run elegant, maintainable browser tests — with first-class support for Laravel's testing API and the ability to run tests in parallel. For the first time, this is browser testing that feels as good as writing unit tests.\n\nHere is the creator of Pest, [Nuno Maduro](https://twitter.com/enunomaduro), demoing the new browser testing features in Pest v4 at Laracon US:\n\n[![Pest v4](/assets/pest4_play.png)](https://youtu.be/f5gAgwwwwOI?si=LtPpySZe3tf8qMjz&t=52)\n\nHere is an example of Browser Testing using [Laravel](https://laravel.com):\n\n```php\nit('may reset the password', function () {\n    // access any laravel testing helpers...\n    Notification::fake();\n\n    // access to the database — using the RefreshDatabase trait (even sqlite in memory...)\n    $this->actingAs(User::factory()->create());\n\n    $page = visit('/sign-in') // visit on a real browser...\n        ->on()->mobile() // or ->desktop(), ->tablet(), etc...\n        ->inDarkMode(); // or ->inLightMode()\n\n    $page->assertSee('Sign In')\n         ->click('Forgot Password?')\n         ->type('email', 'nuno@laravel.com')\n         ->press('Send Reset Link')\n         ->assertSee('We have emailed your password reset link!')\n         ->assertNoJavascriptErrors(); // or ->assertNoConsoleLogs()\n\n    Notification::assertSent(ResetPassword::class);\n});\n```\n\nWith Pest v4's browser testing, you can:\n- Seamlessly use **Laravel features** like `Event::fake()`, `assertAuthenticated()`, and model factories\n- Use `RefreshDatabase`, even with SQLite in-memory databases, to ensure a clean state for each test\n- Test on **multiple browsers** (Chrome, Firefox, Safari)\n- Test on **different devices** and viewports (like iPhone 14 Pro, tablets, or custom breakpoints)\n- Switch **color schemes** (light/dark mode)\n- Interact with the page (click, type, scroll, select, submit, drag-and-drop, touch gestures, etc.)\n- Run **parallel browser tests** for dramatically faster suites\n- Take **screenshots** or pause tests for debugging\n- …all with the elegance of Pest syntax\n- **Playwright-based** — modern, fast, and reliable\n\nTo get started with browser testing in Pest, you need to install the Pest Browser Plugin:\n\n```bash\ncomposer require pestphp/pest-plugin-browser --dev\n\nnpm install playwright@latest\nnpx playwright install\n```\n\nAfter, you may use the `visit()` function anywhere. Finally, running this test is as simple as executing `./vendor/bin/pest` in your terminal. Pest will handle the rest, launching a browser, navigating to the page, and performing the actions you specified.\n\n## Smoke Testing\n\nSmoke testing your application in real browsers has never been easier. With Pest v4, you can literally visit all your application pages, and ensure they don't throw any JavaScript errors, and they don't log any console errors.\n\n```php\n$routes = ['/', '/about', '/contact'];\n\nvisit($routes)->assertNoSmoke();\n\n// assertNoSmoke() is a shorthand for:\n// - assertNoJavascriptErrors()\n// - assertNoConsoleLogs()\n```\n\n## Visual Regression Testing\n\nWant to ensure your pages look exactly as expected over time? Pest v4 introduces visual regression testing with the `assertScreenshotMatches()` assertion. This allows you to take screenshots of your pages and compare them against baseline images, ensuring that your UI remains consistent across changes.\n\n```php\n$pages = visit(['/', '/about', '/contact']);\n\n$pages->assertScreenshotMatches();\n```\n\nThis is just a glimpse of what Browser Testing in Pest v4 can do. Find out more about the new features below, and check out the [Browser Testing documentation](/docs/browser-testing) for a complete guide on how to get started.\n\n## Test Sharding\n\nPest v4 introduces **Test Sharding**, allowing you to split your test suite into smaller, manageable chunks. This is particularly useful for large applications (or when running browser tests) where running all tests at once can be time-consuming.\n\nThis feature is particularly useful on CI platforms, where on things like GitHub actions you can no longer scale vertical, but rather horizontally. This means you can run your tests in parallel across multiple machines, significantly speeding up your test suite execution.\n\nTo get started with Test Sharding, you can use the `--shard` option when running Pest:\n\n```bash\n# GitHub Workflow One\n./vendor/bin/pest --shard=1/4\n\n# GitHub Workflow Two\n./vendor/bin/pest --shard=2/4\n\n# GitHub Workflow Three\n./vendor/bin/pest --shard=3/4\n\n# GitHub Workflow Four\n./vendor/bin/pest --shard=4/4\n```\n\nYou may combine this with the `--parallel` option to run your tests in parallel, and this way trully maximize your test suite execution speed:\n\n```bash\n./vendor/bin/pest --shard=1/4 --parallel\n```\n\nFinally, to easily set up sharding your CI configuration, you just have to ensure each job in your CI configuration runs a different shard of your test suite. For example, in GitHub Actions, you can use the `matrix` strategy to define multiple jobs that run different shards:\n\n```yaml\nstrategy:\n  matrix:\n    shard: [1, 2, 3, 4]\n\nname: Tests (Shard ${{ matrix.shard }}/4)\n\nsteps:\n  - name: Run tests\n    run: pest --parallel --shard ${{ matrix.shard }}/4\n```\n\n## Type Coverage Is Much Faster\n\nRemember the days when you had to wait for your type coverage to run? Not anymore! Pest v4 introduces a new type coverage engine that is significantly faster than previous versions.\n\nType coverage is 2x faster on the first run and instant on subsequent runs. This means you can quickly check your type coverage without waiting for long periods, making your development workflow much more efficient.\n\nIn addition, Type Coverage now supports **Sharding**. This means you can run type coverage with the `--shard` option, just like you do with your tests.\n\n## Profanity Checking\n\nPest v4 introduces a new feature that allows you to check for profanity in your test code. This is particularly useful for maintaining a clean and professional codebase, especially in collaborative environments.\n\nYou can enable profanity checking by adding the `--profanity` option when running Pest:\n\n```bash\n./vendor/bin/pest --profanity\n```\n\nTo start using Pest's Profanity plugin, you need to require the plugin via Composer.\n\n```bash\ncomposer require pestphp/pest-plugin-profanity --dev\n```\n\nAfter requiring the plugin, you may utilize the `--profanity` option to generate a report of your profanity.\n\n```bash\n./vendor/bin/pest --profanity\n```\n\n\n\nIf any of your files contain profanity, they will be highlighted in red and displayed using their respective line\nnumbers and the profane word(s) that have been found.\n\nAs an example, `pr31(f*ck)` means that the word \"fuck\" was found on line 31.\n\nTo learn more about the Profanity plugin and how to configure it, check out the [Profanity documentation](/docs/profanity).\n\n### Skip Locally or On CI\n\nPest v4 introduces the ability to conditionally skip tests based on the environment. You can use `skipLocally()` to skip tests when running locally, or `skipOnCi` to skip tests when running on a CI server.\n\n```php\nit('does not run locally', function () {\n    // This test will be skipped when running locally\n})->skipLocally();\n\nit('does not run on CI', function () {\n    // This test will be skipped when running on a CI server\n})->skipOnCi();\n```\n\n### Miscellaneous Improvements\n\n- You may now use `skipLocally()` or `skipOnCi` to conditionally skip tests based on the environment.\n- The `not->toHaveSuspiciousCharacters()` arch expectation has been added to help you identify potential suspicious characters in your code. This arch expectation is now enabled by default on the `php` arch preset. This expectation requires the `intl` PHP extension.\n- The expectation `toBeSlug` has been added to help you validate that a string is a valid slug.\n\n### On Top of PHPUnit 12\n\nPest v4 is built on top of PHPUnit 12, which means you get all the latest features and improvements from PHPUnit. As such, be sure to check out the [PHPUnit 12 release announcement](https://phpunit.de/announcements/phpunit-12.html).\n\n### Thanks To You, Pest v4 Is Here!\n\nThere's never been a better time to dive into testing and start using Pest. If you're ready to get started with Pest v4 right away, check out our [installation guide](/docs/installation) for step-by-step instructions. And if you're currently using an earlier version of Pest, we've got you covered with detailed upgrade instructions in our [upgrade guide](/docs/upgrade-guide).\n\nThank you for your continued support and feedback. We can't wait to see what you build with Pest v4!\n\n---\n\nThank you for reading about Pest v4's new features! Want to get started with Pest? You can find the installation guide in the next section of the documentation: [Installation →](/docs/installation)\n\n\n\n---\n\n## Browser Testing waitForKey\nBrowser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md\n\n\n### waitForKey\n\nThe `waitForKey` method opens the current page URL in the default web browser and waits for a key press:\n\n```php\n$page->waitForKey(); // Useful for debugging\n```\n\n## Debugging tests\n\n\nSometimes you may want to debug your browser tests. Pest provides a convenient way to do this by using the `--debug` option, which makes pest to open the browser window and pause the execution of the test when it fails. You can then inspect the page and see what went wrong.\n\n```bash\n./vendor/bin/pest --debug\n```\n\nOptionally, you can also use the `debug()` method in your test. It will limit execution to this test (like using [`only()`](filtering-tests.md#only)), pause the execution and open the browser window:\n\n```php\n$page->debug();\n```\n\n\n\nYou can also take a screenshot of the current page using the `screenshot()` method. This is useful for visual debugging:\n\nNOTE: If you don't pass the filename, it will use the test name as the filename.\n\n```php\n$page->screenshot();\n$page->screenshot(fullPage: true);\n$page->screenshot(filename: 'custom-name');\n```\n\n\nYou can also take a screenshot of a specific element using the `screenshotElement()` method:\n\n```php\n$page->screenshotElement('#my-element');\n```\n\n\nYou can also use the `tinker()` method to open a Tinker session in the context of the current page. This allows you to interact with the page using PHP code:\n\n```php\n$page->tinker();\n```\n\n\nAfter you can run your tests with the `--headed` option to open the browser window:\n\n```bash\n./vendor/bin/pest --headed\n```\n\nIf you wish to run the tests in a headed mode by default, you can set it in your `Pest.php` configuration file:\n\n```php\npest()->browser()->headed();\n```\n\n## Continuous Integration\n\nYou may refer to Pest's [Continuous Integration](https://pestphp.com/docs/continuous-integration) documentation for more information on how to run your browser tests in a CI environment.\n\nHowever, if you are using GitHub Actions, you need to add the following steps to your workflow file:\n\n```yaml\n    - uses: actions/setup-node@v4\n      with:\n        node-version: lts/*\n\n    - name: Install dependencies\n      run: npm ci\n\n    - name: Install Playwright Browsers\n      run: npx playwright install --with-deps\n```\n\n---\n\nNow, let's dive into architectural testing and how it can benefit your development process. By performing architectural testing, you can evaluate the overall design of your application and identify potential flaws before they become significant issues: [Arch Testing](/docs/arch-testing)\n\n---\n\n## Test Impact Analysis\nTest Impact Analysis | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/test-impact-analysis.md\n\n---\ntitle: Test Impact Analysis (TIA)\ndescription: Test Impact Analysis is a great way to drastically reduce the time it takes to run your test suite by re-running only the tests affected by your latest changes.\n---\n\n# Test Impact Analysis (TIA)\n\nTest Impact Analysis is a great way to drastically reduce the time it takes to run your test suite by re-running only the tests affected by your latest changes. The first time you run with `--tia`, Pest records a graph of which tests depend on which files. Every run after that, Pest looks at what you changed, runs only the tests that touched those files, and replays cached results for everything else.\n\nA typical Laravel suite that takes 15 seconds replays in under a second. Edits to a single Blade template re-run a handful of feature tests. Comment-only edits, formatter passes, and README touches re-run nothing at all.\n\nTo get started, just add the `--tia` flag to any Pest invocation:\n\n```bash\n./vendor/bin/pest --parallel --tia\n```\n\nThe first run is the **baseline** — Pest enables a coverage driver (PCOV or Xdebug) and records the dependency graph as your tests execute. Expect a small overhead on this run only.\n\nEvery subsequent run is a **replay**. Pest compares your working tree against the baseline and re-runs only the tests affected by your changes:\n\n```\nTests:    774 passed (2658 assertions, 7 affected, 767 replayed)\nDuration: 0.74s\n```\n\n`affected` is the set of tests Pest actually executed. `replayed` is the set whose results were served from cache.\n\n## How Pest Decides What To Run\n\nFor each file you've changed, Pest looks for tests that depend on it:\n\n- **PHP source files** — your `app/` classes, controllers, models, helpers — are tracked through the coverage driver. A change to `app/Models/User.php` re-runs only the tests that touched `User`.\n- **Migrations** are intersected with the tables each test queried during the baseline. A column rename in `create_users_table.php` re-runs only the tests that queried the `users` table.\n- **Inertia pages** under `resources/js/Pages` re-run only the tests that server-side-rendered them.\n- **Shared JS components** under `resources/js/Components`, `Layouts`, and friends re-run only the tests whose pages import them — Pest walks Vite's module graph to figure this out.\n- **Blade templates** re-run only the tests that rendered them, including renders triggered by browser tests.\n- **Anything else** — config files, route files, `.env`, files outside the recorded graph — falls through to a broader pattern. Editing `config/app.php` re-runs every test, because Pest can't statically prove which tests depend on it.\n\n## Cosmetic Edits Don't Run Anything\n\nPest normalises file content before comparing, so cosmetic changes don't trigger any tests. PHP files have whitespace, line comments, and docblocks stripped before hashing. Blade strips `{{-- … --}}` comments. JS, TS, Vue, and Svelte files have line and block comments removed too.\n\nThe result: a comment-only edit, a Prettier reformat, a Pint pass, or a README tweak produces an identical hash, and the file never enters the changed set. Zero tests run.\n\n## Modes\n\nPest supports a few flags alongside `--tia`:\n\n| Flag | Behaviour |\n|---|---|\n| `--tia` | Replay if a baseline graph exists, otherwise record. |\n| `--tia --fresh` | Discard any existing graph and re-record from scratch. Use this after large refactors or when the graph feels stale. |\n| `--tia --refetch` | Force a CI baseline fetch even within the 24-hour cooldown after a previous failed fetch.\n\n## Sharing The Baseline From CI\n\nRecording the baseline locally takes minutes on large suites. Instead, you can have CI record it once per merge to `main`, and every developer downloads the result.\n\nWhen Pest detects no local graph (or the local graph is out of date), it uses GitHub's CLI to download the latest successful run of a `tia-baseline.yml` workflow's `pest-tia-baseline` artifact. Pest validates the fetched graph against your project state — if it matches, it's adopted. Otherwise it's discarded and a local rebuild proceeds.\n\nHere's a starter workflow you can drop into `.github/workflows/tia-baseline.yml`:\n\n```yaml\nname: TIA Baseline\non:\n  push: { branches: [main] }\n  schedule: [{ cron: '0 3 * * *' }]\n  workflow_dispatch:\njobs:\n  baseline:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n        with: { fetch-depth: 0 }\n      - uses: shivammathur/setup-php@v2\n        with: { php-version: '8.5', coverage: xdebug }\n      - run: composer install --no-interaction --prefer-dist\n      - run: ./vendor/bin/pest --parallel --tia --coverage\n      - name: Stage baseline for upload\n        shell: bash\n        run: |\n          mkdir -p .pest-tia-baseline\n          cp -R \"$HOME/.pest/tia\"/*/. .pest-tia-baseline/\n      - uses: actions/upload-artifact@v4\n        with:\n          name: pest-tia-baseline\n          path: .pest-tia-baseline/\n          retention-days: 30\n```\n\nAfter CI runs, every developer who runs `pest --tia` for the first time on the repo will download this baseline and start replaying immediately, paying no record cost.\n\n## Storage\n\nPest stores its state at `~/.pest/tia/<project-key>/`, where the project key is derived from your normalised git remote URL — so `git@github.com:foo/bar.git` and `https://github.com/foo/bar` produce the same key. A non-git project falls back to a hash of the project's absolute path.\n\nSharing state per remote URL means multiple worktrees of the same repository share one cache, while unrelated projects on the same machine stay isolated.\n\n## Custom Watch Patterns\n\nIf your project has a directory layout that doesn't match the framework defaults, you can register custom watch patterns in `tests/Pest.php`:\n\n```php\npest()->tia()->watch([\n    'config/billing/**/*.php' => ['tests/Feature/Billing'],\n\n    //\n]);\n```\n\nEach glob maps to a list of test directories; whenever a matching file changes, every test under those directories is invalidated. Use this when you have specific test directories that genuinely depend on a file Pest can't see otherwise.\n\n---\n\nNow that you've learned how to use Test Impact Analysis to speed up your test suite, let's discuss how to integrate Pest with your continuous integration workflow: [Continuous Integration](/docs/continuous-integration)\n\n\n---\n\n## Dusk Browser Basics Cookies\nDusk | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/dusk.md\n\n\n### Cookies\n\nYou may use the `cookie` method to get or set an encrypted cookie's value. By default, all of the cookies created by Laravel are encrypted:\n\n```php\n$browser->cookie('name');\n\n$browser->cookie('name', 'Taylor');\n```\n\nYou may use the `plainCookie` method to get or set an unencrypted cookie's value:\n\n```php\n$browser->plainCookie('name');\n\n$browser->plainCookie('name', 'Taylor');\n```\n\nYou may use the `deleteCookie` method to delete the given cookie:\n\n```php\n$browser->deleteCookie('name');\n```\n\n---\n\n## Dusk Browser Basics Authentication\nDusk | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/dusk.md\n\n\n### Authentication\n\nOften, you will be testing pages that require authentication. You can use Dusk's `loginAs` method in order to avoid interacting with your application's login screen during every test. The `loginAs` method accepts a primary key associated with your authenticatable model or an authenticatable model instance:\n\n```php\nuse App\\Models\\User;\nuse Laravel\\Dusk\\Browser;\n\n$this->browse(function (Browser $browser) {\n    $browser->loginAs(User::find(1))\n        ->visit('/home');\n});\n```\n\n> [!WARNING]\n> After using the `loginAs` method, the user session will be maintained for all tests within the file.\n\n---\n\n## Socialite Testing\nSocialite | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/socialite.md\n\n\n## Testing\n\nLaravel Socialite provides a convenient way to test OAuth authentication flows without making actual requests to OAuth providers. The `fake` method allows you to mock the OAuth provider's behavior and define the user data that should be returned.\n\n---\n\n## Browser Testing wait\nBrowser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md\n\n\n### wait\n\nThe `wait` method pauses for the given number of seconds:\n\n```php\n$page->wait(2); // Wait for 2 seconds\n```\n\n---\n\n## Filtering Tests `only()`\nFiltering Tests | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/filtering-tests.md\n\n\n### `only()`\n\nDuring development, you may want to focus on running specific tests while excluding all others. Pest provides two ways to do this: running only tests in a specific file, or running only a specific test within a file.\n\n#### Running Only Tests in a File\n\nWhen working on a specific feature, you can mark all tests in a file to run exclusively by calling the `pest()->only()` function at the top of your test file:\n\n```php\n<?php\n\npest()->only();\n\ntest('first test', function () {\n    // This will run\n});\n\ntest('second test', function () {\n    // This will also run\n});\n```\n\nAll tests in files with `pest()->only()` will run, while tests in other files will be skipped.\n\n#### Running a Single Test\n\nTo run only a specific test within a file, chain the `->only()` method to the test:\n\n```php\ntest('sum', function () {\n  $result = sum(1, 2);\n\n  expect($result)->toBe(3);\n})->only();\n\ntest('another test', function () {\n  // This will be skipped\n});\n```\n\n---\n\nAs your codebase grows, manually running your tests with filtering can become tedious. That's where skipping tests comes in. Skipping tests is a useful feature that allows developers to exclude specific tests from the test suite temporarily without deleting them entirely: [Skipping Tests →](/docs/skipping-tests)\n\n---\n\n## Browser Testing url\nBrowser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md\n\n\n### url\n\nThe `url` method gets the page's URL:\n\n```php\n$currentUrl = $page->url();\n```\n\n---\n\n## Browser Testing check\nBrowser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md\n\n\n### check\n\nThe `check` method checks the given checkbox:\n\n```php\n$page->check('terms');\n$page->check('color', 'blue'); // For checkbox with specific value\n```\n\n_[Truncated]_"}]


sed -n '50,95p' tests/Browser/SmokeTest.php


sed -n '50,95p' tests/Browser/SmokeTest.php

    ], browserSmokeHost());

    $pages->assertNoJavaScriptErrors();

    [$home, $collections, $collectionPage, $productPage, $search, $contentPage, $cart] = $pages;

    $home->assertSee($store->name);
    $collections->assertSee('Collections');
    $collectionPage->assertSee($collection->title);
    $productPage->assertSee($product->title);
    $search->assertSee('Search');
    $contentPage->assertSee($page->title);
    $cart->assertSee('Cart');
});

test('admin core pages render for an authenticated store user', function (): void {
    $store = browserSmokeStore();
    $user = User::query()->where('email', 'admin@acme.test')->firstOrFail();

    $this->actingAs($user);
    $this->withSession(['current_store_id' => $store->getKey()]);

    $expected = [
        '/admin' => 'Dashboard',
        '/admin/analytics' => 'Analytics',
        '/admin/apps' => 'Apps',
        '/admin/developers' => 'Developers',
        '/admin/products' => 'Products',
        '/admin/collections' => 'Collections',
        '/admin/inventory' => 'Inventory',
        '/admin/orders' => 'Orders',
        '/admin/customers' => 'Customers',
        '/admin/discounts' => 'Discounts',
        '/admin/pages' => 'Pages',
        '/admin/navigation' => 'Navigation',
        '/admin/themes' => 'Themes',
        '/admin/settings' => 'Settings',
        '/admin/settings/shipping' => 'Shipping',
        '/admin/settings/taxes' => 'Taxes',
        '/admin/settings/checkout' => 'Checkout',
        '/admin/settings/notifications' => 'Notifications',
        '/admin/search/settings' => 'Search',
    ];

    $pages = visit(array_keys($expected), browserSmokeHost());


Output:
Chunk ID: e020e1
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 414
Output:
    ], browserSmokeHost());

    $pages->assertNoJavaScriptErrors();

    [$home, $collections, $collectionPage, $productPage, $search, $contentPage, $cart] = $pages;

    $home->assertSee($store->name);
    $collections->assertSee('Collections');
    $collectionPage->assertSee($collection->title);
    $productPage->assertSee($product->title);
    $search->assertSee('Search');
    $contentPage->assertSee($page->title);
    $cart->assertSee('Cart');
});

test('admin core pages render for an authenticated store user', function (): void {
    $store = browserSmokeStore();
    $user = User::query()->where('email', 'admin@acme.test')->firstOrFail();

    $this->actingAs($user);
    $this->withSession(['current_store_id' => $store->getKey()]);

    $expected = [
        '/admin' => 'Dashboard',
        '/admin/analytics' => 'Analytics',
        '/admin/apps' => 'Apps',
        '/admin/developers' => 'Developers',
        '/admin/products' => 'Products',
        '/admin/collections' => 'Collections',
        '/admin/inventory' => 'Inventory',
        '/admin/orders' => 'Orders',
        '/admin/customers' => 'Customers',
        '/admin/discounts' => 'Discounts',
        '/admin/pages' => 'Pages',
        '/admin/navigation' => 'Navigation',
        '/admin/themes' => 'Themes',
        '/admin/settings' => 'Settings',
        '/admin/settings/shipping' => 'Shipping',
        '/admin/settings/taxes' => 'Taxes',
        '/admin/settings/checkout' => 'Checkout',
        '/admin/settings/notifications' => 'Notifications',
        '/admin/search/settings' => 'Search',
    ];

    $pages = visit(array_keys($expected), browserSmokeHost());


sed -n '95,135p' tests/Browser/SmokeTest.php


sed -n '95,135p' tests/Browser/SmokeTest.php

    $pages->assertNoJavaScriptErrors();

    foreach ($pages as $index => $page) {
        $page->assertSee(array_values($expected)[$index]);
    }
});

test('storefront account auth pages render without javascript errors', function (): void {
    $pages = visit([
        '/account/login',
        '/account/register',
        '/forgot-password',
        '/reset-password/test-token?email=customer@example.test',
        '/account/forgot-password',
        '/account/reset-password/test-token?email=customer@example.test',
    ], browserSmokeHost());

    $pages->assertNoJavaScriptErrors();

    [$login, $register, $forgotPassword, $resetPassword, $accountForgotPassword, $accountResetPassword] = $pages;

    $login->assertSee('Log in');
    $register->assertSee('Create an account');
    $forgotPassword->assertSee('Reset password');
    $resetPassword->assertSee('New password');
    $accountForgotPassword->assertSee('Reset password');
    $accountResetPassword->assertSee('New password');
});

test('storefront home renders on a mobile viewport', function (): void {
    $store = browserSmokeStore();

    visit('/', browserSmokeHost())
        ->on()
        ->mobile()
        ->assertSee($store->name)
        ->assertNoJavaScriptErrors();
});


Output:
Chunk ID: 01b29d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 314
Output:

    $pages->assertNoJavaScriptErrors();

    foreach ($pages as $index => $page) {
        $page->assertSee(array_values($expected)[$index]);
    }
});

test('storefront account auth pages render without javascript errors', function (): void {
    $pages = visit([
        '/account/login',
        '/account/register',
        '/forgot-password',
        '/reset-password/test-token?email=customer@example.test',
        '/account/forgot-password',
        '/account/reset-password/test-token?email=customer@example.test',
    ], browserSmokeHost());

    $pages->assertNoJavaScriptErrors();

    [$login, $register, $forgotPassword, $resetPassword, $accountForgotPassword, $accountResetPassword] = $pages;

    $login->assertSee('Log in');
    $register->assertSee('Create an account');
    $forgotPassword->assertSee('Reset password');
    $resetPassword->assertSee('New password');
    $accountForgotPassword->assertSee('Reset password');
    $accountResetPassword->assertSee('New password');
});

test('storefront home renders on a mobile viewport', function (): void {
    $store = browserSmokeStore();

    visit('/', browserSmokeHost())
        ->on()
        ->mobile()
        ->assertSee($store->name)
        ->assertNoJavaScriptErrors();
});


The robust fix is to stop making this product CRUD suite depend on whatever browser cookies a previous browser test left behind. The repo’s smoke tests already use Laravel’s `actingAs()` plus `withSession()` for authenticated browser visits, so I’m switching Suite 3 to that pattern while leaving the dedicated admin authentication suite responsible for login-form coverage.


Input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/tests/Browser/Admin/ProductManagementTest.php
@@
 <?php

+use App\Models\Store;
+use App\Models\User;
 use Database\Seeders\DatabaseSeeder;
 use Illuminate\Foundation\Testing\RefreshDatabase;
 use Pest\Browser\Playwright\Playwright;
@@
-function adminProductLogin(): mixed
+function adminProductLogin(mixed $testCase): mixed
 {
-    return visit('/admin/login', adminProductHost())
-        ->fill('input[type=email]', 'admin@acme.test')
-        ->fill('input[type=password]', 'password')
-        ->click('@admin-login-button')
+    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
+    $user = User::query()->where('email', 'admin@acme.test')->firstOrFail();
+
+    $testCase->actingAs($user);
+    $testCase->withSession(['current_store_id' => $store->getKey()]);
+
+    return visit('/admin', adminProductHost())
         ->wait(1)
         ->assertPathIs('/admin')
         ->assertSee('Dashboard')
         ->assertNoJavaScriptErrors();
 }

-function adminProductOpenProducts(): mixed
+function adminProductOpenProducts(mixed $testCase): mixed
 {
-    return adminProductLogin()
+    return adminProductLogin($testCase)
         ->click('a[href$="/admin/products"]')
         ->wait(1)
         ->assertPathIs('/admin/products')
@@
 test('shows the product list with seeded products', function (): void {
-    adminProductOpenProducts()
+    adminProductOpenProducts($this)
         ->assertSee('Classic Cotton T-Shirt')
         ->assertSee('Premium Slim Fit Jeans')
         ->assertNoJavaScriptErrors();
 });

 test('can create a new product', function (): void {
-    $page = adminProductOpenProducts()
+    $page = adminProductOpenProducts($this)
         ->click('a[href$="/admin/products/create"]')
         ->wait(1)
         ->assertPathIs('/admin/products/create')
@@
 test('can edit an existing product title', function (): void {
-    $page = adminProductOpenProducts()
+    $page = adminProductOpenProducts($this)
         ->assertSee('Classic Cotton T-Shirt')
         ->click('a:has-text("Classic Cotton T-Shirt")')
         ->wait(1)
@@
 test('can archive a product', function (): void {
-    $page = adminProductOpenProducts()
+    $page = adminProductOpenProducts($this)
         ->click('a[href$="/admin/products/create"]')
         ->wait(1)
         ->assertPathIs('/admin/products/create')
@@
 test('shows draft products only in admin and not storefront', function (): void {
-    $page = adminProductOpenProducts()
+    $page = adminProductOpenProducts($this)
         ->select('select[wire\\:model\\.live="statusFilter"]', 'draft')
         ->wait(1)
         ->assertSee('Unreleased Winter Jacket')
@@
 test('can search products in admin', function (): void {
-    adminProductOpenProducts()
+    adminProductOpenProducts($this)
         ->fill('input[wire\\:model\\.live\\.debounce\\.300ms="search"]', 'Cotton')
         ->wait(1)
         ->assertSee('Classic Cotton T-Shirt')
@@
 test('can filter products by status in admin', function (): void {
-    adminProductOpenProducts()
+    adminProductOpenProducts($this)
         ->select('select[wire\\:model\\.live="statusFilter"]', 'draft')
         ->wait(1)
         ->assertSee('Unreleased Winter Jacket')
*** End Patch


Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/tests/Browser/Admin/ProductManagementTest.php

@@ -2,2 +2,4 @@

+use App\Models\Store;
+use App\Models\User;
 use Database\Seeders\DatabaseSeeder;
@@ -23,8 +25,11 @@

-function adminProductLogin(): mixed
+function adminProductLogin(mixed $testCase): mixed
 {
-    return visit('/admin/login', adminProductHost())
-        ->fill('input[type=email]', 'admin@acme.test')
-        ->fill('input[type=password]', 'password')
-        ->click('@admin-login-button')
+    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
+    $user = User::query()->where('email', 'admin@acme.test')->firstOrFail();
+
+    $testCase->actingAs($user);
+    $testCase->withSession(['current_store_id' => $store->getKey()]);
+
+    return visit('/admin', adminProductHost())
         ->wait(1)
@@ -35,5 +40,5 @@

-function adminProductOpenProducts(): mixed
+function adminProductOpenProducts(mixed $testCase): mixed
 {
-    return adminProductLogin()
+    return adminProductLogin($testCase)
         ->click('a[href$="/admin/products"]')
@@ -98,3 +103,3 @@
 test('shows the product list with seeded products', function (): void {
-    adminProductOpenProducts()
+    adminProductOpenProducts($this)
         ->assertSee('Classic Cotton T-Shirt')
@@ -105,3 +110,3 @@
 test('can create a new product', function (): void {
-    $page = adminProductOpenProducts()
+    $page = adminProductOpenProducts($this)
         ->click('a[href$="/admin/products/create"]')
@@ -131,3 +136,3 @@
 test('can edit an existing product title', function (): void {
-    $page = adminProductOpenProducts()
+    $page = adminProductOpenProducts($this)
         ->assertSee('Classic Cotton T-Shirt')
@@ -149,3 +154,3 @@
 test('can archive a product', function (): void {
-    $page = adminProductOpenProducts()
+    $page = adminProductOpenProducts($this)
         ->click('a[href$="/admin/products/create"]')
@@ -181,3 +186,3 @@
 test('shows draft products only in admin and not storefront', function (): void {
-    $page = adminProductOpenProducts()
+    $page = adminProductOpenProducts($this)
         ->select('select[wire\\:model\\.live="statusFilter"]', 'draft')
@@ -202,3 +207,3 @@
 test('can search products in admin', function (): void {
-    adminProductOpenProducts()
+    adminProductOpenProducts($this)
         ->fill('input[wire\\:model\\.live\\.debounce\\.300ms="search"]', 'Cotton')
@@ -211,3 +216,3 @@
 test('can filter products by status in admin', function (): void {
-    adminProductOpenProducts()
+    adminProductOpenProducts($this)
         ->select('select[wire\\:model\\.live="statusFilter"]', 'draft')


Output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/tests/Browser/Admin/ProductManagementTest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Output:
Chunk ID: 0ff8d0
Wall time: 0.2893 seconds
Process exited with code 0
Original token count: 5
Output:
{"result":"pass"}


/bin/zsh
-lc
vendor/bin/pint --dirty --format agent


php artisan test --compact tests/Browser/Admin/ProductManagementTest.php


Output:
Chunk ID: 2f9723
Wall time: 1.0011 seconds
Process running with session ID 96711
Original token count: 0
Output:


Output:
Chunk ID: ac0d94
Wall time: 5.0019 seconds
Process running with session ID 96711
Original token count: 0
Output:


Output:
Chunk ID: b28564
Wall time: 5.0016 seconds
Process running with session ID 96711
Original token count: 0
Output:


Output:
Chunk ID: d744d5
Wall time: 5.0023 seconds
Process running with session ID 96711
Original token count: 0
Output:


Output:
Chunk ID: ac2940
Wall time: 4.9669 seconds
Process exited with code 0
Original token count: 18
Output:

  .......

  Tests:    7 passed (98 assertions)
  Duration: 43.16s


php artisan test --compact tests/Browser/Admin/ProductManagementTest.php

  .......

  Tests:    7 passed (98 assertions)
  Duration: 43.16s


The affected Suite 3 file is green again with the deterministic auth setup. I’m rerunning the full suite now because the previous failure only appeared in the full browser order.


php -d memory_limit=512M vendor/bin/pest --compact


Output:
Chunk ID: 70d9d9
Wall time: 1.0016 seconds
Process running with session ID 93204
Original token count: 1
Output:

  .


Output:
Chunk ID: 043ce8
Wall time: 5.0017 seconds
Process running with session ID 93204
Original token count: 7
Output:
..........................


Output:
Chunk ID: c5e2ff
Wall time: 5.0015 seconds
Process running with session ID 93204
Original token count: 9
Output:
..................................


Output:
Chunk ID: a00cd7
Wall time: 5.0015 seconds
Process running with session ID 93204
Original token count: 9
Output:
...............
  ..................


Output:
Chunk ID: 35c72c
Wall time: 5.0016 seconds
Process running with session ID 93204
Original token count: 22
Output:
..........................................................
  .........................


Output:
Chunk ID: 4184ea
Wall time: 5.0023 seconds
Process running with session ID 93204
Original token count: 16
Output:
...................................................
  .........


The rerun is in the non-browser portion and remains clean. I’ll keep polling through the browser phase where the previous state leak showed up.


Output:
Chunk ID: 245e76
Wall time: 5.0011 seconds
Process running with session ID 93204
Original token count: 3
Output:
............


Output:
Chunk ID: 7fbddf
Wall time: 5.0011 seconds
Process running with session ID 93204
Original token count: 0
Output:


Output:
Chunk ID: 99c6b8
Wall time: 5.0002 seconds
Process running with session ID 93204
Original token count: 3
Output:
..........


Output:
Chunk ID: 31f5e5
Wall time: 5.0021 seconds
Process running with session ID 93204
Original token count: 0
Output:


Output:
Chunk ID: 826ed5
Wall time: 5.0006 seconds
Process running with session ID 93204
Original token count: 0
Output:


Output:
Chunk ID: ae63b2
Wall time: 5.0009 seconds
Process running with session ID 93204
Original token count: 2
Output:
.......


Output:
Chunk ID: a037ce
Wall time: 5.0008 seconds
Process running with session ID 93204
Original token count: 1
Output:
....


Output:
Chunk ID: b1fd12
Wall time: 5.0020 seconds
Process running with session ID 93204
Original token count: 0
Output:


Output:
Chunk ID: b78c85
Wall time: 5.0009 seconds
Process running with session ID 93204
Original token count: 3
Output:
...........


The full suite has reached the browser tests and is still clean past the admin product section where it failed before. I’m continuing through the remaining storefront browser tests.


Output:
Chunk ID: f7608a
Wall time: 5.0012 seconds
Process running with session ID 93204
Original token count: 4
Output:
...............


Output:
Chunk ID: 62ca3c
Wall time: 5.0013 seconds
Process running with session ID 93204
Original token count: 0
Output:


Output:
Chunk ID: 363df0
Wall time: 5.0008 seconds
Process running with session ID 93204
Original token count: 0
Output:


Output:
Chunk ID: 8697c4
Wall time: 5.0012 seconds
Process running with session ID 93204
Original token count: 0
Output:


Output:
Chunk ID: 0f2de1
Wall time: 5.0013 seconds
Process running with session ID 93204
Original token count: 0
Output:


Output:
Chunk ID: 70754f
Wall time: 5.0016 seconds
Process running with session ID 93204
Original token count: 0
Output:


Output:
Chunk ID: 45bc6c
Wall time: 5.0005 seconds
Process running with session ID 93204
Original token count: 4
Output:
........
  ....


Output:
Chunk ID: dcbcd7
Wall time: 5.0019 seconds
Process running with session ID 93204
Original token count: 0
Output:


Output:
Chunk ID: 75b4e1
Wall time: 5.0017 seconds
Process running with session ID 93204
Original token count: 0
Output:


Output:
Chunk ID: 8518f2
Wall time: 5.0011 seconds
Process running with session ID 93204
Original token count: 0
Output:


Output:
Chunk ID: 1f42e9
Wall time: 5.0017 seconds
Process running with session ID 93204
Original token count: 0
Output:


Output:
Chunk ID: 0af184
Wall time: 5.0024 seconds
Process running with session ID 93204
Original token count: 0
Output:


The run is in the long checkout/cart browser block now; no new failure markers have appeared. I’ll keep the same process open until the final aggregate result is printed.


Output:
Chunk ID: 92a196
Wall time: 5.0008 seconds
Process running with session ID 93204
Original token count: 0
Output:


Output:
Chunk ID: 488fc8
Wall time: 5.0014 seconds
Process running with session ID 93204
Original token count: 0
Output:


Output:
Chunk ID: 4640db
Wall time: 5.0008 seconds
Process running with session ID 93204
Original token count: 0
Output:


Output:
Chunk ID: afa280
Wall time: 5.0012 seconds
Process running with session ID 93204
Original token count: 0
Output:


Output:
Chunk ID: 06b56f
Wall time: 5.0017 seconds
Process running with session ID 93204
Original token count: 4
Output:
.............


Output:
Chunk ID: 440027
Wall time: 5.0023 seconds
Process running with session ID 93204
Original token count: 0
Output:


Output:
Chunk ID: 20583f
Wall time: 5.0007 seconds
Process running with session ID 93204
Original token count: 3
Output:
............


Output:
Chunk ID: 831541
Wall time: 5.0010 seconds
Process running with session ID 93204
Original token count: 1
Output:
....


Output:
Chunk ID: 3857a5
Wall time: 5.0016 seconds
Process running with session ID 93204
Original token count: 0
Output:


Output:
Chunk ID: fd3d01
Wall time: 5.0021 seconds
Process running with session ID 93204
Original token count: 0
Output:


Output:
Chunk ID: c7672e
Wall time: 5.0028 seconds
Process running with session ID 93204
Original token count: 0
Output:


Output:
Chunk ID: b229a9
Wall time: 1.3388 seconds
Process exited with code 0
Original token count: 20
Output:
.............

  Tests:    350 passed (2069 assertions)
  Duration: 451.12s


The full suite passed on the rerun: 350 tests and 2069 assertions. I’m checking the worktree for generated artifacts, then I’ll update `specs/progress.md` for Suite 3 and commit.


find tests/Browser/Screenshots -type f -print


git diff -- resources/views/livewire/admin/products/form.blade.php tests/Browser/Admin/ProductManagementTest.php specs/progress.md


find tests/Browser/Screenshots -type f -print

find: tests/Browser/Screenshots: No such file or directory


git diff -- resources/views/livewire/admin/products/form.blade.php tests/Browser/Admin/ProductManagementTest.php specs/progress.md

diff --git a/resources/views/livewire/admin/products/form.blade.php b/resources/views/livewire/admin/products/form.blade.php
index 401c899b..b3fa0065 100644
--- a/resources/views/livewire/admin/products/form.blade.php
+++ b/resources/views/livewire/admin/products/form.blade.php
@@ -211,7 +211,7 @@

                 <flux:button :href="route('admin.products.index')" wire:navigate variant="ghost">Discard</flux:button>
-                <flux:button type="submit" variant="primary" wire:loading.attr="disabled">
+                <flux:button type="submit" variant="primary" wire:loading.attr="disabled" data-test="product-save-button">
                     Save
                     Saving...
                 </flux:button>


 M resources/views/livewire/admin/products/form.blade.php
?? tests/Browser/Admin/ProductManagementTest.php


Output:
Chunk ID: 7b5ebe
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 27
Output:
 M resources/views/livewire/admin/products/form.blade.php
?? tests/Browser/Admin/ProductManagementTest.php


Output:
Chunk ID: e10564
Wall time: 0.0000 seconds
Process exited with code 1
Original token count: 15
Output:
find: tests/Browser/Screenshots: No such file or directory


Output:
Chunk ID: ca5590
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 256
Output:
diff --git a/resources/views/livewire/admin/products/form.blade.php b/resources/views/livewire/admin/products/form.blade.php
index 401c899b..b3fa0065 100644
--- a/resources/views/livewire/admin/products/form.blade.php
+++ b/resources/views/livewire/admin/products/form.blade.php
@@ -211,7 +211,7 @@

                 <flux:button :href="route('admin.products.index')" wire:navigate variant="ghost">Discard</flux:button>
-                <flux:button type="submit" variant="primary" wire:loading.attr="disabled">
+                <flux:button type="submit" variant="primary" wire:loading.attr="disabled" data-test="product-save-button">
                     Save
                     Saving...
                 </flux:button>


/bin/zsh
-lc
sed -n '1,260p' specs/progress.md

# Shop Build Progress

## Objective

Build a complete, self-contained Laravel shop system from `specs/*`, with implemented acceptance criteria mapped to evidence, passing automated checks, verified browser flows, independent QA, and meaningful commits.

## Current Status

- Status: in progress
- Active slice: Phase 8 - final verification and completion audit
- Started from: Laravel Livewire/Fortify starter kit with no shop domain tables or models present
- Last updated: 2026-05-04

## Phased Plan

1. Foundation: SQLite config, tenancy tables/models/factories/seeders, store resolution middleware, store scoping, admin/customer auth foundations, policies, and Phase 1 tests.
2. Catalog: product, variant, inventory, collection, media schema/models/services, admin catalog CRUD, storefront browsing, and tests.
3. Storefront theme/layout: theme/page/navigation data, storefront layout, product/collection/search/page views, and smoke browser verification.
4. Cart/checkout/pricing: cart API/UI, discount, shipping, tax, checkout state machine, and tests.
5. Payments/orders/fulfillment: mock PSP, order creation, refunds, fulfillments, customer account order views, and tests.
6. Admin panel: dashboard and all admin list/form/detail pages from Spec 03, with Livewire/Flux interactions.
7. Search/analytics/apps/webhooks: SQLite search, event aggregation, developer token/webhook UI, delivery jobs, and tests.
8. Final verification: full Pest suite, Pint, frontend build, Playwright MCP customer/admin flows, independent QA, fixes, and completion audit.

## Acceptance Checklist

| Area | Criteria Source | Status | Evidence |
| --- | --- | --- | --- |
| Database schema | `specs/01-DATABASE-SCHEMA.md` | partial | Phase 1 tenancy/auth tables, Phase 2 catalog tables, Phase 3 theme/content/navigation tables including `navigation_items.parent_id` nesting, Phase 4 cart/checkout/pricing tables, Phase 5 order/payment/fulfillment tables, and Phase 7 search/analytics/apps/webhooks/export tables are implemented: search_settings, search_queries, SQLite FTS5 `products_fts`, analytics_events, analytics_daily, apps, app_installations, oauth_clients, oauth_tokens, webhook_subscriptions, webhook_deliveries, and data_exports. |
| Routes/API | `specs/02-API-ROUTES.md` | partial | Phase 1 auth routes plus catalog/cart/order/customer/discount/content/settings/search/analytics/apps/developers UI routes exist: `/`, `/collections`, `/collections/{handle}`, `/products/{handle}`, `/cart`, `/checkout`, `/checkout/confirmation/{order}`, `/search`, `/pages/{handle}`, `/account/login`, `/account/register`, `/forgot-password`, `/reset-password/{token}`, `/account/forgot-password`, `/account/reset-password/{token}`, `/account`, `/account/orders`, `/account/orders/{order}`, `/account/addresses`, `/account/logout`, `/admin/login`, `/admin/forgot-password`, `/admin/reset-password/{token}`, `/admin/products`, `/admin/products/create`, `/admin/products/{product}/edit`, `/admin/collections`, `/admin/collections/create`, `/admin/collections/{collection}/edit`, `/admin/inventory`, `/admin/orders`, `/admin/orders/{order}`, `/admin/customers`, `/admin/customers/{customer}`, `/admin/discounts`, `/admin/discounts/create`, `/admin/discounts/{discount}/edit`, `/admin/pages`, `/admin/pages/create`, `/admin/pages/{page}/edit`, `/admin/navigation`, `/admin/themes`, `/admin/themes/{theme}/editor`, `/admin/settings`, `/admin/settings/shipping`, `/admin/settings/taxes`, `/admin/settings/checkout`, `/admin/settings/notifications`, `/admin/search/settings`, `/admin/analytics`, `/admin/apps`, `/admin/apps/{installation}`, and `/admin/developers`. Storefront REST API now includes cart line CRUD, checkout address/shipping/discount/payment-method selection, checkout payment completion, token-gated order lookup, product search, search suggestions, and analytics event ingestion under `/api/storefront/v1`. Admin REST API now includes store-scoped product/customer/order list/detail, collection CRUD, discount CRUD, content page CRUD, store settings read/update, search status/reindex endpoints, analytics summary, order export creation/status, shipping zone/rate settings, tax settings read/update, theme upload/publish/settings update, fulfillment creation, and refund creation under `/api/admin/v1/stores/{store}` with session or scoped bearer-token authentication. `/oauth/authorize`, `/oauth/token`, and `/api/apps/v1/stores/{store}` app routes are registered as explicit `501 Not Implemented` stubs per the deferred OAuth/app ecosystem scope note. |
| Admin UI | `specs/03-ADMIN-UI.md` | partial | Flux/Livewire admin shell now includes dashboard KPI/date-range reporting, analytics reporting with filters/export, catalog/product/collection/inventory management, product option matrix generation, product media upload/alt text/reorder/delete controls, order/customer/discount management, page index/form, nested navigation menu editor with drag sorting and parent selection, theme grid, theme settings edi…40705 chars truncated…ervices/CustomerPasswordResetService --no-interaction`, `php artisan make:notification CustomerResetPassword --no-interaction`, `php artisan make:livewire Storefront/Account/Auth/ForgotPassword --class --no-interaction`, `php artisan make:livewire Storefront/Account/Auth/ResetPassword --class --no-interaction`, and `php artisan make:test Auth/CustomerPasswordResetTest --pest --no-interaction` created the customer password reset service, notification, Livewire pages, and tests.
- 2026-05-04: `vendor/bin/pint --dirty --format agent` fixed formatting after the customer password reset changes.
- 2026-05-04: `php artisan test --compact tests/Feature/Auth/CustomerPasswordResetTest.php` passed: 5 tests, 31 assertions.
- 2026-05-04: `php artisan test --compact tests/Feature/Auth/CustomerPasswordResetTest.php tests/Feature/Auth/PasswordResetTest.php tests/Feature/Foundation/CustomerAuthTest.php` passed: 12 tests, 55 assertions.
- 2026-05-04: `php artisan test --compact` passed after the customer password reset changes: 187 tests, 940 assertions.
- 2026-05-04: `php artisan route:list | rg "account/(forgot|reset)| forgot-password | reset-password"` confirmed customer reset routes under `/account/*` and existing Fortify reset routes at the root paths remain registered separately.
- 2026-05-04: `npm run build` passed after the customer password reset Blade changes.
- 2026-05-04: Playwright MCP verified `http://shop.test/account/forgot-password` renders in a fresh browser session, submits an unknown email through Livewire, shows the generic reset-link response, and reports no current console warnings/errors.
- 2026-05-04: `mcp__laravel_boost__.search_docs` consulted Livewire 4 file upload/testing docs, Flux file input docs, Laravel file validation/storage testing docs, and Pest 4 docs before the product media admin UI changes.
- 2026-05-04: `php artisan make:test Admin/ProductMediaManagementTest --pest --no-interaction` created focused product media management coverage.
- 2026-05-04: `vendor/bin/pint --dirty --format agent` passed after adding product form media upload code.
- 2026-05-04: `php artisan test --compact tests/Feature/Admin/ProductMediaManagementTest.php` passed: 4 tests, 24 assertions.
- 2026-05-04: `php artisan test --compact tests/Feature/Admin/ProductMediaManagementTest.php tests/Feature/Catalog/MediaProcessingTest.php tests/Feature/Catalog/CatalogUiTest.php` passed: 9 tests, 81 assertions.
- 2026-05-04: `php artisan test --compact tests/Feature/Admin` passed after the product media UI changes: 41 tests, 246 assertions.
- 2026-05-04: `php artisan test --compact` passed after the product media UI changes: 191 tests, 964 assertions.
- 2026-05-04: `npm run build` passed after the product media Blade changes.
- 2026-05-04: `mcp__laravel_boost__.get_absolute_url(path: "/admin/products/1/edit")` resolved `http://shop.test/admin/products/1/edit`; Playwright MCP verified the product edit page renders the Media section and current console checks report no warnings/errors.
- 2026-05-04: `php artisan migrate:fresh --seed --no-interaction` passed after product media browser verification to restore seeded fixtures.
- 2026-05-04: `mcp__laravel_boost__.search_docs` consulted Laravel 12 query/JSON testing docs and Pest 4 dataset/testing docs before the discount redemption-history changes.
- 2026-05-04: `vendor/bin/pint --dirty --format agent` passed after the discount service/order allocation changes.
- 2026-05-04: `php artisan test --compact tests/Feature/Checkout/PricingServicesTest.php tests/Feature/Orders/OrderServiceTest.php` passed: 11 tests, 63 assertions.
- 2026-05-04: `php artisan test --compact tests/Feature/Checkout tests/Feature/Orders tests/Feature/Api/StorefrontCheckoutApiTest.php tests/Feature/Storefront/CartCheckoutUiTest.php` passed after the discount redemption-history changes: 33 tests, 196 assertions.
- 2026-05-04: `php artisan test --compact` passed after the discount redemption-history changes: 192 tests, 966 assertions.
- 2026-05-04: `mcp__laravel_boost__.search_docs` consulted Laravel 12 query/database docs and Pest 4 testing docs before the search synonym/stop-word parser changes.
- 2026-05-04: `vendor/bin/pint --dirty --format agent` passed after the search synonym/stop-word parser changes.
- 2026-05-04: `php artisan test --compact tests/Feature/Search/SearchServiceTest.php` passed after the search synonym/stop-word parser changes: 5 tests, 15 assertions.
- 2026-05-04: `php artisan test --compact tests/Feature/Search/SearchServiceTest.php tests/Feature/Api/StorefrontSearchApiTest.php tests/Feature/Admin/SearchSettingsTest.php` passed after the search synonym/stop-word parser changes: 12 tests, 50 assertions.
- 2026-05-04: `php artisan test --compact` passed after the search synonym/stop-word parser changes: 194 tests, 970 assertions.
- 2026-05-04: `mcp__laravel_boost__.search_docs` consulted Laravel 12 database/JSON testing docs and Pest 4 docs before the automatic discount attribution changes.