Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/specs/progress.md

@@ -35,3 +35,3 @@
 | Seed/test data | `specs/07-SEEDERS-AND-TEST-DATA.md` | partial | Seeders create Acme Fashion and Acme Electronics stores/domains/settings/admin access with checkout and notification defaults, 2 apps, 4 app installations, 4 OAuth clients, 4 OAuth tokens, 8 webhook subscriptions, 2 search settings rows, 25 indexed products, 60 analytics_daily rows, 210 analytics_events rows, 6 collections, 127 variants, 127 inventory rows, 206 variant option pivots, 2 themes, 6 theme files with local disk contents and metadata, 2 theme settings rows, 6 pages, 4 navigation menus, 21 navigation items including 6 child items, 2 tax settings rows, 2 shipping zones, 6 shipping rates, 6 discounts, the seeded customer, 1 default seeded customer address, and no product media/runtime carts. Order/payment seed data is still missing. |
-| Browser E2E plan | `specs/08-PLAYWRIGHT-E2E-PLAN.md` | partial | Manual Playwright MCP smoke coverage has verified storefront catalog browsing on `shop.test` and `acme-fashion.test`, product detail, add-to-cart, cart drawer/page, checkout through order completion, order confirmation, customer order list/detail, SQLite-backed search and search API JSON, analytics event ingestion API, DB-backed content pages, seeded nested navigation dropdowns, admin dashboard on desktop/mobile, admin analytics, admin app list/detail, admin developers token/webhook flows, admin product/collection/inventory/order/customer/discount/content/navigation/settings/theme/search pages, admin checkout/notification settings pages, admin product media section rendering, admin analytics CSV export, admin customer address creation, admin discount creation/editing/filtering, admin page creation, admin nested navigation rendering/item creation/save, admin theme editor preview, admin theme file list/editor, admin search reindexing, admin fulfillment creation/shipping/delivery, and auth flows without console warnings/errors on successful pages. Automated Pest browser coverage now checks storefront core pages, storefront account auth/reset pages including the customer root reset paths and account aliases, authenticated admin navigation/pages/settings/theme/search/apps/developers surfaces, Suite 2 admin authentication login/validation/redirect/logout/sidebar navigation interactions, Suite 7 storefront browsing, Suite 8 cart flow, Suite 9 checkout flow, Suite 10 customer account registration/login/orders/addresses/logout, Suite 11 inventory policy enforcement, Suite 12 tenant isolation, Suite 13 responsive/mobile flows, Suite 14 accessibility checks, and mobile storefront rendering for JavaScript errors. The remaining Spec 08 interaction/permutation browser suites are still incomplete. |
+| Browser E2E plan | `specs/08-PLAYWRIGHT-E2E-PLAN.md` | partial | Manual Playwright MCP smoke coverage has verified storefront catalog browsing on `shop.test` and `acme-fashion.test`, product detail, add-to-cart, cart drawer/page, checkout through order completion, order confirmation, customer order list/detail, SQLite-backed search and search API JSON, analytics event ingestion API, DB-backed content pages, seeded nested navigation dropdowns, admin dashboard on desktop/mobile, admin analytics, admin app list/detail, admin developers token/webhook flows, admin product/collection/inventory/order/customer/discount/content/navigation/settings/theme/search pages, admin checkout/notification settings pages, admin product media section rendering, admin analytics CSV export, admin customer address creation, admin discount creation/editing/filtering, admin page creation, admin nested navigation rendering/item creation/save, admin theme editor preview, admin theme file list/editor, admin search reindexing, admin fulfillment creation/shipping/delivery, and auth flows without console warnings/errors on successful pages. Automated Pest browser coverage now checks storefront core pages, storefront account auth/reset pages including the customer root reset paths and account aliases, authenticated admin navigation/pages/settings/theme/search/apps/developers surfaces, Suite 2 admin authentication login/validation/redirect/logout/sidebar navigation interactions, Suite 3 admin product listing/create/edit/archive/search/status-filter/draft visibility interactions, Suite 7 storefront browsing, Suite 8 cart flow, Suite 9 checkout flow, Suite 10 customer account registration/login/orders/addresses/logout, Suite 11 inventory policy enforcement, Suite 12 tenant isolation, Suite 13 responsive/mobile flows, Suite 14 accessibility checks, and mobile storefront rendering for JavaScript errors. The remaining Spec 08 interaction/permutation browser suites are still incomplete. |
 | Roadmap phases | `specs/09-IMPLEMENTATION-ROADMAP.md` | in progress | Phase 1 foundation, Phase 2 catalog data/UI, Phase 3 theme/content/navigation data with storefront consumption, Phase 4 cart/checkout/pricing backend foundation, Phase 4 storefront cart/checkout UI through `payment_selected`, the Phase 4 cart/checkout REST API surface, cart-page estimates, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, Phase 5 storefront order completion/customer order surfaces, Phase 5 admin order management, Phase 5 order API surfaces, Phase 6 admin dashboard/customer/discount/content/settings/theme/navigation management, Phase 7 search, Phase 7 analytics, and Phase 7 apps/webhooks are implemented. Final verification and completion audit are next. |
@@ -489,2 +489,8 @@
 - 2026-05-04: `php -d memory_limit=512M vendor/bin/pest --compact` passed the full suite after Suite 14: 343 tests, 1971 assertions.
+- 2026-05-04: `mcp__laravel_boost__.search_docs` consulted Pest browser testing, Livewire form/file-upload docs, Flux input/select/button/table docs, and Laravel browser/session testing docs before adding the Spec 08 admin product-management browser suite.
+- 2026-05-04: `php artisan make:test --pest Browser/Admin/ProductManagementTest --no-interaction` created the Suite 3 product-management browser test, which was moved into the Spec 08 browser directory structure.
+- 2026-05-04: `vendor/bin/pint --dirty --format agent` passed after adding Suite 3 and the product form save-button test marker.
+- 2026-05-04: `php artisan test --compact tests/Browser/Admin/ProductManagementTest.php` passed for the new Suite 3 admin product-management browser coverage: 7 tests, 98 assertions.
+- 2026-05-04: `php artisan test --compact tests/Browser` passed after adding Suite 3: 101 tests, 630 assertions.
+- 2026-05-04: `php -d memory_limit=512M vendor/bin/pest --compact` passed the full suite after Suite 3: 350 tests, 2069 assertions.

@@ -558,3 +564,4 @@
 - Laravel's SQLite `enum()` migrations generate database `CHECK` clauses in this app; `DatabaseConstraintTest` locks representative constraints across tenancy, roles, catalog, orders, tax settings, and exports.
-- Automated browser coverage now combines smoke coverage with Suite 2 admin-auth interactions, Suite 7 storefront browsing interactions, Suite 8 cart-flow interactions, Suite 9 checkout-flow interactions, Suite 10 customer-account interactions, Suite 11 inventory-policy interactions, Suite 12 tenant-isolation interactions, Suite 13 responsive/mobile interactions, and Suite 14 accessibility interactions: storefront catalog/content/search/cart, storefront account auth/reset pages, the full authenticated admin navigation surface, admin login validation, protected-route redirects, logout, sidebar navigation, mobile storefront rendering, seeded collection/product/search/static page browsing, sale/compare-at pricing, draft-product visibility guards, stock/backorder messaging, main-navigation clicks, add-to-cart, cart quantity updates/removal, multi-product carts, subtotal/total display, valid/invalid/expired/maxed/free-shipping/fixed discount code handling, domestic/international checkout shipping, checkout discount carryover, address validation, empty-cart checkout protection, credit-card/PayPal/bank-transfer order completion, payment decline handling, payment-method UI switching, customer registration validation, customer login validation, account auth redirects, customer order history/detail, address book create/update, customer logout, deny-policy sold-out blocking, continue-policy backorder add-to-cart, in-stock status display, deny-policy cart stock-limit feedback, storefront product/collection/search store isolation, admin product/order store isolation, customer account store isolation, mobile hamburger navigation, mobile product/cart/checkout flows, mobile collection filtering, tablet admin login/sidebar navigation, console-clean home/cart pages, heading hierarchy, form labels, product image placeholder accessible text, checkout error linkage, keyboard navigation, and search input labeling.
+- Automated browser coverage now combines smoke coverage with Suite 2 admin-auth interactions, Suite 3 admin product-management interactions, Suite 7 storefront browsing interactions, Suite 8 cart-flow interactions, Suite 9 checkout-flow interactions, Suite 10 customer-account interactions, Suite 11 inventory-policy interactions, Suite 12 tenant-isolation interactions, Suite 13 responsive/mobile interactions, and Suite 14 accessibility interactions: storefront catalog/content/search/cart, storefront account auth/reset pages, the full authenticated admin navigation surface, admin login validation, protected-route redirects, logout, sidebar navigation, admin product listing/create/edit/archive/search/status-filter/draft visibility guards, mobile storefront rendering, seeded collection/product/search/static page browsing, sale/compare-at pricing, draft-product storefront visibility guards, stock/backorder messaging, main-navigation clicks, add-to-cart, cart quantity updates/removal, multi-product carts, subtotal/total display, valid/invalid/expired/maxed/free-shipping/fixed discount code handling, domestic/international checkout shipping, checkout discount carryover, address validation, empty-cart checkout protection, credit-card/PayPal/bank-transfer order completion, payment decline handling, payment-method UI switching, customer registration validation, customer login validation, account auth redirects, customer order history/detail, address book create/update, customer logout, deny-policy sold-out blocking, continue-policy backorder add-to-cart, in-stock status display, deny-policy cart stock-limit feedback, storefront product/collection/search store isolation, admin product/order store isolation, customer account store isolation, mobile hamburger navigation, mobile product/cart/checkout flows, mobile collection filtering, tablet admin login/sidebar navigation, console-clean home/cart pages, heading hierarchy, form labels, product image placeholder accessible text, checkout error linkage, keyboard navigation, and search input labeling.
+- The admin product-management browser suite uses Laravel `actingAs()` and `withSession()` to create deterministic authenticated browser visits; the separate Suite 2 admin-auth suite owns login-form coverage so persisted browser cookies from earlier tests cannot leak into product CRUD assertions.
 - Pest's per-visit browser `host` option is temporary, so host-sensitive storefront browser tests set the Playwright host for the whole test lifecycle to keep clicked absolute URLs resolving through `shop.test`.
@@ -571,3 +578,3 @@

-- Full automated browser interaction/permutation suites from Spec 08 are still incomplete beyond the expanded Pest browser smoke coverage, Suite 2 admin authentication, Suite 7 storefront browsing, Suite 8 cart flow, Suite 9 checkout flow, Suite 10 customer account flow, Suite 11 inventory enforcement, Suite 12 tenant isolation, Suite 13 responsive/mobile, and Suite 14 accessibility.
+- Full automated browser interaction/permutation suites from Spec 08 are still incomplete beyond the expanded Pest browser smoke coverage, Suite 2 admin authentication, Suite 3 admin product management, Suite 7 storefront browsing, Suite 8 cart flow, Suite 9 checkout flow, Suite 10 customer account flow, Suite 11 inventory enforcement, Suite 12 tenant isolation, Suite 13 responsive/mobile, and Suite 14 accessibility.
 - The full suite now needs a higher PHP memory limit when all browser suites run in one process; the default `php artisan test --compact` command hits 128 MB in Pest's browser WebSocket client, while `php -d memory_limit=512M vendor/bin/pest --compact` passes.
