content:
     1→# 08 - Playwright E2E Test Plan
     2→
     3→> 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.
     4→
     5→---
     6→
     7→## Test Suite Summary
     8→
     9→| Suite | File | Tests | Purpose | Dependencies |
    10→|-------|------|:-----:|---------|--------------|
    11→| 1 - Smoke Tests | `tests/Browser/SmokeTest.php` | 10 | Hit every major page, assert HTTP 200 and no JS errors | Store 1, Products, Collections, Pages |
    12→| 2 - Admin Authentication | `tests/Browser/Admin/AuthenticationTest.php` | 10 | Admin login, logout, invalid credentials, session access control | Admin user |
    13→| 3 - Admin Product Management | `tests/Browser/Admin/ProductManagementTest.php` | 7 | Product CRUD: listing, creating, editing, archiving, filtering | Admin user, Products (incl. #15 draft) |
    14→| 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 |
    15→| 5 - Admin Discount Management | `tests/Browser/Admin/DiscountManagementTest.php` | 6 | Discount code listing, creation, editing | Admin user, Discounts |
    16→| 6 - Admin Settings | `tests/Browser/Admin/SettingsTest.php` | 7 | Store settings, shipping zones, tax config, domains | Admin user, Shipping zones, Store 1 |
    17→| 7 - Storefront Browsing | `tests/Browser/Storefront/BrowsingTest.php` | 15 | Home page, collections, product detail, variants, search, pages | Products, Collections, Pages, Products #15/#17/#18 |
    18→| 8 - Cart Flow | `tests/Browser/Storefront/CartTest.php` | 12 | Cart lifecycle: add, update qty, remove, discounts, totals | Products, Discounts (all codes) |
    19→| 9 - Checkout Flow | `tests/Browser/Storefront/CheckoutTest.php` | 13 | Full checkout: contact, address, shipping, payment method selection, payment, confirmation, magic card decline, bank transfer flow | Products, Shipping zones, Discounts |
    20→| 10 - Customer Account | `tests/Browser/Storefront/CustomerAccountTest.php` | 12 | Registration, login, order history, addresses, logout | Customer user, Orders (#1001/#1002/#1004) |
    21→| 11 - Inventory Enforcement | `tests/Browser/Storefront/InventoryTest.php` | 4 | Inventory policy enforcement: deny vs. continue | Products #17 (deny), #18 (continue), #1 |
    22→| 12 - Tenant Isolation | `tests/Browser/Storefront/TenantIsolationTest.php` | 5 | Multi-store data isolation verification | Store 1, Admin user, Customer user |
    23→| 13 - Responsive / Mobile | `tests/Browser/Storefront/ResponsiveTest.php` | 8 | Mobile and tablet viewport rendering and interaction | Products, Admin user |
    24→| 14 - Accessibility | `tests/Browser/Storefront/AccessibilityTest.php` | 11 | Heading hierarchy, ARIA labels, form labels, keyboard nav | Products, Store 1 |
    25→| 15 - Admin Collections | `tests/Browser/Admin/CollectionManagementTest.php` | 3 | Collection CRUD in admin | Admin user, Collections |
    26→| 16 - Admin Customers | `tests/Browser/Admin/CustomerManagementTest.php` | 3 | Customer listing and detail views | Admin user, Customer user, Orders |
    27→| 17 - Admin Pages | `tests/Browser/Admin/PageManagementTest.php` | 3 | CMS page management | Admin user, Pages |
    28→| 18 - Admin Analytics | `tests/Browser/Admin/AnalyticsTest.php` | 3 | Analytics dashboard rendering | Admin user, Orders |
    29→| **Total** | **18 files** | **143** | | |
    30→
    31→---
    32→
    33→## Test Infrastructure Requirements
    34→
    35→### Framework
    36→
    37→- **Test runner:** Pest v4 with browser testing capabilities (Playwright under the hood)
    38→- **Test traits:** Each browser test file must use `TestCase` and `RefreshDatabase`
    39→- **Configuration:** Register the `Browser` test directory in `tests/Pest.php` with appropriate base classes
    40→- **Base URL:** Configured via `APP_URL` in `.env.testing`, set to `http://acme-fashion.test`
    41→
    42→### Pest v4 Browser Testing API Reference
    43→
    44→The coding agent should use these Pest v4 browser methods:
    45→
    46→| Method | Purpose |
    47→|--------|---------|
    48→| `visit(url)` | Open a real browser and navigate to the given URL |
    49→| `visit([url1, url2, ...])` | Visit multiple pages (for batch smoke testing) |
    50→| `fill(field, value)` | Type a value into a named form field |
    51→| `click(text)` | Click a visible element matching the given text |
    52→| `assertSee(text)` | Assert the page contains the given visible text |
    53→| `assertNoJavascriptErrors()` | Assert no uncaught JS exceptions occurred |
    54→| `assertNoConsoleLogs()` | Assert no console warnings or errors were logged |
    55→
    56→Additionally, standard Laravel test features are available within browser tests: factories, `Event::fake()`, `Notification::fake()`, `$this->actingAs()`, and model queries.
    57→
    58→### Environment Configuration
    59→
    60→The `.env.testing` file must contain the following keys:
    61→
    62→| Key | Value | Purpose |
    63→|-----|-------|---------|
    64→| `APP_ENV` | `testing` | Laravel environment |
    65→| `APP_URL` | `http://acme-fashion.test` | Browser base URL (served by Herd) |
    66→| `DB_CONNECTION` | `sqlite` | Database driver |
    67→| `DB_DATABASE` | Absolute path to test SQLite file | Isolated test database |
    68→| `PAYMENT_PROVIDER` | `mock` | Mock PSP - no external API keys needed |
    69→| `MAIL_MAILER` | `array` | Trap emails in memory |
    70→| `QUEUE_CONNECTION` | `sync` | Process jobs synchronously |
    71→
    72→### Prerequisites Before Running
    73→
    74→1. Database seeded with `php artisan migrate:fresh --seed`
    75→2. Laravel Herd serving `acme-fashion.test`
    76→3. Playwright browsers installed via `npx playwright install`
    77→4. Frontend assets built via `npm run build`
    78→
    79→---
    80→
    81→## Seeded Data Dependency Table
    82→
    83→All tests depend on the database seeder providing this baseline data. Each test gets a fresh database via `RefreshDatabase`, then the seeder repopulates it.
    84→
    85→Each browser test file must set `protected bool $seed = true;` on the test class (or call `$this->seed(DatabaseSeeder::class)` in `beforeEach`) to ensure the database is populated after RefreshDatabase clears it. Laravel's `RefreshDatabase` trait does NOT automatically run seeders unless explicitly configured.
    86→
    87→| Entity | Key Details | Used By Suites |
    88→|--------|-------------|----------------|
    89→| **Admin user** | Email: `admin@acme.test`, password: `password`, role: `owner` on Store 1 | 2, 3, 4, 5, 6, 12, 13, 15, 16, 17, 18 |
    90→| **Customer user** | Email: `customer@acme.test`, password: `password`, name: "John Doe", belongs to Store 1 | 10, 12 |
    91→| **Store 1** | Name: "Acme Fashion", handle: `acme-fashion`, domain: `acme-fashion.test`, currency: EUR | 1, 6, 7, 12, 14 |
    92→| **Product #1** | Title: "Classic Cotton T-Shirt", handle: `classic-cotton-t-shirt`, price: 24.99 EUR, status: active, options: Size (S/M/L/XL), Color (Black/White/Navy), inventory: in stock | 1, 3, 7, 8, 9, 11, 13, 14 |
    93→| **Product #2** | Title: "Premium Slim Fit Jeans", handle: `premium-slim-fit-jeans`, status: active, has compare-at pricing (sale), options: Size, Color | 7, 8 |
    94→| **Product #15** | Status: `draft` - must NOT appear on storefront | 3, 7 |
    95→| **Product #17** | Status: active, inventory: 0 on hand, policy: `deny` - displays "Sold out", add-to-cart disabled | 7, 11 |
    96→| **Product #18** | Status: active, inventory: 0 on hand, policy: `continue` - displays "Available on backorder", add-to-cart enabled | 7, 11 |
    97→| **Products (20 total)** | Seeded across collections, with variants, options, images, inventory | 1, 3, 7, 8, 9, 11, 13 |
    98→| **Collections** | "T-Shirts" (handle: `t-shirts`), "New Arrivals" (handle: `new-arrivals`), plus others | 1, 7, 13, 15 |
    99→| **Orders** | #1001 (status: paid, fulfillment: unfulfilled, customer: customer@acme.test), #1002, #1004 also belonging to the same customer | 4, 10, 16, 18 |
   100→| **Discounts** | WELCOME10 (10% off, min 20 EUR purchase, active), FLAT5 (5 EUR fixed, active), FREESHIP (free shipping, active), EXPIRED20 (expired end date), MAXED (usage_count >= usage_limit) | 5, 8, 9 |

content:
# 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`

---

## Seeded Data Dependency Table

All tests depend on the database seeder providing this baseline data. Each test gets a fresh database via `RefreshDatabase`, then the seeder repopulates it.

Each browser test file must set `protected bool $seed = true;` on the test class (or call `$this->seed(DatabaseSeeder::class)` in `beforeEach`) to ensure the database is populated after RefreshDatabase clears it. Laravel's `RefreshDatabase` trait does NOT automatically run seeders unless explicitly configured.

| Entity | Key Details | Used By Suites |
|--------|-------------|----------------|
| **Admin user** | Email: `admin@acme.test`, password: `password`, role: `owner` on Store 1 | 2, 3, 4, 5, 6, 12, 13, 15, 16, 17, 18 |
| **Customer user** | Email: `customer@acme.test`, password: `password`, name: "John Doe", belongs to Store 1 | 10, 12 |
| **Store 1** | Name: "Acme Fashion", handle: `acme-fashion`, domain: `acme-fashion.test`, currency: EUR | 1, 6, 7, 12, 14 |
| **Product #1** | Title: "Classic Cotton T-Shirt", handle: `classic-cotton-t-shirt`, price: 24.99 EUR, status: active, options: Size (S/M/L/XL), Color (Black/White/Navy), inventory: in stock | 1, 3, 7, 8, 9, 11, 13, 14 |
| **Product #2** | Title: "Premium Slim Fit Jeans", handle: `premium-slim-fit-jeans`, status: active, has compare-at pricing (sale), options: Size, Color | 7, 8 |
| **Product #15** | Status: `draft` - must NOT appear on storefront | 3, 7 |
| **Product #17** | Status: active, inventory: 0 on hand, policy: `deny` - displays "Sold out", add-to-cart disabled | 7, 11 |
| **Product #18** | Status: active, inventory: 0 on hand, policy: `continue` - displays "Available on backorder", add-to-cart enabled | 7, 11 |
| **Products (20 total)** | Seeded across collections, with variants, options, images, inventory | 1, 3, 7, 8, 9, 11, 13 |
| **Collections** | "T-Shirts" (handle: `t-shirts`), "New Arrivals" (handle: `new-arrivals`), plus others | 1, 7, 13, 15 |
| **Orders** | #1001 (status: paid, fulfillment: unfulfilled, customer: customer@acme.test), #1002, #1004 also belonging to the same customer | 4, 10, 16, 18 |
| **Discounts** | WELCOME10 (10% off, min 20 EUR purchase, active), FLAT5 (5 EUR fixed, active), FREESHIP (free shipping, active), EXPIRED20 (expired end date), MAXED (usage_count >= usage_limit) | 5, 8, 9 |


teamName: wild-sleeping-shell

userType: external

slug: iterative-seeking-pine

type: user

role: user

content:
f75857b Phase 12: All 317 tests passing, seeders verified, implementation complete
441f94e Phase 11: Comprehensive seeders, error pages, factory states, FTS test fix
4696005 Update progress: Phases 6-10 complete, 317 tests passing
638f33d Phase 8-10: Search (FTS5), analytics, apps and webhooks with 25 new tests
0405470 Phase 6+7: Customer accounts and admin panel with 42 new tests


type: message

role: assistant

type: thinking

thinking:
Good - 317 tests passing, all 12 phases complete. Now I need to continue with:

1. Playwright E2E browser tests (specs/08-PLAYWRIGHT-E2E-PLAN.md)
2. PHPStan max level compliance
3. Deptrac compliance
4. Full review meeting

Let me read the rest of the E2E plan spec and also check if Pest browser testing is set up. But first, let me think about the approach.

The E2E plan calls for 143 browser tests across 18 files. This is a massive undertaking. Let me check what's already available in the project for browser testing.

I should verify whether Pest v4's browser testing is already configured since it uses Playwright under the hood, check if the testing environment file exists, confirm Playwright browsers are installed, and see if there's already a tests/Browser directory. Given the user's priorities around simulating user behavior with Playwright and meeting the acceptance criteria, I need to figure out the most efficient path forward for implementing these E2E tests. I need to check the project setup to see if Pest v4 browser testing is actually configured and available, since the spec references those methods but I should verify they're set up before proceeding.

service_tier: standard

type: assistant


type: text

text: All 317 tests passing. Let me check the E2E test infrastructure and continue reading the spec.


Bash

command: ls /Users/fabianwesner/Herd/shop/tests/Browser/ 2>/dev/null; echo "---"; ls /Users/fabianwesner/Herd/shop/tests/Pest.php 2>/dev/null

description: Check if browser test directory and Pest config exist


content:
---
/Users/fabianwesner/Herd/shop/tests/Pest.php


type: progress

hookEvent: PostToolUse

command: callback


content:
   100→| **Discounts** | WELCOME10 (10% off, min 20 EUR purchase, active), FLAT5 (5 EUR fixed, active), FREESHIP (free shipping, active), EXPIRED20 (expired end date), MAXED (usage_count >= usage_limit) | 5, 8, 9 |
   101→| **Shipping zones** | "Domestic" zone (countries: DE) with rate "Standard Shipping" at 4.99 EUR flat; "International" zone with its own rates | 6, 9 |
   102→| **Tax settings** | Store 1: manual mode, prices_include_tax varies by test | 6 |
   103→| **Pages** | "About" (handle: `about`, status: published) | 1, 7, 17 |
   104→| **Navigation** | Main menu with links to collections, about page | 7 |
   105→
   106→---
   107→
   108→## Test Execution Matrix
   109→
   110→Tests should be executed in the order shown below. Later suites depend on the infrastructure and pages verified by earlier suites.
   111→
   112→```mermaid
   113→graph TD
   114→    S1[Suite 1: Smoke Tests] --> S2[Suite 2: Admin Auth]
   115→    S1 --> S7[Suite 7: Storefront Browsing]
   116→    S2 --> S3[Suite 3: Admin Products]
   117→    S2 --> S4[Suite 4: Admin Orders]
   118→    S2 --> S5[Suite 5: Admin Discounts]
   119→    S2 --> S6[Suite 6: Admin Settings]
   120→    S2 --> S15[Suite 15: Admin Collections]
   121→    S2 --> S16[Suite 16: Admin Customers]
   122→    S2 --> S17[Suite 17: Admin Pages]
   123→    S2 --> S18[Suite 18: Admin Analytics]
   124→    S7 --> S8[Suite 8: Cart Flow]
   125→    S8 --> S9[Suite 9: Checkout Flow]
   126→    S7 --> S10[Suite 10: Customer Account]
   127→    S7 --> S11[Suite 11: Inventory Enforcement]
   128→    S7 --> S12[Suite 12: Tenant Isolation]
   129→    S7 --> S13[Suite 13: Responsive / Mobile]
   130→    S7 --> S14[Suite 14: Accessibility]
   131→```
   132→
   133→| Execution Phase | Suites | Rationale |
   134→|-----------------|--------|-----------|
   135→| Phase 1 | Suite 1 (Smoke) | Verifies all routes load before deeper testing |
   136→| Phase 2 | Suites 2, 7 | Core auth and storefront browsing - gate to all further tests |
   137→| Phase 3 | Suites 3, 4, 5, 6, 10, 11, 15, 16, 17, 18 | Admin CRUD and storefront feature suites (can run in parallel) |
   138→| Phase 4 | Suite 8 (Cart) | Depends on storefront browsing working |
   139→| Phase 5 | Suite 9 (Checkout) | Depends on cart flow working. Tests payment method selection (credit card, PayPal, bank transfer), magic card declines, and bank transfer instructions. |
   140→| Phase 6 | Suites 12, 13, 14 | Cross-cutting concerns: isolation, responsive, accessibility |
   141→
   142→---
   143→
   144→## Test Directory Structure
   145→
   146→```
   147→tests/Browser/
   148→    SmokeTest.php
   149→    Admin/
   150→        AuthenticationTest.php
   151→        ProductManagementTest.php
   152→        OrderManagementTest.php
   153→        DiscountManagementTest.php
   154→        SettingsTest.php
   155→        CollectionManagementTest.php
   156→        CustomerManagementTest.php
   157→        PageManagementTest.php
   158→        AnalyticsTest.php
   159→    Storefront/
   160→        BrowsingTest.php
   161→        CartTest.php
   162→        CheckoutTest.php
   163→        CustomerAccountTest.php
   164→        InventoryTest.php
   165→        TenantIsolationTest.php
   166→        ResponsiveTest.php
   167→        AccessibilityTest.php
   168→```
   169→
   170→---
   171→
   172→## Suite 1: Smoke Tests
   173→
   174→**File:** `tests/Browser/SmokeTest.php`
   175→
   176→**Purpose:** Hit every major public and admin page, assert they load without JavaScript errors. This is the fastest way to catch broken routes, missing views, or uncaught JS exceptions.
   177→
   178→---
   179→
   180→### Test 1.1: Loads the storefront home page
   181→
   182→**Preconditions:** Seeded store
   183→
   184→**Steps:**
   185→1. Navigate to `/`
   186→
   187→**Expected Results:**
   188→- Page displays "Acme Fashion"
   189→- No JavaScript errors
   190→
   191→---
   192→
   193→### Test 1.2: Loads a collection page
   194→
   195→**Preconditions:** Seeded collections
   196→
   197→**Steps:**
   198→1. Navigate to `/collections/t-shirts`
   199→
   200→**Expected Results:**
   201→- Page displays "T-Shirts"
   202→- No JavaScript errors
   203→
   204→---
   205→
   206→### Test 1.3: Loads a product page
   207→
   208→**Preconditions:** Seeded products
   209→
   210→**Steps:**
   211→1. Navigate to `/products/classic-cotton-t-shirt`
   212→
   213→**Expected Results:**
   214→- Page displays "Classic Cotton T-Shirt"
   215→- Page displays "24.99"
   216→- No JavaScript errors
   217→
   218→---
   219→
   220→### Test 1.4: Loads the cart page
   221→
   222→**Preconditions:** None
   223→
   224→**Steps:**
   225→1. Navigate to `/cart`
   226→
   227→**Expected Results:**
   228→- Page displays "Your Cart"
   229→- No JavaScript errors
   230→
   231→---
   232→
   233→### Test 1.5: Loads the customer login page
   234→
   235→**Preconditions:** None
   236→
   237→**Steps:**
   238→1. Navigate to `/account/login`
   239→
   240→**Expected Results:**
   241→- Page displays "Log in"
   242→- No JavaScript errors
   243→
   244→---
   245→
   246→### Test 1.6: Loads the admin login page
   247→
   248→**Preconditions:** None
   249→
   250→**Steps:**
   251→1. Navigate to `/admin/login`
   252→
   253→**Expected Results:**
   254→- Page displays "Sign in"
   255→- No JavaScript errors
   256→
   257→---
   258→
   259→### Test 1.7: Loads the about page
   260→
   261→**Preconditions:** Seeded pages
   262→
   263→**Steps:**
   264→1. Navigate to `/pages/about`
   265→
   266→**Expected Results:**
   267→- Page displays "About"
   268→- No JavaScript errors
   269→
   270→---
   271→
   272→### Test 1.8: Loads the search page
   273→
   274→**Preconditions:** Seeded products
   275→
   276→**Steps:**
   277→1. Navigate to `/search?q=shirt`
   278→
   279→**Expected Results:**
   280→- Page displays "shirt"
   281→- No JavaScript errors
   282→
   283→---
   284→
   285→### Test 1.9: Loads all collections listing
   286→
   287→**Preconditions:** Seeded collections
   288→
   289→**Steps:**
   290→1. Navigate to `/collections`
   291→
   292→**Expected Results:**
   293→- Page displays "Collections"
   294→- No JavaScript errors
   295→
   296→---
   297→
   298→### Test 1.10: Has no errors on critical pages (batch)
   299→

content:
| **Discounts** | WELCOME10 (10% off, min 20 EUR purchase, active), FLAT5 (5 EUR fixed, active), FREESHIP (free shipping, active), EXPIRED20 (expired end date), MAXED (usage_count >= usage_limit) | 5, 8, 9 |
| **Shipping zones** | "Domestic" zone (countries: DE) with rate "Standard Shipping" at 4.99 EUR flat; "International" zone with its own rates | 6, 9 |
| **Tax settings** | Store 1: manual mode, prices_include_tax varies by test | 6 |
| **Pages** | "About" (handle: `about`, status: published) | 1, 7, 17 |
| **Navigation** | Main menu with links to collections, about page | 7 |

---

## Test Execution Matrix

Tests should be executed in the order shown below. Later suites depend on the infrastructure and pages verified by earlier suites.

```mermaid
graph TD
    S1[Suite 1: Smoke Tests] --> S2[Suite 2: Admin Auth]
    S1 --> S7[Suite 7: Storefront Browsing]
    S2 --> S3[Suite 3: Admin Products]
    S2 --> S4[Suite 4: Admin Orders]
    S2 --> S5[Suite 5: Admin Discounts]
    S2 --> S6[Suite 6: Admin Settings]
    S2 --> S15[Suite 15: Admin Collections]
    S2 --> S16[Suite 16: Admin Customers]
    S2 --> S17[Suite 17: Admin Pages]
    S2 --> S18[Suite 18: Admin Analytics]
    S7 --> S8[Suite 8: Cart Flow]
    S8 --> S9[Suite 9: Checkout Flow]
    S7 --> S10[Suite 10: Customer Account]
    S7 --> S11[Suite 11: Inventory Enforcement]
    S7 --> S12[Suite 12: Tenant Isolation]
    S7 --> S13[Suite 13: Responsive / Mobile]
    S7 --> S14[Suite 14: Accessibility]
```

| Execution Phase | Suites | Rationale |
|-----------------|--------|-----------|
| Phase 1 | Suite 1 (Smoke) | Verifies all routes load before deeper testing |
| Phase 2 | Suites 2, 7 | Core auth and storefront browsing - gate to all further tests |
| Phase 3 | Suites 3, 4, 5, 6, 10, 11, 15, 16, 17, 18 | Admin CRUD and storefront feature suites (can run in parallel) |
| Phase 4 | Suite 8 (Cart) | Depends on storefront browsing working |
| Phase 5 | Suite 9 (Checkout) | Depends on cart flow working. Tests payment method selection (credit card, PayPal, bank transfer), magic card declines, and bank transfer instructions. |
| Phase 6 | Suites 12, 13, 14 | Cross-cutting concerns: isolation, responsive, accessibility |

---

## Test Directory Structure

```
tests/Browser/
    SmokeTest.php
    Admin/
        AuthenticationTest.php
        ProductManagementTest.php
        OrderManagementTest.php
        DiscountManagementTest.php
        SettingsTest.php
        CollectionManagementTest.php
        CustomerManagementTest.php
        PageManagementTest.php
        AnalyticsTest.php
    Storefront/
        BrowsingTest.php
        CartTest.php
        CheckoutTest.php
        CustomerAccountTest.php
        InventoryTest.php
        TenantIsolationTest.php
        ResponsiveTest.php
        AccessibilityTest.php
```

---

## Suite 1: Smoke Tests

**File:** `tests/Browser/SmokeTest.php`

**Purpose:** Hit every major public and admin page, assert they load without JavaScript errors. This is the fastest way to catch broken routes, missing views, or uncaught JS exceptions.

---

### Test 1.1: Loads the storefront home page

**Preconditions:** Seeded store

**Steps:**
1. Navigate to `/`

**Expected Results:**
- Page displays "Acme Fashion"
- No JavaScript errors

---

### Test 1.2: Loads a collection page

**Preconditions:** Seeded collections

**Steps:**
1. Navigate to `/collections/t-shirts`

**Expected Results:**
- Page displays "T-Shirts"
- No JavaScript errors

---

### Test 1.3: Loads a product page

**Preconditions:** Seeded products

**Steps:**
1. Navigate to `/products/classic-cotton-t-shirt`

**Expected Results:**
- Page displays "Classic Cotton T-Shirt"
- Page displays "24.99"
- No JavaScript errors

---

### Test 1.4: Loads the cart page

**Preconditions:** None

**Steps:**
1. Navigate to `/cart`

**Expected Results:**
- Page displays "Your Cart"
- No JavaScript errors

---

### Test 1.5: Loads the customer login page

**Preconditions:** None

**Steps:**
1. Navigate to `/account/login`

**Expected Results:**
- Page displays "Log in"
- No JavaScript errors

---

### Test 1.6: Loads the admin login page

**Preconditions:** None

**Steps:**
1. Navigate to `/admin/login`

**Expected Results:**
- Page displays "Sign in"
- No JavaScript errors

---

### Test 1.7: Loads the about page

**Preconditions:** Seeded pages

**Steps:**
1. Navigate to `/pages/about`

**Expected Results:**
- Page displays "About"
- No JavaScript errors

---

### Test 1.8: Loads the search page

**Preconditions:** Seeded products

**Steps:**
1. Navigate to `/search?q=shirt`

**Expected Results:**
- Page displays "shirt"
- No JavaScript errors

---

### Test 1.9: Loads all collections listing

**Preconditions:** Seeded collections

**Steps:**
1. Navigate to `/collections`

**Expected Results:**
- Page displays "Collections"
- No JavaScript errors

---

### Test 1.10: Has no errors on critical pages (batch)


command: composer show pestphp/pest 2>&1 | head -5 && echo "---" && composer show pestphp/pest-plugin-browser 2>&1 | head -5

description: Check Pest version and browser plugin


content:
name     : pestphp/pest
descrip. : The elegant PHP Testing Framework.
keywords : framework, pest, php, test, testing, unit
versions : * v4.3.2
released : 2026-01-28, 2 weeks ago
---

In ShowCommand.php line 325:

  Package "pestphp/pest-plugin-browser" not found, try using --available (-a)
   to show all available packages.


thinking:
The `pestphp/pest-plugin-browser` is not installed. Pest v4 browser testing requires this plugin. Let me check if it's available and what version works with Pest v4.3.2.

Actually, looking at this more carefully, the Pest v4 browser testing is available via `pestphp/pest-plugin-browser`. But it's not installed yet.

Given that the user asked me to "Simulate user behavior using Playwright MCP and confirm all acceptance criteria", I think the most practical approach is:

1. Use the Playwright MCP tools that are already available to me to do the E2E testing/review directly through the browser

2. This avoids needing to install additional packages

But the spec file (08-PLAYWRIGHT-E2E-PLAN.md) specifically calls for Pest v4 browser test files. The user's original request mentions both "Playwright MCP" and the spec file. I have the Playwright MCP tools available right now, while writing 143 Pest browser tests would require installing the plugin first. Since the user explicitly asked me to use Playwright MCP to simulate user behavior and confirm acceptance criteria, I'll start by leveraging those tools directly to test the application.

My plan is to seed the database with fresh data, then walk through the app using Playwright MCP to verify all features work as expected, fix any bugs I find, handle the code quality checks with PHPStan and Deptrac, and finally conduct the review meeting.


text: Pest browser plugin is not installed. Let me first check the site availability and seed the database, then use the Playwright MCP tools to do the E2E walkthrough and verify acceptance criteria.


status: started

serverName: herd


status: completed


site: shop

env:
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:nW7zTogbucXWMRU2+3CxeYXYni4FTNdm8nIUeG7tOzQ=
APP_DEBUG=true
APP_URL=http://shop.test

APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US

APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database

# PHP_CLI_SERVER_WORKERS=4

BCRYPT_ROUNDS=12

LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug

DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=

SESSION_DRIVER=file
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null

BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync

CACHE_STORE=file
# CACHE_PREFIX=

MEMCACHED_HOST=127.0.0.1

REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false

VITE_APP_NAME="${APP_NAME}"
FORGE_TOKEN="eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIxIiwianRpIjoiZTJlODczZTViNzQxOWIwMjk0MDA2MjI4Y2RlNzliMmM5ZDNhNjEzYWJkM2Q3NmMxNWYzZTI3OGE1NTRiZDgwNjk3ZDQ3ZWU1YWM1MmViNmMiLCJpYXQiOjE3NjkxODU2NTUuMDc1NjE0LCJuYmYiOjE3NjkxODU2NTUuMDc1NjE3LCJleHAiOjIwODQ3MTg0NTUuMDcyNzExLCJzdWIiOiIyNDE3NDgiLCJzY29wZXMiOlsic2VydmVyOnZpZXciLCJzZXJ2ZXI6Y3JlYXRlIiwic2VydmVyOmRlbGV0ZSIsInNlcnZlcjphcmNoaXZlIiwic2VydmVyOnRyYW5zZmVyIiwic2VydmVyOm1hbmFnZS1tZXRhIiwic2VydmVyOm1hbmFnZS1wYWNrYWdlcyIsInNlcnZlcjptYW5hZ2UtcGhwIiwic2VydmVyOm1hbmFnZS1sb2dzIiwic2VydmVyOm1hbmFnZS1uZXR3b3JrIiwic2VydmVyOm1hbmFnZS1uZ2lueC10ZW1wbGF0ZXMiLCJzZXJ2ZXI6bWFuYWdlLXNlcnZpY2VzIiwic2VydmVyOm1hbmFnZS1wYXNzd29yZCIsInNlcnZlcjpjcmVhdGUta2V5cyIsInNlcnZlcjpkZWxldGUta2V5cyIsInNlcnZlcjpjcmVhdGUtbW9uaXRvcnMiLCJzZXJ2ZXI6ZGVsZXRlLW1vbml0b3JzIiwic2VydmVyOmNyZWF0ZS1kYXRhYmFzZXMiLCJzZXJ2ZXI6ZGVsZXRlLWRhdGFiYXNlcyIsInNlcnZlcjpjcmVhdGUtYmFja3VwcyIsInNlcnZlcjpkZWxldGUtYmFja3VwcyIsInNlcnZlcjpjcmVhdGUtZGFlbW9ucyIsInNlcnZlcjpkZWxldGUtZGFlbW9ucyIsInNlcnZlcjpjcmVhdGUtc2NoZWR1bGVycyIsInNlcnZlcjpkZWxldGUtc2NoZWR1bGVycyIsInNlcnZlcjp3ZWItdGVybWluYWwiLCJzaXRlOmNyZWF0ZSIsInNpdGU6ZGVsZXRlIiwic2l0ZTptZXRhIiwic2l0ZTptYW5hZ2UtY29tbWFuZHMiLCJzaXRlOm1hbmFnZS1kZXBsb3lzIiwic2l0ZTptYW5hZ2UtbmdpbngiLCJzaXRlOm1hbmFnZS1wcm9qZWN0Iiwic2l0ZTptYW5hZ2UtZW52aXJvbm1lbnQiLCJzaXRlOm1hbmFnZS1ub3RpZmljYXRpb25zIiwic2l0ZTptYW5hZ2UtcXVldWVzIiwic2l0ZTptYW5hZ2UtcmVkaXJlY3RzIiwic2l0ZTptYW5hZ2Utc2VjdXJpdHkiLCJzaXRlOm1hbmFnZS1zc2wiLCJzaXRlOm1hbmFnZS1pbnRlZ3JhdGlvbnMiLCJzaXRlOm1hbmFnZS1oZWFydGJlYXRzIiwic3RvcmFnZTptYW5hZ2UiXX0.jnUjxC3lIQJLCTJ1AbgVDQ-JhExT7gQASOPMdtCWAW8HQbFEz4uvOERS1xGvwCFiUe0dJPt1O1G_Npi9SdjJaKiFJE-MrUoyKTkEOO41Ay6hftxx73p3QlyGfXZkx0IREaX8bgn6JwPFy5CLiOXhTzvBLj3JL0QVR2gUxPtGBgatO5KLQUyA8UCpA_mikRP2itUTshf0t2xF-fwKK6PK2iOIsG4TCaMpt3qPE_CJWataxQKOKFFa1qsek_LLwTwePfnEboys27wGht0G4-sMRTs_QzsGwwhrhpRm3f_16c7uH_mCNDhMLccJrl_N82yaJVPkgcpIag8hhx2sOtjNA6bl8ghyKA4-tdM1CwmFXeK-Lexd1pjyza9J8lG3cezK5wTlcNfrkTkhUNdE51h0IqCAVSnGxlfJIlzBJX9xOZl_NNdl4RItdoxUccfbbsZ7V-OjWoFEnZG2PTeupgnOGm10eEqd4nZkXD3TX_ecqX54pgmEakir3p_as7KVN1EygJlGGrv9FsZrdnyseah127Zz1EG1Mrh8LMVZv9SxQtvsE4Qw77N_rNmNznSgBVRgp7CBkglkolWijB8cGN2ktvVOGSeOJp77Oe1wfnH5E1VDochsDUW2R7ouFx6bJwS6_SV__wLOSjLYRmqHu9rscTKFPAo3A-ZsgJQGyfK7TIw"

DB_DATABASE=database/shop.sqlite


command: php artisan migrate:fresh --seed 2>&1 | tail -20

description: Fresh migrate and seed the database


content:
  Database\Seeders\CustomerSeeder .............................. 3,301 ms DONE

  Database\Seeders\OrderSeeder ....................................... RUNNING
  Database\Seeders\OrderSeeder .................................... 41 ms DONE

  Database\Seeders\ThemeSeeder ....................................... RUNNING
  Database\Seeders\ThemeSeeder ..................................... 2 ms DONE

  Database\Seeders\PageSeeder ........................................ RUNNING
  Database\Seeders\PageSeeder ...................................... 2 ms DONE

  Database\Seeders\NavigationSeeder .................................. RUNNING
  Database\Seeders\NavigationSeeder ................................ 4 ms DONE

  Database\Seeders\AnalyticsSeeder ................................... RUNNING
  Database\Seeders\AnalyticsSeeder ................................ 48 ms DONE

  Database\Seeders\SearchSettingsSeeder .............................. RUNNING
  Database\Seeders\SearchSettingsSeeder ............................ 1 ms DONE


thinking:
The site is available at `http://shop.test`. But the store domains are set up differently - the store domain seeder creates domains like `acme-fashion.test`, not `shop.test`. I need to check how the store resolution middleware works.

Let me check the store domain setup and figure out how to access the storefront. The store resolution middleware probably resolves based on the domain.


text: Database is freshly seeded. The site is at `http://shop.test` but store resolution works via domains. Let me check the store domains and middleware.


command: php artisan tinker --execute="echo App\Models\StoreDomain::withoutGlobalScopes()->get(['id','store_id','domain','is_primary'])->toJson(JSON_PRETTY_PRINT);" 2>&1

description: Check store domains


domain: domain


content: File does not exist.

toolUseResult: Error: File does not exist.


content: No files found


command: php artisan tinker --execute="echo App\Models\StoreDomain::withoutGlobalScopes()->get()->toJson(JSON_PRETTY_PRINT);" 2>&1

description: Get full store domains data


type: storefront

tls_mode: managed

type: admin


content: bootstrap/app.php:16:            'store.resolve' => \App\Http\Middleware\ResolveStore::class,

mode: content


content:
/Users/fabianwesner/Herd/shop/app/Http/Middleware/ResolveStore.php
/Users/fabianwesner/Herd/shop/app/Http/Middleware/CheckStoreRole.php
/Users/fabianwesner/Herd/shop/app/Http/Middleware/CustomerAuthenticate.php
/Users/fabianwesner/Herd/shop/app/Http/Middleware/CustomerAuth.php


content:
     1→<?php
     2→
     3→namespace App\Http\Middleware;
     4→
     5→use App\Models\Store;
     6→use App\Models\StoreDomain;
     7→use Closure;
     8→use Illuminate\Http\Request;
     9→use Illuminate\Support\Facades\Cache;
    10→use Symfony\Component\HttpFoundation\Response;
    11→
    12→class ResolveStore
    13→{
    14→    /**
    15→     * @param  Closure(Request): Response  $next
    16→     */
    17→    public function handle(Request $request, Closure $next, string $context = 'storefront'): Response
    18→    {
    19→        $store = $context === 'admin'
    20→            ? $this->resolveForAdmin($request)
    21→            : $this->resolveForStorefront($request);
    22→
    23→        if (! $store) {
    24→            abort(404, 'Store not found.');
    25→        }
    26→
    27→        if ($store->status->value === 'suspended') {
    28→            abort(503, 'Store is currently unavailable.');
    29→        }
    30→
    31→        app()->singleton('current_store', fn (): Store => $store);
    32→
    33→        return $next($request);
    34→    }
    35→
    36→    private function resolveForStorefront(Request $request): ?Store
    37→    {
    38→        $hostname = $request->getHost();
    39→
    40→        $cacheKey = 'store_domain:'.$hostname;
    41→
    42→        $storeId = Cache::remember($cacheKey, 300, function () use ($hostname): ?int {
    43→            $domain = StoreDomain::query()
    44→                ->where('hostname', $hostname)
    45→                ->first();
    46→
    47→            return $domain?->store_id;
    48→        });
    49→
    50→        if (! $storeId) {
    51→            return null;
    52→        }
    53→
    54→        return Store::query()->find($storeId);
    55→    }
    56→
    57→    private function resolveForAdmin(Request $request): ?Store
    58→    {
    59→        $storeId = $request->session()->get('current_store_id');
    60→
    61→        if (! $storeId) {
    62→            return null;
    63→        }
    64→
    65→        $store = Store::query()->find($storeId);
    66→
    67→        if (! $store) {
    68→            return null;
    69→        }
    70→
    71→        $user = $request->user();
    72→
    73→        if (! $user) {
    74→            return null;
    75→        }
    76→
    77→        $hasAccess = $user->stores()
    78→            ->where('stores.id', $store->id)
    79→            ->exists();
    80→
    81→        if (! $hasAccess) {
    82→            return null;
    83→        }
    84→
    85→        return $store;
    86→    }
    87→}
    88→

content:
<?php

namespace App\Http\Middleware;

use App\Models\Store;
use App\Models\StoreDomain;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\HttpFoundation\Response;

class ResolveStore
{
    /**
     * @param  Closure(Request): Response  $next
     */
    public function handle(Request $request, Closure $next, string $context = 'storefront'): Response
    {
        $store = $context === 'admin'
            ? $this->resolveForAdmin($request)
            : $this->resolveForStorefront($request);

        if (! $store) {
            abort(404, 'Store not found.');
        }

        if ($store->status->value === 'suspended') {
            abort(503, 'Store is currently unavailable.');
        }

        app()->singleton('current_store', fn (): Store => $store);

        return $next($request);
    }

    private function resolveForStorefront(Request $request): ?Store
    {
        $hostname = $request->getHost();

        $cacheKey = 'store_domain:'.$hostname;

        $storeId = Cache::remember($cacheKey, 300, function () use ($hostname): ?int {
            $domain = StoreDomain::query()
                ->where('hostname', $hostname)
                ->first();

            return $domain?->store_id;
        });

        if (! $storeId) {
            return null;
        }

        return Store::query()->find($storeId);
    }

    private function resolveForAdmin(Request $request): ?Store
    {
        $storeId = $request->session()->get('current_store_id');

        if (! $storeId) {
            return null;
        }

        $store = Store::query()->find($storeId);

        if (! $store) {
            return null;
        }

        $user = $request->user();

        if (! $user) {
            return null;
        }

        $hasAccess = $user->stores()
            ->where('stores.id', $store->id)
            ->exists();

        if (! $hasAccess) {
            return null;
        }

        return $store;
    }
}


thinking:
The store resolution for the storefront works by hostname matching. The storefront domain is `acme-fashion.test`. But the Herd site is `shop.test`. Since the store domains in the database are `acme-fashion.test` and `admin.acme-fashion.test`, the site won't resolve at `shop.test`.

I have two options:
1. Add `shop.test` as a storefront domain for store 1 (so it resolves)
2. Navigate the Playwright browser to `http://acme-fashion.test` directly

Since Herd serves the app at `shop.test`, and the hostname `acme-fashion.test` would need to point to the same Herd site, I need to check if Herd has configured `acme-fashion.test` as a linked domain.

Herd probably only serves `shop.test` since that's the folder name. The simplest fix is to add `shop.test` as a store domain for store 1 in the database rather than modifying the seeders, so the browser can actually access the store when running the E2E tests.


text: The storefront resolves by hostname. The Herd site is `shop.test` but store domains are set to `acme-fashion.test`. Let me add `shop.test` as a domain so the site resolves properly, then start the E2E walkthrough.


command:
php artisan tinker --execute="
App\Models\StoreDomain::withoutGlobalScopes()->firstOrCreate(
    ['hostname' => 'shop.test'],
    ['store_id' => 1, 'type' => 'storefront', 'is_primary' => false, 'tls_mode' => 'managed']
);
echo 'Done: shop.test domain added for store 1';
" 2>&1

description: Add shop.test as a store domain


content: Done: shop.test domain added for store 1


text: Good. Now let me start the Playwright E2E walkthrough. I'll begin with the smoke tests - visiting all major pages.


serverName: playwright


text:
### Ran Playwright code
```js
await page.goto('http://shop.test');
```
### Page
- Page URL: http://shop.test/
- Page Title: Shop
- Console: 1 errors, 0 warnings
### Snapshot
```yaml
- generic [active] [ref=e1]:
  - banner [ref=e2]:
    - generic [ref=e3]:
      - link "Acme Fashion" [ref=e4] [cursor=pointer]:
        - /url: http://shop.test
      - navigation [ref=e5]:
        - link "Collections" [ref=e6] [cursor=pointer]:
          - /url: http://shop.test/collections
      - generic [ref=e7]:
        - link "Search" [ref=e8] [cursor=pointer]:
          - /url: http://shop.test/search
          - img [ref=e9]
        - link "Cart" [ref=e11] [cursor=pointer]:
          - /url: http://shop.test/cart
          - img [ref=e12]
        - link "Account" [ref=e14] [cursor=pointer]:
          - /url: http://shop.test/account/login
          - img [ref=e15]
  - main [ref=e17]:
    - generic [ref=e18]:
      - generic [ref=e19]:
        - heading "Acme Fashion" [level=1] [ref=e20]
        - paragraph [ref=e21]: Discover our latest collections and products.
        - link "Browse Collections" [ref=e23] [cursor=pointer]:
          - /url: http://shop.test/collections
      - generic [ref=e24]:
        - heading "Collections" [level=2] [ref=e25]
        - generic [ref=e26]:
          - link "New Arrivals Discover the latest additions to our store." [ref=e27] [cursor=pointer]:
            - /url: http://shop.test/collections/new-arrivals
            - heading "New Arrivals" [level=3] [ref=e28]
            - paragraph [ref=e29]: Discover the latest additions to our store.
          - link "T-Shirts Premium cotton tees for every occasion." [ref=e30] [cursor=pointer]:
            - /url: http://shop.test/collections/t-shirts
            - heading "T-Shirts" [level=3] [ref=e31]
            - paragraph [ref=e32]: Premium cotton tees for every occasion.
          - link "Pants & Jeans Find the perfect fit from our denim and trouser range." [ref=e33] [cursor=pointer]:
            - /url: http://shop.test/collections/pants-jeans
            - heading "Pants & Jeans" [level=3] [ref=e34]
            - paragraph [ref=e35]: Find the perfect fit from our denim and trouser range.
          - link "Sale Great deals on selected items." [ref=e36] [cursor=pointer]:
            - /url: http://shop.test/collections/sale
            - heading "Sale" [level=3] [ref=e37]
            - paragraph [ref=e38]: Great deals on selected items.
  - contentinfo [ref=e39]:
    - generic [ref=e40]:
      - generic [ref=e41]:
        - generic [ref=e42]:
          - heading "Shop" [level=3] [ref=e43]
          - list [ref=e44]:
            - listitem [ref=e45]:
              - link "Collections" [ref=e46] [cursor=pointer]:
                - /url: http://shop.test/collections
        - generic [ref=e47]:
          - heading "Account" [level=3] [ref=e48]
          - list [ref=e49]:
            - listitem [ref=e50]:
              - link "Sign In" [ref=e51] [cursor=pointer]:
                - /url: http://shop.test/account/login
            - listitem [ref=e52]:
              - link "Create Account" [ref=e53] [cursor=pointer]:
                - /url: http://shop.test/account/register
        - generic [ref=e54]:
          - heading "Help" [level=3] [ref=e55]
          - list [ref=e56]:
            - listitem [ref=e57]:
              - link "Search" [ref=e58] [cursor=pointer]:
                - /url: http://shop.test/search
      - generic [ref=e59]: © 2026 Acme Fashion. All rights reserved.
```
### Events
- New console entries: .playwright-mcp/console-2026-02-13T18-58-33-875Z.log#L1
- [LOG] 🔍 Browser logger active (MCP server detecte.../shop.test/_boost/browser-logs @ http://shop.test/:19
- [ERROR] Failed to load resource: the server respon...s of 404 (Not Found) @ http://shop.test/favicon.ico:0


thinking:
The home page loads properly with "Acme Fashion" displayed, collections shown, navigation working. The only error is a missing favicon.ico which is expected and not a real issue.

Let me continue the smoke tests by visiting more pages.


text: Home page loads perfectly - "Acme Fashion" with collections and navigation. The only console error is a missing favicon (expected). Let me continue the smoke tests by visiting more pages.


text:
### Ran Playwright code
```js
await page.goto('http://shop.test/collections/t-shirts');
```
### Page
- Page URL: http://shop.test/collections/t-shirts
- Page Title: Shop
### Snapshot
```yaml
- generic [active] [ref=e1]:
  - banner [ref=e2]:
    - generic [ref=e3]:
      - link "Acme Fashion" [ref=e4] [cursor=pointer]:
        - /url: http://shop.test
      - navigation [ref=e5]:
        - link "Collections" [ref=e6] [cursor=pointer]:
          - /url: http://shop.test/collections
      - generic [ref=e7]:
        - link "Search" [ref=e8] [cursor=pointer]:
          - /url: http://shop.test/search
          - img [ref=e9]
        - link "Cart" [ref=e11] [cursor=pointer]:
          - /url: http://shop.test/cart
          - img [ref=e12]
        - link "Account" [ref=e14] [cursor=pointer]:
          - /url: http://shop.test/account/login
          - img [ref=e15]
  - main [ref=e17]:
    - generic [ref=e18]:
      - generic [ref=e20]:
        - heading "T-Shirts" [level=1] [ref=e21]
        - paragraph [ref=e22]: Premium cotton tees for every occasion.
      - generic [ref=e23]:
        - generic [ref=e25]:
          - generic [ref=e26]: Sort by
          - combobox [ref=e27]:
            - option "Newest" [selected]
            - 'option "Price: Low to High"'
            - 'option "Price: High to Low"'
        - generic [ref=e29]:
          - generic [ref=e30]: Min Price
          - spinbutton [ref=e32]
        - generic [ref=e35]:
          - generic [ref=e36]: Max Price
          - spinbutton [ref=e38]
      - generic [ref=e40]:
        - link "Classic Cotton T-Shirt $24.99" [ref=e41] [cursor=pointer]:
          - /url: http://shop.test/products/classic-cotton-t-shirt
          - generic [ref=e43]:
            - heading "Classic Cotton T-Shirt" [level=3] [ref=e44]
            - paragraph [ref=e45]: $24.99
        - link "Graphic Print Tee $29.99" [ref=e46] [cursor=pointer]:
          - /url: http://shop.test/products/graphic-print-tee
          - generic [ref=e48]:
            - heading "Graphic Print Tee" [level=3] [ref=e49]
            - paragraph [ref=e50]: $29.99
        - link "V-Neck Linen Tee $34.99" [ref=e51] [cursor=pointer]:
          - /url: http://shop.test/products/v-neck-linen-tee
          - generic [ref=e53]:
            - heading "V-Neck Linen Tee" [level=3] [ref=e54]
            - paragraph [ref=e55]: $34.99
        - link "Striped Polo Shirt $27.99" [ref=e56] [cursor=pointer]:
          - /url: http://shop.test/products/striped-polo-shirt
          - generic [ref=e58]:
            - heading "Striped Polo Shirt" [level=3] [ref=e59]
            - paragraph [ref=e60]: $27.99
  - contentinfo [ref=e61]:
    - generic [ref=e62]:
      - generic [ref=e63]:
        - generic [ref=e64]:
          - heading "Shop" [level=3] [ref=e65]
          - list [ref=e66]:
            - listitem [ref=e67]:
              - link "Collections" [ref=e68] [cursor=pointer]:
                - /url: http://shop.test/collections
        - generic [ref=e69]:
          - heading "Account" [level=3] [ref=e70]
          - list [ref=e71]:
            - listitem [ref=e72]:
              - link "Sign In" [ref=e73] [cursor=pointer]:
                - /url: http://shop.test/account/login
            - listitem [ref=e74]:
              - link "Create Account" [ref=e75] [cursor=pointer]:
                - /url: http://shop.test/account/register
        - generic [ref=e76]:
          - heading "Help" [level=3] [ref=e77]
          - list [ref=e78]:
            - listitem [ref=e79]:
              - link "Search" [ref=e80] [cursor=pointer]:
                - /url: http://shop.test/search
      - generic [ref=e81]: © 2026 Acme Fashion. All rights reserved.
```
### Events
- [LOG] 🔍 Browser logger active (MCP server detecte...owser-logs @ http://shop.test/collections/t-shirts:19


text: Collection page works - shows T-Shirts with 4 products, sorting options, and price filters. Let me continue testing the product detail page and other key pages.


speed: standard


text:
### Ran Playwright code
```js
await page.goto('http://shop.test/products/classic-cotton-t-shirt');
```
### Page
- Page URL: http://shop.test/products/classic-cotton-t-shirt
- Page Title: Shop
### Snapshot
```yaml
- generic [active] [ref=e1]:
  - banner [ref=e2]:
    - generic [ref=e3]:
      - link "Acme Fashion" [ref=e4] [cursor=pointer]:
        - /url: http://shop.test
      - navigation [ref=e5]:
        - link "Collections" [ref=e6] [cursor=pointer]:
          - /url: http://shop.test/collections
      - generic [ref=e7]:
        - link "Search" [ref=e8] [cursor=pointer]:
          - /url: http://shop.test/search
          - img [ref=e9]
        - link "Cart" [ref=e11] [cursor=pointer]:
          - /url: http://shop.test/cart
          - img [ref=e12]
        - link "Account" [ref=e14] [cursor=pointer]:
          - /url: http://shop.test/account/login
          - img [ref=e15]
  - main [ref=e17]:
    - generic [ref=e21]:
      - heading "Classic Cotton T-Shirt" [level=1] [ref=e22]
      - paragraph [ref=e24]: $24.99
      - generic [ref=e25]:
        - heading "Variants" [level=3] [ref=e26]
        - generic [ref=e27]:
          - button "ACME-CTSH-S-BLK" [ref=e28]
          - button "ACME-CTSH-S-NVY" [ref=e29]
          - button "ACME-CTSH-M-WHT" [ref=e30]
          - button "ACME-CTSH-M-BLK" [ref=e31]
          - button "ACME-CTSH-M-NVY" [ref=e32]
          - button "ACME-CTSH-L-WHT" [ref=e33]
          - button "ACME-CTSH-L-BLK" [ref=e34]
          - button "ACME-CTSH-L-NVY" [ref=e35]
          - button "ACME-CTSH-XL-WHT" [ref=e36]
          - button "ACME-CTSH-XL-BLK" [ref=e37]
          - button "ACME-CTSH-XL-NVY" [ref=e38]
          - button "ACME-CTSH-S-WHT" [ref=e39]
      - generic [ref=e40]:
        - button "Add to Cart" [disabled]
        - paragraph [ref=e41]: Cart functionality coming soon
      - generic [ref=e42]:
        - heading "Description" [level=3] [ref=e43]
        - paragraph [ref=e45]: A timeless classic cotton t-shirt. Comfortable, breathable, and perfect for everyday wear.
      - generic [ref=e47]:
        - generic [ref=e48]:
          - term [ref=e49]: Vendor
          - definition [ref=e50]: Acme Basics
        - generic [ref=e51]:
          - term [ref=e52]: Type
          - definition [ref=e53]: T-Shirts
        - generic [ref=e54]:
          - term [ref=e55]: SKU
          - definition [ref=e56]: ACME-CTSH-S-WHT
  - contentinfo [ref=e57]:
    - generic [ref=e58]:
      - generic [ref=e59]:
        - generic [ref=e60]:
          - heading "Shop" [level=3] [ref=e61]
          - list [ref=e62]:
            - listitem [ref=e63]:
              - link "Collections" [ref=e64] [cursor=pointer]:
                - /url: http://shop.test/collections
        - generic [ref=e65]:
          - heading "Account" [level=3] [ref=e66]
          - list [ref=e67]:
            - listitem [ref=e68]:
              - link "Sign In" [ref=e69] [cursor=pointer]:
                - /url: http://shop.test/account/login
            - listitem [ref=e70]:
              - link "Create Account" [ref=e71] [cursor=pointer]:
                - /url: http://shop.test/account/register
        - generic [ref=e72]:
          - heading "Help" [level=3] [ref=e73]
          - list [ref=e74]:
            - listitem [ref=e75]:
              - link "Search" [ref=e76] [cursor=pointer]:
                - /url: http://shop.test/search
      - generic [ref=e77]: © 2026 Acme Fashion. All rights reserved.
```
### Events
- [LOG] 🔍 Browser logger active (MCP server detecte...@ http://shop.test/products/classic-cotton-t-shirt:19


thinking:
Product detail page works - shows the Classic Cotton T-Shirt with $24.99 price, all 12 variants, description, vendor/type/SKU. The "Add to Cart" button is disabled with a placeholder message "Cart functionality coming soon". This is likely because the Cart/Checkout UI was deferred to Phase 11 (Step 4.9). Let me check the Livewire component for this.

Actually looking at the progress.md, Step 4.9 says "Storefront Cart/Checkout UI (deferred to Phase 11)". But Phase 11 was about seeders and polish, not about adding the cart UI. So the cart functionality on the storefront is not yet implemented as a Livewire interactive component.

I need to verify what's actually in the Cart and product show components to understand the current state of the add-to-cart functionality.


text: The product page works but "Add to Cart" is disabled with "Cart functionality coming soon". This was deferred from Phase 4 (Step 4.9). Let me check what cart/checkout UI exists and what needs to be built.


content:
     1→<?php
     2→
     3→namespace App\Livewire\Storefront\Products;
     4→
     5→use App\Enums\ProductStatus;
     6→use App\Models\Product;
     7→use App\Models\ProductVariant;
     8→use Illuminate\Contracts\View\View;
     9→use Livewire\Attributes\Layout;
    10→use Livewire\Component;
    11→
    12→#[Layout('storefront.layouts.app')]
    13→class Show extends Component
    14→{
    15→    public Product $product;
    16→
    17→    public ?int $selectedVariantId = null;
    18→
    19→    public function mount(string $handle): void
    20→    {
    21→        $product = Product::query()
    22→            ->where('handle', $handle)
    23→            ->where('status', ProductStatus::Active)
    24→            ->with(['variants', 'options.values', 'media'])
    25→            ->first();
    26→
    27→        if (! $product) {
    28→            abort(404);
    29→        }
    30→
    31→        $this->product = $product;
    32→
    33→        $defaultVariant = $product->variants->firstWhere('is_default', true)
    34→            ?? $product->variants->first();
    35→
    36→        $this->selectedVariantId = $defaultVariant?->id;
    37→    }
    38→
    39→    public function selectVariant(int $variantId): void
    40→    {
    41→        $this->selectedVariantId = $variantId;
    42→    }
    43→
    44→    public function getSelectedVariantProperty(): ?ProductVariant
    45→    {
    46→        if (! $this->selectedVariantId) {
    47→            return null;
    48→        }
    49→
    50→        return $this->product->variants->find($this->selectedVariantId);
    51→    }
    52→
    53→    public function render(): View
    54→    {
    55→        return view('livewire.storefront.products.show');
    56→    }
    57→}
    58→

content:
<?php

namespace App\Livewire\Storefront\Products;

use App\Enums\ProductStatus;
use App\Models\Product;
use App\Models\ProductVariant;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\Layout;
use Livewire\Component;

#[Layout('storefront.layouts.app')]
class Show extends Component
{
    public Product $product;

    public ?int $selectedVariantId = null;

    public function mount(string $handle): void
    {
        $product = Product::query()
            ->where('handle', $handle)
            ->where('status', ProductStatus::Active)
            ->with(['variants', 'options.values', 'media'])
            ->first();

        if (! $product) {
            abort(404);
        }

        $this->product = $product;

        $defaultVariant = $product->variants->firstWhere('is_default', true)
            ?? $product->variants->first();

        $this->selectedVariantId = $defaultVariant?->id;
    }

    public function selectVariant(int $variantId): void
    {
        $this->selectedVariantId = $variantId;
    }

    public function getSelectedVariantProperty(): ?ProductVariant
    {
        if (! $this->selectedVariantId) {
            return null;
        }

        return $this->product->variants->find($this->selectedVariantId);
    }

    public function render(): View
    {
        return view('livewire.storefront.products.show');
    }
}


content:
     1→<?php
     2→
     3→namespace App\Livewire\Storefront\Cart;
     4→
     5→use Illuminate\Contracts\View\View;
     6→use Livewire\Attributes\Layout;
     7→use Livewire\Component;
     8→
     9→#[Layout('storefront.layouts.app')]
    10→class Show extends Component
    11→{
    12→    public function render(): View
    13→    {
    14→        return view('livewire.storefront.cart.show');
    15→    }
    16→}
    17→

content:
<?php

namespace App\Livewire\Storefront\Cart;

use Illuminate\Contracts\View\View;
use Livewire\Attributes\Layout;
use Livewire\Component;

#[Layout('storefront.layouts.app')]
class Show extends Component
{
    public function render(): View
    {
        return view('livewire.storefront.cart.show');
    }
}


thinking:
The Cart Show component is basically a skeleton - it just renders the view. And the Product Show component doesn't have an `addToCart` method. The storefront cart/checkout UI was deferred (Step 4.9) and never implemented.

This is a significant gap. The E2E tests require:
- Adding products to cart
- Cart management (quantity updates, removing items)
- Full checkout flow
- Payment
- Customer accounts interacting with orders

I need to implement the storefront cart/checkout UI before I can do E2E testing. Let me check what the spec says about this.

Let me read the relevant specs for the storefront cart and checkout UI.


text: The Cart and Product components are missing the cart/checkout UI functionality. This was deferred from Phase 4 and never implemented. I need to build this before E2E testing. Let me check the spec and implement it.


content:
     1→# 04 - Storefront UI/UX Specification
     2→
     3→> E-commerce platform - Customer-facing storefront UI/UX requirements
     4→
     5→---
     6→
     7→## Conventions
     8→
     9→| Convention | Detail |
    10→|---|---|
    11→| CSS Framework | Tailwind CSS v4, mobile-first design, dark mode support |
    12→| Interactivity | Livewire v4 for all dynamic UI (cart, filters, checkout) |
    13→| Templating | Blade templates with reusable storefront-prefixed components |
    14→| Accessibility | WCAG 2.2 AA compliance throughout |
    15→| Responsive | Mobile-first with progressive enhancement at small, medium, large, and extra-large breakpoints |
    16→| Currency | All prices stored as integers (cents), formatted with locale-aware helpers |
    17→| Images | Lazy-loaded, with proper alt text and maintained aspect ratios |
    18→
    19→---
    20→
    21→## Currency Formatting
    22→
    23→All monetary amounts are stored as integers in minor units (cents) and must be formatted for display according to these rules:
    24→
    25→| Setting | Value |
    26→|---------|-------|
    27→| Decimal separator | `.` (period) |
    28→| Thousands separator | `,` (comma) |
    29→| Currency symbol | After amount with space (e.g., `24.99 EUR`) |
    30→| Decimal places | Always 2 |
    31→| Free amounts | Display as `0.00 EUR` |
    32→| Negative amounts (refunds) | Prefix with `-` (e.g., `-12.50 EUR`) |
    33→
    34→**Helper:** Use a shared Blade component or helper (e.g., `<x-price :amount="$amount" :currency="$currency" />`) to ensure consistent formatting across all storefront pages.
    35→
    36→**Examples:**
    37→- 2499 cents -> `24.99 EUR`
    38→- 0 cents -> `0.00 EUR`
    39→- 149900 cents -> `1,499.00 EUR`
    40→
    41→---
    42→
    43→## 1. Theme System Architecture
    44→
    45→### 1.1 View Organization
    46→
    47→The storefront views are organized into the following categories:
    48→
    49→| Category | Purpose | Views |
    50→|---|---|---|
    51→| Layouts | Master page shell | app (master layout) |
    52→| Pages | Core storefront pages | home, collection, product, cart, search, page (static CMS) |
    53→| Checkout | Purchase flow | checkout index, order confirmation |
    54→| Account | Customer area | login, register, dashboard, orders (list + detail), addresses |
    55→| Errors | Error states | 404, 503 |
    56→| Components | Reusable UI elements | product-card, price, badge, quantity-selector, address-form, order-summary, breadcrumbs, pagination |
    57→
    58→### 1.2 Layout Hierarchy
    59→
    60→All storefront pages extend a single master layout, which provides the full page shell including header, footer, cart drawer, and global Livewire listeners. Individual page templates fill the main content section.
    61→
    62→### 1.3 Theme Settings
    63→
    64→Theme settings are stored in the database as JSON (keyed by theme ID). A singleton service loads and caches the active theme settings for the current store. Settings control:
    65→
    66→- Announcement bar visibility, text, link, and background color
    67→- Sticky header toggle
    68→- Home page section order and visibility
    69→- Color palette overrides (primary, secondary, accent)
    70→- Typography scale
    71→- Footer content and social links
    72→- Dark mode behavior (system preference, toggle, or forced)
    73→
    74→### 1.4 Section-Based Architecture
    75→
    76→Each page is composed of configurable sections. Sections are Blade includes or Livewire components receiving configuration from theme settings.
    77→
    78→| Section | Description | Used On |
    79→|---|---|---|
    80→| Hero | Full-width banner with heading, subheading, call-to-action | Home |
    81→| Featured Collections | Grid of collection cards | Home |
    82→| Featured Products | Grid of product cards (lazy-loaded) | Home |
    83→| Newsletter | Email signup form | Home, Footer |
    84→| Rich Text | Customizable content block | Home, Pages |
    85→
    86→### 1.5 Blade Components
    87→
    88→All reusable UI elements are implemented as Blade components with a storefront prefix. See Section 16 for the full component library reference.
    89→
    90→---
    91→
    92→## 2. Base Layout
    93→
    94→### 2.1 Document Head
    95→
    96→The document head includes:
    97→
    98→| Element | Description |
    99→|---|---|
   100→| Character encoding | UTF-8 |
   101→| Viewport | Width = device width, initial scale 1 |
   102→| Meta description | Page-specific, yielded from child templates |
   103→| Title | Page-specific title appended with the app name |
   104→| Stylesheets | Compiled CSS via Vite |
   105→| Scripts | Compiled JS via Vite |
   106→| Livewire styles | Injected by Livewire |
   107→| Language attribute | Set on the HTML element from application locale |
   108→| Scroll behavior | Smooth scrolling enabled globally |
   109→
   110→### 2.2 Skip Link
   111→
   112→A visually hidden "Skip to main content" link is the first element in the body. It becomes visible on keyboard focus, positioned fixed at top-left with a clear visual treatment (solid background, shadow, focus ring). Supports dark mode.
   113→
   114→**Accessibility:** Allows keyboard users to bypass the header navigation and jump directly to the main content area.
   115→
   116→### 2.3 Announcement Bar
   117→
   118→Optional, controlled by theme settings. Renders a full-width bar above the header.
   119→
   120→| Attribute | Description |
   121→|---|---|
   122→| Position | Full-width, above the header |
   123→| Background | Configurable color, defaults to dark background (light text) / inverted in dark mode |
   124→| Text | Centered, small size, configurable content |
   125→| Link | Optional, underlined, opens in same tab |
   126→| Dismiss | Close button (X icon) on the right side; dismissal persisted via localStorage |
   127→| Height | Auto height with vertical padding |
   128→
   129→### 2.4 Header
   130→
   131→The header serves as the primary navigation banner.
   132→
   133→**Desktop Layout (large breakpoint and above):**
   134→
   135→```
   136→+--------------------------------------------------------------+
   137→| [Logo]      [Nav Item] [Nav Item] [Nav Item]     [S] [C] [A] |
   138→+--------------------------------------------------------------+
   139→```
   140→
   141→| Element | Position | Description |
   142→|---|---|---|
   143→| Logo | Left | Linked to home page. Shows image if logo URL is set in theme settings, otherwise store name as text. Max height approximately 40px on desktop, 32px on smaller screens |
   144→| Main Navigation | Center | Horizontal nav with links sourced from the "main-menu" navigation menu. Supports one level of dropdown submenus (revealed on hover/focus) |
   145→| Search Icon | Right group | Button that opens the search modal |
   146→| Cart Icon | Right group | Button that opens the cart drawer. Displays item count badge (small circular indicator, primary color background, white text) |
   147→| Account Icon | Right group | Link to account dashboard (logged in) or login page (guest) |
   148→
   149→**Mobile Layout (below large breakpoint):**
   150→
   151→```
   152→+--------------------------------------------------------------+
   153→| [Hamburger]           [Logo]                            [Cart]|
   154→+--------------------------------------------------------------+
   155→```
   156→
   157→| Element | Position | Description |
   158→|---|---|---|
   159→| Hamburger menu | Left | Toggle button for mobile navigation drawer |
   160→| Logo | Center | Same as desktop but horizontally centered |
   161→| Cart icon | Right | Same as desktop |
   162→
   163→**Sticky Header (optional):**
   164→
   165→When enabled in theme settings, the header sticks to the top of the viewport with a semi-transparent background and backdrop blur. A subtle bottom border appears on scroll.
   166→
   167→**Mobile Navigation Drawer:**
   168→
   169→| Attribute | Description |
   170→|---|---|
   171→| Animation | Slides in from the left side |
   172→| Height | Full viewport height |
   173→| Backdrop | Semi-transparent dark overlay behind the drawer |
   174→| Close button | X icon at the top-right of the drawer |
   175→| Navigation items | Vertical stack with comfortable tap targets |
   176→| Submenus | Collapsible accordion pattern with chevron icons |
   177→| Account link | Positioned at the bottom of the navigation list |
   178→| Focus behavior | Focus is trapped within the drawer while open |
   179→| ARIA | Labeled as "Mobile navigation" |
   180→
   181→### 2.5 Main Content Area
   182→
   183→The main content area is a landmark element with a unique ID for skip-link targeting. It occupies at least the full viewport height.
   184→
   185→### 2.6 Footer
   186→
   187→The footer is a content-info landmark.
   188→
   189→**Layout:**
   190→
   191→```
   192→+--------------------------------------------------------------+
   193→| [Column 1: Links]  [Column 2: Links]  [Column 3: Store Info] |
   194→|                                                               |
   195→| [Social Icons]                                                |
   196→| [Copyright] [Payment Icons]                                   |
   197→+--------------------------------------------------------------+
   198→```
   199→
   200→| Element | Description |

content:
# 04 - Storefront UI/UX Specification

> E-commerce platform - Customer-facing storefront UI/UX requirements

---

## Conventions

| Convention | Detail |
|---|---|
| CSS Framework | Tailwind CSS v4, mobile-first design, dark mode support |
| Interactivity | Livewire v4 for all dynamic UI (cart, filters, checkout) |
| Templating | Blade templates with reusable storefront-prefixed components |
| Accessibility | WCAG 2.2 AA compliance throughout |
| Responsive | Mobile-first with progressive enhancement at small, medium, large, and extra-large breakpoints |
| Currency | All prices stored as integers (cents), formatted with locale-aware helpers |
| Images | Lazy-loaded, with proper alt text and maintained aspect ratios |

---

## Currency Formatting

All monetary amounts are stored as integers in minor units (cents) and must be formatted for display according to these rules:

| Setting | Value |
|---------|-------|
| Decimal separator | `.` (period) |
| Thousands separator | `,` (comma) |
| Currency symbol | After amount with space (e.g., `24.99 EUR`) |
| Decimal places | Always 2 |
| Free amounts | Display as `0.00 EUR` |
| Negative amounts (refunds) | Prefix with `-` (e.g., `-12.50 EUR`) |

**Helper:** Use a shared Blade component or helper (e.g., `<x-price :amount="$amount" :currency="$currency" />`) to ensure consistent formatting across all storefront pages.

**Examples:**
- 2499 cents -> `24.99 EUR`
- 0 cents -> `0.00 EUR`
- 149900 cents -> `1,499.00 EUR`

---

## 1. Theme System Architecture

### 1.1 View Organization

The storefront views are organized into the following categories:

| Category | Purpose | Views |
|---|---|---|
| Layouts | Master page shell | app (master layout) |
| Pages | Core storefront pages | home, collection, product, cart, search, page (static CMS) |
| Checkout | Purchase flow | checkout index, order confirmation |
| Account | Customer area | login, register, dashboard, orders (list + detail), addresses |
| Errors | Error states | 404, 503 |
| Components | Reusable UI elements | product-card, price, badge, quantity-selector, address-form, order-summary, breadcrumbs, pagination |

### 1.2 Layout Hierarchy

All storefront pages extend a single master layout, which provides the full page shell including header, footer, cart drawer, and global Livewire listeners. Individual page templates fill the main content section.

### 1.3 Theme Settings

Theme settings are stored in the database as JSON (keyed by theme ID). A singleton service loads and caches the active theme settings for the current store. Settings control:

- Announcement bar visibility, text, link, and background color
- Sticky header toggle
- Home page section order and visibility
- Color palette overrides (primary, secondary, accent)
- Typography scale
- Footer content and social links
- Dark mode behavior (system preference, toggle, or forced)

### 1.4 Section-Based Architecture

Each page is composed of configurable sections. Sections are Blade includes or Livewire components receiving configuration from theme settings.

| Section | Description | Used On |
|---|---|---|
| Hero | Full-width banner with heading, subheading, call-to-action | Home |
| Featured Collections | Grid of collection cards | Home |
| Featured Products | Grid of product cards (lazy-loaded) | Home |
| Newsletter | Email signup form | Home, Footer |
| Rich Text | Customizable content block | Home, Pages |

### 1.5 Blade Components

All reusable UI elements are implemented as Blade components with a storefront prefix. See Section 16 for the full component library reference.

---

## 2. Base Layout

### 2.1 Document Head

The document head includes:

| Element | Description |
|---|---|
| Character encoding | UTF-8 |
| Viewport | Width = device width, initial scale 1 |
| Meta description | Page-specific, yielded from child templates |
| Title | Page-specific title appended with the app name |
| Stylesheets | Compiled CSS via Vite |
| Scripts | Compiled JS via Vite |
| Livewire styles | Injected by Livewire |
| Language attribute | Set on the HTML element from application locale |
| Scroll behavior | Smooth scrolling enabled globally |

### 2.2 Skip Link

A visually hidden "Skip to main content" link is the first element in the body. It becomes visible on keyboard focus, positioned fixed at top-left with a clear visual treatment (solid background, shadow, focus ring). Supports dark mode.

**Accessibility:** Allows keyboard users to bypass the header navigation and jump directly to the main content area.

### 2.3 Announcement Bar

Optional, controlled by theme settings. Renders a full-width bar above the header.

| Attribute | Description |
|---|---|
| Position | Full-width, above the header |
| Background | Configurable color, defaults to dark background (light text) / inverted in dark mode |
| Text | Centered, small size, configurable content |
| Link | Optional, underlined, opens in same tab |
| Dismiss | Close button (X icon) on the right side; dismissal persisted via localStorage |
| Height | Auto height with vertical padding |

### 2.4 Header

The header serves as the primary navigation banner.

**Desktop Layout (large breakpoint and above):**

```
+--------------------------------------------------------------+
| [Logo]      [Nav Item] [Nav Item] [Nav Item]     [S] [C] [A] |
+--------------------------------------------------------------+
```

| Element | Position | Description |
|---|---|---|
| Logo | Left | Linked to home page. Shows image if logo URL is set in theme settings, otherwise store name as text. Max height approximately 40px on desktop, 32px on smaller screens |
| Main Navigation | Center | Horizontal nav with links sourced from the "main-menu" navigation menu. Supports one level of dropdown submenus (revealed on hover/focus) |
| Search Icon | Right group | Button that opens the search modal |
| Cart Icon | Right group | Button that opens the cart drawer. Displays item count badge (small circular indicator, primary color background, white text) |
| Account Icon | Right group | Link to account dashboard (logged in) or login page (guest) |

**Mobile Layout (below large breakpoint):**

```
+--------------------------------------------------------------+
| [Hamburger]           [Logo]                            [Cart]|
+--------------------------------------------------------------+
```

| Element | Position | Description |
|---|---|---|
| Hamburger menu | Left | Toggle button for mobile navigation drawer |
| Logo | Center | Same as desktop but horizontally centered |
| Cart icon | Right | Same as desktop |

**Sticky Header (optional):**

When enabled in theme settings, the header sticks to the top of the viewport with a semi-transparent background and backdrop blur. A subtle bottom border appears on scroll.

**Mobile Navigation Drawer:**

| Attribute | Description |
|---|---|
| Animation | Slides in from the left side |
| Height | Full viewport height |
| Backdrop | Semi-transparent dark overlay behind the drawer |
| Close button | X icon at the top-right of the drawer |
| Navigation items | Vertical stack with comfortable tap targets |
| Submenus | Collapsible accordion pattern with chevron icons |
| Account link | Positioned at the bottom of the navigation list |
| Focus behavior | Focus is trapped within the drawer while open |
| ARIA | Labeled as "Mobile navigation" |

### 2.5 Main Content Area

The main content area is a landmark element with a unique ID for skip-link targeting. It occupies at least the full viewport height.

### 2.6 Footer

The footer is a content-info landmark.

**Layout:**

```
+--------------------------------------------------------------+
| [Column 1: Links]  [Column 2: Links]  [Column 3: Store Info] |
|                                                               |
| [Social Icons]                                                |
| [Copyright] [Payment Icons]                                   |
+--------------------------------------------------------------+
```

| Element | Description |


content:
1→

     2→

     3→        {{-- Product Images --}}
     4→

     5→            @if($product->media->isNotEmpty())
     6→                media->first()->url }}" alt="{{ $product->title }}" class="h-full w-full rounded-lg object-cover">
     7→            @endif
     8→

     9→
    10→        {{-- Product Details --}}
    11→

    12→
{{ $product->title }}

    13→
    14→            @if($this->selectedVariant)
    15→

    16→

    17→                        ${{ number_format($this->selectedVariant->price_amount / 100, 2) }}
    18→

    19→                    @if($this->selectedVariant->compare_at_amount)
    20→

    21→                            ${{ number_format($this->selectedVariant->compare_at_amount / 100, 2) }}
    22→

    23→                    @endif
    24→

    25→            @endif
    26→
    27→            {{-- Variant Selector --}}
    28→            @if($product->variants->count() > 1)
    29→

    30→
Variants

    31→

    32→                        @foreach($product->variants as $variant)
    33→                            <button wire:key="variant-{{ $variant->id }}"
    34→                                    wire:click="selectVariant({{ $variant->id }})"
    35→                                    class="rounded-md border px-4 py-2 text-sm transition-colors {{ $selectedVariantId === $variant->id ? 'border-zinc-900 bg-zinc-900 text-white dark:border-zinc-100 dark:bg-zinc-100 dark:text-zinc-900' : 'border-zinc-300 text-zinc-700 hover:border-zinc-500 dark:border-zinc-600 dark:text-zinc-300 dark:hover:border-zinc-400' }}">
    36→                                {{ $variant->sku ?: 'Variant '.$loop->iteration }}
    37→                            </button>
    38→                        @endforeach
    39→

    40→

    41→            @endif
    42→
    43→            {{-- Add to Cart --}}
    44→

    45→                <flux:button variant="primary" class="w-full" disabled>
    46→                    Add to Cart
    47→                </flux:button>
    48→
Cart functionality coming soon

    49→

    50→
    51→            {{-- Description --}}
    52→            @if($product->description_html)
    53→

    54→
Description

    55→

    56→                        {!! $product->description_html !!}
    57→

    58→

    59→            @endif
    60→
    61→            {{-- Product Meta --}}
    62→

    63→

    64→                    @if($product->vendor)
    65→

    66→
Vendor

    67→
{{ $product->vendor }}

    68→

    69→                    @endif
    70→                    @if($product->product_type)
    71→

    72→
Type

    73→
{{ $product->product_type }}

    74→

    75→                    @endif
    76→                    @if($this->selectedVariant?->sku)
    77→

    78→
SKU

    79→
{{ $this->selectedVariant->sku }}

    80→

    81→                    @endif
    82→

    83→

    84→

    85→

    86→

    87→

content:
        {{-- Product Images --}}

            @if($product->media->isNotEmpty())
                media->first()->url }}" alt="{{ $product->title }}" class="h-full w-full rounded-lg object-cover">
            @endif

        {{-- Product Details --}}

{{ $product->title }}

            @if($this->selectedVariant)

                        ${{ number_format($this->selectedVariant->price_amount / 100, 2) }}

                    @if($this->selectedVariant->compare_at_amount)

                            ${{ number_format($this->selectedVariant->compare_at_amount / 100, 2) }}

                    @endif

            @endif

            {{-- Variant Selector --}}
            @if($product->variants->count() > 1)

Variants

                        @foreach($product->variants as $variant)
                            <button wire:key="variant-{{ $variant->id }}"
                                    wire:click="selectVariant({{ $variant->id }})"
                                    class="rounded-md border px-4 py-2 text-sm transition-colors {{ $selectedVariantId === $variant->id ? 'border-zinc-900 bg-zinc-900 text-white dark:border-zinc-100 dark:bg-zinc-100 dark:text-zinc-900' : 'border-zinc-300 text-zinc-700 hover:border-zinc-500 dark:border-zinc-600 dark:text-zinc-300 dark:hover:border-zinc-400' }}">
                                {{ $variant->sku ?: 'Variant '.$loop->iteration }}
                            </button>
                        @endforeach

            @endif

            {{-- Add to Cart --}}

                <flux:button variant="primary" class="w-full" disabled>
                    Add to Cart
                </flux:button>

Cart functionality coming soon

            {{-- Description --}}
            @if($product->description_html)

Description

                        {!! $product->description_html !!}

            @endif

            {{-- Product Meta --}}

                    @if($product->vendor)

Vendor

{{ $product->vendor }}

                    @endif
                    @if($product->product_type)

Type

{{ $product->product_type }}

                    @endif
                    @if($this->selectedVariant?->sku)

SKU

{{ $this->selectedVariant->sku }}

                    @endif


content:
1→

     2→
Your Cart

     3→
     4→

     5→
Your cart is empty.

     6→

     7→            <flux:button variant="primary" href="{{ route('storefront.collections.index') }}">
     8→                Continue Shopping
     9→            </flux:button>
    10→

    11→

    12→

    13→

content:
Your Cart

Your cart is empty.

            <flux:button variant="primary" href="{{ route('storefront.collections.index') }}">
                Continue Shopping
            </flux:button>


thinking:
I see - the cart functionality on the storefront is a placeholder. I need to implement:

1. **Product Show** - Add to cart functionality (call CartService)
2. **Cart Show** - Full cart view with items, quantities, remove, discounts
3. **Checkout flow** - Multi-step checkout pages (address, shipping, payment, confirmation)

This is a significant amount of work. Let me read the storefront UI spec for the cart and checkout sections to understand what's needed.

Let me read more of the storefront spec.


Task

description: Read storefront UI spec cart/checkout

prompt:
Read the file /Users/fabianwesner/Herd/shop/specs/04-STOREFRONT-UI.md and find all sections related to:
1. Product detail page (add to cart button, quantity selector, inventory states)
2. Cart page/drawer
3. Checkout flow (address, shipping, payment, confirmation)
4. Order confirmation page

Return the full text of these sections, including all requirements and specifications.


text:
Read the file /Users/fabianwesner/Herd/shop/specs/04-STOREFRONT-UI.md and find all sections related to:
1. Product detail page (add to cart button, quantity selector, inventory states)
2. Cart page/drawer
3. Checkout flow (address, shipping, payment, confirmation)
4. Order confirmation page

Return the full text of these sections, including all requirements and specifications.

prompt:
Read the file /Users/fabianwesner/Herd/shop/specs/04-STOREFRONT-UI.md and find all sections related to:
1. Product detail page (add to cart button, quantity selector, inventory states)
2. Cart page/drawer
3. Checkout flow (address, shipping, payment, confirmation)
4. Order confirmation page

Return the full text of these sections, including all requirements and specifications.


prompt:
Read the file /Users/fabianwesner/Herd/shop/specs/04-STOREFRONT-UI.md and find all sections related to:
1. Product detail page (add to cart button, quantity selector, inventory states)
2. Cart page/drawer
3. Checkout flow (address, shipping, payment, confirmation)
4. Order confirmation page

Return the full text of these sections, including all requirements and specifications.