output:
Chunk ID: 962eec
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1888
Output:
---
name: livewire-development
description: "Use for any task or question involving Livewire. Activate if user mentions Livewire, wire: directives, or Livewire-specific concepts like wire:model, wire:click, wire:sort, or islands, invoke this skill. Covers building new components, debugging reactivity issues, real-time form validation, drag-and-drop, loading states, migrating from Livewire 3 to 4, converting component formats (SFC/MFC/class-based), and performance optimization. Do not use for non-Livewire reactive UI (React, Vue, Alpine-only, Inertia.js) or standard Laravel forms without Livewire."
license: MIT
metadata:
  author: laravel
---

# Livewire Development

## Documentation

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

## Basic Usage

### Creating Components

```bash

# Single-file component (SFC - default in v4)

# Creates: resources/views/components/⚡create-post.blade.php

php artisan make:livewire create-post

# Page component (SFC - Full Page in v4)

# Creates: resources/views/pages/⚡create-post.blade.php

php artisan make:livewire pages::create-post

# Multi-file component (MFC)

# Creates: resources/views/components/⚡create-post/create-post.php

#          resources/views/components/⚡create-post/create-post.blade.php

php artisan make:livewire create-post --mfc

# Class-based component (v3 style)

# Creates: app/Livewire/CreatePost.php AND resources/views/livewire/create-post.blade.php

php artisan make:livewire create-post --class

# With namespace

php artisan make:livewire Posts/CreatePost
```

### Converting Between Formats

Use `php artisan livewire:convert create-post` to convert between single-file, multi-file, and class-based formats.

### Choosing a Component Format

> **Always follow the project's existing conventions first.** Before creating any component, inspect the project's existing Livewire components to determine the established format (SFC, MFC, or class-based) and directory structure. Check `app/Livewire/`, `resources/views/components/`, and `resources/views/livewire/` for existing components. If the project already uses a consistent format, **use that same format** — even if it differs from the Livewire v4 defaults below. Only fall back to the v4 defaults (SFC in `resources/views/components/`) when no existing convention is established.

Also check `config/livewire.php` for `make_command.type`, `make_command.emoji`, `component_locations`, and `component_namespaces` overrides, which change the default format and where files are stored.

### Component Format Reference

| Format | Flag | Class Path | View Path |
|--------|------|------------|-----------|
| Single-file (SFC) | default | — | `resources/views/components/⚡create-post.blade.php` (PHP + Blade in one file) |
| Full Page SFC | `pages::name` | — | `resources/views/pages/⚡create-post.blade.php` |
| Multi-file (MFC) | `--mfc` | `resources/views/components/⚡create-post/create-post.php` | `resources/views/components/⚡create-post/create-post.blade.php` |
| Class-based | `--class` | `app/Livewire/CreatePost.php` | `resources/views/livewire/create-post.blade.php` |
| View-based | default (Blade-only) | — | `resources/views/components/⚡create-post.blade.php` (Blade-only with functional state) |

> **Important:** The ⚡ prefix shown above is the **default** behavior in Livewire v4 — it is **configurable**. Check `config/livewire.php` for the `make_command.emoji` setting. When `true` (default), always include the ⚡ prefix in filenames you create. When `false`, omit the ⚡ prefix from all paths above.

Namespaced components map to subdirectories: `make:livewire Posts/CreatePost` creates `resources/views/components/posts/⚡create-post.blade.php` (single-file by default). Use `make:livewire Posts/CreatePost --mfc` for multi-file output at `resources/views/components/posts/⚡create-post/create-post.php` and `resources/views/components/posts/⚡create-post/create-post.blade.php`.

### Single-File Component Example

<!-- Single-File Component Example -->
```php
<?php
use Livewire\Component;

new class extends Component {
    public int $count = 0;

    public function increment(): void
    {
        $this->count++;
    }
};
?>

<div>
    <button wire:click="increment">Count: @{{ $count }}</button>
</div>
```

## Livewire 4 Specifics

### Key Changes From Livewire 3

These things changed in Livewire 4, but may not have been updated in this application. Verify this application's setup to ensure you follow existing conventions.

- Use `Route::livewire()` for full-page components (e.g., `Route::livewire('/posts/create', CreatePost::class)`); config keys renamed: `layout` → `component_layout`, `lazy_placeholder` → `component_placeholder`.
- `wire:model` now ignores child events by default (use `wire:model.deep` for old behavior); `wire:scroll` renamed to `wire:navigate:scroll`.
- Component tags must be properly closed; `wire:transition` now uses View Transitions API (modifiers removed).
- JavaScript: `$wire.$js('name', fn)` → `$wire.$js.name = fn`; `commit`/`request` hooks → `interceptMessage()`/`interceptRequest()`.

### New Features

- Component formats: single-file (SFC), multi-file (MFC), view-based components.
- Islands (`@island`) for isolated updates; async actions (`wire:click.async`, `#[Async]`) for parallel execution.
- Deferred/bundled loading: `defer`, `lazy.bundle` for optimized component loading.

| Feature | Usage | Purpose |
|---------|-------|---------|
| Islands | `@island(name: 'stats')` | Isolated update regions |
| Async | `wire:click.async` or `#[Async]` | Non-blocking actions |
| Deferred | `defer` attribute | Load after page render |
| Bundled | `lazy.bundle` | Load multiple together |

### New Directives

- `wire:sort`, `wire:intersect`, `wire:ref`, `.renderless`, `.preserve-scroll` are available for use.
- `data-loading` attribute automatically added to elements triggering network requests.

| Directive | Purpose |
|-----------|---------|
| `wire:sort` | Drag-and-drop sorting |
| `wire:intersect` | Viewport intersection detection |
| `wire:ref` | Element references for JS |
| `.renderless` | Component without rendering |
| `.preserve-scroll` | Preserve scroll position |

## Best Practices

- Always use `wire:key` in loops
- Use `wire:loading` for loading states
- Use `wire:model.live` for instant updates (default is debounced)
- Validate and authorize in actions (treat like HTTP requests)

## Configuration

- `smart_wire_keys` defaults to `true`; new configs: `component_locations`, `component_namespaces`, `make_command`, `csp_safe`.

## Alpine & JavaScript

- `wire:transition` uses browser View Transitions API; `$errors` and `$intercept` magic properties available.
- Non-blocking `wire:poll` and parallel `wire:model.live` updates improve performance.

For interceptors and hooks, see [reference/javascript-hooks.md](reference/javascript-hooks.md).

## Testing

<!-- Testing Example -->
```php
Livewire::test(Counter::class)
    ->assertSet('count', 0)
    ->call('increment')
    ->assertSet('count', 1);
```

## Verification

1. Browser console: Check for JS errors
2. Network tab: Verify Livewire requests return 200
3. Ensure `wire:key` on all `@foreach` loops

## Common Pitfalls

- Missing `wire:key` in loops → unexpected re-rendering
- Expecting `wire:model` real-time → use `wire:model.live`
- Unclosed component tags → syntax errors in v4
- Using deprecated config keys or JS hooks
- Including Alpine.js separately (already bundled in Livewire 4)


command: sed -n '1,220p' /Users/fabianwesner/Herd/shop/.agents/skills/tailwindcss-development/SKILL.md

SKILL.md

type: read

aggregated_output:
---
name: tailwindcss-development
description: "Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS."
license: MIT
metadata:
  author: laravel
---

# Tailwind CSS Development

## Documentation

Use `search-docs` for detailed Tailwind CSS v4 patterns and documentation.

## Basic Usage

- Use Tailwind CSS classes to style HTML. Check and follow existing Tailwind conventions in the project before introducing new patterns.
- Offer to extract repeated patterns into components that match the project's conventions (e.g., Blade, JSX, Vue).
- Consider class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child elements carefully to reduce repetition, and group elements logically.

## Tailwind CSS v4 Specifics

- Always use Tailwind CSS v4 and avoid deprecated utilities.
- `corePlugins` is not supported in Tailwind v4.

### CSS-First Configuration

In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed:

<!-- CSS-First Config -->
```css
@theme {
  --color-brand: oklch(0.72 0.11 178);
}
```

### Import Syntax

In Tailwind v4, import Tailwind with a regular CSS `@import` statement instead of the `@tailwind` directives used in v3:

<!-- v4 Import Syntax -->
```diff
- @tailwind base;
- @tailwind components;
- @tailwind utilities;
+ @import "tailwindcss";
```

### Replaced Utilities

Tailwind v4 removed deprecated utilities. Use the replacements shown below. Opacity values remain numeric.

