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

namespace App\Livewire\Admin\Products;

use App\Enums\MediaStatus;
use App\Enums\MediaType;
use App\Enums\ProductStatus;
use App\Enums\VariantStatus;
use App\Jobs\ProcessMediaUpload;
use App\Models\Collection;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductMedia;
use App\Models\ProductOption;
use App\Models\ProductOptionValue;
use App\Models\ProductVariant;
use App\Models\Store;
use App\Services\ProductService;
use App\Support\Money;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Livewire\Component;
use Livewire\WithFileUploads;

class Form extends Component
{
    use WithFileUploads;

    public ?Product $product = null;

    public string $title = '';

    public string $descriptionHtml = '';

    public string $status = 'active';

    public string $vendor = '';

    public string $productType = '';

    public string $tags = '';

    public string $handle = '';

    public ?string $publishedAt = null;

    /**
     * @var array<int, int>
     */
    public array $collectionIds = [];

    /**
     * @var array<int, array{name: string, values: string}>
     */
    public array $options = [];

    /**
     * @var array<int, array{id: int|null, label: string, sku: string, price: string, compareAtPrice: string, quantity: int, requiresShipping: bool, options: array<string, string>}>
     */
    public array $variants = [];

    /**
     * @var array<int, array{id: int, url: string, exists: bool, altText: string, position: int, status: string}>
     */
    public array $media = [];

    /**
     * @var array<int, mixed>
     */
    public array $newMedia = [];

    public function mount(?Product $product = null): void
    {
        if ($product?->exists) {
            $store = app('current_store');

            abort_unless($store instanceof Store && (int) $product->store_id === $store->getKey(), 404);

            $this->product = $product->load(['options.values', 'variants.inventoryItem', 'variants.optionValues.option', 'collections', 'media']);
            $this->fillFromProduct($this->product);

            return;
        }

        $this->variants = [[
            'id' => null,
            'label' => 'Default',
            'sku' => '',
            'price' => '0.00',
            'compareAtPrice' => '',
            'quantity' => 0,
            'requiresShipping' => true,
            'options' => [],
        ]];
    }

    public function updatedTitle(): void
    {
        if ($this->product === null && $this->handle === '') {
            $this->handle = Str::slug($this->title);
        }
    }

    public function addOption(): void
    {
        $this->options[] = [
            'name' => '',
            'values' => '',
        ];
    }

    public function removeOption(int $index): void
    {
        unset($this->options[$index]);
        $this->options = array_values($this->options);
    }

    public function generateVariants(): void
    {
        $optionPayload = $this->optionPayload();

        if ($optionPayload === []) {
            $template = $this->variants[0] ?? [];
            $this->variants = [[
                'id' => $template['id'] ?? null,
                'label' => 'Default',
                'sku' => $template['sku'] ?? '',
                'price' => $template['price'] ?? '0.00',
                'compareAtPrice' => $template['compareAtPrice'] ?? '',
                'quantity' => (int) ($template['quantity'] ?? 0),
                'requiresShipping' => (bool) ($template['requiresShipping'] ?? true),
                'options' => [],
            ]];

            return;
        }

        $existingVariants = collect($this->variants)
            ->keyBy(fn (array $variant): string => $this->variantOptionKey($variant['options'] ?? []));
        $template = $this->variants[0] ?? [];

        $this->variants = collect($this->optionCombinations($optionPayload))
            ->map(function (array $options) use ($existingVariants, $template): array {
                $existingVariant = $existingVariants->get($this->variantOptionKey($options));

                return [
                    'id' => $existingVariant['id'] ?? null,
                    'label' => $this->variantLabel($options),
                    'sku' => $existingVariant['sku'] ?? '',
                    'price' => $existingVariant['price'] ?? ($template['price'] ?? '0.00'),
                    'compareAtPrice' => $existingVariant['compareAtPrice'] ?? ($template['compareAtPrice'] ?? ''),
                    'quantity' => (int) ($existingVariant['quantity'] ?? 0),
                    'requiresShipping' => (bool) ($existingVariant['requiresShipping'] ?? ($template['requiresShipping'] ?? true)),
                    'options' => $options,
                ];
            })
            ->values()
            ->all();
    }

    public function save(): void
    {
        $store = app('current_store');
        abort_unless($store instanceof Store, 404);

        $this->ensureVariantMatrixMatchesOptions();

        $this->validate([
            'title' => ['required', 'string', 'max:255'],
            'descriptionHtml' => ['nullable', 'string', 'max:65535'],
            'status' => ['required', Rule::in(['draft', 'active', 'archived'])],
            'vendor' => ['nullable', 'string', 'max:255'],
            'productType' => ['nullable', 'string', 'max:255'],
            'tags' => ['nullable', 'string'],
            'handle' => [
                'required',
                'string',
                'max:255',
                Rule::unique('products', 'handle')
                    ->where('store_id', $store->getKey())
                    ->ignore($this->product?->getKey()),
            ],
            'variants' => ['required', 'array', 'min:1'],
            'variants.*.sku' => ['nullable', 'string', 'max:255'],
            'variants.*.price' => ['required', 'numeric', 'min:0'],
            'variants.*.compareAtPrice' => ['nullable', 'numeric', 'min:0'],
            'variants.*.quantity' => ['required', 'integer', 'min:0'],
            'variants.*.requiresShipping' => ['boolean'],
            'newMedia' => ['array', 'max:10'],
            'newMedia.*' => ['image', 'mimes:jpg,jpeg,png,gif,webp', 'max:5120'],
        ]);

        if (! $this->validateActiveVariantPricing() || ! $this->validateVariantSkus($store)) {
            return;
        }

        $product = DB::transaction(function () use ($store): Product {
            $productService = app(ProductService::class);

            if ($this->product === null) {
                $product = $productService->create($store, [
                    ...$this->productPayload(),
                    'options' => $this->optionPayload(),
                    'variants' => $this->variantPayload($store),
                ]);
            } else {
                $payload = $this->productPayload();
                $requestedStatus = ProductStatus::from((string) $payload['status']);
                unset($payload['status']);

                $product = $productService->update($this->product, $payload);
                $this->syncProductOptions($product);
                $this->syncExistingVariants($product, $store);

                if ($requestedStatus !== $product->refresh()->status) {
                    $productService->transitionStatus($product, $requestedStatus);
                }
            }

            $product->collections()->sync($this->collectionIds);

            return $product->refresh();
        });

        if ($this->newMedia !== []) {
            $this->storeNewMedia($product);
        }

        $this->product = $product->load(['options.values', 'variants.inventoryItem', 'variants.optionValues.option', 'collections', 'media']);
        $this->fillFromProduct($this->product);

        session()->flash('status', 'Product saved');
        $this->dispatch('toast', type: 'success', message: __('Product saved'));
    }

    public function uploadMedia(): void
    {
        $product = $this->productForAction();

        $this->validate([
            'newMedia' => ['required', 'array', 'max:10'],
            'newMedia.*' => ['image', 'mimes:jpg,jpeg,png,gif,webp', 'max:5120'],
        ]);

        $this->storeNewMedia($product);

        $this->product = $product->refresh()->load(['options.values', 'variants.inventoryItem', 'variants.optionValues.option', 'collections', 'media']);
        $this->fillFromProduct($this->product);

        session()->flash('status', 'Media uploaded');
        $this->dispatch('toast', type: 'success', message: __('Media uploaded'));
    }

    public function updateMediaAlt(int $mediaId): void
    {
        $index = $this->mediaIndex($mediaId);

        $this->validate([
            "media.{$index}.altText" => ['nullable', 'string', 'max:255'],
        ]);

        $media = $this->mediaRecord($mediaId);
        $altText = trim((string) $this->media[$index]['altText']);

        $media->forceFill([
            'alt_text' => $altText === '' ? null : $altText,
        ])->save();

        $this->refreshProductMedia();

        session()->flash('status', 'Media updated');
    }

    public function moveMedia(int $mediaId, string $direction): void
    {
        $ids = collect($this->media)->pluck('id')->map(fn (int $id): int => $id)->all();
        $index = array_search($mediaId, $ids, true);

        if ($index === false) {
            return;
        }

        $swapIndex = match ($direction) {
            'up' => $index - 1,
            'down' => $index + 1,
            default => $index,
        };

        if (! isset($ids[$swapIndex])) {
            return;
        }

        [$ids[$index], $ids[$swapIndex]] = [$ids[$swapIndex], $ids[$index]];

        $this->reorderMedia($ids);
    }