@@ -576,2 +583,2 @@

-Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, product media processing/admin upload controls, Phase 3 storefront theme/content/navigation data, the Phase 4 cart/checkout/pricing backend foundation, cart/checkout storefront UI through order completion, cart-page estimates, cart/checkout REST APIs, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, customer account order/address/logout views, admin dashboard/order/customer/discount/content/settings/theme-file/navigation/search/analytics/apps/developers management, storefront search and analytics APIs, product/customer/collection/discount/content page/store settings/search/analytics/shipping/tax/theme/order/export API surfaces, deferred OAuth/app route stubs, expanded automated browser smoke coverage, Suite 2 admin authentication browser coverage, Suite 7 storefront browsing browser coverage, Suite 8 cart-flow browser coverage, Suite 9 checkout-flow browser coverage, Suite 10 customer-account browser coverage, Suite 11 inventory-enforcement browser coverage, Suite 12 tenant-isolation browser coverage, Suite 13 responsive/mobile browser coverage, Suite 14 accessibility browser coverage, outbound webhook delivery foundations, spec-root customer password reset routes, and representative database CHECK constraint coverage are implemented, with the remaining full browser-suite gap tracked above.
+Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, product media processing/admin upload controls, Phase 3 storefront theme/content/navigation data, the Phase 4 cart/checkout/pricing backend foundation, cart/checkout storefront UI through order completion, cart-page estimates, cart/checkout REST APIs, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, customer account order/address/logout views, admin dashboard/order/customer/discount/content/settings/theme-file/navigation/search/analytics/apps/developers management, storefront search and analytics APIs, product/customer/collection/discount/content page/store settings/search/analytics/shipping/tax/theme/order/export API surfaces, deferred OAuth/app route stubs, expanded automated browser smoke coverage, Suite 2 admin authentication browser coverage, Suite 3 admin product-management browser coverage, Suite 7 storefront browsing browser coverage, Suite 8 cart-flow browser coverage, Suite 9 checkout-flow browser coverage, Suite 10 customer-account browser coverage, Suite 11 inventory-enforcement browser coverage, Suite 12 tenant-isolation browser coverage, Suite 13 responsive/mobile browser coverage, Suite 14 accessibility browser coverage, outbound webhook delivery foundations, spec-root customer password reset routes, and representative database CHECK constraint coverage are implemented, with the remaining full browser-suite gap tracked above.


Output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/specs/progress.md\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


git diff -- specs/progress.md resources/views/livewire/admin/products/form.blade.php


 resources/views/livewire/admin/products/form.blade.php |  2 +-
 specs/progress.md                                      | 15 +++++++++++----
 2 files changed, 12 insertions(+), 5 deletions(-)


 M resources/views/livewire/admin/products/form.blade.php
 M specs/progress.md
?? tests/Browser/Admin/ProductManagementTest.php


Output:
Chunk ID: 361f0d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 32
Output:
 M resources/views/livewire/admin/products/form.blade.php
 M specs/progress.md
?? tests/Browser/Admin/ProductManagementTest.php


Output:
Chunk ID: 0d8ebe
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 48
Output:
 resources/views/livewire/admin/products/form.blade.php |  2 +-
 specs/progress.md                                      | 15 +++++++++++----
 2 files changed, 12 insertions(+), 5 deletions(-)


git diff -- specs/progress.md resources/views/livewire/admin/products/form.blade.php

diff --git a/resources/views/livewire/admin/products/form.blade.php b/resources/views/livewire/admin/products/form.blade.php
index 401c899b..b3fa0065 100644
--- a/resources/views/livewire/admin/products/form.blade.php
+++ b/resources/views/livewire/admin/products/form.blade.php
@@ -211,7 +211,7 @@

                 <flux:button :href="route('admin.products.index')" wire:navigate variant="ghost">Discard</flux:button>
-                <flux:button type="submit" variant="primary" wire:loading.attr="disabled">
+                <flux:button type="submit" variant="primary" wire:loading.attr="disabled" data-test="product-save-button">
                     Save
                     Saving...
                 </flux:button>
diff --git a/specs/progress.md b/specs/progress.md
index 1b23ec90..9150b1de 100644
--- a/specs/progress.md
+++ b/specs/progress.md
@@ -33,7 +33,7 @@ ## Acceptance Checklist
 | Business logic | `specs/05-BUSINESS-LOGIC.md` | partial | `ResolveStore`, `BelongsToStore`, `StoreScope`, role checks, customer guard provider, store-scoped `CustomerPasswordResetService`, catalog product lifecycle transitions, SKU uniqueness, variant matrix rebuilding, inventory reserve/release/commit/restock, automatic variant inventory, variant option mapping, media resize/cleanup job, `SearchService` with per-store synonym expansion and stop-word removal, product observer FTS sync, search query logging, search API rate limiting, `AnalyticsService`, analytics event deduplication, daily analytics aggregation, scheduled `AggregateAnalytics`, analytics API rate limiting, `WebhookService`, timestamped HMAC signing/verification, database-queued `DeliverWebhook`, delivery logging/retry backoff metadata, webhook circuit breaker pausing, domain-event webhook dispatch listener, developer token generation backed by `oauth_tokens`, `OrderExportService`, `CartService`, `DiscountService` including one-per-customer code discount redemption checks and automatic/code order-line attribution, `ShippingCalculator`, `TaxCalculator`, `PricingEngine`, checkout transitions through completion, checkout expiration, abandoned cart cleanup, session cart lookup, cart discount handoff to checkout, customer login cart merge, product add-to-cart mutations, storefront cart stock-limit feedback for deny-policy inventory, `PaymentService`, mock PSP, idempotent order creation, checkout UI/API order submission, session/customer/token-gated order confirmation and lookup, customer order-history scoping, customer address book management, customer logout session invalidation, admin dashboard order aggregates, admin analytics reporting, admin customer address management, admin discount rule management, admin content/theme/navigation/settings/search/apps/developers orchestration, admin order UI/API action orchestration, `RefundService`, `FulfillmentService`, bank-transfer payment confirmation, and unpaid bank-transfer cancellation are implemented. |
 | Auth/security | `specs/06-AUTH-AND-SECURITY.md` | partial | Admin Livewire login/logout/reset, customer guard/provider, customer login/register/logout, customer password reset GET/POST routes at `/forgot-password` and `/reset-password/{token}` with store-scoped hashed tokens, reset-link throttling, generic reset responses, login rate limiting, session hardening, concrete resource policies, encrypted app client/webhook secrets, one-time display developer token generation, and a compatible hashed bearer-token admin API middleware with store/ability enforcement are implemented. |
 | Seed/test data | `specs/07-SEEDERS-AND-TEST-DATA.md` | partial | Seeders create Acme Fashion and Acme Electronics stores/domains/settings/admin access with checkout and notification defaults, 2 apps, 4 app installations, 4 OAuth clients, 4 OAuth tokens, 8 webhook subscriptions, 2 search settings rows, 25 indexed products, 60 analytics_daily rows, 210 analytics_events rows, 6 collections, 127 variants, 127 inventory rows, 206 variant option pivots, 2 themes, 6 theme files with local disk contents and metadata, 2 theme settings rows, 6 pages, 4 navigation menus, 21 navigation items including 6 child items, 2 tax settings rows, 2 shipping zones, 6 shipping rates, 6 discounts, the seeded customer, 1 default seeded customer address, and no product media/runtime carts. Order/payment seed data is still missing. |
-| Browser E2E plan | `specs/08-PLAYWRIGHT-E2E-PLAN.md` | partial | Manual Playwright MCP smoke coverage has verified storefront catalog browsing on `shop.test` and `acme-fashion.test`, product detail, add-to-cart, cart drawer/page, checkout through order completion, order confirmation, customer order list/detail, SQLite-backed search and search…10846 chars truncated…ext, checkout error linkage, keyboard navigation, and search input labeling.
+- The admin product-management browser suite uses Laravel `actingAs()` and `withSession()` to create deterministic authenticated browser visits; the separate Suite 2 admin-auth suite owns login-form coverage so persisted browser cookies from earlier tests cannot leak into product CRUD assertions.
 - Pest's per-visit browser `host` option is temporary, so host-sensitive storefront browser tests set the Playwright host for the whole test lifecycle to keep clicked absolute URLs resolving through `shop.test`.
 - The discount seeder now includes the Spec 07/08 codes `WELCOME10`, `FLAT5`, `FREESHIP`, `EXPIRED20`, and `MAXED` while preserving the earlier implementation/test fixture codes `SAVE10` and `5OFF`.
 - Expired discount codes return an expired validation message before the generic non-active status message so storefront/cart validation can present the expected customer-facing error.
@@ -569,9 +576,9 @@ ## Decisions

 ## Open Issues

-- Full automated browser interaction/permutation suites from Spec 08 are still incomplete beyond the expanded Pest browser smoke coverage, Suite 2 admin authentication, Suite 7 storefront browsing, Suite 8 cart flow, Suite 9 checkout flow, Suite 10 customer account flow, Suite 11 inventory enforcement, Suite 12 tenant isolation, Suite 13 responsive/mobile, and Suite 14 accessibility.
+- Full automated browser interaction/permutation suites from Spec 08 are still incomplete beyond the expanded Pest browser smoke coverage, Suite 2 admin authentication, Suite 3 admin product management, Suite 7 storefront browsing, Suite 8 cart flow, Suite 9 checkout flow, Suite 10 customer account flow, Suite 11 inventory enforcement, Suite 12 tenant isolation, Suite 13 responsive/mobile, and Suite 14 accessibility.
 - The full suite now needs a higher PHP memory limit when all browser suites run in one process; the default `php artisan test --compact` command hits 128 MB in Pest's browser WebSocket client, while `php -d memory_limit=512M vendor/bin/pest --compact` passes.

 ## Completion Summary

-Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, product media processing/admin upload controls, Phase 3 storefront theme/content/navigation data, the Phase 4 cart/checkout/pricing backend foundation, cart/checkout storefront UI through order completion, cart-page estimates, cart/checkout REST APIs, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, customer account order/address/logout views, admin dashboard/order/customer/discount/content/settings/theme-file/navigation/search/analytics/apps/developers management, storefront search and analytics APIs, product/customer/collection/discount/content page/store settings/search/analytics/shipping/tax/theme/order/export API surfaces, deferred OAuth/app route stubs, expanded automated browser smoke coverage, Suite 2 admin authentication browser coverage, Suite 7 storefront browsing browser coverage, Suite 8 cart-flow browser coverage, Suite 9 checkout-flow browser coverage, Suite 10 customer-account browser coverage, Suite 11 inventory-enforcement browser coverage, Suite 12 tenant-isolation browser coverage, Suite 13 responsive/mobile browser coverage, Suite 14 accessibility browser coverage, outbound webhook delivery foundations, spec-root customer password reset routes, and representative database CHECK constraint coverage are implemented, with the remaining full browser-suite gap tracked above.
+Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, product media processing/admin upload controls, Phase 3 storefront theme/content/navigation data, the Phase 4 cart/checkout/pricing backend foundation, cart/checkout storefront UI through order completion, cart-page estimates, cart/checkout REST APIs, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, customer account order/address/logout views, admin dashboard/order/customer/discount/content/settings/theme-file/navigation/search/analytics/apps/developers management, storefront search and analytics APIs, product/customer/collection/discount/content page/store settings/search/analytics/shipping/tax/theme/order/export API surfaces, deferred OAuth/app route stubs, expanded automated browser smoke coverage, Suite 2 admin authentication browser coverage, Suite 3 admin product-management browser coverage, Suite 7 storefront browsing browser coverage, Suite 8 cart-flow browser coverage, Suite 9 checkout-flow browser coverage, Suite 10 customer-account browser coverage, Suite 11 inventory-enforcement browser coverage, Suite 12 tenant-isolation browser coverage, Suite 13 responsive/mobile browser coverage, Suite 14 accessibility browser coverage, outbound webhook delivery foundations, spec-root customer password reset routes, and representative database CHECK constraint coverage are implemented, with the remaining full browser-suite gap tracked above.