| Deprecated | Replacement |
|------------|-------------|
| bg-opacity-* | bg-black/* |
| text-opacity-* | text-black/* |
| border-opacity-* | border-black/* |
| divide-opacity-* | divide-black/* |
| ring-opacity-* | ring-black/* |
| placeholder-opacity-* | placeholder-black/* |
| flex-shrink-* | shrink-* |
| flex-grow-* | grow-* |
| overflow-ellipsis | text-ellipsis |
| decoration-slice | box-decoration-slice |
| decoration-clone | box-decoration-clone |

## Spacing

Use `gap` utilities instead of margins for spacing between siblings:

<!-- Gap Utilities -->
```html
<div class="flex gap-8">
    <div>Item 1</div>
    <div>Item 2</div>
</div>
```

## Dark Mode

If existing pages and components support dark mode, new pages and components must support it the same way, typically using the `dark:` variant:

<!-- Dark Mode -->
```html
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
    Content adapts to color scheme
</div>
```

## Common Patterns

### Flexbox Layout

<!-- Flexbox Layout -->
```html
<div class="flex items-center justify-between gap-4">
    <div>Left content</div>
    <div>Right content</div>
</div>
```

### Grid Layout

<!-- Grid Layout -->
```html
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
    <div>Card 1</div>
    <div>Card 2</div>
    <div>Card 3</div>
</div>
```

## Common Pitfalls

- Using deprecated v3 utilities (bg-opacity-*, flex-shrink-*, etc.)
- Using `@tailwind` directives instead of `@import "tailwindcss"`
- Trying to use `tailwind.config.js` instead of CSS `@theme` directive
- Using margins for spacing between siblings instead of gap utilities
- Forgetting to add dark mode variants when the project uses dark mode

status: completed


output:
Chunk ID: c46c5d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 962
Output:
---
name: tailwindcss-development
description: "Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS."
license: MIT
metadata:
  author: laravel
---

# Tailwind CSS Development

## Documentation

Use `search-docs` for detailed Tailwind CSS v4 patterns and documentation.

## Basic Usage

- Use Tailwind CSS classes to style HTML. Check and follow existing Tailwind conventions in the project before introducing new patterns.
- Offer to extract repeated patterns into components that match the project's conventions (e.g., Blade, JSX, Vue).
- Consider class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child elements carefully to reduce repetition, and group elements logically.

## Tailwind CSS v4 Specifics

- Always use Tailwind CSS v4 and avoid deprecated utilities.
- `corePlugins` is not supported in Tailwind v4.

### CSS-First Configuration

In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed:

<!-- CSS-First Config -->
```css
@theme {
  --color-brand: oklch(0.72 0.11 178);
}
```

### Import Syntax

In Tailwind v4, import Tailwind with a regular CSS `@import` statement instead of the `@tailwind` directives used in v3:

<!-- v4 Import Syntax -->
```diff
- @tailwind base;
- @tailwind components;
- @tailwind utilities;
+ @import "tailwindcss";
```

### Replaced Utilities

Tailwind v4 removed deprecated utilities. Use the replacements shown below. Opacity values remain numeric.

| Deprecated | Replacement |
|------------|-------------|
| bg-opacity-* | bg-black/* |
| text-opacity-* | text-black/* |
| border-opacity-* | border-black/* |
| divide-opacity-* | divide-black/* |
| ring-opacity-* | ring-black/* |
| placeholder-opacity-* | placeholder-black/* |
| flex-shrink-* | shrink-* |
| flex-grow-* | grow-* |
| overflow-ellipsis | text-ellipsis |
| decoration-slice | box-decoration-slice |
| decoration-clone | box-decoration-clone |

## Spacing

Use `gap` utilities instead of margins for spacing between siblings:

<!-- Gap Utilities -->
```html
<div class="flex gap-8">
    <div>Item 1</div>
    <div>Item 2</div>
</div>
```

## Dark Mode

If existing pages and components support dark mode, new pages and components must support it the same way, typically using the `dark:` variant:

<!-- Dark Mode -->
```html
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
    Content adapts to color scheme
</div>
```

## Common Patterns

### Flexbox Layout

<!-- Flexbox Layout -->
```html
<div class="flex items-center justify-between gap-4">
    <div>Left content</div>
    <div>Right content</div>
</div>
```

### Grid Layout

<!-- Grid Layout -->
```html
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
    <div>Card 1</div>
    <div>Card 2</div>
    <div>Card 3</div>
</div>
```

## Common Pitfalls

- Using deprecated v3 utilities (bg-opacity-*, flex-shrink-*, etc.)
- Using `@tailwind` directives instead of `@import "tailwindcss"`
- Trying to use `tailwind.config.js` instead of CSS `@theme` directive
- Using margins for spacing between siblings instead of gap utilities
- Forgetting to add dark mode variants when the project uses dark mode


command: sed -n '1,220p' /Users/fabianwesner/Herd/shop/.agents/skills/laravel-best-practices/SKILL.md

aggregated_output:
---
name: laravel-best-practices
description: "Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns."
license: MIT
metadata:
  author: laravel
---

# Laravel Best Practices

Best practices for Laravel, prioritized by impact. Each rule teaches what to do and why. For exact API syntax, verify with `search-docs`.

## Consistency First

Before applying any rule, check what the application already does. Laravel offers multiple valid approaches — the best choice is the one the codebase already uses, even if another pattern would be theoretically better. Inconsistency is worse than a suboptimal pattern.

Check sibling files, related controllers, models, or tests for established patterns. If one exists, follow it — don't introduce a second way. These rules are defaults for when no pattern exists yet, not overrides.

## Quick Reference

### 1. Database Performance → `rules/db-performance.md`

- Eager load with `with()` to prevent N+1 queries
- Enable `Model::preventLazyLoading()` in development
- Select only needed columns, avoid `SELECT *`
- `chunk()` / `chunkById()` for large datasets
- Index columns used in `WHERE`, `ORDER BY`, `JOIN`
- `withCount()` instead of loading relations to count
- `cursor()` for memory-efficient read-only iteration
- Never query in Blade templates

### 2. Advanced Query Patterns → `rules/advanced-queries.md`

- `addSelect()` subqueries over eager-loading entire has-many for a single value
- Dynamic relationships via subquery FK + `belongsTo`
- Conditional aggregates (`CASE WHEN` in `selectRaw`) over multiple count queries
- `setRelation()` to prevent circular N+1 queries
- `whereIn` + `pluck()` over `whereHas` for better index usage
- Two simple queries can beat one complex query
- Compound indexes matching `orderBy` column order
- Correlated subqueries in `orderBy` for has-many sorting (avoid joins)

### 3. Security → `rules/security.md`

- Define `$fillable` or `$guarded` on every model, authorize every action via policies or gates
- No raw SQL with user input — use Eloquent or query builder
- `{{ }}` for output escaping, `@csrf` on all POST/PUT/DELETE forms, `throttle` on auth and API routes
- Validate MIME type, extension, and size for file uploads
- Never commit `.env`, use `config()` for secrets, `encrypted` cast for sensitive DB fields

### 4. Caching → `rules/caching.md`

- `Cache::remember()` over manual get/put
- `Cache::flexible()` for stale-while-revalidate on high-traffic data
- `Cache::memo()` to avoid redundant cache hits within a request
- Cache tags to invalidate related groups
- `Cache::add()` for atomic conditional writes
- `once()` to memoize per-request or per-object lifetime
- `Cache::lock()` / `lockForUpdate()` for race conditions
- Failover cache stores in production

### 5. Eloquent Patterns → `rules/eloquent.md`

- Correct relationship types with return type hints
- Local scopes for reusable query constraints
- Global scopes sparingly — document their existence
- Attribute casts in the `casts()` method
- Cast date columns, use Carbon instances in templates
- `whereBelongsTo($model)` for cleaner queries
- Never hardcode table names — use `(new Model)->getTable()` or Eloquent queries

### 6. Validation & Forms → `rules/validation.md`

- Form Request classes, not inline validation
- Array notation `['required', 'email']` for new code; follow existing convention
- `$request->validated()` only — never `$request->all()`
- `Rule::when()` for conditional validation
- `after()` instead of `withValidator()`

### 7. Configuration → `rules/config.md`

- `env()` only inside config files
- `App::environment()` or `app()->isProduction()`
- Config, lang files, and constants over hardcoded text

### 8. Testing Patterns → `rules/testing.md`

- `LazilyRefreshDatabase` over `RefreshDatabase` for speed
- `assertModelExists()` over raw `assertDatabaseHas()`
- Factory states and sequences over manual overrides
- Use fakes (`Event::fake()`, `Exceptions::fake()`, etc.) — but always after factory setup, not before
- `recycle()` to share relationship instances across factories

### 9. Queue & Job Patterns → `rules/queue-jobs.md`

- `retry_after` must exceed job `timeout`; use exponential backoff `[1, 5, 10]`
- `ShouldBeUnique` to prevent duplicates; `ShouldBeUniqueUntilProcessing` for early lock release
- Always implement `failed()`; with `retryUntil()`, set `$tries = 0`
- `RateLimited` middleware for external API calls; `Bus::batch()` for related jobs
- Horizon for complex multi-queue scenarios

### 10. Routing & Controllers → `rules/routing.md`

- Implicit route model binding
- Scoped bindings for nested resources
- `Route::resource()` or `apiResource()`
- Methods under 10 lines — extract to actions/services
- Type-hint Form Requests for auto-validation

### 11. HTTP Client → `rules/http-client.md`

- Explicit `timeout` and `connectTimeout` on every request
- `retry()` with exponential backoff for external APIs
- Check response status or use `throw()`
- `Http::pool()` for concurrent independent requests
- `Http::fake()` and `preventStrayRequests()` in tests

### 12. Events, Notifications & Mail → `rules/events-notifications.md`, `rules/mail.md`

- Event discovery over manual registration; `event:cache` in production
- `ShouldDispatchAfterCommit` / `afterCommit()` inside transactions
- Queue notifications and mailables with `ShouldQueue`
- On-demand notifications for non-user recipients
- `HasLocalePreference` on notifiable models
- `assertQueued()` not `assertSent()` for queued mailables
- Markdown mailables for transactional emails

### 13. Error Handling → `rules/error-handling.md`

- `report()`/`render()` on exception classes or in `bootstrap/app.php` — follow existing pattern
- `ShouldntReport` for exceptions that should never log
- Throttle high-volume exceptions to protect log sinks
- `dontReportDuplicates()` for multi-catch scenarios
- Force JSON rendering for API routes
- Structured context via `context()` on exception classes

### 14. Task Scheduling → `rules/scheduling.md`

- `withoutOverlapping()` on variable-duration tasks
- `onOneServer()` on multi-server deployments
- `runInBackground()` for concurrent long tasks
- `environments()` to restrict to appropriate environments
- `takeUntilTimeout()` for time-bounded processing
- Schedule groups for shared configuration

### 15. Architecture → `rules/architecture.md`

- Single-purpose Action classes; dependency injection over `app()` helper
- Prefer official Laravel packages and follow conventions, don't override defaults
- Default to `ORDER BY id DESC` or `created_at DESC`; `mb_*` for UTF-8 safety
- `defer()` for post-response work; `Context` for request-scoped data; `Concurrency::run()` for parallel execution

### 16. Migrations → `rules/migrations.md`

- Generate migrations with `php artisan make:migration`
- `constrained()` for foreign keys
- Never modify migrations that have run in production
- Add indexes in the migration, not as an afterthought
- Mirror column defaults in model `$attributes`
- Reversible `down()` by default; forward-fix migrations for intentionally irreversible changes
- One concern per migration — never mix DDL and DML

### 17. Collections → `rules/collections.md`

- Higher-order messages for simple collection operations
- `cursor()` vs. `lazy()` — choose based on relationship needs
- `lazyById()` when updating records while iterating
- `toQuery()` for bulk operations on collections

### 18. Blade & Views → `rules/blade-views.md`

- `$attributes->merge()` in component templates
- Blade components over `@include`; `@pushOnce` for per-component scripts
- View Composers for shared view data
- `@aware` for deeply nested component props

### 19. Conventions & Style → `rules/style.md`

- Follow Laravel naming conventions for all entities
- Prefer Laravel helpers (`Str`, `Arr`, `Number`, `Uri`, `Str::of()`, `$request->string()`) over raw PHP functions
- No JS/CSS in Blade, no HTML in PHP classes
- Code should be readable; comments only for config files

## How to Apply

Always use a sub-agent to read rule files and explore this skill's content.

1. Identify the file type and select relevant sections (e.g., migration → §16, controller → §1, §3, §5, §6, §10)
2. Check sibling files for existing patterns — follow those first per Consistency First
3. Verify API syntax with `search-docs` for the installed Laravel version


command: sed -n '1,220p' /Users/fabianwesner/Herd/shop/.agents/skills/pest-testing/SKILL.md

aggregated_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


output:
Chunk ID: 44ac16
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


output:
Chunk ID: 6c4b23
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2249
Output:
---
name: laravel-best-practices
description: "Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns."
license: MIT
metadata:
  author: laravel
---

# Laravel Best Practices

Best practices for Laravel, prioritized by impact. Each rule teaches what to do and why. For exact API syntax, verify with `search-docs`.

## Consistency First

Before applying any rule, check what the application already does. Laravel offers multiple valid approaches — the best choice is the one the codebase already uses, even if another pattern would be theoretically better. Inconsistency is worse than a suboptimal pattern.

Check sibling files, related controllers, models, or tests for established patterns. If one exists, follow it — don't introduce a second way. These rules are defaults for when no pattern exists yet, not overrides.

## Quick Reference

### 1. Database Performance → `rules/db-performance.md`

- Eager load with `with()` to prevent N+1 queries
- Enable `Model::preventLazyLoading()` in development
- Select only needed columns, avoid `SELECT *`
- `chunk()` / `chunkById()` for large datasets
- Index columns used in `WHERE`, `ORDER BY`, `JOIN`
- `withCount()` instead of loading relations to count
- `cursor()` for memory-efficient read-only iteration
- Never query in Blade templates

### 2. Advanced Query Patterns → `rules/advanced-queries.md`

- `addSelect()` subqueries over eager-loading entire has-many for a single value
- Dynamic relationships via subquery FK + `belongsTo`
- Conditional aggregates (`CASE WHEN` in `selectRaw`) over multiple count queries
- `setRelation()` to prevent circular N+1 queries
- `whereIn` + `pluck()` over `whereHas` for better index usage
- Two simple queries can beat one complex query
- Compound indexes matching `orderBy` column order
- Correlated subqueries in `orderBy` for has-many sorting (avoid joins)

### 3. Security → `rules/security.md`

- Define `$fillable` or `$guarded` on every model, authorize every action via policies or gates
- No raw SQL with user input — use Eloquent or query builder
- `{{ }}` for output escaping, `@csrf` on all POST/PUT/DELETE forms, `throttle` on auth and API routes
- Validate MIME type, extension, and size for file uploads
- Never commit `.env`, use `config()` for secrets, `encrypted` cast for sensitive DB fields

### 4. Caching → `rules/caching.md`

- `Cache::remember()` over manual get/put
- `Cache::flexible()` for stale-while-revalidate on high-traffic data
- `Cache::memo()` to avoid redundant cache hits within a request
- Cache tags to invalidate related groups
- `Cache::add()` for atomic conditional writes
- `once()` to memoize per-request or per-object lifetime
- `Cache::lock()` / `lockForUpdate()` for race conditions
- Failover cache stores in production

### 5. Eloquent Patterns → `rules/eloquent.md`

- Correct relationship types with return type hints
- Local scopes for reusable query constraints
- Global scopes sparingly — document their existence
- Attribute casts in the `casts()` method
- Cast date columns, use Carbon instances in templates
- `whereBelongsTo($model)` for cleaner queries
- Never hardcode table names — use `(new Model)->getTable()` or Eloquent queries

### 6. Validation & Forms → `rules/validation.md`

- Form Request classes, not inline validation
- Array notation `['required', 'email']` for new code; follow existing convention
- `$request->validated()` only — never `$request->all()`
- `Rule::when()` for conditional validation
- `after()` instead of `withValidator()`

### 7. Configuration → `rules/config.md`

- `env()` only inside config files
- `App::environment()` or `app()->isProduction()`
- Config, lang files, and constants over hardcoded text

### 8. Testing Patterns → `rules/testing.md`

- `LazilyRefreshDatabase` over `RefreshDatabase` for speed
- `assertModelExists()` over raw `assertDatabaseHas()`
- Factory states and sequences over manual overrides
- Use fakes (`Event::fake()`, `Exceptions::fake()`, etc.) — but always after factory setup, not before
- `recycle()` to share relationship instances across factories

### 9. Queue & Job Patterns → `rules/queue-jobs.md`

- `retry_after` must exceed job `timeout`; use exponential backoff `[1, 5, 10]`
- `ShouldBeUnique` to prevent duplicates; `ShouldBeUniqueUntilProcessing` for early lock release
- Always implement `failed()`; with `retryUntil()`, set `$tries = 0`
- `RateLimited` middleware for external API calls; `Bus::batch()` for related jobs
- Horizon for complex multi-queue scenarios

### 10. Routing & Controllers → `rules/routing.md`

- Implicit route model binding
- Scoped bindings for nested resources
- `Route::resource()` or `apiResource()`
- Methods under 10 lines — extract to actions/services
- Type-hint Form Requests for auto-validation

### 11. HTTP Client → `rules/http-client.md`

- Explicit `timeout` and `connectTimeout` on every request
- `retry()` with exponential backoff for external APIs
- Check response status or use `throw()`
- `Http::pool()` for concurrent independent requests
- `Http::fake()` and `preventStrayRequests()` in tests

### 12. Events, Notifications & Mail → `rules/events-notifications.md`, `rules/mail.md`

- Event discovery over manual registration; `event:cache` in production
- `ShouldDispatchAfterCommit` / `afterCommit()` inside transactions
- Queue notifications and mailables with `ShouldQueue`
- On-demand notifications for non-user recipients
- `HasLocalePreference` on notifiable models
- `assertQueued()` not `assertSent()` for queued mailables
- Markdown mailables for transactional emails

### 13. Error Handling → `rules/error-handling.md`

- `report()`/`render()` on exception classes or in `bootstrap/app.php` — follow existing pattern
- `ShouldntReport` for exceptions that should never log
- Throttle high-volume exceptions to protect log sinks
- `dontReportDuplicates()` for multi-catch scenarios
- Force JSON rendering for API routes
- Structured context via `context()` on exception classes

### 14. Task Scheduling → `rules/scheduling.md`

- `withoutOverlapping()` on variable-duration tasks
- `onOneServer()` on multi-server deployments
- `runInBackground()` for concurrent long tasks
- `environments()` to restrict to appropriate environments
- `takeUntilTimeout()` for time-bounded processing
- Schedule groups for shared configuration

### 15. Architecture → `rules/architecture.md`

- Single-purpose Action classes; dependency injection over `app()` helper
- Prefer official Laravel packages and follow conventions, don't override defaults
- Default to `ORDER BY id DESC` or `created_at DESC`; `mb_*` for UTF-8 safety
- `defer()` for post-response work; `Context` for request-scoped data; `Concurrency::run()` for parallel execution

### 16. Migrations → `rules/migrations.md`

- Generate migrations with `php artisan make:migration`
- `constrained()` for foreign keys
- Never modify migrations that have run in production
- Add indexes in the migration, not as an afterthought
- Mirror column defaults in model `$attributes`
- Reversible `down()` by default; forward-fix migrations for intentionally irreversible changes
- One concern per migration — never mix DDL and DML

### 17. Collections → `rules/collections.md`

- Higher-order messages for simple collection operations
- `cursor()` vs. `lazy()` — choose based on relationship needs
- `lazyById()` when updating records while iterating
- `toQuery()` for bulk operations on collections

### 18. Blade & Views → `rules/blade-views.md`

- `$attributes->merge()` in component templates
- Blade components over `@include`; `@pushOnce` for per-component scripts
- View Composers for shared view data
- `@aware` for deeply nested component props

### 19. Conventions & Style → `rules/style.md`

- Follow Laravel naming conventions for all entities
- Prefer Laravel helpers (`Str`, `Arr`, `Number`, `Uri`, `Str::of()`, `$request->string()`) over raw PHP functions
- No JS/CSS in Blade, no HTML in PHP classes
- Code should be readable; comments only for config files

## How to Apply

Always use a sub-agent to read rule files and explore this skill's content.

1. Identify the file type and select relevant sections (e.g., migration → §16, controller → §1, §3, §5, §6, §10)
2. Check sibling files for existing patterns — follow those first per Consistency First
3. Verify API syntax with `search-docs` for the installed Laravel version


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


message: I’ve confirmed the installed package versions again. Next I’m checking the storefront layout, cart drawer/modal patterns, and existing browser-test helpers so the mobile suite uses the same conventions as the rest of the app.

phase: commentary


type: message

role: assistant

text: I’ve confirmed the installed package versions again. Next I’m checking the storefront layout, cart drawer/modal patterns, and existing browser-test helpers so the mobile suite uses the same conventions as the rest of the app.


cmd: sed -n '1,260p' resources/views/layouts/storefront.blade.php


cmd: sed -n '1,260p' resources/views/livewire/storefront/cart-drawer.blade.php


cmd: sed -n '1,260p' tests/Browser/Storefront/CheckoutTest.php


cmd: sed -n '1,220p' tests/Browser/Admin/AuthenticationTest.php


cmd: rg -n "resize\(|assertScript|script\(" tests/Browser vendor/pestphp -g '*.php'


command: sed -n '1,260p' tests/Browser/Storefront/CheckoutTest.php

aggregated_output:
<?php

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 storefrontCheckoutHost(): array
{
    return ['host' => 'shop.test'];
}

function storefrontCheckoutStart(): mixed
{
    return visit('/products/classic-cotton-t-shirt', storefrontCheckoutHost())
        ->click('M')
        ->wait(1)
        ->click('Black')
        ->wait(1)
        ->click('Add to cart')
        ->wait(1)
        ->navigate('/cart')
        ->wait(1)
        ->click('main button:has-text("Checkout")')
        ->wait(1)
        ->assertPathIs('/checkout')
        ->assertSee('Checkout');
}

/**
 * @param  array<string, string>  $overrides
 */
