output:
Chunk ID: 2466c0
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1275
Output:
---
name: pest-testing
description: "Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code."
license: MIT
metadata:
  author: laravel
---

# Pest Testing 4

## Documentation

Use `search-docs` for detailed Pest 4 patterns and documentation.

## Basic Usage

### Creating Tests

All tests must be written using Pest. Use `php artisan make:test --pest {name}`.

### Test Organization

- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
- Browser tests: `tests/Browser/` directory.
- Do NOT remove tests without approval - these are core application code.

### Basic Test Structure

Pest supports both `test()` and `it()` functions. Before writing new tests, check existing test files in the same directory to match the project's convention. Use `test()` if existing tests use `test()`, or `it()` if they use `it()`.

<!-- Basic Pest Test Example -->
```php
it('is true', function () {
    expect(true)->toBeTrue();
});
```

### Running Tests

- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`.
- Run all tests: `php artisan test --compact`.
- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`.

## Assertions

Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:

<!-- Pest Response Assertion -->
```php
it('returns all', function () {
    $this->postJson('/api/docs', [])->assertSuccessful();
});
```

| Use | Instead of |
|-----|------------|
| `assertSuccessful()` | `assertStatus(200)` |
| `assertNotFound()` | `assertStatus(404)` |
| `assertForbidden()` | `assertStatus(403)` |

## Mocking

Import mock function before use: `use function Pest\Laravel\mock;`

## Datasets

Use datasets for repetitive tests (validation rules, etc.):

<!-- Pest Dataset Example -->
```php
it('has emails', function (string $email) {
    expect($email)->not->toBeEmpty();
})->with([
    'james' => 'james@laravel.com',
    'taylor' => 'taylor@laravel.com',
]);
```

## Pest 4 Features

| Feature | Purpose |
|---------|---------|
| Browser Testing | Full integration tests in real browsers |
| Smoke Testing | Validate multiple pages quickly |
| Visual Regression | Compare screenshots for visual changes |
| Test Sharding | Parallel CI runs |
| Architecture Testing | Enforce code conventions |

### Browser Test Example

Browser tests run in real browsers for full integration testing:

- Browser tests live in `tests/Browser/`.
- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
- Use `RefreshDatabase` for clean state per test.
- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
- Switch color schemes (light/dark mode) when appropriate.
- Take screenshots or pause tests for debugging.

<!-- Pest Browser Test Example -->
```php
it('may reset the password', function () {
    Notification::fake();

    $this->actingAs(User::factory()->create());

    $page = visit('/sign-in');

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

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

### Smoke Testing

Quickly validate multiple pages have no JavaScript errors:

<!-- Pest Smoke Testing Example -->
```php
$pages = visit(['/', '/about', '/contact']);

$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
```

### Visual Regression Testing

Capture and compare screenshots to detect visual changes.

### Test Sharding

Split tests across parallel processes for faster CI runs.

### Architecture Testing

Pest 4 includes architecture testing (from Pest 3):

<!-- Architecture Test Example -->
```php
arch('controllers')
    ->expect('App\Http\Controllers')
    ->toExtendNothing()
    ->toHaveSuffix('Controller');
```

## Common Pitfalls

- Not importing `use function Pest\Laravel\mock;` before using mock
- Using `assertStatus(200)` instead of `assertSuccessful()`
- Forgetting datasets for repetitive validation tests
- Deleting tests without approval
- Forgetting `assertNoJavaScriptErrors()` in browser tests


message: I’ve reloaded the relevant local skills after the context handoff. Before touching Suite 16 code, I’m pulling the project/version context and the current docs snippets the project rules require.

phase: commentary


type: message

role: assistant

text: I’ve reloaded the relevant local skills after the context handoff. Before touching Suite 16 code, I’m pulling the project/version context and the current docs snippets the project rules require.


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

queries: browser testing actingAs visit assert javascript errors

queries: eloquent relationships eager loading pagination

queries: blade attributes data attributes livewire flux button


server: laravel-boost

tool: application-info

type: text

database_engine: sqlite

roster_name: FORTIFY

package_name: laravel/fortify

roster_name: LARAVEL

package_name: laravel/framework

roster_name: PROMPTS

package_name: laravel/prompts

package_name: livewire/flux

roster_name: LIVEWIRE

package_name: livewire/livewire

roster_name: BOOST

package_name: laravel/boost

roster_name: MCP

package_name: laravel/mcp

roster_name: PAIL

package_name: laravel/pail

roster_name: PINT

package_name: laravel/pint

roster_name: SAIL

package_name: laravel/sail

roster_name: PEST

package_name: pestphp/pest

roster_name: PHPUNIT

package_name: phpunit/phpunit

roster_name: TAILWINDCSS

package_name: tailwindcss


tool: search-docs

text:
# Search Results

## Forms Extracting input fields to Blade components
Forms | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/forms

## Extracting input fields to Blade components

Even in a small component such as the `post.create` example we've been discussing, we end up duplicating lots of form field boilerplate like validation messages and labels.

It can be helpful to extract repetitive UI elements such as these into dedicated [Blade components](https://laravel.com/docs/blade#components) to be shared across your application.

For example, below is the original Blade template from the `post.create` component. We will be extracting the following two text inputs into dedicated Blade components:

```blade
<form wire:submit="save">
    <input type="text" wire:model="title"> <!-- [tl! highlight:3] -->
    <div>
        @error('title') <span class="error">{{ $message }}</span> @enderror
    </div>

    <input type="text" wire:model="content"> <!-- [tl! highlight:3] -->
    <div>
        @error('content') <span class="error">{{ $message }}</span> @enderror
    </div>

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

Here's what the template will look like after extracting a re-usable Blade component called `<x-input-text>`:

```blade
<form wire:submit="save">
    <x-input-text name="title" wire:model="title" /> <!-- [tl! highlight] -->

    <x-input-text name="content" wire:model="content" /> <!-- [tl! highlight] -->

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

Next, here's the source for the `x-input-text` component:

```blade
<!-- resources/views/components/input-text.blade.php -->

@props(['name'])

<input type="text" name="{{ $name }}" {{ $attributes }}>

<div>
    @error($name) <span class="error">{{ $message }}</span> @enderror
</div>
```

As you can see, we took the repetitive HTML and placed it inside a dedicated Blade component.

For the most part, the Blade component contains only the extracted HTML from the original component. However, we have added two things:

* The `@props` directive
* The `{{ $attributes }}` statement on the input

Let's discuss each of these additions:

By specifying `name` as a "prop" using `@props(['name'])` we are telling Blade: if an attribute called "name" is set on this component, take its value and make it available inside this component as `$name`.

For other attributes that don't have an explicit purpose, we used the `{{ $attributes }}` statement. This is used for "attribute forwarding", or in other words, taking any HTML attributes written on the Blade component and forwarding them onto an element within the component.

This ensures `wire:model="title"` and any other extra attributes such as `disabled`, `class="..."`, or `required` still get forwarded to the actual `<input>` element.

### Custom form controls

In the previous example, we "wrapped" an input element into a re-usable Blade component we can use as if it was a native HTML input element.

This pattern is very useful; however, there might be some cases where you want to create an entire input component from scratch (without an underlying native input element), but still be able to bind its value to Livewire properties using `wire:model`.

For example, let's imagine you wanted to create an `<x-input-counter />` component that was a simple "counter" input written in Alpine.

Before we create a Blade component, let's first look at a simple, pure-Alpine, "counter" component for reference:

```blade
<div x-data="{ count: 0 }">
    <button x-on:click="count--">-</button>

    <span x-text="count"></span>

    <button x-on:click="count++">+</button>
</div>
```

As you can see, the component above shows a number alongside two buttons to increment and decrement that number.

Now, let's imagine we want to extract this component into a Blade component called `<x-input-counter />` that we would use within a component like so:

```blade
<x-input-counter wire:model="quantity" />
```

Creating this component is mostly simple. We take the HTML of the counter and place it inside a Blade component template like `resources/views/components/input-counter.blade.php`.

However, making it work with `wire:model="quantity"` so that you can easily bind data from your Livewire component to the "count" inside this Alpine component needs one extra step.

Here's the source for the component:

```blade
<!-- resources/view/components/input-counter.blade.php -->

<div x-data="{ count: 0 }" x-modelable="count" {{ $attributes}}>
    <button x-on:click="count--">-</button>

    <span x-text="count"></span>

    <button x-on:click="count++">+</button>
</div>
```

As you can see, the only different bit about this HTML is the `x-modelable="count"` and `{{ $attributes }}`.

`x-modelable` is a utility in Alpine that tells Alpine to make a certain piece of data available for binding from outside. [The Alpine documentation has more information on this directive.](https://alpinejs.dev/directives/modelable)

`{{ $attributes }}`, as we explored earlier, forwards any attributes passed into the Blade component from outside. In this case, the `wire:model` directive will be forwarded.

Because of `{{ $attributes }}`, when the HTML is rendered in the browser, `wire:model="quantity"` will be rendered alongside `x-modelable="count"` on the root `<div>` of the Alpine component like so:

```blade
<div x-data="{ count: 0 }" x-modelable="count" wire:model="quantity">
```

`x-modelable="count"` tells Alpine to look for any `x-model` or `wire:model` statements and use "count" as the data to bind them to.

Because `x-modelable` works for both `wire:model` and `x-model`, you can also use this Blade component interchangeably with Livewire and Alpine. Here’s an example of using this Blade component in a purely Alpine context:

```blade
<x-input-counter x-model="quantity" />
```

Creating custom input elements in your application is extremely powerful but requires a deeper understanding of the utilities Livewire and Alpine provide and how they interact with each other.

---

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

---

## Urls URLs for Named Routes Eloquent Models
Urls | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/urls.md

#### Eloquent Models

You will often be generating URLs using the route key (typically the primary key) of [Eloquent models](/docs/{{version}}/eloquent). For this reason, you may pass Eloquent models as parameter values. The `route` helper will automatically extract the model's route key:

```php
echo route('post.show', ['post' => $post]);
```

---

## Wire Bind Common use cases
Wire Bind | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/wire-bind

## Common use cases

### Binding styles

```blade
<div wire:bind:style="{ 'color': textColor, 'font-size': fontSize + 'px' }">
    Styled text
