sed -n '1,260p' tests/Browser/Storefront/InventoryTest.php

<?php

use App\Enums\InventoryPolicy;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductOptionValue;
use App\Models\ProductVariant;
use App\Models\Store;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Pest\Browser\Playwright\Playwright;

uses(RefreshDatabase::class);

beforeEach(function (): void {
    Playwright::setHost('shop.test');

    $this->seed(DatabaseSeeder::class);
});

afterEach(function (): void {
    Playwright::setHost(null);
});

function storefrontInventoryHost(): array
{
    return ['host' => 'shop.test'];
}

function storefrontInventoryStore(): Store
{
    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
}

/**
 * @param  array<string, string>  $options
 */
function storefrontInventoryVariant(string $handle, array $options): ProductVariant
{
    $store = storefrontInventoryStore();
    $product = Product::withoutGlobalScopes()
        ->where('store_id', $store->getKey())
        ->where('handle', $handle)
        ->firstOrFail();

    return ProductVariant::withoutGlobalScopes()
        ->with(['optionValues.option'])
        ->where('product_id', $product->getKey())
        ->get()
        ->first(function (ProductVariant $variant) use ($options): bool {
            $variantOptions = $variant->optionValues
                ->mapWithKeys(fn (ProductOptionValue $value): array => [$value->option->name => $value->value])
                ->all();

            return $variantOptions === $options;
        }) ?? throw new RuntimeException('Variant fixture not found.');
}

test('blocks add to cart for out of stock deny policy product', function (): void {
    visit('/products/limited-edition-sneakers', storefrontInventoryHost())
        ->assertSee('Limited Edition Sneakers')
        ->assertSee('Out of stock')
        ->assertSee('Sold out')
        ->assertButtonDisabled('button:has-text("Sold out")')
        ->assertDontSee('Add to cart')
        ->assertNoJavaScriptErrors();
});

test('allows add to cart for out of stock continue policy product', function (): void {
    visit('/products/backorder-denim-jacket', storefrontInventoryHost())
        ->assertSee('Backorder Denim Jacket')
        ->assertSee('Available on backorder')
        ->click('Add to cart')
        ->wait(1)
        ->navigate('/cart')
        ->wait(1)
        ->assertSee('Backorder Denim Jacket')
        ->assertNoJavaScriptErrors();
});

test('shows correct stock status for in stock product', function (): void {
    visit('/products/classic-cotton-t-shirt', storefrontInventoryHost())
        ->assertSee('Classic Cotton T-Shirt')
        ->assertSee('In stock')
        ->assertButtonEnabled('button:has-text("Add to cart")')
        ->assertDontSee('Sold out')
        ->assertDontSee('Available on backorder')
        ->assertNoJavaScriptErrors();
});

test('prevents adding more than available stock for deny policy product', function (): void {
    $variant = storefrontInventoryVariant('classic-cotton-t-shirt', [
        'Size' => 'M',
        'Color' => 'Black',
    ]);

    InventoryItem::withoutGlobalScopes()
        ->where('variant_id', $variant->getKey())
        ->firstOrFail()
        ->forceFill([
            'quantity_on_hand' => 2,
            'quantity_reserved' => 0,
            'policy' => InventoryPolicy::Deny,
        ])
        ->save();

    visit('/products/classic-cotton-t-shirt', storefrontInventoryHost())
        ->click('M')
        ->wait(1)
        ->click('Black')
        ->wait(1)
        ->assertSee('Only 2 left in stock')
        ->click('Add to cart')
        ->wait(1)
        ->navigate('/cart')
        ->wait(1)
        ->click('main button[aria-label="Increase Classic Cotton T-Shirt quantity"]')
        ->wait(1)
        ->assertSee('2 items')
        ->click('main button[aria-label="Increase Classic Cotton T-Shirt quantity"]')
        ->wait(1)
        ->assertSee('Only 2 units are available')
        ->assertSee('2 items')
        ->assertNoJavaScriptErrors();
});


Output:
Chunk ID: 22feca
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1012
Output:
<?php

use App\Enums\InventoryPolicy;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductOptionValue;
use App\Models\ProductVariant;
use App\Models\Store;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Pest\Browser\Playwright\Playwright;

uses(RefreshDatabase::class);

beforeEach(function (): void {
    Playwright::setHost('shop.test');

    $this->seed(DatabaseSeeder::class);
});

afterEach(function (): void {
    Playwright::setHost(null);
});

function storefrontInventoryHost(): array
{
    return ['host' => 'shop.test'];
}

function storefrontInventoryStore(): Store
{
    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
}

/**
 * @param  array<string, string>  $options
 */
function storefrontInventoryVariant(string $handle, array $options): ProductVariant
{
    $store = storefrontInventoryStore();
    $product = Product::withoutGlobalScopes()
        ->where('store_id', $store->getKey())
        ->where('handle', $handle)
        ->firstOrFail();

    return ProductVariant::withoutGlobalScopes()
        ->with(['optionValues.option'])
        ->where('product_id', $product->getKey())
        ->get()
        ->first(function (ProductVariant $variant) use ($options): bool {
            $variantOptions = $variant->optionValues
                ->mapWithKeys(fn (ProductOptionValue $value): array => [$value->option->name => $value->value])
                ->all();

            return $variantOptions === $options;
        }) ?? throw new RuntimeException('Variant fixture not found.');
}

test('blocks add to cart for out of stock deny policy product', function (): void {
    visit('/products/limited-edition-sneakers', storefrontInventoryHost())
        ->assertSee('Limited Edition Sneakers')
        ->assertSee('Out of stock')
        ->assertSee('Sold out')
        ->assertButtonDisabled('button:has-text("Sold out")')
        ->assertDontSee('Add to cart')
        ->assertNoJavaScriptErrors();
});

test('allows add to cart for out of stock continue policy product', function (): void {
    visit('/products/backorder-denim-jacket', storefrontInventoryHost())
        ->assertSee('Backorder Denim Jacket')
        ->assertSee('Available on backorder')
        ->click('Add to cart')
        ->wait(1)
        ->navigate('/cart')
        ->wait(1)
        ->assertSee('Backorder Denim Jacket')
        ->assertNoJavaScriptErrors();
});

test('shows correct stock status for in stock product', function (): void {
    visit('/products/classic-cotton-t-shirt', storefrontInventoryHost())
        ->assertSee('Classic Cotton T-Shirt')
        ->assertSee('In stock')
        ->assertButtonEnabled('button:has-text("Add to cart")')
        ->assertDontSee('Sold out')
        ->assertDontSee('Available on backorder')
        ->assertNoJavaScriptErrors();
});

test('prevents adding more than available stock for deny policy product', function (): void {
    $variant = storefrontInventoryVariant('classic-cotton-t-shirt', [
        'Size' => 'M',
        'Color' => 'Black',
    ]);

    InventoryItem::withoutGlobalScopes()
        ->where('variant_id', $variant->getKey())
        ->firstOrFail()
        ->forceFill([
            'quantity_on_hand' => 2,
            'quantity_reserved' => 0,
            'policy' => InventoryPolicy::Deny,
        ])
        ->save();

    visit('/products/classic-cotton-t-shirt', storefrontInventoryHost())
        ->click('M')
        ->wait(1)
        ->click('Black')
        ->wait(1)
        ->assertSee('Only 2 left in stock')
        ->click('Add to cart')
        ->wait(1)
        ->navigate('/cart')
        ->wait(1)
        ->click('main button[aria-label="Increase Classic Cotton T-Shirt quantity"]')
        ->wait(1)
        ->assertSee('2 items')
        ->click('main button[aria-label="Increase Classic Cotton T-Shirt quantity"]')
        ->wait(1)
        ->assertSee('Only 2 units are available')
        ->assertSee('2 items')
        ->assertNoJavaScriptErrors();
});


Inspect Spec 08 Suite 11 and existing inventory enforcement coverage

Add or adjust inventory enforcement behavior as needed


Output: Plan updated


Progress is updated and the worktree is clean except for the intended Suite 11 files. I’m staging and committing the inventory slice now.


git add app/Livewire/Storefront/Cart/Show.php resources/views/livewire/storefront/cart/show.blade.php tests/Feature/Storefront/CartCheckoutUiTest.php tests/Browser/Storefront/InventoryTest.php specs/progress.md && git diff --cached --stat && git commit -m "Add storefront inventory browser suite"


