output:
Chunk ID: 874396
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 57
Output:
ce2d64c2 Add responsive mobile browser suite
93cc2d35 Add tenant isolation browser suite
b31bff5a Add storefront inventory browser suite
45bf4d44 Add customer account browser suite
4bef455e Add storefront checkout browser suite


message: Committed Suite 13 as `ce2d64c2 Add responsive mobile browser suite`; the worktree is clean. I’m moving to Suite 14 Accessibility next, starting with the spec details and version-specific browser-testing docs before any code changes.

phase: commentary


type: message

role: assistant

text: Committed Suite 13 as `ce2d64c2 Add responsive mobile browser suite`; the worktree is clean. I’m moving to Suite 14 Accessibility next, starting with the spec details and version-specific browser-testing docs before any code changes.


step: Inspect Suite 14 accessibility requirements and existing markup

step: Add accessibility browser coverage

status: pending

step: Fix any accessibility issues the suite exposes

step: Run focused/full tests

step: Update progress and commit the slice

explanation: Suite 13 is committed; starting Suite 14 Accessibility.


output: Plan updated


packages: pestphp/pest livewire/flux livewire/livewire laravel/framework

queries: browser testing keyboard navigation aria labels console warnings

queries: form labels validation errors browser testing

queries: accessibility aria labels keyboard navigation


server: laravel-boost

tool: search-docs

type: text

text:
# Search Results

## Wire Ref Accessing `$wire`
Wire Ref | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/wire-ref

## Accessing `$wire`

If you wish to access `$wire` for a component with a ref, you can do so via the `.$wire` propert on the element:

```php
<div>
    <!-- ... -->

    <livewire:modal wire:ref="modal">
        <!-- ... -->

        <button wire:click="save()">Save</button>
    </livewire:modal>
</div>

<script>
    this.$intercept('save', ({ onFinish }) => {
        onFinish(() => {
            this.$refs.modal.$wire.close()
        })
    })
</script>
```

---

## Validation Form objects
Validation | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/validation

## Form objects

As more properties and validation rules are added to a Livewire component, it can begin to feel too crowded. To alleviate this pain and also provide a helpful abstraction for code reuse, you can use Livewire's *Form Objects* to store your properties and validation rules.

Below is the same `CreatePost` example, but now the properties and rules have been extracted to a dedicated form object named `PostForm`:

```php
<?php

namespace App\Livewire\Forms;

use Livewire\Attributes\Validate;
use Livewire\Form;

class PostForm extends Form
{
    #[Validate('required|min:3')]
	public $title = '';

    #[Validate('required|min:3')]
    public $content = '';
}
```

The `PostForm` above can now be defined as a property on the `CreatePost` component:

```php
<?php

namespace App\Livewire;

use App\Livewire\Forms\PostForm;
use Livewire\Component;
use App\Models\Post;

class CreatePost extends Component
{
    public PostForm $form;

    public function save()
    {
		Post::create(
    		$this->form->all()
    	);

		return redirect()->to('/posts');
    }

    // ...
}
```

As you can see, instead of listing out each property individually, we can retrieve all the property values using the `->all()` method on the form object.

Also, when referencing the property names in the template, you must prepend `form.` to each instance:

```blade
<form wire:submit="save">
	<input type="text" wire:model="form.title">
    <div>@error('form.title') {{ $message }} @enderror</div>

	<textarea wire:model="form.content"></textarea>
    <div>@error('form.content') {{ $message }} @enderror</div>

	<button type="submit">Save</button>
</form>
```

When using form objects, `#[Validate]` attribute validation will be run every time a property is updated. However, if you disable this behavior by specifying `onUpdate: false` on the attribute, you can manually run a form object's validation using `$this->form->validate()`:

```php
public function save()
{
    Post::create(
        $this->form->validate()
    );

    return redirect()->to('/posts');
}
```