function storefrontCheckoutFillAddress(mixed $page, array $overrides = []): mixed
{
    $address = [
        'email' => 'test-buyer@example.com',
        'first_name' => 'Test',
        'last_name' => 'Buyer',
        'address1' => 'Teststrasse 1',
        'city' => 'Berlin',
        'postal_code' => '10115',
        'country' => 'DE',
    ];

    $address = array_replace($address, $overrides);

    return $page
        ->fill('input[wire\\:model="email"]', $address['email'])
        ->fill('input[wire\\:model="shippingAddress.first_name"]', $address['first_name'])
        ->fill('input[wire\\:model="shippingAddress.last_name"]', $address['last_name'])
        ->fill('input[wire\\:model="shippingAddress.address1"]', $address['address1'])
        ->fill('input[wire\\:model="shippingAddress.city"]', $address['city'])
        ->fill('input[wire\\:model="shippingAddress.postal_code"]', $address['postal_code'])
        ->select('select[wire\\:model="shippingAddress.country"]', $address['country']);
}

function storefrontCheckoutSubmitAddress(mixed $page): mixed
{
    return $page
        ->click('form[wire\\:submit="saveAddress"] button[type="submit"]')
        ->wait(1);
}

function storefrontCheckoutReachShipping(array $address = []): mixed
{
    return storefrontCheckoutSubmitAddress(
        storefrontCheckoutFillAddress(storefrontCheckoutStart(), $address),
    );
}