git add app/Livewire/Storefront/Cart/Show.php resources/views/livewire/storefront/cart/show.blade.php tests/Feature/Storefront/CartCheckoutUiTest.php tests/Browser/Storefront/InventoryTest.php specs/progress.md && git diff --cached --stat && git commit -m "Add storefront inventory browser suite"

 app/Livewire/Storefront/Cart/Show.php              |  15 ++-
 .../views/livewire/storefront/cart/show.blade.php  |   4 +
 specs/progress.md                                  |  19 +++-
 tests/Browser/Storefront/InventoryTest.php         | 125 +++++++++++++++++++++
 tests/Feature/Storefront/CartCheckoutUiTest.php    |  22 ++++
 5 files changed, 177 insertions(+), 8 deletions(-)
[2026-05-04-codex-5-5-goal b31bff5a] Add storefront inventory browser suite
 5 files changed, 177 insertions(+), 8 deletions(-)
 create mode 100644 tests/Browser/Storefront/InventoryTest.php


Output:
Chunk ID: 3a362c
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 143
Output:
 app/Livewire/Storefront/Cart/Show.php              |  15 ++-
 .../views/livewire/storefront/cart/show.blade.php  |   4 +
 specs/progress.md                                  |  19 +++-
 tests/Browser/Storefront/InventoryTest.php         | 125 +++++++++++++++++++++
 tests/Feature/Storefront/CartCheckoutUiTest.php    |  22 ++++
 5 files changed, 177 insertions(+), 8 deletions(-)
[2026-05-04-codex-5-5-goal b31bff5a] Add storefront inventory browser suite
 5 files changed, 177 insertions(+), 8 deletions(-)
 create mode 100644 tests/Browser/Storefront/InventoryTest.php


Output:
Chunk ID: 51e6c1
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 0
Output:


Committed `b31bff5a` for Suite 11 and the worktree is clean. I’m moving on to Suite 12 tenant isolation, which should mostly be browser coverage over existing store scoping.


Inspect Suite 12 tenant isolation requirements and current tenant fixtures

Add any missing stable tenant/order fixtures needed for browser checks

Explanation: Suite 11 is committed; the next remaining Spec 08 suite is tenant isolation.


Output: Plan updated


browser testing multiple hosts tenant isolation database seeding

authenticated browser tests route host session guard


sed -n '2520,2705p' specs/08-PLAYWRIGHT-E2E-PLAN.md


rg "acme-fashion|Acme Electronics|Pro Laptop|Wireless Headphones|current_store|StoreDomain|admin.products.index|StoreScope|EnsureCurrentStore" -n app database routes tests/Feature tests/Browser resources | sed -n '1,300p'


# Search Results

## 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)

---

## Verification Introduction Database Preparation
Verification | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/verification.md

### Database Preparation

Next, your `users` table must contain an `email_verified_at` column to store the date and time that the user's email address was verified. Typically, this is included in Laravel's default `0001_01_01_000000_create_users_table.php` database migration.

---

## Upgrade Upgrading To 12.0 From 11.x Authentication Updated `DatabaseTokenRepository` Constructor Signature
Upgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md

#### Updated `DatabaseTokenRepository` Constructor Signature

**Likelihood Of Impact: Very Low**

The constructor of the `Illuminate\Auth\Passwords\DatabaseTokenRepository` class now expects the `$expires` parameter to be given in seconds, rather than minutes.

---

## 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)

---

## Socialite Retrieving User Details Stateless Authentication
Socialite | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/socialite.md

#### Stateless Authentication

The `stateless` method may be used to disable session state verification. This is useful when adding social authentication to a stateless API that does not utilize cookie based sessions:

```php
use Laravel\Socialite\Socialite;

return Socialite::driver('google')->stateless()->user();
```

---

## Upgrade Upgrading To 12.0 From 11.x Database Database Constructor Signature Changes
Upgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md

#### Database Constructor Signature Changes

**Likelihood Of Impact: Very Low**

In Laravel 12, several low-level database classes now require an `Illuminate\Database\Connection` instance to be provided via their constructors.

**These changes are primarily applicable to database package maintainers - it is extremely unlikely any of these changes affect normal application development.**

`Illuminate\Database\Schema\Blueprint`

The constructor of the `Illuminate\Database\Schema\Blueprint` class now expects a `Connection` instance as its first argument. This primarily affects applications or packages that manually instantiate `Blueprint` instances.

`Illuminate\Database\Grammar`

The constructor of the `Illuminate\Database\Grammar` class also now requires a `Connection` instance. In previous versions, the connection was assigned after construction using the `setConnection()` method. This method has been removed in Laravel 12:

```php
// Laravel <= 11.x
$grammar = new MySqlGrammar;
$grammar->setConnection($connection);

// Laravel >= 12.x
$grammar = new MySqlGrammar($connection);
````

In addition, the following APIs have been removed or deprecated:

- The `Blueprint::getPrefix()` method is deprecated.
- The `Connection::withTablePrefix()` method has been removed.
- The `Grammar::getTablePrefix()` and `setTablePrefix()` methods are deprecated.
- The `Grammar::setConnection()` method has been removed.

When working with table prefixes, you should now retrieve them directly from the database connection:

```php
$prefix = $connection->getTablePrefix();
```

If you maintain custom database drivers, schema builders, or grammar implementations, you should review their constructors and ensure a `Connection` instance is provided.

_[Truncated]_


Output:
Wall time: 3.4711 seconds
Output:
[{"type":"text","text":"# Search Results\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## Verification Introduction Database Preparation\nVerification | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/verification.md\n\n\n### Database Preparation\n\nNext, your `users` table must contain an `email_verified_at` column to store the date and time that the user's email address was verified. Typically, this is included in Laravel's default `0001_01_01_000000_create_users_table.php` database migration.\n\n---\n\n## Upgrade Upgrading To 12.0 From 11.x Authentication Updated `DatabaseTokenRepository` Constructor Signature\nUpgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md\n\n\n#### Updated `DatabaseTokenRepository` Constructor Signature\n\n**Likelihood Of Impact: Very Low**\n\nThe constructor of the `Illuminate\\Auth\\Passwords\\DatabaseTokenRepository` class now expects the `$expires` parameter to be given in seconds, rather than minutes.\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## Socialite Retrieving User Details Stateless Authentication\nSocialite | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/socialite.md\n\n\n#### Stateless Authentication\n\nThe `stateless` method may be used to disable session state verification. This is useful when adding social authentication to a stateless API that does not utilize cookie based sessions:\n\n```php\nuse Laravel\\Socialite\\Socialite;\n\nreturn Socialite::driver('google')->stateless()->user();\n```\n\n---\n\n## Upgrade Upgrading To 12.0 From 11.x Database Database Constructor Signature Changes\nUpgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md\n\n\n#### Database Constructor Signature Changes\n\n**Likelihood Of Impact: Very Low**\n\nIn Laravel 12, several low-level database classes now require an `Illuminate\\Database\\Connection` instance to be provided via their constructors.\n\n**These changes are primarily applicable to database package maintainers - it is extremely unlikely any of these changes affect normal application development.**\n\n`Illuminate\\Database\\Schema\\Blueprint`\n\nThe constructor of the `Illuminate\\Database\\Schema\\Blueprint` class now expects a `Connection` instance as its first argument. This primarily affects applications or packages that manually instantiate `Blueprint` instances.\n\n`Illuminate\\Database\\Grammar`\n\nThe constructor of the `Illuminate\\Database\\Grammar` class also now requires a `Connection` instance. In previous versions, the connection was assigned after construction using the `setConnection()` method. This method has been removed in Laravel 12:\n\n```php\n// Laravel <= 11.x\n$grammar = new MySqlGrammar;\n$grammar->setConnection($connection);\n\n// Laravel >= 12.x\n$grammar = new MySqlGrammar($connection);\n````\n\nIn addition, the following APIs have been removed or deprecated:\n\n
\n\n- The `Blueprint::getPrefix()` method is deprecated.\n- The `Connection::withTablePrefix()` method has been removed.\n- The `Grammar::getTablePrefix()` and `setTablePrefix()` methods are deprecated.\n- The `Grammar::setConnection()` method has been removed.\n\n
\n\nWhen working with table prefixes, you should now retrieve them directly from the database connection:\n\n```php\n$prefix = $connection->getTablePrefix();\n```\n\nIf you maintain custom database drivers, schema builders, or grammar implementations, you should review their constructors and ensure a `Connection` instance is provided.\n\n_[Truncated]_"}]


sed -n '2520,2705p' specs/08-PLAYWRIGHT-E2E-PLAN.md

### Viewport Definitions

| Device | Width | Height | Use For |
|--------|-------|--------|---------|
| iPhone (mobile) | 375 | 812 | Storefront tests |
| iPad (tablet) | 768 | 1024 | Admin panel tests |

---

### Test 13.1: Storefront home works on mobile viewport