Form objects are a useful abstraction for most larger datasets and a variety of additional features that make them even more powerful. For more information, check out the comprehensive [form object documentation](/docs/4.x/forms#extracting-a-form-object).

---

## Wire Transition Browser support
Wire Transition | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/wire-transition

## Browser support

View Transitions are supported in Chrome 111+, Edge 111+, and Safari 18+. In browsers that don't support View Transitions, elements will appear and disappear without animation—the functionality still works, just without the visual transition.

> [!warning] Firefox has limited support
> Firefox 144+ supports basic view transitions, but does not support transition types.

[View browser support on caniuse.com →](https://caniuse.com/view-transitions)

---

## Testing Browser testing
Testing | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/testing

## Browser testing

Pest v4 includes first-party browser testing support powered by Playwright. This allows you to test your Livewire components in a real browser, interacting with them just like a user would.

### Installing browser testing

First, install the Pest browser plugin:

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

Next, install Playwright via npm:

```shell
npm install playwright@latest
npx playwright install
```

For complete browser testing documentation, see the [Pest browser testing guide](https://pestphp.com/docs/browser-testing).

### Writing browser tests

Instead of using `Livewire::test()`, you can use `Livewire::visit()` to test your component in a real browser:

```php
it('can create a new post', function () {
    Livewire::visit('post.create')
        ->type('[wire\:model="title"]', 'My first post')
        ->type('[wire\:model="content"]', 'This is the content')
        ->press('Save')
        ->assertSee('Post created successfully');
});
```

Browser tests are slower than unit tests but provide end-to-end confidence that your components work as expected in a real browser environment.

For a complete list of available browser testing assertions, see the [Pest browser testing assertions](https://pestphp.com/docs/browser-testing#content-available-assertions).

> [!info] When to use browser tests
> Use browser tests for critical user flows and complex interactions. For most component testing, the standard `Livewire::test()` approach is faster and sufficient.

---

## Wire Ref Accessing DOM elements
Wire Ref | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/wire-ref

## Accessing DOM elements

When you add `wire:ref` to an HTML element, you can access it via the `$refs` magic property.

Consider a character counter that updates in real-time:

```php
<div>
    <textarea wire:model="message" wire:ref="message"></textarea>

    Characters: <span wire:ref="count">0</span>

    <!-- ... -->
</div>

<script>
    this.$refs.message.addEventListener('input', (e) => {
        this.$refs.count.textContent = e.target.value.length
    })
</script>
```

---

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

### assertNoAccessibilityIssues

The `assertNoAccessibilityIssues` method asserts there are no "serious" accessibility issues on the page:

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

By default, the level is 1 (serious). You can change to one of the following levels:

```
0. Critical
1. Serious
2. Moderate
3. Minor
```

- The level 0 (critical) only reports issues that cause severe barriers for individuals with disabilities. The organization may be subject to legal action if these issues are not addressed.
- The level 1 (serious) includes all critical issues (level 0) and adds issues that significantly impact accessibility. The organization may be subject to legal action if these issues are not addressed.
- The level 2 (moderate) includes all serious issues (level 1) and adds issues that moderately affect accessibility. The end-user would appreciate the fix, but it is not a barrier.
- The level 3 (minor) includes all moderate issues (level 2) and adds issues that have a minor impact on accessibility. These issues are often related to best practices and do not significantly affect the user experience.

## Screenshot Assertions

---

## Understanding Nesting Performance implications
Understanding Nesting | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/understanding-nesting

## Performance implications

Livewire's independent component architecture can have both positive and negative implications for your application.

An advantage of this architecture is it allows you to isolate expensive portions of your application. For example, you can quarantine a slow database query to its own independent component, and its performance overhead won't impact the rest of the page.

However, the biggest drawback of this approach is that because components are entirely separate, inter-component communication/dependencies becomes more difficult.

For example, if you had a property passed down from the above parent `Posts` component to the nested `ShowPost` component, it wouldn't be "reactive". Because each component is independent, if a request to the parent component changed the value of the property being passed into `ShowPost`, it wouldn't update inside `ShowPost`.

Livewire has overcome a number of these hurdles and provides dedicated APIs for these scenarios like: [Reactive properties](/docs/4.x/nesting#reactive-props), [Modelable components](/docs/4.x/nesting#binding-to-child-data-using-wiremodel), and [the `$parent` object](/docs/4.x/nesting#directly-accessing-the-parent-from-the-child).

Armed with this knowledge of how nested Livewire components operate, you will be able to make more informed decisions about when and how to nest components within your application.

---

## Attribute Validate Example: Contact form
Attribute Validate | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-validate

## Example: Contact form

Here's a complete contact form with validation:

```php
<?php // resources/views/pages/⚡contact.blade.php

use Livewire\Attributes\Validate;
use Livewire\Component;
use App\Mail\ContactMessage;
use Illuminate\Support\Facades\Mail;

new class extends Component {
    #[Validate('required|min:2', as: 'name')]
    public $name = '';

    #[Validate('required|email')]
    public $email = '';

    #[Validate('required')]
    public $subject = '';

    #[Validate('required|min:10', as: 'message')]
    public $message = '';

    public function submit()
    {
        $validated = $this->validate();

        Mail::to('support@example.com')->send(new ContactMessage($validated));

        session()->flash('success', 'Message sent successfully!');

        $this->reset();
    }
};
?>

<div>
    @if (session('success'))
        <div class="alert">{{ session('success') }}</div>
    @endif

    <form wire:submit="submit">
        <div>
            <input type="text" wire:model.live.blur="name" placeholder="Your name">
            @error('name') <span class="error">{{ $message }}</span> @enderror
        </div>

        <div>
            <input type="email" wire:model.live.blur="email" placeholder="Your email">
            @error('email') <span class="error">{{ $message }}</span> @enderror
        </div>

        <div>
            <input type="text" wire:model.live.blur="subject" placeholder="Subject">
            @error('subject') <span class="error">{{ $message }}</span> @enderror
        </div>

        <div>
            <textarea wire:model.live.blur="message" placeholder="Your message"></textarea>
            @error('message') <span class="error">{{ $message }}</span> @enderror
        </div>

        <button type="submit">Send Message</button>
    </form>
</div>
```

Users get immediate feedback as they fill out the form, with friendly field names and helpful error messages.

---

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

### assertAriaAttribute

The `assertAriaAttribute` method asserts that the element matching the given selector has the given value in the provided aria attribute:

```php
$page->assertAriaAttribute('button', 'label', 'Close');
```

---

## Wire Model Accessing nested properties
Wire Model | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/wire-model

## Accessing nested properties

`wire:model` supports dot notation for binding to nested properties, array elements, and form object fields:

```blade
<input type="text" wire:model="address.city">

<input type="text" wire:model="items.0.name">

<input type="text" wire:model="form.title">
```

Bracket notation is also supported as an alternative:

```blade
<input type="text" wire:model="address['city']">

<input type="text" wire:model="items[0].name">
```

Both notations are equivalent — `address['city']` resolves the same path as `address.city`. Bracket and dot notation can be mixed freely within a single expression.

---

## Lifecycle Hooks Using hooks inside a form object
Lifecycle Hooks | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/lifecycle-hooks

## Using hooks inside a form object

Form objects in Livewire support property update hooks. These hooks work similarly to [component update hooks](#update), letting you perform actions when properties in the form object change.

Below is an example of a component using a `PostForm` form object:

```php
<?php // resources/views/components/post/⚡create.blade.php

use Livewire\Component;

new class extends Component {
    public PostForm $form;

    // ...
};
```

Here's the `PostForm` form object containing all the available hooks:

```php
namespace App\Livewire\Forms;

use Livewire\Attributes\Validate;
use Livewire\Form;

class PostForm extends Form
{
    public $title = '';

    public $tags = [];

    public function updating($property, $value)
    {
        // ...
    }

    public function updated($property, $value)
    {
        // ...
    }

    public function updatingTitle($value)
    {
        // ...
    }

    public function updatedTitle($value)
    {
        // ...
    }

    public function updatingTags($value, $key)
    {
        // ...
    }

    public function updatedTags($value, $key)
    {
        // ...
    }

    // ...
}
```

---

## Styles Browser support
Styles | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/styles

## Browser support

Scoped styles use [CSS nesting](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting), which is supported in all modern browsers (Chrome 120+, Firefox 117+, Safari 17.2+). For older browser support, consider using a CSS preprocessor or the `@assets` directive with pre-compiled stylesheets.

---

## Validation Accessing errors in JavaScript
Validation | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/validation

## Accessing errors in JavaScript

Livewire provides a `$errors` magic property for client-side access to validation errors:

```blade
<form wire:submit="save">
    <input type="email" wire:model="email">

    <div wire:show="$errors.has('email')">
        <span wire:text="$errors.first('email')"></span>
    </div>

    <button type="submit">Save</button>
</form>
```

### Available methods

- `$errors.has('field')` - Check if a field has errors
- `$errors.missing('field')` - Check if a field doesn't have errors
- `$errors.first('field')` - Get the first error message for a field
- `$errors.get('field')` - Get all error messages for a field
- `$errors.all()` - Get all errors for all fields
- `$errors.clear()` - Clear all errors
- `$errors.clear('field')` - Clear errors for a specific field

When using Alpine.js, access `$errors` through `$wire.$errors`.

---

## Forms See also
Forms | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/forms

## See also

- **[Validation](/docs/4.x/validation)** — Validate form inputs with real-time feedback
- **[wire:model](/docs/4.x/wire-model)** — Bind form inputs to component properties
- **[File Uploads](/docs/4.x/uploads)** — Handle file uploads in forms
- **[Actions](/docs/4.x/actions)** — Process form submissions with actions
- **[Loading States](/docs/4.x/loading-states)** — Show loading indicators during form submission

---

## Installation Components not updating or errors in browser console
Installation | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/installation

## Components not updating or errors in browser console

**Check the following:**
- Ensure `@livewireStyles` is in the `<head>` of your layout
- Ensure `@livewireScripts` is before `</body>` in your layout
- Check your browser's developer console for JavaScript errors
- Verify you're running a supported PHP version (8.1+) and Laravel version (10+)
- Clear your application cache: `php artisan cache:clear`

If issues persist, check the [troubleshooting documentation](/docs/4.x/troubleshooting) for more detailed debugging steps.

---

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

### keys

The `keys` method sends the given keys to the element matching the given selector:

```php
$page->keys('input[name=password]', 'secret');
$page->keys('input[name=password]', ['{Control}', 'a']); // Keyboard shortcuts
```

---

## Blade Anonymous Components Accessing Parent Data
Blade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/blade.md

### Accessing Parent Data

Sometimes you may want to access data from a parent component inside a child component. In these cases, you may use the `@aware` directive. For example, imagine we are building a complex menu component consisting of a parent `<x-menu>` and child `<x-menu.item>`:

```blade
<x-menu color="purple">
    <x-menu.item>...</x-menu.item>
    <x-menu.item>...</x-menu.item>
</x-menu>
```

The `<x-menu>` component may have an implementation like the following:

```blade
<!-- /resources/views/components/menu/index.blade.php -->

@props(['color' => 'gray'])

<ul {{ $attributes->merge(['class' => 'bg-'.$color.'-200']) }}>
    {{ $slot }}
</ul>
```

Because the `color` prop was only passed into the parent (`<x-menu>`), it won't be available inside `<x-menu.item>`. However, if we use the `@aware` directive, we can make it available inside `<x-menu.item>` as well:

```blade
<!-- /resources/views/components/menu/item.blade.php -->

@aware(['color' => 'gray'])

<li {{ $attributes->merge(['class' => 'text-'.$color.'-800']) }}>
    {{ $slot }}
</li>
```

> [!WARNING]
> The `@aware` directive cannot access parent data that is not explicitly passed to the parent component via HTML attributes. Default `@props` values that are not explicitly passed to the parent component cannot be accessed by the `@aware` directive.

---

## Blade Forms Validation Errors
Blade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/blade.md

### Validation Errors

The `@error` directive may be used to quickly check if [validation error messages](/docs/{{version}}/validation#quick-displaying-the-validation-errors) exist for a given attribute. Within an `@error` directive, you may echo the `$message` variable to display the error message:

```blade
<!-- /resources/views/post/create.blade.php -->

<label for="title">Post Title</label>

<input
    id="title"
    type="text"
    class="@error('title') is-invalid @enderror"
/>

@error('title')
    <div class="alert alert-danger">{{ $message }}</div>
@enderror
```

Since the `@error` directive compiles to an "if" statement, you may use the `@else` directive to render content when there is not an error for an attribute:

```blade
<!-- /resources/views/auth.blade.php -->

<label for="email">Email address</label>

<input
    id="email"
    type="email"
    class="@error('email') is-invalid @else is-valid @enderror"
/>
```

You may pass [the name of a specific error bag](/docs/{{version}}/validation#named-error-bags) as the second parameter to the `@error` directive to retrieve validation error messages on pages containing multiple forms:

```blade
<!-- /resources/views/auth.blade.php -->

<label for="email">Email address</label>

<input
    id="email"
    type="email"
    class="@error('email', 'login') is-invalid @enderror"
/>

@error('email', 'login')
    <div class="alert alert-danger">{{ $message }}</div>
@enderror
```

---

## Properties Accessing properties from JavaScript
Properties | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/properties

## Accessing properties from JavaScript

Because Livewire properties are also available in the browser via JavaScript, you can access and manipulate their JavaScript representations from [AlpineJS](https://alpinejs.dev/).

Alpine is a lightweight JavaScript library that is included with Livewire. Alpine provides a way to build lightweight interactions into your Livewire components without making full server roundtrips.

Internally, Livewire's frontend is built on top of Alpine. In fact, every Livewire component is actually an Alpine component under-the-hood. This means that you can freely utilize Alpine inside your Livewire components.

The rest of this page assumes a basic familiarity with Alpine. If you're unfamiliar with Alpine, [take a look at the Alpine documentation](https://alpinejs.dev/docs).

### Accessing properties

Livewire exposes a magic `$wire` object to Alpine. You can access the `$wire` object from any Alpine expression inside your Livewire component.

The `$wire` object can be treated like a JavaScript version of your Livewire component. It has all the same properties and methods as the PHP version of your component, but also contains a few dedicated methods to perform specific functions in your template.

For example, we can use `$wire` to show a live character count of the `todo` input field:

```blade
<div>
    <input type="text" wire:model="todo">

    Todo character length: <h2 x-text="$wire.todo.length"></h2>
</div>
```

As the user types in the field, the character length of the current todo being written will be shown and live-updated on the page, all without sending a network request to the server.

### Manipulating properties

Similarly, you can manipulate your Livewire component properties in JavaScript using `$wire`.

For example, let's add a "Clear" button to the `todos` component to allow the user to reset the input field using only JavaScript:

```blade
<div>
    <input type="text" wire:model="todo">

    <button x-on:click="$wire.todo = ''">Clear</button>
</div>
```

After the user clicks "Clear", the input will be reset to an empty string, without sending a network request to the server.

On the subsequent request, the server-side value of `$todo` will be updated and synchronized.

If you prefer, you can also use the more explicit `.set()` method for setting properties client-side. However, you should note that using `.set()` by default immediately triggers a network request and synchronizes the state with the server. If that is desired, then this is an excellent API:

```blade
<button x-on:click="$wire.set('todo', '')">Clear</button>
```

In order to update the property without sending a network request to the server, you can pass a third bool parameter. This will defer the network request and on a subsequent request, the state will be synchronized on the server-side:
```blade
<button x-on:click="$wire.set('todo', '', false)">Clear</button>
```

_[Truncated]_


output:
Wall time: 3.4094 seconds
Output:
[{"type":"text","text":"# Search Results\n\n## Wire Ref Accessing `$wire`\nWire Ref | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/wire-ref\n\n## Accessing `$wire`\n\nIf you wish to access `$wire` for a component with a ref, you can do so via the `.$wire` propert on the element:\n\n```php\n<div>\n    <!-- ... -->\n\n    <livewire:modal wire:ref=\"modal\">\n        <!-- ... -->\n\n        <button wire:click=\"save()\">Save</button>\n    </livewire:modal>\n</div>\n\n<script>\n    this.$intercept('save', ({ onFinish }) => {\n        onFinish(() => {\n            this.$refs.modal.$wire.close()\n        })\n    })\n</script>\n```\n\n---\n\n## Validation Form objects\nValidation | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/validation\n\n## Form objects\n\nAs more properties and validation rules are added to a Livewire component, it can begin to feel too crowded. To alleviate this pain and also provide a helpful abstraction for code reuse, you can use Livewire's *Form Objects* to store your properties and validation rules.\n\nBelow is the same `CreatePost` example, but now the properties and rules have been extracted to a dedicated form object named `PostForm`:\n\n```php\n<?php\n\nnamespace App\\Livewire\\Forms;\n\nuse Livewire\\Attributes\\Validate;\nuse Livewire\\Form;\n\nclass PostForm extends Form\n{\n    #[Validate('required|min:3')]\n\tpublic $title = '';\n\n    #[Validate('required|min:3')]\n    public $content = '';\n}\n```\n\nThe `PostForm` above can now be defined as a property on the `CreatePost` component:\n\n```php\n<?php\n\nnamespace App\\Livewire;\n\nuse App\\Livewire\\Forms\\PostForm;\nuse Livewire\\Component;\nuse App\\Models\\Post;\n\nclass CreatePost extends Component\n{\n    public PostForm $form;\n\n    public function save()\n    {\n\t\tPost::create(\n    \t\t$this->form->all()\n    \t);\n\n\t\treturn redirect()->to('/posts');\n    }\n\n    // ...\n}\n```\n\nAs you can see, instead of listing out each property individually, we can retrieve all the property values using the `->all()` method on the form object.\n\nAlso, when referencing the property names in the template, you must prepend `form.` to each instance:\n\n```blade\n<form wire:submit=\"save\">\n\t<input type=\"text\" wire:model=\"form.title\">\n    <div>@error('form.title') {{ $message }} @enderror</div>\n\n\t<textarea wire:model=\"form.content\"></textarea>\n    <div>@error('form.content') {{ $message }} @enderror</div>\n\n\t<button type=\"submit\">Save</button>\n</form>\n```\n\nWhen using form objects, `#[Validate]` attribute validation will be run every time a property is updated. However, if you disable this behavior by specifying `onUpdate: false` on the attribute, you can manually run a form object's validation using `$this->form->validate()`:\n\n```php\npublic function save()\n{\n    Post::create(\n        $this->form->validate()\n    );\n\n    return redirect()->to('/posts');\n}\n```\n\nForm objects are a useful abstraction for most larger datasets and a variety of additional features that make them even more powerful. For more information, check out the comprehensive [form object documentation](/docs/4.x/forms#extracting-a-form-object).\n\n---\n\n## Wire Transition Browser support\nWire Transition | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/wire-transition\n\n## Browser support\n\nView Transitions are supported in Chrome 111+, Edge 111+, and Safari 18+. In browsers that don't support View Transitions, elements will appear and disappear without animation—the functionality still works, just without the visual transition.\n\n> [!warning] Firefox has limited support\n> Firefox 144+ supports basic view transitions, but does not support transition types.\n\n[View browser support on caniuse.com →](https://caniuse.com/view-transitions)\n\n---\n\n## Testing Browser testing\nTesting | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/testing\n\n## Browser testing\n\nPest v4 includes first-party browser testing support powered by Playwright. This allows you to test your Livewire components in a real browser, interacting with them just like a user would.\n\n### Installing browser testing\n\nFirst, install the Pest browser plugin:\n\n```shell\ncomposer require pestphp/pest-plugin-browser --dev\n```\n\nNext, install Playwright via npm:\n\n```shell\nnpm install playwright@latest\nnpx playwright install\n```\n\nFor complete browser testing documentation, see the [Pest browser testing guide](https://pestphp.com/docs/browser-testing).\n\n### Writing browser tests\n\nInstead of using `Livewire::test()`, you can use `Livewire::visit()` to test your component in a real browser:\n\n```php\nit('can create a new post', function () {\n    Livewire::visit('post.create')\n        ->type('[wire\\:model=\"title\"]', 'My first post')\n        ->type('[wire\\:model=\"content\"]', 'This is the content')\n        ->press('Save')\n        ->assertSee('Post created successfully');\n});\n```\n\nBrowser tests are slower than unit tests but provide end-to-end confidence that your components work as expected in a real browser environment.\n\nFor a complete list of available browser testing assertions, see the [Pest browser testing assertions](https://pestphp.com/docs/browser-testing#content-available-assertions).\n\n> [!info] When to use browser tests\n> Use browser tests for critical user flows and complex interactions. For most component testing, the standard `Livewire::test()` approach is faster and sufficient.\n\n---\n\n## Wire Ref Accessing DOM elements\nWire Ref | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/wire-ref\n\n## Accessing DOM elements\n\nWhen you add `wire:ref` to an HTML element, you can access it via the `$refs` magic property.\n\nConsider a character counter that updates in real-time:\n\n```php\n<div>\n    <textarea wire:model=\"message\" wire:ref=\"message\"></textarea>\n\n    Characters: <span wire:ref=\"count\">0</span>\n\n    <!-- ... -->\n</div>\n\n<script>\n    this.$refs.message.addEventListener('input', (e) => {\n        this.$refs.count.textContent = e.target.value.length\n    })\n</script>\n```\n\n---\n\n## Browser Testing assertNoAccessibilityIssues\nBrowser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md\n\n\n### assertNoAccessibilityIssues\n\nThe `assertNoAccessibilityIssues` method asserts there are no \"serious\" accessibility issues on the page:\n\n```php\n$page->assertNoAccessibilityIssues();\n```\n\nBy default, the level is 1 (serious). You can change to one of the following levels:\n\n```\n0. Critical\n1. Serious\n2. Moderate\n3. Minor\n```\n\n- The level 0 (critical) only reports issues that cause severe barriers for individuals with disabilities. The organization may be subject to legal action if these issues are not addressed.\n- The level 1 (serious) includes all critical issues (level 0) and adds issues that significantly impact accessibility. The organization may be subject to legal action if these issues are not addressed.\n- The level 2 (moderate) includes all serious issues (level 1) and adds issues that moderately affect accessibility. The end-user would appreciate the fix, but it is not a barrier.\n- The level 3 (minor) includes all moderate issues (level 2) and adds issues that have a minor impact on accessibility. These issues are often related to best practices and do not significantly affect the user experience.\n\n## Screenshot Assertions\n\n---\n\n## Understanding Nesting Performance implications\nUnderstanding Nesting | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/understanding-nesting\n\n## Performance implications\n\nLivewire's independent component architecture can have both positive and negative implications for your application.\n\nAn advantage of this architecture is it allows you to isolate expensive portions of your application. For example, you can quarantine a slow database query to its own independent component, and its performance overhead won't impact the rest of the page.\n\nHowever, the biggest drawback of this approach is that because components are entirely separate, inter-component communication/dependencies becomes more difficult.\n\nFor example, if you had a property passed down from the above parent `Posts` component to the nested `ShowPost` component, it wouldn't be \"reactive\". Because each component is independent, if a request to the parent component changed the value of the property being passed into `ShowPost`, it wouldn't update inside `ShowPost`.\n\nLivewire has overcome a number of these hurdles and provides dedicated APIs for these scenarios like: [Reactive properties](/docs/4.x/nesting#reactive-props), [Modelable components](/docs/4.x/nesting#binding-to-child-data-using-wiremodel), and [the `$parent` object](/docs/4.x/nesting#directly-accessing-the-parent-from-the-child).\n\nArmed with this knowledge of how nested Livewire components operate, you will be able to make more informed decisions about when and how to nest components within your application.\n\n---\n\n## Attribute Validate Example: Contact form\nAttribute Validate | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-validate\n\n## Example: Contact form\n\nHere's a complete contact form with validation:\n\n```php\n<?php // resources/views/pages/⚡contact.blade.php\n\nuse Livewire\\Attributes\\Validate;\nuse Livewire\\Component;\nuse App\\Mail\\ContactMessage;\nuse Illuminate\\Support\\Facades\\Mail;\n\nnew class extends Component {\n    #[Validate('required|min:2', as: 'name')]\n    public $name = '';\n\n    #[Validate('required|email')]\n    public $email = '';\n\n    #[Validate('required')]\n    public $subject = '';\n\n    #[Validate('required|min:10', as: 'message')]\n    public $message = '';\n\n    public function submit()\n    {\n        $validated = $this->validate();\n\n        Mail::to('support@example.com')->send(new ContactMessage($validated));\n\n        session()->flash('success', 'Message sent successfully!');\n\n        $this->reset();\n    }\n};\n?>\n\n<div>\n    @if (session('success'))\n        <div class=\"alert\">{{ session('success') }}</div>\n    @endif\n\n    <form wire:submit=\"submit\">\n        <div>\n            <input type=\"text\" wire:model.live.blur=\"name\" placeholder=\"Your name\">\n            @error('name') <span class=\"error\">{{ $message }}</span> @enderror\n        </div>\n\n        <div>\n            <input type=\"email\" wire:model.live.blur=\"email\" placeholder=\"Your email\">\n            @error('email') <span class=\"error\">{{ $message }}</span> @enderror\n        </div>\n\n        <div>\n            <input type=\"text\" wire:model.live.blur=\"subject\" placeholder=\"Subject\">\n            @error('subject') <span class=\"error\">{{ $message }}</span> @enderror\n        </div>\n\n        <div>\n            <textarea wire:model.live.blur=\"message\" placeholder=\"Your message\"></textarea>\n            @error('message') <span class=\"error\">{{ $message }}</span> @enderror\n        </div>\n\n        <button type=\"submit\">Send Message</button>\n    </form>\n</div>\n```\n\nUsers get immediate feedback as they fill out the form, with friendly field names and helpful error messages.\n\n---\n\n## Browser Testing assertAriaAttribute\nBrowser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md\n\n\n### assertAriaAttribute\n\nThe `assertAriaAttribute` method asserts that the element matching the given selector has the given value in the provided aria attribute:\n\n```php\n$page->assertAriaAttribute('button', 'label', 'Close');\n```\n\n---\n\n## Wire Model Accessing nested properties\nWire Model | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/wire-model\n\n## Accessing nested properties\n\n`wire:model` supports dot notation for binding to nested properties, array elements, and form object fields:\n\n```blade\n<input type=\"text\" wire:model=\"address.city\">\n\n<input type=\"text\" wire:model=\"items.0.name\">\n\n<input type=\"text\" wire:model=\"form.title\">\n```\n\nBracket notation is also supported as an alternative:\n\n```blade\n<input type=\"text\" wire:model=\"address['city']\">\n\n<input type=\"text\" wire:model=\"items[0].name\">\n```\n\nBoth notations are equivalent — `address['city']` resolves the same path as `address.city`. Bracket and dot notation can be mixed freely within a single expression.\n\n---\n\n## Lifecycle Hooks Using hooks inside a form object\nLifecycle Hooks | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/lifecycle-hooks\n\n## Using hooks inside a form object\n\nForm objects in Livewire support property update hooks. These hooks work similarly to [component update hooks](#update), letting you perform actions when properties in the form object change.\n\nBelow is an example of a component using a `PostForm` form object:\n\n```php\n<?php // resources/views/components/post/⚡create.blade.php\n\nuse Livewire\\Component;\n\nnew class extends Component {\n    public PostForm $form;\n\n    // ...\n};\n```\n\nHere's the `PostForm` form object containing all the available hooks:\n\n```php\nnamespace App\\Livewire\\Forms;\n\nuse Livewire\\Attributes\\Validate;\nuse Livewire\\Form;\n\nclass PostForm extends Form\n{\n    public $title = '';\n\n    public $tags = [];\n\n    public function updating($property, $value)\n    {\n        // ...\n    }\n\n    public function updated($property, $value)\n    {\n        // ...\n    }\n\n    public function updatingTitle($value)\n    {\n        // ...\n    }\n\n    public function updatedTitle($value)\n    {\n        // ...\n    }\n\n    public function updatingTags($value, $key)\n    {\n        // ...\n    }\n\n    public function updatedTags($value, $key)\n    {\n        // ...\n    }\n\n    // ...\n}\n```\n\n---\n\n## Styles Browser support\nStyles | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/styles\n\n## Browser support\n\nScoped styles use [CSS nesting](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting), which is supported in all modern browsers (Chrome 120+, Firefox 117+, Safari 17.2+). For older browser support, consider using a CSS preprocessor or the `@assets` directive with pre-compiled stylesheets.\n\n---\n\n## Validation Accessing errors in JavaScript\nValidation | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/validation\n\n## Accessing errors in JavaScript\n\nLivewire provides a `$errors` magic property for client-side access to validation errors:\n\n```blade\n<form wire:submit=\"save\">\n    <input type=\"email\" wire:model=\"email\">\n\n    <div wire:show=\"$errors.has('email')\">\n        <span wire:text=\"$errors.first('email')\"></span>\n    </div>\n\n    <button type=\"submit\">Save</button>\n</form>\n```\n\n### Available methods\n\n- `$errors.has('field')` - Check if a field has errors\n- `$errors.missing('field')` - Check if a field doesn't have errors\n- `$errors.first('field')` - Get the first error message for a field\n- `$errors.get('field')` - Get all error messages for a field\n- `$errors.all()` - Get all errors for all fields\n- `$errors.clear()` - Clear all errors\n- `$errors.clear('field')` - Clear errors for a specific field\n\nWhen using Alpine.js, access `$errors` through `$wire.$errors`.\n\n---\n\n## Forms See also\nForms | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/forms\n\n## See also\n\n- **[Validation](/docs/4.x/validation)** — Validate form inputs with real-time feedback\n- **[wire:model](/docs/4.x/wire-model)** — Bind form inputs to component properties\n- **[File Uploads](/docs/4.x/uploads)** — Handle file uploads in forms\n- **[Actions](/docs/4.x/actions)** — Process form submissions with actions\n- **[Loading States](/docs/4.x/loading-states)** — Show loading indicators during form submission\n\n---\n\n## Installation Components not updating or errors in browser console\nInstallation | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/installation\n\n## Components not updating or errors in browser console\n\n**Check the following:**\n- Ensure `@livewireStyles` is in the `<head>` of your layout\n- Ensure `@livewireScripts` is before `</body>` in your layout\n- Check your browser's developer console for JavaScript errors\n- Verify you're running a supported PHP version (8.1+) and Laravel version (10+)\n- Clear your application cache: `php artisan cache:clear`\n\nIf issues persist, check the [troubleshooting documentation](/docs/4.x/troubleshooting) for more detailed debugging steps.\n\n---\n\n## Browser Testing keys\nBrowser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md\n\n\n### keys\n\nThe `keys` method sends the given keys to the element matching the given selector:\n\n```php\n$page->keys('input[name=password]', 'secret');\n$page->keys('input[name=password]', ['{Control}', 'a']); // Keyboard shortcuts\n```\n\n---\n\n## Blade Anonymous Components Accessing Parent Data\nBlade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/blade.md\n\n\n### Accessing Parent Data\n\nSometimes you may want to access data from a parent component inside a child component. In these cases, you may use the `@aware` directive. For example, imagine we are building a complex menu component consisting of a parent `<x-menu>` and child `<x-menu.item>`:\n\n```blade\n<x-menu color=\"purple\">\n    <x-menu.item>...</x-menu.item>\n    <x-menu.item>...</x-menu.item>\n</x-menu>\n```\n\nThe `<x-menu>` component may have an implementation like the following:\n\n```blade\n<!-- /resources/views/components/menu/index.blade.php -->\n\n@props(['color' => 'gray'])\n\n<ul {{ $attributes->merge(['class' => 'bg-'.$color.'-200']) }}>\n    {{ $slot }}\n</ul>\n```\n\nBecause the `color` prop was only passed into the parent (`<x-menu>`), it won't be available inside `<x-menu.item>`. However, if we use the `@aware` directive, we can make it available inside `<x-menu.item>` as well:\n\n```blade\n<!-- /resources/views/components/menu/item.blade.php -->\n\n@aware(['color' => 'gray'])\n\n<li {{ $attributes->merge(['class' => 'text-'.$color.'-800']) }}>\n    {{ $slot }}\n</li>\n```\n\n> [!WARNING]\n> The `@aware` directive cannot access parent data that is not explicitly passed to the parent component via HTML attributes. Default `@props` values that are not explicitly passed to the parent component cannot be accessed by the `@aware` directive.\n\n---\n\n## Blade Forms Validation Errors\nBlade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/blade.md\n\n\n### Validation Errors\n\nThe `@error` directive may be used to quickly check if [validation error messages](/docs/{{version}}/validation#quick-displaying-the-validation-errors) exist for a given attribute. Within an `@error` directive, you may echo the `$message` variable to display the error message:\n\n```blade\n<!-- /resources/views/post/create.blade.php -->\n\n<label for=\"title\">Post Title</label>\n\n<input\n    id=\"title\"\n    type=\"text\"\n    class=\"@error('title') is-invalid @enderror\"\n/>\n\n@error('title')\n    <div class=\"alert alert-danger\">{{ $message }}</div>\n@enderror\n```\n\nSince the `@error` directive compiles to an \"if\" statement, you may use the `@else` directive to render content when there is not an error for an attribute:\n\n```blade\n<!-- /resources/views/auth.blade.php -->\n\n<label for=\"email\">Email address</label>\n\n<input\n    id=\"email\"\n    type=\"email\"\n    class=\"@error('email') is-invalid @else is-valid @enderror\"\n/>\n```\n\nYou may pass [the name of a specific error bag](/docs/{{version}}/validation#named-error-bags) as the second parameter to the `@error` directive to retrieve validation error messages on pages containing multiple forms:\n\n```blade\n<!-- /resources/views/auth.blade.php -->\n\n<label for=\"email\">Email address</label>\n\n<input\n    id=\"email\"\n    type=\"email\"\n    class=\"@error('email', 'login') is-invalid @enderror\"\n/>\n\n@error('email', 'login')\n    <div class=\"alert alert-danger\">{{ $message }}</div>\n@enderror\n```\n\n---\n\n## Properties Accessing properties from JavaScript\nProperties | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/properties\n\n## Accessing properties from JavaScript\n\nBecause Livewire properties are also available in the browser via JavaScript, you can access and manipulate their JavaScript representations from [AlpineJS](https://alpinejs.dev/).\n\nAlpine is a lightweight JavaScript library that is included with Livewire. Alpine provides a way to build lightweight interactions into your Livewire components without making full server roundtrips.\n\nInternally, Livewire's frontend is built on top of Alpine. In fact, every Livewire component is actually an Alpine component under-the-hood. This means that you can freely utilize Alpine inside your Livewire components.\n\nThe rest of this page assumes a basic familiarity with Alpine. If you're unfamiliar with Alpine, [take a look at the Alpine documentation](https://alpinejs.dev/docs).\n\n### Accessing properties\n\nLivewire exposes a magic `$wire` object to Alpine. You can access the `$wire` object from any Alpine expression inside your Livewire component.\n\nThe `$wire` object can be treated like a JavaScript version of your Livewire component. It has all the same properties and methods as the PHP version of your component, but also contains a few dedicated methods to perform specific functions in your template.\n\nFor example, we can use `$wire` to show a live character count of the `todo` input field:\n\n```blade\n<div>\n    <input type=\"text\" wire:model=\"todo\">\n\n    Todo character length: <h2 x-text=\"$wire.todo.length\"></h2>\n</div>\n```\n\nAs the user types in the field, the character length of the current todo being written will be shown and live-updated on the page, all without sending a network request to the server.\n\n### Manipulating properties\n\nSimilarly, you can manipulate your Livewire component properties in JavaScript using `$wire`.\n\nFor example, let's add a \"Clear\" button to the `todos` component to allow the user to reset the input field using only JavaScript:\n\n```blade\n<div>\n    <input type=\"text\" wire:model=\"todo\">\n\n    <button x-on:click=\"$wire.todo = ''\">Clear</button>\n</div>\n```\n\nAfter the user clicks \"Clear\", the input will be reset to an empty string, without sending a network request to the server.\n\nOn the subsequent request, the server-side value of `$todo` will be updated and synchronized.\n\nIf you prefer, you can also use the more explicit `.set()` method for setting properties client-side. However, you should note that using `.set()` by default immediately triggers a network request and synchronizes the state with the server. If that is desired, then this is an excellent API:\n\n```blade\n<button x-on:click=\"$wire.set('todo', '')\">Clear</button>\n```\n\nIn order to update the property without sending a network request to the server, you can pass a third bool parameter. This will defer the network request and on a subsequent request, the state will be synchronized on the server-side:\n```blade\n<button x-on:click=\"$wire.set('todo', '', false)\">Clear</button>\n```\n\n_[Truncated]_"}]


cmd: sed -n '2680,2868p' specs/08-PLAYWRIGHT-E2E-PLAN.md


cmd: rg -n "assertNoAccessibilityIssues|assertNoConsoleLogs|console warnings|keys\(|press\(" tests/Browser vendor/pestphp/pest-plugin-browser/src -g '*.php'


cmd: sed -n '1,220p' resources/views/components/storefront/product-card.blade.php && sed -n '1,160p' resources/views/components/storefront/breadcrumbs.blade.php


cmd: sed -n '1,220p' resources/views/livewire/storefront/account/auth/login.blade.php && sed -n '1,220p' resources/views/livewire/auth/login.blade.php


cmd: sed -n '1,220p' resources/views/livewire/storefront/search/index.blade.php


command: sed -n '1,220p' resources/views/components/storefront/product-card.blade.php && sed -n '1,160p' resources/views/components/storefront/breadcrumbs.blade.php

product-card.blade.php

type: read

breadcrumbs.blade.php

aggregated_output:
@props(['product'])

@php
    $variant = $product->variants->first();
    $isSoldOut = $product->variants->isNotEmpty()
        && $product->variants->every(fn ($variant) => $variant->inventoryItem?->policy?->value === 'deny' && $variant->inventoryItem?->availableQuantity() <= 0);
    $isOnSale = $variant?->compare_at_amount && $variant->compare_at_amount > $variant->price_amount;
@endphp

class('group') }}>
    handle) }}" class="block space-y-3" wire:navigate>

                @if ($isOnSale)
                    Sale
                @endif

                @if ($isSoldOut)
                    Sold out
                @endif

                <flux:icon name="shopping-bag" class="size-12" />

{{ $product->title }}

            @if ($variant)
                <x-storefront.price :amount="$variant->price_amount" :compare-at="$variant->compare_at_amount" :currency="$variant->currency" class="text-sm" />
            @endif

                {{ $product->variants_count ?? $product->variants->count() }} {{ Str::plural('variant', $product->variants_count ?? $product->variants->count()) }}

@props(['items' => []])

class('flex flex-wrap items-center gap-2 text-sm text-zinc-500 dark:text-zinc-400') }} aria-label="Breadcrumb">
    Home

    @foreach ($items as $item)
        /

        @if (($item['url'] ?? null) && ! $loop->last)
            {{ $item['label'] }}
        @else
            {{ $item['label'] }}
        @endif
    @endforeach

status: completed


command: sed -n '2680,2868p' specs/08-PLAYWRIGHT-E2E-PLAN.md

aggregated_output:
## 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 `/`

**Expected Results:**
- No JavaScript errors
- No console log warnings

---

### Test 14.2: Home page has proper heading hierarchy

**Preconditions:** Seeded store

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

**Expected Results:**
- Page has exactly one h1 element
- "Acme Fashion" is visible (h1 content)
- Headings follow a logical order
- No JavaScript errors

---

### Test 14.3: Product page has proper ARIA labels for variant selector

**Preconditions:** Product #1

**Steps:**
1. Navigate to `/products/classic-cotton-t-shirt`

**Expected Results:**
- "Size" and "Color" labels are visible for variant selectors
- "Add to cart" button is properly labeled
- No JavaScript errors

---

### Test 14.4: Product page images have alt text

**Preconditions:** Product #1 with images

**Steps:**
1. Navigate to `/products/classic-cotton-t-shirt`

**Expected Results:**
- All product images have non-empty, meaningful alt attributes
- No JavaScript errors

---

### Test 14.5: Customer login form has accessible labels

**Preconditions:** None

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

**Expected Results:**
- "Email" label is visible and associated with the email input
- "Password" label is visible and associated with the password input
- No JavaScript errors

---

### Test 14.6: Admin login form has accessible labels

**Preconditions:** None

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

**Expected Results:**
- "Email" label is visible and associated with the email input
- "Password" label is visible and associated with the password input
- No JavaScript errors

---

### Test 14.7: Checkout form has accessible labels

**Preconditions:** Product #1

**Steps:**
1. Navigate to `/products/classic-cotton-t-shirt`
2. Click "M" to select size M
3. Click "Black" to select color Black
4. Click "Add to cart"
5. Navigate to `/cart`
6. Click "Checkout"

**Expected Results:**
- "Email" label is visible
- Form fields have associated labels
- No JavaScript errors

---

### Test 14.8: Checkout validation errors are accessible

**Preconditions:** Product #1

**Steps:**
1. Navigate to `/products/classic-cotton-t-shirt`
2. Click "M" to select size M
3. Click "Black" to select color Black
4. Click "Add to cart"
5. Navigate to `/cart`
6. Click "Checkout"
7. Click "Continue" without filling any fields

**Expected Results:**
- Validation error referencing "email" is visible
- Errors are linked to their respective fields
- No JavaScript errors

---

### Test 14.9: Can navigate storefront with keyboard only

**Preconditions:** Seeded store

**Steps:**
1. Navigate to `/`
2. Press Tab key repeatedly to move focus through interactive elements
3. Press Enter on a focused navigation link

**Expected Results:**
- Focus indicators are visible on interactive elements
- Navigation works via keyboard (Enter activates links)
- No JavaScript errors

---

### Test 14.10: Cart page has no console errors or warnings

**Preconditions:** None

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

**Expected Results:**
- No JavaScript errors
- No console log warnings

---

### Test 14.11: Search page has proper form labels

**Preconditions:** None

**Steps:**
1. Navigate to `/search?q=shirt`

**Expected Results:**
- Search input has a label or aria-label attribute
- No JavaScript errors

**Total: 11 tests**

---

## Suite 15: Admin Collections Management

**File:** `tests/Browser/Admin/CollectionManagementTest.php`


output:
Chunk ID: a50b78
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1106
Output:
## 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 `/`

**Expected Results:**
- No JavaScript errors
- No console log warnings

---

### Test 14.2: Home page has proper heading hierarchy

**Preconditions:** Seeded store

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

**Expected Results:**
- Page has exactly one h1 element
- "Acme Fashion" is visible (h1 content)
- Headings follow a logical order
- No JavaScript errors

---

### Test 14.3: Product page has proper ARIA labels for variant selector

**Preconditions:** Product #1

**Steps:**
1. Navigate to `/products/classic-cotton-t-shirt`

**Expected Results:**
- "Size" and "Color" labels are visible for variant selectors
- "Add to cart" button is properly labeled
- No JavaScript errors

---

### Test 14.4: Product page images have alt text

**Preconditions:** Product #1 with images

**Steps:**
1. Navigate to `/products/classic-cotton-t-shirt`

**Expected Results:**
- All product images have non-empty, meaningful alt attributes
- No JavaScript errors

---

### Test 14.5: Customer login form has accessible labels

**Preconditions:** None

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

**Expected Results:**
- "Email" label is visible and associated with the email input
- "Password" label is visible and associated with the password input
- No JavaScript errors

---

### Test 14.6: Admin login form has accessible labels

**Preconditions:** None

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

**Expected Results:**
- "Email" label is visible and associated with the email input
- "Password" label is visible and associated with the password input
- No JavaScript errors

---

### Test 14.7: Checkout form has accessible labels

**Preconditions:** Product #1

**Steps:**
1. Navigate to `/products/classic-cotton-t-shirt`
2. Click "M" to select size M
3. Click "Black" to select color Black
4. Click "Add to cart"
5. Navigate to `/cart`
6. Click "Checkout"

**Expected Results:**
- "Email" label is visible
- Form fields have associated labels
- No JavaScript errors

---

### Test 14.8: Checkout validation errors are accessible

**Preconditions:** Product #1

**Steps:**
1. Navigate to `/products/classic-cotton-t-shirt`
2. Click "M" to select size M
3. Click "Black" to select color Black
4. Click "Add to cart"
5. Navigate to `/cart`
6. Click "Checkout"
7. Click "Continue" without filling any fields

**Expected Results:**
- Validation error referencing "email" is visible
- Errors are linked to their respective fields
- No JavaScript errors

---

### Test 14.9: Can navigate storefront with keyboard only

**Preconditions:** Seeded store

**Steps:**
1. Navigate to `/`
2. Press Tab key repeatedly to move focus through interactive elements
3. Press Enter on a focused navigation link

**Expected Results:**
- Focus indicators are visible on interactive elements
- Navigation works via keyboard (Enter activates links)
- No JavaScript errors

---

### Test 14.10: Cart page has no console errors or warnings

**Preconditions:** None

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

**Expected Results:**
- No JavaScript errors
- No console log warnings

---

### Test 14.11: Search page has proper form labels

**Preconditions:** None

**Steps:**
1. Navigate to `/search?q=shirt`

**Expected Results:**
- Search input has a label or aria-label attribute
- No JavaScript errors

**Total: 11 tests**

---

## Suite 15: Admin Collections Management

**File:** `tests/Browser/Admin/CollectionManagementTest.php`


command: sed -n '1,220p' resources/views/livewire/storefront/search/index.blade.php

aggregated_output:
    <x-storefront.breadcrumbs :items="[['label' => 'Search results']]" />

            @if (trim($q) !== '')
                {{ $products->total() }} results for "{{ $q }}"
            @else
                Search products
            @endif

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

Filters

                <button wire:click="clearFilters" class="text-sm font-medium text-blue-700 hover:underline dark:text-blue-300">Clear all</button>

                <flux:checkbox wire:model.live="inStock" label="In stock" />

Price

                        <flux:input wire:model.live.debounce.500ms="minPrice" type="number" min="0" placeholder="Min" aria-label="Minimum price" />
                        <flux:input wire:model.live.debounce.500ms="maxPrice" type="number" min="0" placeholder="Max" aria-label="Maximum price" />

Product type

                        @foreach ($productTypes as $type)
                            <flux:checkbox wire:model.live="types" value="{{ $type }}" :label="$type" wire:key="search-type-filter-{{ $type }}" />
                        @endforeach

Vendor

                        @foreach ($productVendors as $vendor)
                            <flux:checkbox wire:model.live="vendors" value="{{ $vendor }}" :label="$vendor" wire:key="search-vendor-filter-{{ $vendor }}" />
                        @endforeach

{{ $products->total() }} products

                <flux:select wire:model.live="sort" aria-label="Sort products" class="sm:max-w-56">
                    <flux:select.option value="relevance">Relevance</flux:select.option>
                    <flux:select.option value="price_asc">Price: Low to High</flux:select.option>
                    <flux:select.option value="price_desc">Price: High to Low</flux:select.option>
                    <flux:select.option value="newest">Newest</flux:select.option>
                </flux:select>

            @if ($products->isEmpty())

                        <flux:icon name="magnifying-glass" class="size-10 text-zinc-400" />

No products found

Try another search term or adjust your filters.

                        <flux:button wire:click="clearFilters" variant="ghost">Clear filters</flux:button>

            @else

                    @foreach ($products as $product)
                        <x-storefront.product-card :product="$product" wire:key="search-product-{{ $product->getKey() }}" />
                    @endforeach

                {{ $products->links() }}
            @endif


command: sed -n '1,220p' resources/views/livewire/storefront/account/auth/login.blade.php && sed -n '1,220p' resources/views/livewire/auth/login.blade.php

login.blade.php

aggregated_output:
    <x-auth-header :title="__('Log in')" :description="__('Access your orders and saved addresses')" />

    <form wire:submit="login" class="flex flex-col gap-6">
        <flux:input
            wire:model="email"
            :label="__('Email address')"
            type="email"
            required
            autofocus
            autocomplete="email"
            placeholder="customer@acme.test"
        />

        <flux:input
            wire:model="password"
            :label="__('Password')"
            type="password"
            required
            autocomplete="current-password"
            :placeholder="__('Password')"
            viewable
        />

            <flux:link :href="route('customer.password.request')" wire:navigate>{{ __('Forgot your password?') }}</flux:link>

        <flux:checkbox wire:model="remember" :label="__('Remember me')" />

        <flux:button variant="primary" type="submit" class="w-full" data-test="customer-login-button">
            {{ __('Log in') }}
        </flux:button>
    </form>

        {{ __('New customer?') }}
        <flux:link :href="route('account.register')" wire:navigate>{{ __('Create an account') }}</flux:link>

<x-layouts::auth>

        <x-auth-header :title="__('Log in to your account')" :description="__('Enter your email and password below to log in')" />

        <x-auth-session-status class="text-center" :status="session('status')" />

        <form method="POST" action="{{ route('login.store') }}" class="flex flex-col gap-6">
            @csrf

            <flux:input
                name="email"
                :label="__('Email address')"
                :value="old('email')"
                type="email"
                required
                autofocus
                autocomplete="email"
                placeholder="email@example.com"
            />

                <flux:input
                    name="password"
                    :label="__('Password')"
                    type="password"
                    required
                    autocomplete="current-password"
                    :placeholder="__('Password')"
                    viewable
                />

                @if (Route::has('password.request'))
                    <flux:link class="absolute top-0 text-sm end-0" :href="route('password.request')" wire:navigate>
                        {{ __('Forgot your password?') }}
                    </flux:link>
                @endif

            <flux:checkbox name="remember" :label="__('Remember me')" :checked="old('remember')" />

                <flux:button variant="primary" type="submit" class="w-full" data-test="login-button">
                    {{ __('Log in') }}
                </flux:button>

        </form>

        @if (Route::has('register'))

                {{ __('Don\'t have an account?') }}
                <flux:link :href="route('register')" wire:navigate>{{ __('Sign up') }}</flux:link>

        @endif

</x-layouts::auth>


command: rg -n "assertNoAccessibilityIssues|assertNoConsoleLogs|console warnings|keys\(|press\(" tests/Browser vendor/pestphp/pest-plugin-browser/src -g '*.php'

type: search

cmd: rg -n "assertNoAccessibilityIssues|assertNoConsoleLogs|console warnings|keys\\(|press\\(" tests/Browser vendor/pestphp/pest-plugin-browser/src -g '*.php'

query: assertNoAccessibilityIssues|assertNoConsoleLogs|console warnings|keys\(|press\(

path: Browser

aggregated_output:
tests/Browser/SmokeTest.php:94:    $pages = visit(array_keys($expected), browserSmokeHost());
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/InteractsWithElements.php:47:    public function keys(string $selector, array|string $keys): self
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/InteractsWithElements.php:54:            $locator->press($key);
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/InteractsWithElements.php:191:    public function press(string $button): self
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/InteractsWithElements.php:201:        return $this->press($button)->wait($seconds);
vendor/pestphp/pest-plugin-browser/src/Playwright/Locator.php:215:    public function press(string $key, ?array $options = null): void
vendor/pestphp/pest-plugin-browser/src/Api/AwaitableWebpage.php:30:            'assertNoAccessibilityIssues',
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/MakesConsoleAssertions.php:25:        $this->assertNoConsoleLogs();
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/MakesConsoleAssertions.php:61:    public function assertNoConsoleLogs(): Webpage
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/MakesConsoleAssertions.php:95:    public function assertNoAccessibilityIssues(int $level = 1): Webpage
vendor/pestphp/pest-plugin-browser/src/Support/JavaScriptSerializer.php:63:            $isAssoc = array_keys($value) !== range(0, count($value) - 1);


output:
Chunk ID: b1f7e6
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 358
Output:
tests/Browser/SmokeTest.php:94:    $pages = visit(array_keys($expected), browserSmokeHost());
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/InteractsWithElements.php:47:    public function keys(string $selector, array|string $keys): self
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/InteractsWithElements.php:54:            $locator->press($key);
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/InteractsWithElements.php:191:    public function press(string $button): self
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/InteractsWithElements.php:201:        return $this->press($button)->wait($seconds);
vendor/pestphp/pest-plugin-browser/src/Playwright/Locator.php:215:    public function press(string $key, ?array $options = null): void
vendor/pestphp/pest-plugin-browser/src/Api/AwaitableWebpage.php:30:            'assertNoAccessibilityIssues',
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/MakesConsoleAssertions.php:25:        $this->assertNoConsoleLogs();
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/MakesConsoleAssertions.php:61:    public function assertNoConsoleLogs(): Webpage
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/MakesConsoleAssertions.php:95:    public function assertNoAccessibilityIssues(int $level = 1): Webpage
vendor/pestphp/pest-plugin-browser/src/Support/JavaScriptSerializer.php:63:            $isAssoc = array_keys($value) !== range(0, count($value) - 1);


output:
Chunk ID: 4adef3
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 677
Output:
@props(['product'])

@php
    $variant = $product->variants->first();
    $isSoldOut = $product->variants->isNotEmpty()
        && $product->variants->every(fn ($variant) => $variant->inventoryItem?->policy?->value === 'deny' && $variant->inventoryItem?->availableQuantity() <= 0);
    $isOnSale = $variant?->compare_at_amount && $variant->compare_at_amount > $variant->price_amount;
@endphp

class('group') }}>
    handle) }}" class="block space-y-3" wire:navigate>

                @if ($isOnSale)
                    Sale
                @endif

                @if ($isSoldOut)
                    Sold out
                @endif

                <flux:icon name="shopping-bag" class="size-12" />