    /**
     * @param  array<int, int>  $order
     */
    public function reorderMedia(array $order): void
    {
        $product = $this->productForAction();
        $validIds = ProductMedia::withoutGlobalScopes()
            ->where('product_id', $product->getKey())
            ->whereIn('id', $order)
            ->pluck('id')
            ->all();

        $orderedIds = collect($order)
            ->map(fn (mixed $mediaId): int => (int) $mediaId)
            ->intersect($validIds)
            ->values();

        DB::transaction(function () use ($orderedIds): void {
            foreach ($orderedIds as $position => $mediaId) {
                ProductMedia::withoutGlobalScopes()
                    ->whereKey($mediaId)
                    ->update(['position' => $position]);
            }
        });

        $this->refreshProductMedia();
    }

    public function removeMedia(int $mediaId): void
    {
        $media = $this->mediaRecord($mediaId);

        $media->delete();

        $this->reorderMedia(
            collect($this->media)
                ->pluck('id')
                ->reject(fn (int $id): bool => $id === $mediaId)
                ->values()
                ->all(),

            <flux:breadcrumbs>
                <flux:breadcrumbs.item :href="route('admin.dashboard')" wire:navigate>Home</flux:breadcrumbs.item>
                <flux:breadcrumbs.item :href="route('admin.products.index')" wire:navigate>Products</flux:breadcrumbs.item>
                <flux:breadcrumbs.item>{{ $isEditing ? $title : 'Add product' }}</flux:breadcrumbs.item>
            </flux:breadcrumbs>

            <flux:heading size="xl" class="mt-3">{{ $isEditing ? $title : 'Add product' }}</flux:heading>

        @if ($isEditing)
            <flux:button wire:click="deleteProduct" variant="danger" icon="archive-box">Archive</flux:button>
        @endif

    @if (session('status'))
        <flux:callout color="green" icon="check-circle">{{ session('status') }}</flux:callout>
    @endif

    <form wire:submit="save" class="grid gap-6 lg:grid-cols-[minmax(0,2fr)_minmax(280px,1fr)]">

                    <flux:input wire:model.live.debounce.300ms="title" label="Title" placeholder="Short Sleeve T-Shirt" />
                    <flux:error name="title" />

                    <flux:textarea wire:model="descriptionHtml" label="Description" rows="8" placeholder="Describe your product..." />
                    <flux:error name="descriptionHtml" />

                    <flux:heading size="lg">Media</flux:heading>
                    <flux:text>{{ count($media) }} {{ Str::plural('item', count($media)) }}</flux:text>

                        <flux:input
                            wire:model="newMedia"
                            type="file"
                            label="Images"
                            multiple
                            accept="image/*"
                            data-test="product-media-input"
                        />

                        @if ($isEditing)
                            <flux:button type="button" wire:click="uploadMedia" variant="primary" icon="cloud-arrow-up" wire:loading.attr="disabled" wire:target="newMedia,uploadMedia" data-test="product-media-upload-button">
                                Upload
                            </flux:button>
                        @endif

                    <flux:error name="newMedia" />
                    <flux:error name="newMedia.0" />

                    @forelse ($media as $index => $item)

                                @if ($item['exists'])

                                @else

                                        <flux:icon.photo class="size-10" />

                                @endif

                                    <flux:badge :color="$item['status'] === 'ready' ? 'green' : ($item['status'] === 'failed' ? 'red' : 'amber')">
                                        {{ Str::headline($item['status']) }}
                                    </flux:badge>

                                        <flux:button type="button" wire:click="moveMedia({{ $item['id'] }}, 'up')" variant="ghost" icon="arrow-up" size="sm" :aria-label="__('Move media up')" />
                                        <flux:button type="button" wire:click="moveMedia({{ $item['id'] }}, 'down')" variant="ghost" icon="arrow-down" size="sm" :aria-label="__('Move media down')" />
                                        <flux:button type="button" wire:click="removeMedia({{ $item['id'] }})" variant="ghost" icon="trash" size="sm" :aria-label="__('Remove media')" />

                                    <flux:input wire:model="media.{{ $index }}.altText" label="Alt text" />
                                    <flux:error name="media.{{ $index }}.altText" />
                                    <flux:button type="button" wire:click="updateMediaAlt({{ $item['id'] }})" variant="ghost" icon="check" size="sm">Save alt text</flux:button>

                    @empty

                            No media for this product.

                    @endforelse

                    <flux:heading size="lg">Variants</flux:heading>
                    <flux:text>{{ count($variants) }} {{ Str::plural('variant', count($variants)) }}</flux:text>

                                Variant
                                SKU
                                Price
                                Compare
                                Qty
                                Ship

                            @foreach ($variants as $index => $variant)

                                    {{ $variant['label'] }}

                                        <flux:input wire:model="variants.{{ $index }}.sku" aria-label="Variant SKU" />

                                        <flux:input wire:model="variants.{{ $index }}.price" type="number" step="0.01" min="0" aria-label="Variant price" />

                                        <flux:input wire:model="variants.{{ $index }}.compareAtPrice" type="number" step="0.01" min="0" aria-label="Compare at price" />

                                        <flux:input wire:model="variants.{{ $index }}.quantity" type="number" min="0" aria-label="Variant quantity" />

                                        <flux:checkbox wire:model="variants.{{ $index }}.requiresShipping" aria-label="Requires shipping" />

                            @endforeach

                    <flux:heading size="lg">Options</flux:heading>

                        <flux:button type="button" wire:click="generateVariants" variant="filled" icon="squares-2x2">Generate variants</flux:button>
                        <flux:button type="button" wire:click="addOption" variant="ghost" icon="plus">Add option</flux:button>

                    @forelse ($options as $index => $option)

                            <flux:input wire:model="options.{{ $index }}.name" label="Option name" placeholder="Size" />
                            <flux:input wire:model="options.{{ $index }}.values" label="Values" placeholder="S, M, L, XL" />

                                <flux:button type="button" wire:click="removeOption({{ $index }})" variant="ghost" icon="trash">Remove</flux:button>

                    @empty
                        <flux:text>No options for this product.</flux:text>
                    @endforelse

                    <flux:select wire:model="status" label="Status">
                        <flux:select.option value="draft">Draft</flux:select.option>
                        <flux:select.option value="active">Active</flux:select.option>
                        <flux:select.option value="archived">Archived</flux:select.option>
                    </flux:select>

                    <flux:input wire:model="handle" label="URL handle" placeholder="short-sleeve-t-shirt" />
                    <flux:error name="handle" />

                    <flux:input wire:model="vendor" label="Vendor" placeholder="Acme Basics" />
                    <flux:input wire:model="productType" label="Product type" placeholder="T-Shirts" />
                    <flux:input wire:model="tags" label="Tags" placeholder="new, cotton, sale" />

                <flux:heading size="lg">Collections</flux:heading>

                    @forelse ($availableCollections as $collection)
                        <flux:checkbox wire:model="collectionIds" value="{{ $collection->getKey() }}" :label="$collection->title" wire:key="product-collection-{{ $collection->getKey() }}" />
                    @empty
                        <flux:text>No collections yet.</flux:text>
                    @endforelse

                <flux:button :href="route('admin.products.index')" wire:navigate variant="ghost">Discard</flux:button>
                <flux:button type="submit" variant="primary" wire:loading.attr="disabled">
                    Save
                    Saving...
                </flux:button>

    </form>


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

use App\Livewire\Admin\Collections\Form as CollectionForm;
use App\Livewire\Admin\Products\Form as ProductForm;
use App\Livewire\Admin\Products\Index as ProductIndex;
use App\Models\Collection;
use App\Models\Product;
use App\Models\Store;
use App\Models\User;
use App\Services\ProductService;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;

uses(RefreshDatabase::class);

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

test('storefront catalog browsing routes render seeded products', function () {
    $this->withHeader('Host', 'shop.test')
        ->get('/')
        ->assertSuccessful()
        ->assertSee('Acme Fashion')
        ->assertSee('Classic Cotton T-Shirt')
        ->assertSee('24.99 EUR');

    $this->withHeader('Host', 'shop.test')
        ->get('/collections/t-shirts')
        ->assertSuccessful()
        ->assertSee('T-Shirts')
        ->assertSee('Classic Cotton T-Shirt')
        ->assertDontSee('Unreleased Winter Jacket');

    $this->withHeader('Host', 'shop.test')
        ->get('/products/classic-cotton-t-shirt')
        ->assertSuccessful()
        ->assertSee('Classic Cotton T-Shirt')
        ->assertSee('Size')
        ->assertSee('Color')
        ->assertSee('Add to cart');

    $this->withHeader('Host', 'shop.test')
        ->get('/search?q=draft')
        ->assertSuccessful()
        ->assertDontSee('Unreleased Winter Jacket');
});

test('admin catalog routes require authentication and render for store admins', function () {
    $this->get('/admin/products')->assertRedirect('/admin/login');

    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
    $user = User::query()->where('email', 'admin@acme.test')->firstOrFail();

    $this->actingAs($user)
        ->withSession(['current_store_id' => $store->getKey()])
        ->get('/admin/products')
        ->assertSuccessful()
        ->assertSee('Products')
        ->assertSee('Add product');

    $this->actingAs($user)
        ->withSession(['current_store_id' => $store->getKey()])
        ->get('/admin/collections')
        ->assertSuccessful()
        ->assertSee('Collections')
        ->assertSee('T-Shirts');

    $this->actingAs($user)
        ->withSession(['current_store_id' => $store->getKey()])
        ->get('/admin/inventory')
        ->assertSuccessful()
        ->assertSee('Inventory')
        ->assertSee('Limited Edition Sneakers');
});

test('admin product form creates edits archives and index filters products', function () {
    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
    $user = User::query()->where('email', 'admin@acme.test')->firstOrFail();
    app()->instance('current_store', $store);

    Livewire::actingAs($user)
        ->test(ProductForm::class)
        ->set('title', 'Test Product Created by E2E')
        ->set('handle', 'test-product-created-by-e2e')
        ->set('descriptionHtml', 'This product was created by the E2E test suite.')
        ->set('vendor', 'Test Vendor')
        ->set('productType', 'T-Shirts')
        ->set('variants.0.price', '29.99')
        ->set('variants.0.sku', 'E2E-TEST-001')
        ->set('variants.0.quantity', 50)
        ->call('save')
        ->assertSee('Product saved');

    $product = Product::query()->where('handle', 'test-product-created-by-e2e')->firstOrFail();

    expect($product->variants()->first()?->price_amount)->toBe(2999)
        ->and($product->variants()->first()?->inventoryItem?->quantity_on_hand)->toBe(50);

    Livewire::actingAs($user)
        ->test(ProductForm::class, ['product' => $product])
        ->set('title', 'Test Product Updated')
        ->call('save')
        ->assertSee('Product saved');

    Livewire::actingAs($user)
        ->test(ProductForm::class, ['product' => $product->refresh()])
        ->set('variants.0.sku', 'ACME-CTSH-S-WHT')
        ->call('save')
        ->assertHasErrors(['variants.0.sku']);

    expect($product->refresh()->variants()->first()?->sku)->toBe('E2E-TEST-001');

    $draftProduct = app(ProductService::class)->create($store, [
        'title' => 'Draft Product With Price Later',
        'handle' => 'draft-product-with-price-later',
        'status' => 'draft',
        'variants' => [[
            'sku' => 'DRAFT-PRICE-LATER-001',
            'price_amount' => 0,
            'currency' => $store->default_currency,
            'quantity_on_hand' => 4,
            'is_default' => true,
            'position' => 0,
        ]],
    ]);

    Livewire::actingAs($user)
        ->test(ProductForm::class, ['product' => $draftProduct])
        ->set('variants.0.price', '17.50')
        ->set('status', 'active')
        ->call('save')
        ->assertSee('Product saved');

    expect($draftProduct->refresh()->status->value)->toBe('active')
        ->and($draftProduct->variants()->first()?->price_amount)->toBe(1750);

    Livewire::actingAs($user)
        ->test(ProductForm::class, ['product' => $product->refresh()])
        ->set('status', 'archived')
        ->call('save')
        ->assertSee('Product saved');

    expect($product->refresh()->status->value)->toBe('archived');

    Livewire::actingAs($user)
        ->test(ProductIndex::class)
        ->set('statusFilter', 'draft')
        ->assertSee('Unreleased Winter Jacket')
        ->assertDontSee('Classic Cotton T-Shirt')
        ->set('statusFilter', 'active')
        ->set('search', 'Cotton')
        ->assertSee('Classic Cotton T-Shirt');
});

test('admin product form generates and syncs option variant matrix', function () {
    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
    $user = User::query()->where('email', 'admin@acme.test')->firstOrFail();
    app()->instance('current_store', $store);

    $component = Livewire::actingAs($user)
        ->test(ProductForm::class)
        ->set('title', 'Matrix Product')
        ->set('handle', 'matrix-product')
        ->set('options', [
            ['name' => 'Size', 'values' => 'S, M'],
            ['name' => 'Color', 'values' => 'Black, White'],
        ])
        ->call('generateVariants');

    $generatedVariants = $component->get('variants');

    expect($generatedVariants)->toHaveCount(4)
        ->and($generatedVariants[0]['label'])->toBe('S / Black')
        ->and($generatedVariants[3]['label'])->toBe('M / White');

    foreach (range(0, 3) as $index) {
        $component
            ->set("variants.{$index}.sku", 'MATRIX-'.$index)
            ->set("variants.{$index}.price", '19.99')
            ->set("variants.{$index}.quantity", 10 + $index);
    }

    $component
        ->call('save')
        ->assertHasNoErrors()
        ->assertSee('Product saved');

    $product = Product::query()->where('handle', 'matrix-product')->firstOrFail();
    $labels = matrixVariantLabels($product);

    expect($product->variants)->toHaveCount(4)
        ->and($labels)->toBe(['M / Black', 'M / White', 'S / Black', 'S / White']);

    $editComponent = Livewire::actingAs($user)
        ->test(ProductForm::class, ['product' => $product->refresh()])
        ->set('options.1.values', 'Black, Navy')
        ->call('generateVariants');

    expect(collect($editComponent->get('variants'))->pluck('label')->sort()->values()->all())
        ->toBe(['M / Black', 'M / Navy', 'S / Black', 'S / Navy']);

    $editComponent
        ->call('save')
        ->assertHasNoErrors()
        ->assertSee('Product saved');

    expect(matrixVariantLabels($product->refresh()))->toBe(['M / Black', 'M / Navy', 'S / Black', 'S / Navy']);
});

function matrixVariantLabels(Product $product): array
{
    return $product->variants()
        ->with(['optionValues.option'])
        ->get()
        ->map(fn ($variant): string => $variant->optionValues
            ->sortBy(fn ($value): int => $value->option->position)
            ->pluck('value')
            ->implode(' / '))
        ->sort()
        ->values()
        ->all();
}

test('admin collection form creates and assigns products', function () {
    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
    $user = User::query()->where('email', 'admin@acme.test')->firstOrFail();
    $product = Product::query()->where('handle', 'classic-cotton-t-shirt')->firstOrFail();
    app()->instance('current_store', $store);

    Livewire::actingAs($user)
        ->test(CollectionForm::class)
        ->set('title', 'E2E Test Collection')
        ->set('handle', 'e2e-test-collection')
        ->set('descriptionHtml', '
Created from tests.
')
        ->call('addProduct', $product->getKey())
        ->call('save')
        ->assertSee('Collection saved');

    $collection = Collection::query()->where('handle', 'e2e-test-collection')->firstOrFail();

    expect($collection->products()->pluck('products.id')->all())->toBe([$product->getKey()]);
});


output:
Chunk ID: 26dd4b
Wall time: 0.1924 seconds
Process exited with code 0
Original token count: 21
Output:

   ERROR  Your application doesn't have any routes matching the given criteria.


command: php artisan route:list --name=admin.products --except-vendor

type: unknown

aggregated_output: ERROR  Your application doesn't have any routes matching the given criteria.

status: completed


cmd: sed -n '280,620p' app/Livewire/Admin/Products/Form.php


cmd: sed -n '180,360p' resources/views/livewire/admin/products/form.blade.php


cmd: php artisan route:list --path=admin/products --except-vendor


cmd: rg -n "Unreleased|Classic Cotton|Premium Slim|status|draft|archiv" database/seeders app/Services tests -g '*.php'


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


cmd: rg -n "assertDontSee|assertSeeIn|select\(|fill\(|clear|value\(" tests/Browser vendor/pestphp/pest-plugin-browser/src/Api/Concerns -g '*.php'


command: sed -n '180,360p' resources/views/livewire/admin/products/form.blade.php

form.blade.php

type: read

aggregated_output:
<flux:select.option value="draft">Draft</flux:select.option>
                        <flux:select.option value="active">Active</flux:select.option>
                        <flux:select.option value="archived">Archived</flux:select.option>
                    </flux:select>

                    <flux:input wire:model="handle" label="URL handle" placeholder="short-sleeve-t-shirt" />
                    <flux:error name="handle" />

                    <flux:input wire:model="vendor" label="Vendor" placeholder="Acme Basics" />
                    <flux:input wire:model="productType" label="Product type" placeholder="T-Shirts" />
                    <flux:input wire:model="tags" label="Tags" placeholder="new, cotton, sale" />

                <flux:heading size="lg">Collections</flux:heading>

                    @forelse ($availableCollections as $collection)
                        <flux:checkbox wire:model="collectionIds" value="{{ $collection->getKey() }}" :label="$collection->title" wire:key="product-collection-{{ $collection->getKey() }}" />
                    @empty
                        <flux:text>No collections yet.</flux:text>
                    @endforelse

                <flux:button :href="route('admin.products.index')" wire:navigate variant="ghost">Discard</flux:button>
                <flux:button type="submit" variant="primary" wire:loading.attr="disabled">
                    Save
                    Saving...
                </flux:button>

    </form>


command: sed -n '280,620p' app/Livewire/Admin/Products/Form.php

aggregated_output:
        $index = array_search($mediaId, $ids, true);

        if ($index === false) {
            return;
        }

        $swapIndex = match ($direction) {
            'up' => $index - 1,
            'down' => $index + 1,
            default => $index,
        };

        if (! isset($ids[$swapIndex])) {
            return;
        }

        [$ids[$index], $ids[$swapIndex]] = [$ids[$swapIndex], $ids[$index]];

        $this->reorderMedia($ids);
    }

    /**
     * @param  array<int, int>  $order
     */
    public function reorderMedia(array $order): void
    {
        $product = $this->productForAction();
        $validIds = ProductMedia::withoutGlobalScopes()
            ->where('product_id', $product->getKey())
            ->whereIn('id', $order)
            ->pluck('id')
            ->all();

        $orderedIds = collect($order)
            ->map(fn (mixed $mediaId): int => (int) $mediaId)
            ->intersect($validIds)
            ->values();

        DB::transaction(function () use ($orderedIds): void {
            foreach ($orderedIds as $position => $mediaId) {
                ProductMedia::withoutGlobalScopes()
                    ->whereKey($mediaId)
                    ->update(['position' => $position]);
            }
        });

        $this->refreshProductMedia();
    }

    public function removeMedia(int $mediaId): void
    {
        $media = $this->mediaRecord($mediaId);

        $media->delete();

        $this->reorderMedia(
            collect($this->media)
                ->pluck('id')
                ->reject(fn (int $id): bool => $id === $mediaId)
                ->values()
                ->all(),
        );

        session()->flash('status', 'Media removed');
    }

    public function deleteProduct(): void
    {
        abort_unless($this->product instanceof Product, 404);

        app(ProductService::class)->transitionStatus($this->product, ProductStatus::Archived);

        session()->flash('status', 'Product saved');
        $this->redirectRoute('admin.products.index', navigate: true);
    }

    public function render(): mixed
    {
        return view('livewire.admin.products.form', [
            'availableCollections' => Collection::query()->orderBy('title')->get(),
            'isEditing' => $this->product !== null,
        ])->layout('layouts.app', [
            'title' => $this->product ? $this->product->title : __('Add product'),
        ]);
    }

    private function fillFromProduct(Product $product): void
    {
        $this->title = $product->title;
        $this->descriptionHtml = (string) $product->description_html;
        $this->status = $product->status->value;
        $this->vendor = (string) $product->vendor;
        $this->productType = (string) $product->product_type;
        $this->tags = implode(', ', $product->tags ?? []);
        $this->handle = $product->handle;
        $this->publishedAt = $product->published_at?->format('Y-m-d\TH:i');
        $this->collectionIds = $product->collections->pluck('id')->map(fn (int $id): int => $id)->all();
        $this->media = $product->media
            ->map(fn (ProductMedia $media): array => [
                'id' => $media->getKey(),
                'url' => Storage::disk('public')->url($media->storage_key),
                'exists' => Storage::disk('public')->exists($media->storage_key),
                'altText' => (string) $media->alt_text,
                'position' => $media->position,
                'status' => $media->status->value,
            ])
            ->values()
            ->all();
        $this->options = $product->options
            ->map(fn (ProductOption $option): array => [
                'name' => $option->name,
                'values' => $option->values->pluck('value')->implode(', '),
            ])
            ->all();
        $this->variants = $product->variants
            ->map(fn (ProductVariant $variant): array => [
                'id' => $variant->getKey(),
                'label' => $variant->optionValues->isEmpty()
                    ? 'Default'
                    : $variant->optionValues->map(fn (ProductOptionValue $value): string => $value->value)->implode(' / '),
                'sku' => (string) $variant->sku,
                'price' => number_format($variant->price_amount / 100, 2, '.', ''),
                'compareAtPrice' => $variant->compare_at_amount ? number_format($variant->compare_at_amount / 100, 2, '.', '') : '',
                'quantity' => $variant->inventoryItem?->quantity_on_hand ?? 0,
                'requiresShipping' => $variant->requires_shipping,
                'options' => $variant->optionValues
                    ->mapWithKeys(fn (ProductOptionValue $value): array => [$value->option?->name ?? 'Option' => $value->value])
                    ->all(),
            ])
            ->values()
            ->all();
    }

    /**
     * @return array<string, mixed>
     */
    private function productPayload(): array
    {
        return [
    …2990 chars truncated…      'sku' => $variantData['sku'],
                'price_amount' => $variantData['price_amount'],
                'compare_at_amount' => $variantData['compare_at_amount'],
                'currency' => $variantData['currency'],
                'requires_shipping' => $variantData['requires_shipping'],
                'is_default' => $position === 0,
                'position' => $position,
            ])->save();

            InventoryItem::withoutGlobalScopes()->updateOrCreate(
                ['variant_id' => $variant->getKey()],
                [
                    'store_id' => $store->getKey(),
                    'quantity_on_hand' => $variantData['quantity_on_hand'],
                    'quantity_reserved' => 0,
                    'policy' => 'deny',
                ],
            );

            $variant->optionValues()->sync($this->optionValueIdsForVariant($product, $variantData['options'] ?? []));
            $syncedVariantIds[] = $variant->getKey();
        }

        ProductVariant::withoutGlobalScopes()
            ->where('product_id', $product->getKey())
            ->when($syncedVariantIds !== [], fn ($query) => $query->whereNotIn('id', $syncedVariantIds))
            ->get()
            ->each(function (ProductVariant $variant): void {
                if ($this->variantHasOrderLines($variant)) {
                    $variant->forceFill(['status' => VariantStatus::Archived])->save();

                    return;
                }

                $variant->delete();
            });
    }