**Preconditions:** Seeded store

**Steps:**
1. Set viewport to iPhone dimensions (375x812)
2. Navigate to `/`

**Expected Results:**
- Page displays "Acme Fashion"
- Mobile menu (hamburger) is visible
- Desktop navigation is hidden
- No horizontal scroll
- No JavaScript errors

---

### Test 13.2: Product page stacks layout on mobile

**Preconditions:** Product #1

**Steps:**
1. Set viewport to mobile (375x812)
2. Navigate to `/products/classic-cotton-t-shirt`

**Expected Results:**
- Page displays "Classic Cotton T-Shirt", "24.99", "Add to cart"
- Gallery and details stack vertically
- No JavaScript errors

---

### Test 13.3: Can add to cart on mobile

**Preconditions:** Product #1

**Steps:**
1. Set viewport to mobile (375x812)
2. Navigate to `/products/classic-cotton-t-shirt`
3. Click "M" to select size M
4. Click "Black" to select color Black
5. Click "Add to cart"

**Expected Results:**
- Product added to cart successfully
- No JavaScript errors

---

### Test 13.4: Cart page works on mobile

**Preconditions:** Product #1

**Steps:**
1. Set viewport to mobile (375x812)
2. Navigate to `/products/classic-cotton-t-shirt`
3. Click "M" to select size M
4. Click "Black" to select color Black
5. Click "Add to cart"
6. Navigate to `/cart`

**Expected Results:**
- Page displays "Classic Cotton T-Shirt"
- "Checkout" button is visible and accessible
- No JavaScript errors

---

### Test 13.5: Checkout flow works on mobile

**Preconditions:** Product #1, Domestic shipping zone

**Steps:**
1. Set viewport to mobile (375x812)
2. Navigate to `/products/classic-cotton-t-shirt`
3. Click "M" to select size M
4. Click "Black" to select color Black
5. Click "Add to cart"
6. Navigate to `/cart`
7. Click "Checkout"
8. Enter email: `mobile@example.com`
9. Click "Continue"
10. Enter first name: `Mobile`
11. Enter last name: `User`
12. Enter address: `Mobile Str 1`
13. Enter city: `Berlin`
14. Enter postal code: `10115`
15. Enter country: `DE`
16. Click "Continue"

**Expected Results:**
- Page displays "Standard Shipping"
- All steps accessible without horizontal scrolling
- No JavaScript errors

---

### Test 13.6: Admin login works on tablet viewport

**Preconditions:** Seeded admin user

**Steps:**
1. Set viewport to iPad dimensions (768x1024)
2. Navigate to `/admin/login`
3. Enter email: `admin@acme.test`
4. Enter password: `password`
5. Click "Sign in"

**Expected Results:**
- Page displays "Dashboard"
- No JavaScript errors

---

### Test 13.7: Admin sidebar navigation works on tablet

**Preconditions:** Seeded admin user

**Steps:**
1. Set viewport to iPad dimensions (768x1024)
2. Navigate to `/admin/login`
3. Log in as admin (email: `admin@acme.test`, password: `password`)
4. Click "Products" in the sidebar
5. Verify "Products" heading is visible
6. Click "Orders" in the sidebar
7. Verify "Orders" heading is visible

**Expected Results:**
- Each section loads correctly
- No JavaScript errors

---

### Test 13.8: Collection page works on mobile with filters

**Preconditions:** Seeded collections

**Steps:**
1. Set viewport to mobile (375x812)
2. Navigate to `/collections/t-shirts`

**Expected Results:**
- Page displays "T-Shirts"
- Products are visible
- Filters are accessible (may be behind a "Filter" button on mobile)
- No JavaScript errors

**Total: 8 tests**

---

## Suite 14: Accessibility

**File:** `tests/Browser/Storefront/AccessibilityTest.php`

**Purpose:** Verify that the storefront meets basic accessibility standards - no JS console errors/warnings, proper heading hierarchy, form labels, ARIA attributes, and keyboard navigability.

### Accessibility Requirements Checklist

| Requirement | What To Verify |
|-------------|----------------|
| No JS errors | assertNoJavascriptErrors on every page |
| No console warnings | assertNoConsoleLogs on key pages |
| Heading hierarchy | One h1 per page, headings in logical order |
| Form labels | Every input has an associated label or aria-label |
| Image alt text | Product images have meaningful alt attributes |
| Keyboard navigation | Interactive elements reachable via Tab key; focus indicators visible |
| Error accessibility | Validation errors linked to fields via aria-describedby or similar |

---

### Test 14.1: Home page has no JavaScript errors or console warnings

**Preconditions:** Seeded store

**Steps:**
1. Navigate to `/`


Output:
Chunk ID: 4b45f0
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1155
Output:
### Viewport Definitions

| Device | Width | Height | Use For |
|--------|-------|--------|---------|
| iPhone (mobile) | 375 | 812 | Storefront tests |
| iPad (tablet) | 768 | 1024 | Admin panel tests |

---

### Test 13.1: Storefront home works on mobile viewport

**Preconditions:** Seeded store

**Steps:**
1. Set viewport to iPhone dimensions (375x812)
2. Navigate to `/`

**Expected Results:**
- Page displays "Acme Fashion"
- Mobile menu (hamburger) is visible
- Desktop navigation is hidden
- No horizontal scroll
- No JavaScript errors

---

### Test 13.2: Product page stacks layout on mobile

**Preconditions:** Product #1

**Steps:**
1. Set viewport to mobile (375x812)
2. Navigate to `/products/classic-cotton-t-shirt`

**Expected Results:**
- Page displays "Classic Cotton T-Shirt", "24.99", "Add to cart"
- Gallery and details stack vertically
- No JavaScript errors

---

### Test 13.3: Can add to cart on mobile

**Preconditions:** Product #1

**Steps:**
1. Set viewport to mobile (375x812)
2. Navigate to `/products/classic-cotton-t-shirt`
3. Click "M" to select size M
4. Click "Black" to select color Black
5. Click "Add to cart"

**Expected Results:**
- Product added to cart successfully
- No JavaScript errors

---

### Test 13.4: Cart page works on mobile

**Preconditions:** Product #1

**Steps:**
1. Set viewport to mobile (375x812)
2. Navigate to `/products/classic-cotton-t-shirt`
3. Click "M" to select size M
4. Click "Black" to select color Black
5. Click "Add to cart"
6. Navigate to `/cart`

**Expected Results:**
- Page displays "Classic Cotton T-Shirt"
- "Checkout" button is visible and accessible
- No JavaScript errors

---

### Test 13.5: Checkout flow works on mobile

**Preconditions:** Product #1, Domestic shipping zone

**Steps:**
1. Set viewport to mobile (375x812)
2. Navigate to `/products/classic-cotton-t-shirt`
3. Click "M" to select size M
4. Click "Black" to select color Black
5. Click "Add to cart"
6. Navigate to `/cart`
7. Click "Checkout"
8. Enter email: `mobile@example.com`
9. Click "Continue"
10. Enter first name: `Mobile`
11. Enter last name: `User`
12. Enter address: `Mobile Str 1`
13. Enter city: `Berlin`
14. Enter postal code: `10115`
15. Enter country: `DE`
16. Click "Continue"

**Expected Results:**
- Page displays "Standard Shipping"
- All steps accessible without horizontal scrolling
- No JavaScript errors

---

### Test 13.6: Admin login works on tablet viewport

**Preconditions:** Seeded admin user

**Steps:**
1. Set viewport to iPad dimensions (768x1024)
2. Navigate to `/admin/login`
3. Enter email: `admin@acme.test`
4. Enter password: `password`
5. Click "Sign in"

**Expected Results:**
- Page displays "Dashboard"
- No JavaScript errors

---

### Test 13.7: Admin sidebar navigation works on tablet

**Preconditions:** Seeded admin user

**Steps:**
1. Set viewport to iPad dimensions (768x1024)
2. Navigate to `/admin/login`
3. Log in as admin (email: `admin@acme.test`, password: `password`)
4. Click "Products" in the sidebar
5. Verify "Products" heading is visible
6. Click "Orders" in the sidebar
7. Verify "Orders" heading is visible

**Expected Results:**
- Each section loads correctly
- No JavaScript errors

---

### Test 13.8: Collection page works on mobile with filters

**Preconditions:** Seeded collections

**Steps:**
1. Set viewport to mobile (375x812)
2. Navigate to `/collections/t-shirts`

**Expected Results:**
- Page displays "T-Shirts"
- Products are visible
- Filters are accessible (may be behind a "Filter" button on mobile)
- No JavaScript errors

**Total: 8 tests**

---

## Suite 14: Accessibility