{{ $product->title }}

            @if ($variant)
                <x-storefront.price :amount="$variant->price_amount" :compare-at="$variant->compare_at_amount" :currency="$variant->currency" class="text-sm" />
            @endif

                {{ $product->variants_count ?? $product->variants->count() }} {{ Str::plural('variant', $product->variants_count ?? $product->variants->count()) }}

@props(['items' => []])

class('flex flex-wrap items-center gap-2 text-sm text-zinc-500 dark:text-zinc-400') }} aria-label="Breadcrumb">
    Home

    @foreach ($items as $item)
        /

        @if (($item['url'] ?? null) && ! $loop->last)
            {{ $item['label'] }}
        @else
            {{ $item['label'] }}
        @endif
    @endforeach


output:
Chunk ID: 188c60
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 905
Output:

    <x-auth-header :title="__('Log in')" :description="__('Access your orders and saved addresses')" />

    <form wire:submit="login" class="flex flex-col gap-6">
        <flux:input
            wire:model="email"
            :label="__('Email address')"
            type="email"
            required
            autofocus
            autocomplete="email"
            placeholder="customer@acme.test"
        />

        <flux:input
            wire:model="password"
            :label="__('Password')"
            type="password"
            required
            autocomplete="current-password"
            :placeholder="__('Password')"
            viewable
        />

            <flux:link :href="route('customer.password.request')" wire:navigate>{{ __('Forgot your password?') }}</flux:link>

        <flux:checkbox wire:model="remember" :label="__('Remember me')" />

        <flux:button variant="primary" type="submit" class="w-full" data-test="customer-login-button">
            {{ __('Log in') }}
        </flux:button>
    </form>

        {{ __('New customer?') }}
        <flux:link :href="route('account.register')" wire:navigate>{{ __('Create an account') }}</flux:link>