Output:
Chunk ID: f28947
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 5212
Output:
diff --git a/resources/views/livewire/admin/products/form.blade.php b/resources/views/livewire/admin/products/form.blade.php
index 401c899b..b3fa0065 100644
--- a/resources/views/livewire/admin/products/form.blade.php
+++ b/resources/views/livewire/admin/products/form.blade.php
@@ -211,7 +211,7 @@

                 <flux:button :href="route('admin.products.index')" wire:navigate variant="ghost">Discard</flux:button>
-                <flux:button type="submit" variant="primary" wire:loading.attr="disabled">
+                <flux:button type="submit" variant="primary" wire:loading.attr="disabled" data-test="product-save-button">
                     Save
                     Saving...
                 </flux:button>
diff --git a/specs/progress.md b/specs/progress.md
index 1b23ec90..9150b1de 100644
--- a/specs/progress.md
+++ b/specs/progress.md
@@ -33,7 +33,7 @@ ## Acceptance Checklist
 | Business logic | `specs/05-BUSINESS-LOGIC.md` | partial | `ResolveStore`, `BelongsToStore`, `StoreScope`, role checks, customer guard provider, store-scoped `CustomerPasswordResetService`, catalog product lifecycle transitions, SKU uniqueness, variant matrix rebuilding, inventory reserve/release/commit/restock, automatic variant inventory, variant option mapping, media resize/cleanup job, `SearchService` with per-store synonym expansion and stop-word removal, product observer FTS sync, search query logging, search API rate limiting, `AnalyticsService`, analytics event deduplication, daily analytics aggregation, scheduled `AggregateAnalytics`, analytics API rate limiting, `WebhookService`, timestamped HMAC signing/verification, database-queued `DeliverWebhook`, delivery logging/retry backoff metadata, webhook circuit breaker pausing, domain-event webhook dispatch listener, developer token generation backed by `oauth_tokens`, `OrderExportService`, `CartService`, `DiscountService` including one-per-customer code discount redemption checks and automatic/code order-line attribution, `ShippingCalculator`, `TaxCalculator`, `PricingEngine`, checkout transitions through completion, checkout expiration, abandoned cart cleanup, session cart lookup, cart discount handoff to checkout, customer login cart merge, product add-to-cart mutations, storefront cart stock-limit feedback for deny-policy inventory, `PaymentService`, mock PSP, idempotent order creation, checkout UI/API order submission, session/customer/token-gated order confirmation and lookup, customer order-history scoping, customer address book management, customer logout session invalidation, admin dashboard order aggregates, admin analytics reporting, admin customer address management, admin discount rule management, admin content/theme/navigation/settings/search/apps/developers orchestration, admin order UI/API action orchestration, `RefundService`, `FulfillmentService`, bank-transfer payment confirmation, and unpaid bank-transfer cancellation are implemented. |
 | Auth/security | `specs/06-AUTH-AND-SECURITY.md` | partial | Admin Livewire login/logout/reset, customer guard/provider, customer login/register/logout, customer password reset GET/POST routes at `/forgot-password` and `/reset-password/{token}` with store-scoped hashed tokens, reset-link throttling, generic reset responses, login rate limiting, session hardening, concrete resource policies, encrypted app client/webhook secrets, one-time display developer token generation, and a compatible hashed bearer-token admin API middleware with store/ability enforcement are implemented. |
 | Seed/test data | `specs/07-SEEDERS-AND-TEST-DATA.md` | partial | Seeders create Acme Fashion and Acme Electronics stores/domains/settings/admin access with checkout and notification defaults, 2 apps, 4 app installations, 4 OAuth clients, 4 OAuth tokens, 8 webhook subscriptions, 2 search settings rows, 25 indexed products, 60 analytics_daily rows, 210 analytics_events rows, 6 collections, 127 variants, 127 inventory rows, 206 variant option pivots, 2 themes, 6 theme files with local disk contents and metadata, 2 theme settings rows, 6 pages, 4 navigation menus, 21 navigation items including 6 child items, 2 tax settings rows, 2 shipping zones, 6 shipping rates, 6 discounts, the seeded customer, 1 default seeded customer address, and no product media/runtime carts. Order/payment seed data is still missing. |
-| Browser E2E plan | `specs/08-PLAYWRIGHT-E2E-PLAN.md` | partial | Manual Playwright MCP smoke coverage has verified storefront catalog browsing on `shop.test` and `acme-fashion.test`, product detail, add-to-cart, cart drawer/page, checkout through order completion, order confirmation, customer order list/detail, SQLite-backed search and search API JSON, analytics event ingestion API, DB-backed content pages, seeded nested navigation dropdowns, admin dashboard on desktop/mobile, admin analytics, admin app list/detail, admin developers token/webhook flows, admin product/collection/inventory/order/customer/discount/content/navigation/settings/theme/search pages, admin checkout/notification settings pages, admin product media section rendering, admin analytics CSV export, admin customer address creation, admin discount creation/editing/filtering, admin page creation, admin nested navigation rendering/item creation/save, admin theme editor preview, admin theme file list/editor, admin search reindexing, admin fulfillment creation/shipping/delivery, and auth flows without console warnings/errors on successful pages. Automated Pest browser coverage now checks storefront core pages, storefront account auth/reset pages including the customer root reset paths and account aliases, authenticated admin navigation/pages/settings/theme/search/apps/developers surfaces, Suite 2 admin authentication login/validation/redirect/logout/sidebar navigation interactions, Suite 7 storefront browsing, Suite 8 cart flow, Suite 9 checkout flow, Suite 10 customer account registration/login/orders/addresses/logout, Suite 11 inventory policy enforcement, Suite 12 tenant isolation, Suite 13 responsive/mobile flows, Suite 14 accessibility checks, and mobile storefront rendering for JavaScript errors. The remaining Spec 08 interaction/permutation browser suites are still incomplete. |
+| Browser E2E plan | `specs/08-PLAYWRIGHT-E2E-PLAN.md` | partial | Manual Playwright MCP smoke coverage has verified storefront catalog browsing on `shop.test` and `acme-fashion.test`, product detail, add-to-cart, cart drawer/page, checkout through order completion, order confirmation, customer order list/detail, SQLite-backed search and search API JSON, analytics event ingestion API, DB-backed content pages, seeded nested navigation dropdowns, admin dashboard on desktop/mobile, admin analytics, admin app list/detail, admin developers token/webhook flows, admin product/collection/inventory/order/customer/discount/content/navigation/settings/theme/search pages, admin checkout/notification settings pages, admin product media section rendering, admin analytics CSV export, admin customer address creation, admin discount creation/editing/filtering, admin page creation, admin nested navigation rendering/item creation/save, admin theme editor preview, admin theme file list/editor, admin search reindexing, admin fulfillment creation/shipping/delivery, and auth flows without console warnings/errors on successful pages. Automated Pest browser coverage now checks storefront core pages, storefront account auth/reset pages including the customer root reset paths and account aliases, authenticated admin navigation/pages/settings/theme/search/apps/developers surfaces, Suite 2 admin authentication login/validation/redirect/logout/sidebar navigation interactions, Suite 3 admin product listing/create/edit/archive/search/status-filter/draft visibility interactions, Suite 7 storefront browsing, Suite 8 cart flow, Suite 9 checkout flow, Suite 10 customer account registration/login/orders/addresses/logout, Suite 11 inventory policy enforcement, Suite 12 tenant isolation, Suite 13 responsive/mobile flows, Suite 14 accessibility checks, and mobile storefront rendering for JavaScript errors. The remaining Spec 08 interaction/permutation browser suites are still incomplete. |
 | Roadmap phases | `specs/09-IMPLEMENTATION-ROADMAP.md` | in progress | Phase 1 foundation, Phase 2 catalog data/UI, Phase 3 theme/content/navigation data with storefront consumption, Phase 4 cart/checkout/pricing backend foundation, Phase 4 storefront cart/checkout UI through `payment_selected`, the Phase 4 cart/checkout REST API surface, cart-page estimates, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, Phase 5 storefront order completion/customer order surfaces, Phase 5 admin order management, Phase 5 order API surfaces, Phase 6 admin dashboard/customer/discount/content/settings/theme/navigation management, Phase 7 search, Phase 7 analytics, and Phase 7 apps/webhooks are implemented. Final verification and completion audit are next. |

 ## Verification Evidence
@@ -487,6 +487,12 @@ ## Verification Evidence
 - 2026-05-04: `npm run build` passed after the storefront accessibility Blade changes.
 - 2026-05-04: `php artisan test --compact tests/Browser/SmokeTest.php tests/Browser/Admin/AuthenticationTest.php tests/Browser/Storefront/BrowsingTest.php tests/Browser/Storefront/CartTest.php tests/Browser/Storefront/CheckoutTest.php tests/Browser/Storefront/CustomerAccountTest.php tests/Browser/Storefront/InventoryTest.php tests/Browser/Storefront/TenantIsolationTest.php tests/Browser/Storefront/ResponsiveTest.php tests/Browser/Storefront/AccessibilityTest.php` passed after adding Suite 14: 94 tests, 532 assertions.
 - 2026-05-04: `php -d memory_limit=512M vendor/bin/pest --compact` passed the full suite after Suite 14: 343 tests, 1971 assertions.
+- 2026-05-04: `mcp__laravel_boost__.search_docs` consulted Pest browser testing, Livewire form/file-upload docs, Flux input/select/button/table docs, and Laravel browser/session testing docs before adding the Spec 08 admin product-management browser suite.
+- 2026-05-04: `php artisan make:test --pest Browser/Admin/ProductManagementTest --no-interaction` created the Suite 3 product-management browser test, which was moved into the Spec 08 browser directory structure.
+- 2026-05-04: `vendor/bin/pint --dirty --format agent` passed after adding Suite 3 and the product form save-button test marker.
+- 2026-05-04: `php artisan test --compact tests/Browser/Admin/ProductManagementTest.php` passed for the new Suite 3 admin product-management browser coverage: 7 tests, 98 assertions.
+- 2026-05-04: `php artisan test --compact tests/Browser` passed after adding Suite 3: 101 tests, 630 assertions.
+- 2026-05-04: `php -d memory_limit=512M vendor/bin/pest --compact` passed the full suite after Suite 3: 350 tests, 2069 assertions.

 ## Decisions

@@ -556,7 +562,8 @@ ## Decisions
 - Product media uploads use Livewire temporary file uploads on the existing product form, create `ProductMedia` rows in `processing` status, and dispatch the existing `ProcessMediaUpload` job instead of adding a second media-processing path.
 - Resource policies now type concrete model parameters for Product, Collection, Customer, Discount, Order, Fulfillment, Refund, Page, and Theme actions; `ChecksStoreRole::storeIdForModel()` stays generic because it is the shared store-id extraction helper.
 - Laravel's SQLite `enum()` migrations generate database `CHECK` clauses in this app; `DatabaseConstraintTest` locks representative constraints across tenancy, roles, catalog, orders, tax settings, and exports.