function storefrontCheckoutReachPayment(array $address = []): mixed
{
    return storefrontCheckoutReachShipping($address)
        ->click('button:has-text("Standard Shipping")')
        ->wait(1)
        ->click('button[wire\\:click="selectShippingMethod"]')
        ->wait(1)
        ->assertSee('Payment')
        ->assertSee('Pay now');
}

function storefrontCheckoutFillSuccessfulCard(mixed $page): mixed
{
    return $page
        ->fill('input[wire\\:model="cardNumber"]', '4242 4242 4242 4242')
        ->fill('input[wire\\:model="cardName"]', 'Test Buyer')
        ->fill('input[wire\\:model="cardExpiry"]', '12/28')
        ->fill('input[wire\\:model="cardCvc"]', '123');
}

function storefrontCheckoutApplyCartDiscount(string $code): mixed
{
    return visit('/products/classic-cotton-t-shirt', storefrontCheckoutHost())
        ->click('M')
        ->wait(1)
        ->click('Black')
        ->wait(1)
        ->click('Add to cart')
        ->wait(1)
        ->navigate('/cart')
        ->wait(1)
        ->fill('input[wire\\:model="discountCode"]', $code)
        ->click('button:has-text("Apply")')
        ->wait(1);
}

test('completes full checkout with credit card', function (): void {
    storefrontCheckoutFillSuccessfulCard(storefrontCheckoutReachPayment())
        ->assertSee('29.98')
        ->click('button:has-text("Pay now")')
        ->wait(2)
        ->assertPathBeginsWith('/checkout/confirmation')
        ->assertSee('Thank you')
        ->assertSee('#1001')
        ->assertNoJavaScriptErrors();
});

test('shows shipping methods based on german address', function (): void {
    storefrontCheckoutReachShipping([
        'email' => 'test@example.com',
        'first_name' => 'Hans',
        'last_name' => 'Mueller',
        'address1' => 'Berliner Str. 10',
        'city' => 'Munich',
        'postal_code' => '80331',
        'country' => 'DE',
    ])
        ->assertSee('Standard Shipping')
        ->assertSee('4.99')
        ->assertNoJavaScriptErrors();
});

test('shows international shipping methods for non german address', function (): void {
    storefrontCheckoutReachShipping([
        'email' => 'test@example.com',
        'first_name' => 'John',
        'last_name' => 'Smith',
        'address1' => '123 Main St',
        'city' => 'New York',
        'postal_code' => '10001',
        'country' => 'US',
    ])
        ->assertSee('International Shipping')
        ->assertSee('14.99')
        ->assertNoJavaScriptErrors();
});

test('applies discount during checkout', function (): void {
    $page = storefrontCheckoutApplyCartDiscount('FLAT5')
        ->assertSee('FLAT5')
        ->click('main button:has-text("Checkout")')
        ->wait(1)
        ->assertPathIs('/checkout');

    storefrontCheckoutSubmitAddress(storefrontCheckoutFillAddress($page))
        ->click('button:has-text("Standard Shipping")')
        ->wait(1)
        ->click('button[wire\\:click="selectShippingMethod"]')
        ->wait(1)
        ->assertSee('FLAT5')
        ->assertSee('5.00')
        ->assertSee('24.98')
        ->assertNoJavaScriptErrors();
});

test('validates required contact email', function (): void {
    storefrontCheckoutStart()
        ->click('form[wire\\:submit="saveAddress"] button[type="submit"]')
        ->wait(1)
        ->assertSee('email field is required')
        ->assertNoJavaScriptErrors();
});

test('validates required shipping address fields', function (): void {
    storefrontCheckoutStart()
        ->fill('input[wire\\:model="email"]', 'test@example.com')
        ->click('form[wire\\:submit="saveAddress"] button[type="submit"]')
        ->wait(1)
        ->assertSee('first name field is required')
        ->assertSee('last name field is required')
        ->assertSee('address1 field is required')
        ->assertSee('city field is required')
        ->assertSee('postal code field is required')
        ->assertNoJavaScriptErrors();
});

test('validates invalid postal code format', function (): void {
    storefrontCheckoutSubmitAddress(storefrontCheckoutFillAddress(storefrontCheckoutStart(), [
        'postal_code' => 'INVALID',
    ]))
        ->assertSee('postal code format is invalid')
        ->assertNoJavaScriptErrors();
});

test('prevents checkout with empty cart', function (): void {
    visit('/cart', storefrontCheckoutHost())
        ->assertSee('Your cart is empty')
        ->assertButtonDisabled('main button:has-text("Checkout")')
        ->assertNoJavaScriptErrors();
});

test('completes checkout with paypal', function (): void {
    storefrontCheckoutReachPayment()
        ->select('select[wire\\:model\\.live="paymentMethod"]', 'paypal')
        ->wait(1)
        ->assertSee('Pay with PayPal')
        ->click('button:has-text("Pay with PayPal")')
        ->wait(2)
        ->assertPathBeginsWith('/checkout/confirmation')
        ->assertSee('Thank you')
        ->assertSee('PayPal')
        ->assertNoJavaScriptErrors();
});

test('completes checkout with bank transfer', function (): void {
    storefrontCheckoutReachPayment()
        ->select('select[wire\\:model\\.live="paymentMethod"]', 'bank_transfer')
        ->wait(1)
        ->assertSee('bank transfer instructions')
        ->click('button:has-text("Place order")')
        ->wait(2)
        ->assertPathBeginsWith('/checkout/confirmation')
        ->assertSee('Thank you')
        ->assertSee('IBAN')
        ->assertSee('BIC')
        ->assertSee('Reference')
        ->assertSee('#1001')
        ->assertNoJavaScriptErrors();
});

test('shows error for declined credit card', function (): void {
    storefrontCheckoutReachPayment()
        ->fill('input[wire\\:model="cardNumber"]', '4000 0000 0000 0002')
        ->fill('input[wire\\:model="cardName"]', 'Test Buyer')
        ->fill('input[wire\\:model="cardExpiry"]', '12/28')
        ->fill('input[wire\\:model="cardCvc"]', '123')
        ->click('button:has-text("Pay now")')
        ->wait(2)
        ->assertPathIs('/checkout')
        ->assertSee('declined')
        ->assertNoJavaScriptErrors();
});