<x-layouts::auth>

        <x-auth-header :title="__('Log in to your account')" :description="__('Enter your email and password below to log in')" />

        <x-auth-session-status class="text-center" :status="session('status')" />

        <form method="POST" action="{{ route('login.store') }}" class="flex flex-col gap-6">
            @csrf

            <flux:input
                name="email"
                :label="__('Email address')"
                :value="old('email')"
                type="email"
                required
                autofocus
                autocomplete="email"
                placeholder="email@example.com"
            />

                <flux:input
                    name="password"
                    :label="__('Password')"
                    type="password"
                    required
                    autocomplete="current-password"
                    :placeholder="__('Password')"
                    viewable
                />

                @if (Route::has('password.request'))
                    <flux:link class="absolute top-0 text-sm end-0" :href="route('password.request')" wire:navigate>
                        {{ __('Forgot your password?') }}
                    </flux:link>
                @endif

            <flux:checkbox name="remember" :label="__('Remember me')" :checked="old('remember')" />

                <flux:button variant="primary" type="submit" class="w-full" data-test="login-button">
                    {{ __('Log in') }}
                </flux:button>

        </form>

        @if (Route::has('register'))

                {{ __('Don\'t have an account?') }}
                <flux:link :href="route('register')" wire:navigate>{{ __('Sign up') }}</flux:link>

        @endif