-- Automated browser coverage now combines smoke coverage with Suite 2 admin-auth interactions, Suite 7 storefront browsing interactions, Suite 8 cart-flow interactions, Suite 9 checkout-flow interactions, Suite 10 customer-account interactions, Suite 11 inventory-policy interactions, Suite 12 tenant-isolation interactions, Suite 13 responsive/mobile interactions, and Suite 14 accessibility interactions: storefront catalog/content/search/cart, storefront account auth/reset pages, the full authenticated admin navigation surface, admin login validation, protected-route redirects, logout, sidebar navigation, mobile storefront rendering, seeded collection/product/search/static page browsing, sale/compare-at pricing, draft-product visibility guards, stock/backorder messaging, main-navigation clicks, add-to-cart, cart quantity updates/removal, multi-product carts, subtotal/total display, valid/invalid/expired/maxed/free-shipping/fixed discount code handling, domestic/international checkout shipping, checkout discount carryover, address validation, empty-cart checkout protection, credit-card/PayPal/bank-transfer order completion, payment decline handling, payment-method UI switching, customer registration validation, customer login validation, account auth redirects, customer order history/detail, address book create/update, customer logout, deny-policy sold-out blocking, continue-policy backorder add-to-cart, in-stock status display, deny-policy cart stock-limit feedback, storefront product/collection/search store isolation, admin product/order store isolation, customer account store isolation, mobile hamburger navigation, mobile product/cart/checkout flows, mobile collection filtering, tablet admin login/sidebar navigation, console-clean home/cart pages, heading hierarchy, form labels, product image placeholder accessible text, checkout error linkage, keyboard navigation, and search input labeling.
+- Automated browser coverage now combines smoke coverage with Suite 2 admin-auth interactions, Suite 3 admin product-management interactions, Suite 7 storefront browsing interactions, Suite 8 cart-flow interactions, Suite 9 checkout-flow interactions, Suite 10 customer-account interactions, Suite 11 inventory-policy interactions, Suite 12 tenant-isolation interactions, Suite 13 responsive/mobile interactions, and Suite 14 accessibility interactions: storefront catalog/content/search/cart, storefront account auth/reset pages, the full authenticated admin navigation surface, admin login validation, protected-route redirects, logout, sidebar navigation, admin product listing/create/edit/archive/search/status-filter/draft visibility guards, mobile storefront rendering, seeded collection/product/search/static page browsing, sale/compare-at pricing, draft-product storefront visibility guards, stock/backorder messaging, main-navigation clicks, add-to-cart, cart quantity updates/removal, multi-product carts, subtotal/total display, valid/invalid/expired/maxed/free-shipping/fixed discount code handling, domestic/international checkout shipping, checkout discount carryover, address validation, empty-cart checkout protection, credit-card/PayPal/bank-transfer order completion, payment decline handling, payment-method UI switching, customer registration validation, customer login validation, account auth redirects, customer order history/detail, address book create/update, customer logout, deny-policy sold-out blocking, continue-policy backorder add-to-cart, in-stock status display, deny-policy cart stock-limit feedback, storefront product/collection/search store isolation, admin product/order store isolation, customer account store isolation, mobile hamburger navigation, mobile product/cart/checkout flows, mobile collection filtering, tablet admin login/sidebar navigation, console-clean home/cart pages, heading hierarchy, form labels, product image placeholder accessible text, checkout error linkage, keyboard navigation, and search input labeling.
+- The admin product-management browser suite uses Laravel `actingAs()` and `withSession()` to create deterministic authenticated browser visits; the separate Suite 2 admin-auth suite owns login-form coverage so persisted browser cookies from earlier tests cannot leak into product CRUD assertions.
 - Pest's per-visit browser `host` option is temporary, so host-sensitive storefront browser tests set the Playwright host for the whole test lifecycle to keep clicked absolute URLs resolving through `shop.test`.
 - The discount seeder now includes the Spec 07/08 codes `WELCOME10`, `FLAT5`, `FREESHIP`, `EXPIRED20`, and `MAXED` while preserving the earlier implementation/test fixture codes `SAVE10` and `5OFF`.
 - Expired discount codes return an expired validation message before the generic non-active status message so storefront/cart validation can present the expected customer-facing error.
@@ -569,9 +576,9 @@ ## Decisions

 ## Open Issues

-- Full automated browser interaction/permutation suites from Spec 08 are still incomplete beyond the expanded Pest browser smoke coverage, Suite 2 admin authentication, Suite 7 storefront browsing, Suite 8 cart flow, Suite 9 checkout flow, Suite 10 customer account flow, Suite 11 inventory enforcement, Suite 12 tenant isolation, Suite 13 responsive/mobile, and Suite 14 accessibility.
+- Full automated browser interaction/permutation suites from Spec 08 are still incomplete beyond the expanded Pest browser smoke coverage, Suite 2 admin authentication, Suite 3 admin product management, Suite 7 storefront browsing, Suite 8 cart flow, Suite 9 checkout flow, Suite 10 customer account flow, Suite 11 inventory enforcement, Suite 12 tenant isolation, Suite 13 responsive/mobile, and Suite 14 accessibility.
 - The full suite now needs a higher PHP memory limit when all browser suites run in one process; the default `php artisan test --compact` command hits 128 MB in Pest's browser WebSocket client, while `php -d memory_limit=512M vendor/bin/pest --compact` passes.

 ## Completion Summary

-Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, product media processing/admin upload controls, Phase 3 storefront theme/content/navigation data, the Phase 4 cart/checkout/pricing backend foundation, cart/checkout storefront UI through order completion, cart-page estimates, cart/checkout REST APIs, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, customer account order/address/logout views, admin dashboard/order/customer/discount/content/settings/theme-file/navigation/search/analytics/apps/developers management, storefront search and analytics APIs, product/customer/collection/discount/content page/store settings/search/analytics/shipping/tax/theme/order/export API surfaces, deferred OAuth/app route stubs, expanded automated browser smoke coverage, Suite 2 admin authentication browser coverage, Suite 7 storefront browsing browser coverage, Suite 8 cart-flow browser coverage, Suite 9 checkout-flow browser coverage, Suite 10 customer-account browser coverage, Suite 11 inventory-enforcement browser coverage, Suite 12 tenant-isolation browser coverage, Suite 13 responsive/mobile browser coverage, Suite 14 accessibility browser coverage, outbound webhook delivery foundations, spec-root customer password reset routes, and representative database CHECK constraint coverage are implemented, with the remaining full browser-suite gap tracked above.
+Not complete. Phase 1 foundation, Phase 2 catalog data/UI surfaces, product media processing/admin upload controls, Phase 3 storefront theme/content/navigation data, the Phase 4 cart/checkout/pricing backend foundation, cart/checkout storefront UI through order completion, cart-page estimates, cart/checkout REST APIs, Phase 5 order/payment backend foundation, Phase 5 refund/fulfillment services, customer account order/address/logout views, admin dashboard/order/customer/discount/content/settings/theme-file/navigation/search/analytics/apps/developers management, storefront search and analytics APIs, product/customer/collection/discount/content page/store settings/search/analytics/shipping/tax/theme/order/export API surfaces, deferred OAuth/app route stubs, expanded automated browser smoke coverage, Suite 2 admin authentication browser coverage, Suite 3 admin product-management browser coverage, Suite 7 storefront browsing browser coverage, Suite 8 cart-flow browser coverage, Suite 9 checkout-flow browser coverage, Suite 10 customer-account browser coverage, Suite 11 inventory-enforcement browser coverage, Suite 12 tenant-isolation browser coverage, Suite 13 responsive/mobile browser coverage, Suite 14 accessibility browser coverage, outbound webhook delivery foundations, spec-root customer password reset routes, and representative database CHECK constraint coverage are implemented, with the remaining full browser-suite gap tracked above.


sed -n '1,260p' tests/Browser/Admin/ProductManagementTest.php


sed -n '1,260p' tests/Browser/Admin/ProductManagementTest.php

<?php

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

uses(RefreshDatabase::class);

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

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

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

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

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

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

    return visit('/admin', adminProductHost())
        ->wait(1)
        ->assertPathIs('/admin')
        ->assertSee('Dashboard')
        ->assertNoJavaScriptErrors();
}

function adminProductOpenProducts(mixed $testCase): mixed
{
    return adminProductLogin($testCase)
        ->click('a[href$="/admin/products"]')
        ->wait(1)
        ->assertPathIs('/admin/products')
        ->assertSee('Products')
        ->assertNoJavaScriptErrors();
}

function adminProductFillProductForm(
    mixed $page,
    string $title,
    string $handle,
    string $sku,
    string $price,
    string $quantity,
    string $description = '',
    string $vendor = '',
    string $productType = '',
): mixed {
    $page
        ->fill('input[wire\\:model\\.live\\.debounce\\.300ms="title"]', $title)
        ->fill('input[wire\\:model="handle"]', $handle)
        ->fill('input[wire\\:model="variants.0.sku"]', $sku)
        ->fill('input[wire\\:model="variants.0.price"]', $price)
        ->fill('input[wire\\:model="variants.0.quantity"]', $quantity);

    if ($description !== '') {
        $page->fill('textarea[wire\\:model="descriptionHtml"]', $description);
    }

    if ($vendor !== '') {
        $page->fill('input[wire\\:model="vendor"]', $vendor);
    }

    if ($productType !== '') {
        $page->fill('input[wire\\:model="productType"]', $productType);
    }

    return $page->wait(1);
}

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

function adminProductReturnToList(mixed $page): mixed
{
    return $page
        ->click('ui-sidebar a[href$="/admin/products"]')
        ->wait(1)
        ->assertPathIs('/admin/products')
        ->assertSee('Products')
        ->assertNoJavaScriptErrors();
}

test('shows the product list with seeded products', function (): void {
    adminProductOpenProducts($this)
        ->assertSee('Classic Cotton T-Shirt')
        ->assertSee('Premium Slim Fit Jeans')
        ->assertNoJavaScriptErrors();
});

test('can create a new product', function (): void {
    $page = adminProductOpenProducts($this)
        ->click('a[href$="/admin/products/create"]')
        ->wait(1)
        ->assertPathIs('/admin/products/create')
        ->assertSee('Add product');

    adminProductFillProductForm(
        $page,
        'Test Product Created by E2E',
        'test-product-created-by-e2e',
        'E2E-TEST-001',
        '29.99',
        '50',
        'This product was created by the E2E test suite.',
        'Test Vendor',
        'T-Shirts',
    );

    adminProductSave($page);

    adminProductReturnToList($page)
        ->assertSee('Test Product Created by E2E')
        ->assertNoJavaScriptErrors();
});

test('can edit an existing product title', function (): void {
    $page = adminProductOpenProducts($this)
        ->assertSee('Classic Cotton T-Shirt')
        ->click('a:has-text("Classic Cotton T-Shirt")')
        ->wait(1)
        ->assertSee('Classic Cotton T-Shirt');

    $page
        ->fill('input[wire\\:model\\.live\\.debounce\\.300ms="title"]', 'Classic Cotton T-Shirt Updated')
        ->wait(1);

    adminProductSave($page);

    adminProductReturnToList($page)
        ->assertSee('Classic Cotton T-Shirt Updated')
        ->assertNoJavaScriptErrors();
});