</div>
```

### Binding href

```blade
<a wire:bind:href="url">Dynamic link</a>
```

### Binding disabled state

```blade
<button wire:bind:disabled="isArchived">Delete</button>
```

### Binding data attributes

```blade
<div wire:bind:data-count="count">...</div>
```

---

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

---

## Upgrade Upgrading To 12.0 From 11.x Eloquent Models and UUIDv7
Upgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md

#### Models and UUIDv7

**Likelihood Of Impact: Medium**

The `HasUuids` trait now returns UUIDs that are compatible with version 7 of the UUID spec (ordered UUIDs). If you would like to continue using ordered UUIDv4 strings for your model's IDs, you should now use the `HasVersion4Uuids` trait:

```php
use Illuminate\Database\Eloquent\Concerns\HasUuids; // [tl! remove]
use Illuminate\Database\Eloquent\Concerns\HasVersion4Uuids as HasUuids; // [tl! add]
```

The `HasVersion7Uuids` trait has been removed. If you were previously using this trait, you should use the `HasUuids` trait instead, which now provides the same behavior.

---

## Blade Components Component Attributes
Blade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/blade.md

### Component Attributes

We've already examined how to pass data attributes to a component; however, sometimes you may need to specify additional HTML attributes, such as `class`, that are not part of the data required for a component to function. Typically, you want to pass these additional attributes down to the root element of the component template. For example, imagine we want to render an `alert` component like so:

```blade
<x-alert type="error" :message="$message" class="mt-4"/>
```

All of the attributes that are not part of the component's constructor will automatically be added to the component's "attribute bag". This attribute bag is automatically made available to the component via the `$attributes` variable. All of the attributes may be rendered within the component by echoing this variable:

```blade
<div {{ $attributes }}>
    <!-- Component content -->
</div>
```

> [!WARNING]
> Using directives such as `@env` within component tags is not supported at this time. For example, `<x-alert :live="@env('production')"/>` will not be compiled.

---

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

### assertScript

The `assertScript` method asserts that the given JavaScript expression evaluates to the given value:

```php
$page->assertScript('document.title', 'Home Page');
$page->assertScript('document.querySelector(".btn").disabled', true);
```

---

## Eloquent Relationships Eager Loading Lazy Eager Loading
Eloquent Relationships | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent-relationships.md

### Lazy Eager Loading

Sometimes you may need to eager load a relationship after the parent model has already been retrieved. For example, this may be useful if you need to dynamically decide whether to load related models:

```php
use App\Models\Book;

$books = Book::all();

if ($condition) {
    $books->load('author', 'publisher');
}
```

If you need to set additional query constraints on the eager loading query, you may pass an array keyed by the relationships you wish to load. The array values should be closure instances which receive the query instance:

```php
$author->load(['books' => function ($query) {
    $query->orderBy('published_date', 'asc');
}]);
```

To load a relationship only when it has not already been loaded, use the `loadMissing` method:

```php
$book->loadMissing('author');
```

---

## Loading States Styling with Tailwind
Loading States | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/loading-states

## Styling with Tailwind

Tailwind v4 and above provides powerful selectors for working with the `data-loading` attribute.

### Basic styling

Use Tailwind's `data-loading:` variant to apply styles when an element is loading:

```blade
<button wire:click="save" class="data-loading:opacity-50">
    Save
</button>
```

### Showing elements during loading

To show an element only while loading is active, use the `not-data-loading:hidden` variant:

```blade
<button wire:click="save">
    Save
</button>

<span class="not-data-loading:hidden">
    Saving...
</span>
```

This approach is preferred over `hidden data-loading:block` because it works regardless of the element's display type (flex, inline, grid, etc.).

### Styling children

You can style child elements when a parent has the `data-loading` attribute using the `in-data-loading:` variant:

```blade
<button wire:click="save">
    <span class="in-data-loading:hidden">Save</span>
    <span class="not-in-data-loading:hidden">Saving...</span>
</button>
```

> [!warning] The in-data-loading variant applies to all ancestors
> The `in-data-loading:` variant will trigger if **any** ancestor element (no matter how far up the tree) has the `data-loading` attribute. This can lead to unexpected behavior if you have nested loading states.

### Styling parents

Style parent elements when they contain a child with `data-loading` using the `has-data-loading:` variant:

```blade
<div class="has-data-loading:opacity-50">
    <button wire:click="save">Save</button>
</div>
```

When the button is clicked, the entire parent div will become semi-transparent.

### Styling siblings

You can style sibling elements using Tailwind's `peer` utility with the `peer-data-loading:` variant:

```blade
<div>
    <button wire:click="save" class="peer">
        Save
    </button>

    <span class="peer-data-loading:opacity-50">
        Saving...
    </span>
</div>
```

### Complex selectors

For more advanced styling needs, you can use arbitrary variants to target specific elements:

```blade
<!-- Style all direct children when loading -->
<div class="[&[data-loading]>*]:opacity-50" wire:click="save">
    <span>Child 1</span>
    <span>Child 2</span>
</div>

<!-- Style specific descendant elements -->
<button class="[&[data-loading]_.icon]:animate-spin" wire:click="save">

    Save
</button>
```

Learn more about Tailwind's state variants and arbitrary selectors in the [Tailwind CSS documentation](https://tailwindcss.com/docs/hover-focus-and-other-states).

---

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

---

## Scout Searching Where Clauses Customizing the Eloquent Results Query
Scout | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/scout.md

#### Customizing the Eloquent Results Query

After Scout retrieves a list of matching Eloquent models from your application's search engine, Eloquent is used to retrieve all of the matching models by their primary keys. You may customize this query by invoking the `query` method. The `query` method accepts a closure that will receive the Eloquent query builder instance as an argument:

```php
use App\Models\Order;
use Illuminate\Database\Eloquent\Builder;

$orders = Order::search('Star Trek')
    ->query(fn (Builder $query) => $query->with('invoices'))
    ->get();
```

When using a third-party engine, this callback is invoked after the relevant models have already been retrieved from the search engine, so it should not be used for "filtering" results — use [Scout where clauses](#where-clauses) instead. However, when using the database engine, the `query` method's constraints are applied directly to the database query, so you may use it for filtering as well.

---

## Pagination Using custom pagination views
Pagination | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/pagination

## Using custom pagination views

If you wish to bypass Livewire's pagination views entirely, you can render your own in one of two ways:

1. The `->links()` method in your Blade view
2. The `paginationView()` or `paginationSimpleView()` method in your component

### Via `->links()`

The first approach is to simply pass your custom pagination Blade view name to the `->links()` method directly:

```blade
{{ $posts->links('custom-pagination-links') }}
```

When rendering the pagination links, Livewire will now look for a view at `resources/views/custom-pagination-links.blade.php`.

### Via `paginationView()` or `paginationSimpleView()`

The second approach is to declare a `paginationView` or `paginationSimpleView` method inside your component which returns the name of the view you would like to use:

```php
public function paginationView()
{
    return 'custom-pagination-links-view';
}

public function paginationSimpleView()
{
    return 'custom-simple-pagination-links-view';
}
```

### Sample pagination view

Below is an unstyled sample of a simple Livewire pagination view for your reference.

As you can see, you can use Livewire's page navigation helpers like `$this->nextPage()` directly inside your template by adding `wire:click="nextPage"` to buttons:

```blade
<div>
    @if ($paginator->hasPages())
        <nav role="navigation" aria-label="Pagination Navigation">
            <span>
                @if ($paginator->onFirstPage())
                    <span>Previous</span>
                @else
                    <button wire:click="previousPage" wire:loading.attr="disabled" rel="prev">Previous</button>
                @endif
            </span>

            <span>
                @if ($paginator->onLastPage())
                    <span>Next</span>
                @else
                    <button wire:click="nextPage" wire:loading.attr="disabled" rel="next">Next</button>
                @endif
            </span>
        </nav>
    @endif
</div>
```

For visual-only loading states (like opacity changes), you can use Livewire's automatic `data-loading` attribute with Tailwind classes instead:

```blade
<button wire:click="nextPage" class="data-loading:opacity-50" rel="next">
    Next
</button>
```

[Learn more about loading states →](/docs/4.x/loading-states)

---

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

---

## Responses Redirects Redirecting to Named Routes Populating Parameters via Eloquent Models
Responses | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/responses.md

#### Populating Parameters via Eloquent Models

If you are redirecting to a route with an "ID" parameter that is being populated from an Eloquent model, you may pass the model itself. The ID will be extracted automatically:

```php
// For a route with the following URI: /profile/{id}