test('shows error for insufficient funds', function (): void {
    storefrontCheckoutReachPayment()
        ->fill('input[wire\\:model="cardNumber"]', '4000 0000 0000 9995')
        ->fill('input[wire\\:model="cardName"]', 'Test Buyer')
        ->fill('input[wire\\:model="cardExpiry"]', '12/28')
        ->fill('input[wire\\:model="cardCvc"]', '123')


command: sed -n '1,260p' resources/views/livewire/storefront/cart-drawer.blade.php

aggregated_output:
<flux:modal name="cart-drawer" class="md:w-[32rem]">

                <flux:heading size="lg">Cart</flux:heading>
                <flux:text class="mt-1">{{ $lineCount }} {{ Str::plural('item', $lineCount) }}</flux:text>

            <flux:button :href="route('cart.show')" wire:navigate variant="subtle" size="sm" icon="shopping-bag">
                View cart
            </flux:button>

        @if ($lines->isEmpty())

                <flux:icon name="shopping-bag" class="mx-auto size-10 text-zinc-400 dark:text-zinc-600" />

Your cart is empty.

                <flux:button :href="route('collections.index')" wire:navigate variant="primary" class="mt-5">
                    Browse products
                </flux:button>

        @else

                @foreach ($lines as $line)

getKey() }}">
                        variant->product->handle) }}" wire:navigate class="flex size-16 shrink-0 items-center justify-center rounded-md border border-zinc-200 bg-zinc-100 text-zinc-400 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-600">
                            <flux:icon name="shopping-bag" class="size-6" />

                                    variant->product->handle) }}" wire:navigate class="line-clamp-2 text-sm font-medium text-zinc-950 hover:underline dark:text-white">
                                        {{ $line->variant->product->title }}

                                        {{ $line->variant->optionValues->map(fn ($value) => $value->option->name.': '.$value->value)->implode(' / ') ?: ($line->variant->sku ?: 'Default') }}

                                <x-storefront.price :amount="$line->line_total_amount" :currency="$cart->currency" class="justify-end text-sm" />

                                    <button type="button" wire:click="decreaseQuantity({{ $line->getKey() }})" class="flex size-9 items-center justify-center border-r border-zinc-200 text-sm dark:border-zinc-700" aria-label="Decrease {{ $line->variant->product->title }} quantity">-</button>
                                    {{ $line->quantity }}
                                    <button type="button" wire:click="increaseQuantity({{ $line->getKey() }})" class="flex size-9 items-center justify-center border-l border-zinc-200 text-sm dark:border-zinc-700" aria-label="Increase {{ $line->variant->product->title }} quantity">+</button>

                                <flux:button wire:click="removeLine({{ $line->getKey() }})" variant="ghost" size="sm" icon="trash" aria-label="Remove {{ $line->variant->product->title }}" />

                @endforeach

                    Subtotal
                    <x-storefront.price :amount="$subtotal" :currency="$cart->currency" />

                <flux:button wire:click="checkout" variant="primary" class="w-full">
                    Checkout
                </flux:button>

        @endif

</flux:modal>


command: sed -n '1,260p' resources/views/layouts/storefront.blade.php

aggregated_output:
<!DOCTYPE html>
getLocale()) }}">

        @include('partials.head')

        @php($store = app()->bound('current_store') ? app('current_store') : null)
        @php($announcement = data_get($themeSettings ?? [], 'announcement', []))
        @php($mainLinks = ($mainNavigation ?? []) !== [] ? $mainNavigation : [['label' => 'Collections', 'url' => route('collections.index'), 'external' => false], ['label' => 'Search', 'url' => route('search.index'), 'external' => false]])
        @php($footerLinks = ($footerNavigation ?? []) !== [] ? $footerNavigation : [['label' => 'Collections', 'url' => route('collections.index'), 'external' => false], ['label' => 'Search', 'url' => route('search.index'), 'external' => false]])

            Skip to main content

        @if (data_get($announcement, 'enabled', false))

                @if (data_get($announcement, 'url'))

                        {{ data_get($announcement, 'text') }}

                @else
                    {{ data_get($announcement, 'text') }}
                @endif

        @endif

         data_get($themeSettings ?? [], 'header.sticky', true),
        ])>

                    {{ $store?->name ?? config('app.name') }}

                    @foreach ($mainLinks as $item)
                        @if (($item['children'] ?? []) !== [])

                                    {{ $item['label'] }}
                                    <flux:icon name="chevron-down" class="size-4" />

                                    @foreach ($item['children'] as $child)

                                            {{ $child['label'] }}

                                    @endforeach

                        @else

                                {{ $item['label'] }}

                        @endif
                    @endforeach

                    <flux:button :href="route('search.index')" wire:navigate variant="subtle" icon="magnifying-glass" aria-label="Search" />
                    <flux:button :href="route('account.dashboard')" wire:navigate variant="subtle" icon="user" aria-label="Account" />
                    <flux:modal.trigger name="cart-drawer">
                        <flux:button variant="subtle" icon="shopping-bag" aria-label="Cart" />
                    </flux:modal.trigger>

        <livewire:storefront.cart-drawer />

            {{ $slot }}

{{ $store?->name ?? config('app.name') }}

{{ data_get($themeSettings ?? [], 'footer.tagline') }}

Shop

                        @foreach ($footerLinks as $item)

                                {{ $item['label'] }}

                            @foreach (($item['children'] ?? []) as $child)

                                    {{ $child['label'] }}

                            @endforeach
                        @endforeach

Customer

                        Account
                        About

        @fluxScripts


command:
/bin/zsh
-lc
git status --short

type: unknown

cmd: git status --short


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


output:
Chunk ID: 3ea986
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1752
Output:
<!DOCTYPE html>
getLocale()) }}">

        @include('partials.head')

        @php($store = app()->bound('current_store') ? app('current_store') : null)
        @php($announcement = data_get($themeSettings ?? [], 'announcement', []))
        @php($mainLinks = ($mainNavigation ?? []) !== [] ? $mainNavigation : [['label' => 'Collections', 'url' => route('collections.index'), 'external' => false], ['label' => 'Search', 'url' => route('search.index'), 'external' => false]])
        @php($footerLinks = ($footerNavigation ?? []) !== [] ? $footerNavigation : [['label' => 'Collections', 'url' => route('collections.index'), 'external' => false], ['label' => 'Search', 'url' => route('search.index'), 'external' => false]])

            Skip to main content

        @if (data_get($announcement, 'enabled', false))

                @if (data_get($announcement, 'url'))

                        {{ data_get($announcement, 'text') }}

                @else
                    {{ data_get($announcement, 'text') }}
                @endif

        @endif

         data_get($themeSettings ?? [], 'header.sticky', true),
        ])>

                    {{ $store?->name ?? config('app.name') }}

                    @foreach ($mainLinks as $item)
                        @if (($item['children'] ?? []) !== [])

                                    {{ $item['label'] }}
                                    <flux:icon name="chevron-down" class="size-4" />

                                    @foreach ($item['children'] as $child)

                                            {{ $child['label'] }}

                                    @endforeach

                        @else

                                {{ $item['label'] }}

                        @endif
                    @endforeach

                    <flux:button :href="route('search.index')" wire:navigate variant="subtle" icon="magnifying-glass" aria-label="Search" />
                    <flux:button :href="route('account.dashboard')" wire:navigate variant="subtle" icon="user" aria-label="Account" />
                    <flux:modal.trigger name="cart-drawer">
                        <flux:button variant="subtle" icon="shopping-bag" aria-label="Cart" />
                    </flux:modal.trigger>

        <livewire:storefront.cart-drawer />

            {{ $slot }}

{{ $store?->name ?? config('app.name') }}

{{ data_get($themeSettings ?? [], 'footer.tagline') }}

Shop

                        @foreach ($footerLinks as $item)

                                {{ $item['label'] }}

                            @foreach (($item['children'] ?? []) as $child)

                                    {{ $child['label'] }}

                            @endforeach
                        @endforeach

Customer

                        Account
                        About

        @fluxScripts


output:
Chunk ID: daa716
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1154
Output:
<flux:modal name="cart-drawer" class="md:w-[32rem]">

                <flux:heading size="lg">Cart</flux:heading>
                <flux:text class="mt-1">{{ $lineCount }} {{ Str::plural('item', $lineCount) }}</flux:text>

            <flux:button :href="route('cart.show')" wire:navigate variant="subtle" size="sm" icon="shopping-bag">
                View cart
            </flux:button>

        @if ($lines->isEmpty())

                <flux:icon name="shopping-bag" class="mx-auto size-10 text-zinc-400 dark:text-zinc-600" />

Your cart is empty.

                <flux:button :href="route('collections.index')" wire:navigate variant="primary" class="mt-5">
                    Browse products
                </flux:button>

        @else

                @foreach ($lines as $line)

getKey() }}">
                        variant->product->handle) }}" wire:navigate class="flex size-16 shrink-0 items-center justify-center rounded-md border border-zinc-200 bg-zinc-100 text-zinc-400 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-600">
                            <flux:icon name="shopping-bag" class="size-6" />

                                    variant->product->handle) }}" wire:navigate class="line-clamp-2 text-sm font-medium text-zinc-950 hover:underline dark:text-white">
                                        {{ $line->variant->product->title }}

                                        {{ $line->variant->optionValues->map(fn ($value) => $value->option->name.': '.$value->value)->implode(' / ') ?: ($line->variant->sku ?: 'Default') }}

                                <x-storefront.price :amount="$line->line_total_amount" :currency="$cart->currency" class="justify-end text-sm" />

                                    <button type="button" wire:click="decreaseQuantity({{ $line->getKey() }})" class="flex size-9 items-center justify-center border-r border-zinc-200 text-sm dark:border-zinc-700" aria-label="Decrease {{ $line->variant->product->title }} quantity">-</button>
                                    {{ $line->quantity }}
                                    <button type="button" wire:click="increaseQuantity({{ $line->getKey() }})" class="flex size-9 items-center justify-center border-l border-zinc-200 text-sm dark:border-zinc-700" aria-label="Increase {{ $line->variant->product->title }} quantity">+</button>

                                <flux:button wire:click="removeLine({{ $line->getKey() }})" variant="ghost" size="sm" icon="trash" aria-label="Remove {{ $line->variant->product->title }}" />

                @endforeach

                    Subtotal
                    <x-storefront.price :amount="$subtotal" :currency="$cart->currency" />

                <flux:button wire:click="checkout" variant="primary" class="w-full">
                    Checkout
                </flux:button>

        @endif

</flux:modal>


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

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 storefrontCheckoutHost(): array
{
    return ['host' => 'shop.test'];
}

function storefrontCheckoutStart(): mixed
{
    return visit('/products/classic-cotton-t-shirt', storefrontCheckoutHost())
        ->click('M')
        ->wait(1)
        ->click('Black')
        ->wait(1)
        ->click('Add to cart')
        ->wait(1)
        ->navigate('/cart')
        ->wait(1)
        ->click('main button:has-text("Checkout")')
        ->wait(1)
        ->assertPathIs('/checkout')
        ->assertSee('Checkout');
}

/**
 * @param  array<string, string>  $overrides
 */