**File:** `tests/Browser/Storefront/AccessibilityTest.php`

**Purpose:** Verify that the storefront meets basic accessibility standards - no JS console errors/warnings, proper heading hierarchy, form labels, ARIA attributes, and keyboard navigability.

### Accessibility Requirements Checklist

| Requirement | What To Verify |
|-------------|----------------|
| No JS errors | assertNoJavascriptErrors on every page |
| No console warnings | assertNoConsoleLogs on key pages |
| Heading hierarchy | One h1 per page, headings in logical order |
| Form labels | Every input has an associated label or aria-label |
| Image alt text | Product images have meaningful alt attributes |
| Keyboard navigation | Interactive elements reachable via Tab key; focus indicators visible |
| Error accessibility | Validation errors linked to fields via aria-describedby or similar |

---

### Test 14.1: Home page has no JavaScript errors or console warnings

**Preconditions:** Seeded store

**Steps:**
1. Navigate to `/`


rg "acme-fashion|Acme Electronics|Pro Laptop|Wireless Headphones|current_store|StoreDomain|admin.products.index|StoreScope|EnsureCurrentStore" -n app database routes tests/Feature tests/Browser resources | sed -n '1,300p'

rg 'acme-fashion|Acme Electronics|Pro Laptop|Wireless Headphones|current_store|StoreDomain|admin.products.index|StoreScope|EnsureCurrentStore' -n app database routes tests/Feature tests/Browser resources

Question: acme-fashion|Acme Electronics|Pro Laptop|Wireless Headphones|current_store|StoreDomain|admin.products.index|StoreScope|EnsureCurrentStore