return redirect()->route('profile', [$user]);
```

If you would like to customize the value that is placed in the route parameter, you can specify the column in the route parameter definition (`/profile/{id:slug}`) or you can override the `getRouteKey` method on your Eloquent model:

```php
/**
 * Get the value of the model's route key.
 */
public function getRouteKey(): mixed
{
    return $this->slug;
}
```

---

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

### assertNoJavaScriptErrors

The `assertNoJavaScriptErrors` method asserts there are no JavaScript errors on the page:

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

---

## Blade Components Component Attributes Non-Class Attribute Merging
Blade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/blade.md

#### Non-Class Attribute Merging

When merging attributes that are not `class` attributes, the values provided to the `merge` method will be considered the "default" values of the attribute. However, unlike the `class` attribute, these attributes will not be merged with injected attribute values. Instead, they will be overwritten. For example, a `button` component's implementation may look like the following:

```blade
<button {{ $attributes->merge(['type' => 'button']) }}>
    {{ $slot }}
</button>
```

To render the button component with a custom `type`, it may be specified when consuming the component. If no type is specified, the `button` type will be used:

```blade
<x-button type="submit">
    Submit
</x-button>
```

The rendered HTML of the `button` component in this example would be:

```blade
<button type="submit">
    Submit
</button>
```

If you would like an attribute other than `class` to have its default value and injected values joined together, you may use the `prepends` method. In this example, the `data-controller` attribute will always begin with `profile-controller` and any additional injected `data-controller` values will be placed after this default value:

```blade
<div {{ $attributes->merge(['data-controller' => $attributes->prepends('profile-controller')]) }}>
    {{ $slot }}
</div>
```

---

## Blade Components Slots Slot Attributes
Blade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/blade.md

#### Slot Attributes

Like Blade components, you may assign additional [attributes](#component-attributes) to slots such as CSS class names:

```xml
<x-card class="shadow-sm">
    <x-slot:heading class="font-bold">
        Heading
    </x-slot>

    Content

    <x-slot:footer class="text-sm">
        Footer
    </x-slot>
</x-card>
```

To interact with slot attributes, you may access the `attributes` property of the slot's variable. For more information on how to interact with attributes, please consult the documentation on [component attributes](#component-attributes):

```blade
@props([
    'heading',
    'footer',
])

<div {{ $attributes->class(['border']) }}>
    <h1 {{ $heading->attributes->class(['text-lg']) }}>
        {{ $heading }}
    </h1>

    {{ $slot }}

    <footer {{ $footer->attributes->class(['text-gray-700']) }}>
        {{ $footer }}
    </footer>