</x-layouts::auth>


output:
Chunk ID: 7335d7
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1122
Output:

    <x-storefront.breadcrumbs :items="[['label' => 'Search results']]" />

            @if (trim($q) !== '')
                {{ $products->total() }} results for "{{ $q }}"
            @else
                Search products
            @endif

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

Filters

                <button wire:click="clearFilters" class="text-sm font-medium text-blue-700 hover:underline dark:text-blue-300">Clear all</button>

                <flux:checkbox wire:model.live="inStock" label="In stock" />

Price

                        <flux:input wire:model.live.debounce.500ms="minPrice" type="number" min="0" placeholder="Min" aria-label="Minimum price" />
                        <flux:input wire:model.live.debounce.500ms="maxPrice" type="number" min="0" placeholder="Max" aria-label="Maximum price" />

Product type

                        @foreach ($productTypes as $type)
                            <flux:checkbox wire:model.live="types" value="{{ $type }}" :label="$type" wire:key="search-type-filter-{{ $type }}" />
                        @endforeach

Vendor

                        @foreach ($productVendors as $vendor)
                            <flux:checkbox wire:model.live="vendors" value="{{ $vendor }}" :label="$vendor" wire:key="search-vendor-filter-{{ $vendor }}" />
                        @endforeach

{{ $products->total() }} products

                <flux:select wire:model.live="sort" aria-label="Sort products" class="sm:max-w-56">
                    <flux:select.option value="relevance">Relevance</flux:select.option>
                    <flux:select.option value="price_asc">Price: Low to High</flux:select.option>
                    <flux:select.option value="price_desc">Price: High to Low</flux:select.option>
                    <flux:select.option value="newest">Newest</flux:select.option>
                </flux:select>

            @if ($products->isEmpty())

                        <flux:icon name="magnifying-glass" class="size-10 text-zinc-400" />