    private function syncProductOptions(Product $product): void
    {
        $optionPayload = $this->optionPayload();
        $syncedOptionIds = [];

        foreach ($optionPayload as $optionData) {
            $option = ProductOption::withoutGlobalScopes()
                ->where('product_id', $product->getKey())
                ->where('position', $optionData['position'])
                ->first()
                ?? $product->options()->create([
                    'name' => $optionData['name'],
                    'position' => $optionData['position'],
                ]);

            $option->forceFill([
                'name' => $optionData['name'],
                'position' => $optionData['position'],
            ])->save();

            $syncedOptionIds[] = $option->getKey();
            $syncedValueIds = [];

            foreach ($optionData['values'] as $valueData) {
                $value = ProductOptionValue::withoutGlobalScopes()
                    ->where('product_option_id', $option->getKey())
                    ->where('position', $valueData['position'])
                    ->first()
                    ?? $option->values()->create([
                        'value' => $valueData['value'],
                        'position' => $valueData['position'],
                    ]);

                $value->forceFill([
                    'value' => $valueData['value'],
                    'position' => $valueData['position'],
                ])->save();

                $syncedValueIds[] = $value->getKey();
            }

            ProductOptionValue::withoutGlobalScopes()
                ->where('product_option_id', $option->getKey())
                ->when($syncedValueIds !== [], fn ($query) => $query->whereNotIn('id', $syncedValueIds))
                ->delete();
        }

        ProductOption::withoutGlobalScopes()
            ->where('product_id', $product->getKey())
            ->when($syncedOptionIds !== [], fn ($query) => $query->whereNotIn('id', $syncedOptionIds))
            ->delete();
    }