</div>
```

_[Truncated]_


output:
Wall time: 3.3193 seconds
Output:
[{"type":"text","text":"# Search Results\n\n## Forms Extracting input fields to Blade components\nForms | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/forms\n\n## Extracting input fields to Blade components\n\nEven in a small component such as the `post.create` example we've been discussing, we end up duplicating lots of form field boilerplate like validation messages and labels.\n\nIt can be helpful to extract repetitive UI elements such as these into dedicated [Blade components](https://laravel.com/docs/blade#components) to be shared across your application.\n\nFor example, below is the original Blade template from the `post.create` component. We will be extracting the following two text inputs into dedicated Blade components:\n\n```blade\n<form wire:submit=\"save\">\n    <input type=\"text\" wire:model=\"title\"> <!-- [tl! highlight:3] -->\n    <div>\n        @error('title') <span class=\"error\">{{ $message }}</span> @enderror\n    </div>\n\n    <input type=\"text\" wire:model=\"content\"> <!-- [tl! highlight:3] -->\n    <div>\n        @error('content') <span class=\"error\">{{ $message }}</span> @enderror\n    </div>\n\n    <button type=\"submit\">Save</button>\n</form>\n```\n\nHere's what the template will look like after extracting a re-usable Blade component called `<x-input-text>`:\n\n```blade\n<form wire:submit=\"save\">\n    <x-input-text name=\"title\" wire:model=\"title\" /> <!-- [tl! highlight] -->\n\n    <x-input-text name=\"content\" wire:model=\"content\" /> <!-- [tl! highlight] -->\n\n    <button type=\"submit\">Save</button>\n</form>\n```\n\nNext, here's the source for the `x-input-text` component:\n\n```blade\n<!-- resources/views/components/input-text.blade.php -->\n\n@props(['name'])\n\n<input type=\"text\" name=\"{{ $name }}\" {{ $attributes }}>\n\n<div>\n    @error($name) <span class=\"error\">{{ $message }}</span> @enderror\n</div>\n```\n\nAs you can see, we took the repetitive HTML and placed it inside a dedicated Blade component.\n\nFor the most part, the Blade component contains only the extracted HTML from the original component. However, we have added two things:\n\n* The `@props` directive\n* The `{{ $attributes }}` statement on the input\n\nLet's discuss each of these additions:\n\nBy specifying `name` as a \"prop\" using `@props(['name'])` we are telling Blade: if an attribute called \"name\" is set on this component, take its value and make it available inside this component as `$name`.\n\nFor other attributes that don't have an explicit purpose, we used the `{{ $attributes }}` statement. This is used for \"attribute forwarding\", or in other words, taking any HTML attributes written on the Blade component and forwarding them onto an element within the component.\n\nThis ensures `wire:model=\"title\"` and any other extra attributes such as `disabled`, `class=\"...\"`, or `required` still get forwarded to the actual `<input>` element.\n\n### Custom form controls\n\nIn the previous example, we \"wrapped\" an input element into a re-usable Blade component we can use as if it was a native HTML input element.\n\nThis pattern is very useful; however, there might be some cases where you want to create an entire input component from scratch (without an underlying native input element), but still be able to bind its value to Livewire properties using `wire:model`.\n\nFor example, let's imagine you wanted to create an `<x-input-counter />` component that was a simple \"counter\" input written in Alpine.\n\nBefore we create a Blade component, let's first look at a simple, pure-Alpine, \"counter\" component for reference:\n\n```blade\n<div x-data=\"{ count: 0 }\">\n    <button x-on:click=\"count--\">-</button>\n\n    <span x-text=\"count\"></span>\n\n    <button x-on:click=\"count++\">+</button>\n</div>\n```\n\nAs you can see, the component above shows a number alongside two buttons to increment and decrement that number.\n\nNow, let's imagine we want to extract this component into a Blade component called `<x-input-counter />` that we would use within a component like so:\n\n```blade\n<x-input-counter wire:model=\"quantity\" />\n```\n\nCreating this component is mostly simple. We take the HTML of the counter and place it inside a Blade component template like `resources/views/components/input-counter.blade.php`.\n\nHowever, making it work with `wire:model=\"quantity\"` so that you can easily bind data from your Livewire component to the \"count\" inside this Alpine component needs one extra step.\n\nHere's the source for the component:\n\n```blade\n<!-- resources/view/components/input-counter.blade.php -->\n\n<div x-data=\"{ count: 0 }\" x-modelable=\"count\" {{ $attributes}}>\n    <button x-on:click=\"count--\">-</button>\n\n    <span x-text=\"count\"></span>\n\n    <button x-on:click=\"count++\">+</button>\n</div>\n```\n\nAs you can see, the only different bit about this HTML is the `x-modelable=\"count\"` and `{{ $attributes }}`.\n\n`x-modelable` is a utility in Alpine that tells Alpine to make a certain piece of data available for binding from outside. [The Alpine documentation has more information on this directive.](https://alpinejs.dev/directives/modelable)\n\n`{{ $attributes }}`, as we explored earlier, forwards any attributes passed into the Blade component from outside. In this case, the `wire:model` directive will be forwarded.\n\nBecause of `{{ $attributes }}`, when the HTML is rendered in the browser, `wire:model=\"quantity\"` will be rendered alongside `x-modelable=\"count\"` on the root `<div>` of the Alpine component like so:\n\n```blade\n<div x-data=\"{ count: 0 }\" x-modelable=\"count\" wire:model=\"quantity\">\n```\n\n`x-modelable=\"count\"` tells Alpine to look for any `x-model` or `wire:model` statements and use \"count\" as the data to bind them to.\n\nBecause `x-modelable` works for both `wire:model` and `x-model`, you can also use this Blade component interchangeably with Livewire and Alpine. Here’s an example of using this Blade component in a purely Alpine context:\n\n```blade\n<x-input-counter x-model=\"quantity\" />\n```\n\nCreating custom input elements in your application is extremely powerful but requires a deeper understanding of the utilities Livewire and Alpine provide and how they interact with each other.\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## Urls URLs for Named Routes Eloquent Models\nUrls | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/urls.md\n\n\n#### Eloquent Models\n\nYou will often be generating URLs using the route key (typically the primary key) of [Eloquent models](/docs/{{version}}/eloquent). For this reason, you may pass Eloquent models as parameter values. The `route` helper will automatically extract the model's route key:\n\n```php\necho route('post.show', ['post' => $post]);\n```\n\n---\n\n## Wire Bind Common use cases\nWire Bind | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/wire-bind\n\n## Common use cases\n\n### Binding styles\n\n```blade\n<div wire:bind:style=\"{ 'color': textColor, 'font-size': fontSize + 'px' }\">\n    Styled text\n</div>\n```\n\n### Binding href\n\n```blade\n<a wire:bind:href=\"url\">Dynamic link</a>\n```\n\n### Binding disabled state\n\n```blade\n<button wire:bind:disabled=\"isArchived\">Delete</button>\n```\n\n### Binding data attributes\n\n```blade\n<div wire:bind:data-count=\"count\">...</div>\n```\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## Upgrade Upgrading To 12.0 From 11.x Eloquent Models and UUIDv7\nUpgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md\n\n\n#### Models and UUIDv7\n\n**Likelihood Of Impact: Medium**\n\nThe `HasUuids` trait now returns UUIDs that are compatible with version 7 of the UUID spec (ordered UUIDs). If you would like to continue using ordered UUIDv4 strings for your model's IDs, you should now use the `HasVersion4Uuids` trait:\n\n```php\nuse Illuminate\\Database\\Eloquent\\Concerns\\HasUuids; // [tl! remove]\nuse Illuminate\\Database\\Eloquent\\Concerns\\HasVersion4Uuids as HasUuids; // [tl! add]\n```\n\nThe `HasVersion7Uuids` trait has been removed. If you were previously using this trait, you should use the `HasUuids` trait instead, which now provides the same behavior.\n\n---\n\n## Blade Components Component Attributes\nBlade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/blade.md\n\n\n### Component Attributes\n\nWe've already examined how to pass data attributes to a component; however, sometimes you may need to specify additional HTML attributes, such as `class`, that are not part of the data required for a component to function. Typically, you want to pass these additional attributes down to the root element of the component template. For example, imagine we want to render an `alert` component like so:\n\n```blade\n<x-alert type=\"error\" :message=\"$message\" class=\"mt-4\"/>\n```\n\nAll of the attributes that are not part of the component's constructor will automatically be added to the component's \"attribute bag\". This attribute bag is automatically made available to the component via the `$attributes` variable. All of the attributes may be rendered within the component by echoing this variable:\n\n```blade\n<div {{ $attributes }}>\n    <!-- Component content -->\n</div>\n```\n\n> [!WARNING]\n> Using directives such as `@env` within component tags is not supported at this time. For example, `<x-alert :live=\"@env('production')\"/>` will not be compiled.\n\n---\n\n## Browser Testing assertScript\nBrowser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md\n\n\n### assertScript\n\nThe `assertScript` method asserts that the given JavaScript expression evaluates to the given value:\n\n```php\n$page->assertScript('document.title', 'Home Page');\n$page->assertScript('document.querySelector(\".btn\").disabled', true);\n```\n\n---\n\n## Eloquent Relationships Eager Loading Lazy Eager Loading\nEloquent Relationships | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent-relationships.md\n\n\n### Lazy Eager Loading\n\nSometimes you may need to eager load a relationship after the parent model has already been retrieved. For example, this may be useful if you need to dynamically decide whether to load related models:\n\n```php\nuse App\\Models\\Book;\n\n$books = Book::all();\n\nif ($condition) {\n    $books->load('author', 'publisher');\n}\n```\n\nIf you need to set additional query constraints on the eager loading query, you may pass an array keyed by the relationships you wish to load. The array values should be closure instances which receive the query instance:\n\n```php\n$author->load(['books' => function ($query) {\n    $query->orderBy('published_date', 'asc');\n}]);\n```\n\nTo load a relationship only when it has not already been loaded, use the `loadMissing` method:\n\n```php\n$book->loadMissing('author');\n```\n\n---\n\n## Loading States Styling with Tailwind\nLoading States | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/loading-states\n\n## Styling with Tailwind\n\nTailwind v4 and above provides powerful selectors for working with the `data-loading` attribute.\n\n### Basic styling\n\nUse Tailwind's `data-loading:` variant to apply styles when an element is loading:\n\n```blade\n<button wire:click=\"save\" class=\"data-loading:opacity-50\">\n    Save\n</button>\n```\n\n### Showing elements during loading\n\nTo show an element only while loading is active, use the `not-data-loading:hidden` variant:\n\n```blade\n<button wire:click=\"save\">\n    Save\n</button>\n\n<span class=\"not-data-loading:hidden\">\n    Saving...\n</span>\n```\n\nThis approach is preferred over `hidden data-loading:block` because it works regardless of the element's display type (flex, inline, grid, etc.).\n\n### Styling children\n\nYou can style child elements when a parent has the `data-loading` attribute using the `in-data-loading:` variant:\n\n```blade\n<button wire:click=\"save\">\n    <span class=\"in-data-loading:hidden\">Save</span>\n    <span class=\"not-in-data-loading:hidden\">Saving...</span>\n</button>\n```\n\n> [!warning] The in-data-loading variant applies to all ancestors\n> The `in-data-loading:` variant will trigger if **any** ancestor element (no matter how far up the tree) has the `data-loading` attribute. This can lead to unexpected behavior if you have nested loading states.\n\n### Styling parents\n\nStyle parent elements when they contain a child with `data-loading` using the `has-data-loading:` variant:\n\n```blade\n<div class=\"has-data-loading:opacity-50\">\n    <button wire:click=\"save\">Save</button>\n</div>\n```\n\nWhen the button is clicked, the entire parent div will become semi-transparent.\n\n### Styling siblings\n\nYou can style sibling elements using Tailwind's `peer` utility with the `peer-data-loading:` variant:\n\n```blade\n<div>\n    <button wire:click=\"save\" class=\"peer\">\n        Save\n    </button>\n\n    <span class=\"peer-data-loading:opacity-50\">\n        Saving...\n    </span>\n</div>\n```\n\n### Complex selectors\n\nFor more advanced styling needs, you can use arbitrary variants to target specific elements:\n\n```blade\n<!-- Style all direct children when loading -->\n<div class=\"[&[data-loading]>*]:opacity-50\" wire:click=\"save\">\n    <span>Child 1</span>\n    <span>Child 2</span>\n</div>\n\n<!-- Style specific descendant elements -->\n<button class=\"[&[data-loading]_.icon]:animate-spin\" wire:click=\"save\">\n    \n    Save\n</button>\n```\n\nLearn more about Tailwind's state variants and arbitrary selectors in the [Tailwind CSS documentation](https://tailwindcss.com/docs/hover-focus-and-other-states).\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## Scout Searching Where Clauses Customizing the Eloquent Results Query\nScout | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/scout.md\n\n\n#### Customizing the Eloquent Results Query\n\nAfter Scout retrieves a list of matching Eloquent models from your application's search engine, Eloquent is used to retrieve all of the matching models by their primary keys. You may customize this query by invoking the `query` method. The `query` method accepts a closure that will receive the Eloquent query builder instance as an argument:\n\n```php\nuse App\\Models\\Order;\nuse Illuminate\\Database\\Eloquent\\Builder;\n\n$orders = Order::search('Star Trek')\n    ->query(fn (Builder $query) => $query->with('invoices'))\n    ->get();\n```\n\nWhen using a third-party engine, this callback is invoked after the relevant models have already been retrieved from the search engine, so it should not be used for \"filtering\" results — use [Scout where clauses](#where-clauses) instead. However, when using the database engine, the `query` method's constraints are applied directly to the database query, so you may use it for filtering as well.\n\n---\n\n## Pagination Using custom pagination views\nPagination | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/pagination\n\n## Using custom pagination views\n\nIf you wish to bypass Livewire's pagination views entirely, you can render your own in one of two ways:\n\n1. The `->links()` method in your Blade view\n2. The `paginationView()` or `paginationSimpleView()` method in your component\n\n### Via `->links()`\n\nThe first approach is to simply pass your custom pagination Blade view name to the `->links()` method directly:\n\n```blade\n{{ $posts->links('custom-pagination-links') }}\n```\n\nWhen rendering the pagination links, Livewire will now look for a view at `resources/views/custom-pagination-links.blade.php`.\n\n### Via `paginationView()` or `paginationSimpleView()`\n\nThe second approach is to declare a `paginationView` or `paginationSimpleView` method inside your component which returns the name of the view you would like to use:\n\n```php\npublic function paginationView()\n{\n    return 'custom-pagination-links-view';\n}\n\npublic function paginationSimpleView()\n{\n    return 'custom-simple-pagination-links-view';\n}\n```\n\n### Sample pagination view\n\nBelow is an unstyled sample of a simple Livewire pagination view for your reference.\n\nAs you can see, you can use Livewire's page navigation helpers like `$this->nextPage()` directly inside your template by adding `wire:click=\"nextPage\"` to buttons:\n\n```blade\n<div>\n    @if ($paginator->hasPages())\n        <nav role=\"navigation\" aria-label=\"Pagination Navigation\">\n            <span>\n                @if ($paginator->onFirstPage())\n                    <span>Previous</span>\n                @else\n                    <button wire:click=\"previousPage\" wire:loading.attr=\"disabled\" rel=\"prev\">Previous</button>\n                @endif\n            </span>\n\n            <span>\n                @if ($paginator->onLastPage())\n                    <span>Next</span>\n                @else\n                    <button wire:click=\"nextPage\" wire:loading.attr=\"disabled\" rel=\"next\">Next</button>\n                @endif\n            </span>\n        </nav>\n    @endif\n</div>\n```\n\nFor visual-only loading states (like opacity changes), you can use Livewire's automatic `data-loading` attribute with Tailwind classes instead:\n\n```blade\n<button wire:click=\"nextPage\" class=\"data-loading:opacity-50\" rel=\"next\">\n    Next\n</button>\n```\n\n[Learn more about loading states →](/docs/4.x/loading-states)\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## Responses Redirects Redirecting to Named Routes Populating Parameters via Eloquent Models\nResponses | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/responses.md\n\n\n#### Populating Parameters via Eloquent Models\n\nIf you are redirecting to a route with an \"ID\" parameter that is being populated from an Eloquent model, you may pass the model itself. The ID will be extracted automatically:\n\n```php\n// For a route with the following URI: /profile/{id}\n\nreturn redirect()->route('profile', [$user]);\n```\n\nIf you would like to customize the value that is placed in the route parameter, you can specify the column in the route parameter definition (`/profile/{id:slug}`) or you can override the `getRouteKey` method on your Eloquent model:\n\n```php\n/**\n * Get the value of the model's route key.\n */\npublic function getRouteKey(): mixed\n{\n    return $this->slug;\n}\n```\n\n---\n\n## Browser Testing assertNoJavaScriptErrors\nBrowser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md\n\n\n### assertNoJavaScriptErrors\n\nThe `assertNoJavaScriptErrors` method asserts there are no JavaScript errors on the page:\n\n```php\n$page->assertNoJavaScriptErrors();\n```\n\n---\n\n## Blade Components Component Attributes Non-Class Attribute Merging\nBlade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/blade.md\n\n\n#### Non-Class Attribute Merging\n\nWhen merging attributes that are not `class` attributes, the values provided to the `merge` method will be considered the \"default\" values of the attribute. However, unlike the `class` attribute, these attributes will not be merged with injected attribute values. Instead, they will be overwritten. For example, a `button` component's implementation may look like the following:\n\n```blade\n<button {{ $attributes->merge(['type' => 'button']) }}>\n    {{ $slot }}\n</button>\n```\n\nTo render the button component with a custom `type`, it may be specified when consuming the component. If no type is specified, the `button` type will be used:\n\n```blade\n<x-button type=\"submit\">\n    Submit\n</x-button>\n```\n\nThe rendered HTML of the `button` component in this example would be:\n\n```blade\n<button type=\"submit\">\n    Submit\n</button>\n```\n\nIf you would like an attribute other than `class` to have its default value and injected values joined together, you may use the `prepends` method. In this example, the `data-controller` attribute will always begin with `profile-controller` and any additional injected `data-controller` values will be placed after this default value:\n\n```blade\n<div {{ $attributes->merge(['data-controller' => $attributes->prepends('profile-controller')]) }}>\n    {{ $slot }}\n</div>\n```\n\n---\n\n## Blade Components Slots Slot Attributes\nBlade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/blade.md\n\n\n#### Slot Attributes\n\nLike Blade components, you may assign additional [attributes](#component-attributes) to slots such as CSS class names:\n\n```xml\n<x-card class=\"shadow-sm\">\n    <x-slot:heading class=\"font-bold\">\n        Heading\n    </x-slot>\n\n    Content\n\n    <x-slot:footer class=\"text-sm\">\n        Footer\n    </x-slot>\n</x-card>\n```\n\nTo interact with slot attributes, you may access the `attributes` property of the slot's variable. For more information on how to interact with attributes, please consult the documentation on [component attributes](#component-attributes):\n\n```blade\n@props([\n    'heading',\n    'footer',\n])\n\n<div {{ $attributes->class(['border']) }}>\n    <h1 {{ $heading->attributes->class(['text-lg']) }}>\n        {{ $heading }}\n    </h1>\n\n    {{ $slot }}\n\n    <footer {{ $footer->attributes->class(['text-gray-700']) }}>\n        {{ $footer }}\n    </footer>\n</div>\n```\n\n_[Truncated]_"}]