No products found

Try another search term or adjust your filters.

                        <flux:button wire:click="clearFilters" variant="ghost">Clear filters</flux:button>

            @else

                    @foreach ($products as $product)
                        <x-storefront.product-card :product="$product" wire:key="search-product-{{ $product->getKey() }}" />
                    @endforeach

                {{ $products->links() }}
            @endif


cmd: sed -n '1,220p' resources/views/livewire/storefront/home.blade.php


cmd: sed -n '1,280p' app/Livewire/Storefront/Checkout/Show.php


cmd: sed -n '1,160p' vendor/pestphp/pest-plugin-browser/src/Api/Concerns/MakesConsoleAssertions.php


cmd: sed -n '180,215p' vendor/pestphp/pest-plugin-browser/src/Api/Concerns/InteractsWithElements.php


cmd: sed -n '1,120p' vendor/pestphp/pest-plugin-browser/src/Api/Concerns/InteractsWithToolbar.php


cmd: sed -n '1,80p' vendor/pestphp/pest-plugin-browser/src/Api/Concerns/InteractsWithTab.php


command: sed -n '1,220p' resources/views/livewire/storefront/home.blade.php

aggregated_output:
    @php($hero = data_get($themeSettings, 'home.hero', []))

{{ data_get($hero, 'eyebrow') }}