test('can archive a product', function (): void {
    $page = adminProductOpenProducts($this)
        ->click('a[href$="/admin/products/create"]')
        ->wait(1)
        ->assertPathIs('/admin/products/create')
        ->assertSee('Add product');

    adminProductFillProductForm(
        $page,
        'Product To Archive',
        'product-to-archive',
        'E2E-ARCHIVE-001',
        '19.99',
        '10',
    );

    adminProductSave($page);

    adminProductReturnToList($page)
        ->assertSee('Product To Archive')
        ->click('a:has-text("Product To Archive")')
        ->wait(1)
        ->assertSee('Product To Archive')
        ->select('select[wire\\:model="status"]', 'archived');

    adminProductSave($page);

    adminProductReturnToList($page)
        ->assertDontSee('Product To Archive')
        ->assertNoJavaScriptErrors();
});

test('shows draft products only in admin and not storefront', function (): void {
    $page = adminProductOpenProducts($this)
        ->select('select[wire\\:model\\.live="statusFilter"]', 'draft')
        ->wait(1)
        ->assertSee('Unreleased Winter Jacket')
        ->assertSee('Draft')
        ->assertDontSee('Classic Cotton T-Shirt')
        ->assertNoJavaScriptErrors();

    $page->navigate('/collections/t-shirts')
        ->wait(1)
        ->assertPathIs('/collections/t-shirts')
        ->assertDontSee('Unreleased Winter Jacket')
        ->assertNoJavaScriptErrors()
        ->navigate('/search?q=draft')
        ->wait(1)
        ->assertPathIs('/search')
        ->assertDontSee('Unreleased Winter Jacket')
        ->assertNoJavaScriptErrors();
});

test('can search products in admin', function (): void {
    adminProductOpenProducts($this)
        ->fill('input[wire\\:model\\.live\\.debounce\\.300ms="search"]', 'Cotton')
        ->wait(1)
        ->assertSee('Classic Cotton T-Shirt')
        ->assertDontSee('Premium Slim Fit Jeans')
        ->assertNoJavaScriptErrors();
});

test('can filter products by status in admin', function (): void {
    adminProductOpenProducts($this)
        ->select('select[wire\\:model\\.live="statusFilter"]', 'draft')
        ->wait(1)
        ->assertSee('Unreleased Winter Jacket')
        ->assertDontSee('Classic Cotton T-Shirt')
        ->assertNoJavaScriptErrors()
        ->select('select[wire\\:model\\.live="statusFilter"]', 'active')
        ->wait(1)
        ->assertSee('Classic Cotton T-Shirt')
        ->assertDontSee('Unreleased Winter Jacket')
        ->assertNoJavaScriptErrors();
});


Output:
Chunk ID: d5ca13
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1676
Output:
<?php

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

uses(RefreshDatabase::class);

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

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

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

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

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

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

    return visit('/admin', adminProductHost())
        ->wait(1)
        ->assertPathIs('/admin')
        ->assertSee('Dashboard')
        ->assertNoJavaScriptErrors();
}

function adminProductOpenProducts(mixed $testCase): mixed
{
    return adminProductLogin($testCase)
        ->click('a[href$="/admin/products"]')
        ->wait(1)
        ->assertPathIs('/admin/products')
        ->assertSee('Products')
        ->assertNoJavaScriptErrors();
}

function adminProductFillProductForm(
    mixed $page,
    string $title,
    string $handle,
    string $sku,
    string $price,
    string $quantity,
    string $description = '',
    string $vendor = '',
    string $productType = '',
): mixed {
    $page
        ->fill('input[wire\\:model\\.live\\.debounce\\.300ms="title"]', $title)
        ->fill('input[wire\\:model="handle"]', $handle)
        ->fill('input[wire\\:model="variants.0.sku"]', $sku)
        ->fill('input[wire\\:model="variants.0.price"]', $price)
        ->fill('input[wire\\:model="variants.0.quantity"]', $quantity);

    if ($description !== '') {
        $page->fill('textarea[wire\\:model="descriptionHtml"]', $description);
    }

    if ($vendor !== '') {
        $page->fill('input[wire\\:model="vendor"]', $vendor);
    }

    if ($productType !== '') {
        $page->fill('input[wire\\:model="productType"]', $productType);
    }

    return $page->wait(1);
}

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

function adminProductReturnToList(mixed $page): mixed
{
    return $page
        ->click('ui-sidebar a[href$="/admin/products"]')
        ->wait(1)
        ->assertPathIs('/admin/products')
        ->assertSee('Products')
        ->assertNoJavaScriptErrors();
}

test('shows the product list with seeded products', function (): void {
    adminProductOpenProducts($this)
        ->assertSee('Classic Cotton T-Shirt')
        ->assertSee('Premium Slim Fit Jeans')
        ->assertNoJavaScriptErrors();
});

test('can create a new product', function (): void {
    $page = adminProductOpenProducts($this)
        ->click('a[href$="/admin/products/create"]')
        ->wait(1)
        ->assertPathIs('/admin/products/create')
        ->assertSee('Add product');

    adminProductFillProductForm(
        $page,
        'Test Product Created by E2E',
        'test-product-created-by-e2e',
        'E2E-TEST-001',
        '29.99',
        '50',
        'This product was created by the E2E test suite.',
        'Test Vendor',
        'T-Shirts',
    );

    adminProductSave($page);

    adminProductReturnToList($page)
        ->assertSee('Test Product Created by E2E')
        ->assertNoJavaScriptErrors();
});

test('can edit an existing product title', function (): void {
    $page = adminProductOpenProducts($this)
        ->assertSee('Classic Cotton T-Shirt')
        ->click('a:has-text("Classic Cotton T-Shirt")')
        ->wait(1)
        ->assertSee('Classic Cotton T-Shirt');

    $page
        ->fill('input[wire\\:model\\.live\\.debounce\\.300ms="title"]', 'Classic Cotton T-Shirt Updated')
        ->wait(1);

    adminProductSave($page);

    adminProductReturnToList($page)
        ->assertSee('Classic Cotton T-Shirt Updated')
        ->assertNoJavaScriptErrors();
});

test('can archive a product', function (): void {
    $page = adminProductOpenProducts($this)
        ->click('a[href$="/admin/products/create"]')
        ->wait(1)
        ->assertPathIs('/admin/products/create')
        ->assertSee('Add product');

    adminProductFillProductForm(
        $page,
        'Product To Archive',
        'product-to-archive',
        'E2E-ARCHIVE-001',
        '19.99',
        '10',
    );

    adminProductSave($page);

    adminProductReturnToList($page)
        ->assertSee('Product To Archive')
        ->click('a:has-text("Product To Archive")')
        ->wait(1)
        ->assertSee('Product To Archive')
        ->select('select[wire\\:model="status"]', 'archived');

    adminProductSave($page);

    adminProductReturnToList($page)
        ->assertDontSee('Product To Archive')
        ->assertNoJavaScriptErrors();
});

test('shows draft products only in admin and not storefront', function (): void {
    $page = adminProductOpenProducts($this)
        ->select('select[wire\\:model\\.live="statusFilter"]', 'draft')
        ->wait(1)
        ->assertSee('Unreleased Winter Jacket')
        ->assertSee('Draft')
        ->assertDontSee('Classic Cotton T-Shirt')
        ->assertNoJavaScriptErrors();

    $page->navigate('/collections/t-shirts')
        ->wait(1)
        ->assertPathIs('/collections/t-shirts')
        ->assertDontSee('Unreleased Winter Jacket')
        ->assertNoJavaScriptErrors()
        ->navigate('/search?q=draft')
        ->wait(1)
        ->assertPathIs('/search')
        ->assertDontSee('Unreleased Winter Jacket')
        ->assertNoJavaScriptErrors();
});

test('can search products in admin', function (): void {
    adminProductOpenProducts($this)
        ->fill('input[wire\\:model\\.live\\.debounce\\.300ms="search"]', 'Cotton')
        ->wait(1)
        ->assertSee('Classic Cotton T-Shirt')
        ->assertDontSee('Premium Slim Fit Jeans')
        ->assertNoJavaScriptErrors();
});

test('can filter products by status in admin', function (): void {
    adminProductOpenProducts($this)
        ->select('select[wire\\:model\\.live="statusFilter"]', 'draft')
        ->wait(1)
        ->assertSee('Unreleased Winter Jacket')
        ->assertDontSee('Classic Cotton T-Shirt')
        ->assertNoJavaScriptErrors()
        ->select('select[wire\\:model\\.live="statusFilter"]', 'active')
        ->wait(1)
        ->assertSee('Classic Cotton T-Shirt')
        ->assertDontSee('Unreleased Winter Jacket')
        ->assertNoJavaScriptErrors();
});


The progress log now reflects Suite 3 and the full-suite rerun. I’m staging the product-management browser test, the one stable save-button marker, and the progress update, then I’ll commit this slice.


git add resources/views/livewire/admin/products/form.blade.php tests/Browser/Admin/ProductManagementTest.php specs/progress.md


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


git diff --cached -- tests/Browser/Admin/ProductManagementTest.php


git diff --cached -- tests/Browser/Admin/ProductManagementTest.php