message: Docs confirm the browser assertions and Livewire pagination patterns I need. I’m checking the existing customer admin components and nearby browser suites now so Suite 16 follows the same shape instead of inventing a new test style.


text: Docs confirm the browser assertions and Livewire pagination patterns I need. I’m checking the existing customer admin components and nearby browser suites now so Suite 16 follows the same shape instead of inventing a new test style.


cmd: sed -n '1,240p' app/Livewire/Admin/Customers/Index.php


cmd: sed -n '1,260p' app/Livewire/Admin/Customers/Show.php


cmd: sed -n '1,260p' resources/views/livewire/admin/customers/index.blade.php


cmd: sed -n '1,320p' resources/views/livewire/admin/customers/show.blade.php


cmd: sed -n '1,260p' tests/Feature/Admin/CustomerManagementTest.php


cmd: sed -n '1,260p' tests/Browser/Admin/CollectionManagementTest.php


command: sed -n '1,240p' app/Livewire/Admin/Customers/Index.php

Index.php

type: read

aggregated_output:
<?php

namespace App\Livewire\Admin\Customers;

use App\Models\Customer;
use App\Models\Store;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Attributes\Locked;
use Livewire\Component;
use Livewire\WithPagination;

class Index extends Component
{
    use WithPagination;

    #[Locked]
    public int $storeId;

    public string $storeCurrency = 'EUR';

    public string $search = '';

    public function mount(): void
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        $this->storeId = $store->getKey();
        $this->storeCurrency = $store->default_currency;
    }

    public function updatedSearch(): void
    {
        $this->resetPage();
    }

    public function customers(): LengthAwarePaginator
    {
        return Customer::withoutGlobalScopes()
            ->withCount('orders')
            ->withSum('orders as total_spent', 'total_amount')
            ->where('store_id', $this->storeId)
            ->when($this->search !== '', function (Builder $query): void {
                $search = '%'.$this->search.'%';

                $query->where(function (Builder $query) use ($search): void {
                    $query
                        ->where('name', 'like', $search)
                        ->orWhere('email', 'like', $search);
                });
            })
            ->latest('created_at')
            ->paginate(20);
    }

    public function render(): mixed
    {
        return view('livewire.admin.customers.index', [
            'customers' => $this->customers(),
        ])->layout('layouts.app', [
            'title' => __('Customers'),
        ]);
    }
}

status: completed


command: sed -n '1,260p' app/Livewire/Admin/Customers/Show.php

aggregated_output:
<?php

namespace App\Livewire\Admin\Customers;

use App\Models\Customer;
use App\Models\CustomerAddress;
use App\Models\Order;
use App\Models\Store;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Locked;
use Livewire\Component;
use Livewire\WithPagination;

class Show extends Component
{
    use WithPagination;

    #[Locked]
    public int $storeId;

    #[Locked]
    public int $customerId;

    public ?int $editingAddressId = null;

    public string $addressLabel = '';

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

    public function mount(Customer $customer): void
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        $customer = Customer::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->whereKey($customer->getKey())
            ->first();

        abort_unless($customer instanceof Customer, 404);

        $this->storeId = $store->getKey();
        $this->customerId = $customer->getKey();
    }

    public function openAddressForm(?int $addressId = null): void
    {
        $this->editingAddressId = $addressId;

        if ($addressId) {
            $address = $this->address($addressId);
            $this->addressLabel = (string) $address->label;
            $this->addressJson = array_merge($this->emptyAddressJson(), $address->address_json ?? []);
        } else {
            $this->resetAddressForm();
        }

        $this->modal('address-form')->show();
    }

    public function saveAddress(): void
    {
        $this->validate([
            'addressLabel' => ['nullable', 'string', 'max:255'],
            'addressJson.first_name' => ['nullable', 'string', 'max:255'],
            'addressJson.last_name' => ['nullable', 'string', 'max:255'],
            'addressJson.address1' => ['required', 'string', 'max:255'],
            'addressJson.address2' => ['nullable', 'string', 'max:255'],
            'addressJson.city' => ['required', 'string', 'max:255'],
            'addressJson.province_code' => ['nullable', 'string', 'max:255'],
            'addressJson.country' => ['required', 'string', 'size:2'],
            'addressJson.postal_code' => ['required', 'string', 'max:32'],
        ]);

        $address = $this->editingAddressId
            ? $this->address($this->editingAddressId)
            : new CustomerAddress(['customer_id' => $this->customerId]);

        $address->fill([
            'label' => $this->addressLabel !== '' ? $this->addressLabel : null,
            'address_json' => $this->addressJson,
            'is_default' => $address->exists ? $address->is_default : ! $this->customer()->addresses()->exists(),
        ]);
        $address->save();

        $this->resetAddressForm();
        $this->modal('address-form')->close();
        $this->dispatch('toast', type: 'success', message: __('Address saved'));
    }

    public function deleteAddress(int $addressId): void
    {
        $address = $this->address($addressId);
        $wasDefault = $address->is_default;
        $address->delete();

        if ($wasDefault) {
            $this->setFirstAddressAsDefault();
        }

        $this->dispatch('toast', type: 'success', message: __('Address deleted'));
    }

    public function setDefaultAddress(int $addressId): void
    {
        $address = $this->address($addressId);

        DB::transaction(function () use ($address): void {
            CustomerAddress::query()
                ->where('customer_id', $this->customerId)
                ->update(['is_default' => false]);

            $address->forceFill(['is_default' => true])->save();
        });

        $this->dispatch('toast', type: 'success', message: __('Default address updated'));
    }

    public function orders(): LengthAwarePaginator
    {
        return Order::withoutGlobalScopes()
            ->where('store_id', $this->storeId)
            ->where('customer_id', $this->customerId)
            ->latest('placed_at')
            ->latest('id')
            ->paginate(10);
    }

    public function render(): mixed
    {
        $customer = $this->customer();

        return view('livewire.admin.customers.show', [
            'customer' => $customer,
            'orders' => $this->orders(),
        ])->layout('layouts.app', [
            'title' => $customer->name ?: $customer->email,
        ]);
    }

    private function customer(): Customer
    {
        return Customer::withoutGlobalScopes()
            ->with(['addresses' => fn ($query) => $query->orderByDesc('is_default')->orderBy('id')])
            ->where('store_id', $this->storeId)
            ->whereKey($this->customerId)
            ->firstOrFail();
    }

    private function address(int $addressId): CustomerAddress
    {
        return CustomerAddress::query()
            ->where('customer_id', $this->customerId)
            ->whereKey($addressId)
            ->firstOrFail();
    }

    private function setFirstAddressAsDefault(): void
    {
        $nextAddress = CustomerAddress::query()
            ->where('customer_id', $this->customerId)
            ->oldest('id')
            ->first();

        if ($nextAddress instanceof CustomerAddress) {
            $nextAddress->forceFill(['is_default' => true])->save();
        }
    }

    private function resetAddressForm(): void
    {
        $this->editingAddressId = null;
        $this->addressLabel = '';
        $this->addressJson = $this->emptyAddressJson();
    }

    /**
     * @return array{first_name: string, last_name: string, address1: string, address2: string, city: string, province_code: string, country: string, postal_code: string}
     */
    private function emptyAddressJson(): array
    {
        return [
            'first_name' => '',
            'last_name' => '',
            'address1' => '',
            'address2' => '',
            'city' => '',
            'province_code' => '',
            'country' => 'DE',
            'postal_code' => '',
        ];
    }
}