{{ data_get($hero, 'heading', $store->name) }}

{{ data_get($hero, 'subheading') }}

                    <flux:button :href="data_get($hero, 'primary_url', '/collections')" wire:navigate variant="primary">{{ data_get($hero, 'primary_label', 'Shop') }}</flux:button>
                    <flux:button :href="data_get($hero, 'secondary_url', '/collections')" wire:navigate variant="ghost">{{ data_get($hero, 'secondary_label', 'View collections') }}</flux:button>

                @foreach ($featuredProducts->take(4) as $product)

getKey() }}">
                        <x-storefront.product-card :product="$product" />

                @endforeach

Featured collections

Curated paths through the current store catalog.

            All collections

            @foreach ($featuredCollections as $collection)
                handle) }}" class="group rounded-lg border border-zinc-200 bg-white p-5 transition hover:border-zinc-300 dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700" wire:navigate wire:key="home-collection-{{ $collection->getKey() }}">

                        <flux:icon name="rectangle-stack" class="size-10" />

{{ $collection->title }}

{{ $collection->products_count }} products

            @endforeach

Featured products

Active products currently published for {{ $store->name }}.

            @foreach ($featuredProducts as $product)
                <x-storefront.product-card :product="$product" wire:key="home-product-{{ $product->getKey() }}" />
            @endforeach


command: sed -n '1,280p' app/Livewire/Storefront/Checkout/Show.php

aggregated_output:
<?php

namespace App\Livewire\Storefront\Checkout;

use App\Enums\CheckoutStatus;
use App\Exceptions\InvalidCheckoutTransitionException;
use App\Exceptions\InvalidDiscountException;
use App\Exceptions\PaymentFailedException;
use App\Exceptions\UnserviceableShippingAddressException;
use App\Models\Cart;
use App\Models\CartLine;
use App\Models\Checkout;
use App\Models\Customer;
use App\Models\ShippingRate;
use App\Models\Store;
use App\Services\CartService;
use App\Services\CheckoutService;
use App\Services\PricingEngine;
use App\Services\ShippingCalculator;
use Closure;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Locked;
use Livewire\Component;

class Show extends Component
{
    #[Locked]
    public int $storeId;

    public string $step = 'address';

    public string $email = '';

    /**
     * @var array<string, string>
     */
    public array $shippingAddress = [
        'first_name' => '',
        'last_name' => '',
        'address1' => '',
        'address2' => '',
        'city' => '',
        'province_code' => '',
        'country' => 'DE',
        'postal_code' => '',
    ];

    /**
     * @var array<string, string>
     */
    public array $billingAddress = [
        'first_name' => '',
        'last_name' => '',
        'address1' => '',
        'address2' => '',
        'city' => '',
        'province_code' => '',
        'country' => 'DE',
        'postal_code' => '',
    ];

    public bool $billingSame = true;

    public ?int $selectedShippingRateId = null;

    public string $discountCode = '';

    public string $paymentMethod = 'credit_card';

    public string $cardNumber = '';

    public string $cardName = '';

    public string $cardExpiry = '';

    public string $cardCvc = '';

    public function mount(): void
    {
        $this->storeId = $this->store()->getKey();
        $this->email = $this->customer()?->email ?? '';

        $this->fillFromCheckout($this->checkout());
    }

    public function saveAddress(): void
    {
        $this->validate([
            'email' => ['required', 'email'],
            'shippingAddress.first_name' => ['required', 'string'],
            'shippingAddress.last_name' => ['required', 'string'],
            'shippingAddress.address1' => ['required', 'string'],
            'shippingAddress.city' => ['required', 'string'],
            'shippingAddress.country' => ['required', 'string', 'size:2'],
            'shippingAddress.postal_code' => [
                'required',
                'string',
                function (string $attribute, mixed $value, Closure $fail): void {
                    if (strtoupper($this->shippingAddress['country']) === 'DE' && preg_match('/^\d{5}$/', (string) $value) !== 1) {
                        $fail('The postal code format is invalid.');
                    }
                },
            ],
            'billingSame' => ['boolean'],
        ]);

        $checkout = $this->checkout();

        if (! $checkout instanceof Checkout) {
            return;
        }

        try {
            $checkout = app(CheckoutService::class)->setAddress($checkout, [
                'email' => $this->email,
                'shipping_address' => $this->shippingAddress,
                'billing_address' => $this->billingSame ? $this->shippingAddress : $this->billingAddress,
            ]);

            $this->selectedShippingRateId = null;

            if (! $this->requiresShipping()) {
                app(CheckoutService::class)->setShippingMethod($checkout, null);
                $this->step = 'payment';

                return;
            }

            $this->step = 'shipping';
        } catch (InvalidCheckoutTransitionException $exception) {
            throw ValidationException::withMessages([
                'email' => $exception->getMessage(),
            ]);
        }
    }

    public function selectShippingMethod(): void
    {
        $checkout = $this->checkout();

        if (! $checkout instanceof Checkout) {
            return;
        }

        if ($this->requiresShipping()) {
            $this->validate([
                'selectedShippingRateId' => ['required', 'integer'],
            ]);
        }

        try {
            app(CheckoutService::class)->setShippingMethod($checkout, $this->selectedShippingRateId);
            $this->step = 'payment';
        } catch (InvalidCheckoutTransitionException|UnserviceableShippingAddressException $exception) {
            throw ValidationException::withMessages([
                'selectedShippingRateId' => $exception->getMessage(),
            ]);
        }
    }

    public function applyDiscount(): void
    {
        $checkout = $this->checkout();

        if (! $checkout instanceof Checkout) {
            return;
        }

        $checkout->forceFill([
            'discount_code' => trim($this->discountCode) !== '' ? trim($this->discountCode) : null,
        ])->save();

        try {
            app(PricingEngine::class)->calculate($checkout);
            $this->resetErrorBag('discountCode');
        } catch (InvalidDiscountException $exception) {
            $checkout->forceFill(['discount_code' => null])->save();
            $this->discountCode = '';
            app(PricingEngine::class)->calculate($checkout);

            throw ValidationException::withMessages([
                'discountCode' => $exception->getMessage(),
            ]);
        }
    }

    public function selectPaymentMethod(): void
    {
        $this->validate([
            'paymentMethod' => ['required', 'in:credit_card,paypal,bank_transfer'],
        ]);

        $checkout = $this->checkout();

        if (! $checkout instanceof Checkout) {
            return;
        }

        try {
            app(CheckoutService::class)->selectPaymentMethod($checkout, $this->paymentMethod);
            $this->step = 'reserved';
        } catch (InvalidCheckoutTransitionException $exception) {
            throw ValidationException::withMessages([
                'paymentMethod' => $exception->getMessage(),
            ]);
        }
    }

    public function placeOrder(): void
    {
        $rules = [
            'paymentMethod' => ['required', 'in:credit_card,paypal,bank_transfer'],
        ];

        if ($this->paymentMethod === 'credit_card') {
            $rules = [
                ...$rules,
                'cardNumber' => ['required', 'string'],
                'cardName' => ['nullable', 'string'],
                'cardExpiry' => ['nullable', 'string'],
                'cardCvc' => ['nullable', 'string'],
            ];
        }

        $this->validate($rules);

        $checkout = $this->checkout();

        if (! $checkout instanceof Checkout) {
            return;
        }

        try {
            if ($checkout->status !== CheckoutStatus::PaymentSelected) {
                $checkout = app(CheckoutService::class)->selectPaymentMethod($checkout, $this->paymentMethod);
            }

            $order = app(CheckoutService::class)->completeCheckout($checkout, [
                'card_number' => $this->cardNumber,
                'cardholder_name' => $this->cardName,
                'expiry' => $this->cardExpiry,
                'cvc' => $this->cardCvc,
            ]);

            session([
                'last_order_id' => $order->getKey(),
            ]);
            session()->forget(['cart_id', 'cart_discount_code']);

            $this->redirectRoute('checkout.confirmation', ['order' => $order], navigate: true);
        } catch (InvalidCheckoutTransitionException|PaymentFailedException $exception) {
            $this->step = 'payment';

            throw ValidationException::withMessages([
                $this->paymentMethod === 'credit_card' ? 'cardNumber' : 'paymentMethod' => $exception->getMessage(),
            ]);
        }
    }

    public function store(): Store
    {
        if (isset($this->storeId)) {
            $store = Store::query()->findOrFail($this->storeId);
            app()->instance('current_store', $store);

            return $store;
        }

        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        return $store;
    }