diff --git a/tests/Browser/Admin/ProductManagementTest.php b/tests/Browser/Admin/ProductManagementTest.php
new file mode 100644
index 00000000..1a9a726f
--- /dev/null
+++ b/tests/Browser/Admin/ProductManagementTest.php
@@ -0,0 +1,228 @@
+<?php
+
+use App\Models\Store;
+use App\Models\User;
+use Database\Seeders\DatabaseSeeder;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Pest\Browser\Playwright\Playwright;
+
+uses(RefreshDatabase::class);
+
+beforeEach(function (): void {
+    Playwright::setHost('shop.test');
+
+    $this->seed(DatabaseSeeder::class);
+});
+
+afterEach(function (): void {
+    Playwright::setHost(null);
+});
+
+function adminProductHost(): array
+{
+    return ['host' => 'shop.test'];
+}
+
+function adminProductLogin(mixed $testCase): mixed
+{
+    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
+    $user = User::query()->where('email', 'admin@acme.test')->firstOrFail();
+
+    $testCase->actingAs($user);
+    $testCase->withSession(['current_store_id' => $store->getKey()]);
+
+    return visit('/admin', adminProductHost())
+        ->wait(1)
+        ->assertPathIs('/admin')
+        ->assertSee('Dashboard')
+        ->assertNoJavaScriptErrors();
+}
+
+function adminProductOpenProducts(mixed $testCase): mixed
+{
+    return adminProductLogin($testCase)
+        ->click('a[href$="/admin/products"]')
+        ->wait(1)
+        ->assertPathIs('/admin/products')
+        ->assertSee('Products')
+        ->assertNoJavaScriptErrors();
+}
+
+function adminProductFillProductForm(
+    mixed $page,
+    string $title,
+    string $handle,
+    string $sku,
+    string $price,
+    string $quantity,
+    string $description = '',
+    string $vendor = '',
+    string $productType = '',
+): mixed {
+    $page
+        ->fill('input[wire\\:model\\.live\\.debounce\\.300ms="title"]', $title)
+        ->fill('input[wire\\:model="handle"]', $handle)
+        ->fill('input[wire\\:model="variants.0.sku"]', $sku)
+        ->fill('input[wire\\:model="variants.0.price"]', $price)
+        ->fill('input[wire\\:model="variants.0.quantity"]', $quantity);
+
+    if ($description !== '') {
+        $page->fill('textarea[wire\\:model="descriptionHtml"]', $description);
+    }
+
+    if ($vendor !== '') {
+        $page->fill('input[wire\\:model="vendor"]', $vendor);
+    }
+
+    if ($productType !== '') {
+        $page->fill('input[wire\\:model="productType"]', $productType);
+    }
+
+    return $page->wait(1);
+}
+
+function adminProductSave(mixed $page): mixed
+{
+    return $page
+        ->click('button[data-test="product-save-button"]')
+        ->wait(1)
+        ->assertSee('Product saved')
+        ->assertNoJavaScriptErrors();
+}
+
+function adminProductReturnToList(mixed $page): mixed
+{
+    return $page
+        ->click('ui-sidebar a[href$="/admin/products"]')
+        ->wait(1)
+        ->assertPathIs('/admin/products')
+        ->assertSee('Products')
+        ->assertNoJavaScriptErrors();
+}
+
+test('shows the product list with seeded products', function (): void {
+    adminProductOpenProducts($this)
+        ->assertSee('Classic Cotton T-Shirt')
+        ->assertSee('Premium Slim Fit Jeans')
+        ->assertNoJavaScriptErrors();
+});
+
+test('can create a new product', function (): void {
+    $page = adminProductOpenProducts($this)
+        ->click('a[href$="/admin/products/create"]')
+        ->wait(1)
+        ->assertPathIs('/admin/products/create')
+        ->assertSee('Add product');
+
+    adminProductFillProductForm(
+        $page,
+        'Test Product Created by E2E',
+        'test-product-created-by-e2e',
+        'E2E-TEST-001',
+        '29.99',
+        '50',
+        'This product was created by the E2E test suite.',
+        'Test Vendor',
+        'T-Shirts',
+    );
+
+    adminProductSave($page);
+
+    adminProductReturnToList($page)
+        ->assertSee('Test Product Created by E2E')
+        ->assertNoJavaScriptErrors();
+});
+
+test('can edit an existing product title', function (): void {
+    $page = adminProductOpenProducts($this)
+        ->assertSee('Classic Cotton T-Shirt')
+        ->click('a:has-text("Classic Cotton T-Shirt")')
+        ->wait(1)
+        ->assertSee('Classic Cotton T-Shirt');
+
+    $page
+        ->fill('input[wire\\:model\\.live\\.debounce\\.300ms="title"]', 'Classic Cotton T-Shirt Updated')
+        ->wait(1);
+
+    adminProductSave($page);
+
+    adminProductReturnToList($page)
+        ->assertSee('Classic Cotton T-Shirt Updated')
+        ->assertNoJavaScriptErrors();
+});
+
+test('can archive a product', function (): void {
+    $page = adminProductOpenProducts($this)
+        ->click('a[href$="/admin/products/create"]')
+        ->wait(1)
+        ->assertPathIs('/admin/products/create')
+        ->assertSee('Add product');
+
+    adminProductFillProductForm(
+        $page,
+        'Product To Archive',
+        'product-to-archive',
+        'E2E-ARCHIVE-001',
+        '19.99',
+        '10',
+    );
+
+    adminProductSave($page);
+
+    adminProductReturnToList($page)
+        ->assertSee('Product To Archive')
+        ->click('a:has-text("Product To Archive")')
+        ->wait(1)
+        ->assertSee('Product To Archive')
+        ->select('select[wire\\:model="status"]', 'archived');
+
+    adminProductSave($page);
+
+    adminProductReturnToList($page)
+        ->assertDontSee('Product To Archive')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows draft products only in admin and not storefront', function (): void {
+    $page = adminProductOpenProducts($this)
+        ->select('select[wire\\:model\\.live="statusFilter"]', 'draft')
+        ->wait(1)
+        ->assertSee('Unreleased Winter Jacket')
+        ->assertSee('Draft')
+        ->assertDontSee('Classic Cotton T-Shirt')
+        ->assertNoJavaScriptErrors();
+
+    $page->navigate('/collections/t-shirts')
+        ->wait(1)
+        ->assertPathIs('/collections/t-shirts')
+        ->assertDontSee('Unreleased Winter Jacket')
+        ->assertNoJavaScriptErrors()
+        ->navigate('/search?q=draft')
+        ->wait(1)
+        ->assertPathIs('/search')
+        ->assertDontSee('Unreleased Winter Jacket')
+        ->assertNoJavaScriptErrors();
+});
+
+test('can search products in admin', function (): void {
+    adminProductOpenProducts($this)
+        ->fill('input[wire\\:model\\.live\\.debounce\\.300ms="search"]', 'Cotton')
+        ->wait(1)
+        ->assertSee('Classic Cotton T-Shirt')
+        ->assertDontSee('Premium Slim Fit Jeans')
+        ->assertNoJavaScriptErrors();
+});
+
+test('can filter products by status in admin', function (): void {
+    adminProductOpenProducts($this)
+        ->select('select[wire\\:model\\.live="statusFilter"]', 'draft')
+        ->wait(1)
+        ->assertSee('Unreleased Winter Jacket')
+        ->assertDontSee('Classic Cotton T-Shirt')
+        ->assertNoJavaScriptErrors()
+        ->select('select[wire\\:model\\.live="statusFilter"]', 'active')
+        ->wait(1)
+        ->assertSee('Classic Cotton T-Shirt')
+        ->assertDontSee('Unreleased Winter Jacket')
+        ->assertNoJavaScriptErrors();
+});


 .../views/livewire/admin/products/form.blade.php   |   2 +-
 specs/progress.md                                  |  15 +-
 tests/Browser/Admin/ProductManagementTest.php      | 228 +++++++++++++++++++++
 3 files changed, 240 insertions(+), 5 deletions(-)


M  resources/views/livewire/admin/products/form.blade.php
M  specs/progress.md
A  tests/Browser/Admin/ProductManagementTest.php


Output:
Chunk ID: 3e7324
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 32
Output:
M  resources/views/livewire/admin/products/form.blade.php
M  specs/progress.md
A  tests/Browser/Admin/ProductManagementTest.php


Output:
Chunk ID: f63539
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 64
Output:
 .../views/livewire/admin/products/form.blade.php   |   2 +-
 specs/progress.md                                  |  15 +-
 tests/Browser/Admin/ProductManagementTest.php      | 228 +++++++++++++++++++++
 3 files changed, 240 insertions(+), 5 deletions(-)


Output:
Chunk ID: 591e22
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1792
Output:
diff --git a/tests/Browser/Admin/ProductManagementTest.php b/tests/Browser/Admin/ProductManagementTest.php
new file mode 100644
index 00000000..1a9a726f
--- /dev/null
+++ b/tests/Browser/Admin/ProductManagementTest.php
@@ -0,0 +1,228 @@
+<?php
+
+use App\Models\Store;
+use App\Models\User;
+use Database\Seeders\DatabaseSeeder;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Pest\Browser\Playwright\Playwright;
+
+uses(RefreshDatabase::class);
+
+beforeEach(function (): void {
+    Playwright::setHost('shop.test');
+
+    $this->seed(DatabaseSeeder::class);
+});
+
+afterEach(function (): void {
+    Playwright::setHost(null);
+});
+
+function adminProductHost(): array
+{
+    return ['host' => 'shop.test'];
+}
+
+function adminProductLogin(mixed $testCase): mixed
+{
+    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
+    $user = User::query()->where('email', 'admin@acme.test')->firstOrFail();
+
+    $testCase->actingAs($user);
+    $testCase->withSession(['current_store_id' => $store->getKey()]);
+
+    return visit('/admin', adminProductHost())
+        ->wait(1)
+        ->assertPathIs('/admin')
+        ->assertSee('Dashboard')
+        ->assertNoJavaScriptErrors();
+}
+
+function adminProductOpenProducts(mixed $testCase): mixed
+{
+    return adminProductLogin($testCase)
+        ->click('a[href$="/admin/products"]')
+        ->wait(1)
+        ->assertPathIs('/admin/products')
+        ->assertSee('Products')
+        ->assertNoJavaScriptErrors();
+}
+
+function adminProductFillProductForm(
+    mixed $page,
+    string $title,
+    string $handle,
+    string $sku,
+    string $price,
+    string $quantity,
+    string $description = '',
+    string $vendor = '',
+    string $productType = '',
+): mixed {
+    $page
+        ->fill('input[wire\\:model\\.live\\.debounce\\.300ms="title"]', $title)
+        ->fill('input[wire\\:model="handle"]', $handle)
+        ->fill('input[wire\\:model="variants.0.sku"]', $sku)
+        ->fill('input[wire\\:model="variants.0.price"]', $price)
+        ->fill('input[wire\\:model="variants.0.quantity"]', $quantity);
+
+    if ($description !== '') {
+        $page->fill('textarea[wire\\:model="descriptionHtml"]', $description);
+    }
+
+    if ($vendor !== '') {
+        $page->fill('input[wire\\:model="vendor"]', $vendor);
+    }
+
+    if ($productType !== '') {
+        $page->fill('input[wire\\:model="productType"]', $productType);
+    }
+
+    return $page->wait(1);
+}
+
+function adminProductSave(mixed $page): mixed
+{
+    return $page
+        ->click('button[data-test="product-save-button"]')
+        ->wait(1)
+        ->assertSee('Product saved')
+        ->assertNoJavaScriptErrors();
+}
+
+function adminProductReturnToList(mixed $page): mixed
+{
+    return $page
+        ->click('ui-sidebar a[href$="/admin/products"]')
+        ->wait(1)
+        ->assertPathIs('/admin/products')
+        ->assertSee('Products')
+        ->assertNoJavaScriptErrors();
+}
+
+test('shows the product list with seeded products', function (): void {
+    adminProductOpenProducts($this)
+        ->assertSee('Classic Cotton T-Shirt')
+        ->assertSee('Premium Slim Fit Jeans')
+        ->assertNoJavaScriptErrors();
+});
+
+test('can create a new product', function (): void {
+    $page = adminProductOpenProducts($this)
+        ->click('a[href$="/admin/products/create"]')
+        ->wait(1)
+        ->assertPathIs('/admin/products/create')
+        ->assertSee('Add product');
+
+    adminProductFillProductForm(
+        $page,
+        'Test Product Created by E2E',
+        'test-product-created-by-e2e',
+        'E2E-TEST-001',
+        '29.99',
+        '50',
+        'This product was created by the E2E test suite.',
+        'Test Vendor',
+        'T-Shirts',
+    );
+
+    adminProductSave($page);
+
+    adminProductReturnToList($page)
+        ->assertSee('Test Product Created by E2E')
+        ->assertNoJavaScriptErrors();
+});
+
+test('can edit an existing product title', function (): void {
+    $page = adminProductOpenProducts($this)
+        ->assertSee('Classic Cotton T-Shirt')
+        ->click('a:has-text("Classic Cotton T-Shirt")')
+        ->wait(1)
+        ->assertSee('Classic Cotton T-Shirt');
+
+    $page
+        ->fill('input[wire\\:model\\.live\\.debounce\\.300ms="title"]', 'Classic Cotton T-Shirt Updated')
+        ->wait(1);
+
+    adminProductSave($page);
+
+    adminProductReturnToList($page)
+        ->assertSee('Classic Cotton T-Shirt Updated')
+        ->assertNoJavaScriptErrors();
+});
+
+test('can archive a product', function (): void {
+    $page = adminProductOpenProducts($this)
+        ->click('a[href$="/admin/products/create"]')
+        ->wait(1)
+        ->assertPathIs('/admin/products/create')
+        ->assertSee('Add product');
+
+    adminProductFillProductForm(
+        $page,
+        'Product To Archive',
+        'product-to-archive',
+        'E2E-ARCHIVE-001',
+        '19.99',
+        '10',
+    );
+
+    adminProductSave($page);
+
+    adminProductReturnToList($page)
+        ->assertSee('Product To Archive')
+        ->click('a:has-text("Product To Archive")')
+        ->wait(1)
+        ->assertSee('Product To Archive')
+        ->select('select[wire\\:model="status"]', 'archived');
+
+    adminProductSave($page);
+
+    adminProductReturnToList($page)
+        ->assertDontSee('Product To Archive')
+        ->assertNoJavaScriptErrors();
+});
+
+test('shows draft products only in admin and not storefront', function (): void {
+    $page = adminProductOpenProducts($this)
+        ->select('select[wire\\:model\\.live="statusFilter"]', 'draft')
+        ->wait(1)
+        ->assertSee('Unreleased Winter Jacket')
+        ->assertSee('Draft')
+        ->assertDontSee('Classic Cotton T-Shirt')
+        ->assertNoJavaScriptErrors();
+
+    $page->navigate('/collections/t-shirts')
+        ->wait(1)
+        ->assertPathIs('/collections/t-shirts')
+        ->assertDontSee('Unreleased Winter Jacket')
+        ->assertNoJavaScriptErrors()
+        ->navigate('/search?q=draft')
+        ->wait(1)
+        ->assertPathIs('/search')
+        ->assertDontSee('Unreleased Winter Jacket')
+        ->assertNoJavaScriptErrors();
+});
+
+test('can search products in admin', function (): void {
+    adminProductOpenProducts($this)
+        ->fill('input[wire\\:model\\.live\\.debounce\\.300ms="search"]', 'Cotton')
+        ->wait(1)
+        ->assertSee('Classic Cotton T-Shirt')
+        ->assertDontSee('Premium Slim Fit Jeans')
+        ->assertNoJavaScriptErrors();
+});
+
+test('can filter products by status in admin', function (): void {
+    adminProductOpenProducts($this)
+        ->select('select[wire\\:model\\.live="statusFilter"]', 'draft')
+        ->wait(1)
+        ->assertSee('Unreleased Winter Jacket')
+        ->assertDontSee('Classic Cotton T-Shirt')
+        ->assertNoJavaScriptErrors()
+        ->select('select[wire\\:model\\.live="statusFilter"]', 'active')
+        ->wait(1)
+        ->assertSee('Classic Cotton T-Shirt')
+        ->assertDontSee('Unreleased Winter Jacket')
+        ->assertNoJavaScriptErrors();
+});