command: sed -n '1,260p' resources/views/livewire/admin/customers/index.blade.php

aggregated_output:
        <flux:heading size="xl">Customers</flux:heading>
        <flux:text class="mt-1">Search customer accounts and review order value.</flux:text>

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

                        Name
                        Email
                        Orders
                        Total spent
                        Created

                    @forelse ($customers as $customer)

getKey() }}">

                                    {{ $customer->name ?: 'Unnamed customer' }}

                            {{ $customer->email }}
                            {{ $customer->orders_count }}

                                <x-storefront.price :amount="(int) ($customer->total_spent ?? 0)" :currency="$storeCurrency" class="justify-end" />

                            {{ $customer->created_at?->format('M j, Y') }}

                    @empty

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

                                    <flux:heading size="lg">No customers found</flux:heading>
                                    <flux:text>Customer accounts will appear here after registration or checkout.</flux:text>

                    @endforelse

    {{ $customers->links() }}


command: sed -n '1,320p' resources/views/livewire/admin/customers/show.blade.php

aggregated_output:
            <flux:heading size="xl">{{ $customer->name ?: 'Unnamed customer' }}</flux:heading>
            <flux:text class="mt-1">{{ $customer->email }}</flux:text>

        <flux:button :href="route('admin.customers.index')" wire:navigate variant="filled" icon="arrow-left">
            Customers
        </flux:button>

                <flux:heading size="lg">Customer info</flux:heading>

Name

{{ $customer->name ?: 'Unnamed customer' }}

Email

{{ $customer->email }}

Created

{{ $customer->created_at?->format('M j, Y') }}

Marketing

                            <flux:badge :color="$customer->marketing_opt_in ? 'green' : 'zinc'">
                                {{ $customer->marketing_opt_in ? 'Opted in' : 'Not opted in' }}
                            </flux:badge>

                    <flux:heading size="lg">Order history</flux:heading>

                                Order
                                Date
                                Status
                                Total

                            @forelse ($orders as $order)
                                @php
                                    $statusColor = match ($order->financial_status->value) {
                                        'paid' => 'green',
                                        'partially_refunded' => 'amber',
                                        'refunded', 'voided' => 'red',
                                        default => 'zinc',
                                    };
                                @endphp

getKey() }}">

                                            {{ $order->order_number }}

                                    {{ $order->placed_at?->format('M j, Y') }}

                                        <flux:badge :color="$statusColor">{{ Str::headline($order->financial_status->value) }}</flux:badge>

                                        <x-storefront.price :amount="$order->total_amount" :currency="$order->currency" class="justify-end" />

                            @empty

                                    No orders for this custom…610 chars truncated…

                @forelse ($customer->addresses as $address)
                    @php($addressData = $address->address_json ?? [])

getKey() }}" class="rounded-lg border border-zinc-200 p-4 dark:border-zinc-700">

{{ $address->label ?: 'Address' }}

                                    @if ($address->is_default)
                                        <flux:badge color="green">Default</flux:badge>
                                    @endif

{{ data_get($addressData, 'address1') }}

                                    @if (data_get($addressData, 'address2'))

{{ data_get($addressData, 'address2') }}

                                    @endif

{{ trim(data_get($addressData, 'postal_code').' '.data_get($addressData, 'city')) }}

{{ data_get($addressData, 'country') }}

                            <flux:button wire:click="openAddressForm({{ $address->getKey() }})" size="sm" variant="filled">Edit</flux:button>
                            @unless ($address->is_default)
                                <flux:button wire:click="setDefaultAddress({{ $address->getKey() }})" size="sm" variant="ghost">Set default</flux:button>
                            @endunless
                            <flux:button wire:click="deleteAddress({{ $address->getKey() }})" wire:confirm="Delete this address?" size="sm" variant="danger">Delete</flux:button>

                @empty

                        <flux:icon name="map-pin" class="mx-auto size-8 text-zinc-400" />
                        <flux:text class="mt-3">No addresses saved.</flux:text>

                @endforelse

    <flux:modal name="address-form" class="md:w-[32rem]">
        <form wire:submit="saveAddress" class="space-y-6">

                <flux:heading size="lg">{{ $editingAddressId ? 'Edit address' : 'Add address' }}</flux:heading>

            <flux:input wire:model="addressLabel" label="Label" placeholder="Home, Office..." />

                <flux:input wire:model="addressJson.first_name" label="First name" />
                <flux:input wire:model="addressJson.last_name" label="Last name" />

                    <flux:input wire:model="addressJson.address1" label="Address line 1" />
                    <flux:error name="addressJson.address1" />

                    <flux:input wire:model="addressJson.address2" label="Address line 2" />

                <flux:input wire:model="addressJson.city" label="City" />
                <flux:input wire:model="addressJson.province_code" label="State / Province" />
                <flux:input wire:model="addressJson.postal_code" label="Postal code" />
                <flux:select wire:model="addressJson.country" label="Country">
                    <flux:select.option value="DE">Germany</flux:select.option>
                    <flux:select.option value="US">United States</flux:select.option>
                    <flux:select.option value="GB">United Kingdom</flux:select.option>
                    <flux:select.option value="FR">France</flux:select.option>
                    <flux:select.option value="NL">Netherlands</flux:select.option>
                </flux:select>

            <flux:error name="addressJson.city" />
            <flux:error name="addressJson.postal_code" />
            <flux:error name="addressJson.country" />

                <flux:modal.close>
                    <flux:button variant="filled">Cancel</flux:button>
                </flux:modal.close>
                <flux:button type="submit" variant="primary">Save</flux:button>

        </form>
    </flux:modal>


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

namespace App\Livewire\Admin\Customers;

use App\Models\Customer;
use App\Models\Store;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Attributes\Locked;
use Livewire\Component;
use Livewire\WithPagination;

class Index extends Component
{
    use WithPagination;

    #[Locked]
    public int $storeId;

    public string $storeCurrency = 'EUR';

    public string $search = '';

    public function mount(): void
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        $this->storeId = $store->getKey();
        $this->storeCurrency = $store->default_currency;
    }

    public function updatedSearch(): void
    {
        $this->resetPage();
    }

    public function customers(): LengthAwarePaginator
    {
        return Customer::withoutGlobalScopes()
            ->withCount('orders')
            ->withSum('orders as total_spent', 'total_amount')
            ->where('store_id', $this->storeId)
            ->when($this->search !== '', function (Builder $query): void {
                $search = '%'.$this->search.'%';

                $query->where(function (Builder $query) use ($search): void {
                    $query
                        ->where('name', 'like', $search)
                        ->orWhere('email', 'like', $search);
                });
            })
            ->latest('created_at')
            ->paginate(20);
    }

    public function render(): mixed
    {
        return view('livewire.admin.customers.index', [
            'customers' => $this->customers(),
        ])->layout('layouts.app', [
            'title' => __('Customers'),
        ]);
    }
}


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

namespace App\Livewire\Admin\Customers;

use App\Models\Customer;
use App\Models\CustomerAddress;
use App\Models\Order;
use App\Models\Store;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Locked;
use Livewire\Component;
use Livewire\WithPagination;

class Show extends Component
{
    use WithPagination;

    #[Locked]
    public int $storeId;

    #[Locked]
    public int $customerId;

    public ?int $editingAddressId = null;

    public string $addressLabel = '';

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

    public function mount(Customer $customer): void
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        $customer = Customer::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->whereKey($customer->getKey())
            ->first();

        abort_unless($customer instanceof Customer, 404);