function storefrontCheckoutFillAddress(mixed $page, array $overrides = []): mixed
{
    $address = [
        'email' => 'test-buyer@example.com',
        'first_name' => 'Test',
        'last_name' => 'Buyer',
        'address1' => 'Teststrasse 1',
        'city' => 'Berlin',
        'postal_code' => '10115',
        'country' => 'DE',
    ];

    $address = array_replace($address, $overrides);

    return $page
        ->fill('input[wire\\:model="email"]', $address['email'])
        ->fill('input[wire\\:model="shippingAddress.first_name"]', $address['first_name'])
        ->fill('input[wire\\:model="shippingAddress.last_name"]', $address['last_name'])
        ->fill('input[wire\\:model="shippingAddress.address1"]', $address['address1'])
        ->fill('input[wire\\:model="shippingAddress.city"]', $address['city'])
        ->fill('input[wire\\:model="shippingAddress.postal_code"]', $address['postal_code'])
        ->select('select[wire\\:model="shippingAddress.country"]', $address['country']);
}

function storefrontCheckoutSubmitAddress(mixed $page): mixed
{
    return $page
        ->click('form[wire\\:submit="saveAddress"] button[type="submit"]')
        ->wait(1);
}

function storefrontCheckoutReachShipping(array $address = []): mixed
{
    return storefrontCheckoutSubmitAddress(
        storefrontCheckoutFillAddress(storefrontCheckoutStart(), $address),
    );
}

function storefrontCheckoutReachPayment(array $address = []): mixed
{
    return storefrontCheckoutReachShipping($address)
        ->click('button:has-text("Standard Shipping")')
        ->wait(1)
        ->click('button[wire\\:click="selectShippingMethod"]')
        ->wait(1)
        ->assertSee('Payment')
        ->assertSee('Pay now');
}

function storefrontCheckoutFillSuccessfulCard(mixed $page): mixed
{
    return $page
        ->fill('input[wire\\:model="cardNumber"]', '4242 4242 4242 4242')
        ->fill('input[wire\\:model="cardName"]', 'Test Buyer')
        ->fill('input[wire\\:model="cardExpiry"]', '12/28')
        ->fill('input[wire\\:model="cardCvc"]', '123');
}

function storefrontCheckoutApplyCartDiscount(string $code): mixed
{
    return visit('/products/classic-cotton-t-shirt', storefrontCheckoutHost())
        ->click('M')
        ->wait(1)
        ->click('Black')
        ->wait(1)
        ->click('Add to cart')
        ->wait(1)
        ->navigate('/cart')
        ->wait(1)
        ->fill('input[wire\\:model="discountCode"]', $code)
        ->click('button:has-text("Apply")')
        ->wait(1);
}

test('completes full checkout with credit card', function (): void {
    storefrontCheckoutFillSuccessfulCard(storefrontCheckoutReachPayment())
        ->assertSee('29.98')
        ->click('button:has-text("Pay now")')
        ->wait(2)
        ->assertPathBeginsWith('/checkout/confirmation')
        ->assertSee('Thank you')
        ->assertSee('#1001')
        ->assertNoJavaScriptErrors();
});

test('shows shipping methods based on german address', function (): void {
    storefrontCheckoutReachShipping([
        'email' => 'test@example.com',
        'first_name' => 'Hans',
        'last_name' => 'Mueller',
        'address1' => 'Berliner Str. 10',
        'city' => 'Munich',
        'postal_code' => '80331',
        'country' => 'DE',
    ])
        ->assertSee('Standard Shipping')
        ->assertSee('4.99')
        ->assertNoJavaScriptErrors();
});

test('shows international shipping methods for non german address', function (): void {
    storefrontCheckoutReachShipping([
        'email' => 'test@example.com',
        'first_name' => 'John',
        'last_name' => 'Smith',
        'address1' => '123 Main St',
        'city' => 'New York',
        'postal_code' => '10001',
        'country' => 'US',
    ])
        ->assertSee('International Shipping')
        ->assertSee('14.99')
        ->assertNoJavaScriptErrors();
});

test('applies discount during checkout', function (): void {
    $page = storefrontCheckoutApplyCartDiscount('FLAT5')
        ->assertSee('FLAT5')
        ->click('main button:has-text("Checkout")')
        ->wait(1)
        ->assertPathIs('/checkout');

    storefrontCheckoutSubmitAddress(storefrontCheckoutFillAddress($page))
        ->click('button:has-text("Standard Shipping")')
        ->wait(1)
        ->click('button[wire\\:click="selectShippingMethod"]')
        ->wait(1)
        ->assertSee('FLAT5')
        ->assertSee('5.00')
        ->assertSee('24.98')
        ->assertNoJavaScriptErrors();
});

test('validates required contact email', function (): void {
    storefrontCheckoutStart()
        ->click('form[wire\\:submit="saveAddress"] button[type="submit"]')
        ->wait(1)
        ->assertSee('email field is required')
        ->assertNoJavaScriptErrors();
});

test('validates required shipping address fields', function (): void {
    storefrontCheckoutStart()
        ->fill('input[wire\\:model="email"]', 'test@example.com')
        ->click('form[wire\\:submit="saveAddress"] button[type="submit"]')
        ->wait(1)
        ->assertSee('first name field is required')
        ->assertSee('last name field is required')
        ->assertSee('address1 field is required')
        ->assertSee('city field is required')
        ->assertSee('postal code field is required')
        ->assertNoJavaScriptErrors();
});

test('validates invalid postal code format', function (): void {
    storefrontCheckoutSubmitAddress(storefrontCheckoutFillAddress(storefrontCheckoutStart(), [
        'postal_code' => 'INVALID',
    ]))
        ->assertSee('postal code format is invalid')
        ->assertNoJavaScriptErrors();
});

test('prevents checkout with empty cart', function (): void {
    visit('/cart', storefrontCheckoutHost())
        ->assertSee('Your cart is empty')
        ->assertButtonDisabled('main button:has-text("Checkout")')
        ->assertNoJavaScriptErrors();
});

test('completes checkout with paypal', function (): void {
    storefrontCheckoutReachPayment()
        ->select('select[wire\\:model\\.live="paymentMethod"]', 'paypal')
        ->wait(1)
        ->assertSee('Pay with PayPal')
        ->click('button:has-text("Pay with PayPal")')
        ->wait(2)
        ->assertPathBeginsWith('/checkout/confirmation')
        ->assertSee('Thank you')
        ->assertSee('PayPal')
        ->assertNoJavaScriptErrors();
});

test('completes checkout with bank transfer', function (): void {
    storefrontCheckoutReachPayment()
        ->select('select[wire\\:model\\.live="paymentMethod"]', 'bank_transfer')
        ->wait(1)
        ->assertSee('bank transfer instructions')
        ->click('button:has-text("Place order")')
        ->wait(2)
        ->assertPathBeginsWith('/checkout/confirmation')
        ->assertSee('Thank you')
        ->assertSee('IBAN')
        ->assertSee('BIC')
        ->assertSee('Reference')
        ->assertSee('#1001')
        ->assertNoJavaScriptErrors();
});

test('shows error for declined credit card', function (): void {
    storefrontCheckoutReachPayment()
        ->fill('input[wire\\:model="cardNumber"]', '4000 0000 0000 0002')
        ->fill('input[wire\\:model="cardName"]', 'Test Buyer')
        ->fill('input[wire\\:model="cardExpiry"]', '12/28')
        ->fill('input[wire\\:model="cardCvc"]', '123')
        ->click('button:has-text("Pay now")')
        ->wait(2)
        ->assertPathIs('/checkout')
        ->assertSee('declined')
        ->assertNoJavaScriptErrors();
});