    public function cart(): ?Cart
    {
        $cart = app(CartService::class)->currentForSession($this->store(), $this->customer());


output:
Chunk ID: d3ed72
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1030
Output:

    @php($hero = data_get($themeSettings, 'home.hero', []))

{{ data_get($hero, 'eyebrow') }}

{{ data_get($hero, 'heading', $store->name) }}

{{ data_get($hero, 'subheading') }}

                    <flux:button :href="data_get($hero, 'primary_url', '/collections')" wire:navigate variant="primary">{{ data_get($hero, 'primary_label', 'Shop') }}</flux:button>
                    <flux:button :href="data_get($hero, 'secondary_url', '/collections')" wire:navigate variant="ghost">{{ data_get($hero, 'secondary_label', 'View collections') }}</flux:button>

                @foreach ($featuredProducts->take(4) as $product)

getKey() }}">
                        <x-storefront.product-card :product="$product" />

                @endforeach

Featured collections

Curated paths through the current store catalog.

            All collections

            @foreach ($featuredCollections as $collection)
                handle) }}" class="group rounded-lg border border-zinc-200 bg-white p-5 transition hover:border-zinc-300 dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700" wire:navigate wire:key="home-collection-{{ $collection->getKey() }}">

                        <flux:icon name="rectangle-stack" class="size-10" />

{{ $collection->title }}

{{ $collection->products_count }} products

            @endforeach

Featured products

Active products currently published for {{ $store->name }}.

            @foreach ($featuredProducts as $product)
                <x-storefront.product-card :product="$product" wire:key="home-product-{{ $product->getKey() }}" />
            @endforeach


output:
Chunk ID: a2a3cb
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2101
Output:
<?php

namespace App\Livewire\Storefront\Checkout;

use App\Enums\CheckoutStatus;
use App\Exceptions\InvalidCheckoutTransitionException;
use App\Exceptions\InvalidDiscountException;
use App\Exceptions\PaymentFailedException;
use App\Exceptions\UnserviceableShippingAddressException;
use App\Models\Cart;
use App\Models\CartLine;
use App\Models\Checkout;
use App\Models\Customer;
use App\Models\ShippingRate;
use App\Models\Store;
use App\Services\CartService;
use App\Services\CheckoutService;
use App\Services\PricingEngine;
use App\Services\ShippingCalculator;
use Closure;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Livewire\Attributes\Locked;
use Livewire\Component;

class Show extends Component
{
    #[Locked]
    public int $storeId;

    public string $step = 'address';

    public string $email = '';

    /**
     * @var array<string, string>
     */
    public array $shippingAddress = [
        'first_name' => '',
        'last_name' => '',
        'address1' => '',
        'address2' => '',
        'city' => '',
        'province_code' => '',
        'country' => 'DE',
        'postal_code' => '',
    ];

    /**
     * @var array<string, string>
     */
    public array $billingAddress = [
        'first_name' => '',
        'last_name' => '',
        'address1' => '',
        'address2' => '',
        'city' => '',
        'province_code' => '',
        'country' => 'DE',
        'postal_code' => '',
    ];

    public bool $billingSame = true;

    public ?int $selectedShippingRateId = null;

    public string $discountCode = '';

    public string $paymentMethod = 'credit_card';

    public string $cardNumber = '';

    public string $cardName = '';

    public string $cardExpiry = '';

    public string $cardCvc = '';

    public function mount(): void
    {
        $this->storeId = $this->store()->getKey();
        $this->email = $this->customer()?->email ?? '';

        $this->fillFromCheckout($this->checkout());
    }

    public function saveAddress(): void
    {
        $this->validate([
            'email' => ['required', 'email'],
            'shippingAddress.first_name' => ['required', 'string'],
            'shippingAddress.last_name' => ['required', 'string'],
            'shippingAddress.address1' => ['required', 'string'],
            'shippingAddress.city' => ['required', 'string'],
            'shippingAddress.country' => ['required', 'string', 'size:2'],
            'shippingAddress.postal_code' => [
                'required',
                'string',
                function (string $attribute, mixed $value, Closure $fail): void {
                    if (strtoupper($this->shippingAddress['country']) === 'DE' && preg_match('/^\d{5}$/', (string) $value) !== 1) {
                        $fail('The postal code format is invalid.');
                    }
                },
            ],
            'billingSame' => ['boolean'],
        ]);

        $checkout = $this->checkout();

        if (! $checkout instanceof Checkout) {
            return;
        }

        try {
            $checkout = app(CheckoutService::class)->setAddress($checkout, [
                'email' => $this->email,
                'shipping_address' => $this->shippingAddress,
                'billing_address' => $this->billingSame ? $this->shippingAddress : $this->billingAddress,
            ]);

            $this->selectedShippingRateId = null;

            if (! $this->requiresShipping()) {
                app(CheckoutService::class)->setShippingMethod($checkout, null);
                $this->step = 'payment';

                return;
            }

            $this->step = 'shipping';
        } catch (InvalidCheckoutTransitionException $exception) {
            throw ValidationException::withMessages([
                'email' => $exception->getMessage(),
            ]);
        }
    }

    public function selectShippingMethod(): void
    {
        $checkout = $this->checkout();

        if (! $checkout instanceof Checkout) {
            return;
        }

        if ($this->requiresShipping()) {
            $this->validate([
                'selectedShippingRateId' => ['required', 'integer'],
            ]);
        }

        try {
            app(CheckoutService::class)->setShippingMethod($checkout, $this->selectedShippingRateId);
            $this->step = 'payment';
        } catch (InvalidCheckoutTransitionException|UnserviceableShippingAddressException $exception) {
            throw ValidationException::withMessages([
                'selectedShippingRateId' => $exception->getMessage(),
            ]);
        }
    }

    public function applyDiscount(): void
    {
        $checkout = $this->checkout();

        if (! $checkout instanceof Checkout) {
            return;
        }

        $checkout->forceFill([
            'discount_code' => trim($this->discountCode) !== '' ? trim($this->discountCode) : null,
        ])->save();

        try {
            app(PricingEngine::class)->calculate($checkout);
            $this->resetErrorBag('discountCode');
        } catch (InvalidDiscountException $exception) {
            $checkout->forceFill(['discount_code' => null])->save();
            $this->discountCode = '';
            app(PricingEngine::class)->calculate($checkout);

            throw ValidationException::withMessages([
                'discountCode' => $exception->getMessage(),
            ]);
        }
    }

    public function selectPaymentMethod(): void
    {
        $this->validate([
            'paymentMethod' => ['required', 'in:credit_card,paypal,bank_transfer'],
        ]);

        $checkout = $this->checkout();

        if (! $checkout instanceof Checkout) {
            return;
        }

        try {
            app(CheckoutService::class)->selectPaymentMethod($checkout, $this->paymentMethod);
            $this->step = 'reserved';
        } catch (InvalidCheckoutTransitionException $exception) {
            throw ValidationException::withMessages([
                'paymentMethod' => $exception->getMessage(),
            ]);
        }
    }

    public function placeOrder(): void
    {
        $rules = [
            'paymentMethod' => ['required', 'in:credit_card,paypal,bank_transfer'],
        ];

        if ($this->paymentMethod === 'credit_card') {
            $rules = [
                ...$rules,
                'cardNumber' => ['required', 'string'],
                'cardName' => ['nullable', 'string'],
                'cardExpiry' => ['nullable', 'string'],
                'cardCvc' => ['nullable', 'string'],
            ];
        }

        $this->validate($rules);

        $checkout = $this->checkout();

        if (! $checkout instanceof Checkout) {
            return;
        }

        try {
            if ($checkout->status !== CheckoutStatus::PaymentSelected) {
                $checkout = app(CheckoutService::class)->selectPaymentMethod($checkout, $this->paymentMethod);
            }

            $order = app(CheckoutService::class)->completeCheckout($checkout, [
                'card_number' => $this->cardNumber,
                'cardholder_name' => $this->cardName,
                'expiry' => $this->cardExpiry,
                'cvc' => $this->cardCvc,
            ]);

            session([
                'last_order_id' => $order->getKey(),
            ]);
            session()->forget(['cart_id', 'cart_discount_code']);

            $this->redirectRoute('checkout.confirmation', ['order' => $order], navigate: true);
        } catch (InvalidCheckoutTransitionException|PaymentFailedException $exception) {
            $this->step = 'payment';

            throw ValidationException::withMessages([
                $this->paymentMethod === 'credit_card' ? 'cardNumber' : 'paymentMethod' => $exception->getMessage(),
            ]);
        }
    }

    public function store(): Store
    {
        if (isset($this->storeId)) {
            $store = Store::query()->findOrFail($this->storeId);
            app()->instance('current_store', $store);

            return $store;
        }

        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        return $store;
    }

    public function cart(): ?Cart
    {
        $cart = app(CartService::class)->currentForSession($this->store(), $this->customer());


command: sed -n '1,120p' vendor/pestphp/pest-plugin-browser/src/Api/Concerns/InteractsWithToolbar.php

aggregated_output:
<?php

declare(strict_types=1);

namespace Pest\Browser\Api\Concerns;

use Pest\Browser\Api\Webpage;
use Pest\Browser\Support\ComputeUrl;

/**
 * @mixin Webpage
 */
trait InteractsWithToolbar
{
    /**
     * Reloads the current page.
     */
    public function refresh(): self
    {
        $this->page->reload();

        return $this;
    }

    /**
     * Navigates to the given URL.
     *
     * @param  array<string, mixed>  $options
     */
    public function navigate(string $url, array $options = []): self
    {
        $url = ComputeUrl::from($url);

        $this->page->goto($url, $options);

        return $this;
    }

    /**
     * Navigates to the next page in the history.
     */
    public function forward(): self
    {
        $this->page->forward();

        return $this;
    }

    /**
     * Navigates to the previous page in the history.
     */
    public function back(): self
    {
        $this->page->back();

        return $this;
    }
}


command: sed -n '180,215p' vendor/pestphp/pest-plugin-browser/src/Api/Concerns/InteractsWithElements.php

aggregated_output:
     */
    public function attach(string $field, string $path): self
    {
        $this->guessLocator($field)->setInputFiles($path);

        return $this;
    }

    /**
     * Press the button with the given text or name.
     */
    public function press(string $button): self
    {
        return $this->click($button);
    }

    /**
     * Press the button with the given text or name.
     */
    public function pressAndWaitFor(string $button, int|float $seconds = 1): self
    {
        return $this->press($button)->wait($seconds);
    }

    /**
     * Drag an element to another element using selectors.
     */
    public function drag(string $from, string $to): self
    {
        $fromLocator = $this->guessLocator($from);
        $toLocator = $this->guessLocator($to);

        $fromLocator->dragTo($toLocator);

        return $this;
    }