        $this->storeId = $store->getKey();
        $this->customerId = $customer->getKey();
    }

    public function openAddressForm(?int $addressId = null): void
    {
        $this->editingAddressId = $addressId;

        if ($addressId) {
            $address = $this->address($addressId);
            $this->addressLabel = (string) $address->label;
            $this->addressJson = array_merge($this->emptyAddressJson(), $address->address_json ?? []);
        } else {
            $this->resetAddressForm();
        }

        $this->modal('address-form')->show();
    }

    public function saveAddress(): void
    {
        $this->validate([
            'addressLabel' => ['nullable', 'string', 'max:255'],
            'addressJson.first_name' => ['nullable', 'string', 'max:255'],
            'addressJson.last_name' => ['nullable', 'string', 'max:255'],
            'addressJson.address1' => ['required', 'string', 'max:255'],
            'addressJson.address2' => ['nullable', 'string', 'max:255'],
            'addressJson.city' => ['required', 'string', 'max:255'],
            'addressJson.province_code' => ['nullable', 'string', 'max:255'],
            'addressJson.country' => ['required', 'string', 'size:2'],
            'addressJson.postal_code' => ['required', 'string', 'max:32'],
        ]);

        $address = $this->editingAddressId
            ? $this->address($this->editingAddressId)
            : new CustomerAddress(['customer_id' => $this->customerId]);

        $address->fill([
            'label' => $this->addressLabel !== '' ? $this->addressLabel : null,
            'address_json' => $this->addressJson,
            'is_default' => $address->exists ? $address->is_default : ! $this->customer()->addresses()->exists(),
        ]);
        $address->save();

        $this->resetAddressForm();
        $this->modal('address-form')->close();
        $this->dispatch('toast', type: 'success', message: __('Address saved'));
    }

    public function deleteAddress(int $addressId): void
    {
        $address = $this->address($addressId);
        $wasDefault = $address->is_default;
        $address->delete();

        if ($wasDefault) {
            $this->setFirstAddressAsDefault();
        }

        $this->dispatch('toast', type: 'success', message: __('Address deleted'));
    }

    public function setDefaultAddress(int $addressId): void
    {
        $address = $this->address($addressId);

        DB::transaction(function () use ($address): void {
            CustomerAddress::query()
                ->where('customer_id', $this->customerId)
                ->update(['is_default' => false]);

            $address->forceFill(['is_default' => true])->save();
        });

        $this->dispatch('toast', type: 'success', message: __('Default address updated'));
    }

    public function orders(): LengthAwarePaginator
    {
        return Order::withoutGlobalScopes()
            ->where('store_id', $this->storeId)
            ->where('customer_id', $this->customerId)
            ->latest('placed_at')
            ->latest('id')
            ->paginate(10);
    }

    public function render(): mixed
    {
        $customer = $this->customer();

        return view('livewire.admin.customers.show', [
            'customer' => $customer,
            'orders' => $this->orders(),
        ])->layout('layouts.app', [
            'title' => $customer->name ?: $customer->email,
        ]);
    }

    private function customer(): Customer
    {
        return Customer::withoutGlobalScopes()
            ->with(['addresses' => fn ($query) => $query->orderByDesc('is_default')->orderBy('id')])
            ->where('store_id', $this->storeId)
            ->whereKey($this->customerId)
            ->firstOrFail();
    }

    private function address(int $addressId): CustomerAddress
    {
        return CustomerAddress::query()
            ->where('customer_id', $this->customerId)
            ->whereKey($addressId)
            ->firstOrFail();
    }

    private function setFirstAddressAsDefault(): void
    {
        $nextAddress = CustomerAddress::query()
            ->where('customer_id', $this->customerId)
            ->oldest('id')
            ->first();

        if ($nextAddress instanceof CustomerAddress) {
            $nextAddress->forceFill(['is_default' => true])->save();
        }
    }

    private function resetAddressForm(): void
    {
        $this->editingAddressId = null;
        $this->addressLabel = '';
        $this->addressJson = $this->emptyAddressJson();
    }

    /**
     * @return array{first_name: string, last_name: string, address1: string, address2: string, city: string, province_code: string, country: string, postal_code: string}
     */
    private function emptyAddressJson(): array
    {
        return [
            'first_name' => '',
            'last_name' => '',
            'address1' => '',
            'address2' => '',
            'city' => '',
            'province_code' => '',
            'country' => 'DE',
            'postal_code' => '',
        ];
    }
}


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

        <flux:heading size="xl">Customers</flux:heading>
        <flux:text class="mt-1">Search customer accounts and review order value.</flux:text>

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

                        Name
                        Email
                        Orders
                        Total spent
                        Created

                    @forelse ($customers as $customer)

getKey() }}">

                                    {{ $customer->name ?: 'Unnamed customer' }}

                            {{ $customer->email }}
                            {{ $customer->orders_count }}

                                <x-storefront.price :amount="(int) ($customer->total_spent ?? 0)" :currency="$storeCurrency" class="justify-end" />

                            {{ $customer->created_at?->format('M j, Y') }}

                    @empty

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

                                    <flux:heading size="lg">No customers found</flux:heading>
                                    <flux:text>Customer accounts will appear here after registration or checkout.</flux:text>

                    @endforelse

    {{ $customers->links() }}


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

            <flux:heading size="xl">{{ $customer->name ?: 'Unnamed customer' }}</flux:heading>
            <flux:text class="mt-1">{{ $customer->email }}</flux:text>

        <flux:button :href="route('admin.customers.index')" wire:navigate variant="filled" icon="arrow-left">
            Customers
        </flux:button>

                <flux:heading size="lg">Customer info</flux:heading>

Name

{{ $customer->name ?: 'Unnamed customer' }}

Email

{{ $customer->email }}

Created

{{ $customer->created_at?->format('M j, Y') }}

Marketing

                            <flux:badge :color="$customer->marketing_opt_in ? 'green' : 'zinc'">
                                {{ $customer->marketing_opt_in ? 'Opted in' : 'Not opted in' }}
                            </flux:badge>

                    <flux:heading size="lg">Order history</flux:heading>

                                Order
                                Date
                                Status
                                Total

                            @forelse ($orders as $order)
                                @php
                                    $statusColor = match ($order->financial_status->value) {
                                        'paid' => 'green',
                                        'partially_refunded' => 'amber',
                                        'refunded', 'voided' => 'red',
                                        default => 'zinc',
                                    };
                                @endphp

getKey() }}">

                                            {{ $order->order_number }}

                                    {{ $order->placed_at?->format('M j, Y') }}

                                        <flux:badge :color="$statusColor">{{ Str::headline($order->financial_status->value) }}</flux:badge>

                                        <x-storefront.price :amount="$order->total_amount" :currency="$order->currency" class="justify-end" />

                            @empty

                                    No orders for this customer.

                            @endforelse

            {{ $orders->links() }}

                <flux:heading size="lg">Addresses</flux:heading>
                <flux:button wire:click="openAddressForm" variant="primary" icon="plus" size="sm">Add</flux:button>

                @forelse ($customer->addresses as $address)
                    @php($addressData = $address->address_json ?? [])

getKey() }}" class="rounded-lg border border-zinc-200 p-4 dark:border-zinc-700">

{{ $address->label ?: 'Address' }}

                                    @if ($address->is_default)
                                        <flux:badge color="green">Default</flux:badge>
                                    @endif

{{ data_get($addressData, 'address1') }}

                                    @if (data_get($addressData, 'address2'))

{{ data_get($addressData, 'address2') }}

                                    @endif

{{ trim(data_get($addressData, 'postal_code').' '.data_get($addressData, 'city')) }}

{{ data_get($addressData, 'country') }}

                            <flux:button wire:click="openAddressForm({{ $address->getKey() }})" size="sm" variant="filled">Edit</flux:button>
                            @unless ($address->is_default)
                                <flux:button wire:click="setDefaultAddress({{ $address->getKey() }})" size="sm" variant="ghost">Set default</flux:button>
                            @endunless
                            <flux:button wire:click="deleteAddress({{ $address->getKey() }})" wire:confirm="Delete this address?" size="sm" variant="danger">Delete</flux:button>

                @empty

                        <flux:icon name="map-pin" class="mx-auto size-8 text-zinc-400" />
                        <flux:text class="mt-3">No addresses saved.</flux:text>

                @endforelse

    <flux:modal name="address-form" class="md:w-[32rem]">
        <form wire:submit="saveAddress" class="space-y-6">

                <flux:heading size="lg">{{ $editingAddressId ? 'Edit address' : 'Add address' }}</flux:heading>

            <flux:input wire:model="addressLabel" label="Label" placeholder="Home, Office..." />

                <flux:input wire:model="addressJson.first_name" label="First name" />
                <flux:input wire:model="addressJson.last_name" label="Last name" />

                    <flux:input wire:model="addressJson.address1" label="Address line 1" />
                    <flux:error name="addressJson.address1" />

                    <flux:input wire:model="addressJson.address2" label="Address line 2" />

                <flux:input wire:model="addressJson.city" label="City" />
                <flux:input wire:model="addressJson.province_code" label="State / Province" />
                <flux:input wire:model="addressJson.postal_code" label="Postal code" />
                <flux:select wire:model="addressJson.country" label="Country">
                    <flux:select.option value="DE">Germany</flux:select.option>
                    <flux:select.option value="US">United States</flux:select.option>
                    <flux:select.option value="GB">United Kingdom</flux:select.option>
                    <flux:select.option value="FR">France</flux:select.option>
                    <flux:select.option value="NL">Netherlands</flux:select.option>
                </flux:select>

            <flux:error name="addressJson.city" />
            <flux:error name="addressJson.postal_code" />
            <flux:error name="addressJson.country" />

                <flux:modal.close>
                    <flux:button variant="filled">Cancel</flux:button>
                </flux:modal.close>
                <flux:button type="submit" variant="primary">Save</flux:button>

        </form>
    </flux:modal>