git commit -m "Add admin product browser suite"


git commit -m "Add admin product browser suite"

[2026-05-04-codex-5-5-goal 03125377] Add admin product browser suite
 3 files changed, 240 insertions(+), 5 deletions(-)
 create mode 100644 tests/Browser/Admin/ProductManagementTest.php


Output:
Chunk ID: e8f3b4
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 47
Output:
[2026-05-04-codex-5-5-goal 03125377] Add admin product browser suite
 3 files changed, 240 insertions(+), 5 deletions(-)
 create mode 100644 tests/Browser/Admin/ProductManagementTest.php


03125377 Add admin product browser suite


Output:
Chunk ID: 7835e9
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 0
Output:


Output:
Chunk ID: 6d4757
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 11
Output:
03125377 Add admin product browser suite


Inspect Suite 3 product-management requirements and current admin product UI

Explanation: Suite 3 is committed as 03125377 after focused, browser-directory, and full-suite verification.


Output: Plan updated


Suite 3 is committed as `03125377`. I’m moving to the next missing Spec 08 browser suite now, starting by reading the suite matrix and the relevant admin surface so the next slice is scoped the same way.


sed -n '1,80p' specs/08-PLAYWRIGHT-E2E-PLAN.md


sed -n '675,980p' specs/08-PLAYWRIGHT-E2E-PLAN.md


sed -n '1,80p' specs/08-PLAYWRIGHT-E2E-PLAN.md

# 08 - Playwright E2E Test Plan

> Complete end-to-end browser test specification for the e-commerce platform using Pest v4 browser testing (Playwright-backed). All tests verify the entire application works through a real browser. This document defines requirements and specifications for the coding agent to implement.

---

## Test Suite Summary