tests/Feature/Analytics/AnalyticsServiceTest.php:20:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
resources/views/layouts/storefront.blade.php:7:        @php($store = app()->bound('current_store') ? app('current_store') : null)
tests/Browser/SmokeTest.php:19:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Browser/SmokeTest.php:70:    $this->withSession(['current_store_id' => $store->getKey()]);
database/factories/StoreDomainFactory.php:5:use App\Enums\StoreDomainType;
database/factories/StoreDomainFactory.php:10: * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\StoreDomain>
database/factories/StoreDomainFactory.php:12:class StoreDomainFactory extends Factory
database/factories/StoreDomainFactory.php:24:            'type' => StoreDomainType::Storefront,
database/factories/StoreDomainFactory.php:33:            'type' => StoreDomainType::Admin,
database/factories/StoreDomainFactory.php:40:            'type' => StoreDomainType::Api,
tests/Feature/Foundation/TenancyTest.php:5:use App\Models\StoreDomain;
tests/Feature/Foundation/TenancyTest.php:22:        ->and(StoreDomain::query()->where('hostname', 'shop.test')->exists())->toBeTrue()
tests/Feature/Foundation/TenancyTest.php:40:    StoreDomain::factory()->create([
tests/Feature/Foundation/TenancyTest.php:53:    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Foundation/TenancyTest.php:56:        ->withSession(['current_store_id' => $store->getKey()])
tests/Feature/Foundation/AuthorizationTest.php:42:    app()->instance('current_store', $store);
tests/Feature/Foundation/AuthorizationTest.php:58:    app()->instance('current_store', $store);
tests/Feature/Auth/CustomerPasswordResetTest.php:7:use App\Models\StoreDomain;
tests/Feature/Auth/CustomerPasswordResetTest.php:27:    StoreDomain::factory()->create([
tests/Feature/Auth/CustomerPasswordResetTest.php:70:    app()->instance('current_store', $store);
tests/Feature/Auth/CustomerPasswordResetTest.php:104:    StoreDomain::factory()->create([
tests/Feature/Auth/CustomerPasswordResetTest.php:155:    app()->instance('current_store', $store);
tests/Feature/Auth/CustomerPasswordResetTest.php:178:    app()->instance('current_store', $store);
tests/Feature/Auth/CustomerPasswordResetTest.php:209:    app()->instance('current_store', $store);
tests/Feature/Auth/CustomerPasswordResetTest.php:240:    app()->instance('current_store', $otherStore);
tests/Feature/Auth/CustomerPasswordResetTest.php:256:    app()->instance('current_store', $store);
tests/Feature/Foundation/CustomerAuthTest.php:29:    app()->instance('current_store', $firstStore);
tests/Feature/Foundation/CustomerAuthTest.php:43:    app()->instance('current_store', $secondStore);
tests/Feature/Foundation/CustomerAuthTest.php:59:    app()->instance('current_store', $store);
tests/Feature/Foundation/CustomerAuthTest.php:74:    app()->instance('current_store', $store);
resources/views/layouts/app/sidebar.blade.php:35:                    <flux:sidebar.item icon="cube" :href="route('admin.products.index')" :current="request()->routeIs('admin.products.*')" wire:navigate>
tests/Browser/Storefront/CustomerAccountTest.php:44:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Orders/OrderServiceTest.php:36:    app()->instance('current_store', $store);
tests/Browser/Storefront/InventoryTest.php:32:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Orders/BankTransferOrderTest.php:32:    app()->instance('current_store', $store);
tests/Feature/Admin/CustomerManagementTest.php:23:    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Admin/CustomerManagementTest.php:24:    app()->instance('current_store', $store);
tests/Feature/Admin/CustomerManagementTest.php:72:        ->withSession(['current_store_id' => $store->getKey()])
tests/Feature/Admin/CustomerManagementTest.php:80:        ->withSession(['current_store_id' => $store->getKey()])
tests/Feature/Admin/CustomerManagementTest.php:88:        ->withSession(['current_store_id' => $store->getKey()])
tests/Feature/Admin/CustomerManagementTest.php:159:    app()->instance('current_store', $store);
tests/Feature/Orders/RefundServiceTest.php:28:    app()->instance('current_store', $store);
tests/Feature/Admin/OrderManagementTest.php:33:    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Admin/OrderManagementTest.php:34:    app()->instance('current_store', $store);
tests/Feature/Admin/OrderManagementTest.php:225:    app()->instance('current_store', $store);
tests/Feature/Cart/CartServiceTest.php:22:    app()->instance('current_store', $store);
tests/Feature/Orders/FulfillmentServiceTest.php:28:    app()->instance('current_store', $store);
tests/Feature/Storefront/CustomerAccountTest.php:22:    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
te…19946 chars truncated…nt_store', function (Builder $builder): void {
app/Models/ProductVariant.php:52:            if (! app()->bound('current_store')) {
app/Models/ProductVariant.php:56:            $store = app('current_store');
app/Livewire/Storefront/CartDrawer.php:76:            app()->instance('current_store', $store);
app/Livewire/Storefront/CartDrawer.php:81:        $store = app('current_store');
app/Livewire/Admin/Discounts/Form.php:63:        $store = app('current_store');
app/Livewire/Admin/Discounts/Form.php:87:        $store = app('current_store');
app/Livewire/Admin/Discounts/Form.php:361:        $store = app('current_store');
app/Enums/StoreDomainType.php:5:enum StoreDomainType: string
app/Http/Controllers/Api/Admin/V1/AnalyticsSummaryController.php:70:        app()->instance('current_store', $store);
app/Livewire/Storefront/Search/Index.php:120:        $store = app('current_store');
app/Livewire/Admin/Discounts/Index.php:35:        $store = app('current_store');
app/Livewire/Storefront/Account/Orders/Show.php:22:        $store = app('current_store');
app/Livewire/Storefront/Products/Show.php:92:            app()->instance('current_store', $store);
app/Livewire/Storefront/Products/Show.php:97:        $store = app('current_store');
app/Http/Controllers/Api/Admin/V1/SearchIndexController.php:72:        app()->instance('current_store', $store);
app/Livewire/Storefront/Account/Orders/Index.php:23:        $store = app('current_store');
app/Livewire/Storefront/Checkout/Confirmation.php:22:        $store = app('current_store');
app/Livewire/Admin/Navigation/Index.php:344:        $store = app('current_store');
app/Http/Controllers/Api/Admin/V1/CollectionController.php:109:        app()->instance('current_store', $store);
app/Livewire/Storefront/Account/Addresses/Index.php:46:        $store = app('current_store');
app/Livewire/Storefront/Checkout/Show.php:266:            app()->instance('current_store', $store);
app/Livewire/Storefront/Checkout/Show.php:271:        $store = app('current_store');
app/Livewire/Storefront/Account/Auth/ForgotPassword.php:19:        $store = app('current_store');
app/Livewire/Storefront/Cart/Show.php:155:            app()->instance('current_store', $store);
app/Livewire/Storefront/Cart/Show.php:160:        $store = app('current_store');
app/Http/Controllers/Api/Storefront/V1/CartController.php:31:        $store = app('current_store');
app/Livewire/Admin/Products/Form.php:78:            $store = app('current_store');
app/Livewire/Admin/Products/Form.php:166:        $store = app('current_store');
app/Livewire/Admin/Products/Form.php:353:        $this->redirectRoute('admin.products.index', navigate: true);
app/Livewire/Admin/Products/Form.php:773:        $store = app('current_store');
app/Livewire/Storefront/Account/Auth/Login.php:25:        $store = app('current_store');
app/Livewire/Storefront/Account/Auth/Login.php:50:        app()->instance('current_store', Store::query()->findOrFail($this->storeId));
app/Livewire/Storefront/Account/Auth/Register.php:27:        $store = app('current_store');
app/Livewire/Storefront/Account/Auth/Register.php:38:        app()->instance('current_store', $store);
app/Livewire/Storefront/Account/Auth/ResetPassword.php:26:        $store = app('current_store');
app/Livewire/Admin/Products/Index.php:123:        return view('livewire.admin.products.index', [
app/Livewire/Admin/Dashboard.php:74:        $store = app('current_store');
app/Http/Controllers/Api/Storefront/V1/OrderController.php:16:        $store = app('current_store');
app/Http/Controllers/Api/Storefront/V1/SearchController.php:56:        $store = app('current_store');
app/Providers/AppServiceProvider.php:135:            $store = app()->bound('current_store') ? app('current_store') : null;
app/Http/Controllers/Api/Storefront/V1/AnalyticsEventController.php:22:        $store = app('current_store');
app/Http/Middleware/ResolveStore.php:7:use App\Models\StoreDomain;
app/Http/Middleware/ResolveStore.php:28:        app()->instance('current_store', $store);
app/Http/Middleware/ResolveStore.php:47:            return StoreDomain::query()
app/Http/Middleware/ResolveStore.php:63:        $storeId = $request->session()->get('current_store_id');
app/Http/Middleware/ResolveStore.php:69:                $request->session()->put('current_store_id', $storeId);
app/Http/Middleware/CheckStoreRole.php:20:        $store = app()->bound('current_store') ? app('current_store') : null;
app/Http/Middleware/AuthenticateAdminApi.php:24:        app()->instance('current_store', $store);
app/Http/Resources/Admin/V1/StoreSettingsResource.php:5:use App\Models\StoreDomain;
app/Http/Resources/Admin/V1/StoreSettingsResource.php:30:                ->map(fn (StoreDomain $domain): array => [
app/Http/Requests/Api/Storefront/V1/StoreCartRequest.php:27:        $store = app()->bound('current_store') ? app('current_store') : null;
app/Http/Controllers/Storefront/Account/Auth/CustomerPasswordResetController.php:48:        $store = app('current_store');


Output:
Chunk ID: 90624e
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 7487
Output:
tests/Feature/Analytics/AnalyticsServiceTest.php:20:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
resources/views/layouts/storefront.blade.php:7:        @php($store = app()->bound('current_store') ? app('current_store') : null)
tests/Browser/SmokeTest.php:19:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Browser/SmokeTest.php:70:    $this->withSession(['current_store_id' => $store->getKey()]);
database/factories/StoreDomainFactory.php:5:use App\Enums\StoreDomainType;
database/factories/StoreDomainFactory.php:10: * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\StoreDomain>
database/factories/StoreDomainFactory.php:12:class StoreDomainFactory extends Factory
database/factories/StoreDomainFactory.php:24:            'type' => StoreDomainType::Storefront,
database/factories/StoreDomainFactory.php:33:            'type' => StoreDomainType::Admin,
database/factories/StoreDomainFactory.php:40:            'type' => StoreDomainType::Api,
tests/Feature/Foundation/TenancyTest.php:5:use App\Models\StoreDomain;
tests/Feature/Foundation/TenancyTest.php:22:        ->and(StoreDomain::query()->where('hostname', 'shop.test')->exists())->toBeTrue()
tests/Feature/Foundation/TenancyTest.php:40:    StoreDomain::factory()->create([
tests/Feature/Foundation/TenancyTest.php:53:    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Foundation/TenancyTest.php:56:        ->withSession(['current_store_id' => $store->getKey()])
tests/Feature/Foundation/AuthorizationTest.php:42:    app()->instance('current_store', $store);
tests/Feature/Foundation/AuthorizationTest.php:58:    app()->instance('current_store', $store);
tests/Feature/Auth/CustomerPasswordResetTest.php:7:use App\Models\StoreDomain;
tests/Feature/Auth/CustomerPasswordResetTest.php:27:    StoreDomain::factory()->create([
tests/Feature/Auth/CustomerPasswordResetTest.php:70:    app()->instance('current_store', $store);
tests/Feature/Auth/CustomerPasswordResetTest.php:104:    StoreDomain::factory()->create([
tests/Feature/Auth/CustomerPasswordResetTest.php:155:    app()->instance('current_store', $store);
tests/Feature/Auth/CustomerPasswordResetTest.php:178:    app()->instance('current_store', $store);
tests/Feature/Auth/CustomerPasswordResetTest.php:209:    app()->instance('current_store', $store);
tests/Feature/Auth/CustomerPasswordResetTest.php:240:    app()->instance('current_store', $otherStore);
tests/Feature/Auth/CustomerPasswordResetTest.php:256:    app()->instance('current_store', $store);
tests/Feature/Foundation/CustomerAuthTest.php:29:    app()->instance('current_store', $firstStore);
tests/Feature/Foundation/CustomerAuthTest.php:43:    app()->instance('current_store', $secondStore);
tests/Feature/Foundation/CustomerAuthTest.php:59:    app()->instance('current_store', $store);
tests/Feature/Foundation/CustomerAuthTest.php:74:    app()->instance('current_store', $store);
resources/views/layouts/app/sidebar.blade.php:35:                    <flux:sidebar.item icon="cube" :href="route('admin.products.index')" :current="request()->routeIs('admin.products.*')" wire:navigate>
tests/Browser/Storefront/CustomerAccountTest.php:44:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Orders/OrderServiceTest.php:36:    app()->instance('current_store', $store);
tests/Browser/Storefront/InventoryTest.php:32:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Orders/BankTransferOrderTest.php:32:    app()->instance('current_store', $store);
tests/Feature/Admin/CustomerManagementTest.php:23:    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Admin/CustomerManagementTest.php:24:    app()->instance('current_store', $store);
tests/Feature/Admin/CustomerManagementTest.php:72:        ->withSession(['current_store_id' => $store->getKey()])
tests/Feature/Admin/CustomerManagementTest.php:80:        ->withSession(['current_store_id' => $store->getKey()])
tests/Feature/Admin/CustomerManagementTest.php:88:        ->withSession(['current_store_id' => $store->getKey()])
tests/Feature/Admin/CustomerManagementTest.php:159:    app()->instance('current_store', $store);
tests/Feature/Orders/RefundServiceTest.php:28:    app()->instance('current_store', $store);
tests/Feature/Admin/OrderManagementTest.php:33:    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Admin/OrderManagementTest.php:34:    app()->instance('current_store', $store);
tests/Feature/Admin/OrderManagementTest.php:225:    app()->instance('current_store', $store);
tests/Feature/Cart/CartServiceTest.php:22:    app()->instance('current_store', $store);
tests/Feature/Orders/FulfillmentServiceTest.php:28:    app()->instance('current_store', $store);
tests/Feature/Storefront/CustomerAccountTest.php:22:    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Storefront/CustomerAccountTest.php:24:    app()->instance('current_store', $store);
tests/Feature/Admin/ContentManagementTest.php:33:    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Admin/ContentManagementTest.php:34:    app()->instance('current_store', $store);
tests/Feature/Admin/ContentManagementTest.php:67:        ->withSession(['current_store_id' => $store->getKey()])
tests/Feature/Admin/ContentManagementTest.php:74:        ->withSession(['current_store_id' => $store->getKey()])
tests/Feature/Admin/ContentManagementTest.php:80:        ->withSession(['current_store_id' => $store->getKey()])
tests/Feature/Admin/ContentManagementTest.php:86:        ->withSession(['current_store_id' => $store->getKey()])
tests/Feature/Admin/ContentManagementTest.php:92:        ->withSession(['current_store_id' => $store->getKey()])
tests/Feature/Storefront/OrderViewsTest.php:32:    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Storefront/OrderViewsTest.php:33:    app()->instance('current_store', $store);
tests/Feature/Api/StorefrontCheckoutApiTest.php:22:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Search/SearchServiceTest.php:21:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Admin/DiscountManagementTest.php:30:    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Admin/DiscountManagementTest.php:31:    app()->instance('current_store', $store);
tests/Feature/Admin/DiscountManagementTest.php:77:        ->withSession(['current_store_id' => $store->getKey()])
tests/Feature/Admin/DiscountManagementTest.php:86:        ->withSession(['current_store_id' => $store->getKey()])
tests/Feature/Admin/DiscountManagementTest.php:92:        ->withSession(['current_store_id' => $store->getKey()])
tests/Feature/Admin/DiscountManagementTest.php:195:        ->withSession(['current_store_id' => $store->getKey()])
tests/Feature/Admin/DiscountManagementTest.php:200:        ->withSession(['current_store_id' => $store->getKey()])
tests/Feature/Admin/DiscountManagementTest.php:205:        ->withSession(['current_store_id' => $store->getKey()])
tests/Feature/Storefront/CartCheckoutUiTest.php:32:    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Storefront/CartCheckoutUiTest.php:33:    app()->instance('current_store', $store);
tests/Feature/Api/StorefrontSearchApiTest.php:19:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Checkout/PricingServicesTest.php:30:    app()->instance('current_store', $store);
resources/views/livewire/admin/products/form.blade.php:6:                <flux:breadcrumbs.item :href="route('admin.products.index')" wire:navigate>Products</flux:breadcrumbs.item>
resources/views/livewire/admin/products/form.blade.php:213:                <flux:button :href="route('admin.products.index')" wire:navigate variant="ghost">Discard</flux:button>
tests/Feature/Admin/ProductMediaManagementTest.php:40:    app()->instance('current_store', $store);
tests/Feature/Admin/ProductMediaManagementTest.php:75:    app()->instance('current_store', $store);
tests/Feature/Admin/ProductMediaManagementTest.php:110:    app()->instance('current_store', $store);
tests/Feature/Admin/ProductMediaManagementTest.php:145:    app()->instance('current_store', $store);
tests/Feature/Storefront/ThemeDataTest.php:52:    app()->instance('current_store', $firstStore);
tests/Feature/Storefront/ThemeDataTest.php:74:    app()->instance('current_store', $firstStore);
tests/Feature/Storefront/ThemeDataTest.php:84:    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Storefront/ThemeDataTest.php:95:    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Storefront/ThemeDataTest.php:116:    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Api/AdminSearchIndexApiTest.php:22:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Checkout/CheckoutServiceTest.php:24:    app()->instance('current_store', $store);
tests/Feature/Admin/AppsDevelopersTest.php:24:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Admin/AppsDevelopersTest.php:60:    app()->instance('current_store', $store);
tests/Feature/Admin/DashboardTest.php:23:    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Admin/DashboardTest.php:24:    app()->instance('current_store', $store);
tests/Feature/Admin/DashboardTest.php:80:        ->withSession(['current_store_id' => $store->getKey()])
tests/Feature/Api/AdminAnalyticsSummaryApiTest.php:23:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Api/AdminThemeApiTest.php:24:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Admin/AnalyticsDashboardTest.php:21:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Admin/AnalyticsDashboardTest.php:58:    app()->instance('current_store', $store);
tests/Feature/Admin/SettingsManagementTest.php:14:use App\Models\StoreDomain;
tests/Feature/Admin/SettingsManagementTest.php:31:    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Admin/SettingsManagementTest.php:32:    app()->instance('current_store', $store);
tests/Feature/Admin/SettingsManagementTest.php:60:            ->withSession(['current_store_id' => $store->getKey()])
tests/Feature/Admin/SettingsManagementTest.php:65:            ->withSession(['current_store_id' => $store->getKey()])
tests/Feature/Admin/SettingsManagementTest.php:98:        ->and(StoreDomain::query()->where('hostname', 'atelier.test')->where('store_id', $store->getKey())->exists())->toBeTrue();
tests/Feature/Api/AdminStoreSettingsApiTest.php:19:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Api/AdminOrderExportApiTest.php:23:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Admin/SearchSettingsTest.php:23:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Admin/SearchSettingsTest.php:41:    app()->instance('current_store', $store);
tests/Feature/Admin/SearchSettingsTest.php:60:    app()->instance('current_store', $store);
tests/Feature/Catalog/ProductServiceTest.php:34:    app()->instance('current_store', $store);
tests/Feature/Catalog/ProductServiceTest.php:50:    app()->instance('current_store', $store);
tests/Feature/Catalog/ProductServiceTest.php:70:    app()->instance('current_store', $store);
tests/Feature/Catalog/ProductServiceTest.php:119:    app()->instance('current_store', $store);
tests/Feature/Catalog/ProductServiceTest.php:144:    app()->instance('current_store', $store);
tests/Feature/Api/AdminPageApiTest.php:19:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Api/AdminCatalogApiTest.php:23:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Api/StorefrontOrderApiTest.php:21:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Api/StorefrontCartApiTest.php:20:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Api/AdminTaxSettingsApiTest.php:20:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Catalog/CatalogFoundationTest.php:44:    app()->instance('current_store', $firstStore);
tests/Feature/Catalog/CatalogFoundationTest.php:64:    app()->instance('current_store', $firstStore);
tests/Feature/Catalog/CatalogFoundationTest.php:77:    app()->instance('current_store', $resolvedStore);
tests/Feature/Api/StorefrontAnalyticsApiTest.php:16:    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Api/AdminOrderApiTest.php:28:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Api/AdminCollectionApiTest.php:20:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Catalog/InventoryServiceTest.php:14:    app()->instance('current_store', $store);
tests/Feature/Catalog/InventoryServiceTest.php:39:    app()->instance('current_store', $store);
tests/Feature/Api/AdminShippingSettingsApiTest.php:21:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
database/seeders/DatabaseSeeder.php:17:            StoreDomainSeeder::class,
tests/Feature/Api/AdminDiscountApiTest.php:22:    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
database/seeders/StoreDomainSeeder.php:6:use App\Models\StoreDomain;
database/seeders/StoreDomainSeeder.php:9:class StoreDomainSeeder extends Seeder
database/seeders/StoreDomainSeeder.php:17:            'acme-fashion' => ['shop.test', 'acme-fashion.test'],
database/seeders/StoreDomainSeeder.php:25:                StoreDomain::query()->updateOrCreate(
tests/Feature/Catalog/VariantMatrixServiceTest.php:15:    app()->instance('current_store', $store);
tests/Feature/Catalog/VariantMatrixServiceTest.php:43:    app()->instance('current_store', $store);
tests/Feature/Catalog/VariantMatrixServiceTest.php:56:    app()->instance('current_store', $store);
database/seeders/ProductSeeder.php:23:            $this->seedProducts('acme-fashion', $this->fashionProducts());
database/seeders/ProductSeeder.php:306:            $this->product('Pro Laptop 15', 'pro-laptop-15', 'TechCorp', 'Laptops', ['featured'], ['featured'], '
Performance laptop for tenant isolation tests.
', [['Storage' => ['256GB', '512GB', '1TB']]], ['price_amount' => 99999, 'weight_g' => 1800, 'quantity_on_hand' => 10], 'TECH-LAPTOP', variants: [
database/seeders/ProductSeeder.php:311:            $this->product('Wireless Headphones', 'wireless-headphones', 'AudioMax', 'Audio', ['featured'], ['featured'], '
Wireless headphones for tenant isolation tests.
', [['Color' => ['Black', 'Silver']]], ['price_amount' => 14999, 'weight_g' => 250, 'quantity_on_hand' => 25], 'TECH-HEADPHONES'),
tests/Feature/Catalog/CatalogUiTest.php:54:    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Catalog/CatalogUiTest.php:58:        ->withSession(['current_store_id' => $store->getKey()])
tests/Feature/Catalog/CatalogUiTest.php:65:        ->withSession(['current_store_id' => $store->getKey()])
tests/Feature/Catalog/CatalogUiTest.php:72:        ->withSession(['current_store_id' => $store->getKey()])
tests/Feature/Catalog/CatalogUiTest.php:80:    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Catalog/CatalogUiTest.php:82:    app()->instance('current_store', $store);
tests/Feature/Catalog/CatalogUiTest.php:159:    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Catalog/CatalogUiTest.php:161:    app()->instance('current_store', $store);
tests/Feature/Catalog/CatalogUiTest.php:228:    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
tests/Feature/Catalog/CatalogUiTest.php:231:    app()->instance('current_store', $store);
database/seeders/StoreSeeder.php:19:            ['handle' => 'acme-fashion', 'name' => 'Acme Fashion'],
database/seeders/StoreSeeder.php:20:            ['handle' => 'acme-electronics', 'name' => 'Acme Electronics'],
database/seeders/CustomerAddressSeeder.php:17:        $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
database/seeders/CollectionSeeder.php:17:            'acme-fashion' => [
database/seeders/AnalyticsSeeder.php:19:            if ($store->handle === 'acme-fashion') {
database/seeders/AnalyticsSeeder.php:71:            'https://instagram.com/acme-fashion',
database/seeders/CustomerSeeder.php:16:        $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
app/Livewire/Admin/Developers/Index.php:196:        $store = app('current_store');
app/Livewire/Admin/Analytics/Index.php:307:        $store = app('current_store');
app/Http/Controllers/Api/Admin/V1/PageController.php:102:        app()->instance('current_store', $store);
app/Livewire/Admin/Themes/Editor.php:35:        $store = app('current_store');
app/Http/Controllers/Api/Admin/V1/ThemeSettingsController.php:50:        app()->instance('current_store', $store);
app/Livewire/Admin/Themes/Index.php:27:        $store = app('current_store');
app/Http/Controllers/Api/Admin/V1/ShippingZoneController.php:64:        app()->instance('current_store', $store);
app/Models/Concerns/BelongsToStore.php:5:use App\Models\Scopes\StoreScope;
app/Models/Concerns/BelongsToStore.php:12:        static::addGlobalScope(new StoreScope);
app/Models/Concerns/BelongsToStore.php:15:            if ($model->store_id || ! app()->bound('current_store')) {
app/Models/Concerns/BelongsToStore.php:19:            $store = app('current_store');
app/Livewire/Admin/Apps/Show.php:70:        $store = app('current_store');
app/Livewire/Admin/Settings/Notifications.php:112:        $store = app('current_store');
app/Http/Controllers/Api/Admin/V1/TaxSettingsController.php:63:        app()->instance('current_store', $store);
app/Livewire/Admin/Apps/Index.php:65:        $store = app('current_store');
app/Livewire/Admin/Settings/Checkout.php:117:        $store = app('current_store');
app/Http/Controllers/Api/Admin/V1/CustomerController.php:60:        app()->instance('current_store', $store);
app/Models/Scopes/StoreScope.php:10:class StoreScope implements Scope
app/Models/Scopes/StoreScope.php:14:        if (! app()->bound('current_store')) {
app/Models/Scopes/StoreScope.php:18:        $store = app('current_store');
app/Livewire/Admin/Pages/Form.php:33:        $store = app('current_store');
app/Livewire/Admin/Pages/Form.php:62:        $store = app('current_store');
app/Livewire/Admin/Pages/Form.php:111:        $store = app('current_store');
app/Livewire/Admin/Settings/Shipping.php:276:        $store = app('current_store');
app/Http/Controllers/Api/Admin/V1/OrderRefundController.php:49:        app()->instance('current_store', $store);
app/Livewire/Admin/Pages/Index.php:27:        $store = app('current_store');
app/Http/Controllers/Api/Admin/V1/StoreSettingsController.php:94:        app()->instance('current_store', $store);
app/Livewire/Admin/Settings/Index.php:5:use App\Enums\StoreDomainType;
app/Livewire/Admin/Settings/Index.php:7:use App\Models\StoreDomain;
app/Livewire/Admin/Settings/Index.php:116:            'newType' => ['required', Rule::in(array_column(StoreDomainType::cases(), 'value'))],
app/Livewire/Admin/Settings/Index.php:122:        StoreDomain::query()->create([
app/Livewire/Admin/Settings/Index.php:125:            'type' => StoreDomainType::from($validated['newType']),
app/Livewire/Admin/Settings/Index.php:144:        StoreDomain::query()
app/Livewire/Admin/Settings/Index.php:165:            StoreDomain::query()
app/Livewire/Admin/Settings/Index.php:177:     * @return Collection<int, StoreDomain>
app/Livewire/Admin/Settings/Index.php:181:        return StoreDomain::query()
app/Livewire/Admin/Settings/Index.php:233:        $store = app('current_store');
app/Livewire/Admin/Settings/Index.php:253:    private function domain(int $domainId): StoreDomain
app/Livewire/Admin/Settings/Index.php:255:        return StoreDomain::query()
app/Models/StoreDomain.php:5:use App\Enums\StoreDomainType;
app/Models/StoreDomain.php:10:class StoreDomain extends Model
app/Models/StoreDomain.php:12:    /** @use HasFactory<\Database\Factories\StoreDomainFactory> */
app/Models/StoreDomain.php:42:            'type' => StoreDomainType::class,
app/Livewire/Admin/Collections/Form.php:36:            $store = app('current_store');
app/Livewire/Admin/Collections/Form.php:71:        $store = app('current_store');
app/Livewire/Admin/Settings/Taxes.php:113:        $store = app('current_store');
app/Http/Controllers/Api/Admin/V1/ProductController.php:69:        app()->instance('current_store', $store);
app/Models/NavigationItem.php:42:        static::addGlobalScope('current_store', function (Builder $builder): void {
app/Models/NavigationItem.php:43:            if (! app()->bound('current_store')) {
app/Models/NavigationItem.php:47:            $store = app('current_store');
app/Models/Store.php:40:     * @return HasMany<StoreDomain, $this>
app/Models/Store.php:44:        return $this->hasMany(StoreDomain::class);
app/Http/Controllers/Api/Admin/V1/ThemeController.php:71:        app()->instance('current_store', $store);
app/Auth/CustomerUserProvider.php:36:        $store = app()->bound('current_store') ? app('current_store') : null;
app/Livewire/Admin/Customers/Show.php:45:        $store = app('current_store');
app/Livewire/Admin/Orders/Show.php:47:        $store = app('current_store');
app/Models/ThemeSettings.php:31:        static::addGlobalScope('current_store', function (Builder $builder): void {
app/Models/ThemeSettings.php:32:            if (! app()->bound('current_store')) {
app/Models/ThemeSettings.php:36:            $store = app('current_store');
app/Models/ProductOption.php:36:        static::addGlobalScope('current_store', function (Builder $builder): void {
app/Models/ProductOption.php:37:            if (! app()->bound('current_store')) {
app/Models/ProductOption.php:41:            $store = app('current_store');
app/Http/Controllers/Api/Admin/V1/DiscountController.php:81:        app()->instance('current_store', $store);
app/Models/CartLine.php:43:        static::addGlobalScope('current_store', function (Builder $builder): void {
app/Models/CartLine.php:44:            if (! app()->bound('current_store')) {
app/Models/CartLine.php:48:            $store = app('current_store');
app/Livewire/Admin/Customers/Index.php:26:        $store = app('current_store');
app/Livewire/Admin/Orders/Index.php:39:        $store = app('current_store');
app/Traits/ChecksStoreRole.php:54:        if (! app()->bound('current_store')) {
app/Traits/ChecksStoreRole.php:58:        $store = app('current_store');
app/Models/ThemeFile.php:37:        static::addGlobalScope('current_store', function (Builder $builder): void {
app/Models/ThemeFile.php:38:            if (! app()->bound('current_store')) {
app/Models/ThemeFile.php:42:            $store = app('current_store');
app/Http/Controllers/Api/Admin/V1/OrderFulfillmentController.php:43:        app()->instance('current_store', $store);
app/Models/ShippingRate.php:40:        static::addGlobalScope('current_store', function (Builder $builder): void {
app/Models/ShippingRate.php:41:            if (! app()->bound('current_store')) {
app/Models/ShippingRate.php:45:            $store = app('current_store');
app/Models/ProductMedia.php:47:        static::addGlobalScope('current_store', function (Builder $builder): void {
app/Models/ProductMedia.php:48:            if (! app()->bound('current_store')) {
app/Models/ProductMedia.php:52:            $store = app('current_store');
app/Http/Controllers/Api/Admin/V1/ShippingRateController.php:58:        app()->instance('current_store', $store);
app/Http/Controllers/Api/Admin/V1/OrderController.php:66:        app()->instance('current_store', $store);
app/Livewire/Admin/Search/Settings.php:153:        $store = app('current_store');
app/Models/ProductOptionValue.php:36:        static::addGlobalScope('current_store', function (Builder $builder): void {
app/Models/ProductOptionValue.php:37:            if (! app()->bound('current_store')) {
app/Models/ProductOptionValue.php:41:            $store = app('current_store');
app/Http/Controllers/Api/Admin/V1/OrderExportController.php:54:        app()->instance('current_store', $store);
app/Livewire/Storefront/Home.php:16:        $store = app('current_store');
app/Models/ProductVariant.php:51:        static::addGlobalScope('current_store', function (Builder $builder): void {
app/Models/ProductVariant.php:52:            if (! app()->bound('current_store')) {
app/Models/ProductVariant.php:56:            $store = app('current_store');
app/Livewire/Storefront/CartDrawer.php:76:            app()->instance('current_store', $store);
app/Livewire/Storefront/CartDrawer.php:81:        $store = app('current_store');
app/Livewire/Admin/Discounts/Form.php:63:        $store = app('current_store');
app/Livewire/Admin/Discounts/Form.php:87:        $store = app('current_store');
app/Livewire/Admin/Discounts/Form.php:361:        $store = app('current_store');
app/Enums/StoreDomainType.php:5:enum StoreDomainType: string
app/Http/Controllers/Api/Admin/V1/AnalyticsSummaryController.php:70:        app()->instance('current_store', $store);
app/Livewire/Storefront/Search/Index.php:120:        $store = app('current_store');
app/Livewire/Admin/Discounts/Index.php:35:        $store = app('current_store');
app/Livewire/Storefront/Account/Orders/Show.php:22:        $store = app('current_store');
app/Livewire/Storefront/Products/Show.php:92:            app()->instance('current_store', $store);
app/Livewire/Storefront/Products/Show.php:97:        $store = app('current_store');
app/Http/Controllers/Api/Admin/V1/SearchIndexController.php:72:        app()->instance('current_store', $store);
app/Livewire/Storefront/Account/Orders/Index.php:23:        $store = app('current_store');
app/Livewire/Storefront/Checkout/Confirmation.php:22:        $store = app('current_store');
app/Livewire/Admin/Navigation/Index.php:344:        $store = app('current_store');
app/Http/Controllers/Api/Admin/V1/CollectionController.php:109:        app()->instance('current_store', $store);
app/Livewire/Storefront/Account/Addresses/Index.php:46:        $store = app('current_store');
app/Livewire/Storefront/Checkout/Show.php:266:            app()->instance('current_store', $store);
app/Livewire/Storefront/Checkout/Show.php:271:        $store = app('current_store');
app/Livewire/Storefront/Account/Auth/ForgotPassword.php:19:        $store = app('current_store');
app/Livewire/Storefront/Cart/Show.php:155:            app()->instance('current_store', $store);
app/Livewire/Storefront/Cart/Show.php:160:        $store = app('current_store');
app/Http/Controllers/Api/Storefront/V1/CartController.php:31:        $store = app('current_store');
app/Livewire/Admin/Products/Form.php:78:            $store = app('current_store');
app/Livewire/Admin/Products/Form.php:166:        $store = app('current_store');
app/Livewire/Admin/Products/Form.php:353:        $this->redirectRoute('admin.products.index', navigate: true);
app/Livewire/Admin/Products/Form.php:773:        $store = app('current_store');
app/Livewire/Storefront/Account/Auth/Login.php:25:        $store = app('current_store');
app/Livewire/Storefront/Account/Auth/Login.php:50:        app()->instance('current_store', Store::query()->findOrFail($this->storeId));
app/Livewire/Storefront/Account/Auth/Register.php:27:        $store = app('current_store');
app/Livewire/Storefront/Account/Auth/Register.php:38:        app()->instance('current_store', $store);
app/Livewire/Storefront/Account/Auth/ResetPassword.php:26:        $store = app('current_store');
app/Livewire/Admin/Products/Index.php:123:        return view('livewire.admin.products.index', [
app/Livewire/Admin/Dashboard.php:74:        $store = app('current_store');
app/Http/Controllers/Api/Storefront/V1/OrderController.php:16:        $store = app('current_store');
app/Http/Controllers/Api/Storefront/V1/SearchController.php:56:        $store = app('current_store');
app/Providers/AppServiceProvider.php:135:            $store = app()->bound('current_store') ? app('current_store') : null;
app/Http/Controllers/Api/Storefront/V1/AnalyticsEventController.php:22:        $store = app('current_store');
app/Http/Middleware/ResolveStore.php:7:use App\Models\StoreDomain;
app/Http/Middleware/ResolveStore.php:28:        app()->instance('current_store', $store);
app/Http/Middleware/ResolveStore.php:47:            return StoreDomain::query()
app/Http/Middleware/ResolveStore.php:63:        $storeId = $request->session()->get('current_store_id');
app/Http/Middleware/ResolveStore.php:69:                $request->session()->put('current_store_id', $storeId);
app/Http/Middleware/CheckStoreRole.php:20:        $store = app()->bound('current_store') ? app('current_store') : null;
app/Http/Middleware/AuthenticateAdminApi.php:24:        app()->instance('current_store', $store);
app/Http/Resources/Admin/V1/StoreSettingsResource.php:5:use App\Models\StoreDomain;
app/Http/Resources/Admin/V1/StoreSettingsResource.php:30:                ->map(fn (StoreDomain $domain): array => [
app/Http/Requests/Api/Storefront/V1/StoreCartRequest.php:27:        $store = app()->bound('current_store') ? app('current_store') : null;
app/Http/Controllers/Storefront/Account/Auth/CustomerPasswordResetController.php:48:        $store = app('current_store');


sed -n '1,180p' resources/views/livewire/admin/products/index.blade.php


sed -n '1,180p' resources/views/livewire/admin/orders/index.blade.php


sed -n '1,180p' app/Livewire/Admin/Products/Index.php && sed -n '1,140p' app/Livewire/Admin/Orders/Index.php


sed -n '1,120p' app/Http/Middleware/ResolveStore.php && sed -n '1,80p' database/seeders/StoreDomainSeeder.php


sed -n '1,180p' resources/views/livewire/admin/products/index.blade.php

            <flux:heading size="xl">Products</flux:heading>
            <flux:text class="mt-1">Manage catalog items, variants, status, and inventory.</flux:text>

        <flux:button :href="route('admin.products.create')" wire:navigate variant="primary" icon="plus">
            Add product
        </flux:button>

        <flux:input wire:model.live.debounce.300ms="search" icon="magnifying-glass" placeholder="Search products..." aria-label="Search products" />

        <flux:select wire:model.live="statusFilter" aria-label="Status filter">
            <flux:select.option value="all">All statuses</flux:select.option>
            <flux:select.option value="active">Active</flux:select.option>
            <flux:select.option value="draft">Draft</flux:select.option>
            <flux:select.option value="archived">Archived</flux:select.option>
        </flux:select>

        <flux:select wire:model.live="typeFilter" aria-label="Product type filter">
            <flux:select.option value="all">All types</flux:select.option>
            @foreach ($productTypes as $type)
                <flux:select.option value="{{ $type }}">{{ $type }}</flux:select.option>
            @endforeach
        </flux:select>

    @if (count($selectedIds) > 0)

            <flux:text>{{ count($selectedIds) }} products selected</flux:text>

                <flux:button wire:click="bulkSetActive" variant="ghost">Set active</flux:button>
                <flux:button wire:click="bulkArchive" variant="ghost">Archive</flux:button>
                <flux:button wire:click="bulkDelete" variant="danger">Delete</flux:button>

    @endif

                            <input type="checkbox" wire:click="toggleSelectAll" @checked($selectAll) aria-label="Select all visible products" class="rounded border-zinc-300 dark:border-zinc-600">

                        Product
                        Status
                        Inventory
                        Type
                        Vendor

                            <button wire:click="sortBy('updated_at')" class="font-semibold">Updated</button>

                    @forelse ($products as $product)
                        @php
                            $inventory = $product->variants->sum(fn ($variant) => $variant->inventoryItem?->quantity_on_hand ?? 0);
                            $statusColor = match ($product->status->value) {
                                'active' => 'green',
                                'archived' => 'red',
                                default => 'zinc',
                            };
                        @endphp

getKey() }}">

                                <input type="checkbox" wire:model.live="selectedIds" value="{{ $product->getKey() }}" aria-label="Select {{ $product->title }}" class="rounded border-zinc-300 dark:border-zinc-600">

                                        <flux:icon name="cube" class="size-5" />

                                            {{ $product->title }}

{{ $product->variants_count }} variants

                                <flux:badge :color="$statusColor">{{ Str::title($product->status->value) }}</flux:badge>

                            {{ $inventory }}
                            {{ $product->product_type ?: '-' }}
                            {{ $product->vendor ?: '-' }}
                            {{ $product->updated_at?->diffForHumans() }}

                    @empty

                                        <flux:icon name="cube" class="size-6" />

                                    <flux:heading size="lg">No products found</flux:heading>
                                    <flux:text>Adjust your filters or add a product.</flux:text>
                                    <flux:button :href="route('admin.products.create')" wire:navigate variant="primary">Add product</flux:button>

                    @endforelse

    {{ $products->links() }}