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

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

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

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

Filters

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

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

Price

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

Product type

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

Vendor

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

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

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

            @if ($products->isEmpty())

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

No products found

Try another search term or adjust your filters.

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

            @else

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

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


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

show.blade.php

type: read

aggregated_output:
    <x-storefront.breadcrumbs :items="[
        ['label' => $product->collections->first()?->title ?? 'Products', 'url' => $product->collections->first() ? route('collections.show', $product->collections->first()->handle) : route('collections.index')],
        ['label' => $product->title],
    ]" />

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

                    @if ($selectedVariant?->compare_at_amount && $selectedVariant->compare_at_amount > $selectedVariant->price_amount)
                        Sale
                    @endif
                    @foreach ($product->tags ?? [] as $tag)
                        {{ $tag }}
                    @endforeach

{{ $product->title }}

                @if ($selectedVariant)
                    <x-storefront.price :amount="$selectedVariant->price_amount" :compare-at="$selectedVariant->compare_at_amount" :currency="$selectedVariant->currency" class="text-2xl" aria-live="polite" />
                @endif

            @if ($product->options->isNotEmpty())

                    @foreach ($product->options as $option)
                        <fieldset class="space-y-3" wire:key="product-option-selector-{{ $option->getKey() }}">
                            <legend class="text-sm font-medium text-zinc-950 dark:text-white">{{ $option->name }}</legend>

                                @foreach ($option->values as $value)
                                    @php($selected = ($selectedOptions[$option->name] ?? null) === $value->value)
                                    <button
                                        type="button"
                                        wire:click="selectOption(@js($option->name), @js($value->value))"
                                        @class([
                                            'min-h-10 rounded-md border px-4 py-2 text-sm font-medium transition',
                                            'border-blue-700 bg-blue-50 text-blue-800 dark:border-blue-300 dark:bg-blue-950 dark:text-blue-100' => $selected,
                                            'border-zinc-200 text-zinc-700 hover:border-zinc-400 dark:border-zinc-700 dark:text-zinc-200 dark:hover:border-zinc-500' => ! $selected,
                                        ])
                                        aria-pressed="{{ $selected ? 'true' : 'false' }}"
                                    >
                                        {{ $value->value }}
                                    </button>
                                @endforeach

                        </fieldset>
                    @endforeach

            @endif

{{ $stockState['message'] }}

                    <button type="button" wire:click="decreaseQuantity" class="flex size-11 items-center justify-center border-r border-zinc-200 text-lg dark:border-zinc-700" aria-label="Decrease quantity">-</button>
                    <input wire:model.live="quantity" type="number" min="1" class="h-11 w-full border-0 bg-transparent text-center text-sm" aria-label="Quantity">
                    <button type="button" wire:click="increaseQuantity" class="flex size-11 items-center justify-center border-l border-zinc-200 text-lg dark:border-zinc-700" aria-label="Increase quantity">+</button>

                <flux:button wire:click="addToCart" variant="primary" class="w-full" :disabled="! $this->canAddToCart()">
                    {{ $this->canAddToCart() ? 'Add to cart' : 'Sold out' }}
                </flux:button>

            @if ($product->description_html)

                    {!! $product->description_html !!}

            @endif

Vendor

{{ $product->vendor ?: '-' }}

Product type

{{ $product->product_type ?: '-' }}

                @if ($selectedVariant)

SKU

{{ $selectedVariant->sku ?: '-' }}

Shipping

{{ $selectedVariant->requires_shipping ? 'Required' : 'Digital delivery' }}

                @endif

status: completed


output:
Chunk ID: e627fb
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 124
Output:
@props([
    'amount',
    'currency' => 'EUR',
    'compareAt' => null,
])

class('inline-flex flex-wrap items-baseline gap-2') }}>
    {{ \App\Support\Money::format((int) $amount, $currency) }}

    @if ($compareAt && $compareAt > $amount)
        {{ \App\Support\Money::format((int) $compareAt, $currency) }}
    @endif


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

    <x-storefront.breadcrumbs :items="[
        ['label' => $product->collections->first()?->title ?? 'Products', 'url' => $product->collections->first() ? route('collections.show', $product->collections->first()->handle) : route('collections.index')],
        ['label' => $product->title],
    ]" />

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

                    @if ($selectedVariant?->compare_at_amount && $selectedVariant->compare_at_amount > $selectedVariant->price_amount)
                        Sale
                    @endif
                    @foreach ($product->tags ?? [] as $tag)
                        {{ $tag }}
                    @endforeach

{{ $product->title }}

                @if ($selectedVariant)
                    <x-storefront.price :amount="$selectedVariant->price_amount" :compare-at="$selectedVariant->compare_at_amount" :currency="$selectedVariant->currency" class="text-2xl" aria-live="polite" />
                @endif

            @if ($product->options->isNotEmpty())

                    @foreach ($product->options as $option)
                        <fieldset class="space-y-3" wire:key="product-option-selector-{{ $option->getKey() }}">
                            <legend class="text-sm font-medium text-zinc-950 dark:text-white">{{ $option->name }}</legend>

                                @foreach ($option->values as $value)
                                    @php($selected = ($selectedOptions[$option->name] ?? null) === $value->value)
                                    <button
                                        type="button"
                                        wire:click="selectOption(@js($option->name), @js($value->value))"
                                        @class([
                                            'min-h-10 rounded-md border px-4 py-2 text-sm font-medium transition',
                                            'border-blue-700 bg-blue-50 text-blue-800 dark:border-blue-300 dark:bg-blue-950 dark:text-blue-100' => $selected,
                                            'border-zinc-200 text-zinc-700 hover:border-zinc-400 dark:border-zinc-700 dark:text-zinc-200 dark:hover:border-zinc-500' => ! $selected,
                                        ])
                                        aria-pressed="{{ $selected ? 'true' : 'false' }}"
                                    >
                                        {{ $value->value }}
                                    </button>
                                @endforeach

                        </fieldset>
                    @endforeach

            @endif

{{ $stockState['message'] }}

                    <button type="button" wire:click="decreaseQuantity" class="flex size-11 items-center justify-center border-r border-zinc-200 text-lg dark:border-zinc-700" aria-label="Decrease quantity">-</button>
                    <input wire:model.live="quantity" type="number" min="1" class="h-11 w-full border-0 bg-transparent text-center text-sm" aria-label="Quantity">
                    <button type="button" wire:click="increaseQuantity" class="flex size-11 items-center justify-center border-l border-zinc-200 text-lg dark:border-zinc-700" aria-label="Increase quantity">+</button>

                <flux:button wire:click="addToCart" variant="primary" class="w-full" :disabled="! $this->canAddToCart()">
                    {{ $this->canAddToCart() ? 'Add to cart' : 'Sold out' }}
                </flux:button>

            @if ($product->description_html)

                    {!! $product->description_html !!}

            @endif

Vendor

{{ $product->vendor ?: '-' }}

Product type

{{ $product->product_type ?: '-' }}

                @if ($selectedVariant)

SKU

{{ $selectedVariant->sku ?: '-' }}

Shipping

{{ $selectedVariant->requires_shipping ? 'Required' : 'Digital delivery' }}

                @endif


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

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

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

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

Filters

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

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

Price

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

Product type

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

Vendor

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

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

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

            @if ($products->isEmpty())

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

No products found

Try another search term or adjust your filters.

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

            @else

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

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


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

namespace Database\Seeders;

use App\Models\Collection as ProductCollection;
use App\Models\NavigationItem;
use App\Models\NavigationMenu;
use App\Models\Page;
use App\Models\Store;
use Illuminate\Database\Seeder;

class NavigationItemSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Store::query()->get()->each(function (Store $store): void {
            $menus = NavigationMenu::withoutGlobalScopes()
                ->where('store_id', $store->getKey())
                ->get()
                ->keyBy('handle');

            $this->replaceItems($menus->get('main-menu'), $this->mainMenuItems($store));
            $this->replaceItems($menus->get('footer-menu'), $this->footerMenuItems($store));
        });
    }

    /**
     * @param  array<int, array<string, mixed>>  $items
     */
    private function replaceItems(?NavigationMenu $menu, array $items): void
    {
        if ($menu === null) {
            return;
        }

        NavigationItem::withoutGlobalScopes()
            ->where('menu_id', $menu->getKey())
            ->delete();

        $this->createItems($menu, $items);
    }

    /**
     * @param  array<int, array<string, mixed>>  $items
     */
    private function createItems(NavigationMenu $menu, array $items, ?int $parentId = null): void
    {
        foreach ($items as $position => $item) {
            $navigationItem = NavigationItem::withoutGlobalScopes()->create([
                'menu_id' => $menu->getKey(),
                'parent_id' => $parentId,
                'type' => $item['type'],
                'label' => $item['label'],
                'url' => $item['url'] ?? null,
                'resource_id' => $item['resource_id'] ?? null,
                'position' => $position,
            ]);

            if (($item['children'] ?? []) !== []) {
                $this->createItems($menu, array_values($item['children']), $navigationItem->getKey());
            }
        }
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function mainMenuItems(Store $store): array
    {
        $newArrivals = $this->collection($store, 'new-arrivals') ?? $this->collection($store, 'featured');
        $secondaryCollection = $this->collection($store, 't-shirts') ?? $this->collection($store, 'accessories');
        $tertiaryCollection = $this->collection($store, 'pants-jeans');
        $sale = $this->collection($store, 'sale');
        $about = $this->page($store, 'about');

        return array_values(array_filter([
            ['label' => 'Home', 'type' => 'link', 'url' => '/'],
            [
                'label' => 'Shop',
                'type' => 'link',
                'url' => '/collections',
                'children' => array_values(array_filter([
                    $newArrivals ? ['label' => $newArrivals->title, 'type' => 'collection', 'resource_id' => $newArrivals->getKey()] : null,
                    $secondaryCollection ? ['label' => $secondaryCollection->title, 'type' => 'collection', 'resource_id' => $secondaryCollection->getKey()] : null,
                    $tertiaryCollection ? ['label' => $tertiaryCollection->title, 'type' => 'collection', 'resource_id' => $tertiaryCollection->getKey()] : null,
                    $sale ? ['label' => 'Sale', 'type' => 'collection', 'resource_id' => $sale->getKey()] : null,
                ])),
            ],
            ['label' => 'Search', 'type' => 'link', 'url' => '/search'],
            $about ? ['label' => 'About', 'type' => 'page', 'resource_id' => $about->getKey()] : null,
        ]));
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function footerMenuItems(Store $store): array
    {
        $sale = $this->collection($store, 'sale');
        $about = $this->page($store, 'about');
        $faq = $this->page($store, 'faq');

        return array_values(array_filter([
            $sale ? ['label' => 'Sale', 'type' => 'collection', 'resource_id' => $sale->getKey()] : null,
            $about ? ['label' => 'About', 'type' => 'page', 'resource_id' => $about->getKey()] : null,
            $faq ? ['label' => 'FAQ', 'type' => 'page', 'resource_id' => $faq->getKey()] : null,
            ['label' => 'Account', 'type' => 'link', 'url' => '/account'],
        ]));
    }

    private function collection(Store $store, string $handle): ?ProductCollection
    {
        return ProductCollection::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->where('handle', $handle)
            ->first();
    }

    private function page(Store $store, string $handle): ?Page
    {
        return Page::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->where('handle', $handle)
            ->first();
    }
}


packages: pestphp/pest laravel/framework

queries: browser testing visit click assert see css selector


server: laravel-boost

tool: search-docs

type: text

text:
# Search Results

## Database Testing Running Seeders
Database Testing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/database-testing.md

## Running Seeders

If you would like to use [database seeders](/docs/{{version}}/seeding) to populate your database during a feature test, you may invoke the `seed` method. By default, the `seed` method will execute the `DatabaseSeeder`, which should execute all of your other seeders. Alternatively, you pass a specific seeder class name to the `seed` method:

```php tab=Pest
<?php

use Database\Seeders\OrderStatusSeeder;
use Database\Seeders\TransactionStatusSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;

pest()->use(RefreshDatabase::class);

test('orders can be created', function () {
    // Run the DatabaseSeeder...
    $this->seed();

    // Run a specific seeder...
    $this->seed(OrderStatusSeeder::class);

    // ...

    // Run an array of specific seeders...
    $this->seed([
        OrderStatusSeeder::class,
        TransactionStatusSeeder::class,
        // ...
    ]);
});
```

```php tab=PHPUnit
<?php

namespace Tests\Feature;

use Database\Seeders\OrderStatusSeeder;
use Database\Seeders\TransactionStatusSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    use RefreshDatabase;

    /**
     * Test creating a new order.
     */
    public function test_orders_can_be_created(): void
    {
        // Run the DatabaseSeeder...
        $this->seed();

        // Run a specific seeder...
        $this->seed(OrderStatusSeeder::class);

        // ...

        // Run an array of specific seeders...
        $this->seed([
            OrderStatusSeeder::class,
            TransactionStatusSeeder::class,
            // ...
        ]);
    }
}
```

Alternatively, you may instruct Laravel to automatically seed the database before each test that uses the `RefreshDatabase` trait. You may accomplish this by defining a `$seed` property on your base test class:

```php
<?php

namespace Tests;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    /**
     * Indicates whether the default seeder should run before each test.
     *
     * @var bool
     */
    protected $seed = true;
}
```

When the `$seed` property is `true`, the test will run the `Database\Seeders\DatabaseSeeder` class before each test that uses the `RefreshDatabase` trait. However, you may specify a specific seeder that should be executed by defining a `$seeder` property on your test class:

```php
use Database\Seeders\OrderStatusSeeder;

/**
 * Run a specific seeder before each test.
 *
 * @var string
 */
protected $seeder = OrderStatusSeeder::class;
```

---

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

---
title: Browser Testing
description:
---

# Browser Testing

Browser testing is an essential part of modern web development, allowing you to ensure that your application works correctly across different browsers and devices. Pest provides a simple and elegant way to write browser tests. Here is an example of how to write a browser test using Pest:

```php
it('may welcome the user', function () {
    $page = visit('/');

    $page->assertSee('Welcome');
});
```

This is a basic example of a browser test that checks if the homepage contains the text "Welcome". However, Pest's browser testing capabilities go beyond this simple example. You can use various methods to interact with the page, such as clicking buttons, filling out forms, and navigating between pages.

Here is an example of a more complex browser test, on Laravel, that checks if a user can sign in:

```php
it('may sign in the user', function () {
    Event::fake();

    User::factory()->create([ // assumes RefreshDatabase trait is used on Pest.php...
        'email' => 'nuno@laravel.com',
        'password' => 'password',
    ]);

    $page = visit('/')->on()->mobile()->firefox();

    $page->click('Sign In')
         ->assertUrlIs('/login')
         ->assertSee('Sign In to Your Account')
         ->fill('email', 'nuno@laravel.com')
         ->fill('password', 'password')
         ->click('Submit')
         ->assertSee('Dashboard');

    $this->assertAuthenticated();

    Event::assertDispatched(UserLoggedIn::class);
});
```

Note that, you are leveraging the full power of Laravel's testing capabilities, such as refresh database, event faking, and authentication assertions, while also actually doing browser testing.

## Getting Started

To get started with browser testing in Pest, you need to install the Pest Browser plugin. You can do this by running the following command:

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

npm install playwright@latest
npx playwright install
```

Finally, add `tests/Browser/Screenshots` to your `.gitignore` file to avoid committing screenshots taken during browser tests.

### Running Browser Tests

Running browser tests is similar to running regular Pest tests:

```bash
./vendor/bin/pest
```

We recommend running tests in parallel using the `--parallel` option to speed up the execution:

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

For debugging purposes, you can run the tests in a headed mode and pause the execution at the end of the failed test run:

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

### Navigation

The `visit()` method is used to navigate to a specific URL in your browser test. It provides various methods to interact with the page:

```php
test('example', function () {
    $page = visit('/');

    $page->assertSee('Welcome');
});
```

### Using Other Browsers

By default, the `visit()` method uses Chrome as the browser. However, if you want to use a different browser, you can specify it using the `--browser` option when running the tests:

```bash
./vendor/bin/pest --browser firefox
./vendor/bin/pest --browser safari
```

If you wish to use a different browser by default without specifying it in the command line, you can set it in your `Pest.php` configuration file:

```php
pest()->browser()->inFirefox();
pest()->browser()->inSafari();
```

### Using Other Devices

The `visit()` method uses a desktop viewport. However, you can specify a mobile viewport using the `onMobile()` method. For example:

```php
$page = visit('/')->on()->mobile();
```

If you wish to use a specific device, you can use the `on()` method and chain it with the `macbook14`, `iPhone14Pro`, etc:

```php
$page = visit('/')->on()->iPhone14Pro();
```

### Using Dark Mode

Pest enforces a light color scheme by default. However, you can specify a dark color scheme using the `inDarkMode()` method:

```php
$page = visit('/')->inDarkMode();
```

### Visiting Multiple Pages

You can visit multiple pages simultaneously by passing an array of URLs to the `visit()` method. This is useful for testing scenarios where you need to interact with multiple pages at once:

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

$pages->assertNoSmoke()
    ->assertNoAccessibilityIssues()
    ->assertNoConsoleLogs()
    ->assertNoJavaScriptErrors();

[$homePage, $aboutPage] = $pages;

$homePage->assertSee('Welcome to our website');
$aboutPage->assertSee('About Us');
```

### Navigation

After visiting a page, you can navigate to other pages using the `navigate()` method. This method allows you to navigate to a different URL while keeping the current browser context:

```php
$page = visit('/');

$page->navigate('/about')
     ->assertSee('About Us');
```

### Locating Elements

You can locate elements in the DOM using text or CSS selectors. Pest provides a simple syntax for locating elements:

```php
// Clicks the first link with the text "Login"
$page->click('Login');

// Clicks the first element with the class "btn-primary"
$page->click('.btn-primary');

// Clicks the element with the data-test attribute "login"
$page->click('@login');

// Clicks the element with the ID "submit-button"
$page->click('#submit-button');

// etc...
```

### Configuring Timeouts

Sometimes, elements may take time to appear on the page. By default, Pest waits for `5` seconds before timing out. You can configure the default timeout for browser tests in your `Pest.php` configuration file:

```php
pest()->browser()->timeout(10000);
```

### Configuring User Agent

By default, the User Agent will default to the Browser you're running for tests such as: `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/133.0.6943.16 Safari/537.36`

You may wish to override the User Agent of the browser for all of your tests, you can configure this in the `Pest.php` configuration file:

```php
pest()->browser()->userAgent('CustomUserAgent');
```

### Configuring Host

By default, the server will bind to `127.0.0.1` for all browser tests.

You may wish to override the host for subdomain applications. You can configure this in the `Pest.php` configuration file:

```php
pest()->browser()->withHost('some-subdomain.localhost');
```

### Geolocation

Sometimes, you need to define where the browser believes it is physically on the earth.  This method takes a latitude and longitude and will set the `geolocation` permission in the browser and then make the coordinates available via Javascript's getCurrentPosition API:

```php
$page = visit('/')
     ->geolocation(39.399872, -8.224454);
```

### Configuring Locale

You can set the locale for your test requests using the `withLocale` method. This is particularly useful for testing multilingual applications.

```php
$page = visit('/')->withLocale('fr-FR');

$page->assertSee('Bienvenue');
```
### Configuring Timezone

You can set the timezone for your test requests using the `withTimezone` method. This is useful for testing date and time displays in different time zones.

```php
$page = visit('/')->withTimezone('America/New_York');

$page->assertSee('EST');
```

### Configuring UserAgent

You can set the User-Agent header for your test requests using the `withUserAgent` method. This is useful for testing how your application responds to different types of clients, such as mobile browsers or bots.

```php
$page = visit('/')->withUserAgent('Googlebot');

$page->assertSee('Welcome, bot!');
```

### Configuring Host

You can set the host for your test server using the `withHost` method. This is useful for testing subdomains or where different hosts serve different content.

```php
$page = visit('/dashboard')->withHost('some-subdomain.localhost');

$page->assertSee('Welcome to Some Subdomain');
```

## Table of Contents

### Available Assertions

[assertTitle](#assert-title)
[assertTitleContains](#assert-title-contains)
[assertSee](#assert-see)
[assertDontSee](#assert-dont-see)
[assertSeeIn](#assert-see-in)
[assertDontSeeIn](#assert-dont-see-in)
[assertSeeAnythingIn](#assert-see-anything-in)
[assertSeeNothingIn](#assert-see-nothing-in)
[assertCount](#assert-count)
[assertScript](#assert-script)
[assertSourceHas](#assert-source-has)
[assertSourceMissing](#assert-source-missing)
[assertSeeLink](#assert-see-link)
[assertDontSeeLink](#assert-dont-see-link)
[assertChecked](#assert-checked)
[assertNotChecked](#assert-not-checked)
[assertIndeterminate](#assert-indeterminate)
[assertRadioSelected](#assert-radio-selected)
[assertRadioNotSelected](#assert-radio-not-selected)
[assertSelected](#assert-selected)
[assertNotSelected](#assert-not-selected)
[assertValue](#assert-value)
[assertValueIsNot](#assert-value-is-not)
[assertAttribute](#assert-attribute)
[assertAttributeMissing](#assert-attribute-missing)
[assertAttributeContains](#assert-attribute-contains)
[assertAttributeDoesntContain](#assert-attribute-doesnt-contain)
[assertAriaAttribute](#assert-aria-attribute)
[assertDataAttribute](#assert-data-attribute)
[assertVisible](#assert-visible)
[assertPresent](#assert-present)
[assertNotPresent](#assert-not-present)
[assertMissing](#assert-missing)
[assertEnabled](#assert-enabled)
[assertDisabled](#assert-disabled)
[assertButtonEnabled](#assert-button-enabled)
[assertButtonDisabled](#assert-button-disabled)
[assertUrlIs](#assert-url-is)
[assertSchemeIs](#assert-scheme-is)
[assertSchemeIsNot](#assert-scheme-is-not)
[assertHostIs](#assert-host-is)
[assertHostIsNot](#assert-host-is-not)
[assertPortIs](#assert-port-is)
[assertPortIsNot](#assert-port-is-not)
[assertPathBeginsWith](#assert-path-begins-with)
[assertPathEndsWith](#assert-path-ends-with)
[assertPathContains](#assert-path-contains)
[assertPathIs](#assert-path-is)
[assertPathIsNot](#assert-path-is-not)
[assertQueryStringHas](#assert-query-string-has)
[assertQueryStringMissing](#assert-query-string-missing)
[assertFragmentIs](#assert-fragment-is)
[assertFragmentBeginsWith](#assert-fragment-begins-with)
[assertFragmentIsNot](#assert-fragment-is-not)
[assertNoSmoke](#assert-no-smoke)
[assertNoConsoleLogs](#assert-no-console-logs)
[assertNoJavaScriptErrors](#assert-no-javascript-errors)
[assertNoAccessibilityIssues](#assert-no-accessibility-issues)
[assertScreenshotMatches](#assert-screenshot-matches)

### Element Interactions

[click](#click)
[text](#text)
[attribute](#attribute)
[keys](#keys)
[withKeyDown](#withKeyDown)
[type](#type)
[typeSlowly](#type-slowly)
[select](#select)
[append](#append)
[clear](#clear)
[radio](#radio)
[check](#check)
[uncheck](#uncheck)
[attach](#attach)
[press](#press)
[pressAndWaitFor](#press-and-wait-for)
[drag](#drag)
[hover](#hover)
[submit](#submit)
[value](#value)
[withinIframe](#within-iframe)
[resize](#resize)
[script](#script)
[content](#content)
[url](#url)
[wait](#wait)
[waitForKey](#wait-for-key)

### Debugging tests

[debug](#debug)
[screenshot](#screenshot)
[screenshotElement](#screenshot-element)
[tinker](#tinker)
[headed](#headed)

## Element Assertions

_[Truncated]_


output:
Wall time: 3.0678 seconds
Output:
[{"type":"text","text":"# Search Results\n\n## Database Testing Running Seeders\nDatabase Testing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/database-testing.md\n\n\n## Running Seeders\n\nIf you would like to use [database seeders](/docs/{{version}}/seeding) to populate your database during a feature test, you may invoke the `seed` method. By default, the `seed` method will execute the `DatabaseSeeder`, which should execute all of your other seeders. Alternatively, you pass a specific seeder class name to the `seed` method:\n\n```php tab=Pest\n<?php\n\nuse Database\\Seeders\\OrderStatusSeeder;\nuse Database\\Seeders\\TransactionStatusSeeder;\nuse Illuminate\\Foundation\\Testing\\RefreshDatabase;\n\npest()->use(RefreshDatabase::class);\n\ntest('orders can be created', function () {\n    // Run the DatabaseSeeder...\n    $this->seed();\n\n    // Run a specific seeder...\n    $this->seed(OrderStatusSeeder::class);\n\n    // ...\n\n    // Run an array of specific seeders...\n    $this->seed([\n        OrderStatusSeeder::class,\n        TransactionStatusSeeder::class,\n        // ...\n    ]);\n});\n```\n\n```php tab=PHPUnit\n<?php\n\nnamespace Tests\\Feature;\n\nuse Database\\Seeders\\OrderStatusSeeder;\nuse Database\\Seeders\\TransactionStatusSeeder;\nuse Illuminate\\Foundation\\Testing\\RefreshDatabase;\nuse Tests\\TestCase;\n\nclass ExampleTest extends TestCase\n{\n    use RefreshDatabase;\n\n    /**\n     * Test creating a new order.\n     */\n    public function test_orders_can_be_created(): void\n    {\n        // Run the DatabaseSeeder...\n        $this->seed();\n\n        // Run a specific seeder...\n        $this->seed(OrderStatusSeeder::class);\n\n        // ...\n\n        // Run an array of specific seeders...\n        $this->seed([\n            OrderStatusSeeder::class,\n            TransactionStatusSeeder::class,\n            // ...\n        ]);\n    }\n}\n```\n\nAlternatively, you may instruct Laravel to automatically seed the database before each test that uses the `RefreshDatabase` trait. You may accomplish this by defining a `$seed` property on your base test class:\n\n```php\n<?php\n\nnamespace Tests;\n\nuse Illuminate\\Foundation\\Testing\\TestCase as BaseTestCase;\n\nabstract class TestCase extends BaseTestCase\n{\n    /**\n     * Indicates whether the default seeder should run before each test.\n     *\n     * @var bool\n     */\n    protected $seed = true;\n}\n```\n\nWhen the `$seed` property is `true`, the test will run the `Database\\Seeders\\DatabaseSeeder` class before each test that uses the `RefreshDatabase` trait. However, you may specify a specific seeder that should be executed by defining a `$seeder` property on your test class:\n\n```php\nuse Database\\Seeders\\OrderStatusSeeder;\n\n/**\n * Run a specific seeder before each test.\n *\n * @var string\n */\nprotected $seeder = OrderStatusSeeder::class;\n```\n\n---\n\n## Browser Testing\nBrowser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md\n\n---\ntitle: Browser Testing\ndescription:\n---\n\n# Browser Testing\n\nBrowser testing is an essential part of modern web development, allowing you to ensure that your application works correctly across different browsers and devices. Pest provides a simple and elegant way to write browser tests. Here is an example of how to write a browser test using Pest:\n\n```php\nit('may welcome the user', function () {\n    $page = visit('/');\n\n    $page->assertSee('Welcome');\n});\n```\n\nThis is a basic example of a browser test that checks if the homepage contains the text \"Welcome\". However, Pest's browser testing capabilities go beyond this simple example. You can use various methods to interact with the page, such as clicking buttons, filling out forms, and navigating between pages.\n\nHere is an example of a more complex browser test, on Laravel, that checks if a user can sign in:\n\n```php\nit('may sign in the user', function () {\n    Event::fake();\n\n    User::factory()->create([ // assumes RefreshDatabase trait is used on Pest.php...\n        'email' => 'nuno@laravel.com',\n        'password' => 'password',\n    ]);\n\n    $page = visit('/')->on()->mobile()->firefox();\n\n    $page->click('Sign In')\n         ->assertUrlIs('/login')\n         ->assertSee('Sign In to Your Account')\n         ->fill('email', 'nuno@laravel.com')\n         ->fill('password', 'password')\n         ->click('Submit')\n         ->assertSee('Dashboard');\n\n    $this->assertAuthenticated();\n\n    Event::assertDispatched(UserLoggedIn::class);\n});\n```\n\nNote that, you are leveraging the full power of Laravel's testing capabilities, such as refresh database, event faking, and authentication assertions, while also actually doing browser testing.\n\n## Getting Started\n\nTo get started with browser testing in Pest, you need to install the Pest Browser plugin. You can do this by running the following command:\n\n```bash\ncomposer require pestphp/pest-plugin-browser --dev\n\nnpm install playwright@latest\nnpx playwright install\n```\n\nFinally, add `tests/Browser/Screenshots` to your `.gitignore` file to avoid committing screenshots taken during browser tests.\n\n### Running Browser Tests\n\nRunning browser tests is similar to running regular Pest tests:\n\n```bash\n./vendor/bin/pest\n```\n\nWe recommend running tests in parallel using the `--parallel` option to speed up the execution:\n\n```bash\n./vendor/bin/pest --parallel\n```\n\nFor debugging purposes, you can run the tests in a headed mode and pause the execution at the end of the failed test run:\n\n```bash\n./vendor/bin/pest --debug\n```\n\n### Navigation\n\nThe `visit()` method is used to navigate to a specific URL in your browser test. It provides various methods to interact with the page:\n\n```php\ntest('example', function () {\n    $page = visit('/');\n\n    $page->assertSee('Welcome');\n});\n```\n\n### Using Other Browsers\n\nBy default, the `visit()` method uses Chrome as the browser. However, if you want to use a different browser, you can specify it using the `--browser` option when running the tests:\n\n```bash\n./vendor/bin/pest --browser firefox\n./vendor/bin/pest --browser safari\n```\n\nIf you wish to use a different browser by default without specifying it in the command line, you can set it in your `Pest.php` configuration file:\n\n```php\npest()->browser()->inFirefox();\npest()->browser()->inSafari();\n```\n\n### Using Other Devices\n\nThe `visit()` method uses a desktop viewport. However, you can specify a mobile viewport using the `onMobile()` method. For example:\n\n```php\n$page = visit('/')->on()->mobile();\n```\n\nIf you wish to use a specific device, you can use the `on()` method and chain it with the `macbook14`, `iPhone14Pro`, etc:\n\n```php\n$page = visit('/')->on()->iPhone14Pro();\n```\n\n### Using Dark Mode\n\nPest enforces a light color scheme by default. However, you can specify a dark color scheme using the `inDarkMode()` method:\n\n```php\n$page = visit('/')->inDarkMode();\n```\n\n### Visiting Multiple Pages\n\nYou can visit multiple pages simultaneously by passing an array of URLs to the `visit()` method. This is useful for testing scenarios where you need to interact with multiple pages at once:\n\n```php\n$pages = visit(['/', '/about']);\n\n$pages->assertNoSmoke()\n    ->assertNoAccessibilityIssues()\n    ->assertNoConsoleLogs()\n    ->assertNoJavaScriptErrors();\n\n[$homePage, $aboutPage] = $pages;\n\n$homePage->assertSee('Welcome to our website');\n$aboutPage->assertSee('About Us');\n```\n\n### Navigation\n\nAfter visiting a page, you can navigate to other pages using the `navigate()` method. This method allows you to navigate to a different URL while keeping the current browser context:\n\n```php\n$page = visit('/');\n\n$page->navigate('/about')\n     ->assertSee('About Us');\n```\n\n### Locating Elements\n\nYou can locate elements in the DOM using text or CSS selectors. Pest provides a simple syntax for locating elements:\n\n```php\n// Clicks the first link with the text \"Login\"\n$page->click('Login');\n\n// Clicks the first element with the class \"btn-primary\"\n$page->click('.btn-primary');\n\n// Clicks the element with the data-test attribute \"login\"\n$page->click('@login');\n\n// Clicks the element with the ID \"submit-button\"\n$page->click('#submit-button');\n\n// etc...\n```\n\n### Configuring Timeouts\n\nSometimes, elements may take time to appear on the page. By default, Pest waits for `5` seconds before timing out. You can configure the default timeout for browser tests in your `Pest.php` configuration file:\n\n```php\npest()->browser()->timeout(10000);\n```\n\n### Configuring User Agent\n\nBy default, the User Agent will default to the Browser you're running for tests such as: `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/133.0.6943.16 Safari/537.36` \n\nYou may wish to override the User Agent of the browser for all of your tests, you can configure this in the `Pest.php` configuration file:\n\n```php\npest()->browser()->userAgent('CustomUserAgent');\n```\n\n### Configuring Host\n\nBy default, the server will bind to `127.0.0.1` for all browser tests.\n\nYou may wish to override the host for subdomain applications. You can configure this in the `Pest.php` configuration file:\n\n```php\npest()->browser()->withHost('some-subdomain.localhost');\n```\n\n### Geolocation\n\nSometimes, you need to define where the browser believes it is physically on the earth.  This method takes a latitude and longitude and will set the `geolocation` permission in the browser and then make the coordinates available via Javascript's getCurrentPosition API:\n\n```php\n$page = visit('/')\n     ->geolocation(39.399872, -8.224454);\n```\n\n### Configuring Locale\n\nYou can set the locale for your test requests using the `withLocale` method. This is particularly useful for testing multilingual applications.\n\n```php\n$page = visit('/')->withLocale('fr-FR');\n\n$page->assertSee('Bienvenue');\n```\n### Configuring Timezone\n\nYou can set the timezone for your test requests using the `withTimezone` method. This is useful for testing date and time displays in different time zones.\n\n```php\n$page = visit('/')->withTimezone('America/New_York');\n\n$page->assertSee('EST');\n```\n\n### Configuring UserAgent\n\nYou can set the User-Agent header for your test requests using the `withUserAgent` method. This is useful for testing how your application responds to different types of clients, such as mobile browsers or bots.\n\n```php\n$page = visit('/')->withUserAgent('Googlebot');\n\n$page->assertSee('Welcome, bot!');\n```\n\n### Configuring Host\n\nYou can set the host for your test server using the `withHost` method. This is useful for testing subdomains or where different hosts serve different content.\n\n```php\n$page = visit('/dashboard')->withHost('some-subdomain.localhost');\n\n$page->assertSee('Welcome to Some Subdomain');\n```\n\n## Table of Contents\n\n### Available Assertions\n\n
\n\n[assertTitle](#assert-title)\n[assertTitleContains](#assert-title-contains)\n[assertSee](#assert-see)\n[assertDontSee](#assert-dont-see)\n[assertSeeIn](#assert-see-in)\n[assertDontSeeIn](#assert-dont-see-in)\n[assertSeeAnythingIn](#assert-see-anything-in)\n[assertSeeNothingIn](#assert-see-nothing-in)\n[assertCount](#assert-count)\n[assertScript](#assert-script)\n[assertSourceHas](#assert-source-has)\n[assertSourceMissing](#assert-source-missing)\n[assertSeeLink](#assert-see-link)\n[assertDontSeeLink](#assert-dont-see-link)\n[assertChecked](#assert-checked)\n[assertNotChecked](#assert-not-checked)\n[assertIndeterminate](#assert-indeterminate)\n[assertRadioSelected](#assert-radio-selected)\n[assertRadioNotSelected](#assert-radio-not-selected)\n[assertSelected](#assert-selected)\n[assertNotSelected](#assert-not-selected)\n[assertValue](#assert-value)\n[assertValueIsNot](#assert-value-is-not)\n[assertAttribute](#assert-attribute)\n[assertAttributeMissing](#assert-attribute-missing)\n[assertAttributeContains](#assert-attribute-contains)\n[assertAttributeDoesntContain](#assert-attribute-doesnt-contain)\n[assertAriaAttribute](#assert-aria-attribute)\n[assertDataAttribute](#assert-data-attribute)\n[assertVisible](#assert-visible)\n[assertPresent](#assert-present)\n[assertNotPresent](#assert-not-present)\n[assertMissing](#assert-missing)\n[assertEnabled](#assert-enabled)\n[assertDisabled](#assert-disabled)\n[assertButtonEnabled](#assert-button-enabled)\n[assertButtonDisabled](#assert-button-disabled)\n[assertUrlIs](#assert-url-is)\n[assertSchemeIs](#assert-scheme-is)\n[assertSchemeIsNot](#assert-scheme-is-not)\n[assertHostIs](#assert-host-is)\n[assertHostIsNot](#assert-host-is-not)\n[assertPortIs](#assert-port-is)\n[assertPortIsNot](#assert-port-is-not)\n[assertPathBeginsWith](#assert-path-begins-with)\n[assertPathEndsWith](#assert-path-ends-with)\n[assertPathContains](#assert-path-contains)\n[assertPathIs](#assert-path-is)\n[assertPathIsNot](#assert-path-is-not)\n[assertQueryStringHas](#assert-query-string-has)\n[assertQueryStringMissing](#assert-query-string-missing)\n[assertFragmentIs](#assert-fragment-is)\n[assertFragmentBeginsWith](#assert-fragment-begins-with)\n[assertFragmentIsNot](#assert-fragment-is-not)\n[assertNoSmoke](#assert-no-smoke)\n[assertNoConsoleLogs](#assert-no-console-logs)\n[assertNoJavaScriptErrors](#assert-no-javascript-errors)\n[assertNoAccessibilityIssues](#assert-no-accessibility-issues)\n[assertScreenshotMatches](#assert-screenshot-matches)\n\n
\n\n### Element Interactions\n\n
\n\n[click](#click)\n[text](#text)\n[attribute](#attribute)\n[keys](#keys)\n[withKeyDown](#withKeyDown)\n[type](#type)\n[typeSlowly](#type-slowly)\n[select](#select)\n[append](#append)\n[clear](#clear)\n[radio](#radio)\n[check](#check)\n[uncheck](#uncheck)\n[attach](#attach)\n[press](#press)\n[pressAndWaitFor](#press-and-wait-for)\n[drag](#drag)\n[hover](#hover)\n[submit](#submit)\n[value](#value)\n[withinIframe](#within-iframe)\n[resize](#resize)\n[script](#script)\n[content](#content)\n[url](#url)\n[wait](#wait)\n[waitForKey](#wait-for-key)\n\n
\n\n### Debugging tests\n\n
\n\n[debug](#debug)\n[screenshot](#screenshot)\n[screenshotElement](#screenshot-element)\n[tinker](#tinker)\n[headed](#headed)\n\n
\n\n## Element Assertions\n\n_[Truncated]_"}]


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


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: a35317
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


cmd: find tests/Browser -maxdepth 3 -type f -print | sort


command: find tests/Browser -maxdepth 3 -type f -print | sort

path: Browser


output:
Chunk ID: cb6b5e
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 18
Output:
tests/Browser/Admin/AuthenticationTest.php
tests/Browser/SmokeTest.php


message: I have the exact assertions and the relevant implementation details now. I’m creating the Pest browser file through Artisan, then I’ll move it into `tests/Browser` to match the existing browser suite layout.

phase: commentary


type: message

role: assistant

text: I have the exact assertions and the relevant implementation details now. I’m creating the Pest browser file through Artisan, then I’ll move it into `tests/Browser` to match the existing browser suite layout.


cmd: php artisan make:test Browser/Storefront/BrowsingTest --pest --no-interaction


output:
Chunk ID: eb6cf4
Wall time: 0.0258 seconds
Process exited with code 0
Original token count: 23
Output:

   INFO  Test [tests/Feature/Browser/Storefront/BrowsingTest.php] created successfully.


command: php artisan make:test Browser/Storefront/BrowsingTest --pest --no-interaction

type: unknown

aggregated_output: INFO  Test [tests/Feature/Browser/Storefront/BrowsingTest.php] created successfully.


cmd: mkdir -p tests/Browser/Storefront && mv tests/Feature/Browser/Storefront/BrowsingTest.php tests/Browser/Storefront/BrowsingTest.php && rmdir tests/Feature/Browser/Storefront 2>/dev/null || true && rmdir tests/Feature/Browser 2>/dev/null || true


cmd: sed -n '1,120p' tests/Feature/Browser/Storefront/BrowsingTest.php


command: sed -n '1,120p' tests/Feature/Browser/Storefront/BrowsingTest.php

aggregated_output:
<?php

test('example', function () {
    $response = $this->get('/');

    $response->assertStatus(200);
});


command: mkdir -p tests/Browser/Storefront && mv tests/Feature/Browser/Storefront/BrowsingTest.php tests/Browser/Storefront/BrowsingTest.php && rmdir tests/Feature/Browser/Storefront 2>/dev/null || true && rmdir tests/Feature/Browser 2>/dev/null || true


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


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

test('example', function () {
    $response = $this->get('/');

    $response->assertStatus(200);
});


cmd: sed -n '1,120p' tests/Browser/Storefront/BrowsingTest.php


command: sed -n '1,120p' tests/Browser/Storefront/BrowsingTest.php


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

test('example', function () {
    $response = $this->get('/');

    $response->assertStatus(200);
});


cmd: rg "assertSeeIn|assertDisabled|assertEnabled|assertButton" tests vendor/pestphp -n


command: rg "assertSeeIn|assertDisabled|assertEnabled|assertButton" tests vendor/pestphp -n

type: search

cmd: rg 'assertSeeIn|assertDisabled|assertEnabled|assertButton' tests vendor/pestphp -n

query: assertSeeIn|assertDisabled|assertEnabled|assertButton

path: tests

aggregated_output:
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/MakesElementAssertions.php:92:    public function assertSeeIn(string $selector, string|int|float $text): Webpage
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/MakesElementAssertions.php:543:    public function assertEnabled(string $field): Webpage
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/MakesElementAssertions.php:553:    public function assertDisabled(string $field): Webpage
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/MakesElementAssertions.php:563:    public function assertButtonEnabled(string $button): Webpage
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/MakesElementAssertions.php:575:    public function assertButtonDisabled(string $button): Webpage


output:
Chunk ID: 97e599
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 187
Output:
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/MakesElementAssertions.php:92:    public function assertSeeIn(string $selector, string|int|float $text): Webpage
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/MakesElementAssertions.php:543:    public function assertEnabled(string $field): Webpage
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/MakesElementAssertions.php:553:    public function assertDisabled(string $field): Webpage
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/MakesElementAssertions.php:563:    public function assertButtonEnabled(string $button): Webpage
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/MakesElementAssertions.php:575:    public function assertButtonDisabled(string $button): Webpage


cmd: sed -n '70,110p' vendor/pestphp/pest-plugin-browser/src/Api/Concerns/MakesElementAssertions.php && sed -n '520,590p' vendor/pestphp/pest-plugin-browser/src/Api/Concerns/MakesElementAssertions.php


command: sed -n '70,110p' vendor/pestphp/pest-plugin-browser/src/Api/Concerns/MakesElementAssertions.php && sed -n '520,590p' vendor/pestphp/pest-plugin-browser/src/Api/Concerns/MakesElementAssertions.php

MakesElementAssertions.php

aggregated_output:
        $text = (string) $text;

        $locator = $this->page->unstrict(
            fn () => $this->page->getByText($text),
        );

        foreach ($locator->all() as $element) {
            if ($element->isVisible()) {
                throw new ExpectationFailedException(
                    "Expected not to see text [{$text}] on the page initially with the url [{$this->initialUrl}], but it was found.",
                );
            }
        }

        expect(true)->toBeTrue();

        return $this;
    }

    /**
     * Assert that the given text is present within the selector.
     */
    public function assertSeeIn(string $selector, string|int|float $text): Webpage
    {
        $text = (string) $text;

        $locator = $this->guessLocator($selector);

        expect($locator->getByText($text)->isVisible())->toBeTrue("Expected to see text [{$text}] within element [{$selector}] on the page initially with the url [{$this->initialUrl}], but it was not found or not visible.");

        return $this;
    }

    /**
     * Assert that the given text is not present within the selector.
     */
    public function assertDontSeeIn(string $selector, string|int|float $text): Webpage
    {
        $text = (string) $text;

        $locator = $this->guessLocator($selector);
    public function assertNotPresent(string $selector): Webpage
    {
        $count = $this->guessLocator($selector)->count();
        expect($count)->toBe(0, "Expected element [{$selector}] not to be present in the DOM on the page initially with the url [{$this->initialUrl}], but it was found.");

        return $this;
    }

    /**
     * Assert that the element matching the given selector is not visible.
     */
    public function assertMissing(string $selector): Webpage
    {
        $locator = $this->guessLocator($selector);

        expect($locator->isVisible())->toBeFalse("Expected element [{$selector}] not to be visible on the page initially with the url [{$this->initialUrl}], but it was.");

        return $this;
    }

    /**
     * Assert that the given field is enabled.
     */
    public function assertEnabled(string $field): Webpage
    {
        expect($this->guessLocator($field)->isEnabled())->toBeTrue("Expected field [{$field}] to be enabled on the page initially with the url [{$this->initialUrl}], but it was disabled.");

        return $this;
    }

    /**
     * Assert that the given field is disabled.
     */
    public function assertDisabled(string $field): Webpage
    {
        expect($this->guessLocator($field)->isDisabled())->toBeTrue("Expected field [{$field}] to be disabled on the page initially with the url [{$this->initialUrl}], but it was enabled.");

        return $this;
    }

    /**
     * Assert that the given button is enabled.
     */
    public function assertButtonEnabled(string $button): Webpage
    {
        $selector = $this->guessLocator($button);

        expect($selector->isEnabled())->toBeTrue("Expected button [{$button}] to be enabled on the page initially with the url [{$this->initialUrl}], but it was disabled.");

        return $this;
    }

    /**
     * Assert that the given button is disabled.
     */
    public function assertButtonDisabled(string $button): Webpage
    {
        $selector = $this->guessLocator($button);

        expect($selector->isDisabled())->toBeTrue("Expected button [{$button}] to be disabled on the page initially with the url [{$this->initialUrl}], but it was enabled.");

        return $this;
    }

    /**
     * Wait for the given text to appear on the page.
     *
     * @deprecated Use `assertSee` instead.
     */
    public function waitForText(string|int|float $text): Webpage
    {


output:
Chunk ID: 1b9490
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 928
Output:
        $text = (string) $text;

        $locator = $this->page->unstrict(
            fn () => $this->page->getByText($text),
        );

        foreach ($locator->all() as $element) {
            if ($element->isVisible()) {
                throw new ExpectationFailedException(
                    "Expected not to see text [{$text}] on the page initially with the url [{$this->initialUrl}], but it was found.",
                );
            }
        }

        expect(true)->toBeTrue();

        return $this;
    }

    /**
     * Assert that the given text is present within the selector.
     */
    public function assertSeeIn(string $selector, string|int|float $text): Webpage
    {
        $text = (string) $text;

        $locator = $this->guessLocator($selector);

        expect($locator->getByText($text)->isVisible())->toBeTrue("Expected to see text [{$text}] within element [{$selector}] on the page initially with the url [{$this->initialUrl}], but it was not found or not visible.");

        return $this;
    }

    /**
     * Assert that the given text is not present within the selector.
     */
    public function assertDontSeeIn(string $selector, string|int|float $text): Webpage
    {
        $text = (string) $text;

        $locator = $this->guessLocator($selector);
    public function assertNotPresent(string $selector): Webpage
    {
        $count = $this->guessLocator($selector)->count();
        expect($count)->toBe(0, "Expected element [{$selector}] not to be present in the DOM on the page initially with the url [{$this->initialUrl}], but it was found.");

        return $this;
    }

    /**
     * Assert that the element matching the given selector is not visible.
     */
    public function assertMissing(string $selector): Webpage
    {
        $locator = $this->guessLocator($selector);

        expect($locator->isVisible())->toBeFalse("Expected element [{$selector}] not to be visible on the page initially with the url [{$this->initialUrl}], but it was.");

        return $this;
    }

    /**
     * Assert that the given field is enabled.
     */
    public function assertEnabled(string $field): Webpage
    {
        expect($this->guessLocator($field)->isEnabled())->toBeTrue("Expected field [{$field}] to be enabled on the page initially with the url [{$this->initialUrl}], but it was disabled.");

        return $this;
    }

    /**
     * Assert that the given field is disabled.
     */
    public function assertDisabled(string $field): Webpage
    {
        expect($this->guessLocator($field)->isDisabled())->toBeTrue("Expected field [{$field}] to be disabled on the page initially with the url [{$this->initialUrl}], but it was enabled.");

        return $this;
    }

    /**
     * Assert that the given button is enabled.
     */
    public function assertButtonEnabled(string $button): Webpage
    {
        $selector = $this->guessLocator($button);

        expect($selector->isEnabled())->toBeTrue("Expected button [{$button}] to be enabled on the page initially with the url [{$this->initialUrl}], but it was disabled.");

        return $this;
    }

    /**
     * Assert that the given button is disabled.
     */
    public function assertButtonDisabled(string $button): Webpage
    {
        $selector = $this->guessLocator($button);

        expect($selector->isDisabled())->toBeTrue("Expected button [{$button}] to be disabled on the page initially with the url [{$this->initialUrl}], but it was enabled.");

        return $this;
    }

    /**
     * Wait for the given text to appear on the page.
     *
     * @deprecated Use `assertSee` instead.
     */
    public function waitForText(string|int|float $text): Webpage
    {


cmd: rg "function hover|function click" vendor/pestphp/pest-plugin-browser/src/Api -n


command: rg "function hover|function click" vendor/pestphp/pest-plugin-browser/src/Api -n

type: search

cmd: rg 'function hover|function click' vendor/pestphp/pest-plugin-browser/src/Api -n

query: function hover|function click

path: Api

aggregated_output:
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/InteractsWithElements.php:17:    public function click(string $text): self
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/InteractsWithElements.php:93:    public function hover(string $selector): self


output:
Chunk ID: a13dce
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 65
Output:
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/InteractsWithElements.php:17:    public function click(string $text): self
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/InteractsWithElements.php:93:    public function hover(string $selector): self


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


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

aggregated_output:
<?php

declare(strict_types=1);

namespace Pest\Browser\Api\Concerns;

use Pest\Browser\Api\Webpage;

/**
 * @mixin Webpage
 */
trait InteractsWithElements
{
    /**
     * Click the link with the given text.
     */
    public function click(string $text): self
    {
        $this->guessLocator($text)->click();

        return $this;
    }

    /**
     * Get the text of the element matching the given selector.
     */
    public function text(string $selector): ?string
    {
        return $this->guessLocator($selector)->textContent();
    }

    /**
     * Get the given attribute from the element matching the given selector.
     */
    public function attribute(string $selector, string $attribute): ?string
    {
        $locator = $this->guessLocator($selector);

        return $locator->getAttribute($attribute) ?? null;
    }

    /**
     * Send the given keys to the element matching the given selector.
     *
     * @param  array<int, string>  $keys
     */
    public function keys(string $selector, array|string $keys): self
    {
        $keys = is_array($keys) ? $keys : [$keys];

        $locator = $this->guessLocator($selector);

        foreach ($keys as $key) {
            $locator->press($key);
        }

        return $this;
    }

    /**
     * Type the given value in the given field.
     */
    public function type(string $field, string $value): self
    {
        $this->guessLocator($field)->fill($value);

        return $this;
    }

    /**
     * Type the given value slowly in the given field.
     */
    public function typeSlowly(string $field, string $value, int $delay = 100): self
    {
        $options = ['delay' => $delay];

        $this->guessLocator($field)->type($value, $options);

        return $this;
    }

    /**
     * Fills the given value in the given field.
     */
    public function fill(string $field, string $value): self
    {
        return $this->type($field, $value);
    }

    /**
     * Hovers over the element matching the given selector.
     */
    public function hover(string $selector): self
    {
        $this->guessLocator($selector)->hover();

        return $this;
    }

    /**
     * Right-click the element matching the given selector.
     */
    public function rightClick(string $text): Webpage
    {
        $this->guessLocator($text)->click([
            'button' => 'right',
        ]);

        return $this;
    }

    /**
     * Select the given value in the given field.
     *
     * @param  array<int, string|int>|string|int  $option
     */
    public function select(string $field, array|string|int $option): self
    {
        $this->guessLocator($field)->selectOption($option);


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

declare(strict_types=1);

namespace Pest\Browser\Api\Concerns;

use Pest\Browser\Api\Webpage;

/**
 * @mixin Webpage
 */
trait InteractsWithElements
{
    /**
     * Click the link with the given text.
     */
    public function click(string $text): self
    {
        $this->guessLocator($text)->click();

        return $this;
    }

    /**
     * Get the text of the element matching the given selector.
     */
    public function text(string $selector): ?string
    {
        return $this->guessLocator($selector)->textContent();
    }

    /**
     * Get the given attribute from the element matching the given selector.
     */
    public function attribute(string $selector, string $attribute): ?string
    {
        $locator = $this->guessLocator($selector);

        return $locator->getAttribute($attribute) ?? null;
    }

    /**
     * Send the given keys to the element matching the given selector.
     *
     * @param  array<int, string>  $keys
     */
    public function keys(string $selector, array|string $keys): self
    {
        $keys = is_array($keys) ? $keys : [$keys];

        $locator = $this->guessLocator($selector);

        foreach ($keys as $key) {
            $locator->press($key);
        }

        return $this;
    }

    /**
     * Type the given value in the given field.
     */
    public function type(string $field, string $value): self
    {
        $this->guessLocator($field)->fill($value);

        return $this;
    }

    /**
     * Type the given value slowly in the given field.
     */
    public function typeSlowly(string $field, string $value, int $delay = 100): self
    {
        $options = ['delay' => $delay];

        $this->guessLocator($field)->type($value, $options);

        return $this;
    }

    /**
     * Fills the given value in the given field.
     */
    public function fill(string $field, string $value): self
    {
        return $this->type($field, $value);
    }

    /**
     * Hovers over the element matching the given selector.
     */
    public function hover(string $selector): self
    {
        $this->guessLocator($selector)->hover();

        return $this;
    }

    /**
     * Right-click the element matching the given selector.
     */
    public function rightClick(string $text): Webpage
    {
        $this->guessLocator($text)->click([
            'button' => 'right',
        ]);

        return $this;
    }

    /**
     * Select the given value in the given field.
     *
     * @param  array<int, string|int>|string|int  $option
     */
    public function select(string $field, array|string|int $option): self
    {
        $this->guessLocator($field)->selectOption($option);


cmd: rg "function guessLocator" -n vendor/pestphp/pest-plugin-browser/src/Api


command: rg "function guessLocator" -n vendor/pestphp/pest-plugin-browser/src/Api

type: search

cmd: rg 'function guessLocator' -n vendor/pestphp/pest-plugin-browser/src/Api

query: function guessLocator

path: Api

aggregated_output: vendor/pestphp/pest-plugin-browser/src/Api/Webpage.php:101:    private function guessLocator(string $selector, ?string $value = null): Locator


output:
Chunk ID: d568f2
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 36
Output:
vendor/pestphp/pest-plugin-browser/src/Api/Webpage.php:101:    private function guessLocator(string $selector, ?string $value = null): Locator


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


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

aggregated_output:
    /**
     * 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);
    }
}


output:
Chunk ID: 73d51b
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 102
Output:
    /**
     * 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);
    }
}


cmd: sed -n '1,260p' vendor/pestphp/pest-plugin-browser/src/Support/GuessLocator.php


command: sed -n '1,260p' vendor/pestphp/pest-plugin-browser/src/Support/GuessLocator.php

aggregated_output:
<?php

declare(strict_types=1);

namespace Pest\Browser\Support;

use Pest\Browser\Playwright\Locator;
use Pest\Browser\Playwright\Page;
use PHPUnit\Framework\ExpectationFailedException;

/**
 * @internal
 */
final readonly class GuessLocator
{
    /**
     * Creates a new guess locator instance.
     */
    public function __construct(
        private Page $page,
    ) {
        //
    }

    /**
     * Guesses the locator for the given page and selector.
     */
    public function for(string $selector, ?string $value = null): Locator
    {
        if (Selector::isExplicit($selector)) {
            if ($value !== null) {
                $selector .= sprintf('[value=%s]', Selector::escapeForAttributeSelectorOrRegex($value, true));
            }

            return $this->page->locator($selector);
        }

        if (Selector::isDataTest($selector)) {
            $id = Selector::escapeForAttributeSelectorOrRegex(str_replace('@', '', $selector), true);

            return $this->page->unstrict(
                fn (): Locator => $this->page->locator(
                    "[data-testid=$id], [data-test=$id]",
                ),
            );
        }

        foreach (['[id="%s"]', '[name="%s"]'] as $format) {
            $formattedSelector = sprintf($format, $selector);

            if ($value !== null) {
                $formattedSelector .= sprintf('[value=%s]', Selector::escapeForAttributeSelectorOrRegex($value, true));
            }

            $locator = $this->page->unstrict(
                fn (): Locator => $this->page->locator($formattedSelector),
            );

            if ($locator->count() > 0) {
                return $locator;
            }
        }

        if ($value !== null) {
            return throw new ExpectationFailedException(
                sprintf('Selector [%s] does not match any element.', $selector)
            );
        }

        return $this->page->unstrict(
            fn (): Locator => $this->page->getByText($selector, true),
        );
    }
}


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

declare(strict_types=1);

namespace Pest\Browser\Support;

use Pest\Browser\Playwright\Locator;
use Pest\Browser\Playwright\Page;
use PHPUnit\Framework\ExpectationFailedException;

/**
 * @internal
 */
final readonly class GuessLocator
{
    /**
     * Creates a new guess locator instance.
     */
    public function __construct(
        private Page $page,
    ) {
        //
    }

    /**
     * Guesses the locator for the given page and selector.
     */
    public function for(string $selector, ?string $value = null): Locator
    {
        if (Selector::isExplicit($selector)) {
            if ($value !== null) {
                $selector .= sprintf('[value=%s]', Selector::escapeForAttributeSelectorOrRegex($value, true));
            }

            return $this->page->locator($selector);
        }

        if (Selector::isDataTest($selector)) {
            $id = Selector::escapeForAttributeSelectorOrRegex(str_replace('@', '', $selector), true);

            return $this->page->unstrict(
                fn (): Locator => $this->page->locator(
                    "[data-testid=$id], [data-test=$id]",
                ),
            );
        }

        foreach (['[id="%s"]', '[name="%s"]'] as $format) {
            $formattedSelector = sprintf($format, $selector);

            if ($value !== null) {
                $formattedSelector .= sprintf('[value=%s]', Selector::escapeForAttributeSelectorOrRegex($value, true));
            }

            $locator = $this->page->unstrict(
                fn (): Locator => $this->page->locator($formattedSelector),
            );

            if ($locator->count() > 0) {
                return $locator;
            }
        }

        if ($value !== null) {
            return throw new ExpectationFailedException(
                sprintf('Selector [%s] does not match any element.', $selector)
            );
        }

        return $this->page->unstrict(
            fn (): Locator => $this->page->getByText($selector, true),
        );
    }
}


command:
/bin/zsh
-lc
sed -n '1,160p' app/Support/Money.php

Money.php

cmd: sed -n '1,160p' app/Support/Money.php

aggregated_output:
<?php

namespace App\Support;

class Money
{
    public static function format(int $amount, string $currency = 'EUR'): string
    {
        return number_format($amount / 100, 2, '.', ',').' '.$currency;
    }

    public static function fromDecimalString(string|int|float|null $amount): int
    {
        if ($amount === null || $amount === '') {
            return 0;
        }

        return (int) round(((float) $amount) * 100);
    }
}


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

namespace App\Support;

class Money
{
    public static function format(int $amount, string $currency = 'EUR'): string
    {
        return number_format($amount / 100, 2, '.', ',').' '.$currency;
    }

    public static function fromDecimalString(string|int|float|null $amount): int
    {
        if ($amount === null || $amount === '') {
            return 0;
        }

        return (int) round(((float) $amount) * 100);
    }
}


message: I’m replacing the generated placeholder with the 15 storefront browsing cases: seeded home/collection/product/search/page coverage, stock messaging, draft visibility, and the desktop nav click.


text: I’m replacing the generated placeholder with the 15 storefront browsing cases: seeded home/collection/product/search/page coverage, stock messaging, draft visibility, and the desktop nav click.


status: completed

input:
*** Begin Patch
*** Update File: tests/Browser/Storefront/BrowsingTest.php
@@
 <?php

-test('example', function () {
-    $response = $this->get('/');
+use Database\Seeders\DatabaseSeeder;
+use Illuminate\Foundation\Testing\RefreshDatabase;

-    $response->assertStatus(200);
+uses(RefreshDatabase::class);
+
+beforeEach(function (): void {
+    $this->seed(DatabaseSeeder::class);
+});
+
+function storefrontBrowsingHost(): array
+{
+    return ['host' => 'shop.test'];
+}
+
+test('shows featured products on home page', function (): void {
+    visit('/', storefrontBrowsingHost())
+        ->assertSee('Acme Fashion')
+        ->assertSee('Classic Cotton T-Shirt')
+        ->assertSee('24.99')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows collection with product grid', function (): void {
+    visit('/collections/t-shirts', storefrontBrowsingHost())
+        ->assertSee('T-Shirts')
+        ->assertSee('Classic Cotton T-Shirt')
+        ->assertNoJavaScriptErrors();
+});
+
+test('can navigate from collection to product', function (): void {
+    visit('/collections/t-shirts', storefrontBrowsingHost())
+        ->click('a[href$="/products/classic-cotton-t-shirt"]')
+        ->wait(1)
+        ->assertPathIs('/products/classic-cotton-t-shirt')
+        ->assertSee('Classic Cotton T-Shirt')
+        ->assertSee('24.99')
+        ->assertSee('Add to cart')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows product detail with variant options', function (): void {
+    visit('/products/classic-cotton-t-shirt', storefrontBrowsingHost())
+        ->assertSee('Classic Cotton T-Shirt')
+        ->assertSee('24.99')
+        ->assertSee('Size')
+        ->assertSee('Color')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows size and color option values', function (): void {
+    visit('/products/classic-cotton-t-shirt', storefrontBrowsingHost())
+        ->assertSee('S')
+        ->assertSee('M')
+        ->assertSee('L')
+        ->assertSee('XL')
+        ->assertSee('Black')
+        ->assertSee('White')
+        ->assertSee('Navy')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows sale and compare at pricing for sale product', function (): void {
+    visit('/products/premium-slim-fit-jeans', storefrontBrowsingHost())
+        ->assertSee('Premium Slim Fit Jeans')
+        ->assertSee('79.99')
+        ->assertSee('99.99')
+        ->assertPresent('.line-through')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows search results for valid query', function (): void {
+    visit('/search?q=cotton', storefrontBrowsingHost())
+        ->assertSee('Classic Cotton T-Shirt')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows no results message for invalid query', function (): void {
+    visit('/search?q=zznonexistentproductzz', storefrontBrowsingHost())
+        ->assertSee('No products found')
+        ->assertNoJavaScriptErrors();
+});
+
+test('does not show draft products on storefront collections', function (): void {
+    visit('/collections', storefrontBrowsingHost())
+        ->assertDontSee('Unreleased Winter Jacket')
+        ->assertNoJavaScriptErrors();
+});
+
+test('does not show draft products in search results', function (): void {
+    visit('/search?q=draft', storefrontBrowsingHost())
+        ->assertDontSee('Unreleased Winter Jacket')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows out of stock messaging for deny policy product', function (): void {
+    visit('/products/limited-edition-sneakers', storefrontBrowsingHost())
+        ->assertSee('Limited Edition Sneakers')
+        ->assertSee('Out of stock')
+        ->assertSee('Sold out')
+        ->assertButtonDisabled('button:has-text("Sold out")')
+        ->assertDontSee('Add to cart')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows backorder messaging for continue policy product', function (): void {
+    visit('/products/backorder-denim-jacket', storefrontBrowsingHost())
+        ->assertSee('Backorder Denim Jacket')
+        ->assertSee('Available on backorder')
+        ->assertSee('Add to cart')
+        ->assertButtonEnabled('button:has-text("Add to cart")')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows new arrivals collection', function (): void {
+    visit('/collections/new-arrivals', storefrontBrowsingHost())
+        ->assertSee('New Arrivals')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows static about page', function (): void {
+    visit('/pages/about', storefrontBrowsingHost())
+        ->assertSee('About')
+        ->assertNoJavaScriptErrors();
+});
+
+test('navigates between pages using the main navigation', function (): void {
+    visit('/', storefrontBrowsingHost())
+        ->hover('nav[aria-label="Main navigation"] a[href$="/collections"]')
+        ->click('nav[aria-label="Main navigation"] a[href$="/collections/t-shirts"]')
+        ->wait(1)
+        ->assertPathIs('/collections/t-shirts')
+        ->assertSee('T-Shirts')
+        ->assertNoJavaScriptErrors();
 });
*** End Patch


stdout:
Success. Updated the following files:
M tests/Browser/Storefront/BrowsingTest.php

type: update

unified_diff:
@@ -2,6 +2,135 @@

-test('example', function () {
-    $response = $this->get('/');
+use Database\Seeders\DatabaseSeeder;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+
+uses(RefreshDatabase::class);
+
+beforeEach(function (): void {
+    $this->seed(DatabaseSeeder::class);
+});
+
+function storefrontBrowsingHost(): array
+{
+    return ['host' => 'shop.test'];
+}
+
+test('shows featured products on home page', function (): void {
+    visit('/', storefrontBrowsingHost())
+        ->assertSee('Acme Fashion')
+        ->assertSee('Classic Cotton T-Shirt')
+        ->assertSee('24.99')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows collection with product grid', function (): void {
+    visit('/collections/t-shirts', storefrontBrowsingHost())
+        ->assertSee('T-Shirts')
+        ->assertSee('Classic Cotton T-Shirt')
+        ->assertNoJavaScriptErrors();
+});
+
+test('can navigate from collection to product', function (): void {
+    visit('/collections/t-shirts', storefrontBrowsingHost())
+        ->click('a[href$="/products/classic-cotton-t-shirt"]')
+        ->wait(1)
+        ->assertPathIs('/products/classic-cotton-t-shirt')
+        ->assertSee('Classic Cotton T-Shirt')
+        ->assertSee('24.99')
+        ->assertSee('Add to cart')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows product detail with variant options', function (): void {
+    visit('/products/classic-cotton-t-shirt', storefrontBrowsingHost())
+        ->assertSee('Classic Cotton T-Shirt')
+        ->assertSee('24.99')
+        ->assertSee('Size')
+        ->assertSee('Color')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows size and color option values', function (): void {
+    visit('/products/classic-cotton-t-shirt', storefrontBrowsingHost())
+        ->assertSee('S')
+        ->assertSee('M')
+        ->assertSee('L')
+        ->assertSee('XL')
+        ->assertSee('Black')
+        ->assertSee('White')
+        ->assertSee('Navy')
+        ->assertNoJavaScriptErrors();
+});

-    $response->assertStatus(200);
+test('shows sale and compare at pricing for sale product', function (): void {
+    visit('/products/premium-slim-fit-jeans', storefrontBrowsingHost())
+        ->assertSee('Premium Slim Fit Jeans')
+        ->assertSee('79.99')
+        ->assertSee('99.99')
+        ->assertPresent('.line-through')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows search results for valid query', function (): void {
+    visit('/search?q=cotton', storefrontBrowsingHost())
+        ->assertSee('Classic Cotton T-Shirt')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows no results message for invalid query', function (): void {
+    visit('/search?q=zznonexistentproductzz', storefrontBrowsingHost())
+        ->assertSee('No products found')
+        ->assertNoJavaScriptErrors();
+});
+
+test('does not show draft products on storefront collections', function (): void {
+    visit('/collections', storefrontBrowsingHost())
+        ->assertDontSee('Unreleased Winter Jacket')
+        ->assertNoJavaScriptErrors();
+});
+
+test('does not show draft products in search results', function (): void {
+    visit('/search?q=draft', storefrontBrowsingHost())
+        ->assertDontSee('Unreleased Winter Jacket')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows out of stock messaging for deny policy product', function (): void {
+    visit('/products/limited-edition-sneakers', storefrontBrowsingHost())
+        ->assertSee('Limited Edition Sneakers')
+        ->assertSee('Out of stock')
+        ->assertSee('Sold out')
+        ->assertButtonDisabled('button:has-text("Sold out")')
+        ->assertDontSee('Add to cart')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows backorder messaging for continue policy product', function (): void {
+    visit('/products/backorder-denim-jacket', storefrontBrowsingHost())
+        ->assertSee('Backorder Denim Jacket')
+        ->assertSee('Available on backorder')
+        ->assertSee('Add to cart')
+        ->assertButtonEnabled('button:has-text("Add to cart")')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows new arrivals collection', function (): void {
+    visit('/collections/new-arrivals', storefrontBrowsingHost())
+        ->assertSee('New Arrivals')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows static about page', function (): void {
+    visit('/pages/about', storefrontBrowsingHost())
+        ->assertSee('About')
+        ->assertNoJavaScriptErrors();
+});
+
+test('navigates between pages using the main navigation', function (): void {
+    visit('/', storefrontBrowsingHost())
+        ->hover('nav[aria-label="Main navigation"] a[href$="/collections"]')
+        ->click('nav[aria-label="Main navigation"] a[href$="/collections/t-shirts"]')
+        ->wait(1)
+        ->assertPathIs('/collections/t-shirts')
+        ->assertSee('T-Shirts')
+        ->assertNoJavaScriptErrors();
 });


output: {"output":"Success. Updated the following files:\nM tests/Browser/Storefront/BrowsingTest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


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