| Suite | File | Tests | Purpose | Dependencies |
|-------|------|:-----:|---------|--------------|
| 1 - Smoke Tests | `tests/Browser/SmokeTest.php` | 10 | Hit every major page, assert HTTP 200 and no JS errors | Store 1, Products, Collections, Pages |
| 2 - Admin Authentication | `tests/Browser/Admin/AuthenticationTest.php` | 10 | Admin login, logout, invalid credentials, session access control | Admin user |
| 3 - Admin Product Management | `tests/Browser/Admin/ProductManagementTest.php` | 7 | Product CRUD: listing, creating, editing, archiving, filtering | Admin user, Products (incl. #15 draft) |
| 4 - Admin Order Management | `tests/Browser/Admin/OrderManagementTest.php` | 11 | Order listing, filtering, detail view, fulfillment, refund, confirm payment, fulfillment guard, mark shipped/delivered | Admin user, Orders (#1001, #1005), Customer |
| 5 - Admin Discount Management | `tests/Browser/Admin/DiscountManagementTest.php` | 6 | Discount code listing, creation, editing | Admin user, Discounts |
| 6 - Admin Settings | `tests/Browser/Admin/SettingsTest.php` | 7 | Store settings, shipping zones, tax config, domains | Admin user, Shipping zones, Store 1 |
| 7 - Storefront Browsing | `tests/Browser/Storefront/BrowsingTest.php` | 15 | Home page, collections, product detail, variants, search, pages | Products, Collections, Pages, Products #15/#17/#18 |
| 8 - Cart Flow | `tests/Browser/Storefront/CartTest.php` | 12 | Cart lifecycle: add, update qty, remove, discounts, totals | Products, Discounts (all codes) |
| 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 |
| 10 - Customer Account | `tests/Browser/Storefront/CustomerAccountTest.php` | 12 | Registration, login, order history, addresses, logout | Customer user, Orders (#1001/#1002/#1004) |
| 11 - Inventory Enforcement | `tests/Browser/Storefront/InventoryTest.php` | 4 | Inventory policy enforcement: deny vs. continue | Products #17 (deny), #18 (continue), #1 |
| 12 - Tenant Isolation | `tests/Browser/Storefront/TenantIsolationTest.php` | 5 | Multi-store data isolation verification | Store 1, Admin user, Customer user |
| 13 - Responsive / Mobile | `tests/Browser/Storefront/ResponsiveTest.php` | 8 | Mobile and tablet viewport rendering and interaction | Products, Admin user |
| 14 - Accessibility | `tests/Browser/Storefront/AccessibilityTest.php` | 11 | Heading hierarchy, ARIA labels, form labels, keyboard nav | Products, Store 1 |
| 15 - Admin Collections | `tests/Browser/Admin/CollectionManagementTest.php` | 3 | Collection CRUD in admin | Admin user, Collections |
| 16 - Admin Customers | `tests/Browser/Admin/CustomerManagementTest.php` | 3 | Customer listing and detail views | Admin user, Customer user, Orders |
| 17 - Admin Pages | `tests/Browser/Admin/PageManagementTest.php` | 3 | CMS page management | Admin user, Pages |
| 18 - Admin Analytics | `tests/Browser/Admin/AnalyticsTest.php` | 3 | Analytics dashboard rendering | Admin user, Orders |
| **Total** | **18 files** | **143** | | |

---

## Test Infrastructure Requirements

### Framework

- **Test runner:** Pest v4 with browser testing capabilities (Playwright under the hood)
- **Test traits:** Each browser test file must use `TestCase` and `RefreshDatabase`
- **Configuration:** Register the `Browser` test directory in `tests/Pest.php` with appropriate base classes
- **Base URL:** Configured via `APP_URL` in `.env.testing`, set to `http://acme-fashion.test`

### Pest v4 Browser Testing API Reference

The coding agent should use these Pest v4 browser methods:

| Method | Purpose |
|--------|---------|
| `visit(url)` | Open a real browser and navigate to the given URL |
| `visit([url1, url2, ...])` | Visit multiple pages (for batch smoke testing) |
| `fill(field, value)` | Type a value into a named form field |
| `click(text)` | Click a visible element matching the given text |
| `assertSee(text)` | Assert the page contains the given visible text |
| `assertNoJavascriptErrors()` | Assert no uncaught JS exceptions occurred |
| `assertNoConsoleLogs()` | Assert no console warnings or errors were logged |

Additionally, standard Laravel test features are available within browser tests: factories, `Event::fake()`, `Notification::fake()`, `$this->actingAs()`, and model queries.

### Environment Configuration

The `.env.testing` file must contain the following keys:

| Key | Value | Purpose |
|-----|-------|---------|
| `APP_ENV` | `testing` | Laravel environment |
| `APP_URL` | `http://acme-fashion.test` | Browser base URL (served by Herd) |
| `DB_CONNECTION` | `sqlite` | Database driver |
| `DB_DATABASE` | Absolute path to test SQLite file | Isolated test database |
| `PAYMENT_PROVIDER` | `mock` | Mock PSP - no external API keys needed |
| `MAIL_MAILER` | `array` | Trap emails in memory |
| `QUEUE_CONNECTION` | `sync` | Process jobs synchronously |

### Prerequisites Before Running

1. Database seeded with `php artisan migrate:fresh --seed`
2. Laravel Herd serving `acme-fashion.test`
3. Playwright browsers installed via `npx playwright install`
4. Frontend assets built via `npm run build`

---


Output:
Chunk ID: 013ea2
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1408
Output:
# 08 - Playwright E2E Test Plan

> Complete end-to-end browser test specification for the e-commerce platform using Pest v4 browser testing (Playwright-backed). All tests verify the entire application works through a real browser. This document defines requirements and specifications for the coding agent to implement.

---

## Test Suite Summary

| Suite | File | Tests | Purpose | Dependencies |
|-------|------|:-----:|---------|--------------|
| 1 - Smoke Tests | `tests/Browser/SmokeTest.php` | 10 | Hit every major page, assert HTTP 200 and no JS errors | Store 1, Products, Collections, Pages |
| 2 - Admin Authentication | `tests/Browser/Admin/AuthenticationTest.php` | 10 | Admin login, logout, invalid credentials, session access control | Admin user |
| 3 - Admin Product Management | `tests/Browser/Admin/ProductManagementTest.php` | 7 | Product CRUD: listing, creating, editing, archiving, filtering | Admin user, Products (incl. #15 draft) |
| 4 - Admin Order Management | `tests/Browser/Admin/OrderManagementTest.php` | 11 | Order listing, filtering, detail view, fulfillment, refund, confirm payment, fulfillment guard, mark shipped/delivered | Admin user, Orders (#1001, #1005), Customer |
| 5 - Admin Discount Management | `tests/Browser/Admin/DiscountManagementTest.php` | 6 | Discount code listing, creation, editing | Admin user, Discounts |
| 6 - Admin Settings | `tests/Browser/Admin/SettingsTest.php` | 7 | Store settings, shipping zones, tax config, domains | Admin user, Shipping zones, Store 1 |
| 7 - Storefront Browsing | `tests/Browser/Storefront/BrowsingTest.php` | 15 | Home page, collections, product detail, variants, search, pages | Products, Collections, Pages, Products #15/#17/#18 |
| 8 - Cart Flow | `tests/Browser/Storefront/CartTest.php` | 12 | Cart lifecycle: add, update qty, remove, discounts, totals | Products, Discounts (all codes) |
| 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 |
| 10 - Customer Account | `tests/Browser/Storefront/CustomerAccountTest.php` | 12 | Registration, login, order history, addresses, logout | Customer user, Orders (#1001/#1002/#1004) |
| 11 - Inventory Enforcement | `tests/Browser/Storefront/InventoryTest.php` | 4 | Inventory policy enforcement: deny vs. continue | Products #17 (deny), #18 (continue), #1 |
| 12 - Tenant Isolation | `tests/Browser/Storefront/TenantIsolationTest.php` | 5 | Multi-store data isolation verification | Store 1, Admin user, Customer user |
| 13 - Responsive / Mobile | `tests/Browser/Storefront/ResponsiveTest.php` | 8 | Mobile and tablet viewport rendering and interaction | Products, Admin user |
| 14 - Accessibility | `tests/Browser/Storefront/AccessibilityTest.php` | 11 | Heading hierarchy, ARIA labels, form labels, keyboard nav | Products, Store 1 |
| 15 - Admin Collections | `tests/Browser/Admin/CollectionManagementTest.php` | 3 | Collection CRUD in admin | Admin user, Collections |
| 16 - Admin Customers | `tests/Browser/Admin/CustomerManagementTest.php` | 3 | Customer listing and detail views | Admin user, Customer user, Orders |
| 17 - Admin Pages | `tests/Browser/Admin/PageManagementTest.php` | 3 | CMS page management | Admin user, Pages |
| 18 - Admin Analytics | `tests/Browser/Admin/AnalyticsTest.php` | 3 | Analytics dashboard rendering | Admin user, Orders |
| **Total** | **18 files** | **143** | | |

---

## Test Infrastructure Requirements

### Framework

- **Test runner:** Pest v4 with browser testing capabilities (Playwright under the hood)
- **Test traits:** Each browser test file must use `TestCase` and `RefreshDatabase`
- **Configuration:** Register the `Browser` test directory in `tests/Pest.php` with appropriate base classes
- **Base URL:** Configured via `APP_URL` in `.env.testing`, set to `http://acme-fashion.test`

### Pest v4 Browser Testing API Reference

The coding agent should use these Pest v4 browser methods:

| Method | Purpose |
|--------|---------|
| `visit(url)` | Open a real browser and navigate to the given URL |
| `visit([url1, url2, ...])` | Visit multiple pages (for batch smoke testing) |
| `fill(field, value)` | Type a value into a named form field |
| `click(text)` | Click a visible element matching the given text |
| `assertSee(text)` | Assert the page contains the given visible text |
| `assertNoJavascriptErrors()` | Assert no uncaught JS exceptions occurred |
| `assertNoConsoleLogs()` | Assert no console warnings or errors were logged |

Additionally, standard Laravel test features are available within browser tests: factories, `Event::fake()`, `Notification::fake()`, `$this->actingAs()`, and model queries.

### Environment Configuration

The `.env.testing` file must contain the following keys:

| Key | Value | Purpose |
|-----|-------|---------|
| `APP_ENV` | `testing` | Laravel environment |
| `APP_URL` | `http://acme-fashion.test` | Browser base URL (served by Herd) |
| `DB_CONNECTION` | `sqlite` | Database driver |
| `DB_DATABASE` | Absolute path to test SQLite file | Isolated test database |
| `PAYMENT_PROVIDER` | `mock` | Mock PSP - no external API keys needed |
| `MAIL_MAILER` | `array` | Trap emails in memory |
| `QUEUE_CONNECTION` | `sync` | Process jobs synchronously |

### Prerequisites Before Running

1. Database seeded with `php artisan migrate:fresh --seed`
2. Laravel Herd serving `acme-fashion.test`
3. Playwright browsers installed via `npx playwright install`
4. Frontend assets built via `npm run build`

---


tests/Browser/Admin:
AuthenticationTest.php
ProductManagementTest.php

tests/Browser/Storefront:
AccessibilityTest.php
BrowsingTest.php
CartTest.php
CheckoutTest.php
CustomerAccountTest.php
InventoryTest.php
ResponsiveTest.php
TenantIsolationTest.php


sed -n '675,980p' specs/08-PLAYWRIGHT-E2E-PLAN.md

## Suite 4: Admin Order Management

**File:** `tests/Browser/Admin/OrderManagementTest.php`

**Purpose:** Verify order listing, status filtering, order detail with timeline, fulfillment creation, and refund processing.

### Order State Transitions (via admin actions)

```mermaid
stateDiagram-v2
    state financial {
        pending_f --> paid: payment captured
        paid --> partially_refunded: partial refund
        paid --> refunded: full refund
    }
    state fulfillment {
        unfulfilled --> partial: partial fulfillment
        unfulfilled --> fulfilled: full fulfillment
        partial --> fulfilled: remaining items fulfilled
    }
```

---

### Test 4.1: Shows the order list with seeded orders

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

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Orders" in the sidebar

**Expected Results:**
- Page displays "#1001"
- No JavaScript errors

---

### Test 4.2: Can filter orders by status

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

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Orders" in the sidebar
4. Click the "Paid" status filter/tab
5. Verify "#1001" is visible
6. Click the "Fulfilled" status filter/tab
7. Click the "All" tab to reset the filter

**Expected Results:**
- After step 4: "#1001" is visible, no JavaScript errors
- After step 6: filtered results shown, no JavaScript errors
- After step 7: full list shown, no JavaScript errors

---

### Test 4.3: Shows order detail with line items and totals

**Preconditions:** Logged in as admin, Order #1001 exists

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Orders" in the sidebar
4. Click "#1001" to open the order detail

**Expected Results:**
- Page displays "#1001"
- Page displays "Paid" (financial status)
- Page displays "Unfulfilled" (fulfillment status)
- Page displays "Classic Cotton T-Shirt" (line item)
- Page displays "Subtotal", "Shipping", "Tax", and "Total" sections
- No JavaScript errors

---

### Test 4.4: Shows order timeline events

**Preconditions:** Logged in as admin, Order #1001 exists

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Orders" in the sidebar
4. Click "#1001" to open the order detail

**Expected Results:**
- Page displays "Timeline" section
- Timeline shows at least the order creation event
- No JavaScript errors

---

### Test 4.5: Can create a fulfillment

**Preconditions:** Logged in as admin, Order #1001 is unfulfilled

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Orders" in the sidebar
4. Click "#1001" to open the order detail
5. Click "Create fulfillment"
6. Enter tracking company: `DHL`
7. Enter tracking number: `DHL123456789`
8. Click "Fulfill items"

**Expected Results:**
- Page displays "Fulfillment created"
- Page displays "DHL" and "DHL123456789" in the fulfillment section
- No JavaScript errors

---

### Test 4.6: Can process a refund

**Preconditions:** Logged in as admin, Order #1001 is paid

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Orders" in the sidebar
4. Click "#1001" to open the order detail
5. Click "Refund"
6. Enter amount: `10.00`
7. Enter reason: `Customer requested partial refund`
8. Click "Process refund"

**Expected Results:**
- Page displays "Refund processed"
- Page displays "Partially refunded" (updated financial status)
- No JavaScript errors

---

### Test 4.7: Shows customer information in order detail

**Preconditions:** Logged in as admin, Order #1001 exists with customer

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Orders" in the sidebar
4. Click "#1001" to open the order detail

**Expected Results:**
- Page displays "customer@acme.test" in the customer info section
- No JavaScript errors

---

### Test 4.8: Can confirm bank transfer payment

**Preconditions:** Logged in as admin, Order #1005 exists with payment_method=bank_transfer and financial_status=pending

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Orders" in the sidebar
4. Click "#1005" to open the order detail
5. Verify "Pending" financial status badge is displayed
6. Verify "Confirm payment" button is visible
7. Click "Confirm payment"

**Expected Results:**
- Page displays "Payment confirmed"
- Financial status badge changes to "Paid"
- "Confirm payment" button is no longer visible
- No JavaScript errors

---

### Test 4.9: Shows fulfillment guard for unpaid order

**Preconditions:** Logged in as admin, Order #1005 exists with financial_status=pending (before payment confirmation)

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Orders" in the sidebar
4. Click "#1005" to open the order detail

**Expected Results:**
- Page displays a warning message about payment required before fulfillment
- "Create fulfillment" button is disabled or hidden
- No JavaScript errors

---

### Test 4.10: Can mark fulfillment as shipped

**Preconditions:** Logged in as admin, Order #1001 is paid and has been fulfilled (fulfillment created in test 4.5)

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Orders" in the sidebar
4. Click "#1001" to open the order detail
5. Create a fulfillment first (if not already done via test 4.5)
6. Click "Mark as shipped" on the fulfillment card

**Expected Results:**
- Fulfillment status badge changes to "Shipped"
- No JavaScript errors

---

### Test 4.11: Can mark fulfillment as delivered

**Preconditions:** Logged in as admin, Order #1001 has a shipped fulfillment

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Orders" in the sidebar
4. Click "#1001" to open the order detail
5. Click "Mark as delivered" on the shipped fulfillment card

**Expected Results:**
- Fulfillment status badge changes to "Delivered"
- Order fulfillment status updates to "Fulfilled"
- No JavaScript errors

**Total: 11 tests**

---

## Suite 5: Admin Discount Management

**File:** `tests/Browser/Admin/DiscountManagementTest.php`

**Purpose:** Verify discount code listing, creation (all three value types), editing, and status display.

### Discount Types

| Value Type | Description | Fields Required |
|------------|-------------|-----------------|
| `percent` | Percentage off the order subtotal | code, value (whole number, e.g., 25 for 25%), starts_at, optional ends_at |
| `fixed` | Fixed currency amount off | code, value (e.g., 10.00), starts_at, optional ends_at |
| `free_shipping` | Waive shipping costs entirely | code, starts_at, optional ends_at |

---

### Test 5.1: Shows seeded discount codes

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

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Discounts" in the sidebar

**Expected Results:**
- Page displays "WELCOME10"
- Page displays "FLAT5"
- Page displays "FREESHIP"
- No JavaScript errors

---

### Test 5.2: Can create a new percentage discount code

**Preconditions:** Logged in as admin

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Discounts" in the sidebar
4. Click "Create discount"
5. Enter code: `E2ETEST25`
6. Click "Percentage" to select the value type
7. Enter value: `25`
8. Enter starts_at: `2026-01-01`
9. Enter ends_at: `2026-12-31`
10. Click "Save"
11. Click "Discounts" to return to the list

**Expected Results:**
- After step 10: page displays "Discount saved", no JavaScript errors
- After step 11: page displays "E2ETEST25" in the list

---

### Test 5.3: Can create a fixed amount discount code

**Preconditions:** Logged in as admin

**Steps:**
1. Navigate to `/admin/login`
2. Log in as admin
3. Click "Discounts" then "Create discount"
4. Enter code: `E2EFLAT10`
5. Click "Fixed amount" to select the value type
6. Enter value: `10.00`
7. Enter starts_at: `2026-01-01`
8. Click "Save"

**Expected Results:**
- Page displays "Discount saved"
- No JavaScript errors

---