test('shows error for insufficient funds', function (): void {
    storefrontCheckoutReachPayment()
        ->fill('input[wire\\:model="cardNumber"]', '4000 0000 0000 9995')
        ->fill('input[wire\\:model="cardName"]', 'Test Buyer')
        ->fill('input[wire\\:model="cardExpiry"]', '12/28')
        ->fill('input[wire\\:model="cardCvc"]', '123')


command: sed -n '1,220p' tests/Browser/Admin/AuthenticationTest.php

aggregated_output:
<?php

use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

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

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

function adminAuthenticationLogin(): mixed
{
    return visit('/admin/login', adminAuthenticationHost())
        ->fill('input[type=email]', 'admin@acme.test')
        ->fill('input[type=password]', 'password')
        ->click('@admin-login-button')
        ->wait(1)
        ->assertPathIs('/admin')
        ->assertSee('Dashboard')
        ->assertNoJavaScriptErrors();
}

function adminAuthenticationSubmitWithoutBrowserRequired(string $email, string $password): mixed
{
    $page = visit('/admin/login', adminAuthenticationHost());

    $page->script('() => document.querySelectorAll("[required]").forEach((element) => element.removeAttribute("required"))');

    if ($email !== '') {
        $page->fill('input[type=email]', $email);
    }

    if ($password !== '') {
        $page->fill('input[type=password]', $password);
    }

    return $page->click('@admin-login-button')->wait(1);
}

test('can log in as admin', function (): void {
    adminAuthenticationLogin();
});

test('shows error for invalid credentials', function (): void {
    visit('/admin/login', adminAuthenticationHost())
        ->fill('input[type=email]', 'admin@acme.test')
        ->fill('input[type=password]', 'wrongpassword')
        ->click('@admin-login-button')
        ->wait(1)
        ->assertPathIs('/admin/login')
        ->assertSee('Invalid credentials')
        ->assertNoJavaScriptErrors();
});

test('shows error for empty email', function (): void {
    adminAuthenticationSubmitWithoutBrowserRequired('', 'password')
        ->assertPathIs('/admin/login')
        ->assertSee('email field is required')
        ->assertNoJavaScriptErrors();
});

test('shows error for empty password', function (): void {
    adminAuthenticationSubmitWithoutBrowserRequired('admin@acme.test', '')
        ->assertPathIs('/admin/login')
        ->assertSee('password field is required')
        ->assertNoJavaScriptErrors();
});

test('redirects unauthenticated users to login from dashboard', function (): void {
    visit('/admin', adminAuthenticationHost())
        ->wait(1)
        ->assertPathIs('/admin/login')
        ->assertSee('Sign in')
        ->assertNoJavaScriptErrors();
});

test('redirects unauthenticated users to login from products', function (): void {
    visit('/admin/products', adminAuthenticationHost())
        ->wait(1)
        ->assertPathIs('/admin/login')
        ->assertSee('Sign in')
        ->assertNoJavaScriptErrors();
});

test('can log out', function (): void {
    adminAuthenticationLogin()
        ->click('@sidebar-menu-button')
        ->click('button[data-test="logout-button"]:visible')
        ->wait(1)
        ->assertPathIs('/admin/login')
        ->assertSee('Sign in')
        ->assertNoJavaScriptErrors();
});

test('can navigate through admin sidebar sections', function (): void {
    $page = adminAuthenticationLogin();

    foreach ([
        '/admin/products' => 'Products',
        '/admin/orders' => 'Orders',
        '/admin/customers' => 'Customers',
        '/admin/discounts' => 'Discounts',
        '/admin/settings' => 'Store Settings',
    ] as $path => $heading) {
        $page->click("a[href$=\"{$path}\"]")
            ->wait(1)
            ->assertPathIs($path)
            ->assertSee($heading)
            ->assertNoJavaScriptErrors();
    }
});

test('can navigate to analytics from sidebar', function (): void {
    adminAuthenticationLogin()
        ->click('a[href$="/admin/analytics"]')
        ->wait(1)
        ->assertPathIs('/admin/analytics')
        ->assertSee('Analytics')
        ->assertNoJavaScriptErrors();
});

test('can navigate to themes from sidebar', function (): void {
    adminAuthenticationLogin()
        ->click('a[href$="/admin/themes"]')
        ->wait(1)
        ->assertPathIs('/admin/themes')
        ->assertSee('Themes')
        ->assertNoJavaScriptErrors();
});


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

use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

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

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

function adminAuthenticationLogin(): mixed
{
    return visit('/admin/login', adminAuthenticationHost())
        ->fill('input[type=email]', 'admin@acme.test')
        ->fill('input[type=password]', 'password')
        ->click('@admin-login-button')
        ->wait(1)
        ->assertPathIs('/admin')
        ->assertSee('Dashboard')
        ->assertNoJavaScriptErrors();
}

function adminAuthenticationSubmitWithoutBrowserRequired(string $email, string $password): mixed
{
    $page = visit('/admin/login', adminAuthenticationHost());

    $page->script('() => document.querySelectorAll("[required]").forEach((element) => element.removeAttribute("required"))');

    if ($email !== '') {
        $page->fill('input[type=email]', $email);
    }

    if ($password !== '') {
        $page->fill('input[type=password]', $password);
    }

    return $page->click('@admin-login-button')->wait(1);
}

test('can log in as admin', function (): void {
    adminAuthenticationLogin();
});

test('shows error for invalid credentials', function (): void {
    visit('/admin/login', adminAuthenticationHost())
        ->fill('input[type=email]', 'admin@acme.test')
        ->fill('input[type=password]', 'wrongpassword')
        ->click('@admin-login-button')
        ->wait(1)
        ->assertPathIs('/admin/login')
        ->assertSee('Invalid credentials')
        ->assertNoJavaScriptErrors();
});

test('shows error for empty email', function (): void {
    adminAuthenticationSubmitWithoutBrowserRequired('', 'password')
        ->assertPathIs('/admin/login')
        ->assertSee('email field is required')
        ->assertNoJavaScriptErrors();
});

test('shows error for empty password', function (): void {
    adminAuthenticationSubmitWithoutBrowserRequired('admin@acme.test', '')
        ->assertPathIs('/admin/login')
        ->assertSee('password field is required')
        ->assertNoJavaScriptErrors();
});

test('redirects unauthenticated users to login from dashboard', function (): void {
    visit('/admin', adminAuthenticationHost())
        ->wait(1)
        ->assertPathIs('/admin/login')
        ->assertSee('Sign in')
        ->assertNoJavaScriptErrors();
});

test('redirects unauthenticated users to login from products', function (): void {
    visit('/admin/products', adminAuthenticationHost())
        ->wait(1)
        ->assertPathIs('/admin/login')
        ->assertSee('Sign in')
        ->assertNoJavaScriptErrors();
});

test('can log out', function (): void {
    adminAuthenticationLogin()
        ->click('@sidebar-menu-button')
        ->click('button[data-test="logout-button"]:visible')
        ->wait(1)
        ->assertPathIs('/admin/login')
        ->assertSee('Sign in')
        ->assertNoJavaScriptErrors();
});

test('can navigate through admin sidebar sections', function (): void {
    $page = adminAuthenticationLogin();

    foreach ([
        '/admin/products' => 'Products',
        '/admin/orders' => 'Orders',
        '/admin/customers' => 'Customers',
        '/admin/discounts' => 'Discounts',
        '/admin/settings' => 'Store Settings',
    ] as $path => $heading) {
        $page->click("a[href$=\"{$path}\"]")
            ->wait(1)
            ->assertPathIs($path)
            ->assertSee($heading)
            ->assertNoJavaScriptErrors();
    }
});

test('can navigate to analytics from sidebar', function (): void {
    adminAuthenticationLogin()
        ->click('a[href$="/admin/analytics"]')
        ->wait(1)
        ->assertPathIs('/admin/analytics')
        ->assertSee('Analytics')
        ->assertNoJavaScriptErrors();
});

test('can navigate to themes from sidebar', function (): void {
    adminAuthenticationLogin()
        ->click('a[href$="/admin/themes"]')
        ->wait(1)
        ->assertPathIs('/admin/themes')
        ->assertSee('Themes')
        ->assertNoJavaScriptErrors();
});


command: rg -n "resize\(|assertScript|script\(" tests/Browser vendor/pestphp -g '*.php'

type: search

cmd: rg -n "resize\\(|assertScript|script\\(" tests/Browser vendor/pestphp -g '*.php'

query: resize\(|assertScript|script\(

path: Browser

aggregated_output:
tests/Browser/Admin/AuthenticationTest.php:33:    $page->script('() => document.querySelectorAll("[required]").forEach((element) => element.removeAttribute("required"))');
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/InteractsWithViewPort.php:12:    public function resize(int $width, int $height): self
vendor/pestphp/pest-plugin-browser/src/Api/TestableLivewire.php:47:            $actual = $this->awaitablePage->script(<<<JS
vendor/pestphp/pest-plugin-browser/src/Api/TestableLivewire.php:63:            $actual = $this->awaitablePage->script(<<<JS
vendor/pestphp/pest-plugin-browser/src/Api/Webpage.php:85:    public function script(string $content): mixed
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/MakesElementAssertions.php:155:    public function assertScript(string $expression, mixed $expected = true): Webpage


output:
Chunk ID: 6f3e35
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 210
Output:
tests/Browser/Admin/AuthenticationTest.php:33:    $page->script('() => document.querySelectorAll("[required]").forEach((element) => element.removeAttribute("required"))');
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/InteractsWithViewPort.php:12:    public function resize(int $width, int $height): self
vendor/pestphp/pest-plugin-browser/src/Api/TestableLivewire.php:47:            $actual = $this->awaitablePage->script(<<<JS
vendor/pestphp/pest-plugin-browser/src/Api/TestableLivewire.php:63:            $actual = $this->awaitablePage->script(<<<JS
vendor/pestphp/pest-plugin-browser/src/Api/Webpage.php:85:    public function script(string $content): mixed
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/MakesElementAssertions.php:155:    public function assertScript(string $expression, mixed $expected = true): Webpage


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


cmd: sed -n '60,115p' vendor/pestphp/pest-plugin-browser/src/Api/Webpage.php


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


cmd: rg -n "variant=\"bare\"|flux:modal.trigger|name=\".*navigation|sidebar-menu-button|mobile" resources/views tests/Browser -g '*.blade.php' -g '*.php'


cmd: sed -n '1,260p' resources/views/components/layouts/app/sidebar.blade.php 2>/dev/null || sed -n '1,260p' resources/views/layouts/app.blade.php


cmd: sed -n '1,260p' tests/Browser/Storefront/CollectionTest.php


command: sed -n '60,115p' vendor/pestphp/pest-plugin-browser/src/Api/Webpage.php

aggregated_output:
    {
        return $this->page->content();
    }

    /**
     * Gets the page's URL.
     */
    public function url(): string
    {
        return $this->page->url();
    }

    /**
     * Submits the first form found on the page.
     */
    public function submit(): self
    {
        $this->guessLocator('[type="submit"]')->click();

        return $this;
    }

    /**
     * Executes a script in the context of the page.
     */
    public function script(string $content): mixed
    {
        return $this->page->evaluate($content);
    }

    /**
     * Gets the page instance.
     */
    public function value(string $selector): string
    {
        return $this->guessLocator($selector)->inputValue();
    }

    /**
     * Gets the locator for the given selector.
     */
    private function guessLocator(string $selector, ?string $value = null): Locator
    {
        return (new GuessLocator($this->page))->for($selector, $value);
    }
}


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

aggregated_output:
<?php

declare(strict_types=1);

namespace Pest\Browser\Api\Concerns;

trait InteractsWithViewPort
{
    /**
     * Resizes the page.
     */
    public function resize(int $width, int $height): self
    {
        $this->page->setViewportSize($width, $height);

        return $this;
    }
}


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

declare(strict_types=1);

namespace Pest\Browser\Api\Concerns;

trait InteractsWithViewPort
{
    /**
     * Resizes the page.
     */
    public function resize(int $width, int $height): self
    {
        $this->page->setViewportSize($width, $height);

        return $this;
    }
}


output:
Chunk ID: 2a84d3
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 240
Output:
    {
        return $this->page->content();
    }

    /**
     * Gets the page's URL.
     */
    public function url(): string
    {
        return $this->page->url();
    }

    /**
     * Submits the first form found on the page.
     */
    public function submit(): self
    {
        $this->guessLocator('[type="submit"]')->click();

        return $this;
    }

    /**
     * Executes a script in the context of the page.
     */
    public function script(string $content): mixed
    {
        return $this->page->evaluate($content);
    }

    /**
     * Gets the page instance.
     */
    public function value(string $selector): string
    {
        return $this->guessLocator($selector)->inputValue();
    }

    /**
     * Gets the locator for the given selector.
     */
    private function guessLocator(string $selector, ?string $value = null): Locator
    {
        return (new GuessLocator($this->page))->for($selector, $value);
    }
}


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

aggregated_output:
     * Assert that no text is present within the selector.
     */
    public function assertSeeNothingIn(string $selector): Webpage
    {
        $text = $this->guessLocator($selector)->textContent();

        expect($text)->toBeEmpty("Expected element [{$selector}] to be empty on the page initially with the url [{$this->initialUrl}], but it contained text: [{$text}].");

        return $this;
    }

    /**
     * Assert that a given element is present a given amount of times.
     */
    public function assertCount(string $selector, int $expected): Webpage
    {
        $count = $this->guessLocator($selector)->count();
        expect($count)->toBe($expected, "Expected to find {$expected} elements matching [{$selector}] on the page initially with the url [{$this->initialUrl}], but found {$count}.");

        return $this;
    }

    /**
     * Assert that the given JavaScript expression evaluates to the given value.
     */
    public function assertScript(string $expression, mixed $expected = true): Webpage
    {
        if (! self::strContainsAny($expression, ['===', '!==', '==', '!=', '>', '<', '>=', '<=', '&&', '||']) && ! str_starts_with($expression, 'return ') && ! str_starts_with($expression, 'function')) {
            $expression = "function() { return {$expression}; }";
        }

        $result = $this->page->evaluate($expression);

        if (is_bool($expected)) {
            $expectedStr = $expected ? 'true' : 'false';
        } elseif (is_string($expected)) {
            $expectedStr = "'{$expected}'";
        } elseif (is_scalar($expected) || is_null($expected)) {
            $expectedStr = (string) $expected;
        } else {
            $expectedStr = gettype($expected);
        }

        // Format result value for display
        if (is_bool($result)) {
            $resultStr = $result ? 'true' : 'false';
        } elseif (is_string($result)) {
            $resultStr = "'{$result}'";
        } elseif (is_scalar($result) || is_null($result)) {
            $resultStr = (string) $result;
        } else {


output:
Chunk ID: 307698
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 515
Output:
     * Assert that no text is present within the selector.
     */
    public function assertSeeNothingIn(string $selector): Webpage
    {
        $text = $this->guessLocator($selector)->textContent();

        expect($text)->toBeEmpty("Expected element [{$selector}] to be empty on the page initially with the url [{$this->initialUrl}], but it contained text: [{$text}].");

        return $this;
    }

    /**
     * Assert that a given element is present a given amount of times.
     */
    public function assertCount(string $selector, int $expected): Webpage
    {
        $count = $this->guessLocator($selector)->count();
        expect($count)->toBe($expected, "Expected to find {$expected} elements matching [{$selector}] on the page initially with the url [{$this->initialUrl}], but found {$count}.");

        return $this;
    }

    /**
     * Assert that the given JavaScript expression evaluates to the given value.
     */
    public function assertScript(string $expression, mixed $expected = true): Webpage
    {
        if (! self::strContainsAny($expression, ['===', '!==', '==', '!=', '>', '<', '>=', '<=', '&&', '||']) && ! str_starts_with($expression, 'return ') && ! str_starts_with($expression, 'function')) {
            $expression = "function() { return {$expression}; }";
        }

        $result = $this->page->evaluate($expression);

        if (is_bool($expected)) {
            $expectedStr = $expected ? 'true' : 'false';
        } elseif (is_string($expected)) {
            $expectedStr = "'{$expected}'";
        } elseif (is_scalar($expected) || is_null($expected)) {
            $expectedStr = (string) $expected;
        } else {
            $expectedStr = gettype($expected);
        }

        // Format result value for display
        if (is_bool($result)) {
            $resultStr = $result ? 'true' : 'false';
        } elseif (is_string($result)) {
            $resultStr = "'{$result}'";
        } elseif (is_scalar($result) || is_null($result)) {
            $resultStr = (string) $result;
        } else {


command: rg -n "variant=\"bare\"|flux:modal.trigger|name=\".*navigation|sidebar-menu-button|mobile" resources/views tests/Browser -g '*.blade.php' -g '*.php'

type: search

cmd: rg -n 'variant="bare"|flux:modal.trigger|name=".*navigation|sidebar-menu-button|mobile' resources/views tests/Browser -g '*.blade.php' -g '*.php'

query: variant=\"bare\"|flux:modal.trigger|name=\".*navigation|sidebar-menu-button|mobile

path: views

aggregated_output:
tests/Browser/Admin/AuthenticationTest.php:93:        ->click('@sidebar-menu-button')
tests/Browser/SmokeTest.php:125:test('storefront home renders on a mobile viewport', function (): void {
tests/Browser/SmokeTest.php:130:        ->mobile()
resources/views/layouts/storefront.blade.php:65:                    <flux:modal.trigger name="cart-drawer">
resources/views/layouts/storefront.blade.php:67:                    </flux:modal.trigger>
resources/views/layouts/app/sidebar.blade.php:9:        <flux:sidebar sticky collapsible="mobile" class="border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
resources/views/components/desktop-user-menu.blade.php:8:        data-test="sidebar-menu-button"
resources/views/livewire/admin/developers/index.blade.php:31:            <flux:modal.trigger name="generate-token">
resources/views/livewire/admin/developers/index.blade.php:33:            </flux:modal.trigger>
resources/views/layouts/app/header.blade.php:48:        <flux:sidebar collapsible="mobile" sticky class="lg:hidden border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
resources/views/livewire/admin/analytics/index.blade.php:28:                <flux:select.option value="mobile">Mobile</flux:select.option>
resources/views/livewire/settings/delete-user-form.blade.php:7:    <flux:modal.trigger name="confirm-user-deletion">
resources/views/livewire/settings/delete-user-form.blade.php:11:    </flux:modal.trigger>
resources/views/livewire/admin/orders/show.blade.php:22:                <flux:modal.trigger name="refund-order">
resources/views/livewire/admin/orders/show.blade.php:26:                </flux:modal.trigger>
resources/views/livewire/admin/orders/show.blade.php:30:                <flux:modal.trigger name="fulfillment-order">
resources/views/livewire/admin/orders/show.blade.php:34:                </flux:modal.trigger>


output:
Chunk ID: 0d9df1
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 472
Output:
tests/Browser/Admin/AuthenticationTest.php:93:        ->click('@sidebar-menu-button')
tests/Browser/SmokeTest.php:125:test('storefront home renders on a mobile viewport', function (): void {
tests/Browser/SmokeTest.php:130:        ->mobile()
resources/views/layouts/storefront.blade.php:65:                    <flux:modal.trigger name="cart-drawer">
resources/views/layouts/storefront.blade.php:67:                    </flux:modal.trigger>
resources/views/layouts/app/sidebar.blade.php:9:        <flux:sidebar sticky collapsible="mobile" class="border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
resources/views/components/desktop-user-menu.blade.php:8:        data-test="sidebar-menu-button"
resources/views/livewire/admin/developers/index.blade.php:31:            <flux:modal.trigger name="generate-token">
resources/views/livewire/admin/developers/index.blade.php:33:            </flux:modal.trigger>
resources/views/layouts/app/header.blade.php:48:        <flux:sidebar collapsible="mobile" sticky class="lg:hidden border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
resources/views/livewire/admin/analytics/index.blade.php:28:                <flux:select.option value="mobile">Mobile</flux:select.option>
resources/views/livewire/settings/delete-user-form.blade.php:7:    <flux:modal.trigger name="confirm-user-deletion">
resources/views/livewire/settings/delete-user-form.blade.php:11:    </flux:modal.trigger>
resources/views/livewire/admin/orders/show.blade.php:22:                <flux:modal.trigger name="refund-order">
resources/views/livewire/admin/orders/show.blade.php:26:                </flux:modal.trigger>
resources/views/livewire/admin/orders/show.blade.php:30:                <flux:modal.trigger name="fulfillment-order">
resources/views/livewire/admin/orders/show.blade.php:34:                </flux:modal.trigger>


command: sed -n '1,260p' tests/Browser/Storefront/CollectionTest.php

aggregated_output: sed: tests/Browser/Storefront/CollectionTest.php: No such file or directory

status: failed


command: sed -n '1,260p' resources/views/components/layouts/app/sidebar.blade.php 2>/dev/null || sed -n '1,260p' resources/views/layouts/app.blade.php

aggregated_output:
<x-layouts::app.sidebar :title="$title ?? null">
    <flux:main>
        {{ $slot }}
    </flux:main>
</x-layouts::app.sidebar>


output:
Chunk ID: 9b675b
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 32
Output:
<x-layouts::app.sidebar :title="$title ?? null">
    <flux:main>
        {{ $slot }}
    </flux:main>
</x-layouts::app.sidebar>


output:
Chunk ID: aa424d
Wall time: 0.0000 seconds
Process exited with code 1
Original token count: 19
Output:
sed: tests/Browser/Storefront/CollectionTest.php: No such file or directory


cmd: sed -n '1,260p' resources/views/layouts/app/sidebar.blade.php


cmd: sed -n '1,260p' resources/views/layouts/app/header.blade.php


cmd: sed -n '1,180p' tests/Browser/SmokeTest.php


cmd: rg -n "collections|filter|Classic Cotton|Add to cart|cart.show|checkout" tests/Browser/Storefront resources/views/livewire/storefront -g '*.php' -g '*.blade.php'


cmd: rg -n "assertVisible|assertPresent|assertButtonEnabled|assertButtonDisabled" vendor/pestphp/pest-plugin-browser/src tests/Browser -g '*.php'


cmd: rg -n "test\(" tests/Browser/Storefront -g '*.php'