command: sed -n '1,260p' tests/Browser/Admin/CollectionManagementTest.php

aggregated_output:
<?php

use App\Models\Collection;
use App\Models\Store;
use App\Models\User;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Pest\Browser\Playwright\Playwright;

uses(RefreshDatabase::class);

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

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

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

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

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

function adminCollectionBrowserAuthenticate(mixed $testCase): Store
{
    $store = adminCollectionBrowserStore();
    $user = User::query()->where('email', 'admin@acme.test')->firstOrFail();

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

    return $store;
}

function adminCollectionOpenCollections(mixed $testCase): mixed
{
    adminCollectionBrowserAuthenticate($testCase);

    return visit('/admin/collections', adminCollectionBrowserHost())
        ->wait(1)
        ->assertPathIs('/admin/collections')
        ->assertSee('Collections')
        ->assertNoJavaScriptErrors();
}

function adminCollectionSave(mixed $page): mixed
{
    return $page
        ->click('button[data-test="collection-save-button"]')
        ->wait(1)
        ->assertSee('Collection saved')
        ->assertNoJavaScriptErrors();
}

test('shows the collection list with seeded collections', function (): void {
    adminCollectionOpenCollections($this)
        ->assertSee('T-Shirts')
        ->assertSee('New Arrivals')
        ->assertNoJavaScriptErrors();
});

test('can create a new collection', function (): void {
    $store = adminCollectionBrowserAuthenticate($this);
    $page = adminCollectionOpenCollections($this)
        ->click('a[href$="/admin/collections/create"]')
        ->wait(1)
        ->assertPathIs('/admin/collections/create')
        ->assertSee('Add collection')
        ->assertNoJavaScriptErrors();

    $page
        ->fill('input[wire\\:model\\.live\\.debounce\\.300ms="title"]', 'E2E Test Collection')
        ->fill('input[wire\\:model="handle"]', 'e2e-test-collection')
        ->fill('textarea[wire\\:model="descriptionHtml"]', 'A collection created by the E2E test suite.');

    adminCollectionSave($page)
        ->navigate('/admin/collections')
        ->wait(1)
        ->assertPathIs('/admin/collections')
        ->assertSee('E2E Test Collection')
        ->assertNoJavaScriptErrors();

    $this->assertDatabaseHas('collections', [
        'store_id' => $store->getKey(),
        'title' => 'E2E Test Collection',
        'handle' => 'e2e-test-collection',
        'description_html' => 'A collection created by the E2E test suite.',
    ]);
});

test('can edit a collection', function (): void {
    $store = adminCollectionBrowserAuthenticate($this);

    $page = adminCollectionOpenCollections($this)
        ->click('a:has-text("T-Shirts")')
        ->wait(1)
        ->assertPathContains('/admin/collections/')
        ->assertValue('input[wire\\:model\\.live\\.debounce\\.300ms="title"]', 'T-Shirts')
        ->assertNoJavaScriptErrors();

    $page->fill('textarea[wire\\:model="descriptionHtml"]', 'Updated description for T-Shirts collection.');

    adminCollectionSave($page);

    $collection = Collection::withoutGlobalScopes()
        ->where('store_id', $store->getKey())
        ->where('title', 'T-Shirts')
        ->firstOrFail();

    expect($collection->description_html)->toBe('Updated description for T-Shirts collection.');
});


command: sed -n '1,260p' tests/Feature/Admin/CustomerManagementTest.php

aggregated_output:
<?php

use App\Livewire\Admin\Customers\Index as AdminCustomersIndex;
use App\Livewire\Admin\Customers\Show as AdminCustomerShow;
use App\Models\Customer;
use App\Models\CustomerAddress;
use App\Models\Order;
use App\Models\Store;
use App\Models\User;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;

uses(RefreshDatabase::class);

beforeEach(function (): void {
    $this->withoutVite();
    $this->seed(DatabaseSeeder::class);
});

function adminCustomerManagementStore(): Store
{
    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
    app()->instance('current_store', $store);

    return $store;
}

function adminCustomerManagementUser(): User
{
    return User::query()->where('email', 'admin@acme.test')->firstOrFail();
}

function adminCustomerManagementCustomer(Store $store, string $email, string $name): Customer
{
    return Customer::factory()->create([
        'store_id' => $store->getKey(),
        'email' => $email,
        'name' => $name,
    ]);
}

function adminCustomerManagementOrder(Store $store, Customer $customer, string $orderNumber, int $totalAmount): Order
{
    return Order::factory()->paid()->create([
        'store_id' => $store->getKey(),
        'customer_id' => $customer->getKey(),
        'order_number' => $orderNumber,
        'subtotal_amount' => $totalAmount,
        'shipping_amount' => 0,
        'tax_amount' => 0,
        'total_amount' => $totalAmount,
        'email' => $customer->email,
    ]);
}

test('admin customer routes require authentication and render store scoped customers', function (): void {
    $store = adminCustomerManagementStore();
    $customer = adminCustomerManagementCustomer($store, 'jane@example.test', 'Jane Example');
    adminCustomerManagementOrder($store, $customer, '#8101', 7000);
    CustomerAddress::factory()->default()->create([
        'customer_id' => $customer->getKey(),
        'label' => 'Home',
    ]);

    $otherStore = Store::factory()->create();
    $otherCustomer = adminCustomerManagementCustomer($otherStore, 'other@example.test', 'Other Customer');

    $this->get('/admin/customers')->assertRedirect('/admin/login');

    $this->actingAs(adminCustomerManagementUser())
        ->withSession(['current_store_id' => $store->getKey()])
        ->get('/admin/customers')
        ->assertSuccessful()
        ->assertSee('Jane Example')
        ->assertSee('70.00 EUR')
        ->assertDontSee('Other Customer');

    $this->actingAs(adminCustomerManagementUser())
        ->withSession(['current_store_id' => $store->getKey()])
        ->get('/admin/customers/'.$customer->getKey())
        ->assertSuccessful()
        ->assertSee('Jane Example')
        ->assertSee('#8101')
        ->assertSee('Home');

    $this->actingAs(adminCustomerManagementUser())
        ->withSession(['current_store_id' => $store->getKey()])
        ->get('/admin/customers/'.$otherCustomer->getKey())
        ->assertNotFound();
});

test('admin customer index filters by name and email', function (): void {
    $store = adminCustomerManagementStore();
    $user = adminCustomerManagementUser();
    adminCustomerManagementCustomer($store, 'jane@example.test', 'Jane Example');
    adminCustomerManagementCustomer($store, 'bravo@example.test', 'Bravo Example');

    Livewire::actingAs($user)
        ->test(AdminCustomersIndex::class)
        ->assertSee('Jane Example')
        ->assertSee('Bravo Example')
        ->set('search', 'jane')
        ->assertSee('Jane Example')
        ->assertDontSee('Bravo Example')
        ->set('search', 'bravo@example.test')
        ->assertSee('Bravo Example')
        ->assertDontSee('Jane Example');
});

test('admin customer detail manages addresses', function (): void {
    $store = adminCustomerManagementStore();
    $user = adminCustomerManagementUser();
    $customer = adminCustomerManagementCustomer($store, 'jane@example.test', 'Jane Example');
    $home = CustomerAddress::factory()->default()->create([
        'customer_id' => $customer->getKey(),
        'label' => 'Home',
    ]);

    Livewire::actingAs($user)
        ->test(AdminCustomerShow::class, ['customer' => $customer])
        ->call('openAddressForm')
        ->set('addressLabel', 'Office')
        ->set('addressJson.address1', 'Business Street 5')
        ->set('addressJson.city', 'Berlin')
        ->set('addressJson.postal_code', '10115')
        ->set('addressJson.country', 'DE')
        ->call('saveAddress')
        ->assertHasNoErrors();

    $office = CustomerAddress::query()
        ->where('customer_id', $customer->getKey())
        ->where('label', 'Office')
        ->firstOrFail();

    expect($office->is_default)->toBeFalse();

    Livewire::actingAs($user)
        ->test(AdminCustomerShow::class, ['customer' => $customer])
        ->call('setDefaultAddress', $office->getKey())
        ->assertHasNoErrors()
        ->call('openAddressForm', $office->getKey())
        ->set('addressLabel', 'HQ')
        ->call('saveAddress')
        ->assertHasNoErrors()
        ->call('deleteAddress', $home->getKey())
        ->assertHasNoErrors();

    expect($office->refresh()->is_default)->toBeTrue()
        ->and($office->label)->toBe('HQ')
        ->and(CustomerAddress::query()->whereKey($home->getKey())->exists())->toBeFalse();
});

test('admin customer detail rejects customers from another store', function (): void {
    $store = adminCustomerManagementStore();
    $otherStore = Store::factory()->create();
    $otherCustomer = adminCustomerManagementCustomer($otherStore, 'other@example.test', 'Other Customer');

    app()->instance('current_store', $store);

    Livewire::actingAs(adminCustomerManagementUser())
        ->test(AdminCustomerShow::class, ['customer' => $otherCustomer])
        ->assertStatus(404);
});