ls tests/Browser/Admin && sed -n '1,220p' tests/Browser/Admin/AuthenticationTest.php && sed -n '1,240p' tests/Browser/Admin/OrderManagementTest.php

OrderManagementTest.php

AuthenticationTest.php
<?php

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

uses(RefreshDatabase::class);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

test('can navigate to themes from sidebar', function (): void {
    adminAuthenticationLogin()
        ->click('a[href$="/admin/themes"]')
        ->wait(1)
        ->assertPathIs('/admin/themes')
        ->assertSee('Themes')
        ->assertNoJavaScriptErrors();
});
sed: tests/Browser/Admin/OrderManagementTest.php: No such file or directory


rg -n "admin/products|admin\.products|ProductManagement|Products" routes app resources tests/Browser tests/Feature specs/08-PLAYWRIGHT-E2E-PLAN.md

rg -n "admin/products|admin\\.products|ProductManagement|Products" routes app resources tests/Browser tests/Feature specs/08-PLAYWRIGHT-E2E-PLAN.md

Question: admin/products|admin\.products|ProductManagement|Products

tests/Browser/Admin/AuthenticationTest.php:84:    visit('/admin/products', adminAuthenticationHost())
tests/Browser/Admin/AuthenticationTest.php:105:        '/admin/products' => 'Products',
specs/08-PLAYWRIGHT-E2E-PLAN.md:11:| 1 - Smoke Tests | `tests/Browser/SmokeTest.php` | 10 | Hit every major page, assert HTTP 200 and no JS errors | Store 1, Products, Collections, Pages |
specs/08-PLAYWRIGHT-E2E-PLAN.md:13:| 3 - Admin Product Management | `tests/Browser/Admin/ProductManagementTest.php` | 7 | Product CRUD: listing, creating, editing, archiving, filtering | Admin user, Products (incl. #15 draft) |
specs/08-PLAYWRIGHT-E2E-PLAN.md:17:| 7 - Storefront Browsing | `tests/Browser/Storefront/BrowsingTest.php` | 15 | Home page, collections, product detail, variants, search, pages | Products, Collections, Pages, Products #15/#17/#18 |
specs/08-PLAYWRIGHT-E2E-PLAN.md:18:| 8 - Cart Flow | `tests/Browser/Storefront/CartTest.php` | 12 | Cart lifecycle: add, update qty, remove, discounts, totals | Products, Discounts (all codes) |
specs/08-PLAYWRIGHT-E2E-PLAN.md:19:| 9 - Checkout Flow | `tests/Browser/Storefront/CheckoutTest.php` | 13 | Full checkout: contact, address, shipping, payment method selection, payment, confirmation, magic card decline, bank transfer flow | Products, Shipping zones, Discounts |
specs/08-PLAYWRIGHT-E2E-PLAN.md:21:| 11 - Inventory Enforcement | `tests/Browser/Storefront/InventoryTest.php` | 4 | Inventory policy enforcement: deny vs. continue | Products #17 (deny), #18 (continue), #1 |
specs/08-PLAYWRIGHT-E2E-PLAN.md:23:| 13 - Responsive / Mobile | `tests/Browser/Storefront/ResponsiveTest.php` | 8 | Mobile and tablet viewport rendering and interaction | Products, Admin user |
specs/08-PLAYWRIGHT-E2E-PLAN.md:24:| 14 - Accessibility | `tests/Browser/Storefront/AccessibilityTest.php` | 11 | Heading hierarchy, ARIA labels, form labels, keyboard nav | Products, Store 1 |
specs/08-PLAYWRIGHT-E2E-PLAN.md:97:| **Products (20 total)** | Seeded across collections, with variants, options, images, inventory | 1, 3, 7, 8, 9, 11, 13 |
specs/08-PLAYWRIGHT-E2E-PLAN.md:116:    S2 --> S3[Suite 3: Admin Products]
specs/08-PLAYWRIGHT-E2E-PLAN.md:151:        ProductManagementTest.php
specs/08-PLAYWRIGHT-E2E-PLAN.md:326:    Dashboard --> Products: click "Products"
specs/08-PLAYWRIGHT-E2E-PLAN.md:418:1. Navigate to `/admin/products` (without logging in)
specs/08-PLAYWRIGHT-E2E-PLAN.md:453:5. Click "Products" in the sidebar
specs/08-PLAYWRIGHT-E2E-PLAN.md:454:6. Verify "Products" heading is visible
specs/08-PLAYWRIGHT-E2E-PLAN.md:510:**File:** `tests/Browser/Admin/ProductManagementTest.php`
specs/08-PLAYWRIGHT-E2E-PLAN.md:537:5. Click "Products" in the sidebar
specs/08-PLAYWRIGHT-E2E-PLAN.md:553:3. Click "Products" in the sidebar
specs/08-PLAYWRIGHT-E2E-PLAN.md:563:13. Click "Products" to return to the product list
specs/08-PLAYWRIGHT-E2E-PLAN.md:578:3. Click "Products" in the sidebar
specs/08-PLAYWRIGHT-E2E-PLAN.md:583:8. Click "Products" to return to the product list
specs/08-PLAYWRIGHT-E2E-PLAN.md:598:3. Click "Products" in the sidebar
specs/08-PLAYWRIGHT-E2E-PLAN.md:606:11. Click "Products" to return to the product list
specs/08-PLAYWRIGHT-E2E-PLAN.md:610:15. Click "Products" to return to the product list
specs/08-PLAYWRIGHT-E2E-PLAN.md:626:3. Click "Products" in the sidebar
specs/08-PLAYWRIGHT-E2E-PLAN.md:647:3. Click "Products" in the sidebar
specs/08-PLAYWRIGHT-E2E-PLAN.md:663:3. Click "Products" in the sidebar
specs/08-PLAYWRIGHT-E2E-PLAN.md:1534:**Preconditions:** Products #1 and #2 exist
specs/08-PLAYWRIGHT-E2E-PLAN.md:2414:        S1Products[Fashion Products]
specs/08-PLAYWRIGHT-E2E-PLAN.md:2419:        S2Products[Other Products]
specs/08-PLAYWRIGHT-E2E-PLAN.md:2423:    S1Products -.-x S2Products
specs/08-PLAYWRIGHT-E2E-PLAN.md:2442:- Products from other stores (e.g., electronics from Store 2) are absent
specs/08-PLAYWRIGHT-E2E-PLAN.md:2468:3. Click "Products" in the sidebar
specs/08-PLAYWRIGHT-E2E-PLAN.md:2651:4. Click "Products" in the sidebar
specs/08-PLAYWRIGHT-E2E-PLAN.md:2652:5. Verify "Products" heading is visible
specs/08-PLAYWRIGHT-E2E-PLAN.md:2672:- Products are visible
specs/08-PLAYWRIGHT-E2E-PLAN.md:3124:| 3 - Admin Product Management | `tests/Browser/Admin/ProductManagementTest.php` | 7 |
routes/web.php:24:use App\Livewire\Admin\Products\Form as AdminProductForm;
routes/web.php:25:use App\Livewire\Admin\Products\Index as AdminProductsIndex;
routes/web.php:48:use App\Livewire\Storefront\Products\Show as StorefrontProductShow;
routes/web.php:125:    Route::livewire('products', AdminProductsIndex::class)->name('products.index');
tests/Browser/SmokeTest.php:77:        '/admin/products' => 'Products',
tests/Browser/Storefront/TenantIsolationTest.php:124:        ->click('a[href$="/admin/products"]')
tests/Browser/Storefront/TenantIsolationTest.php:126:        ->assertPathIs('/admin/products')
tests/Browser/Storefront/TenantIsolationTest.php:127:        ->assertSee('Products')
resources/views/layouts/a…3704 chars truncated…oduct->collections->first()?->title ?? 'Products', 'url' => $product->collections->first() ? route('collections.show', $product->collections->first()->handle) : route('collections.index')],
app/Services/ProductService.php:128:            throw InvalidProductTransitionException::because('Products with order history cannot be reverted to draft.');
resources/views/livewire/admin/dashboard.blade.php:83:                @forelse ($topProducts as $product)
app/Http/Controllers/Api/Admin/V1/AnalyticsSummaryController.php:59:                'top_products' => $this->topProducts($store, $from, $to),
app/Http/Controllers/Api/Admin/V1/AnalyticsSummaryController.php:130:    private function topProducts(Store $store, CarbonInterface $from, CarbonInterface $to): array
resources/views/livewire/admin/inventory/index.blade.php:34:                                <a href="{{ route('admin.products.edit', $item->variant->product) }}" class="font-medium text-zinc-950 hover:underline dark:text-white" wire:navigate>
resources/views/livewire/admin/discounts/form.blade.php:84:                            @foreach ($selectedProducts as $product)
app/Http/Controllers/Api/Admin/V1/CollectionController.php:57:                $this->replaceProducts($collection, $validated['product_ids']);
app/Http/Controllers/Api/Admin/V1/CollectionController.php:83:                $this->replaceProducts($collection, $validated['product_ids']);
app/Http/Controllers/Api/Admin/V1/CollectionController.php:85:                $this->addProducts($collection, $validated['add_product_ids'] ?? []);
app/Http/Controllers/Api/Admin/V1/CollectionController.php:86:                $this->removeProducts($collection, $validated['remove_product_ids'] ?? []);
app/Http/Controllers/Api/Admin/V1/CollectionController.php:183:    private function replaceProducts(Collection $collection, array $productIds): void
app/Http/Controllers/Api/Admin/V1/CollectionController.php:191:    private function addProducts(Collection $collection, array $productIds): void
app/Http/Controllers/Api/Admin/V1/CollectionController.php:205:    private function removeProducts(Collection $collection, array $productIds): void
app/Http/Controllers/Api/Storefront/V1/SearchController.php:6:use App\Http\Requests\Api\Storefront\V1\SearchProductsRequest;
app/Http/Controllers/Api/Storefront/V1/SearchController.php:7:use App\Http\Requests\Api\Storefront\V1\SuggestProductsRequest;
app/Http/Controllers/Api/Storefront/V1/SearchController.php:15:    public function index(SearchProductsRequest $request, SearchService $search): JsonResponse
app/Http/Controllers/Api/Storefront/V1/SearchController.php:43:    public function suggest(SuggestProductsRequest $request, SearchService $search): JsonResponse
app/Livewire/Admin/Analytics/Index.php:60:    public array $topProducts = [];
app/Livewire/Admin/Analytics/Index.php:144:        $this->loadTopProducts($start, $end);
app/Livewire/Admin/Analytics/Index.php:182:    private function loadTopProducts(CarbonInterface $start, CarbonInterface $end): void
app/Livewire/Admin/Analytics/Index.php:200:        $this->topProducts = $rows
app/Livewire/Admin/Discounts/Form.php:220:    public function selectedProducts(): SupportCollection
app/Livewire/Admin/Discounts/Form.php:244:            'selectedProducts' => $this->selectedProducts(),
app/Livewire/Admin/Dashboard.php:60:    public array $topProducts = [];
app/Livewire/Admin/Dashboard.php:127:        $this->loadTopProducts();
app/Livewire/Admin/Dashboard.php:152:    public function loadTopProducts(): void
app/Livewire/Admin/Dashboard.php:156:        $this->topProducts = OrderLine::query()
app/Http/Requests/Api/Storefront/V1/SearchProductsRequest.php:8:class SearchProductsRequest extends FormRequest
app/Http/Requests/Api/Storefront/V1/SuggestProductsRequest.php:7:class SuggestProductsRequest extends FormRequest
app/Livewire/Admin/Products/Form.php:3:namespace App\Livewire\Admin\Products;
app/Livewire/Admin/Products/Form.php:353:        $this->redirectRoute('admin.products.index', navigate: true);
app/Livewire/Admin/Products/Form.php:358:        return view('livewire.admin.products.form', [
app/Livewire/Admin/Products/Index.php:3:namespace App\Livewire\Admin\Products;
app/Livewire/Admin/Products/Index.php:123:        return view('livewire.admin.products.index', [
app/Livewire/Admin/Products/Index.php:127:            'title' => __('Products'),
app/Livewire/Admin/Products/Index.php:143:        $this->dispatch('toast', type: 'success', message: __('Products updated'));
app/Livewire/Admin/Collections/Form.php:130:    public function assignedProducts(): SupportCollection
app/Livewire/Admin/Collections/Form.php:147:            'assignedProducts' => $this->assignedProducts(),
app/Livewire/Storefront/Home.php:23:    public function featuredProducts(): SupportCollection
app/Livewire/Storefront/Home.php:57:            'featuredProducts' => $this->featuredProducts(),
app/Livewire/Storefront/Products/Show.php:3:namespace App\Livewire\Storefront\Products;


Output:
Chunk ID: e17416
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 3426
Output:
tests/Browser/Admin/AuthenticationTest.php:84:    visit('/admin/products', adminAuthenticationHost())
tests/Browser/Admin/AuthenticationTest.php:105:        '/admin/products' => 'Products',
specs/08-PLAYWRIGHT-E2E-PLAN.md:11:| 1 - Smoke Tests | `tests/Browser/SmokeTest.php` | 10 | Hit every major page, assert HTTP 200 and no JS errors | Store 1, Products, Collections, Pages |
specs/08-PLAYWRIGHT-E2E-PLAN.md:13:| 3 - Admin Product Management | `tests/Browser/Admin/ProductManagementTest.php` | 7 | Product CRUD: listing, creating, editing, archiving, filtering | Admin user, Products (incl. #15 draft) |
specs/08-PLAYWRIGHT-E2E-PLAN.md:17:| 7 - Storefront Browsing | `tests/Browser/Storefront/BrowsingTest.php` | 15 | Home page, collections, product detail, variants, search, pages | Products, Collections, Pages, Products #15/#17/#18 |
specs/08-PLAYWRIGHT-E2E-PLAN.md:18:| 8 - Cart Flow | `tests/Browser/Storefront/CartTest.php` | 12 | Cart lifecycle: add, update qty, remove, discounts, totals | Products, Discounts (all codes) |
specs/08-PLAYWRIGHT-E2E-PLAN.md:19:| 9 - Checkout Flow | `tests/Browser/Storefront/CheckoutTest.php` | 13 | Full checkout: contact, address, shipping, payment method selection, payment, confirmation, magic card decline, bank transfer flow | Products, Shipping zones, Discounts |
specs/08-PLAYWRIGHT-E2E-PLAN.md:21:| 11 - Inventory Enforcement | `tests/Browser/Storefront/InventoryTest.php` | 4 | Inventory policy enforcement: deny vs. continue | Products #17 (deny), #18 (continue), #1 |
specs/08-PLAYWRIGHT-E2E-PLAN.md:23:| 13 - Responsive / Mobile | `tests/Browser/Storefront/ResponsiveTest.php` | 8 | Mobile and tablet viewport rendering and interaction | Products, Admin user |
specs/08-PLAYWRIGHT-E2E-PLAN.md:24:| 14 - Accessibility | `tests/Browser/Storefront/AccessibilityTest.php` | 11 | Heading hierarchy, ARIA labels, form labels, keyboard nav | Products, Store 1 |
specs/08-PLAYWRIGHT-E2E-PLAN.md:97:| **Products (20 total)** | Seeded across collections, with variants, options, images, inventory | 1, 3, 7, 8, 9, 11, 13 |
specs/08-PLAYWRIGHT-E2E-PLAN.md:116:    S2 --> S3[Suite 3: Admin Products]
specs/08-PLAYWRIGHT-E2E-PLAN.md:151:        ProductManagementTest.php
specs/08-PLAYWRIGHT-E2E-PLAN.md:326:    Dashboard --> Products: click "Products"
specs/08-PLAYWRIGHT-E2E-PLAN.md:418:1. Navigate to `/admin/products` (without logging in)
specs/08-PLAYWRIGHT-E2E-PLAN.md:453:5. Click "Products" in the sidebar
specs/08-PLAYWRIGHT-E2E-PLAN.md:454:6. Verify "Products" heading is visible
specs/08-PLAYWRIGHT-E2E-PLAN.md:510:**File:** `tests/Browser/Admin/ProductManagementTest.php`
specs/08-PLAYWRIGHT-E2E-PLAN.md:537:5. Click "Products" in the sidebar
specs/08-PLAYWRIGHT-E2E-PLAN.md:553:3. Click "Products" in the sidebar
specs/08-PLAYWRIGHT-E2E-PLAN.md:563:13. Click "Products" to return to the product list
specs/08-PLAYWRIGHT-E2E-PLAN.md:578:3. Click "Products" in the sidebar
specs/08-PLAYWRIGHT-E2E-PLAN.md:583:8. Click "Products" to return to the product list
specs/08-PLAYWRIGHT-E2E-PLAN.md:598:3. Click "Products" in the sidebar
specs/08-PLAYWRIGHT-E2E-PLAN.md:606:11. Click "Products" to return to the product list
specs/08-PLAYWRIGHT-E2E-PLAN.md:610:15. Click "Products" to return to the product list
specs/08-PLAYWRIGHT-E2E-PLAN.md:626:3. Click "Products" in the sidebar
specs/08-PLAYWRIGHT-E2E-PLAN.md:647:3. Click "Products" in the sidebar
specs/08-PLAYWRIGHT-E2E-PLAN.md:663:3. Click "Products" in the sidebar
specs/08-PLAYWRIGHT-E2E-PLAN.md:1534:**Preconditions:** Products #1 and #2 exist
specs/08-PLAYWRIGHT-E2E-PLAN.md:2414:        S1Products[Fashion Products]
specs/08-PLAYWRIGHT-E2E-PLAN.md:2419:        S2Products[Other Products]
specs/08-PLAYWRIGHT-E2E-PLAN.md:2423:    S1Products -.-x S2Products
specs/08-PLAYWRIGHT-E2E-PLAN.md:2442:- Products from other stores (e.g., electronics from Store 2) are absent
specs/08-PLAYWRIGHT-E2E-PLAN.md:2468:3. Click "Products" in the sidebar
specs/08-PLAYWRIGHT-E2E-PLAN.md:2651:4. Click "Products" in the sidebar
specs/08-PLAYWRIGHT-E2E-PLAN.md:2652:5. Verify "Products" heading is visible
specs/08-PLAYWRIGHT-E2E-PLAN.md:2672:- Products are visible
specs/08-PLAYWRIGHT-E2E-PLAN.md:3124:| 3 - Admin Product Management | `tests/Browser/Admin/ProductManagementTest.php` | 7 |
routes/web.php:24:use App\Livewire\Admin\Products\Form as AdminProductForm;
routes/web.php:25:use App\Livewire\Admin\Products\Index as AdminProductsIndex;
routes/web.php:48:use App\Livewire\Storefront\Products\Show as StorefrontProductShow;
routes/web.php:125:    Route::livewire('products', AdminProductsIndex::class)->name('products.index');
tests/Browser/SmokeTest.php:77:        '/admin/products' => 'Products',
tests/Browser/Storefront/TenantIsolationTest.php:124:        ->click('a[href$="/admin/products"]')
tests/Browser/Storefront/TenantIsolationTest.php:126:        ->assertPathIs('/admin/products')
tests/Browser/Storefront/TenantIsolationTest.php:127:        ->assertSee('Products')
resources/views/layouts/app/sidebar.blade.php:34:                <flux:sidebar.group :heading="__('Products')" class="grid">
resources/views/layouts/app/sidebar.blade.php:35:                    <flux:sidebar.item icon="cube" :href="route('admin.products.index')" :current="request()->routeIs('admin.products.*')" wire:navigate>
resources/views/layouts/app/sidebar.blade.php:36:                        {{ __('Products') }}
tests/Feature/Storefront/CartCheckoutUiTest.php:9:use App\Livewire\Storefront\Products\Show as ProductShow;
tests/Browser/Storefront/ResponsiveTest.php:168:        ->click('a[href$="/admin/products"]')
tests/Browser/Storefront/ResponsiveTest.php:170:        ->assertPathIs('/admin/products')
tests/Browser/Storefront/ResponsiveTest.php:171:        ->assertSee('Products');
tests/Feature/Api/AdminThemeApiTest.php:65:        'sections/featured-products.blade.php' => '
API Products
',
resources/views/livewire/admin/analytics/index.blade.php:109:                        @forelse ($topProducts as $product)
tests/Feature/Admin/ProductMediaManagementTest.php:6:use App\Livewire\Admin\Products\Form as ProductForm;
resources/views/livewire/admin/products/index.blade.php:4:            <flux:heading size="xl">Products</flux:heading>
resources/views/livewire/admin/products/index.blade.php:8:        <flux:button :href="route('admin.products.create')" wire:navigate variant="primary" icon="plus">
resources/views/livewire/admin/products/index.blade.php:83:
resources/views/livewire/admin/products/index.blade.php:107:                                    <flux:button :href="route('admin.products.create')" wire:navigate variant="primary">Add product</flux:button>
resources/views/livewire/storefront/home.blade.php:22:                @foreach ($featuredProducts->take(4) as $product)
resources/views/livewire/storefront/home.blade.php:62:            @foreach ($featuredProducts as $product)
app/Services/SearchService.php:38:        $paginator->setCollection($this->hydrateProducts(
app/Services/SearchService.php:63:        return $this->hydrateProducts($ids);
app/Services/SearchService.php:221:        $paginator->setCollection($this->hydrateProducts(
app/Services/SearchService.php:361:    private function hydrateProducts(array $ids): Collection
resources/views/livewire/admin/products/form.blade.php:6:                <flux:breadcrumbs.item :href="route('admin.products.index')" wire:navigate>Products</flux:breadcrumbs.item>
resources/views/livewire/admin/products/form.blade.php:213:                <flux:button :href="route('admin.products.index')" wire:navigate variant="ghost">Discard</flux:button>
resources/views/livewire/admin/collections/index.blade.php:30:                        Products
tests/Feature/Catalog/CatalogUiTest.php:4:use App\Livewire\Admin\Products\Form as ProductForm;
tests/Feature/Catalog/CatalogUiTest.php:5:use App\Livewire\Admin\Products\Index as ProductIndex;
tests/Feature/Catalog/CatalogUiTest.php:52:    $this->get('/admin/products')->assertRedirect('/admin/login');
tests/Feature/Catalog/CatalogUiTest.php:59:        ->get('/admin/products')
tests/Feature/Catalog/CatalogUiTest.php:61:        ->assertSee('Products')
resources/views/livewire/admin/collections/form.blade.php:31:                <flux:heading size="lg">Products</flux:heading>
resources/views/livewire/admin/collections/form.blade.php:53:                        @forelse ($assignedProducts as $product)
resources/views/livewire/storefront/products/show.blade.php:3:        ['label' => $product->collections->first()?->title ?? 'Products', 'url' => $product->collections->first() ? route('collections.show', $product->collections->first()->handle) : route('collections.index')],
app/Services/ProductService.php:128:            throw InvalidProductTransitionException::because('Products with order history cannot be reverted to draft.');
resources/views/livewire/admin/dashboard.blade.php:83:                @forelse ($topProducts as $product)
app/Http/Controllers/Api/Admin/V1/AnalyticsSummaryController.php:59:                'top_products' => $this->topProducts($store, $from, $to),
app/Http/Controllers/Api/Admin/V1/AnalyticsSummaryController.php:130:    private function topProducts(Store $store, CarbonInterface $from, CarbonInterface $to): array
resources/views/livewire/admin/inventory/index.blade.php:34:                                variant->product) }}" class="font-medium text-zinc-950 hover:underline dark:text-white" wire:navigate>
resources/views/livewire/admin/discounts/form.blade.php:84:                            @foreach ($selectedProducts as $product)
app/Http/Controllers/Api/Admin/V1/CollectionController.php:57:                $this->replaceProducts($collection, $validated['product_ids']);
app/Http/Controllers/Api/Admin/V1/CollectionController.php:83:                $this->replaceProducts($collection, $validated['product_ids']);
app/Http/Controllers/Api/Admin/V1/CollectionController.php:85:                $this->addProducts($collection, $validated['add_product_ids'] ?? []);
app/Http/Controllers/Api/Admin/V1/CollectionController.php:86:                $this->removeProducts($collection, $validated['remove_product_ids'] ?? []);
app/Http/Controllers/Api/Admin/V1/CollectionController.php:183:    private function replaceProducts(Collection $collection, array $productIds): void
app/Http/Controllers/Api/Admin/V1/CollectionController.php:191:    private function addProducts(Collection $collection, array $productIds): void
app/Http/Controllers/Api/Admin/V1/CollectionController.php:205:    private function removeProducts(Collection $collection, array $productIds): void
app/Http/Controllers/Api/Storefront/V1/SearchController.php:6:use App\Http\Requests\Api\Storefront\V1\SearchProductsRequest;
app/Http/Controllers/Api/Storefront/V1/SearchController.php:7:use App\Http\Requests\Api\Storefront\V1\SuggestProductsRequest;
app/Http/Controllers/Api/Storefront/V1/SearchController.php:15:    public function index(SearchProductsRequest $request, SearchService $search): JsonResponse
app/Http/Controllers/Api/Storefront/V1/SearchController.php:43:    public function suggest(SuggestProductsRequest $request, SearchService $search): JsonResponse
app/Livewire/Admin/Analytics/Index.php:60:    public array $topProducts = [];
app/Livewire/Admin/Analytics/Index.php:144:        $this->loadTopProducts($start, $end);
app/Livewire/Admin/Analytics/Index.php:182:    private function loadTopProducts(CarbonInterface $start, CarbonInterface $end): void
app/Livewire/Admin/Analytics/Index.php:200:        $this->topProducts = $rows
app/Livewire/Admin/Discounts/Form.php:220:    public function selectedProducts(): SupportCollection
app/Livewire/Admin/Discounts/Form.php:244:            'selectedProducts' => $this->selectedProducts(),
app/Livewire/Admin/Dashboard.php:60:    public array $topProducts = [];
app/Livewire/Admin/Dashboard.php:127:        $this->loadTopProducts();
app/Livewire/Admin/Dashboard.php:152:    public function loadTopProducts(): void
app/Livewire/Admin/Dashboard.php:156:        $this->topProducts = OrderLine::query()
app/Http/Requests/Api/Storefront/V1/SearchProductsRequest.php:8:class SearchProductsRequest extends FormRequest
app/Http/Requests/Api/Storefront/V1/SuggestProductsRequest.php:7:class SuggestProductsRequest extends FormRequest
app/Livewire/Admin/Products/Form.php:3:namespace App\Livewire\Admin\Products;
app/Livewire/Admin/Products/Form.php:353:        $this->redirectRoute('admin.products.index', navigate: true);
app/Livewire/Admin/Products/Form.php:358:        return view('livewire.admin.products.form', [
app/Livewire/Admin/Products/Index.php:3:namespace App\Livewire\Admin\Products;
app/Livewire/Admin/Products/Index.php:123:        return view('livewire.admin.products.index', [
app/Livewire/Admin/Products/Index.php:127:            'title' => __('Products'),
app/Livewire/Admin/Products/Index.php:143:        $this->dispatch('toast', type: 'success', message: __('Products updated'));
app/Livewire/Admin/Collections/Form.php:130:    public function assignedProducts(): SupportCollection
app/Livewire/Admin/Collections/Form.php:147:            'assignedProducts' => $this->assignedProducts(),
app/Livewire/Storefront/Home.php:23:    public function featuredProducts(): SupportCollection
app/Livewire/Storefront/Home.php:57:            'featuredProducts' => $this->featuredProducts(),
app/Livewire/Storefront/Products/Show.php:3:namespace App\Livewire\Storefront\Products;


Output:
Chunk ID: 0e4b76
Wall time: 0.2292 seconds
Process exited with code 1
Original token count: 0
Output:


Output:
Chunk ID: a663eb
Wall time: 0.0000 seconds
Process exited with code 1
Original token count: 1060
Output:
AuthenticationTest.php
<?php

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

uses(RefreshDatabase::class);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

test('can navigate to themes from sidebar', function (): void {
    adminAuthenticationLogin()
        ->click('a[href$="/admin/themes"]')
        ->wait(1)
        ->assertPathIs('/admin/themes')
        ->assertSee('Themes')
        ->assertNoJavaScriptErrors();
});
sed: tests/Browser/Admin/OrderManagementTest.php: No such file or directory


php artisan route:list --except-vendor | rg "admin/products|admin\.products"


I’m using the `pest-testing` skill for the browser suite work, with the existing Boost doc search from this slice as the version-specific reference point. I’ll also refresh the app metadata through Boost before writing the test file so the local package context stays explicit.


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


sed -n '112,145p' routes/web.php && sed -n '510,675p' specs/08-PLAYWRIGHT-E2E-PLAN.md


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

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


sed -n '112,145p' routes/web.php && sed -n '510,675p' specs/08-PLAYWRIGHT-E2E-PLAN.md

Route::get('oauth/authorize', [OAuthController::class, 'authorize'])
    ->middleware('auth')
    ->name('oauth.authorize');

Route::post('oauth/token', [OAuthController::class, 'token'])
    ->name('oauth.token');

Route::middleware(['auth', EnsureUserEmailIsVerified::class, 'admin'])->prefix('admin')->name('admin.')->group(function (): void {
    Route::livewire('/', AdminDashboard::class)->name('dashboard');
    Route::livewire('analytics', AdminAnalyticsIndex::class)->name('analytics.index');
    Route::livewire('apps', AdminAppsIndex::class)->name('apps.index');
    Route::livewire('apps/{installation}', AdminAppShow::class)->name('apps.show');
    Route::livewire('developers', AdminDevelopersIndex::class)->name('developers.index');
    Route::livewire('products', AdminProductsIndex::class)->name('products.index');
    Route::livewire('products/create', AdminProductForm::class)->name('products.create');
    Route::livewire('products/{product}/edit', AdminProductForm::class)->name('products.edit');
    Route::livewire('inventory', AdminInventoryIndex::class)->name('inventory.index');
    Route::livewire('orders', AdminOrdersIndex::class)->name('orders.index');
    Route::livewire('orders/{order}', AdminOrderShow::class)->name('orders.show');
    Route::livewire('customers', AdminCustomersIndex::class)->name('customers.index');
    Route::livewire('customers/{customer}', AdminCustomerShow::class)->name('customers.show');
    Route::livewire('discounts', AdminDiscountsIndex::class)->name('discounts.index');
    Route::livewire('discounts/create', AdminDiscountForm::class)->name('discounts.create');
    Route::livewire('discounts/{discount}/edit', AdminDiscountForm::class)->name('discounts.edit');
    Route::livewire('pages', AdminPagesIndex::class)->name('pages.index');
    Route::livewire('pages/create', AdminPageForm::class)->name('pages.create');
    Route::livewire('pages/{page}/edit', AdminPageForm::class)->name('pages.edit');
    Route::livewire('navigation', AdminNavigationIndex::class)->name('navigation.index');
    Route::livewire('themes', AdminThemesIndex::class)->name('themes.index');
    Route::livewire('themes/{theme}/editor', AdminThemeEditor::class)->name('themes.editor');
    Route::livewire('settings', AdminSettingsIndex::class)->name('settings.index');
    Route::livewire('settings/shipping', AdminSettingsShipping::class)->name('settings.shipping');
    Route::livewire('settings/taxes', AdminSettingsTaxes::class)->name('settings.taxes');
    Route::livewire('settings/checkout', AdminSettingsCheckout::class)->name('settings.checkout');
**File:** `tests/Browser/Admin/ProductManagementTest.php`

**Purpose:** Verify CRUD operations for products through the admin interface - listing, creating, editing, archiving, searching, and filtering by status.

### Product Lifecycle States

```mermaid
stateDiagram-v2
    [*] --> Draft: create product (default)
    Draft --> Active: set status to Active + Save
    Active --> Archived: set status to Archived + Save
    Draft --> Archived: set status to Archived + Save
    Archived --> Active: set status to Active + Save
    Active --> Draft: set status to Draft + Save
```

---

### Test 3.1: Shows the product list with seeded products

**Preconditions:** Logged in as admin, seeded products

**Steps:**
1. Navigate to `/admin/login`
2. Enter email: `admin@acme.test`
3. Enter password: `password`
4. Click the "Sign in" button
5. Click "Products" in the sidebar

**Expected Results:**
- Page displays "Classic Cotton T-Shirt"
- Page displays "Premium Slim Fit Jeans"
- No JavaScript errors

---

### Test 3.2: Can create a new product

**Preconditions:** Logged in as admin

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin (email: `admin@acme.test`, password: `password`)
3. Click "Products" in the sidebar
4. Click "Add product"
5. Enter title: `Test Product Created by E2E`
6. Enter description: `This product was created by the E2E test suite.`
7. Enter vendor: `Test Vendor`
8. Enter product type: `T-Shirts`
9. Enter variant price (variants.0.price): `29.99`
10. Enter variant SKU (variants.0.sku): `E2E-TEST-001`
11. Enter variant quantity (variants.0.quantity): `50`
12. Click "Save"
13. Click "Products" to return to the product list

**Expected Results:**
- After step 12: page displays "Product saved", no JavaScript errors
- After step 13: page displays "Test Product Created by E2E" in the list

---

### Test 3.3: Can edit an existing product title

**Preconditions:** Logged in as admin, seeded products

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Products" in the sidebar
4. Verify "Classic Cotton T-Shirt" is visible in the list
5. Click "Classic Cotton T-Shirt" to open the edit form
6. Clear the title field and enter: `Classic Cotton T-Shirt Updated`
7. Click "Save"
8. Click "Products" to return to the product list

**Expected Results:**
- After step 7: page displays "Product saved", no JavaScript errors
- After step 8: page displays "Classic Cotton T-Shirt Updated" in the list

---

### Test 3.4: Can archive a product

**Preconditions:** Logged in as admin

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Products" in the sidebar
4. Click "Add product"
5. Enter title: `Product To Archive`
6. Enter variant price: `19.99`
7. Enter variant SKU: `E2E-ARCHIVE-001`
8. Enter variant quantity: `10`
9. Click "Save"
10. Verify "Product saved" is displayed
11. Click "Products" to return to the product list
12. Click "Product To Archive" to open the edit form
13. Change status to "Archived" (click the Archived option)
14. Click "Save"
15. Click "Products" to return to the product list

**Expected Results:**
- After step 9: page displays "Product saved"
- After step 14: page displays "Product saved"
- After step 15: with default "Active" filter, the archived product should not appear in the list

---

### Test 3.5: Shows draft products only in admin, not storefront

**Preconditions:** Logged in as admin, Product #15 exists with draft status

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Products" in the sidebar
4. Verify Product #15 is visible in the admin list with a "Draft" badge
5. Open a new browser context / navigate to `/collections/t-shirts` (as guest)
6. Verify the draft product does NOT appear in the collection listing
7. Navigate to `/search?q=draft`
8. Verify the draft product does NOT appear in search results

**Expected Results:**
- Step 4: draft product visible in admin, no JavaScript errors
- Step 6: draft product absent from storefront collection, no JavaScript errors
- Step 8: draft product absent from search results, no JavaScript errors

---

### Test 3.6: Can search products in admin

**Preconditions:** Logged in as admin, seeded products

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Products" in the sidebar
4. Enter "Cotton" in the search input field

**Expected Results:**
- Page displays "Classic Cotton T-Shirt" in filtered results
- No JavaScript errors

---

### Test 3.7: Can filter products by status in admin

**Preconditions:** Logged in as admin, seeded products including drafts

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Products" in the sidebar
4. Click the "Draft" status filter tab
5. Verify only draft products are visible
6. Click the "Active" status filter tab

**Expected Results:**
- After step 4: only draft products are visible, no JavaScript errors
- After step 6: "Classic Cotton T-Shirt" is visible, no JavaScript errors

**Total: 7 tests**

---


Output:
Chunk ID: 247d96
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1897
Output:
Route::get('oauth/authorize', [OAuthController::class, 'authorize'])
    ->middleware('auth')
    ->name('oauth.authorize');

Route::post('oauth/token', [OAuthController::class, 'token'])
    ->name('oauth.token');

Route::middleware(['auth', EnsureUserEmailIsVerified::class, 'admin'])->prefix('admin')->name('admin.')->group(function (): void {
    Route::livewire('/', AdminDashboard::class)->name('dashboard');
    Route::livewire('analytics', AdminAnalyticsIndex::class)->name('analytics.index');
    Route::livewire('apps', AdminAppsIndex::class)->name('apps.index');
    Route::livewire('apps/{installation}', AdminAppShow::class)->name('apps.show');
    Route::livewire('developers', AdminDevelopersIndex::class)->name('developers.index');
    Route::livewire('products', AdminProductsIndex::class)->name('products.index');
    Route::livewire('products/create', AdminProductForm::class)->name('products.create');
    Route::livewire('products/{product}/edit', AdminProductForm::class)->name('products.edit');
    Route::livewire('inventory', AdminInventoryIndex::class)->name('inventory.index');
    Route::livewire('orders', AdminOrdersIndex::class)->name('orders.index');
    Route::livewire('orders/{order}', AdminOrderShow::class)->name('orders.show');
    Route::livewire('customers', AdminCustomersIndex::class)->name('customers.index');
    Route::livewire('customers/{customer}', AdminCustomerShow::class)->name('customers.show');
    Route::livewire('discounts', AdminDiscountsIndex::class)->name('discounts.index');
    Route::livewire('discounts/create', AdminDiscountForm::class)->name('discounts.create');
    Route::livewire('discounts/{discount}/edit', AdminDiscountForm::class)->name('discounts.edit');
    Route::livewire('pages', AdminPagesIndex::class)->name('pages.index');
    Route::livewire('pages/create', AdminPageForm::class)->name('pages.create');
    Route::livewire('pages/{page}/edit', AdminPageForm::class)->name('pages.edit');
    Route::livewire('navigation', AdminNavigationIndex::class)->name('navigation.index');
    Route::livewire('themes', AdminThemesIndex::class)->name('themes.index');
    Route::livewire('themes/{theme}/editor', AdminThemeEditor::class)->name('themes.editor');
    Route::livewire('settings', AdminSettingsIndex::class)->name('settings.index');
    Route::livewire('settings/shipping', AdminSettingsShipping::class)->name('settings.shipping');
    Route::livewire('settings/taxes', AdminSettingsTaxes::class)->name('settings.taxes');
    Route::livewire('settings/checkout', AdminSettingsCheckout::class)->name('settings.checkout');
**File:** `tests/Browser/Admin/ProductManagementTest.php`

**Purpose:** Verify CRUD operations for products through the admin interface - listing, creating, editing, archiving, searching, and filtering by status.

### Product Lifecycle States

```mermaid
stateDiagram-v2
    [*] --> Draft: create product (default)
    Draft --> Active: set status to Active + Save
    Active --> Archived: set status to Archived + Save
    Draft --> Archived: set status to Archived + Save
    Archived --> Active: set status to Active + Save
    Active --> Draft: set status to Draft + Save
```

---

### Test 3.1: Shows the product list with seeded products

**Preconditions:** Logged in as admin, seeded products

**Steps:**
1. Navigate to `/admin/login`
2. Enter email: `admin@acme.test`
3. Enter password: `password`
4. Click the "Sign in" button
5. Click "Products" in the sidebar

**Expected Results:**
- Page displays "Classic Cotton T-Shirt"
- Page displays "Premium Slim Fit Jeans"
- No JavaScript errors

---

### Test 3.2: Can create a new product

**Preconditions:** Logged in as admin

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin (email: `admin@acme.test`, password: `password`)
3. Click "Products" in the sidebar
4. Click "Add product"
5. Enter title: `Test Product Created by E2E`
6. Enter description: `This product was created by the E2E test suite.`
7. Enter vendor: `Test Vendor`
8. Enter product type: `T-Shirts`
9. Enter variant price (variants.0.price): `29.99`
10. Enter variant SKU (variants.0.sku): `E2E-TEST-001`
11. Enter variant quantity (variants.0.quantity): `50`
12. Click "Save"
13. Click "Products" to return to the product list

**Expected Results:**
- After step 12: page displays "Product saved", no JavaScript errors
- After step 13: page displays "Test Product Created by E2E" in the list

---

### Test 3.3: Can edit an existing product title

**Preconditions:** Logged in as admin, seeded products

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Products" in the sidebar
4. Verify "Classic Cotton T-Shirt" is visible in the list
5. Click "Classic Cotton T-Shirt" to open the edit form
6. Clear the title field and enter: `Classic Cotton T-Shirt Updated`
7. Click "Save"
8. Click "Products" to return to the product list

**Expected Results:**
- After step 7: page displays "Product saved", no JavaScript errors
- After step 8: page displays "Classic Cotton T-Shirt Updated" in the list

---

### Test 3.4: Can archive a product

**Preconditions:** Logged in as admin

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Products" in the sidebar
4. Click "Add product"
5. Enter title: `Product To Archive`
6. Enter variant price: `19.99`
7. Enter variant SKU: `E2E-ARCHIVE-001`
8. Enter variant quantity: `10`
9. Click "Save"
10. Verify "Product saved" is displayed
11. Click "Products" to return to the product list
12. Click "Product To Archive" to open the edit form
13. Change status to "Archived" (click the Archived option)
14. Click "Save"
15. Click "Products" to return to the product list

**Expected Results:**
- After step 9: page displays "Product saved"
- After step 14: page displays "Product saved"
- After step 15: with default "Active" filter, the archived product should not appear in the list

---

### Test 3.5: Shows draft products only in admin, not storefront

**Preconditions:** Logged in as admin, Product #15 exists with draft status

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Products" in the sidebar
4. Verify Product #15 is visible in the admin list with a "Draft" badge
5. Open a new browser context / navigate to `/collections/t-shirts` (as guest)
6. Verify the draft product does NOT appear in the collection listing
7. Navigate to `/search?q=draft`
8. Verify the draft product does NOT appear in search results

**Expected Results:**
- Step 4: draft product visible in admin, no JavaScript errors
- Step 6: draft product absent from storefront collection, no JavaScript errors
- Step 8: draft product absent from search results, no JavaScript errors

---

### Test 3.6: Can search products in admin

**Preconditions:** Logged in as admin, seeded products

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Products" in the sidebar
4. Enter "Cotton" in the search input field

**Expected Results:**
- Page displays "Classic Cotton T-Shirt" in filtered results
- No JavaScript errors

---

### Test 3.7: Can filter products by status in admin

**Preconditions:** Logged in as admin, seeded products including drafts

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Products" in the sidebar
4. Click the "Draft" status filter tab
5. Verify only draft products are visible
6. Click the "Active" status filter tab

**Expected Results:**
- After step 4: only draft products are visible, no JavaScript errors
- After step 6: "Classic Cotton T-Shirt" is visible, no JavaScript errors

**Total: 7 tests**

---


rg -n "Unreleased|Classic Cotton|Premium Slim|draft|t-shirts" database tests app resources


sed -n '1,220p' tests/Browser/Storefront/BrowsingTest.php && sed -n '1,180p' tests/Browser/Storefront/InventoryTest.php


sed -n '1,260p' resources/views/livewire/admin/products/form.blade.php && sed -n '1,180p' resources/views/livewire/admin/products/index.blade.php


sed -n '1,220p' tests/Browser/Storefront/BrowsingTest.php && sed -n '1,180p' tests/Browser/Storefront/InventoryTest.php

InventoryTest.php

<?php

use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Pest\Browser\Playwright\Playwright;

uses(RefreshDatabase::class);

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

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

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

function 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();
});
<?php

use App\Enums\InventoryPolicy;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductOptionValue;
use App\Models\ProductVariant;
use App\Models\Store;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Pest\Browser\Playwright\Playwright;

uses(RefreshDatabase::class);

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

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

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

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

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

/**
 * @param  array<string, string>  $options
 */
function storefrontInventoryVariant(string $handle, array $options): ProductVariant
{
    $store = storefrontInventoryStore();
    $product = Product::withoutGlobalScopes()
        ->where('store_id', $store->getKey())
        ->where('handle', $handle)
        ->firstOrFail();

    return ProductVariant::withoutGlobalScopes()
        ->with(['optionValues.option'])
        ->where('product_id', $product->getKey())
        ->get()
        ->first(function (ProductVariant $variant) use ($options): bool {
            $variantOptions = $variant->optionValues
                ->mapWithKeys(fn (ProductOptionValue $value): array => [$value->option->name => $value->value])
                ->all();

            return $variantOptions === $options;
        }) ?? throw new RuntimeException('Variant fixture not found.');
}

test('blocks add to cart for out of stock deny policy product', function (): void {
    visit('/products/limited-edition-sneakers', storefrontInventoryHost())
        ->assertSee('Limited Edition Sneakers')
        ->assertSee('Out of stock')
        ->assertSee('Sold out')
        ->assertButtonDisabled('button:has-text("Sold out")')
        ->assertDontSee('Add to cart')
        ->assertNoJavaScriptErrors();
});

test('allows add to cart for out of stock continue policy product', function (): void {
    visit('/products/backorder-denim-jacket', storefrontInventoryHost())
        ->assertSee('Backorder Denim Jacket')
        ->assertSee('Available on backorder')
        ->click('Add to cart')
        ->wait(1)
        ->navigate('/cart')
        ->wait(1)
        ->assertSee('Backorder Denim Jacket')
        ->assertNoJavaScriptErrors();
});

test('shows correct stock status for in stock product', function (): void {
    visit('/products/classic-cotton-t-shirt', storefrontInventoryHost())
        ->assertSee('Classic Cotton T-Shirt')
        ->assertSee('In stock')
        ->assertButtonEnabled('button:has-text("Add to cart")')
        ->assertDontSee('Sold out')
        ->assertDontSee('Available on backorder')
        ->assertNoJavaScriptErrors();
});

test('prevents adding more than available stock for deny policy product', function (): void {
    $variant = storefrontInventoryVariant('classic-cotton-t-shirt', [
        'Size' => 'M',
        'Color' => 'Black',
    ]);

    InventoryItem::withoutGlobalScopes()
        ->where('variant_id', $variant->getKey())
        ->firstOrFail()
        ->forceFill([
            'quantity_on_hand' => 2,
            'quantity_reserved' => 0,
            'policy' => InventoryPolicy::Deny,
        ])
        ->save();

    visit('/products/classic-cotton-t-shirt', storefrontInventoryHost())
        ->click('M')
        ->wait(1)
        ->click('Black')
        ->wait(1)
        ->assertSee('Only 2 left in stock')
        ->click('Add to cart')
        ->wait(1)
        ->navigate('/cart')
        ->wait(1)
        ->click('main button[aria-label="Increase Classic Cotton T-Shirt quantity"]')
        ->wait(1)
        ->assertSee('2 items')
        ->click('main button[aria-label="Increase Classic Cotton T-Shirt quantity"]')
        ->wait(1)
        ->assertSee('Only 2 units are available')
        ->assertSee('2 items')
        ->assertNoJavaScriptErrors();
});


rg -n "Unreleased|Classic Cotton|Premium Slim|draft|t-shirts" database tests app resources

rg -n 'Unreleased|Classic Cotton|Premium Slim|draft|t-shirts' database tests app resources

Question: Unreleased|Classic Cotton|Premium Slim|draft|t-shirts

resources/views/livewire/admin/themes/index.blade.php:4:        <flux:text class="mt-1">Published and draft storefront themes.</flux:text>
database/factories/ProductFactory.php:39:    public function draft(): static
app/Models/Theme.php:33:        'status' => 'draft',
app/Models/Page.php:32:        'status' => 'draft',
app/Models/Product.php:37:        'status' => 'draft',
app/Enums/ThemeStatus.php:7:    case Draft = 'draft';
tests/Feature/Api/AdminCollectionApiTest.php:79:            'status' => 'draft',
tests/Feature/Api/AdminCollectionApiTest.php:85:        ->assertJsonPath('data.status', 'draft')
app/Enums/ProductStatus.php:7:    case Draft = 'draft';
app/Http/Controllers/Api/Admin/V1/ProductController.php:21:            'status' => ['nullable', Rule::in(['draft', 'active', 'archived'])],
resources/views/livewire/admin/pages/form.blade.php:42:                        <flux:select.option value="draft">Draft</flux:select.option>
tests/Feature/Api/AdminThemeApiTest.php:116:        ->assertJsonPath('data.status', 'draft')
tests/Feature/Api/AdminThemeApiTest.php:130:    $draft = adminThemeApiDraftTheme($store);
tests/Feature/Api/AdminThemeApiTest.php:133:        ->putJson("/api/admin/v1/stores/{$store->getKey()}/themes/{$draft->getKey()}/settings", [
tests/Feature/Api/AdminThemeApiTest.php:146:        ->postJson("/api/admin/v1/stores/{$store->getKey()}/themes/{$draft->getKey()}/publish")
tests/Feature/Api/AdminThemeApiTest.php:150:    expect(ThemeSettings::withoutGlobalScopes()->where('theme_id', $draft->getKey())->first()?->settings_json['home']['hero']['heading'])->toBe('API Saved Hero')
tests/Feature/Api/AdminThemeApiTest.php:151:        ->and($draft->refresh()->status)->toBe(ThemeStatus::Published)
tests/Feature/Api/AdminThemeApiTest.php:159:    $draft = adminThemeApiDraftTheme($store);
tests/Feature/Api/AdminThemeApiTest.php:165:        ->postJson("/api/admin/v1/stores/{$store->getKey()}/themes/{$draft->getKey()}/publish")
tests/Feature/Api/AdminThemeApiTest.php:169:        ->postJson("/api/admin/v1/stores/{$store->getKey()}/themes/{$draft->getKey()}/publish")
tests/Feature/Api/AdminThemeApiTest.php:173:        ->putJson("/api/admin/v1/stores/{$store->getKey()}/themes/{$draft->getKey()}/settings", [
tests/Feature/Api/AdminThemeApiTest.php:186:    $draft = Theme::factory()->create([
tests/Feature/Api/AdminThemeApiTest.php:201:        ->putJson("/api/admin/v1/stores/{$store->getKey()}/themes/{$draft->getKey()}/settings", [
tests/Feature/Api/AdminThemeApiTest.php:208:        ->postJson("/api/admin/v1/stores/{$store->getKey()}/themes/{$draft->getKey()}/publish")
app/Http/Controllers/Api/Admin/V1/DiscountController.php:161:            'status' => ['sometimes', Rule::in(['draft', 'active', 'expired', 'disabled'])],
resources/views/livewire/admin/collections/index.blade.php:19:            <flux:select.option value="draft">Draft</flux:select.option>
resources/views/livewire/admin/collections/form.blade.php:78:                    <flux:select.option value="draft">Draft</flux:select.option>
database/factories/CollectionFactory.php:35:    public function draft(): static
tests/Feature/Api/AdminPageApiTest.php:76:            'status' => 'draft',
tests/Feature/Api/AdminPageApiTest.php:81:        ->assertJsonPath('data.status', 'draft');
app/Http/Controllers/Api/Admin/V1/CollectionController.php:25:            'status' => ['nullable', Rule::in(['draft', 'active', 'archived'])],
app/Http/Controllers/Api/Admin/V1/CollectionController.php:137:            'status' => ['sometimes', Rule::in(['draft', 'active', 'archived'])],
app/Enums/CollectionStatus.php:7:    case Draft = 'draft';
resources/views/livewire/admin/products/index.blade.php:19:            <flux:select.option value="draft">Draft</flux:select.option>
app/Enums/DiscountStatus.php:7:    case Draft = 'draft';
tests/Feature/Cart/CartServiceTest.php:92:    $draftProduct = Product::factory()
tests/Feature/Cart/CartServiceTest.php:93:        ->draft()
tests/Feature/Cart/CartServiceTest.php:96:    $draftVariant = ProductVariant::withoutGlobalScopes()
tests/Feature/Cart/CartServiceTest.php:97:        ->where('product_id', $draftProduct->getKey())
tests/Feature/Cart/CartServiceTest.php:100:    expect(fn () => $service->addLine($cart, $draftVariant->getKey(), 1))
resources/views/livewire/admin/products/form.blade.php:180:                        <flux:select.option value="draft">Draft</flux:select.option>
app/Services/ProductService.php:128:            throw InvalidProductTransitionException::because('Products with order history cannot be reverted to draft.');
app/Services/ProductService.php:148:            throw InvalidProductTransitionException::because('Only draft products with no order history can be deleted.');
tests/Feature/Foundation/DatabaseConstraintTest.php:20:        ->and($tableSql['products'])->toContain('"status" varchar check ("status" in (\'draft\', \'active\', \'archived\'))')
app/Enums/PageStatus.php:7:    case Draft = 'draft';
tests/Feature/Admin/Discount…5911 chars truncated…atabase/seeders/NavigationItemSeeder.php:74:        $secondaryCollection = $this->collection($store, 't-shirts') ?? $this->collection($store, 'accessories');
database/seeders/AnalyticsSeeder.php:113:                'url' => $index % 2 === 0 ? '/' : '/collections/t-shirts',
tests/Browser/Storefront/TenantIsolationTest.php:105:        ->assertSee('Classic Cotton T-Shirt')
tests/Browser/Storefront/TenantIsolationTest.php:112:    visit('/collections/t-shirts', tenantIsolationHost())
tests/Browser/Storefront/TenantIsolationTest.php:114:        ->assertSee('Classic Cotton T-Shirt')
tests/Browser/Storefront/TenantIsolationTest.php:140:        ->assertSee('Classic Cotton T-Shirt')
tests/Browser/Storefront/BrowsingTest.php:27:        ->assertSee('Classic Cotton T-Shirt')
tests/Browser/Storefront/BrowsingTest.php:33:    visit('/collections/t-shirts', storefrontBrowsingHost())
tests/Browser/Storefront/BrowsingTest.php:35:        ->assertSee('Classic Cotton T-Shirt')
tests/Browser/Storefront/BrowsingTest.php:40:    visit('/collections/t-shirts', storefrontBrowsingHost())
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/BrowsingTest.php:138:        ->click('nav[aria-label="Main navigation"] a[href$="/collections/t-shirts"]')
tests/Browser/Storefront/BrowsingTest.php:140:        ->assertPathIs('/collections/t-shirts')
database/migrations/2026_05_03_213902_create_collections_table.php:21:            $table->enum('status', ['draft', 'active', 'archived'])->default('active');
tests/Browser/Storefront/AccessibilityTest.php:108:        && placeholder.getAttribute('aria-label')?.includes('Classic Cotton T-Shirt');
tests/Browser/Storefront/CartTest.php:41:        ->assertSee('Classic Cotton T-Shirt');
tests/Browser/Storefront/CartTest.php:56:        ->assertSee('Classic Cotton T-Shirt')
tests/Browser/Storefront/CartTest.php:69:        ->click('main button[aria-label="Increase Classic Cotton T-Shirt quantity"]')
tests/Browser/Storefront/CartTest.php:78:        ->click('main button[aria-label="Remove Classic Cotton T-Shirt"]')
tests/Browser/Storefront/CartTest.php:92:        ->assertSee('Classic Cotton T-Shirt')
tests/Browser/Storefront/CartTest.php:93:        ->assertSee('Premium Slim Fit Jeans')
tests/Browser/Storefront/InventoryTest.php:83:        ->assertSee('Classic Cotton T-Shirt')
tests/Browser/Storefront/InventoryTest.php:117:        ->click('main button[aria-label="Increase Classic Cotton T-Shirt quantity"]')
tests/Browser/Storefront/InventoryTest.php:120:        ->click('main button[aria-label="Increase Classic Cotton T-Shirt quantity"]')
app/Livewire/Admin/Products/Form.php:174:            'status' => ['required', Rule::in(['draft', 'active', 'archived'])],
tests/Browser/Storefront/ResponsiveTest.php:60:        ->assertSee('Classic Cotton T-Shirt');
tests/Browser/Storefront/ResponsiveTest.php:102:        ->assertSee('Classic Cotton T-Shirt')
tests/Browser/Storefront/ResponsiveTest.php:129:        ->assertSee('Classic Cotton T-Shirt')
tests/Browser/Storefront/ResponsiveTest.php:183:    $page = storefrontResponsiveMobileVisit('/collections/t-shirts')
tests/Browser/Storefront/ResponsiveTest.php:186:        ->assertSee('Classic Cotton T-Shirt')
tests/Browser/Storefront/CustomerAccountTest.php:86:                'title_snapshot' => 'Classic Cotton T-Shirt',
app/Livewire/Admin/Collections/Form.php:85:            'status' => ['required', Rule::in(['draft', 'active', 'archived'])],
database/migrations/2026_05_03_223824_create_themes_table.php:19:            $table->enum('status', ['draft', 'published'])->default('draft');
app/Livewire/Admin/Pages/Form.php:27:    public string $status = 'draft';
database/migrations/2026_05_03_223841_create_pages_table.php:20:            $table->enum('status', ['draft', 'published', 'archived'])->default('draft');
database/migrations/2026_05_03_225427_create_discounts_table.php:26:            $table->enum('status', ['draft', 'active', 'expired', 'disabled'])->default('active');
database/migrations/2026_05_03_213856_create_products_table.php:19:            $table->enum('status', ['draft', 'active', 'archived'])->default('draft');


sed -n '1,260p' resources/views/livewire/admin/products/form.blade.php && sed -n '1,180p' resources/views/livewire/admin/products/index.blade.php

index.blade.php

            <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="…10327 chars truncated…on>
                <flux:button wire:click="bulkDelete" variant="danger">Delete</flux:button>

    @endif

                            <input type="checkbox" wire:click="toggleSelectAll" @checked($selectAll) aria-label="Select all visible products" class="rounded border-zinc-300 dark:border-zinc-600">

                        Product
                        Status
                        Inventory
                        Type
                        Vendor

                            <button wire:click="sortBy('updated_at')" class="font-semibold">Updated</button>

                    @forelse ($products as $product)
                        @php
                            $inventory = $product->variants->sum(fn ($variant) => $variant->inventoryItem?->quantity_on_hand ?? 0);
                            $statusColor = match ($product->status->value) {
                                'active' => 'green',
                                'archived' => 'red',
                                default => 'zinc',
                            };
                        @endphp

getKey() }}">

                                <input type="checkbox" wire:model.live="selectedIds" value="{{ $product->getKey() }}" aria-label="Select {{ $product->title }}" class="rounded border-zinc-300 dark:border-zinc-600">

                                        <flux:icon name="cube" class="size-5" />

                                            {{ $product->title }}

{{ $product->variants_count }} variants

                                <flux:badge :color="$statusColor">{{ Str::title($product->status->value) }}</flux:badge>

                            {{ $inventory }}
                            {{ $product->product_type ?: '-' }}
                            {{ $product->vendor ?: '-' }}
                            {{ $product->updated_at?->diffForHumans() }}

                    @empty

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

                                    <flux:heading size="lg">No products found</flux:heading>
                                    <flux:text>Adjust your filters or add a product.</flux:text>
                                    <flux:button :href="route('admin.products.create')" wire:navigate variant="primary">Add product</flux:button>

                    @endforelse

    {{ $products->links() }}