    private function ensureVariantMatrixMatchesOptions(): void
    {
        $desiredKeys = collect($this->optionCombinations($this->optionPayload()))
            ->map(fn (array $options): string => $this->variantOptionKey($options))
            ->sort()
            ->values()
            ->all();
        $currentKeys = collect($this->variants)
            ->map(fn (array $variant): string => $this->variantOptionKey($variant['options'] ?? []))
            ->sort()
            ->values()
            ->all();

        if ($desiredKeys !== $currentKeys) {
            $this->generateVariants();
        }
    }

    /**
     * @param  array<int, array{name: string, position: int, values: array<int, array{value: string, position: int}>}>  $options
     * @return array<int, array<string, string>>
     */
    private function optionCombinations(array $options): array
    {
        if ($options === []) {
            return [[]];
        }

        return collect($options)
            ->reduce(function (array $combinations, array $option): array {
                $next = [];

                foreach ($combinations as $combination) {
                    foreach ($option['values'] as $value) {
                        $next[] = [
                            ...$combination,
                            $option['name'] => $value['value'],
                        ];
                    }
                }


output:
Chunk ID: dd6139
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 3248
Output:
        $index = array_search($mediaId, $ids, true);

        if ($index === false) {
            return;
        }

        $swapIndex = match ($direction) {
            'up' => $index - 1,
            'down' => $index + 1,
            default => $index,
        };

        if (! isset($ids[$swapIndex])) {
            return;
        }

        [$ids[$index], $ids[$swapIndex]] = [$ids[$swapIndex], $ids[$index]];

        $this->reorderMedia($ids);
    }

    /**
     * @param  array<int, int>  $order
     */
    public function reorderMedia(array $order): void
    {
        $product = $this->productForAction();
        $validIds = ProductMedia::withoutGlobalScopes()
            ->where('product_id', $product->getKey())
            ->whereIn('id', $order)
            ->pluck('id')
            ->all();

        $orderedIds = collect($order)
            ->map(fn (mixed $mediaId): int => (int) $mediaId)
            ->intersect($validIds)
            ->values();

        DB::transaction(function () use ($orderedIds): void {
            foreach ($orderedIds as $position => $mediaId) {
                ProductMedia::withoutGlobalScopes()
                    ->whereKey($mediaId)
                    ->update(['position' => $position]);
            }
        });

        $this->refreshProductMedia();
    }

    public function removeMedia(int $mediaId): void
    {
        $media = $this->mediaRecord($mediaId);

        $media->delete();

        $this->reorderMedia(
            collect($this->media)
                ->pluck('id')
                ->reject(fn (int $id): bool => $id === $mediaId)
                ->values()
                ->all(),
        );

        session()->flash('status', 'Media removed');
    }

    public function deleteProduct(): void
    {
        abort_unless($this->product instanceof Product, 404);

        app(ProductService::class)->transitionStatus($this->product, ProductStatus::Archived);

        session()->flash('status', 'Product saved');
        $this->redirectRoute('admin.products.index', navigate: true);
    }

    public function render(): mixed
    {
        return view('livewire.admin.products.form', [
            'availableCollections' => Collection::query()->orderBy('title')->get(),
            'isEditing' => $this->product !== null,
        ])->layout('layouts.app', [
            'title' => $this->product ? $this->product->title : __('Add product'),
        ]);
    }

    private function fillFromProduct(Product $product): void
    {
        $this->title = $product->title;
        $this->descriptionHtml = (string) $product->description_html;
        $this->status = $product->status->value;
        $this->vendor = (string) $product->vendor;
        $this->productType = (string) $product->product_type;
        $this->tags = implode(', ', $product->tags ?? []);
        $this->handle = $product->handle;
        $this->publishedAt = $product->published_at?->format('Y-m-d\TH:i');
        $this->collectionIds = $product->collections->pluck('id')->map(fn (int $id): int => $id)->all();
        $this->media = $product->media
            ->map(fn (ProductMedia $media): array => [
                'id' => $media->getKey(),
                'url' => Storage::disk('public')->url($media->storage_key),
                'exists' => Storage::disk('public')->exists($media->storage_key),
                'altText' => (string) $media->alt_text,
                'position' => $media->position,
                'status' => $media->status->value,
            ])
            ->values()
            ->all();
        $this->options = $product->options
            ->map(fn (ProductOption $option): array => [
                'name' => $option->name,
                'values' => $option->values->pluck('value')->implode(', '),
            ])
            ->all();
        $this->variants = $product->variants
            ->map(fn (ProductVariant $variant): array => [
                'id' => $variant->getKey(),
                'label' => $variant->optionValues->isEmpty()
                    ? 'Default'
                    : $variant->optionValues->map(fn (ProductOptionValue $value): string => $value->value)->implode(' / '),
                'sku' => (string) $variant->sku,
                'price' => number_format($variant->price_amount / 100, 2, '.', ''),
                'compareAtPrice' => $variant->compare_at_amount ? number_format($variant->compare_at_amount / 100, 2, '.', '') : '',
                'quantity' => $variant->inventoryItem?->quantity_on_hand ?? 0,
                'requiresShipping' => $variant->requires_shipping,
                'options' => $variant->optionValues
                    ->mapWithKeys(fn (ProductOptionValue $value): array => [$value->option?->name ?? 'Option' => $value->value])
                    ->all(),
            ])
            ->values()
            ->all();
    }

    /**
     * @return array<string, mixed>
     */
    private function productPayload(): array
    {
        return [
            'title' => $this->title,
            'description_html' => $this->descriptionHtml ?: null,
            'status' => $this->status,
            'vendor' => $this->vendor ?: null,
            'product_type' => $this->productType ?: null,
            'tags' => collect(explode(',', $this->tags))
                ->map(fn (string $tag): string => trim($tag))
                ->filter()
                ->values()
                ->all(),
            'handle' => Str::slug($this->handle),
        ];
    }

    /**
     * @return array<int, array{name: string, position: int, values: array<int, array{value: string, position: int}>}>
     */
    private function optionPayload(): array
    {
        return collect($this->options)
            ->map(function (array $option, int $position): array {
                return [
                    'name' => $option['name'],
                    'position' => $position,
                    'values' => collect(explode(',', $option['values']))
                        ->map(fn (string $value): string => trim($value))
                        ->filter()
                        ->values()
                        ->map(fn (string $value, int $valuePosition): array => [
                            'value' => $value,
                            'position' => $valuePosition,
                        ])
                        ->all(),
                ];
            })
            ->filter(fn (array $option): bool => $option['name'] !== '' && $option['values'] !== [])
            ->values()
            ->all();
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function variantPayload(Store $store): array
    {
        return collect($this->variants)
            ->map(fn (array $variant, int $position): array => [
                'sku' => $variant['sku'] ?: null,
                'price_amount' => Money::fromDecimalString($variant['price']),
                'compare_at_amount' => $variant['compareAtPrice'] === '' ? null : Money::fromDecimalString($variant['compareAtPrice']),
                'currency' => $store->default_currency,
                'quantity_on_hand' => (int) $variant['quantity'],
                'requires_shipping' => (bool) $variant['requiresShipping'],
                'is_default' => $position === 0,
                'position' => $position,
                'options' => $variant['options'] ?? [],
            ])
            ->all();
    }

    private function syncExistingVariants(Product $product, Store $store): void
    {
        $syncedVariantIds = [];

        foreach ($this->variantPayload($store) as $position => $variantData) {
            $variantId = $this->variants[$position]['id'] ?? null;
            $variant = $variantId
                ? ProductVariant::withoutGlobalScopes()->where('product_id', $product->getKey())->findOrFail($variantId)
                : $product->variants()->create(['position' => $position]);

            $variant->forceFill([
                'sku' => $variantData['sku'],
                'price_amount' => $variantData['price_amount'],
                'compare_at_amount' => $variantData['compare_at_amount'],
                'currency' => $variantData['currency'],
                'requires_shipping' => $variantData['requires_shipping'],
                'is_default' => $position === 0,
                'position' => $position,
            ])->save();

            InventoryItem::withoutGlobalScopes()->updateOrCreate(
                ['variant_id' => $variant->getKey()],
                [
                    'store_id' => $store->getKey(),
                    'quantity_on_hand' => $variantData['quantity_on_hand'],
                    'quantity_reserved' => 0,
                    'policy' => 'deny',
                ],
            );

            $variant->optionValues()->sync($this->optionValueIdsForVariant($product, $variantData['options'] ?? []));
            $syncedVariantIds[] = $variant->getKey();
        }

        ProductVariant::withoutGlobalScopes()
            ->where('product_id', $product->getKey())
            ->when($syncedVariantIds !== [], fn ($query) => $query->whereNotIn('id', $syncedVariantIds))
            ->get()
            ->each(function (ProductVariant $variant): void {
                if ($this->variantHasOrderLines($variant)) {
                    $variant->forceFill(['status' => VariantStatus::Archived])->save();

                    return;
                }

                $variant->delete();
            });
    }

    private function syncProductOptions(Product $product): void
    {
        $optionPayload = $this->optionPayload();
        $syncedOptionIds = [];

        foreach ($optionPayload as $optionData) {
            $option = ProductOption::withoutGlobalScopes()
                ->where('product_id', $product->getKey())
                ->where('position', $optionData['position'])
                ->first()
                ?? $product->options()->create([
                    'name' => $optionData['name'],
                    'position' => $optionData['position'],
                ]);

            $option->forceFill([
                'name' => $optionData['name'],
                'position' => $optionData['position'],
            ])->save();

            $syncedOptionIds[] = $option->getKey();
            $syncedValueIds = [];

            foreach ($optionData['values'] as $valueData) {
                $value = ProductOptionValue::withoutGlobalScopes()
                    ->where('product_option_id', $option->getKey())
                    ->where('position', $valueData['position'])
                    ->first()
                    ?? $option->values()->create([
                        'value' => $valueData['value'],
                        'position' => $valueData['position'],
                    ]);

                $value->forceFill([
                    'value' => $valueData['value'],
                    'position' => $valueData['position'],
                ])->save();

                $syncedValueIds[] = $value->getKey();
            }

            ProductOptionValue::withoutGlobalScopes()
                ->where('product_option_id', $option->getKey())
                ->when($syncedValueIds !== [], fn ($query) => $query->whereNotIn('id', $syncedValueIds))
                ->delete();
        }

        ProductOption::withoutGlobalScopes()
            ->where('product_id', $product->getKey())
            ->when($syncedOptionIds !== [], fn ($query) => $query->whereNotIn('id', $syncedOptionIds))
            ->delete();
    }

    private function ensureVariantMatrixMatchesOptions(): void
    {
        $desiredKeys = collect($this->optionCombinations($this->optionPayload()))
            ->map(fn (array $options): string => $this->variantOptionKey($options))
            ->sort()
            ->values()
            ->all();
        $currentKeys = collect($this->variants)
            ->map(fn (array $variant): string => $this->variantOptionKey($variant['options'] ?? []))
            ->sort()
            ->values()
            ->all();

        if ($desiredKeys !== $currentKeys) {
            $this->generateVariants();
        }
    }

    /**
     * @param  array<int, array{name: string, position: int, values: array<int, array{value: string, position: int}>}>  $options
     * @return array<int, array<string, string>>
     */
    private function optionCombinations(array $options): array
    {
        if ($options === []) {
            return [[]];
        }

        return collect($options)
            ->reduce(function (array $combinations, array $option): array {
                $next = [];

                foreach ($combinations as $combination) {
                    foreach ($option['values'] as $value) {
                        $next[] = [
                            ...$combination,
                            $option['name'] => $value['value'],
                        ];
                    }
                }


output:
Chunk ID: 123e60
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 578
Output:
                        <flux:select.option value="draft">Draft</flux:select.option>
                        <flux:select.option value="active">Active</flux:select.option>
                        <flux:select.option value="archived">Archived</flux:select.option>
                    </flux:select>

                    <flux:input wire:model="handle" label="URL handle" placeholder="short-sleeve-t-shirt" />
                    <flux:error name="handle" />

                    <flux:input wire:model="vendor" label="Vendor" placeholder="Acme Basics" />
                    <flux:input wire:model="productType" label="Product type" placeholder="T-Shirts" />
                    <flux:input wire:model="tags" label="Tags" placeholder="new, cotton, sale" />

                <flux:heading size="lg">Collections</flux:heading>

                    @forelse ($availableCollections as $collection)
                        <flux:checkbox wire:model="collectionIds" value="{{ $collection->getKey() }}" :label="$collection->title" wire:key="product-collection-{{ $collection->getKey() }}" />
                    @empty
                        <flux:text>No collections yet.</flux:text>
                    @endforelse

                <flux:button :href="route('admin.products.index')" wire:navigate variant="ghost">Discard</flux:button>
                <flux:button type="submit" variant="primary" wire:loading.attr="disabled">
                    Save
                    Saving...
                </flux:button>

    </form>


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

aggregated_output:
<?php

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

uses(RefreshDatabase::class);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

test('can navigate to analytics from sidebar', function (): void {
    adminAuthenticationLogin()


command: rg -n "assertDontSee|assertSeeIn|select\(|fill\(|clear|value\(" tests/Browser vendor/pestphp/pest-plugin-browser/src/Api/Concerns -g '*.php'

type: search

cmd: rg -n "assertDontSee|assertSeeIn|select\\(|fill\\(|clear|value\\(" tests/Browser vendor/pestphp/pest-plugin-browser/src/Api/Concerns -g '*.php'

query: assertDontSee|assertSeeIn|select\(|fill\(|clear|value\(

path: Browser

aggregated_output:
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/InteractsWithElements.php:65:        $this->guessLocator($field)->fill($value);
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/InteractsWithElements.php:85:    public function fill(string $field, string $value): self
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/InteractsWithElements.php:117:    public function select(string $field, array|string|int $option): self
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/InteractsWithElements.php:125:     * Type the given value in the given field without clearing it.
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/InteractsWithElements.php:133:        $locator->fill($currentValue.$value);
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/InteractsWithElements.php:141:    public function clear(string $field): self
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/InteractsWithElements.php:143:        $this->guessLocator($field)->clear();
tests/Browser/Admin/AuthenticationTest.php:20:        ->fill('input[type=email]', 'admin@acme.test')
tests/Browser/Admin/AuthenticationTest.php:21:        ->fill('input[type=password]', 'password')
tests/Browser/Admin/AuthenticationTest.php:36:        $page->fill('input[type=email]', $email);
tests/Browser/Admin/AuthenticationTest.php:40:        $page->fill('input[type=password]', $password);
tests/Browser/Admin/AuthenticationTest.php:52:        ->fill('input[type=email]', 'admin@acme.test')
tests/Browser/Admin/AuthenticationTest.php:53:        ->fill('input[type=password]', 'wrongpassword')
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/MakesElementAssertions.php:68:    public function assertDontSee(string|int|float $text): Webpage
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:106:    public function assertDontSeeIn(string $selector, string|int|float $text): Webpage
vendor/pestphp/pest-plugin-browser/src/Api/Concerns/MakesElementAssertions.php:254:    public function assertDontSeeLink(string $link): Webpage
tests/Browser/Storefront/TenantIsolationTest.php:80:        ->fill('input[type=email]', 'admin@acme.test')
tests/Browser/Storefront/TenantIsolationTest.php:81:        ->fill('input[type=password]', 'password')
tests/Browser/Storefront/TenantIsolationTest.php:92:        ->fill('input[type=email]', 'customer@acme.test')
tests/Browser/Storefront/TenantIsolationTest.php:93:        ->fill('input[type=password]', 'password')
tests/Browser/Storefront/TenantIsolationTest.php:106:        ->assertDontSee('Pro Laptop 15')
tests/Browser/Storefront/TenantIsolationTest.php:107:        ->assertDontSee('Wireless Headphones')
tests/Browser/Storefront/TenantIsolationTest.php:115:        ->assertDontSee('Pro Laptop 15')
tests/Browser/Storefront/TenantIsolationTest.php:116:        ->assertDontSee('Wireless Headphones')
tests/Browser/Storefront/TenantIsolationTest.php:128:        ->assertDontSee('Pro Laptop 15')
tests/Browser/Storefront/TenantIsolationTest.php:129:        ->assertDontSee('Wireless Headphones')
tests/Browser/Storefront/TenantIsolationTest.php:134:        ->assertDontSee('#2001')
tests/Browser/Storefront/TenantIsolationTest.php:141:        ->assertDontSee('Pro Laptop 15')
tests/Browser/Storefront/TenantIsolationTest.php:145:        ->assertDontSee('Pro Laptop 15')
tests/Browser/Storefront/TenantIsolationTest.php:160:        ->assertDontSee('#2001')
tests/Browser/Storefront/InventoryTest.php:65:        ->assertDontSee('Add to cart')
tests/Browser/Storefront/InventoryTest.php:86:        ->assertDontSee('Sold out')
tests/Browser/Storefront/InventoryTest.php:87:        ->assertDontSee('Available on backorder')
tests/Browser/Storefront/CheckoutTest.php:59:        ->fill('input[wire\\:model="email"]', $address['email'])
tests/Browser/Storefront/CheckoutTest.php:60:        ->fill('input[wire\\:model="shippingAddress.first_name"]', $address['first_name'])
tests/Browser/Storefront/CheckoutTest.php:61:        ->fill('input[wire\\:model="shippingAddress.last_name"]', $address['last_name'])
tests/Browser/Storefront/CheckoutTest.php:62:        ->fill('input[wire\\:model="shippingAddress.address1"]', $address['address1'])
tests/Browser/Storefront/CheckoutTest.php:63:        ->fill('input[wire\\:model="shippingAddress.city"]', $address['city'])
tests/Browser/Storefront/CheckoutTest.php:64:        ->fill('input[wire\\:model="shippingAddress.postal_code"]', $address['postal_code'])
tests/Browser/Storefront/CheckoutTest.php:65:        ->select('select[wire\\:model="shippingAddress.country"]', $address['country']);
tests/Browser/Storefront/CheckoutTest.php:96:        ->fill('input[wire\\:model="cardNumber"]', '4242 4242 4242 4242')
tests/Browser/Storefront/CheckoutTest.php:97:        ->fill('input[wire\\:model="cardName"]', 'Test Buyer')
tests/Browser/Storefront/CheckoutTest.php:98:…1036 chars truncated…model="cardCvc"]', '123')
tests/Browser/Storefront/CheckoutTest.php:257:        ->fill('input[wire\\:model="cardNumber"]', '4000 0000 0000 9995')
tests/Browser/Storefront/CheckoutTest.php:258:        ->fill('input[wire\\:model="cardName"]', 'Test Buyer')
tests/Browser/Storefront/CheckoutTest.php:259:        ->fill('input[wire\\:model="cardExpiry"]', '12/28')
tests/Browser/Storefront/CheckoutTest.php:260:        ->fill('input[wire\\:model="cardCvc"]', '123')
tests/Browser/Storefront/CheckoutTest.php:272:        ->select('select[wire\\:model\\.live="paymentMethod"]', 'paypal')
tests/Browser/Storefront/CheckoutTest.php:276:        ->select('select[wire\\:model\\.live="paymentMethod"]', 'bank_transfer')
tests/Browser/Storefront/CustomerAccountTest.php:32:        ->fill('input[type=email]', 'customer@acme.test')
tests/Browser/Storefront/CustomerAccountTest.php:33:        ->fill('input[type=password]', 'password')
tests/Browser/Storefront/CustomerAccountTest.php:98:        ->fill('input[wire\\:model="name"]', 'New Customer')
tests/Browser/Storefront/CustomerAccountTest.php:99:        ->fill('input[wire\\:model="email"]', 'new-customer-e2e@example.com')
tests/Browser/Storefront/CustomerAccountTest.php:100:        ->fill('input[wire\\:model="password"]', 'password123')
tests/Browser/Storefront/CustomerAccountTest.php:101:        ->fill('input[wire\\:model="password_confirmation"]', 'password123')
tests/Browser/Storefront/CustomerAccountTest.php:111:        ->fill('input[wire\\:model="name"]', 'Duplicate Customer')
tests/Browser/Storefront/CustomerAccountTest.php:112:        ->fill('input[wire\\:model="email"]', 'customer@acme.test')
tests/Browser/Storefront/CustomerAccountTest.php:113:        ->fill('input[wire\\:model="password"]', 'password123')
tests/Browser/Storefront/CustomerAccountTest.php:114:        ->fill('input[wire\\:model="password_confirmation"]', 'password123')
tests/Browser/Storefront/CustomerAccountTest.php:124:        ->fill('input[wire\\:model="name"]', 'Test Customer')
tests/Browser/Storefront/CustomerAccountTest.php:125:        ->fill('input[wire\\:model="email"]', 'mismatch@example.com')
tests/Browser/Storefront/CustomerAccountTest.php:126:        ->fill('input[wire\\:model="password"]', 'password123')
tests/Browser/Storefront/CustomerAccountTest.php:127:        ->fill('input[wire\\:model="password_confirmation"]', 'different456')
tests/Browser/Storefront/CustomerAccountTest.php:141:        ->fill('input[type=email]', 'customer@acme.test')
tests/Browser/Storefront/CustomerAccountTest.php:142:        ->fill('input[type=password]', 'wrongpassword')
tests/Browser/Storefront/CustomerAccountTest.php:203:        ->fill('input[wire\\:model="address.first_name"]', 'John')
tests/Browser/Storefront/CustomerAccountTest.php:204:        ->fill('input[wire\\:model="address.last_name"]', 'Doe')
tests/Browser/Storefront/CustomerAccountTest.php:205:        ->fill('input[wire\\:model="address.address1"]', 'New Street 42')
tests/Browser/Storefront/CustomerAccountTest.php:206:        ->fill('input[wire\\:model="address.city"]', 'Hamburg')
tests/Browser/Storefront/CustomerAccountTest.php:207:        ->fill('input[wire\\:model="address.postal_code"]', '20095')
tests/Browser/Storefront/CustomerAccountTest.php:208:        ->select('select[wire\\:model="address.country"]', 'DE')
tests/Browser/Storefront/CustomerAccountTest.php:223:        ->fill('input[wire\\:model="address.city"]', 'Frankfurt')
tests/Browser/Storefront/BrowsingTest.php:94:        ->assertDontSee('Unreleased Winter Jacket')
tests/Browser/Storefront/BrowsingTest.php:100:        ->assertDontSee('Unreleased Winter Jacket')
tests/Browser/Storefront/BrowsingTest.php:110:        ->assertDontSee('Add to cart')
tests/Browser/Storefront/CartTest.php:47:        ->fill('input[wire\\:model="discountCode"]', $code)
tests/Browser/Storefront/ResponsiveTest.php:66:        ->fill('input[wire\\:model="email"]', 'mobile-buyer@example.com')
tests/Browser/Storefront/ResponsiveTest.php:67:        ->fill('input[wire\\:model="shippingAddress.first_name"]', 'Mobile')
tests/Browser/Storefront/ResponsiveTest.php:68:        ->fill('input[wire\\:model="shippingAddress.last_name"]', 'Buyer')
tests/Browser/Storefront/ResponsiveTest.php:69:        ->fill('input[wire\\:model="shippingAddress.address1"]', 'Responsive Strasse 1')
tests/Browser/Storefront/ResponsiveTest.php:70:        ->fill('input[wire\\:model="shippingAddress.city"]', 'Berlin')
tests/Browser/Storefront/ResponsiveTest.php:71:        ->fill('input[wire\\:model="shippingAddress.postal_code"]', '10115')
tests/Browser/Storefront/ResponsiveTest.php:72:        ->select('select[wire\\:model="shippingAddress.country"]', 'DE');
tests/Browser/Storefront/ResponsiveTest.php:78:        ->fill('input[type=email]', 'admin@acme.test')
tests/Browser/Storefront/ResponsiveTest.php:79:        ->fill('input[type=password]', 'password')
tests/Browser/Storefront/ResponsiveTest.php:187:        ->fill('input[aria-label="Maximum price"]', '10')


command: rg -n "Unreleased|Classic Cotton|Premium Slim|status|draft|archiv" database/seeders app/Services tests -g '*.php'

type: search

cmd: rg -n 'Unreleased|Classic Cotton|Premium Slim|status|draft|archiv' database/seeders app/Services tests -g '*.php'

query: Unreleased|Classic Cotton|Premium Slim|status|draft|archiv

path: seeders

aggregated_output:
app/Services/DiscountService.php:48:            ->where('status', DiscountStatus::Active->value)
app/Services/DiscountService.php:65:        if ($discount->status === DiscountStatus::Expired || ($discount->ends_at !== null && $discount->ends_at->isPast())) {
app/Services/DiscountService.php:69:        if ($discount->status !== DiscountStatus::Active) {
tests/Browser/Storefront/TenantIsolationTest.php:105:        ->assertSee('Classic Cotton T-Shirt')
tests/Browser/Storefront/TenantIsolationTest.php:114:        ->assertSee('Classic Cotton T-Shirt')
tests/Browser/Storefront/TenantIsolationTest.php:140:        ->assertSee('Classic Cotton T-Shirt')
app/Services/VariantMatrixService.php:64:                    $variant->forceFill(['status' => VariantStatus::Archived])->save();
app/Services/VariantMatrixService.php:83:                'status' => VariantStatus::Active,
app/Services/VariantMatrixService.php:105:            'status' => VariantStatus::Active,
app/Services/VariantMatrixService.php:127:                    'status' => VariantStatus::Archived,
app/Services/VariantMatrixService.php:177:            'status' => VariantStatus::Active,
tests/Browser/SmokeTest.php:31:        ->where('status', 'active')
tests/Browser/SmokeTest.php:35:        ->where('status', 'active')
tests/Browser/SmokeTest.php:39:        ->where('status', 'published')
database/seeders/DiscountSeeder.php:32:                        'status' => $discount['status'] ?? 'active',
database/seeders/DiscountSeeder.php:40:     * @return array<int, array{code: string, value_type: string, value_amount: int, rules_json: array<string, mixed>, starts_at?: \Illuminate\Support\Carbon, ends_at?: \Illuminate\Support\Carbon, usage_limit?: int|null, usage_count?: int, status?: string}>
database/seeders/DiscountSeeder.php:50:            ['code' => 'EXPIRED20', 'value_type' => 'percent', 'value_amount' => 20, 'ends_at' => now()->subDay(), 'rules_json' => [], 'status' => 'expired'],
database/seeders/PageSeeder.php:26:                        'status' => 'published',
tests/Browser/Storefront/BrowsingTest.php:27:        ->assertSee('Classic Cotton T-Shirt')
tests/Browser/Storefront/BrowsingTest.php:35:        ->assertSee('Classic Cotton T-Shirt')
tests/Browser/Storefront/BrowsingTest.php:44:        ->assertSee('Classic Cotton T-Shirt')
tests/Browser/Storefront/BrowsingTest.php:52:        ->assertSee('Classic Cotton T-Shirt')
tests/Browser/Storefront/BrowsingTest.php:73:        ->assertSee('Premium Slim Fit Jeans')
tests/Browser/Storefront/BrowsingTest.php:82:        ->assertSee('Classic Cotton T-Shirt')
tests/Browser/Storefront/BrowsingTest.php:92:test('does not show draft products on storefront collections', function (): void {
tests/Browser/Storefront/BrowsingTest.php:94:        ->assertDontSee('Unreleased Winter Jacket')
tests/Browser/Storefront/BrowsingTest.php:98:test('does not show draft products in search results', function (): void {
tests/Browser/Storefront/BrowsingTest.php:99:    visit('/search?q=draft', storefrontBrowsingHost())
tests/Browser/Storefront/BrowsingTest.php:100:        ->assertDontSee('Unreleased Winter Jacket')
tests/Browser/Storefront/AccessibilityTest.php:108:        && placeholder.getAttribute('aria-label')?.includes('Classic Cotton T-Shirt');
tests/Feature/Webhooks/WebhookDeliveryTest.php:24:        'status' => WebhookSubscriptionStatus::Active,
tests/Feature/Webhooks/WebhookDeliveryTest.php:29:        'status' => WebhookSubscriptionStatus::Active,
tests/Feature/Webhooks/WebhookDeliveryTest.php:57:        'status' => WebhookDeliveryStatus::Pending,
tests/Feature/Webhooks/WebhookDeliveryTest.php:82:    expect($delivery->status)->toBe(WebhookDeliveryStatus::Success)
tests/Feature/Webhooks/WebhookDeliveryTest.php:95:        'status' => WebhookSubscriptionStatus::Active,
tests/Feature/Webhooks/WebhookDeliveryTest.php:100:        'status' => WebhookDeliveryStatus::Failed,
tests/Feature/Webhooks/WebhookDeliveryTest.php:106:        'status' => WebhookDeliveryStatus::Pending,
tests/Feature/Webhooks/WebhookDeliveryTest.php:114:    expect($subscription->refresh()->status)->toBe(WebhookSubscriptionStatus::Paused)
tests/Feature/Webhooks/WebhookDeliveryTest.php:115:        ->and($delivery->refresh()->status)->toBe(WebhookDeliveryStatus::Failed)
app/Services/CartService.php:31:            'status' => CartStatus::Active,
app/Services/CartService.php:43:                ->where('status', CartStatus::Active)
app/Services/CartService.php:51:                    ->where('status', CartStatus::Active)
app/Services/CartService.php:68:                ->where('status', CartStatus::Active)
app/Services/CartService.php:88:                ->where('status', CartStatus::Active)
app/Services/CartService.php:105:            ->where('status', CartStatus::Active)
app/Services/CartService.php:224:            $guestCart->forceFill(['status' => CartStatus::Abandoned])->save();
app/Services/CartService.php:252:        if ($variant->product->status !== ProductStatus::Active || $va…36186 chars truncated…ogFoundationTest.php:24:        ->and(Schema::hasColumns('collections', ['store_id', 'title', 'handle', 'type', 'status']))->toBeTrue()
tests/Feature/Storefront/CartCheckoutUiTest.php:74:        ->assertSee('Classic Cotton T-Shirt')
tests/Feature/Storefront/CartCheckoutUiTest.php:159:    expect($guestCart->refresh()->status)->toBe(CartStatus::Abandoned)
tests/Feature/Storefront/CartCheckoutUiTest.php:199:    expect($checkout->status)->toBe(CheckoutStatus::PaymentSelected)
tests/Feature/Api/StorefrontOrderApiTest.php:111:        ->assertJsonPath('data.financial_status', 'paid')
tests/Feature/Api/StorefrontOrderApiTest.php:144:    expect($checkout->status)->toBe(CheckoutStatus::ShippingSelected)
tests/Feature/Api/AdminOrderExportApiTest.php:69:        'status' => 'pending',
tests/Feature/Api/AdminOrderExportApiTest.php:70:        'financial_status' => 'pending',
tests/Feature/Api/AdminOrderExportApiTest.php:82:                'status' => 'paid',
tests/Feature/Api/AdminOrderExportApiTest.php:88:        ->assertJsonPath('status', 'completed');
tests/Feature/Api/AdminOrderExportApiTest.php:97:        ->assertJsonPath('data.status', 'completed')
tests/Feature/Api/AdminOrderExportApiTest.php:104:    expect($csv)->toContain('order_number,created_at,status')
tests/Feature/Api/AdminOrderExportApiTest.php:158:                'status' => 'lost',
tests/Feature/Api/AdminOrderExportApiTest.php:164:        ->assertJsonValidationErrors(['format', 'filters.status', 'filters.created_before']);
tests/Feature/Storefront/OrderViewsTest.php:88:        ->and($cart->refresh()->status)->toBe(CartStatus::Converted)
tests/Feature/Storefront/OrderViewsTest.php:122:        ->and($checkout->status)->toBe(CheckoutStatus::ShippingSelected)
tests/Feature/Storefront/OrderViewsTest.php:173:        'title_snapshot' => 'Classic Cotton T-Shirt',
tests/Feature/Storefront/OrderViewsTest.php:183:        ->assertSee('Classic Cotton T-Shirt')
tests/Feature/Storefront/ThemeDataTest.php:27:    expect(Schema::hasColumns('themes', ['store_id', 'name', 'version', 'status', 'published_at']))->toBeTrue()
tests/Feature/Storefront/ThemeDataTest.php:30:        ->and(Schema::hasColumns('pages', ['store_id', 'title', 'handle', 'body_html', 'status', 'published_at']))->toBeTrue()
tests/Feature/Storefront/ThemeDataTest.php:121:        'handle' => 'private-draft',
tests/Feature/Storefront/ThemeDataTest.php:123:        'status' => PageStatus::Draft,
tests/Feature/Storefront/ThemeDataTest.php:134:        ->get('/pages/private-draft')
tests/Feature/Catalog/MediaProcessingTest.php:33:    expect($media->status)->toBe(MediaStatus::Ready)
tests/Feature/Catalog/MediaProcessingTest.php:68:    expect($media->refresh()->status)->toBe(MediaStatus::Failed);
tests/Feature/Catalog/CatalogUiTest.php:27:        ->assertSee('Classic Cotton T-Shirt')
tests/Feature/Catalog/CatalogUiTest.php:34:        ->assertSee('Classic Cotton T-Shirt')
tests/Feature/Catalog/CatalogUiTest.php:35:        ->assertDontSee('Unreleased Winter Jacket');
tests/Feature/Catalog/CatalogUiTest.php:40:        ->assertSee('Classic Cotton T-Shirt')
tests/Feature/Catalog/CatalogUiTest.php:46:        ->get('/search?q=draft')
tests/Feature/Catalog/CatalogUiTest.php:48:        ->assertDontSee('Unreleased Winter Jacket');
tests/Feature/Catalog/CatalogUiTest.php:79:test('admin product form creates edits archives and index filters products', function () {
tests/Feature/Catalog/CatalogUiTest.php:116:    $draftProduct = app(ProductService::class)->create($store, [
tests/Feature/Catalog/CatalogUiTest.php:118:        'handle' => 'draft-product-with-price-later',
tests/Feature/Catalog/CatalogUiTest.php:119:        'status' => 'draft',
tests/Feature/Catalog/CatalogUiTest.php:131:        ->test(ProductForm::class, ['product' => $draftProduct])
tests/Feature/Catalog/CatalogUiTest.php:133:        ->set('status', 'active')
tests/Feature/Catalog/CatalogUiTest.php:137:    expect($draftProduct->refresh()->status->value)->toBe('active')
tests/Feature/Catalog/CatalogUiTest.php:138:        ->and($draftProduct->variants()->first()?->price_amount)->toBe(1750);
tests/Feature/Catalog/CatalogUiTest.php:142:        ->set('status', 'archived')
tests/Feature/Catalog/CatalogUiTest.php:146:    expect($product->refresh()->status->value)->toBe('archived');
tests/Feature/Catalog/CatalogUiTest.php:150:        ->set('statusFilter', 'draft')
tests/Feature/Catalog/CatalogUiTest.php:151:        ->assertSee('Unreleased Winter Jacket')
tests/Feature/Catalog/CatalogUiTest.php:152:        ->assertDontSee('Classic Cotton T-Shirt')
tests/Feature/Catalog/CatalogUiTest.php:153:        ->set('statusFilter', 'active')
tests/Feature/Catalog/CatalogUiTest.php:155:        ->assertSee('Classic Cotton T-Shirt');
tests/Feature/Api/AdminCollectionApiTest.php:59:            'status' => 'active',
tests/Feature/Api/AdminCollectionApiTest.php:79:            'status' => 'draft',
tests/Feature/Api/AdminCollectionApiTest.php:85:        ->assertJsonPath('data.status', 'draft')


output:
Chunk ID: f40c72
Wall time: 0.2312 seconds
Process exited with code 0
Original token count: 21
Output:

   ERROR  Your application doesn't have any routes matching the given criteria.