{{ $installation->app->name }}
Installed {{ $installation->installed_at?->diffForHumans() ?? 'recently' }}
diff --git a/.env.example b/.env.example index c0660ea1..bc0a4354 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,8 @@ -APP_NAME=Laravel +APP_NAME=Shop APP_ENV=local APP_KEY= APP_DEBUG=true -APP_URL=http://localhost +APP_URL=http://shop.test APP_LOCALE=en APP_FALLBACK_LOCALE=en @@ -27,17 +27,25 @@ DB_CONNECTION=sqlite # DB_USERNAME=root # DB_PASSWORD= -SESSION_DRIVER=database +SESSION_DRIVER=file SESSION_LIFETIME=120 -SESSION_ENCRYPT=false +SESSION_ENCRYPT=true +SESSION_COOKIE=shop_session +SESSION_SECURE_COOKIE=false SESSION_PATH=/ SESSION_DOMAIN=null BROADCAST_CONNECTION=log FILESYSTEM_DISK=local -QUEUE_CONNECTION=database +QUEUE_CONNECTION=sync -CACHE_STORE=database +SANCTUM_TOKEN_PREFIX=shop_ +SANCTUM_EXPIRATION=525600 + +CODE_QUALITY_MULTI_TENANT=true +CODE_QUALITY_TENANT_KEYS=store_id + +CACHE_STORE=file # CACHE_PREFIX= MEMCACHED_HOST=127.0.0.1 @@ -63,3 +71,16 @@ AWS_BUCKET= AWS_USE_PATH_STYLE_ENDPOINT=false VITE_APP_NAME="${APP_NAME}" +# Code-quality checker (php artisan code-quality:check) +CODE_QUALITY_TIMEOUT=600 +CODE_QUALITY_CHECK_TIMEOUT=180 +CODE_QUALITY_AI_ENABLED=false +CODE_QUALITY_CODEX_BINARY=codex +CODE_QUALITY_AI_MODEL=gpt-5.4-mini +CODE_QUALITY_AI_CONCURRENCY=3 +CODE_QUALITY_AI_TIMEOUT=180 +CODE_QUALITY_AI_REASONING=low +CODE_QUALITY_AI_VERBOSITY=low +CODE_QUALITY_AI_MAX_PROMPT_FILE_BYTES=120000 +CODE_QUALITY_AI_FREE_PATH_KEYWORDS= +CODE_QUALITY_FRONTEND_ROUTE_HELPERS=route,action,wayfinder diff --git a/.gitignore b/.gitignore index c7cf1fa6..4582f38c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.phpunit.cache +/.playwright-mcp/ /node_modules /public/build /public/hot diff --git a/.playwright-mcp/console-2026-07-11T11-01-40-401Z.log b/.playwright-mcp/console-2026-07-11T11-01-40-401Z.log new file mode 100644 index 00000000..7ca61290 --- /dev/null +++ b/.playwright-mcp/console-2026-07-11T11-01-40-401Z.log @@ -0,0 +1,4 @@ +[ 216ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-fashion.test/_boost/browser-logs @ http://acme-fashion.test/:57 +[ 411ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://acme-fashion.test/favicon.ico:0 +[ 29302ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-fashion.test/_boost/browser-logs @ http://acme-fashion.test/:57 +[ 33075ms] [LOG] [vite] server connection lost. Polling for restart... @ http://acme-fashion.test/:116 diff --git a/.playwright-mcp/console-2026-07-11T11-02-45-521Z.log b/.playwright-mcp/console-2026-07-11T11-02-45-521Z.log new file mode 100644 index 00000000..61212ad4 --- /dev/null +++ b/.playwright-mcp/console-2026-07-11T11-02-45-521Z.log @@ -0,0 +1 @@ +[ 86ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-fashion.test/_boost/browser-logs @ http://acme-fashion.test/:56 diff --git a/.playwright-mcp/console-2026-07-11T11-02-54-584Z.log b/.playwright-mcp/console-2026-07-11T11-02-54-584Z.log new file mode 100644 index 00000000..9974f20c --- /dev/null +++ b/.playwright-mcp/console-2026-07-11T11-02-54-584Z.log @@ -0,0 +1,3 @@ +[ 105ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-fashion.test/_boost/browser-logs @ http://acme-fashion.test/products/classic-cotton-t-shirt:56 +[ 7744ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://acme-fashion.test/livewire-6701cc17/update:0 +[ 7763ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-fashion.test/_boost/browser-logs @ :6 diff --git a/.playwright-mcp/console-2026-07-11T11-04-12-738Z.log b/.playwright-mcp/console-2026-07-11T11-04-12-738Z.log new file mode 100644 index 00000000..ff90dc77 --- /dev/null +++ b/.playwright-mcp/console-2026-07-11T11-04-12-738Z.log @@ -0,0 +1,4 @@ +[ 92ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-fashion.test/_boost/browser-logs @ http://acme-fashion.test/products/classic-cotton-t-shirt:56 +[ 32518ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-fashion.test/_boost/browser-logs @ http://acme-fashion.test/cart:56 +[ 45939ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://acme-fashion.test/livewire-6701cc17/update:0 +[ 45951ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-fashion.test/_boost/browser-logs @ :6 diff --git a/.playwright-mcp/console-2026-07-11T11-05-51-521Z.log b/.playwright-mcp/console-2026-07-11T11-05-51-521Z.log new file mode 100644 index 00000000..15086794 --- /dev/null +++ b/.playwright-mcp/console-2026-07-11T11-05-51-521Z.log @@ -0,0 +1,4 @@ +[ 78ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-fashion.test/_boost/browser-logs @ http://acme-fashion.test/products/classic-cotton-t-shirt:56 +[ 1296ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-fashion.test/_boost/browser-logs @ http://acme-fashion.test/cart:56 +[ 1940ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-fashion.test/_boost/browser-logs @ http://acme-fashion.test/checkout/2:56 +[ 48647ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-fashion.test/_boost/browser-logs @ http://acme-fashion.test/checkout/2/confirmation?order=19:56 diff --git a/.playwright-mcp/console-2026-07-11T11-07-38-637Z.log b/.playwright-mcp/console-2026-07-11T11-07-38-637Z.log new file mode 100644 index 00000000..498a1abc --- /dev/null +++ b/.playwright-mcp/console-2026-07-11T11-07-38-637Z.log @@ -0,0 +1,4 @@ +[ 88ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-fashion.test/_boost/browser-logs @ http://acme-fashion.test/products/classic-cotton-t-shirt:56 +[ 1315ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-fashion.test/_boost/browser-logs @ http://acme-fashion.test/cart:56 +[ 1954ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-fashion.test/_boost/browser-logs @ http://acme-fashion.test/checkout/3:56 +[ 48735ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-fashion.test/_boost/browser-logs @ http://acme-fashion.test/checkout/3/confirmation?order=20:56 diff --git a/.playwright-mcp/console-2026-07-11T11-08-36-938Z.log b/.playwright-mcp/console-2026-07-11T11-08-36-938Z.log new file mode 100644 index 00000000..3639e81a --- /dev/null +++ b/.playwright-mcp/console-2026-07-11T11-08-36-938Z.log @@ -0,0 +1,5 @@ +[ 57ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-fashion.test/_boost/browser-logs @ http://acme-fashion.test/admin/login:51 +[ 48918ms] [WARNING] The resource http://acme-fashion.test/build/assets/app-Cxwdcq-d.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://acme-fashion.test/admin/orders/5:0 +[ 59121ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://acme-fashion.test/livewire-6701cc17/update:0 +[ 59137ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-fashion.test/_boost/browser-logs @ :6 +[ 62543ms] [WARNING] The resource http://acme-fashion.test/build/assets/app-Cxwdcq-d.css was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://acme-fashion.test/admin/orders/5:0 diff --git a/.playwright-mcp/console-2026-07-11T11-10-28-191Z.log b/.playwright-mcp/console-2026-07-11T11-10-28-191Z.log new file mode 100644 index 00000000..3229b315 --- /dev/null +++ b/.playwright-mcp/console-2026-07-11T11-10-28-191Z.log @@ -0,0 +1 @@ +[ 110ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-fashion.test/_boost/browser-logs @ http://acme-fashion.test/dashboard:51 diff --git a/.playwright-mcp/console-2026-07-11T11-10-28-362Z.log b/.playwright-mcp/console-2026-07-11T11-10-28-362Z.log new file mode 100644 index 00000000..0d72d14d --- /dev/null +++ b/.playwright-mcp/console-2026-07-11T11-10-28-362Z.log @@ -0,0 +1 @@ +[ 59ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-fashion.test/_boost/browser-logs @ http://acme-fashion.test/admin/orders/5:50 diff --git a/.playwright-mcp/console-2026-07-11T11-10-38-930Z.log b/.playwright-mcp/console-2026-07-11T11-10-38-930Z.log new file mode 100644 index 00000000..d704af3c --- /dev/null +++ b/.playwright-mcp/console-2026-07-11T11-10-38-930Z.log @@ -0,0 +1 @@ +[ 56ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-fashion.test/_boost/browser-logs @ http://acme-fashion.test/account/login:56 diff --git a/.playwright-mcp/console-2026-07-11T11-12-12-689Z.log b/.playwright-mcp/console-2026-07-11T11-12-12-689Z.log new file mode 100644 index 00000000..b671f953 --- /dev/null +++ b/.playwright-mcp/console-2026-07-11T11-12-12-689Z.log @@ -0,0 +1,2 @@ +[ 65ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-fashion.test/_boost/browser-logs @ http://acme-fashion.test/account:56 +[ 6103ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-fashion.test/_boost/browser-logs @ http://acme-fashion.test/account/orders/1:56 diff --git a/.playwright-mcp/console-2026-07-11T11-12-27-893Z.log b/.playwright-mcp/console-2026-07-11T11-12-27-893Z.log new file mode 100644 index 00000000..0a5b6fb6 --- /dev/null +++ b/.playwright-mcp/console-2026-07-11T11-12-27-893Z.log @@ -0,0 +1 @@ +[ 149ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-electronics.test/_boost/browser-logs @ http://acme-electronics.test/:56 diff --git a/.playwright-mcp/console-2026-07-11T11-13-17-355Z.log b/.playwright-mcp/console-2026-07-11T11-13-17-355Z.log new file mode 100644 index 00000000..5f1a577a --- /dev/null +++ b/.playwright-mcp/console-2026-07-11T11-13-17-355Z.log @@ -0,0 +1 @@ +[ 91ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-electronics.test/_boost/browser-logs @ http://acme-electronics.test/:56 diff --git a/.playwright-mcp/console-2026-07-11T11-24-36-090Z.log b/.playwright-mcp/console-2026-07-11T11-24-36-090Z.log new file mode 100644 index 00000000..2337dc8f --- /dev/null +++ b/.playwright-mcp/console-2026-07-11T11-24-36-090Z.log @@ -0,0 +1 @@ +[ 210ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-fashion.test/_boost/browser-logs @ http://acme-fashion.test/:56 diff --git a/.playwright-mcp/console-2026-07-11T11-24-36-383Z.log b/.playwright-mcp/console-2026-07-11T11-24-36-383Z.log new file mode 100644 index 00000000..172d9e91 --- /dev/null +++ b/.playwright-mcp/console-2026-07-11T11-24-36-383Z.log @@ -0,0 +1 @@ +[ 67ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-fashion.test/_boost/browser-logs @ http://acme-fashion.test/dashboard:51 diff --git a/.playwright-mcp/console-2026-07-11T11-25-54-877Z.log b/.playwright-mcp/console-2026-07-11T11-25-54-877Z.log new file mode 100644 index 00000000..35bcd48f --- /dev/null +++ b/.playwright-mcp/console-2026-07-11T11-25-54-877Z.log @@ -0,0 +1 @@ +[ 95ms] [LOG] 🔍 Browser logger active (MCP server detected). Posting to: http://acme-fashion.test/_boost/browser-logs @ http://acme-fashion.test/admin:50 diff --git a/.playwright-mcp/page-2026-07-11T11-01-40-796Z.yml b/.playwright-mcp/page-2026-07-11T11-01-40-796Z.yml new file mode 100644 index 00000000..630f7911 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-01-40-796Z.yml @@ -0,0 +1,131 @@ +- generic [active] [ref=e1]: + - link "Skip to main content" [ref=e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=e3]: Free shipping available with code FREESHIP + - banner [ref=e4]: + - generic [ref=e5]: + - link "Acme Fashion" [ref=e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=e7]: + - link "Home" [ref=e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=e12]: + - button "Open product search" [ref=e14]: + - generic [ref=e19]: Search + - button "Open shopping cart" [ref=e21]: + - generic [ref=e26]: Cart + - main [ref=e27]: + - generic [ref=e28]: + - generic [ref=e31]: + - generic [ref=e32]: New season + - heading "Find your next favorite." [level=1] [ref=e33] + - paragraph [ref=e34]: Explore curated essentials from Acme Fashion. + - link "Shop collections" [ref=e36] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - generic [ref=e38]: + - generic [ref=e39]: + - generic [ref=e40]: + - paragraph [ref=e41]: Explore + - heading "Featured collections" [level=2] [ref=e42] + - link "View all" [ref=e43] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - generic [ref=e44]: + - link [ref=e45] [cursor=pointer]: + - /url: http://acme-fashion.test/collections/new-arrivals + - heading "New Arrivals" [level=3] [ref=e46] + - paragraph [ref=e47]: 7 products + - link [ref=e48] [cursor=pointer]: + - /url: http://acme-fashion.test/collections/t-shirts + - heading "T-Shirts" [level=3] [ref=e49] + - paragraph [ref=e50]: 4 products + - link [ref=e51] [cursor=pointer]: + - /url: http://acme-fashion.test/collections/pants-jeans + - heading "Pants & Jeans" [level=3] [ref=e52] + - paragraph [ref=e53]: 4 products + - generic [ref=e54]: + - heading "Featured products" [level=2] [ref=e55] + - generic [ref=e56]: + - article [ref=e57]: + - link "Cashmere Overcoat image placeholder Cashmere Overcoat €499.99" [ref=e58] [cursor=pointer]: + - /url: http://acme-fashion.test/products/cashmere-overcoat + - img "Cashmere Overcoat image placeholder" [ref=e59]: + - generic [ref=e60]: Cashmere Overcoat + - generic [ref=e61]: + - heading "Cashmere Overcoat" [level=2] [ref=e62] + - generic [ref=e63]: €499.99 + - article [ref=e65]: + - link "Gift Card image placeholder Gift Card €25.00" [ref=e66] [cursor=pointer]: + - /url: http://acme-fashion.test/products/gift-card + - img "Gift Card image placeholder" [ref=e67]: + - generic [ref=e68]: Gift Card + - generic [ref=e69]: + - heading "Gift Card" [level=2] [ref=e70] + - generic [ref=e71]: €25.00 + - article [ref=e73]: + - link "Backorder Denim Jacket image placeholder Backorder Denim Jacket €99.99" [ref=e74] [cursor=pointer]: + - /url: http://acme-fashion.test/products/backorder-denim-jacket + - img "Backorder Denim Jacket image placeholder" [ref=e75]: + - generic [ref=e76]: Backorder Denim Jacket + - generic [ref=e77]: + - heading "Backorder Denim Jacket" [level=2] [ref=e78] + - generic [ref=e79]: €99.99 + - article [ref=e81]: + - link "Limited Edition Sneakers image placeholder Limited Edition Sneakers €159.99" [ref=e82] [cursor=pointer]: + - /url: http://acme-fashion.test/products/limited-edition-sneakers + - img "Limited Edition Sneakers image placeholder" [ref=e83]: + - generic [ref=e84]: Limited Edition Sneakers + - generic [ref=e85]: + - heading "Limited Edition Sneakers" [level=2] [ref=e86] + - generic [ref=e87]: €159.99 + - article [ref=e89]: + - link "Bucket Hat image placeholder Bucket Hat €24.99" [ref=e90] [cursor=pointer]: + - /url: http://acme-fashion.test/products/bucket-hat + - img "Bucket Hat image placeholder" [ref=e91]: + - generic [ref=e92]: Bucket Hat + - generic [ref=e93]: + - heading "Bucket Hat" [level=2] [ref=e94] + - generic [ref=e95]: €24.99 + - article [ref=e97]: + - link "Canvas Tote Bag image placeholder Canvas Tote Bag €19.99" [ref=e98] [cursor=pointer]: + - /url: http://acme-fashion.test/products/canvas-tote-bag + - img "Canvas Tote Bag image placeholder" [ref=e99]: + - generic [ref=e100]: Canvas Tote Bag + - generic [ref=e101]: + - heading "Canvas Tote Bag" [level=2] [ref=e102] + - generic [ref=e103]: €19.99 + - article [ref=e105]: + - link "Wool Scarf image placeholder Wool Scarf €29.99" [ref=e106] [cursor=pointer]: + - /url: http://acme-fashion.test/products/wool-scarf + - img "Wool Scarf image placeholder" [ref=e107]: + - generic [ref=e108]: Wool Scarf + - generic [ref=e109]: + - heading "Wool Scarf" [level=2] [ref=e110] + - generic [ref=e111]: €29.99 + - article [ref=e113]: + - link "Wide Leg Trousers image placeholder Wide Leg Trousers €49.99 €69.99 Sale price" [ref=e114] [cursor=pointer]: + - /url: http://acme-fashion.test/products/wide-leg-trousers + - img "Wide Leg Trousers image placeholder" [ref=e115]: + - generic [ref=e116]: Wide Leg Trousers + - generic [ref=e117]: + - heading "Wide Leg Trousers" [level=2] [ref=e118] + - generic [ref=e119]: + - generic [ref=e120]: €49.99 + - generic [ref=e121]: €69.99 + - generic [ref=e122]: Sale price + - contentinfo [ref=e123]: + - generic [ref=e124]: + - generic [ref=e125]: + - paragraph [ref=e126]: Acme Fashion + - paragraph [ref=e127]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=e128]: + - link "Shop all collections" [ref=e129] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=e130] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=e131]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-02-45-664Z.yml b/.playwright-mcp/page-2026-07-11T11-02-45-664Z.yml new file mode 100644 index 00000000..630f7911 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-02-45-664Z.yml @@ -0,0 +1,131 @@ +- generic [active] [ref=e1]: + - link "Skip to main content" [ref=e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=e3]: Free shipping available with code FREESHIP + - banner [ref=e4]: + - generic [ref=e5]: + - link "Acme Fashion" [ref=e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=e7]: + - link "Home" [ref=e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=e12]: + - button "Open product search" [ref=e14]: + - generic [ref=e19]: Search + - button "Open shopping cart" [ref=e21]: + - generic [ref=e26]: Cart + - main [ref=e27]: + - generic [ref=e28]: + - generic [ref=e31]: + - generic [ref=e32]: New season + - heading "Find your next favorite." [level=1] [ref=e33] + - paragraph [ref=e34]: Explore curated essentials from Acme Fashion. + - link "Shop collections" [ref=e36] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - generic [ref=e38]: + - generic [ref=e39]: + - generic [ref=e40]: + - paragraph [ref=e41]: Explore + - heading "Featured collections" [level=2] [ref=e42] + - link "View all" [ref=e43] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - generic [ref=e44]: + - link [ref=e45] [cursor=pointer]: + - /url: http://acme-fashion.test/collections/new-arrivals + - heading "New Arrivals" [level=3] [ref=e46] + - paragraph [ref=e47]: 7 products + - link [ref=e48] [cursor=pointer]: + - /url: http://acme-fashion.test/collections/t-shirts + - heading "T-Shirts" [level=3] [ref=e49] + - paragraph [ref=e50]: 4 products + - link [ref=e51] [cursor=pointer]: + - /url: http://acme-fashion.test/collections/pants-jeans + - heading "Pants & Jeans" [level=3] [ref=e52] + - paragraph [ref=e53]: 4 products + - generic [ref=e54]: + - heading "Featured products" [level=2] [ref=e55] + - generic [ref=e56]: + - article [ref=e57]: + - link "Cashmere Overcoat image placeholder Cashmere Overcoat €499.99" [ref=e58] [cursor=pointer]: + - /url: http://acme-fashion.test/products/cashmere-overcoat + - img "Cashmere Overcoat image placeholder" [ref=e59]: + - generic [ref=e60]: Cashmere Overcoat + - generic [ref=e61]: + - heading "Cashmere Overcoat" [level=2] [ref=e62] + - generic [ref=e63]: €499.99 + - article [ref=e65]: + - link "Gift Card image placeholder Gift Card €25.00" [ref=e66] [cursor=pointer]: + - /url: http://acme-fashion.test/products/gift-card + - img "Gift Card image placeholder" [ref=e67]: + - generic [ref=e68]: Gift Card + - generic [ref=e69]: + - heading "Gift Card" [level=2] [ref=e70] + - generic [ref=e71]: €25.00 + - article [ref=e73]: + - link "Backorder Denim Jacket image placeholder Backorder Denim Jacket €99.99" [ref=e74] [cursor=pointer]: + - /url: http://acme-fashion.test/products/backorder-denim-jacket + - img "Backorder Denim Jacket image placeholder" [ref=e75]: + - generic [ref=e76]: Backorder Denim Jacket + - generic [ref=e77]: + - heading "Backorder Denim Jacket" [level=2] [ref=e78] + - generic [ref=e79]: €99.99 + - article [ref=e81]: + - link "Limited Edition Sneakers image placeholder Limited Edition Sneakers €159.99" [ref=e82] [cursor=pointer]: + - /url: http://acme-fashion.test/products/limited-edition-sneakers + - img "Limited Edition Sneakers image placeholder" [ref=e83]: + - generic [ref=e84]: Limited Edition Sneakers + - generic [ref=e85]: + - heading "Limited Edition Sneakers" [level=2] [ref=e86] + - generic [ref=e87]: €159.99 + - article [ref=e89]: + - link "Bucket Hat image placeholder Bucket Hat €24.99" [ref=e90] [cursor=pointer]: + - /url: http://acme-fashion.test/products/bucket-hat + - img "Bucket Hat image placeholder" [ref=e91]: + - generic [ref=e92]: Bucket Hat + - generic [ref=e93]: + - heading "Bucket Hat" [level=2] [ref=e94] + - generic [ref=e95]: €24.99 + - article [ref=e97]: + - link "Canvas Tote Bag image placeholder Canvas Tote Bag €19.99" [ref=e98] [cursor=pointer]: + - /url: http://acme-fashion.test/products/canvas-tote-bag + - img "Canvas Tote Bag image placeholder" [ref=e99]: + - generic [ref=e100]: Canvas Tote Bag + - generic [ref=e101]: + - heading "Canvas Tote Bag" [level=2] [ref=e102] + - generic [ref=e103]: €19.99 + - article [ref=e105]: + - link "Wool Scarf image placeholder Wool Scarf €29.99" [ref=e106] [cursor=pointer]: + - /url: http://acme-fashion.test/products/wool-scarf + - img "Wool Scarf image placeholder" [ref=e107]: + - generic [ref=e108]: Wool Scarf + - generic [ref=e109]: + - heading "Wool Scarf" [level=2] [ref=e110] + - generic [ref=e111]: €29.99 + - article [ref=e113]: + - link "Wide Leg Trousers image placeholder Wide Leg Trousers €49.99 €69.99 Sale price" [ref=e114] [cursor=pointer]: + - /url: http://acme-fashion.test/products/wide-leg-trousers + - img "Wide Leg Trousers image placeholder" [ref=e115]: + - generic [ref=e116]: Wide Leg Trousers + - generic [ref=e117]: + - heading "Wide Leg Trousers" [level=2] [ref=e118] + - generic [ref=e119]: + - generic [ref=e120]: €49.99 + - generic [ref=e121]: €69.99 + - generic [ref=e122]: Sale price + - contentinfo [ref=e123]: + - generic [ref=e124]: + - generic [ref=e125]: + - paragraph [ref=e126]: Acme Fashion + - paragraph [ref=e127]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=e128]: + - link "Shop all collections" [ref=e129] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=e130] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=e131]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-02-54-726Z.yml b/.playwright-mcp/page-2026-07-11T11-02-54-726Z.yml new file mode 100644 index 00000000..d9950a43 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-02-54-726Z.yml @@ -0,0 +1,98 @@ +- generic [active] [ref=f1e1]: + - link "Skip to main content" [ref=f1e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=f1e3]: Free shipping available with code FREESHIP + - banner [ref=f1e4]: + - generic [ref=f1e5]: + - link "Acme Fashion" [ref=f1e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=f1e7]: + - link "Home" [ref=f1e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=f1e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f1e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=f1e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=f1e12]: + - button "Open product search" [ref=f1e14]: + - generic [ref=f1e19]: Search + - button "Open shopping cart" [ref=f1e21]: + - generic [ref=f1e26]: Cart + - main [ref=f1e27]: + - generic [ref=f1e28]: + - navigation "Breadcrumb" [ref=f1e29]: + - list [ref=f1e30]: + - listitem [ref=f1e31]: + - link "Home" [ref=f1e32] [cursor=pointer]: + - /url: http://acme-fashion.test + - generic [ref=f1e33]: / + - listitem [ref=f1e34]: + - link "Products" [ref=f1e35] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - generic [ref=f1e36]: / + - listitem [ref=f1e37]: + - generic [ref=f1e38]: Classic Cotton T-Shirt + - generic [ref=f1e39]: + - img "Classic Cotton T-Shirt" [ref=f1e40] + - generic [ref=f1e41]: + - generic [ref=f1e42]: + - paragraph [ref=f1e43]: Acme Basics + - heading "Classic Cotton T-Shirt" [level=1] [ref=f1e44] + - generic [ref=f1e45]: €24.99 + - group "Choose a variant" [ref=f1e48]: + - generic [ref=f1e50]: + - generic [ref=f1e51] [cursor=pointer]: + - generic [ref=f1e52]: ACME-CLASSICC-S-WHITE-1 + - radio "Select variant ACME-CLASSICC-S-WHITE-1" [checked] [ref=f1e53] + - generic [ref=f1e54] [cursor=pointer]: + - generic [ref=f1e55]: ACME-CLASSICC-S-BLACK-2 + - radio "Select variant ACME-CLASSICC-S-BLACK-2" [ref=f1e56] + - generic [ref=f1e57] [cursor=pointer]: + - generic [ref=f1e58]: ACME-CLASSICC-S-NAVY-3 + - radio "Select variant ACME-CLASSICC-S-NAVY-3" [ref=f1e59] + - generic [ref=f1e60] [cursor=pointer]: + - generic [ref=f1e61]: ACME-CLASSICC-M-WHITE-4 + - radio "Select variant ACME-CLASSICC-M-WHITE-4" [ref=f1e62] + - generic [ref=f1e63] [cursor=pointer]: + - generic [ref=f1e64]: ACME-CLASSICC-M-BLACK-5 + - radio "Select variant ACME-CLASSICC-M-BLACK-5" [ref=f1e65] + - generic [ref=f1e66] [cursor=pointer]: + - generic [ref=f1e67]: ACME-CLASSICC-M-NAVY-6 + - radio "Select variant ACME-CLASSICC-M-NAVY-6" [ref=f1e68] + - generic [ref=f1e69] [cursor=pointer]: + - generic [ref=f1e70]: ACME-CLASSICC-L-WHITE-7 + - radio "Select variant ACME-CLASSICC-L-WHITE-7" [ref=f1e71] + - generic [ref=f1e72] [cursor=pointer]: + - generic [ref=f1e73]: ACME-CLASSICC-L-BLACK-8 + - radio "Select variant ACME-CLASSICC-L-BLACK-8" [ref=f1e74] + - generic [ref=f1e75] [cursor=pointer]: + - generic [ref=f1e76]: ACME-CLASSICC-L-NAVY-9 + - radio "Select variant ACME-CLASSICC-L-NAVY-9" [ref=f1e77] + - generic [ref=f1e78] [cursor=pointer]: + - generic [ref=f1e79]: ACME-CLASSICC-XL-WHITE-10 + - radio "Select variant ACME-CLASSICC-XL-WHITE-10" [ref=f1e80] + - generic [ref=f1e81] [cursor=pointer]: + - generic [ref=f1e82]: ACME-CLASSICC-XL-BLACK-11 + - radio "Select variant ACME-CLASSICC-XL-BLACK-11" [ref=f1e83] + - generic [ref=f1e84] [cursor=pointer]: + - generic [ref=f1e85]: ACME-CLASSICC-XL-NAVY-12 + - radio "Select variant ACME-CLASSICC-XL-NAVY-12" [ref=f1e86] + - paragraph [ref=f1e87]: In stock + - generic [ref=f1e88]: + - generic [ref=f1e89]: Quantity + - spinbutton "Quantity" [ref=f1e91]: "1" + - button "Add to cart" [ref=f1e92] + - paragraph [ref=f1e99]: A timeless classic cotton t-shirt. Comfortable, breathable, and perfect for everyday wear. + - contentinfo [ref=f1e100]: + - generic [ref=f1e101]: + - generic [ref=f1e102]: + - paragraph [ref=f1e103]: Acme Fashion + - paragraph [ref=f1e104]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=f1e105]: + - link "Shop all collections" [ref=f1e106] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f1e107] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=f1e108]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-03-03-373Z.yml b/.playwright-mcp/page-2026-07-11T11-03-03-373Z.yml new file mode 100644 index 00000000..2f650104 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-03-03-373Z.yml @@ -0,0 +1,260 @@ +- generic [active] [ref=f1e1]: + - dialog [ref=f1e109]: + - iframe [ref=f1e110]: + - generic [ref=f2e2]: + - generic [ref=f2e4]: + - generic [ref=f2e5]: Internal Server Error + - button "Copy as Markdown" [ref=f2e11] [cursor=pointer] + - generic [ref=f2e18]: + - generic [ref=f2e19]: + - heading "Illuminate\\Contracts\\Container\\BindingResolutionException" [level=1] [ref=f2e20] + - generic [ref=f2e21]: vendor/laravel/framework/src/Illuminate/Container/Container.php:1124 + - paragraph [ref=f2e23]: Target class [current_store] does not exist. + - generic [ref=f2e24]: + - generic [ref=f2e25]: + - generic [ref=f2e26]: + - generic [ref=f2e27]: LARAVEL + - generic [ref=f2e28]: 12.51.0 + - generic [ref=f2e29]: + - generic [ref=f2e30]: PHP + - generic [ref=f2e31]: 8.4.17 + - generic [ref=f2e32]: UNHANDLED + - generic [ref=f2e36]: CODE 0 + - generic [ref=f2e38]: + - generic [ref=f2e39]: "500" + - generic [ref=f2e43]: POST + - generic [ref=f2e47]: http://acme-fashion.test/livewire-6701cc17/update + - button [ref=f2e48] [cursor=pointer] + - generic [ref=f2e53]: + - generic [ref=f2e54]: + - heading "Exception trace" [level=3] [ref=f2e60] + - generic [ref=f2e61]: + - generic [ref=f2e63] [cursor=pointer]: + - generic [ref=f2e68]: 6 vendor frames + - button [ref=f2e69] + - generic [ref=f2e74]: + - generic [ref=f2e75] [cursor=pointer]: + - generic [ref=f2e78]: + - code [ref=f2e82]: + - generic [ref=f2e83]: app/Livewire/Storefront/Products/Show.php + - generic [ref=f2e84]: app/Livewire/Storefront/Products/Show.php:34 + - button [ref=f2e87] + - code [ref=f2e96]: + - generic [ref=f2e97]: "29 {" + - generic [ref=f2e98]: 30 $this->validate(['selectedVariantId' => ['required', 'integer'], 'quantity' => ['required', 'integer', 'min:1']]); + - generic [ref=f2e99]: 31 $variant = $this->product->variants->firstWhere('id', $this->selectedVariantId); + - generic [ref=f2e100]: 32 abort_unless($variant instanceof ProductVariant, 404); + - generic [ref=f2e101]: 33 $customer = auth('customer')->user(); + - generic [ref=f2e102]: 34 $cart = $carts->getOrCreateForSession(app('current_store'), $customer); + - generic [ref=f2e103]: 35 $carts->addLine($cart, $variant->id, $this->quantity); + - generic [ref=f2e104]: 36 session()->flash('storefront_status', 'Added to cart'); + - generic [ref=f2e105]: 37 $this->dispatch('cart-updated'); + - generic [ref=f2e106]: "38 }" + - generic [ref=f2e107]: "39" + - generic [ref=f2e108]: "40 public function render(): View" + - generic [ref=f2e109]: "41 {" + - generic [ref=f2e110]: 42 return view('livewire.storefront.products.show', [ + - generic [ref=f2e111]: 43 'selectedVariant' => $this->product->variants->firstWhere('id', $this->selectedVariantId), + - generic [ref=f2e112]: 44 ])->layout('layouts.storefront', ['title' => $this->product->title.' - '.app('current_store')->name]); + - generic [ref=f2e113]: "45 }" + - generic [ref=f2e114]: "46" + - generic [ref=f2e116] [cursor=pointer]: + - generic [ref=f2e121]: 58 vendor frames + - button [ref=f2e122] + - generic [ref=f2e128] [cursor=pointer]: + - generic [ref=f2e131]: + - code [ref=f2e135]: + - generic [ref=f2e136]: public/index.php + - generic [ref=f2e137]: public/index.php:20 + - button [ref=f2e140] + - generic [ref=f2e146] [cursor=pointer]: + - generic [ref=f2e151]: 1 vendor frame + - button [ref=f2e152] + - generic [ref=f2e157]: + - generic [ref=f2e158]: + - heading "Queries" [level=3] [ref=f2e163] + - generic [ref=f2e164]: 1-2 of 2 + - generic [ref=f2e166]: + - generic [ref=f2e167]: + - generic [ref=f2e168]: + - generic [ref=f2e169]: sqlite + - code [ref=f2e176]: + - generic [ref=f2e177]: select * from "products" where "products"."id" = 1 limit 1 + - generic [ref=f2e178]: 2.29ms + - generic [ref=f2e179]: + - generic [ref=f2e180]: + - generic [ref=f2e181]: sqlite + - code [ref=f2e188]: + - generic [ref=f2e189]: select * from "product_variants" where "product_variants"."product_id" = 1 and "product_variants"."product_id" is not null order by "position" asc + - generic [ref=f2e190]: 0.34ms + - generic [ref=f2e192]: + - generic [ref=f2e193]: + - heading "Headers" [level=2] [ref=f2e194] + - generic [ref=f2e195]: + - generic [ref=f2e196]: + - generic [ref=f2e197]: cookie + - generic [ref=f2e199]: XSRF-TOKEN=eyJpdiI6IkxDU3lEMmg4KzhRNm42U1k0RGxIZnc9PSIsInZhbHVlIjoiZGwzMmlkK3MwekJWVDI3RmhtNG1ud2lWVW85TzZtT1RHV2NEMGt0Ry9vQ2graVAzUkN0VXhRemZoc0F1MEZZOXZucG1iMmxFZ1Y3YXRIcFl2eG43Q2JxYUkyUXdlaHc5Z3cxeWVJS2tuQm9lbG1UTy9KMmVYeGgycks5MDNEejciLCJtYWMiOiI1YTg5NWNmYTE0NGZmNGUyOTQ2OTRjMzBjZTJmMjI5OWRlOTMxNWExYTZlMGZiZDFlZThjZjA0ZGU4YjMxNzJkIiwidGFnIjoiIn0%3D; shop_session=eyJpdiI6IjhzeldZb2tJeU9yODNkN3lUU1V1WXc9PSIsInZhbHVlIjoiWVdNUkpFQTNSSEVYa25ZTlc5VUc1NXJieVJLMTh5VDg5RzZhaHhwRFJtWmFmT0M2MzJZbDM2dHV5OTJBek9NTXdvTjBNLzdhdTdPWlZkT0NkNTBQdXFTTGNuK3J2ejJkWHVhZ1ZEM2k4S0xpNWE4RGg4eXR3RldZbGpza2FvVmMiLCJtYWMiOiI5MWNmNWMwZmY5NjQ1ZWY3ZDg4MDdlZTkxZGNhNzdmNTdlYjIyYjg5ODg3Y2I4YWY0ZmI1ZTcwOTVjMjE2NTIxIiwidGFnIjoiIn0%3D + - generic [ref=f2e200]: + - generic [ref=f2e201]: accept-language + - generic [ref=f2e203]: en-GB,en-US;q=0.9,en;q=0.8 + - generic [ref=f2e204]: + - generic [ref=f2e205]: accept-encoding + - generic [ref=f2e207]: gzip, deflate + - generic [ref=f2e208]: + - generic [ref=f2e209]: referer + - generic [ref=f2e211]: http://acme-fashion.test/products/classic-cotton-t-shirt + - generic [ref=f2e212]: + - generic [ref=f2e213]: origin + - generic [ref=f2e215]: http://acme-fashion.test + - generic [ref=f2e216]: + - generic [ref=f2e217]: accept + - generic [ref=f2e219]: "*/*" + - generic [ref=f2e220]: + - generic [ref=f2e221]: x-livewire + - generic [ref=f2e223]: "1" + - generic [ref=f2e224]: + - generic [ref=f2e225]: content-type + - generic [ref=f2e227]: application/json + - generic [ref=f2e228]: + - generic [ref=f2e229]: user-agent + - generic [ref=f2e231]: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 + - generic [ref=f2e232]: + - generic [ref=f2e233]: content-length + - generic [ref=f2e235]: "634" + - generic [ref=f2e236]: + - generic [ref=f2e237]: connection + - generic [ref=f2e239]: keep-alive + - generic [ref=f2e240]: + - generic [ref=f2e241]: host + - generic [ref=f2e243]: acme-fashion.test + - generic [ref=f2e244]: + - heading "Body" [level=2] [ref=f2e245] + - code [ref=f2e250]: + - generic [ref=f2e251]: "{" + - generic [ref=f2e252]: "\"_token\": \"BLvnxGmt1x5SpbfmG3dDyA7evVNRFS6OHjX7PUMY\"," + - generic [ref=f2e253]: "\"components\": [" + - generic [ref=f2e254]: "{" + - generic [ref=f2e255]: "\"snapshot\": \"{\"data\":{\"product\":[null,{\"class\":\"AppModelsProduct\",\"key\":1,\"s\":\"mdl\"}],\"selectedVariantId\":1,\"quantity\":1},\"memo\":{\"id\":\"QOGOz2vMLHz5tBvp2JxQ\",\"name\":\"storefront.products.show\",\"path\":\"products/classic-cotton-t-shirt\",\"method\":\"GET\",\"release\":\"a-a-a\",\"children\":[],\"scripts\":[],\"assets\":[],\"errors\":[],\"locale\":\"en\",\"islands\":[]},\"checksum\":\"afd9c7c4f72b2530e8844a6f625f168e6e1baa8d50bfd65eace590a5fb0887ca\"}\"," + - generic [ref=f2e256]: "\"updates\": []," + - generic [ref=f2e257]: "\"calls\": [" + - generic [ref=f2e258]: "{" + - generic [ref=f2e259]: "\"method\": \"addToCart\"," + - generic [ref=f2e260]: "\"params\": []," + - generic [ref=f2e261]: "\"metadata\": []" + - generic [ref=f2e262]: "}" + - generic [ref=f2e263]: "]" + - generic [ref=f2e264]: "}" + - generic [ref=f2e265]: "]" + - generic [ref=f2e266]: "}" + - generic [ref=f2e267]: + - heading "Routing" [level=2] [ref=f2e268] + - generic [ref=f2e269]: + - generic [ref=f2e270]: + - generic [ref=f2e271]: controller + - generic [ref=f2e273]: Livewire\Mechanisms\HandleRequests\HandleRequests@handleUpdate + - generic [ref=f2e274]: + - generic [ref=f2e275]: route name + - generic [ref=f2e277]: default-livewire.update + - generic [ref=f2e278]: + - generic [ref=f2e279]: middleware + - generic [ref=f2e281]: web + - generic [ref=f2e282]: + - heading "Routing parameters" [level=2] [ref=f2e283] + - generic [ref=f2e284]: // No routing parameters + - link "Skip to main content" [ref=f1e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=f1e3]: Free shipping available with code FREESHIP + - banner [ref=f1e4]: + - generic [ref=f1e5]: + - link "Acme Fashion" [ref=f1e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=f1e7]: + - link "Home" [ref=f1e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=f1e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f1e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=f1e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=f1e12]: + - button "Open product search" [ref=f1e14]: + - generic [ref=f1e19]: Search + - button "Open shopping cart" [ref=f1e21]: + - generic [ref=f1e26]: Cart + - main [ref=f1e27]: + - generic [ref=f1e28]: + - navigation "Breadcrumb" [ref=f1e29]: + - list [ref=f1e30]: + - listitem [ref=f1e31]: + - link "Home" [ref=f1e32] [cursor=pointer]: + - /url: http://acme-fashion.test + - generic [ref=f1e33]: / + - listitem [ref=f1e34]: + - link "Products" [ref=f1e35] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - generic [ref=f1e36]: / + - listitem [ref=f1e37]: + - generic [ref=f1e38]: Classic Cotton T-Shirt + - generic [ref=f1e39]: + - img "Classic Cotton T-Shirt" [ref=f1e40] + - generic [ref=f1e41]: + - generic [ref=f1e42]: + - paragraph [ref=f1e43]: Acme Basics + - heading "Classic Cotton T-Shirt" [level=1] [ref=f1e44] + - generic [ref=f1e45]: €24.99 + - group "Choose a variant" [ref=f1e48]: + - generic [ref=f1e50]: + - generic [ref=f1e51] [cursor=pointer]: + - generic [ref=f1e52]: ACME-CLASSICC-S-WHITE-1 + - radio "Select variant ACME-CLASSICC-S-WHITE-1" [checked] [ref=f1e53] + - generic [ref=f1e54] [cursor=pointer]: + - generic [ref=f1e55]: ACME-CLASSICC-S-BLACK-2 + - radio "Select variant ACME-CLASSICC-S-BLACK-2" [ref=f1e56] + - generic [ref=f1e57] [cursor=pointer]: + - generic [ref=f1e58]: ACME-CLASSICC-S-NAVY-3 + - radio "Select variant ACME-CLASSICC-S-NAVY-3" [ref=f1e59] + - generic [ref=f1e60] [cursor=pointer]: + - generic [ref=f1e61]: ACME-CLASSICC-M-WHITE-4 + - radio "Select variant ACME-CLASSICC-M-WHITE-4" [ref=f1e62] + - generic [ref=f1e63] [cursor=pointer]: + - generic [ref=f1e64]: ACME-CLASSICC-M-BLACK-5 + - radio "Select variant ACME-CLASSICC-M-BLACK-5" [ref=f1e65] + - generic [ref=f1e66] [cursor=pointer]: + - generic [ref=f1e67]: ACME-CLASSICC-M-NAVY-6 + - radio "Select variant ACME-CLASSICC-M-NAVY-6" [ref=f1e68] + - generic [ref=f1e69] [cursor=pointer]: + - generic [ref=f1e70]: ACME-CLASSICC-L-WHITE-7 + - radio "Select variant ACME-CLASSICC-L-WHITE-7" [ref=f1e71] + - generic [ref=f1e72] [cursor=pointer]: + - generic [ref=f1e73]: ACME-CLASSICC-L-BLACK-8 + - radio "Select variant ACME-CLASSICC-L-BLACK-8" [ref=f1e74] + - generic [ref=f1e75] [cursor=pointer]: + - generic [ref=f1e76]: ACME-CLASSICC-L-NAVY-9 + - radio "Select variant ACME-CLASSICC-L-NAVY-9" [ref=f1e77] + - generic [ref=f1e78] [cursor=pointer]: + - generic [ref=f1e79]: ACME-CLASSICC-XL-WHITE-10 + - radio "Select variant ACME-CLASSICC-XL-WHITE-10" [ref=f1e80] + - generic [ref=f1e81] [cursor=pointer]: + - generic [ref=f1e82]: ACME-CLASSICC-XL-BLACK-11 + - radio "Select variant ACME-CLASSICC-XL-BLACK-11" [ref=f1e83] + - generic [ref=f1e84] [cursor=pointer]: + - generic [ref=f1e85]: ACME-CLASSICC-XL-NAVY-12 + - radio "Select variant ACME-CLASSICC-XL-NAVY-12" [ref=f1e86] + - paragraph [ref=f1e87]: In stock + - generic [ref=f1e88]: + - generic [ref=f1e89]: Quantity + - spinbutton "Quantity" [ref=f1e91]: "1" + - button "Add to cart" [ref=f1e92] + - paragraph [ref=f1e99]: A timeless classic cotton t-shirt. Comfortable, breathable, and perfect for everyday wear. + - contentinfo [ref=f1e100]: + - generic [ref=f1e101]: + - generic [ref=f1e102]: + - paragraph [ref=f1e103]: Acme Fashion + - paragraph [ref=f1e104]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=f1e105]: + - link "Shop all collections" [ref=f1e106] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f1e107] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=f1e108]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-04-12-885Z.yml b/.playwright-mcp/page-2026-07-11T11-04-12-885Z.yml new file mode 100644 index 00000000..702c3446 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-04-12-885Z.yml @@ -0,0 +1,98 @@ +- generic [active] [ref=e1]: + - link "Skip to main content" [ref=e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=e3]: Free shipping available with code FREESHIP + - banner [ref=e4]: + - generic [ref=e5]: + - link "Acme Fashion" [ref=e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=e7]: + - link "Home" [ref=e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=e12]: + - button "Open product search" [ref=e14]: + - generic [ref=e19]: Search + - button "Open shopping cart" [ref=e21]: + - generic [ref=e26]: Cart + - main [ref=e27]: + - generic [ref=e28]: + - navigation "Breadcrumb" [ref=e29]: + - list [ref=e30]: + - listitem [ref=e31]: + - link "Home" [ref=e32] [cursor=pointer]: + - /url: http://acme-fashion.test + - generic [ref=e33]: / + - listitem [ref=e34]: + - link "Products" [ref=e35] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - generic [ref=e36]: / + - listitem [ref=e37]: + - generic [ref=e38]: Classic Cotton T-Shirt + - generic [ref=e39]: + - img "Classic Cotton T-Shirt" [ref=e40] + - generic [ref=e41]: + - generic [ref=e42]: + - paragraph [ref=e43]: Acme Basics + - heading "Classic Cotton T-Shirt" [level=1] [ref=e44] + - generic [ref=e45]: €24.99 + - group "Choose a variant" [ref=e48]: + - generic [ref=e50]: + - generic [ref=e51] [cursor=pointer]: + - generic [ref=e52]: ACME-CLASSICC-S-WHITE-1 + - radio "Select variant ACME-CLASSICC-S-WHITE-1" [checked] [ref=e53] + - generic [ref=e54] [cursor=pointer]: + - generic [ref=e55]: ACME-CLASSICC-S-BLACK-2 + - radio "Select variant ACME-CLASSICC-S-BLACK-2" [ref=e56] + - generic [ref=e57] [cursor=pointer]: + - generic [ref=e58]: ACME-CLASSICC-S-NAVY-3 + - radio "Select variant ACME-CLASSICC-S-NAVY-3" [ref=e59] + - generic [ref=e60] [cursor=pointer]: + - generic [ref=e61]: ACME-CLASSICC-M-WHITE-4 + - radio "Select variant ACME-CLASSICC-M-WHITE-4" [ref=e62] + - generic [ref=e63] [cursor=pointer]: + - generic [ref=e64]: ACME-CLASSICC-M-BLACK-5 + - radio "Select variant ACME-CLASSICC-M-BLACK-5" [ref=e65] + - generic [ref=e66] [cursor=pointer]: + - generic [ref=e67]: ACME-CLASSICC-M-NAVY-6 + - radio "Select variant ACME-CLASSICC-M-NAVY-6" [ref=e68] + - generic [ref=e69] [cursor=pointer]: + - generic [ref=e70]: ACME-CLASSICC-L-WHITE-7 + - radio "Select variant ACME-CLASSICC-L-WHITE-7" [ref=e71] + - generic [ref=e72] [cursor=pointer]: + - generic [ref=e73]: ACME-CLASSICC-L-BLACK-8 + - radio "Select variant ACME-CLASSICC-L-BLACK-8" [ref=e74] + - generic [ref=e75] [cursor=pointer]: + - generic [ref=e76]: ACME-CLASSICC-L-NAVY-9 + - radio "Select variant ACME-CLASSICC-L-NAVY-9" [ref=e77] + - generic [ref=e78] [cursor=pointer]: + - generic [ref=e79]: ACME-CLASSICC-XL-WHITE-10 + - radio "Select variant ACME-CLASSICC-XL-WHITE-10" [ref=e80] + - generic [ref=e81] [cursor=pointer]: + - generic [ref=e82]: ACME-CLASSICC-XL-BLACK-11 + - radio "Select variant ACME-CLASSICC-XL-BLACK-11" [ref=e83] + - generic [ref=e84] [cursor=pointer]: + - generic [ref=e85]: ACME-CLASSICC-XL-NAVY-12 + - radio "Select variant ACME-CLASSICC-XL-NAVY-12" [ref=e86] + - paragraph [ref=e87]: In stock + - generic [ref=e88]: + - generic [ref=e89]: Quantity + - spinbutton "Quantity" [ref=e91]: "1" + - button "Add to cart" [ref=e92] + - paragraph [ref=e99]: A timeless classic cotton t-shirt. Comfortable, breathable, and perfect for everyday wear. + - contentinfo [ref=e100]: + - generic [ref=e101]: + - generic [ref=e102]: + - paragraph [ref=e103]: Acme Fashion + - paragraph [ref=e104]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=e105]: + - link "Shop all collections" [ref=e106] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=e107] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=e108]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-04-18-086Z.yml b/.playwright-mcp/page-2026-07-11T11-04-18-086Z.yml new file mode 100644 index 00000000..affd4429 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-04-18-086Z.yml @@ -0,0 +1,110 @@ +- generic [active] [ref=e1]: + - link "Skip to main content" [ref=e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=e3]: Free shipping available with code FREESHIP + - banner [ref=e4]: + - generic [ref=e5]: + - link "Acme Fashion" [ref=e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=e7]: + - link "Home" [ref=e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=e12]: + - button "Open product search" [ref=e14]: + - generic [ref=e19]: Search + - generic [ref=e20]: + - button "Open shopping cart" [ref=e21]: + - generic [ref=e26]: Cart (1) + - dialog [ref=e109]: + - generic [ref=e110]: + - generic [ref=e111]: Shopping cart + - generic [ref=e112]: + - generic [ref=e113]: + - paragraph [ref=e114]: Classic Cotton T-Shirt + - paragraph [ref=e115]: Qty 1 + - button "Remove" [ref=e116] + - link "View cart" [ref=e122] [cursor=pointer]: + - /url: http://acme-fashion.test/cart + - button "Close modal" [ref=e125] + - main [ref=e27]: + - generic [ref=e28]: + - navigation "Breadcrumb" [ref=e29]: + - list [ref=e30]: + - listitem [ref=e31]: + - link "Home" [ref=e32] [cursor=pointer]: + - /url: http://acme-fashion.test + - generic [ref=e33]: / + - listitem [ref=e34]: + - link "Products" [ref=e35] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - generic [ref=e36]: / + - listitem [ref=e37]: + - generic [ref=e38]: Classic Cotton T-Shirt + - generic [ref=e39]: + - img "Classic Cotton T-Shirt" [ref=e40] + - generic [ref=e41]: + - generic [ref=e42]: + - paragraph [ref=e43]: Acme Basics + - heading "Classic Cotton T-Shirt" [level=1] [ref=e44] + - generic [ref=e45]: €24.99 + - group "Choose a variant" [ref=e48]: + - generic [ref=e50]: + - generic [ref=e51] [cursor=pointer]: + - generic [ref=e52]: ACME-CLASSICC-S-WHITE-1 + - radio "Select variant ACME-CLASSICC-S-WHITE-1" [checked] [ref=e53] + - generic [ref=e54] [cursor=pointer]: + - generic [ref=e55]: ACME-CLASSICC-S-BLACK-2 + - radio "Select variant ACME-CLASSICC-S-BLACK-2" [ref=e56] + - generic [ref=e57] [cursor=pointer]: + - generic [ref=e58]: ACME-CLASSICC-S-NAVY-3 + - radio "Select variant ACME-CLASSICC-S-NAVY-3" [ref=e59] + - generic [ref=e60] [cursor=pointer]: + - generic [ref=e61]: ACME-CLASSICC-M-WHITE-4 + - radio "Select variant ACME-CLASSICC-M-WHITE-4" [ref=e62] + - generic [ref=e63] [cursor=pointer]: + - generic [ref=e64]: ACME-CLASSICC-M-BLACK-5 + - radio "Select variant ACME-CLASSICC-M-BLACK-5" [ref=e65] + - generic [ref=e66] [cursor=pointer]: + - generic [ref=e67]: ACME-CLASSICC-M-NAVY-6 + - radio "Select variant ACME-CLASSICC-M-NAVY-6" [ref=e68] + - generic [ref=e69] [cursor=pointer]: + - generic [ref=e70]: ACME-CLASSICC-L-WHITE-7 + - radio "Select variant ACME-CLASSICC-L-WHITE-7" [ref=e71] + - generic [ref=e72] [cursor=pointer]: + - generic [ref=e73]: ACME-CLASSICC-L-BLACK-8 + - radio "Select variant ACME-CLASSICC-L-BLACK-8" [ref=e74] + - generic [ref=e75] [cursor=pointer]: + - generic [ref=e76]: ACME-CLASSICC-L-NAVY-9 + - radio "Select variant ACME-CLASSICC-L-NAVY-9" [ref=e77] + - generic [ref=e78] [cursor=pointer]: + - generic [ref=e79]: ACME-CLASSICC-XL-WHITE-10 + - radio "Select variant ACME-CLASSICC-XL-WHITE-10" [ref=e80] + - generic [ref=e81] [cursor=pointer]: + - generic [ref=e82]: ACME-CLASSICC-XL-BLACK-11 + - radio "Select variant ACME-CLASSICC-XL-BLACK-11" [ref=e83] + - generic [ref=e84] [cursor=pointer]: + - generic [ref=e85]: ACME-CLASSICC-XL-NAVY-12 + - radio "Select variant ACME-CLASSICC-XL-NAVY-12" [ref=e86] + - paragraph [ref=e87]: In stock + - generic [ref=e88]: + - generic [ref=e89]: Quantity + - spinbutton "Quantity" [ref=e91]: "1" + - button "Add to cart" [ref=e92] + - paragraph [ref=e99]: A timeless classic cotton t-shirt. Comfortable, breathable, and perfect for everyday wear. + - contentinfo [ref=e100]: + - generic [ref=e101]: + - generic [ref=e102]: + - paragraph [ref=e103]: Acme Fashion + - paragraph [ref=e104]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=e105]: + - link "Shop all collections" [ref=e106] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=e107] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=e108]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-04-45-784Z.yml b/.playwright-mcp/page-2026-07-11T11-04-45-784Z.yml new file mode 100644 index 00000000..941753d2 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-04-45-784Z.yml @@ -0,0 +1,54 @@ +- generic [active] [ref=f1e1]: + - link "Skip to main content" [ref=f1e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=f1e3]: Free shipping available with code FREESHIP + - banner [ref=f1e4]: + - generic [ref=f1e5]: + - link "Acme Fashion" [ref=f1e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=f1e7]: + - link "Home" [ref=f1e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=f1e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f1e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=f1e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=f1e12]: + - button "Open product search" [ref=f1e14]: + - generic [ref=f1e19]: Search + - button "Open shopping cart" [ref=f1e21]: + - generic [ref=f1e26]: Cart + - main [ref=f1e27]: + - generic [ref=f1e28]: + - heading "Your cart" [level=1] [ref=f1e29] + - generic [ref=f1e30]: + - article [ref=f1e32]: + - generic [ref=f1e34]: + - heading "Classic Cotton T-Shirt" [level=2] [ref=f1e35] + - generic [ref=f1e36]: €24.99 + - generic [ref=f1e38]: + - button "Decrease quantity" [ref=f1e39]: + - generic [ref=f1e44]: − + - generic [ref=f1e45]: "1" + - button "Increase quantity" [ref=f1e46]: + - generic [ref=f1e51]: + + - button "Remove" [ref=f1e52] + - complementary [ref=f1e58]: + - heading "Order summary" [level=2] [ref=f1e59] + - generic [ref=f1e60]: + - generic [ref=f1e61]: Subtotal + - generic [ref=f1e62]: €24.99 + - button "Checkout" [ref=f1e64] + - contentinfo [ref=f1e70]: + - generic [ref=f1e71]: + - generic [ref=f1e72]: + - paragraph [ref=f1e73]: Acme Fashion + - paragraph [ref=f1e74]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=f1e75]: + - link "Shop all collections" [ref=f1e76] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f1e77] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=f1e78]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-04-59-720Z.yml b/.playwright-mcp/page-2026-07-11T11-04-59-720Z.yml new file mode 100644 index 00000000..26918e3b --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-04-59-720Z.yml @@ -0,0 +1,249 @@ +- generic [active] [ref=f1e1]: + - dialog [ref=f1e79]: + - iframe [ref=f1e80]: + - generic [ref=f2e2]: + - generic [ref=f2e4]: + - generic [ref=f2e5]: Internal Server Error + - button "Copy as Markdown" [ref=f2e11] [cursor=pointer] + - generic [ref=f2e18]: + - generic [ref=f2e19]: + - heading "TypeError" [level=1] [ref=f2e20] + - generic [ref=f2e21]: app/Livewire/Storefront/Cart/Show.php:38 + - paragraph [ref=f2e23]: "App\\Livewire\\Storefront\\Cart\\Show::checkout(): Return value must be of type Illuminate\\Http\\RedirectResponse, Livewire\\Features\\SupportRedirects\\Redirector returned" + - generic [ref=f2e24]: + - generic [ref=f2e25]: + - generic [ref=f2e26]: + - generic [ref=f2e27]: LARAVEL + - generic [ref=f2e28]: 12.51.0 + - generic [ref=f2e29]: + - generic [ref=f2e30]: PHP + - generic [ref=f2e31]: 8.4.17 + - generic [ref=f2e32]: UNHANDLED + - generic [ref=f2e36]: CODE 0 + - generic [ref=f2e38]: + - generic [ref=f2e39]: "500" + - generic [ref=f2e43]: POST + - generic [ref=f2e47]: http://acme-fashion.test/livewire-6701cc17/update + - button [ref=f2e48] [cursor=pointer] + - generic [ref=f2e53]: + - generic [ref=f2e54]: + - heading "Exception trace" [level=3] [ref=f2e60] + - generic [ref=f2e61]: + - generic [ref=f2e62]: + - generic [ref=f2e63] [cursor=pointer]: + - generic [ref=f2e66]: + - code [ref=f2e70]: + - generic [ref=f2e71]: app/Livewire/Storefront/Cart/Show.php + - generic [ref=f2e72]: app/Livewire/Storefront/Cart/Show.php:38 + - button [ref=f2e75] + - code [ref=f2e84]: + - generic [ref=f2e85]: "33" + - generic [ref=f2e86]: "34 public function checkout(CheckoutService $checkouts): RedirectResponse" + - generic [ref=f2e87]: "35 {" + - generic [ref=f2e88]: 36 $checkout = $checkouts->create($this->cart()); + - generic [ref=f2e89]: "37" + - generic [ref=f2e90]: 38 return redirect()->route('storefront.checkout.show', $checkout->id); + - generic [ref=f2e91]: "39 }" + - generic [ref=f2e92]: "40" + - generic [ref=f2e93]: "41 public function render(): View" + - generic [ref=f2e94]: "42 {" + - generic [ref=f2e95]: 43 return view('livewire.storefront.cart.show', ['cart' => $this->cart()]) + - generic [ref=f2e96]: 44 ->layout('layouts.storefront', ['title' => 'Cart - '.app('current_store')->name]); + - generic [ref=f2e97]: "45 }" + - generic [ref=f2e98]: "46" + - generic [ref=f2e99]: "47 private function cart(): Cart" + - generic [ref=f2e100]: "48 {" + - generic [ref=f2e101]: 49 return Cart::query()->with(['lines.variant.product', 'lines.variant.inventoryItem'])->findOrFail($this->cartId); + - generic [ref=f2e102]: "50" + - generic [ref=f2e104] [cursor=pointer]: + - generic [ref=f2e109]: 58 vendor frames + - button [ref=f2e110] + - generic [ref=f2e116] [cursor=pointer]: + - generic [ref=f2e119]: + - code [ref=f2e123]: + - generic [ref=f2e124]: public/index.php + - generic [ref=f2e125]: public/index.php:20 + - button [ref=f2e128] + - generic [ref=f2e134] [cursor=pointer]: + - generic [ref=f2e139]: 1 vendor frame + - button [ref=f2e140] + - generic [ref=f2e145]: + - generic [ref=f2e146]: + - heading "Queries" [level=3] [ref=f2e151] + - generic [ref=f2e152]: 1-8 of 8 + - generic [ref=f2e154]: + - generic [ref=f2e155]: + - generic [ref=f2e156]: + - generic [ref=f2e157]: sqlite + - code [ref=f2e164]: + - generic [ref=f2e165]: select * from "stores" where "stores"."id" = 1 limit 1 + - generic [ref=f2e166]: 2.79ms + - generic [ref=f2e167]: + - generic [ref=f2e168]: + - generic [ref=f2e169]: sqlite + - code [ref=f2e176]: + - generic [ref=f2e177]: select * from "carts" where "carts"."id" = 1 and "carts"."store_id" = 1 limit 1 + - generic [ref=f2e178]: 0.12ms + - generic [ref=f2e179]: + - generic [ref=f2e180]: + - generic [ref=f2e181]: sqlite + - code [ref=f2e188]: + - generic [ref=f2e189]: select * from "cart_lines" where "cart_lines"."cart_id" in (1) + - generic [ref=f2e190]: 0.04ms + - generic [ref=f2e191]: + - generic [ref=f2e192]: + - generic [ref=f2e193]: sqlite + - code [ref=f2e200]: + - generic [ref=f2e201]: select * from "product_variants" where "product_variants"."id" in (1) + - generic [ref=f2e202]: 0.07ms + - generic [ref=f2e203]: + - generic [ref=f2e204]: + - generic [ref=f2e205]: sqlite + - code [ref=f2e212]: + - generic [ref=f2e213]: select * from "products" where "products"."id" in (1) and "products"."store_id" = 1 + - generic [ref=f2e214]: 0.19ms + - generic [ref=f2e215]: + - generic [ref=f2e216]: + - generic [ref=f2e217]: sqlite + - code [ref=f2e224]: + - generic [ref=f2e225]: select * from "inventory_items" where "inventory_items"."variant_id" in (1) and "inventory_items"."store_id" = 1 + - generic [ref=f2e226]: 0.06ms + - generic [ref=f2e227]: + - generic [ref=f2e228]: + - generic [ref=f2e229]: sqlite + - code [ref=f2e236]: + - generic [ref=f2e237]: select exists(select * from "cart_lines" where "cart_lines"."cart_id" = 1 and "cart_lines"."cart_id" is not null) as "exists" + - generic [ref=f2e238]: 0.02ms + - generic [ref=f2e239]: + - generic [ref=f2e240]: + - generic [ref=f2e241]: sqlite + - code [ref=f2e248]: + - generic [ref=f2e249]: insert into "checkouts" ("status", "store_id", "cart_id", "customer_id", "updated_at", "created_at") values ('started', 1, 1, NULL, '2026-07-11 11:04:58', '2026-07-11 11:04:58') + - generic [ref=f2e250]: 0.38ms + - generic [ref=f2e252]: + - generic [ref=f2e253]: + - heading "Headers" [level=2] [ref=f2e254] + - generic [ref=f2e255]: + - generic [ref=f2e256]: + - generic [ref=f2e257]: cookie + - generic [ref=f2e259]: XSRF-TOKEN=eyJpdiI6IlFTcEhjR2ZBZHZqL3Z1UTQzdkJtdlE9PSIsInZhbHVlIjoiZDQwWkVsbWJCcFE4Y1h2OExYNjlQdTlqVFFBbEs4TnVaN1g1Ykc4RTB4QVBuWGlwS29odUh0bFF1Wk92RGFQbmJhZjR4dzFoK2g1VDdGaXd1b3F3SUQxWkhCNXdaZWFDNzNmb3ppeFFqcGRRYUFhRDN0YWk3S2U4TlFEZjVZeWgiLCJtYWMiOiI5MzliOGIzZmYwMDViODlkN2QyMTY4YWU5YWNkZTg3ZjI0ODQzMWFiMzZkMzc1NDgwNjllZTMxYTQxNGE4NjkwIiwidGFnIjoiIn0%3D; shop_session=eyJpdiI6IkpPSU5QdENWTkpmT1dmenhMWUlUeXc9PSIsInZhbHVlIjoiVVZIWmVGSUlXUjRMUU9HM1B4K2lSRUZkUzdBbFVUVUE1Mk1SQXZkc1NyMmN3L1h5RXVTNHUvSC9BcWxteGpXYUNYbCtxSEVCaUNvN2VFZDRpejlyeTJJYTI3bXpmQk9nSmlTSmYxZ2p3ZmVGLzE1OEN5MVlXelhZZm1abXRZTnQiLCJtYWMiOiJjOGU5Y2RhYWRjN2FjODY0NjA1Y2I4M2E2NDcyNDkwNGFiODkwYjE1NDEzOTNiM2JhODYzZWE4ZmQ4YWZiMzBhIiwidGFnIjoiIn0%3D + - generic [ref=f2e260]: + - generic [ref=f2e261]: accept-language + - generic [ref=f2e263]: en-GB,en-US;q=0.9,en;q=0.8 + - generic [ref=f2e264]: + - generic [ref=f2e265]: accept-encoding + - generic [ref=f2e267]: gzip, deflate + - generic [ref=f2e268]: + - generic [ref=f2e269]: referer + - generic [ref=f2e271]: http://acme-fashion.test/cart + - generic [ref=f2e272]: + - generic [ref=f2e273]: origin + - generic [ref=f2e275]: http://acme-fashion.test + - generic [ref=f2e276]: + - generic [ref=f2e277]: accept + - generic [ref=f2e279]: "*/*" + - generic [ref=f2e280]: + - generic [ref=f2e281]: x-livewire + - generic [ref=f2e283]: "1" + - generic [ref=f2e284]: + - generic [ref=f2e285]: content-type + - generic [ref=f2e287]: application/json + - generic [ref=f2e288]: + - generic [ref=f2e289]: user-agent + - generic [ref=f2e291]: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 + - generic [ref=f2e292]: + - generic [ref=f2e293]: content-length + - generic [ref=f2e295]: "490" + - generic [ref=f2e296]: + - generic [ref=f2e297]: connection + - generic [ref=f2e299]: keep-alive + - generic [ref=f2e300]: + - generic [ref=f2e301]: host + - generic [ref=f2e303]: acme-fashion.test + - generic [ref=f2e304]: + - heading "Body" [level=2] [ref=f2e305] + - code [ref=f2e310]: + - generic [ref=f2e311]: "{" + - generic [ref=f2e312]: "\"_token\": \"BLvnxGmt1x5SpbfmG3dDyA7evVNRFS6OHjX7PUMY\"," + - generic [ref=f2e313]: "\"components\": [" + - generic [ref=f2e314]: "{" + - generic [ref=f2e315]: "\"snapshot\": \"{\"data\":{\"cartId\":1},\"memo\":{\"id\":\"zDXuALWmt4P62je0agSV\",\"name\":\"storefront.cart.show\",\"path\":\"cart\",\"method\":\"GET\",\"release\":\"a-a-a\",\"children\":[],\"scripts\":[],\"assets\":[],\"errors\":[],\"locale\":\"en\",\"islands\":[]},\"checksum\":\"ff7e7b024d96314eeb12025b6375d8dcff76f6001dc4b10afc22518fbd8c9423\"}\"," + - generic [ref=f2e316]: "\"updates\": []," + - generic [ref=f2e317]: "\"calls\": [" + - generic [ref=f2e318]: "{" + - generic [ref=f2e319]: "\"method\": \"checkout\"," + - generic [ref=f2e320]: "\"params\": []," + - generic [ref=f2e321]: "\"metadata\": []" + - generic [ref=f2e322]: "}" + - generic [ref=f2e323]: "]" + - generic [ref=f2e324]: "}" + - generic [ref=f2e325]: "]" + - generic [ref=f2e326]: "}" + - generic [ref=f2e327]: + - heading "Routing" [level=2] [ref=f2e328] + - generic [ref=f2e329]: + - generic [ref=f2e330]: + - generic [ref=f2e331]: controller + - generic [ref=f2e333]: Livewire\Mechanisms\HandleRequests\HandleRequests@handleUpdate + - generic [ref=f2e334]: + - generic [ref=f2e335]: route name + - generic [ref=f2e337]: default-livewire.update + - generic [ref=f2e338]: + - generic [ref=f2e339]: middleware + - generic [ref=f2e341]: web + - generic [ref=f2e342]: + - heading "Routing parameters" [level=2] [ref=f2e343] + - generic [ref=f2e344]: // No routing parameters + - link "Skip to main content" [ref=f1e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=f1e3]: Free shipping available with code FREESHIP + - banner [ref=f1e4]: + - generic [ref=f1e5]: + - link "Acme Fashion" [ref=f1e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=f1e7]: + - link "Home" [ref=f1e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=f1e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f1e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=f1e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=f1e12]: + - button "Open product search" [ref=f1e14]: + - generic [ref=f1e19]: Search + - button "Open shopping cart" [ref=f1e21]: + - generic [ref=f1e26]: Cart + - main [ref=f1e27]: + - generic [ref=f1e28]: + - heading "Your cart" [level=1] [ref=f1e29] + - generic [ref=f1e30]: + - article [ref=f1e32]: + - generic [ref=f1e34]: + - heading "Classic Cotton T-Shirt" [level=2] [ref=f1e35] + - generic [ref=f1e36]: €24.99 + - generic [ref=f1e38]: + - button "Decrease quantity" [ref=f1e39]: + - generic [ref=f1e44]: − + - generic [ref=f1e45]: "1" + - button "Increase quantity" [ref=f1e46]: + - generic [ref=f1e51]: + + - button "Remove" [ref=f1e52] + - complementary [ref=f1e58]: + - heading "Order summary" [level=2] [ref=f1e59] + - generic [ref=f1e60]: + - generic [ref=f1e61]: Subtotal + - generic [ref=f1e62]: €24.99 + - button "Checkout" [ref=f1e64] + - contentinfo [ref=f1e70]: + - generic [ref=f1e71]: + - generic [ref=f1e72]: + - paragraph [ref=f1e73]: Acme Fashion + - paragraph [ref=f1e74]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=f1e75]: + - link "Shop all collections" [ref=f1e76] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f1e77] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=f1e78]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-05-51-648Z.yml b/.playwright-mcp/page-2026-07-11T11-05-51-648Z.yml new file mode 100644 index 00000000..702c3446 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-05-51-648Z.yml @@ -0,0 +1,98 @@ +- generic [active] [ref=e1]: + - link "Skip to main content" [ref=e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=e3]: Free shipping available with code FREESHIP + - banner [ref=e4]: + - generic [ref=e5]: + - link "Acme Fashion" [ref=e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=e7]: + - link "Home" [ref=e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=e12]: + - button "Open product search" [ref=e14]: + - generic [ref=e19]: Search + - button "Open shopping cart" [ref=e21]: + - generic [ref=e26]: Cart + - main [ref=e27]: + - generic [ref=e28]: + - navigation "Breadcrumb" [ref=e29]: + - list [ref=e30]: + - listitem [ref=e31]: + - link "Home" [ref=e32] [cursor=pointer]: + - /url: http://acme-fashion.test + - generic [ref=e33]: / + - listitem [ref=e34]: + - link "Products" [ref=e35] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - generic [ref=e36]: / + - listitem [ref=e37]: + - generic [ref=e38]: Classic Cotton T-Shirt + - generic [ref=e39]: + - img "Classic Cotton T-Shirt" [ref=e40] + - generic [ref=e41]: + - generic [ref=e42]: + - paragraph [ref=e43]: Acme Basics + - heading "Classic Cotton T-Shirt" [level=1] [ref=e44] + - generic [ref=e45]: €24.99 + - group "Choose a variant" [ref=e48]: + - generic [ref=e50]: + - generic [ref=e51] [cursor=pointer]: + - generic [ref=e52]: ACME-CLASSICC-S-WHITE-1 + - radio "Select variant ACME-CLASSICC-S-WHITE-1" [checked] [ref=e53] + - generic [ref=e54] [cursor=pointer]: + - generic [ref=e55]: ACME-CLASSICC-S-BLACK-2 + - radio "Select variant ACME-CLASSICC-S-BLACK-2" [ref=e56] + - generic [ref=e57] [cursor=pointer]: + - generic [ref=e58]: ACME-CLASSICC-S-NAVY-3 + - radio "Select variant ACME-CLASSICC-S-NAVY-3" [ref=e59] + - generic [ref=e60] [cursor=pointer]: + - generic [ref=e61]: ACME-CLASSICC-M-WHITE-4 + - radio "Select variant ACME-CLASSICC-M-WHITE-4" [ref=e62] + - generic [ref=e63] [cursor=pointer]: + - generic [ref=e64]: ACME-CLASSICC-M-BLACK-5 + - radio "Select variant ACME-CLASSICC-M-BLACK-5" [ref=e65] + - generic [ref=e66] [cursor=pointer]: + - generic [ref=e67]: ACME-CLASSICC-M-NAVY-6 + - radio "Select variant ACME-CLASSICC-M-NAVY-6" [ref=e68] + - generic [ref=e69] [cursor=pointer]: + - generic [ref=e70]: ACME-CLASSICC-L-WHITE-7 + - radio "Select variant ACME-CLASSICC-L-WHITE-7" [ref=e71] + - generic [ref=e72] [cursor=pointer]: + - generic [ref=e73]: ACME-CLASSICC-L-BLACK-8 + - radio "Select variant ACME-CLASSICC-L-BLACK-8" [ref=e74] + - generic [ref=e75] [cursor=pointer]: + - generic [ref=e76]: ACME-CLASSICC-L-NAVY-9 + - radio "Select variant ACME-CLASSICC-L-NAVY-9" [ref=e77] + - generic [ref=e78] [cursor=pointer]: + - generic [ref=e79]: ACME-CLASSICC-XL-WHITE-10 + - radio "Select variant ACME-CLASSICC-XL-WHITE-10" [ref=e80] + - generic [ref=e81] [cursor=pointer]: + - generic [ref=e82]: ACME-CLASSICC-XL-BLACK-11 + - radio "Select variant ACME-CLASSICC-XL-BLACK-11" [ref=e83] + - generic [ref=e84] [cursor=pointer]: + - generic [ref=e85]: ACME-CLASSICC-XL-NAVY-12 + - radio "Select variant ACME-CLASSICC-XL-NAVY-12" [ref=e86] + - paragraph [ref=e87]: In stock + - generic [ref=e88]: + - generic [ref=e89]: Quantity + - spinbutton "Quantity" [ref=e91]: "1" + - button "Add to cart" [ref=e92] + - paragraph [ref=e99]: A timeless classic cotton t-shirt. Comfortable, breathable, and perfect for everyday wear. + - contentinfo [ref=e100]: + - generic [ref=e101]: + - generic [ref=e102]: + - paragraph [ref=e103]: Acme Fashion + - paragraph [ref=e104]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=e105]: + - link "Shop all collections" [ref=e106] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=e107] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=e108]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-05-52-725Z.yml b/.playwright-mcp/page-2026-07-11T11-05-52-725Z.yml new file mode 100644 index 00000000..dfd7502c --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-05-52-725Z.yml @@ -0,0 +1,110 @@ +- generic [active] [ref=e1]: + - link "Skip to main content" [ref=e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=e3]: Free shipping available with code FREESHIP + - banner [ref=e4]: + - generic [ref=e5]: + - link "Acme Fashion" [ref=e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=e7]: + - link "Home" [ref=e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=e12]: + - button "Open product search" [ref=e14]: + - generic [ref=e19]: Search + - generic [ref=e20]: + - button "Open shopping cart" [ref=e21]: + - generic [ref=e26]: Cart (2) + - dialog [ref=e109]: + - generic [ref=e110]: + - generic [ref=e111]: Shopping cart + - generic [ref=e112]: + - generic [ref=e113]: + - paragraph [ref=e114]: Classic Cotton T-Shirt + - paragraph [ref=e115]: Qty 2 + - button "Remove" [ref=e116] + - link "View cart" [ref=e122] [cursor=pointer]: + - /url: http://acme-fashion.test/cart + - button "Close modal" [ref=e125] + - main [ref=e27]: + - generic [ref=e28]: + - navigation "Breadcrumb" [ref=e29]: + - list [ref=e30]: + - listitem [ref=e31]: + - link "Home" [ref=e32] [cursor=pointer]: + - /url: http://acme-fashion.test + - generic [ref=e33]: / + - listitem [ref=e34]: + - link "Products" [ref=e35] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - generic [ref=e36]: / + - listitem [ref=e37]: + - generic [ref=e38]: Classic Cotton T-Shirt + - generic [ref=e39]: + - img "Classic Cotton T-Shirt" [ref=e40] + - generic [ref=e41]: + - generic [ref=e42]: + - paragraph [ref=e43]: Acme Basics + - heading "Classic Cotton T-Shirt" [level=1] [ref=e44] + - generic [ref=e45]: €24.99 + - group "Choose a variant" [ref=e48]: + - generic [ref=e50]: + - generic [ref=e51] [cursor=pointer]: + - generic [ref=e52]: ACME-CLASSICC-S-WHITE-1 + - radio "Select variant ACME-CLASSICC-S-WHITE-1" [checked] [ref=e53] + - generic [ref=e54] [cursor=pointer]: + - generic [ref=e55]: ACME-CLASSICC-S-BLACK-2 + - radio "Select variant ACME-CLASSICC-S-BLACK-2" [ref=e56] + - generic [ref=e57] [cursor=pointer]: + - generic [ref=e58]: ACME-CLASSICC-S-NAVY-3 + - radio "Select variant ACME-CLASSICC-S-NAVY-3" [ref=e59] + - generic [ref=e60] [cursor=pointer]: + - generic [ref=e61]: ACME-CLASSICC-M-WHITE-4 + - radio "Select variant ACME-CLASSICC-M-WHITE-4" [ref=e62] + - generic [ref=e63] [cursor=pointer]: + - generic [ref=e64]: ACME-CLASSICC-M-BLACK-5 + - radio "Select variant ACME-CLASSICC-M-BLACK-5" [ref=e65] + - generic [ref=e66] [cursor=pointer]: + - generic [ref=e67]: ACME-CLASSICC-M-NAVY-6 + - radio "Select variant ACME-CLASSICC-M-NAVY-6" [ref=e68] + - generic [ref=e69] [cursor=pointer]: + - generic [ref=e70]: ACME-CLASSICC-L-WHITE-7 + - radio "Select variant ACME-CLASSICC-L-WHITE-7" [ref=e71] + - generic [ref=e72] [cursor=pointer]: + - generic [ref=e73]: ACME-CLASSICC-L-BLACK-8 + - radio "Select variant ACME-CLASSICC-L-BLACK-8" [ref=e74] + - generic [ref=e75] [cursor=pointer]: + - generic [ref=e76]: ACME-CLASSICC-L-NAVY-9 + - radio "Select variant ACME-CLASSICC-L-NAVY-9" [ref=e77] + - generic [ref=e78] [cursor=pointer]: + - generic [ref=e79]: ACME-CLASSICC-XL-WHITE-10 + - radio "Select variant ACME-CLASSICC-XL-WHITE-10" [ref=e80] + - generic [ref=e81] [cursor=pointer]: + - generic [ref=e82]: ACME-CLASSICC-XL-BLACK-11 + - radio "Select variant ACME-CLASSICC-XL-BLACK-11" [ref=e83] + - generic [ref=e84] [cursor=pointer]: + - generic [ref=e85]: ACME-CLASSICC-XL-NAVY-12 + - radio "Select variant ACME-CLASSICC-XL-NAVY-12" [ref=e86] + - paragraph [ref=e87]: In stock + - generic [ref=e88]: + - generic [ref=e89]: Quantity + - spinbutton "Quantity" [ref=e91]: "1" + - button "Add to cart" [ref=e92] + - paragraph [ref=e99]: A timeless classic cotton t-shirt. Comfortable, breathable, and perfect for everyday wear. + - contentinfo [ref=e100]: + - generic [ref=e101]: + - generic [ref=e102]: + - paragraph [ref=e103]: Acme Fashion + - paragraph [ref=e104]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=e105]: + - link "Shop all collections" [ref=e106] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=e107] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=e108]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-05-53-349Z.yml b/.playwright-mcp/page-2026-07-11T11-05-53-349Z.yml new file mode 100644 index 00000000..929a1943 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-05-53-349Z.yml @@ -0,0 +1,54 @@ +- generic [active] [ref=f1e1]: + - link "Skip to main content" [ref=f1e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=f1e3]: Free shipping available with code FREESHIP + - banner [ref=f1e4]: + - generic [ref=f1e5]: + - link "Acme Fashion" [ref=f1e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=f1e7]: + - link "Home" [ref=f1e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=f1e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f1e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=f1e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=f1e12]: + - button "Open product search" [ref=f1e14]: + - generic [ref=f1e19]: Search + - button "Open shopping cart" [ref=f1e21]: + - generic [ref=f1e26]: Cart + - main [ref=f1e27]: + - generic [ref=f1e28]: + - heading "Your cart" [level=1] [ref=f1e29] + - generic [ref=f1e30]: + - article [ref=f1e32]: + - generic [ref=f1e34]: + - heading "Classic Cotton T-Shirt" [level=2] [ref=f1e35] + - generic [ref=f1e36]: €49.98 + - generic [ref=f1e38]: + - button "Decrease quantity" [ref=f1e39]: + - generic [ref=f1e44]: − + - generic [ref=f1e45]: "2" + - button "Increase quantity" [ref=f1e46]: + - generic [ref=f1e51]: + + - button "Remove" [ref=f1e52] + - complementary [ref=f1e58]: + - heading "Order summary" [level=2] [ref=f1e59] + - generic [ref=f1e60]: + - generic [ref=f1e61]: Subtotal + - generic [ref=f1e62]: €49.98 + - button "Checkout" [ref=f1e64] + - contentinfo [ref=f1e70]: + - generic [ref=f1e71]: + - generic [ref=f1e72]: + - paragraph [ref=f1e73]: Acme Fashion + - paragraph [ref=f1e74]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=f1e75]: + - link "Shop all collections" [ref=f1e76] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f1e77] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=f1e78]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-05-53-490Z.yml b/.playwright-mcp/page-2026-07-11T11-05-53-490Z.yml new file mode 100644 index 00000000..5304bc85 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-05-53-490Z.yml @@ -0,0 +1,78 @@ +- generic [active] [ref=f2e1]: + - link "Skip to main content" [ref=f2e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=f2e3]: Free shipping available with code FREESHIP + - banner [ref=f2e4]: + - generic [ref=f2e5]: + - link "Acme Fashion" [ref=f2e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=f2e7]: + - link "Home" [ref=f2e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=f2e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f2e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=f2e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=f2e12]: + - button "Open product search" [ref=f2e14]: + - generic [ref=f2e19]: Search + - button "Open shopping cart" [ref=f2e21]: + - generic [ref=f2e26]: Cart + - main [ref=f2e27]: + - generic [ref=f2e28]: + - heading "Checkout" [level=1] [ref=f2e29] + - generic [ref=f2e30]: + - generic [ref=f2e32]: + - generic [ref=f2e33]: 1. Contact and shipping address + - generic [ref=f2e34]: + - generic [ref=f2e35]: + - generic [ref=f2e36]: Email + - textbox "Email" [ref=f2e38] + - generic [ref=f2e39]: + - generic [ref=f2e40]: First name + - textbox "First name" [ref=f2e42] + - generic [ref=f2e43]: + - generic [ref=f2e44]: Last name + - textbox "Last name" [ref=f2e46] + - generic [ref=f2e47]: + - generic [ref=f2e48]: Address + - textbox "Address" [ref=f2e50] + - generic [ref=f2e51]: + - generic [ref=f2e52]: City + - textbox "City" [ref=f2e54] + - generic [ref=f2e55]: + - generic [ref=f2e56]: Postal code + - textbox "Postal code" [ref=f2e58] + - generic [ref=f2e59]: + - generic [ref=f2e60]: Country code + - textbox "Country code" [ref=f2e62]: DE + - generic [ref=f2e63]: + - generic [ref=f2e64]: Phone (optional) + - textbox "Phone (optional)" [ref=f2e66] + - button "Continue to shipping" [ref=f2e67] + - complementary [ref=f2e73]: + - heading "Order summary" [level=2] [ref=f2e74] + - generic [ref=f2e76]: + - generic [ref=f2e77]: 2 × Classic Cotton T-Shirt + - generic [ref=f2e78]: €49.98 + - generic [ref=f2e81]: + - generic [ref=f2e82]: Total + - generic [ref=f2e83]: €49.98 + - generic [ref=f2e85]: + - generic [ref=f2e86]: + - generic [ref=f2e87]: Discount code + - textbox "Discount code" [ref=f2e89] + - button "Apply discount" [ref=f2e90] + - contentinfo [ref=f2e96]: + - generic [ref=f2e97]: + - generic [ref=f2e98]: + - paragraph [ref=f2e99]: Acme Fashion + - paragraph [ref=f2e100]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=f2e101]: + - link "Shop all collections" [ref=f2e102] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f2e103] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=f2e104]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-06-10-106Z.yml b/.playwright-mcp/page-2026-07-11T11-06-10-106Z.yml new file mode 100644 index 00000000..313a7505 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-06-10-106Z.yml @@ -0,0 +1,89 @@ +- generic [active] [ref=f2e1]: + - link "Skip to main content" [ref=f2e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=f2e3]: Free shipping available with code FREESHIP + - banner [ref=f2e4]: + - generic [ref=f2e5]: + - link "Acme Fashion" [ref=f2e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=f2e7]: + - link "Home" [ref=f2e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=f2e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f2e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=f2e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=f2e12]: + - button "Open product search" [ref=f2e14]: + - generic [ref=f2e19]: Search + - button "Open shopping cart" [ref=f2e21]: + - generic [ref=f2e26]: Cart + - main [ref=f2e27]: + - generic [ref=f2e28]: + - heading "Checkout" [level=1] [ref=f2e29] + - generic [ref=f2e30]: + - generic [ref=f2e31]: + - generic [ref=f2e32]: + - generic [ref=f2e33]: 1. Contact and shipping address + - generic [ref=f2e34]: + - generic [ref=f2e35]: + - generic [ref=f2e36]: Email + - textbox "Email" [ref=f2e38]: browser-review@example.test + - generic [ref=f2e39]: + - generic [ref=f2e40]: First name + - textbox "First name" [ref=f2e42]: Browser + - generic [ref=f2e43]: + - generic [ref=f2e44]: Last name + - textbox "Last name" [ref=f2e46]: Reviewer + - generic [ref=f2e47]: + - generic [ref=f2e48]: Address + - textbox "Address" [ref=f2e50]: Teststrasse 42 + - generic [ref=f2e51]: + - generic [ref=f2e52]: City + - textbox "City" [ref=f2e54]: Berlin + - generic [ref=f2e55]: + - generic [ref=f2e56]: Postal code + - textbox "Postal code" [ref=f2e58]: "10115" + - generic [ref=f2e59]: + - generic [ref=f2e60]: Country code + - textbox "Country code" [ref=f2e62]: DE + - generic [ref=f2e63]: + - generic [ref=f2e64]: Phone (optional) + - textbox "Phone (optional)" [ref=f2e66] + - button "Continue to shipping" [ref=f2e67] + - generic [ref=f2e105]: + - generic [ref=f2e106]: 2. Shipping method + - group "Choose a shipping method" [ref=f2e107]: + - generic [ref=f2e109] [cursor=pointer]: + - generic [ref=f2e110]: Standard Shipping + - radio "Standard Shipping" [ref=f2e111] + - generic [ref=f2e112] [cursor=pointer]: + - generic [ref=f2e113]: Express Shipping + - radio "Express Shipping" [ref=f2e114] + - button "Continue to payment" [ref=f2e115] + - complementary [ref=f2e73]: + - heading "Order summary" [level=2] [ref=f2e74] + - generic [ref=f2e76]: + - generic [ref=f2e77]: 2 × Classic Cotton T-Shirt + - generic [ref=f2e78]: €49.98 + - generic [ref=f2e81]: + - generic [ref=f2e82]: Total + - generic [ref=f2e83]: €49.98 + - generic [ref=f2e85]: + - generic [ref=f2e86]: + - generic [ref=f2e87]: Discount code + - textbox "Discount code" [ref=f2e89] + - button "Apply discount" [ref=f2e90] + - contentinfo [ref=f2e96]: + - generic [ref=f2e97]: + - generic [ref=f2e98]: + - paragraph [ref=f2e99]: Acme Fashion + - paragraph [ref=f2e100]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=f2e101]: + - link "Shop all collections" [ref=f2e102] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f2e103] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=f2e104]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-06-22-031Z.yml b/.playwright-mcp/page-2026-07-11T11-06-22-031Z.yml new file mode 100644 index 00000000..156ef3ad --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-06-22-031Z.yml @@ -0,0 +1,89 @@ +- generic [ref=f2e1]: + - link "Skip to main content" [ref=f2e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=f2e3]: Free shipping available with code FREESHIP + - banner [ref=f2e4]: + - generic [ref=f2e5]: + - link "Acme Fashion" [ref=f2e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=f2e7]: + - link "Home" [ref=f2e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=f2e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f2e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=f2e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=f2e12]: + - button "Open product search" [ref=f2e14]: + - generic [ref=f2e19]: Search + - button "Open shopping cart" [ref=f2e21]: + - generic [ref=f2e26]: Cart + - main [ref=f2e27]: + - generic [ref=f2e28]: + - heading "Checkout" [level=1] [ref=f2e29] + - generic [ref=f2e30]: + - generic [ref=f2e31]: + - generic [ref=f2e32]: + - generic [ref=f2e33]: 1. Contact and shipping address + - generic [ref=f2e34]: + - generic [ref=f2e35]: + - generic [ref=f2e36]: Email + - textbox "Email" [ref=f2e38]: browser-review@example.test + - generic [ref=f2e39]: + - generic [ref=f2e40]: First name + - textbox "First name" [ref=f2e42]: Browser + - generic [ref=f2e43]: + - generic [ref=f2e44]: Last name + - textbox "Last name" [ref=f2e46]: Reviewer + - generic [ref=f2e47]: + - generic [ref=f2e48]: Address + - textbox "Address" [ref=f2e50]: Teststrasse 42 + - generic [ref=f2e51]: + - generic [ref=f2e52]: City + - textbox "City" [ref=f2e54]: Berlin + - generic [ref=f2e55]: + - generic [ref=f2e56]: Postal code + - textbox "Postal code" [ref=f2e58]: "10115" + - generic [ref=f2e59]: + - generic [ref=f2e60]: Country code + - textbox "Country code" [ref=f2e62]: DE + - generic [ref=f2e63]: + - generic [ref=f2e64]: Phone (optional) + - textbox "Phone (optional)" [ref=f2e66] + - button "Continue to shipping" [ref=f2e67] + - generic [ref=f2e105]: + - generic [ref=f2e106]: 2. Shipping method + - group "Choose a shipping method" [ref=f2e107]: + - generic [ref=f2e109] [cursor=pointer]: + - generic [ref=f2e110]: Standard Shipping + - radio "Standard Shipping" [checked] [active] [ref=f2e111] + - generic [ref=f2e112] [cursor=pointer]: + - generic [ref=f2e113]: Express Shipping + - radio "Express Shipping" [ref=f2e114] + - button "Continue to payment" [ref=f2e115] + - complementary [ref=f2e73]: + - heading "Order summary" [level=2] [ref=f2e74] + - generic [ref=f2e76]: + - generic [ref=f2e77]: 2 × Classic Cotton T-Shirt + - generic [ref=f2e78]: €49.98 + - generic [ref=f2e81]: + - generic [ref=f2e82]: Total + - generic [ref=f2e83]: €49.98 + - generic [ref=f2e85]: + - generic [ref=f2e86]: + - generic [ref=f2e87]: Discount code + - textbox "Discount code" [ref=f2e89] + - button "Apply discount" [ref=f2e90] + - contentinfo [ref=f2e96]: + - generic [ref=f2e97]: + - generic [ref=f2e98]: + - paragraph [ref=f2e99]: Acme Fashion + - paragraph [ref=f2e100]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=f2e101]: + - link "Shop all collections" [ref=f2e102] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f2e103] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=f2e104]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-06-23-086Z.yml b/.playwright-mcp/page-2026-07-11T11-06-23-086Z.yml new file mode 100644 index 00000000..3459b1b9 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-06-23-086Z.yml @@ -0,0 +1,105 @@ +- generic [ref=f2e1]: + - link "Skip to main content" [ref=f2e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=f2e3]: Free shipping available with code FREESHIP + - banner [ref=f2e4]: + - generic [ref=f2e5]: + - link "Acme Fashion" [ref=f2e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=f2e7]: + - link "Home" [ref=f2e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=f2e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f2e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=f2e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=f2e12]: + - button "Open product search" [ref=f2e14]: + - generic [ref=f2e19]: Search + - button "Open shopping cart" [ref=f2e21]: + - generic [ref=f2e26]: Cart + - main [ref=f2e27]: + - generic [ref=f2e28]: + - heading "Checkout" [level=1] [ref=f2e29] + - generic [ref=f2e30]: + - generic [ref=f2e31]: + - generic [ref=f2e32]: + - generic [ref=f2e33]: 1. Contact and shipping address + - generic [ref=f2e34]: + - generic [ref=f2e35]: + - generic [ref=f2e36]: Email + - textbox "Email" [ref=f2e38]: browser-review@example.test + - generic [ref=f2e39]: + - generic [ref=f2e40]: First name + - textbox "First name" [ref=f2e42]: Browser + - generic [ref=f2e43]: + - generic [ref=f2e44]: Last name + - textbox "Last name" [ref=f2e46]: Reviewer + - generic [ref=f2e47]: + - generic [ref=f2e48]: Address + - textbox "Address" [ref=f2e50]: Teststrasse 42 + - generic [ref=f2e51]: + - generic [ref=f2e52]: City + - textbox "City" [ref=f2e54]: Berlin + - generic [ref=f2e55]: + - generic [ref=f2e56]: Postal code + - textbox "Postal code" [ref=f2e58]: "10115" + - generic [ref=f2e59]: + - generic [ref=f2e60]: Country code + - textbox "Country code" [ref=f2e62]: DE + - generic [ref=f2e63]: + - generic [ref=f2e64]: Phone (optional) + - textbox "Phone (optional)" [ref=f2e66] + - button "Continue to shipping" [ref=f2e67] + - generic [ref=f2e105]: + - generic [ref=f2e106]: 2. Shipping method + - group "Choose a shipping method" [ref=f2e107]: + - generic [ref=f2e109] [cursor=pointer]: + - generic [ref=f2e110]: Standard Shipping + - radio "Standard Shipping" [checked] [ref=f2e111] + - generic [ref=f2e112] [cursor=pointer]: + - generic [ref=f2e113]: Express Shipping + - radio "Express Shipping" [ref=f2e114] + - button "Continue to payment" [active] [ref=f2e115] + - generic [ref=f2e121]: + - generic [ref=f2e122]: 3. Payment + - group "Payment method" [ref=f2e123]: + - generic [ref=f2e125] [cursor=pointer]: + - radio "Credit card" [checked] [ref=f2e126] + - generic [ref=f2e127]: Credit card + - generic [ref=f2e128] [cursor=pointer]: + - radio "PayPal" [ref=f2e129] + - generic [ref=f2e130]: PayPal + - generic [ref=f2e131] [cursor=pointer]: + - radio "Bank transfer" [ref=f2e132] + - generic [ref=f2e133]: Bank transfer + - generic [ref=f2e134]: + - generic [ref=f2e135]: Card number + - textbox "Card number" [ref=f2e137]: 4242 4242 4242 4242 + - button "Review payment" [ref=f2e138] + - complementary [ref=f2e73]: + - heading "Order summary" [level=2] [ref=f2e74] + - generic [ref=f2e76]: + - generic [ref=f2e77]: 2 × Classic Cotton T-Shirt + - generic [ref=f2e78]: €49.98 + - generic [ref=f2e81]: + - generic [ref=f2e82]: Total + - generic [ref=f2e83]: €54.97 + - generic [ref=f2e85]: + - generic [ref=f2e86]: + - generic [ref=f2e87]: Discount code + - textbox "Discount code" [ref=f2e89] + - button "Apply discount" [ref=f2e90] + - contentinfo [ref=f2e96]: + - generic [ref=f2e97]: + - generic [ref=f2e98]: + - paragraph [ref=f2e99]: Acme Fashion + - paragraph [ref=f2e100]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=f2e101]: + - link "Shop all collections" [ref=f2e102] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f2e103] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=f2e104]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-06-31-119Z.yml b/.playwright-mcp/page-2026-07-11T11-06-31-119Z.yml new file mode 100644 index 00000000..508b1c97 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-06-31-119Z.yml @@ -0,0 +1,105 @@ +- generic [ref=f2e1]: + - link "Skip to main content" [ref=f2e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=f2e3]: Free shipping available with code FREESHIP + - banner [ref=f2e4]: + - generic [ref=f2e5]: + - link "Acme Fashion" [ref=f2e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=f2e7]: + - link "Home" [ref=f2e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=f2e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f2e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=f2e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=f2e12]: + - button "Open product search" [ref=f2e14]: + - generic [ref=f2e19]: Search + - button "Open shopping cart" [ref=f2e21]: + - generic [ref=f2e26]: Cart + - main [ref=f2e27]: + - generic [ref=f2e28]: + - heading "Checkout" [level=1] [ref=f2e29] + - generic [ref=f2e30]: + - generic [ref=f2e31]: + - generic [ref=f2e32]: + - generic [ref=f2e33]: 1. Contact and shipping address + - generic [ref=f2e34]: + - generic [ref=f2e35]: + - generic [ref=f2e36]: Email + - textbox "Email" [ref=f2e38]: browser-review@example.test + - generic [ref=f2e39]: + - generic [ref=f2e40]: First name + - textbox "First name" [ref=f2e42]: Browser + - generic [ref=f2e43]: + - generic [ref=f2e44]: Last name + - textbox "Last name" [ref=f2e46]: Reviewer + - generic [ref=f2e47]: + - generic [ref=f2e48]: Address + - textbox "Address" [ref=f2e50]: Teststrasse 42 + - generic [ref=f2e51]: + - generic [ref=f2e52]: City + - textbox "City" [ref=f2e54]: Berlin + - generic [ref=f2e55]: + - generic [ref=f2e56]: Postal code + - textbox "Postal code" [ref=f2e58]: "10115" + - generic [ref=f2e59]: + - generic [ref=f2e60]: Country code + - textbox "Country code" [ref=f2e62]: DE + - generic [ref=f2e63]: + - generic [ref=f2e64]: Phone (optional) + - textbox "Phone (optional)" [ref=f2e66] + - button "Continue to shipping" [ref=f2e67] + - generic [ref=f2e105]: + - generic [ref=f2e106]: 2. Shipping method + - group "Choose a shipping method" [ref=f2e107]: + - generic [ref=f2e109] [cursor=pointer]: + - generic [ref=f2e110]: Standard Shipping + - radio "Standard Shipping" [checked] [ref=f2e111] + - generic [ref=f2e112] [cursor=pointer]: + - generic [ref=f2e113]: Express Shipping + - radio "Express Shipping" [ref=f2e114] + - button "Continue to payment" [ref=f2e115] + - generic [ref=f2e121]: + - generic [ref=f2e122]: 3. Payment + - group "Payment method" [ref=f2e123]: + - generic [ref=f2e125] [cursor=pointer]: + - radio "Credit card" [checked] [ref=f2e126] + - generic [ref=f2e127]: Credit card + - generic [ref=f2e128] [cursor=pointer]: + - radio "PayPal" [ref=f2e129] + - generic [ref=f2e130]: PayPal + - generic [ref=f2e131] [cursor=pointer]: + - radio "Bank transfer" [ref=f2e132] + - generic [ref=f2e133]: Bank transfer + - generic [ref=f2e134]: + - generic [ref=f2e135]: Card number + - textbox "Card number" [ref=f2e137]: 4242 4242 4242 4242 + - button "Pay now" [active] [ref=f2e144] + - complementary [ref=f2e73]: + - heading "Order summary" [level=2] [ref=f2e74] + - generic [ref=f2e76]: + - generic [ref=f2e77]: 2 × Classic Cotton T-Shirt + - generic [ref=f2e78]: €49.98 + - generic [ref=f2e81]: + - generic [ref=f2e82]: Total + - generic [ref=f2e83]: €54.97 + - generic [ref=f2e85]: + - generic [ref=f2e86]: + - generic [ref=f2e87]: Discount code + - textbox "Discount code" [ref=f2e89] + - button "Apply discount" [ref=f2e90] + - contentinfo [ref=f2e96]: + - generic [ref=f2e97]: + - generic [ref=f2e98]: + - paragraph [ref=f2e99]: Acme Fashion + - paragraph [ref=f2e100]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=f2e101]: + - link "Shop all collections" [ref=f2e102] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f2e103] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=f2e104]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-06-40-194Z.yml b/.playwright-mcp/page-2026-07-11T11-06-40-194Z.yml new file mode 100644 index 00000000..e73b14e7 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-06-40-194Z.yml @@ -0,0 +1,48 @@ +- generic [active] [ref=f3e1]: + - link "Skip to main content" [ref=f3e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=f3e3]: Free shipping available with code FREESHIP + - banner [ref=f3e4]: + - generic [ref=f3e5]: + - link "Acme Fashion" [ref=f3e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=f3e7]: + - link "Home" [ref=f3e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=f3e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f3e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=f3e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=f3e12]: + - button "Open product search" [ref=f3e14]: + - generic [ref=f3e19]: Search + - button "Open shopping cart" [ref=f3e21]: + - generic [ref=f3e26]: Cart + - main [ref=f3e27]: + - generic [ref=f3e28]: + - generic [ref=f3e29]: ✓ + - heading "Thank you for your order!" [level=1] [ref=f3e30] + - paragraph [ref=f3e31]: "Order #1016 has been placed." + - generic [ref=f3e32]: + - heading "Order details" [level=2] [ref=f3e33] + - generic [ref=f3e34]: + - generic [ref=f3e35]: 2 × Classic Cotton T-Shirt + - generic [ref=f3e36]: €49.98 + - generic [ref=f3e38]: + - generic [ref=f3e39]: Total + - generic [ref=f3e40]: €54.97 + - link "Continue shopping" [ref=f3e42] [cursor=pointer]: + - /url: http://acme-fashion.test + - contentinfo [ref=f3e43]: + - generic [ref=f3e44]: + - generic [ref=f3e45]: + - paragraph [ref=f3e46]: Acme Fashion + - paragraph [ref=f3e47]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=f3e48]: + - link "Shop all collections" [ref=f3e49] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f3e50] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=f3e51]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-07-38-774Z.yml b/.playwright-mcp/page-2026-07-11T11-07-38-774Z.yml new file mode 100644 index 00000000..702c3446 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-07-38-774Z.yml @@ -0,0 +1,98 @@ +- generic [active] [ref=e1]: + - link "Skip to main content" [ref=e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=e3]: Free shipping available with code FREESHIP + - banner [ref=e4]: + - generic [ref=e5]: + - link "Acme Fashion" [ref=e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=e7]: + - link "Home" [ref=e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=e12]: + - button "Open product search" [ref=e14]: + - generic [ref=e19]: Search + - button "Open shopping cart" [ref=e21]: + - generic [ref=e26]: Cart + - main [ref=e27]: + - generic [ref=e28]: + - navigation "Breadcrumb" [ref=e29]: + - list [ref=e30]: + - listitem [ref=e31]: + - link "Home" [ref=e32] [cursor=pointer]: + - /url: http://acme-fashion.test + - generic [ref=e33]: / + - listitem [ref=e34]: + - link "Products" [ref=e35] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - generic [ref=e36]: / + - listitem [ref=e37]: + - generic [ref=e38]: Classic Cotton T-Shirt + - generic [ref=e39]: + - img "Classic Cotton T-Shirt" [ref=e40] + - generic [ref=e41]: + - generic [ref=e42]: + - paragraph [ref=e43]: Acme Basics + - heading "Classic Cotton T-Shirt" [level=1] [ref=e44] + - generic [ref=e45]: €24.99 + - group "Choose a variant" [ref=e48]: + - generic [ref=e50]: + - generic [ref=e51] [cursor=pointer]: + - generic [ref=e52]: ACME-CLASSICC-S-WHITE-1 + - radio "Select variant ACME-CLASSICC-S-WHITE-1" [checked] [ref=e53] + - generic [ref=e54] [cursor=pointer]: + - generic [ref=e55]: ACME-CLASSICC-S-BLACK-2 + - radio "Select variant ACME-CLASSICC-S-BLACK-2" [ref=e56] + - generic [ref=e57] [cursor=pointer]: + - generic [ref=e58]: ACME-CLASSICC-S-NAVY-3 + - radio "Select variant ACME-CLASSICC-S-NAVY-3" [ref=e59] + - generic [ref=e60] [cursor=pointer]: + - generic [ref=e61]: ACME-CLASSICC-M-WHITE-4 + - radio "Select variant ACME-CLASSICC-M-WHITE-4" [ref=e62] + - generic [ref=e63] [cursor=pointer]: + - generic [ref=e64]: ACME-CLASSICC-M-BLACK-5 + - radio "Select variant ACME-CLASSICC-M-BLACK-5" [ref=e65] + - generic [ref=e66] [cursor=pointer]: + - generic [ref=e67]: ACME-CLASSICC-M-NAVY-6 + - radio "Select variant ACME-CLASSICC-M-NAVY-6" [ref=e68] + - generic [ref=e69] [cursor=pointer]: + - generic [ref=e70]: ACME-CLASSICC-L-WHITE-7 + - radio "Select variant ACME-CLASSICC-L-WHITE-7" [ref=e71] + - generic [ref=e72] [cursor=pointer]: + - generic [ref=e73]: ACME-CLASSICC-L-BLACK-8 + - radio "Select variant ACME-CLASSICC-L-BLACK-8" [ref=e74] + - generic [ref=e75] [cursor=pointer]: + - generic [ref=e76]: ACME-CLASSICC-L-NAVY-9 + - radio "Select variant ACME-CLASSICC-L-NAVY-9" [ref=e77] + - generic [ref=e78] [cursor=pointer]: + - generic [ref=e79]: ACME-CLASSICC-XL-WHITE-10 + - radio "Select variant ACME-CLASSICC-XL-WHITE-10" [ref=e80] + - generic [ref=e81] [cursor=pointer]: + - generic [ref=e82]: ACME-CLASSICC-XL-BLACK-11 + - radio "Select variant ACME-CLASSICC-XL-BLACK-11" [ref=e83] + - generic [ref=e84] [cursor=pointer]: + - generic [ref=e85]: ACME-CLASSICC-XL-NAVY-12 + - radio "Select variant ACME-CLASSICC-XL-NAVY-12" [ref=e86] + - paragraph [ref=e87]: In stock + - generic [ref=e88]: + - generic [ref=e89]: Quantity + - spinbutton "Quantity" [ref=e91]: "1" + - button "Add to cart" [ref=e92] + - paragraph [ref=e99]: A timeless classic cotton t-shirt. Comfortable, breathable, and perfect for everyday wear. + - contentinfo [ref=e100]: + - generic [ref=e101]: + - generic [ref=e102]: + - paragraph [ref=e103]: Acme Fashion + - paragraph [ref=e104]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=e105]: + - link "Shop all collections" [ref=e106] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=e107] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=e108]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-07-39-867Z.yml b/.playwright-mcp/page-2026-07-11T11-07-39-867Z.yml new file mode 100644 index 00000000..affd4429 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-07-39-867Z.yml @@ -0,0 +1,110 @@ +- generic [active] [ref=e1]: + - link "Skip to main content" [ref=e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=e3]: Free shipping available with code FREESHIP + - banner [ref=e4]: + - generic [ref=e5]: + - link "Acme Fashion" [ref=e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=e7]: + - link "Home" [ref=e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=e12]: + - button "Open product search" [ref=e14]: + - generic [ref=e19]: Search + - generic [ref=e20]: + - button "Open shopping cart" [ref=e21]: + - generic [ref=e26]: Cart (1) + - dialog [ref=e109]: + - generic [ref=e110]: + - generic [ref=e111]: Shopping cart + - generic [ref=e112]: + - generic [ref=e113]: + - paragraph [ref=e114]: Classic Cotton T-Shirt + - paragraph [ref=e115]: Qty 1 + - button "Remove" [ref=e116] + - link "View cart" [ref=e122] [cursor=pointer]: + - /url: http://acme-fashion.test/cart + - button "Close modal" [ref=e125] + - main [ref=e27]: + - generic [ref=e28]: + - navigation "Breadcrumb" [ref=e29]: + - list [ref=e30]: + - listitem [ref=e31]: + - link "Home" [ref=e32] [cursor=pointer]: + - /url: http://acme-fashion.test + - generic [ref=e33]: / + - listitem [ref=e34]: + - link "Products" [ref=e35] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - generic [ref=e36]: / + - listitem [ref=e37]: + - generic [ref=e38]: Classic Cotton T-Shirt + - generic [ref=e39]: + - img "Classic Cotton T-Shirt" [ref=e40] + - generic [ref=e41]: + - generic [ref=e42]: + - paragraph [ref=e43]: Acme Basics + - heading "Classic Cotton T-Shirt" [level=1] [ref=e44] + - generic [ref=e45]: €24.99 + - group "Choose a variant" [ref=e48]: + - generic [ref=e50]: + - generic [ref=e51] [cursor=pointer]: + - generic [ref=e52]: ACME-CLASSICC-S-WHITE-1 + - radio "Select variant ACME-CLASSICC-S-WHITE-1" [checked] [ref=e53] + - generic [ref=e54] [cursor=pointer]: + - generic [ref=e55]: ACME-CLASSICC-S-BLACK-2 + - radio "Select variant ACME-CLASSICC-S-BLACK-2" [ref=e56] + - generic [ref=e57] [cursor=pointer]: + - generic [ref=e58]: ACME-CLASSICC-S-NAVY-3 + - radio "Select variant ACME-CLASSICC-S-NAVY-3" [ref=e59] + - generic [ref=e60] [cursor=pointer]: + - generic [ref=e61]: ACME-CLASSICC-M-WHITE-4 + - radio "Select variant ACME-CLASSICC-M-WHITE-4" [ref=e62] + - generic [ref=e63] [cursor=pointer]: + - generic [ref=e64]: ACME-CLASSICC-M-BLACK-5 + - radio "Select variant ACME-CLASSICC-M-BLACK-5" [ref=e65] + - generic [ref=e66] [cursor=pointer]: + - generic [ref=e67]: ACME-CLASSICC-M-NAVY-6 + - radio "Select variant ACME-CLASSICC-M-NAVY-6" [ref=e68] + - generic [ref=e69] [cursor=pointer]: + - generic [ref=e70]: ACME-CLASSICC-L-WHITE-7 + - radio "Select variant ACME-CLASSICC-L-WHITE-7" [ref=e71] + - generic [ref=e72] [cursor=pointer]: + - generic [ref=e73]: ACME-CLASSICC-L-BLACK-8 + - radio "Select variant ACME-CLASSICC-L-BLACK-8" [ref=e74] + - generic [ref=e75] [cursor=pointer]: + - generic [ref=e76]: ACME-CLASSICC-L-NAVY-9 + - radio "Select variant ACME-CLASSICC-L-NAVY-9" [ref=e77] + - generic [ref=e78] [cursor=pointer]: + - generic [ref=e79]: ACME-CLASSICC-XL-WHITE-10 + - radio "Select variant ACME-CLASSICC-XL-WHITE-10" [ref=e80] + - generic [ref=e81] [cursor=pointer]: + - generic [ref=e82]: ACME-CLASSICC-XL-BLACK-11 + - radio "Select variant ACME-CLASSICC-XL-BLACK-11" [ref=e83] + - generic [ref=e84] [cursor=pointer]: + - generic [ref=e85]: ACME-CLASSICC-XL-NAVY-12 + - radio "Select variant ACME-CLASSICC-XL-NAVY-12" [ref=e86] + - paragraph [ref=e87]: In stock + - generic [ref=e88]: + - generic [ref=e89]: Quantity + - spinbutton "Quantity" [ref=e91]: "1" + - button "Add to cart" [ref=e92] + - paragraph [ref=e99]: A timeless classic cotton t-shirt. Comfortable, breathable, and perfect for everyday wear. + - contentinfo [ref=e100]: + - generic [ref=e101]: + - generic [ref=e102]: + - paragraph [ref=e103]: Acme Fashion + - paragraph [ref=e104]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=e105]: + - link "Shop all collections" [ref=e106] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=e107] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=e108]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-07-40-483Z.yml b/.playwright-mcp/page-2026-07-11T11-07-40-483Z.yml new file mode 100644 index 00000000..941753d2 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-07-40-483Z.yml @@ -0,0 +1,54 @@ +- generic [active] [ref=f1e1]: + - link "Skip to main content" [ref=f1e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=f1e3]: Free shipping available with code FREESHIP + - banner [ref=f1e4]: + - generic [ref=f1e5]: + - link "Acme Fashion" [ref=f1e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=f1e7]: + - link "Home" [ref=f1e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=f1e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f1e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=f1e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=f1e12]: + - button "Open product search" [ref=f1e14]: + - generic [ref=f1e19]: Search + - button "Open shopping cart" [ref=f1e21]: + - generic [ref=f1e26]: Cart + - main [ref=f1e27]: + - generic [ref=f1e28]: + - heading "Your cart" [level=1] [ref=f1e29] + - generic [ref=f1e30]: + - article [ref=f1e32]: + - generic [ref=f1e34]: + - heading "Classic Cotton T-Shirt" [level=2] [ref=f1e35] + - generic [ref=f1e36]: €24.99 + - generic [ref=f1e38]: + - button "Decrease quantity" [ref=f1e39]: + - generic [ref=f1e44]: − + - generic [ref=f1e45]: "1" + - button "Increase quantity" [ref=f1e46]: + - generic [ref=f1e51]: + + - button "Remove" [ref=f1e52] + - complementary [ref=f1e58]: + - heading "Order summary" [level=2] [ref=f1e59] + - generic [ref=f1e60]: + - generic [ref=f1e61]: Subtotal + - generic [ref=f1e62]: €24.99 + - button "Checkout" [ref=f1e64] + - contentinfo [ref=f1e70]: + - generic [ref=f1e71]: + - generic [ref=f1e72]: + - paragraph [ref=f1e73]: Acme Fashion + - paragraph [ref=f1e74]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=f1e75]: + - link "Shop all collections" [ref=f1e76] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f1e77] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=f1e78]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-07-41-607Z.yml b/.playwright-mcp/page-2026-07-11T11-07-41-607Z.yml new file mode 100644 index 00000000..a321819a --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-07-41-607Z.yml @@ -0,0 +1,78 @@ +- generic [active] [ref=f2e1]: + - link "Skip to main content" [ref=f2e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=f2e3]: Free shipping available with code FREESHIP + - banner [ref=f2e4]: + - generic [ref=f2e5]: + - link "Acme Fashion" [ref=f2e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=f2e7]: + - link "Home" [ref=f2e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=f2e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f2e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=f2e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=f2e12]: + - button "Open product search" [ref=f2e14]: + - generic [ref=f2e19]: Search + - button "Open shopping cart" [ref=f2e21]: + - generic [ref=f2e26]: Cart + - main [ref=f2e27]: + - generic [ref=f2e28]: + - heading "Checkout" [level=1] [ref=f2e29] + - generic [ref=f2e30]: + - generic [ref=f2e32]: + - generic [ref=f2e33]: 1. Contact and shipping address + - generic [ref=f2e34]: + - generic [ref=f2e35]: + - generic [ref=f2e36]: Email + - textbox "Email" [ref=f2e38] + - generic [ref=f2e39]: + - generic [ref=f2e40]: First name + - textbox "First name" [ref=f2e42] + - generic [ref=f2e43]: + - generic [ref=f2e44]: Last name + - textbox "Last name" [ref=f2e46] + - generic [ref=f2e47]: + - generic [ref=f2e48]: Address + - textbox "Address" [ref=f2e50] + - generic [ref=f2e51]: + - generic [ref=f2e52]: City + - textbox "City" [ref=f2e54] + - generic [ref=f2e55]: + - generic [ref=f2e56]: Postal code + - textbox "Postal code" [ref=f2e58] + - generic [ref=f2e59]: + - generic [ref=f2e60]: Country code + - textbox "Country code" [ref=f2e62]: DE + - generic [ref=f2e63]: + - generic [ref=f2e64]: Phone (optional) + - textbox "Phone (optional)" [ref=f2e66] + - button "Continue to shipping" [ref=f2e67] + - complementary [ref=f2e73]: + - heading "Order summary" [level=2] [ref=f2e74] + - generic [ref=f2e76]: + - generic [ref=f2e77]: 1 × Classic Cotton T-Shirt + - generic [ref=f2e78]: €24.99 + - generic [ref=f2e81]: + - generic [ref=f2e82]: Total + - generic [ref=f2e83]: €24.99 + - generic [ref=f2e85]: + - generic [ref=f2e86]: + - generic [ref=f2e87]: Discount code + - textbox "Discount code" [ref=f2e89] + - button "Apply discount" [ref=f2e90] + - contentinfo [ref=f2e96]: + - generic [ref=f2e97]: + - generic [ref=f2e98]: + - paragraph [ref=f2e99]: Acme Fashion + - paragraph [ref=f2e100]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=f2e101]: + - link "Shop all collections" [ref=f2e102] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f2e103] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=f2e104]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-07-51-525Z.yml b/.playwright-mcp/page-2026-07-11T11-07-51-525Z.yml new file mode 100644 index 00000000..adc0f522 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-07-51-525Z.yml @@ -0,0 +1,89 @@ +- generic [active] [ref=f2e1]: + - link "Skip to main content" [ref=f2e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=f2e3]: Free shipping available with code FREESHIP + - banner [ref=f2e4]: + - generic [ref=f2e5]: + - link "Acme Fashion" [ref=f2e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=f2e7]: + - link "Home" [ref=f2e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=f2e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f2e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=f2e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=f2e12]: + - button "Open product search" [ref=f2e14]: + - generic [ref=f2e19]: Search + - button "Open shopping cart" [ref=f2e21]: + - generic [ref=f2e26]: Cart + - main [ref=f2e27]: + - generic [ref=f2e28]: + - heading "Checkout" [level=1] [ref=f2e29] + - generic [ref=f2e30]: + - generic [ref=f2e31]: + - generic [ref=f2e32]: + - generic [ref=f2e33]: 1. Contact and shipping address + - generic [ref=f2e34]: + - generic [ref=f2e35]: + - generic [ref=f2e36]: Email + - textbox "Email" [ref=f2e38]: decline-review@example.test + - generic [ref=f2e39]: + - generic [ref=f2e40]: First name + - textbox "First name" [ref=f2e42]: Decline + - generic [ref=f2e43]: + - generic [ref=f2e44]: Last name + - textbox "Last name" [ref=f2e46]: Reviewer + - generic [ref=f2e47]: + - generic [ref=f2e48]: Address + - textbox "Address" [ref=f2e50]: Teststrasse 42 + - generic [ref=f2e51]: + - generic [ref=f2e52]: City + - textbox "City" [ref=f2e54]: Berlin + - generic [ref=f2e55]: + - generic [ref=f2e56]: Postal code + - textbox "Postal code" [ref=f2e58]: "10115" + - generic [ref=f2e59]: + - generic [ref=f2e60]: Country code + - textbox "Country code" [ref=f2e62]: DE + - generic [ref=f2e63]: + - generic [ref=f2e64]: Phone (optional) + - textbox "Phone (optional)" [ref=f2e66] + - button "Continue to shipping" [ref=f2e67] + - generic [ref=f2e105]: + - generic [ref=f2e106]: 2. Shipping method + - group "Choose a shipping method" [ref=f2e107]: + - generic [ref=f2e109] [cursor=pointer]: + - generic [ref=f2e110]: Standard Shipping + - radio "Standard Shipping" [ref=f2e111] + - generic [ref=f2e112] [cursor=pointer]: + - generic [ref=f2e113]: Express Shipping + - radio "Express Shipping" [ref=f2e114] + - button "Continue to payment" [ref=f2e115] + - complementary [ref=f2e73]: + - heading "Order summary" [level=2] [ref=f2e74] + - generic [ref=f2e76]: + - generic [ref=f2e77]: 1 × Classic Cotton T-Shirt + - generic [ref=f2e78]: €24.99 + - generic [ref=f2e81]: + - generic [ref=f2e82]: Total + - generic [ref=f2e83]: €24.99 + - generic [ref=f2e85]: + - generic [ref=f2e86]: + - generic [ref=f2e87]: Discount code + - textbox "Discount code" [ref=f2e89] + - button "Apply discount" [ref=f2e90] + - contentinfo [ref=f2e96]: + - generic [ref=f2e97]: + - generic [ref=f2e98]: + - paragraph [ref=f2e99]: Acme Fashion + - paragraph [ref=f2e100]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=f2e101]: + - link "Shop all collections" [ref=f2e102] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f2e103] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=f2e104]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-07-59-019Z.yml b/.playwright-mcp/page-2026-07-11T11-07-59-019Z.yml new file mode 100644 index 00000000..11c59541 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-07-59-019Z.yml @@ -0,0 +1,89 @@ +- generic [ref=f2e1]: + - link "Skip to main content" [ref=f2e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=f2e3]: Free shipping available with code FREESHIP + - banner [ref=f2e4]: + - generic [ref=f2e5]: + - link "Acme Fashion" [ref=f2e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=f2e7]: + - link "Home" [ref=f2e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=f2e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f2e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=f2e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=f2e12]: + - button "Open product search" [ref=f2e14]: + - generic [ref=f2e19]: Search + - button "Open shopping cart" [ref=f2e21]: + - generic [ref=f2e26]: Cart + - main [ref=f2e27]: + - generic [ref=f2e28]: + - heading "Checkout" [level=1] [ref=f2e29] + - generic [ref=f2e30]: + - generic [ref=f2e31]: + - generic [ref=f2e32]: + - generic [ref=f2e33]: 1. Contact and shipping address + - generic [ref=f2e34]: + - generic [ref=f2e35]: + - generic [ref=f2e36]: Email + - textbox "Email" [ref=f2e38]: decline-review@example.test + - generic [ref=f2e39]: + - generic [ref=f2e40]: First name + - textbox "First name" [ref=f2e42]: Decline + - generic [ref=f2e43]: + - generic [ref=f2e44]: Last name + - textbox "Last name" [ref=f2e46]: Reviewer + - generic [ref=f2e47]: + - generic [ref=f2e48]: Address + - textbox "Address" [ref=f2e50]: Teststrasse 42 + - generic [ref=f2e51]: + - generic [ref=f2e52]: City + - textbox "City" [ref=f2e54]: Berlin + - generic [ref=f2e55]: + - generic [ref=f2e56]: Postal code + - textbox "Postal code" [ref=f2e58]: "10115" + - generic [ref=f2e59]: + - generic [ref=f2e60]: Country code + - textbox "Country code" [ref=f2e62]: DE + - generic [ref=f2e63]: + - generic [ref=f2e64]: Phone (optional) + - textbox "Phone (optional)" [ref=f2e66] + - button "Continue to shipping" [ref=f2e67] + - generic [ref=f2e105]: + - generic [ref=f2e106]: 2. Shipping method + - group "Choose a shipping method" [ref=f2e107]: + - generic [ref=f2e109] [cursor=pointer]: + - generic [ref=f2e110]: Standard Shipping + - radio "Standard Shipping" [checked] [active] [ref=f2e111] + - generic [ref=f2e112] [cursor=pointer]: + - generic [ref=f2e113]: Express Shipping + - radio "Express Shipping" [ref=f2e114] + - button "Continue to payment" [ref=f2e115] + - complementary [ref=f2e73]: + - heading "Order summary" [level=2] [ref=f2e74] + - generic [ref=f2e76]: + - generic [ref=f2e77]: 1 × Classic Cotton T-Shirt + - generic [ref=f2e78]: €24.99 + - generic [ref=f2e81]: + - generic [ref=f2e82]: Total + - generic [ref=f2e83]: €24.99 + - generic [ref=f2e85]: + - generic [ref=f2e86]: + - generic [ref=f2e87]: Discount code + - textbox "Discount code" [ref=f2e89] + - button "Apply discount" [ref=f2e90] + - contentinfo [ref=f2e96]: + - generic [ref=f2e97]: + - generic [ref=f2e98]: + - paragraph [ref=f2e99]: Acme Fashion + - paragraph [ref=f2e100]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=f2e101]: + - link "Shop all collections" [ref=f2e102] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f2e103] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=f2e104]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-08-00-072Z.yml b/.playwright-mcp/page-2026-07-11T11-08-00-072Z.yml new file mode 100644 index 00000000..2c646cad --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-08-00-072Z.yml @@ -0,0 +1,105 @@ +- generic [ref=f2e1]: + - link "Skip to main content" [ref=f2e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=f2e3]: Free shipping available with code FREESHIP + - banner [ref=f2e4]: + - generic [ref=f2e5]: + - link "Acme Fashion" [ref=f2e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=f2e7]: + - link "Home" [ref=f2e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=f2e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f2e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=f2e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=f2e12]: + - button "Open product search" [ref=f2e14]: + - generic [ref=f2e19]: Search + - button "Open shopping cart" [ref=f2e21]: + - generic [ref=f2e26]: Cart + - main [ref=f2e27]: + - generic [ref=f2e28]: + - heading "Checkout" [level=1] [ref=f2e29] + - generic [ref=f2e30]: + - generic [ref=f2e31]: + - generic [ref=f2e32]: + - generic [ref=f2e33]: 1. Contact and shipping address + - generic [ref=f2e34]: + - generic [ref=f2e35]: + - generic [ref=f2e36]: Email + - textbox "Email" [ref=f2e38]: decline-review@example.test + - generic [ref=f2e39]: + - generic [ref=f2e40]: First name + - textbox "First name" [ref=f2e42]: Decline + - generic [ref=f2e43]: + - generic [ref=f2e44]: Last name + - textbox "Last name" [ref=f2e46]: Reviewer + - generic [ref=f2e47]: + - generic [ref=f2e48]: Address + - textbox "Address" [ref=f2e50]: Teststrasse 42 + - generic [ref=f2e51]: + - generic [ref=f2e52]: City + - textbox "City" [ref=f2e54]: Berlin + - generic [ref=f2e55]: + - generic [ref=f2e56]: Postal code + - textbox "Postal code" [ref=f2e58]: "10115" + - generic [ref=f2e59]: + - generic [ref=f2e60]: Country code + - textbox "Country code" [ref=f2e62]: DE + - generic [ref=f2e63]: + - generic [ref=f2e64]: Phone (optional) + - textbox "Phone (optional)" [ref=f2e66] + - button "Continue to shipping" [ref=f2e67] + - generic [ref=f2e105]: + - generic [ref=f2e106]: 2. Shipping method + - group "Choose a shipping method" [ref=f2e107]: + - generic [ref=f2e109] [cursor=pointer]: + - generic [ref=f2e110]: Standard Shipping + - radio "Standard Shipping" [checked] [ref=f2e111] + - generic [ref=f2e112] [cursor=pointer]: + - generic [ref=f2e113]: Express Shipping + - radio "Express Shipping" [ref=f2e114] + - button "Continue to payment" [active] [ref=f2e115] + - generic [ref=f2e121]: + - generic [ref=f2e122]: 3. Payment + - group "Payment method" [ref=f2e123]: + - generic [ref=f2e125] [cursor=pointer]: + - radio "Credit card" [checked] [ref=f2e126] + - generic [ref=f2e127]: Credit card + - generic [ref=f2e128] [cursor=pointer]: + - radio "PayPal" [ref=f2e129] + - generic [ref=f2e130]: PayPal + - generic [ref=f2e131] [cursor=pointer]: + - radio "Bank transfer" [ref=f2e132] + - generic [ref=f2e133]: Bank transfer + - generic [ref=f2e134]: + - generic [ref=f2e135]: Card number + - textbox "Card number" [ref=f2e137]: 4242 4242 4242 4242 + - button "Review payment" [ref=f2e138] + - complementary [ref=f2e73]: + - heading "Order summary" [level=2] [ref=f2e74] + - generic [ref=f2e76]: + - generic [ref=f2e77]: 1 × Classic Cotton T-Shirt + - generic [ref=f2e78]: €24.99 + - generic [ref=f2e81]: + - generic [ref=f2e82]: Total + - generic [ref=f2e83]: €29.98 + - generic [ref=f2e85]: + - generic [ref=f2e86]: + - generic [ref=f2e87]: Discount code + - textbox "Discount code" [ref=f2e89] + - button "Apply discount" [ref=f2e90] + - contentinfo [ref=f2e96]: + - generic [ref=f2e97]: + - generic [ref=f2e98]: + - paragraph [ref=f2e99]: Acme Fashion + - paragraph [ref=f2e100]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=f2e101]: + - link "Shop all collections" [ref=f2e102] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f2e103] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=f2e104]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-08-01-120Z.yml b/.playwright-mcp/page-2026-07-11T11-08-01-120Z.yml new file mode 100644 index 00000000..bd4af3cd --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-08-01-120Z.yml @@ -0,0 +1,105 @@ +- generic [ref=f2e1]: + - link "Skip to main content" [ref=f2e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=f2e3]: Free shipping available with code FREESHIP + - banner [ref=f2e4]: + - generic [ref=f2e5]: + - link "Acme Fashion" [ref=f2e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=f2e7]: + - link "Home" [ref=f2e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=f2e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f2e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=f2e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=f2e12]: + - button "Open product search" [ref=f2e14]: + - generic [ref=f2e19]: Search + - button "Open shopping cart" [ref=f2e21]: + - generic [ref=f2e26]: Cart + - main [ref=f2e27]: + - generic [ref=f2e28]: + - heading "Checkout" [level=1] [ref=f2e29] + - generic [ref=f2e30]: + - generic [ref=f2e31]: + - generic [ref=f2e32]: + - generic [ref=f2e33]: 1. Contact and shipping address + - generic [ref=f2e34]: + - generic [ref=f2e35]: + - generic [ref=f2e36]: Email + - textbox "Email" [ref=f2e38]: decline-review@example.test + - generic [ref=f2e39]: + - generic [ref=f2e40]: First name + - textbox "First name" [ref=f2e42]: Decline + - generic [ref=f2e43]: + - generic [ref=f2e44]: Last name + - textbox "Last name" [ref=f2e46]: Reviewer + - generic [ref=f2e47]: + - generic [ref=f2e48]: Address + - textbox "Address" [ref=f2e50]: Teststrasse 42 + - generic [ref=f2e51]: + - generic [ref=f2e52]: City + - textbox "City" [ref=f2e54]: Berlin + - generic [ref=f2e55]: + - generic [ref=f2e56]: Postal code + - textbox "Postal code" [ref=f2e58]: "10115" + - generic [ref=f2e59]: + - generic [ref=f2e60]: Country code + - textbox "Country code" [ref=f2e62]: DE + - generic [ref=f2e63]: + - generic [ref=f2e64]: Phone (optional) + - textbox "Phone (optional)" [ref=f2e66] + - button "Continue to shipping" [ref=f2e67] + - generic [ref=f2e105]: + - generic [ref=f2e106]: 2. Shipping method + - group "Choose a shipping method" [ref=f2e107]: + - generic [ref=f2e109] [cursor=pointer]: + - generic [ref=f2e110]: Standard Shipping + - radio "Standard Shipping" [checked] [ref=f2e111] + - generic [ref=f2e112] [cursor=pointer]: + - generic [ref=f2e113]: Express Shipping + - radio "Express Shipping" [ref=f2e114] + - button "Continue to payment" [ref=f2e115] + - generic [ref=f2e121]: + - generic [ref=f2e122]: 3. Payment + - group "Payment method" [ref=f2e123]: + - generic [ref=f2e125] [cursor=pointer]: + - radio "Credit card" [checked] [ref=f2e126] + - generic [ref=f2e127]: Credit card + - generic [ref=f2e128] [cursor=pointer]: + - radio "PayPal" [ref=f2e129] + - generic [ref=f2e130]: PayPal + - generic [ref=f2e131] [cursor=pointer]: + - radio "Bank transfer" [ref=f2e132] + - generic [ref=f2e133]: Bank transfer + - generic [ref=f2e134]: + - generic [ref=f2e135]: Card number + - textbox "Card number" [ref=f2e137]: 4242 4242 4242 4242 + - button "Pay now" [active] [ref=f2e144] + - complementary [ref=f2e73]: + - heading "Order summary" [level=2] [ref=f2e74] + - generic [ref=f2e76]: + - generic [ref=f2e77]: 1 × Classic Cotton T-Shirt + - generic [ref=f2e78]: €24.99 + - generic [ref=f2e81]: + - generic [ref=f2e82]: Total + - generic [ref=f2e83]: €29.98 + - generic [ref=f2e85]: + - generic [ref=f2e86]: + - generic [ref=f2e87]: Discount code + - textbox "Discount code" [ref=f2e89] + - button "Apply discount" [ref=f2e90] + - contentinfo [ref=f2e96]: + - generic [ref=f2e97]: + - generic [ref=f2e98]: + - paragraph [ref=f2e99]: Acme Fashion + - paragraph [ref=f2e100]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=f2e101]: + - link "Shop all collections" [ref=f2e102] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f2e103] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=f2e104]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-08-11-001Z.yml b/.playwright-mcp/page-2026-07-11T11-08-11-001Z.yml new file mode 100644 index 00000000..ee6a3037 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-08-11-001Z.yml @@ -0,0 +1,106 @@ +- generic [ref=f2e1]: + - link "Skip to main content" [ref=f2e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=f2e3]: Free shipping available with code FREESHIP + - banner [ref=f2e4]: + - generic [ref=f2e5]: + - link "Acme Fashion" [ref=f2e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=f2e7]: + - link "Home" [ref=f2e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=f2e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f2e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=f2e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=f2e12]: + - button "Open product search" [ref=f2e14]: + - generic [ref=f2e19]: Search + - button "Open shopping cart" [ref=f2e21]: + - generic [ref=f2e26]: Cart + - main [ref=f2e27]: + - generic [ref=f2e28]: + - heading "Checkout" [level=1] [ref=f2e29] + - generic [ref=f2e30]: + - generic [ref=f2e31]: + - generic [ref=f2e32]: + - generic [ref=f2e33]: 1. Contact and shipping address + - generic [ref=f2e34]: + - generic [ref=f2e35]: + - generic [ref=f2e36]: Email + - textbox "Email" [ref=f2e38]: decline-review@example.test + - generic [ref=f2e39]: + - generic [ref=f2e40]: First name + - textbox "First name" [ref=f2e42]: Decline + - generic [ref=f2e43]: + - generic [ref=f2e44]: Last name + - textbox "Last name" [ref=f2e46]: Reviewer + - generic [ref=f2e47]: + - generic [ref=f2e48]: Address + - textbox "Address" [ref=f2e50]: Teststrasse 42 + - generic [ref=f2e51]: + - generic [ref=f2e52]: City + - textbox "City" [ref=f2e54]: Berlin + - generic [ref=f2e55]: + - generic [ref=f2e56]: Postal code + - textbox "Postal code" [ref=f2e58]: "10115" + - generic [ref=f2e59]: + - generic [ref=f2e60]: Country code + - textbox "Country code" [ref=f2e62]: DE + - generic [ref=f2e63]: + - generic [ref=f2e64]: Phone (optional) + - textbox "Phone (optional)" [ref=f2e66] + - button "Continue to shipping" [ref=f2e67] + - generic [ref=f2e105]: + - generic [ref=f2e106]: 2. Shipping method + - group "Choose a shipping method" [ref=f2e107]: + - generic [ref=f2e109] [cursor=pointer]: + - generic [ref=f2e110]: Standard Shipping + - radio "Standard Shipping" [checked] [ref=f2e111] + - generic [ref=f2e112] [cursor=pointer]: + - generic [ref=f2e113]: Express Shipping + - radio "Express Shipping" [ref=f2e114] + - button "Continue to payment" [ref=f2e115] + - generic [ref=f2e121]: + - generic [ref=f2e122]: 3. Payment + - group "Payment method" [ref=f2e123]: + - generic [ref=f2e125] [cursor=pointer]: + - radio "Credit card" [checked] [ref=f2e126] + - generic [ref=f2e127]: Credit card + - generic [ref=f2e128] [cursor=pointer]: + - radio "PayPal" [ref=f2e129] + - generic [ref=f2e130]: PayPal + - generic [ref=f2e131] [cursor=pointer]: + - radio "Bank transfer" [ref=f2e132] + - generic [ref=f2e133]: Bank transfer + - generic [ref=f2e134]: + - generic [ref=f2e135]: Card number + - textbox "Card number" [invalid] [ref=f2e137]: 4000 0000 0000 0002 + - alert [ref=f2e145]: The payment was declined. + - button "Review payment" [active] [ref=f2e148] + - complementary [ref=f2e73]: + - heading "Order summary" [level=2] [ref=f2e74] + - generic [ref=f2e76]: + - generic [ref=f2e77]: 1 × Classic Cotton T-Shirt + - generic [ref=f2e78]: €24.99 + - generic [ref=f2e81]: + - generic [ref=f2e82]: Total + - generic [ref=f2e83]: €29.98 + - generic [ref=f2e85]: + - generic [ref=f2e86]: + - generic [ref=f2e87]: Discount code + - textbox "Discount code" [ref=f2e89] + - button "Apply discount" [ref=f2e90] + - contentinfo [ref=f2e96]: + - generic [ref=f2e97]: + - generic [ref=f2e98]: + - paragraph [ref=f2e99]: Acme Fashion + - paragraph [ref=f2e100]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=f2e101]: + - link "Shop all collections" [ref=f2e102] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f2e103] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=f2e104]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-08-26-207Z.yml b/.playwright-mcp/page-2026-07-11T11-08-26-207Z.yml new file mode 100644 index 00000000..18b7912f --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-08-26-207Z.yml @@ -0,0 +1,102 @@ +- generic [ref=f2e1]: + - link "Skip to main content" [ref=f2e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=f2e3]: Free shipping available with code FREESHIP + - banner [ref=f2e4]: + - generic [ref=f2e5]: + - link "Acme Fashion" [ref=f2e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=f2e7]: + - link "Home" [ref=f2e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=f2e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f2e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=f2e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=f2e12]: + - button "Open product search" [ref=f2e14]: + - generic [ref=f2e19]: Search + - button "Open shopping cart" [ref=f2e21]: + - generic [ref=f2e26]: Cart + - main [ref=f2e27]: + - generic [ref=f2e28]: + - heading "Checkout" [level=1] [ref=f2e29] + - generic [ref=f2e30]: + - generic [ref=f2e31]: + - generic [ref=f2e32]: + - generic [ref=f2e33]: 1. Contact and shipping address + - generic [ref=f2e34]: + - generic [ref=f2e35]: + - generic [ref=f2e36]: Email + - textbox "Email" [ref=f2e38]: decline-review@example.test + - generic [ref=f2e39]: + - generic [ref=f2e40]: First name + - textbox "First name" [ref=f2e42]: Decline + - generic [ref=f2e43]: + - generic [ref=f2e44]: Last name + - textbox "Last name" [ref=f2e46]: Reviewer + - generic [ref=f2e47]: + - generic [ref=f2e48]: Address + - textbox "Address" [ref=f2e50]: Teststrasse 42 + - generic [ref=f2e51]: + - generic [ref=f2e52]: City + - textbox "City" [ref=f2e54]: Berlin + - generic [ref=f2e55]: + - generic [ref=f2e56]: Postal code + - textbox "Postal code" [ref=f2e58]: "10115" + - generic [ref=f2e59]: + - generic [ref=f2e60]: Country code + - textbox "Country code" [ref=f2e62]: DE + - generic [ref=f2e63]: + - generic [ref=f2e64]: Phone (optional) + - textbox "Phone (optional)" [ref=f2e66] + - button "Continue to shipping" [ref=f2e67] + - generic [ref=f2e105]: + - generic [ref=f2e106]: 2. Shipping method + - group "Choose a shipping method" [ref=f2e107]: + - generic [ref=f2e109] [cursor=pointer]: + - generic [ref=f2e110]: Standard Shipping + - radio "Standard Shipping" [checked] [ref=f2e111] + - generic [ref=f2e112] [cursor=pointer]: + - generic [ref=f2e113]: Express Shipping + - radio "Express Shipping" [ref=f2e114] + - button "Continue to payment" [ref=f2e115] + - generic [ref=f2e121]: + - generic [ref=f2e122]: 3. Payment + - group "Payment method" [ref=f2e123]: + - generic [ref=f2e125] [cursor=pointer]: + - radio "Credit card" [ref=f2e126] + - generic [ref=f2e127]: Credit card + - generic [ref=f2e128] [cursor=pointer]: + - radio "PayPal" [ref=f2e129] + - generic [ref=f2e130]: PayPal + - generic [ref=f2e131] [cursor=pointer]: + - radio "Bank transfer" [checked] [active] [ref=f2e132] + - generic [ref=f2e133]: Bank transfer + - button "Review payment" [ref=f2e148] + - complementary [ref=f2e73]: + - heading "Order summary" [level=2] [ref=f2e74] + - generic [ref=f2e76]: + - generic [ref=f2e77]: 1 × Classic Cotton T-Shirt + - generic [ref=f2e78]: €24.99 + - generic [ref=f2e81]: + - generic [ref=f2e82]: Total + - generic [ref=f2e83]: €29.98 + - generic [ref=f2e85]: + - generic [ref=f2e86]: + - generic [ref=f2e87]: Discount code + - textbox "Discount code" [ref=f2e89] + - button "Apply discount" [ref=f2e90] + - contentinfo [ref=f2e96]: + - generic [ref=f2e97]: + - generic [ref=f2e98]: + - paragraph [ref=f2e99]: Acme Fashion + - paragraph [ref=f2e100]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=f2e101]: + - link "Shop all collections" [ref=f2e102] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f2e103] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=f2e104]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-08-27-255Z.yml b/.playwright-mcp/page-2026-07-11T11-08-27-255Z.yml new file mode 100644 index 00000000..845afbef --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-08-27-255Z.yml @@ -0,0 +1,102 @@ +- generic [ref=f2e1]: + - link "Skip to main content" [ref=f2e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=f2e3]: Free shipping available with code FREESHIP + - banner [ref=f2e4]: + - generic [ref=f2e5]: + - link "Acme Fashion" [ref=f2e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=f2e7]: + - link "Home" [ref=f2e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=f2e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f2e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=f2e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=f2e12]: + - button "Open product search" [ref=f2e14]: + - generic [ref=f2e19]: Search + - button "Open shopping cart" [ref=f2e21]: + - generic [ref=f2e26]: Cart + - main [ref=f2e27]: + - generic [ref=f2e28]: + - heading "Checkout" [level=1] [ref=f2e29] + - generic [ref=f2e30]: + - generic [ref=f2e31]: + - generic [ref=f2e32]: + - generic [ref=f2e33]: 1. Contact and shipping address + - generic [ref=f2e34]: + - generic [ref=f2e35]: + - generic [ref=f2e36]: Email + - textbox "Email" [ref=f2e38]: decline-review@example.test + - generic [ref=f2e39]: + - generic [ref=f2e40]: First name + - textbox "First name" [ref=f2e42]: Decline + - generic [ref=f2e43]: + - generic [ref=f2e44]: Last name + - textbox "Last name" [ref=f2e46]: Reviewer + - generic [ref=f2e47]: + - generic [ref=f2e48]: Address + - textbox "Address" [ref=f2e50]: Teststrasse 42 + - generic [ref=f2e51]: + - generic [ref=f2e52]: City + - textbox "City" [ref=f2e54]: Berlin + - generic [ref=f2e55]: + - generic [ref=f2e56]: Postal code + - textbox "Postal code" [ref=f2e58]: "10115" + - generic [ref=f2e59]: + - generic [ref=f2e60]: Country code + - textbox "Country code" [ref=f2e62]: DE + - generic [ref=f2e63]: + - generic [ref=f2e64]: Phone (optional) + - textbox "Phone (optional)" [ref=f2e66] + - button "Continue to shipping" [ref=f2e67] + - generic [ref=f2e105]: + - generic [ref=f2e106]: 2. Shipping method + - group "Choose a shipping method" [ref=f2e107]: + - generic [ref=f2e109] [cursor=pointer]: + - generic [ref=f2e110]: Standard Shipping + - radio "Standard Shipping" [checked] [ref=f2e111] + - generic [ref=f2e112] [cursor=pointer]: + - generic [ref=f2e113]: Express Shipping + - radio "Express Shipping" [ref=f2e114] + - button "Continue to payment" [ref=f2e115] + - generic [ref=f2e121]: + - generic [ref=f2e122]: 3. Payment + - group "Payment method" [ref=f2e123]: + - generic [ref=f2e125] [cursor=pointer]: + - radio "Credit card" [ref=f2e126] + - generic [ref=f2e127]: Credit card + - generic [ref=f2e128] [cursor=pointer]: + - radio "PayPal" [ref=f2e129] + - generic [ref=f2e130]: PayPal + - generic [ref=f2e131] [cursor=pointer]: + - radio "Bank transfer" [checked] [ref=f2e132] + - generic [ref=f2e133]: Bank transfer + - button "Pay now" [active] [ref=f2e149] + - complementary [ref=f2e73]: + - heading "Order summary" [level=2] [ref=f2e74] + - generic [ref=f2e76]: + - generic [ref=f2e77]: 1 × Classic Cotton T-Shirt + - generic [ref=f2e78]: €24.99 + - generic [ref=f2e81]: + - generic [ref=f2e82]: Total + - generic [ref=f2e83]: €29.98 + - generic [ref=f2e85]: + - generic [ref=f2e86]: + - generic [ref=f2e87]: Discount code + - textbox "Discount code" [ref=f2e89] + - button "Apply discount" [ref=f2e90] + - contentinfo [ref=f2e96]: + - generic [ref=f2e97]: + - generic [ref=f2e98]: + - paragraph [ref=f2e99]: Acme Fashion + - paragraph [ref=f2e100]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=f2e101]: + - link "Shop all collections" [ref=f2e102] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f2e103] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=f2e104]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-08-28-388Z.yml b/.playwright-mcp/page-2026-07-11T11-08-28-388Z.yml new file mode 100644 index 00000000..37ad2647 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-08-28-388Z.yml @@ -0,0 +1,52 @@ +- generic [active] [ref=f3e1]: + - link "Skip to main content" [ref=f3e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=f3e3]: Free shipping available with code FREESHIP + - banner [ref=f3e4]: + - generic [ref=f3e5]: + - link "Acme Fashion" [ref=f3e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=f3e7]: + - link "Home" [ref=f3e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=f3e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f3e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=f3e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=f3e12]: + - button "Open product search" [ref=f3e14]: + - generic [ref=f3e19]: Search + - button "Open shopping cart" [ref=f3e21]: + - generic [ref=f3e26]: Cart + - main [ref=f3e27]: + - generic [ref=f3e28]: + - generic [ref=f3e29]: ✓ + - heading "Thank you for your order!" [level=1] [ref=f3e30] + - paragraph [ref=f3e31]: "Order #1017 has been placed." + - generic [ref=f3e34]: + - generic [ref=f3e35]: Bank transfer instructions + - paragraph [ref=f3e36]: Mock Bank AG · IBAN DE89 3704 0044 0532 0130 00 · BIC COBADEFFXXX + - paragraph [ref=f3e37]: "Use #1017 as the payment reference." + - generic [ref=f3e38]: + - heading "Order details" [level=2] [ref=f3e39] + - generic [ref=f3e40]: + - generic [ref=f3e41]: 1 × Classic Cotton T-Shirt + - generic [ref=f3e42]: €24.99 + - generic [ref=f3e44]: + - generic [ref=f3e45]: Total + - generic [ref=f3e46]: €29.98 + - link "Continue shopping" [ref=f3e48] [cursor=pointer]: + - /url: http://acme-fashion.test + - contentinfo [ref=f3e49]: + - generic [ref=f3e50]: + - generic [ref=f3e51]: + - paragraph [ref=f3e52]: Acme Fashion + - paragraph [ref=f3e53]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=f3e54]: + - link "Shop all collections" [ref=f3e55] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f3e56] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=f3e57]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-08-37-037Z.yml b/.playwright-mcp/page-2026-07-11T11-08-37-037Z.yml new file mode 100644 index 00000000..999e0160 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-08-37-037Z.yml @@ -0,0 +1,20 @@ +- generic [ref=e3]: + - link "Shop" [ref=e4] [cursor=pointer]: + - /url: http://acme-fashion.test/home + - main [ref=e11]: + - generic [ref=e12]: + - heading "Sign in" [level=1] [ref=e16] + - paragraph [ref=e17]: Access your store administration. + - generic [ref=e18]: + - generic [ref=e19]: + - generic [ref=e20]: Email + - textbox "Email" [active] [ref=e22] + - generic [ref=e23]: + - generic [ref=e24]: Password + - generic [ref=e25]: + - textbox "Password" [ref=e26] + - button "Toggle password visibility" [ref=e28] + - generic [ref=e32]: + - checkbox "Remember me" [ref=e33] + - generic [ref=e35]: Remember me + - button "Sign in" [ref=e36] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-08-45-914Z.yml b/.playwright-mcp/page-2026-07-11T11-08-45-914Z.yml new file mode 100644 index 00000000..f891679f --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-08-45-914Z.yml @@ -0,0 +1,158 @@ +- generic [ref=e43]: + - complementary [ref=e44]: + - navigation "Admin navigation" [ref=e45]: + - generic [ref=e50]: + - generic [ref=e51]: Shop + - generic [ref=e52]: Commerce platform + - generic [ref=e53]: + - link "Dashboard" [ref=e55] [cursor=pointer]: + - /url: /admin + - generic [ref=e59]: Products + - generic [ref=e60]: + - link "Products" [ref=e61] [cursor=pointer]: + - /url: /admin/products + - link "Collections" [ref=e65] [cursor=pointer]: + - /url: /admin/collections + - link "Inventory" [ref=e69] [cursor=pointer]: + - /url: /admin/inventory + - generic [ref=e73]: Orders + - link "Orders" [ref=e75] [cursor=pointer]: + - /url: /admin/orders + - generic [ref=e79]: Customers + - link "Customers" [ref=e81] [cursor=pointer]: + - /url: /admin/customers + - generic [ref=e85]: Discounts + - link "Discounts" [ref=e87] [cursor=pointer]: + - /url: /admin/discounts + - generic [ref=e92]: Content + - generic [ref=e93]: + - link "Pages" [ref=e94] [cursor=pointer]: + - /url: /admin/pages + - link "Navigation" [ref=e98] [cursor=pointer]: + - /url: /admin/navigation + - link "Themes" [ref=e102] [cursor=pointer]: + - /url: /admin/themes + - generic [ref=e106]: Insights + - generic [ref=e107]: + - link "Analytics" [ref=e108] [cursor=pointer]: + - /url: /admin/analytics + - link "Search" [ref=e113] [cursor=pointer]: + - /url: /admin/settings/search + - generic [ref=e117]: Platform + - generic [ref=e118]: + - link "Settings" [ref=e119] [cursor=pointer]: + - /url: /admin/settings + - link "Apps" [ref=e124] [cursor=pointer]: + - /url: /admin/apps + - link "Developers" [ref=e128] [cursor=pointer]: + - /url: /admin/developers + - generic [ref=e132]: + - banner [ref=e133]: + - generic [ref=e134]: + - button "Acme Fashion" [ref=e137] + - generic [ref=e141]: + - button "Notifications" [ref=e143] + - button "AU Admin User" [ref=e147]: + - generic [ref=e148]: AU + - generic [ref=e151]: Admin User + - main [ref=e155]: + - generic [ref=e156]: + - generic [ref=e157]: + - link "Home" [ref=e159] [cursor=pointer]: + - /url: /admin + - generic [ref=e162]: Dashboard + - generic [ref=e164]: + - generic [ref=e165]: + - heading "Dashboard" [level=1] [ref=e166] + - paragraph [ref=e167]: A live view of store performance. + - combobox "Date range" [ref=e169]: + - option "Today" + - option "Last 7 days" + - option "Last 30 days" [selected] + - option "Custom range" + - generic [ref=e170]: + - generic [ref=e173]: + - paragraph [ref=e174]: Total sales + - generic [ref=e175]: €1,602.07 + - generic [ref=e180]: + - paragraph [ref=e181]: Orders + - generic [ref=e182]: "17" + - generic [ref=e187]: + - paragraph [ref=e188]: Average order value + - generic [ref=e189]: €94.23 + - generic [ref=e194]: + - paragraph [ref=e195]: Visitors + - generic [ref=e196]: 3,481 + - generic [ref=e199]: + - generic [ref=e200]: + - generic [ref=e201]: Orders over time + - generic "Orders chart" [ref=e203]: + - generic: + - 'generic "2026-06-16: 1"' + - generic: + - 'generic "2026-06-21: 1"' + - generic: + - 'generic "2026-06-26: 1"' + - generic: + - 'generic "2026-06-27: 1"' + - generic: + - 'generic "2026-06-29: 1"' + - generic: + - 'generic "2026-07-01: 1"' + - generic: + - 'generic "2026-07-06: 1"' + - generic: + - 'generic "2026-07-07: 1"' + - generic: + - 'generic "2026-07-08: 1"' + - generic: + - 'generic "2026-07-09: 1"' + - generic: + - 'generic "2026-07-10: 3"' + - generic: + - 'generic "2026-07-11: 4"' + - generic [ref=e204]: + - generic [ref=e205]: Conversion funnel + - generic [ref=e207]: + - generic [ref=e209]: + - generic [ref=e210]: Visits + - generic [ref=e211]: 3,481 + - generic [ref=e215]: + - generic [ref=e216]: Add To Cart + - generic [ref=e217]: "740" + - generic [ref=e221]: + - generic [ref=e222]: Checkout Started + - generic [ref=e223]: "352" + - generic [ref=e227]: + - generic [ref=e228]: Checkout Completed + - generic [ref=e229]: "152" + - generic [ref=e232]: + - generic [ref=e233]: Top products + - table [ref=e236]: + - rowgroup [ref=e237]: + - row [ref=e238]: + - columnheader "Product" [ref=e239] + - columnheader "Units sold" [ref=e240] + - columnheader "Revenue" [ref=e241] + - rowgroup [ref=e242]: + - row [ref=e243]: + - cell "Cashmere Overcoat" [ref=e244] + - cell "1" [ref=e245] + - cell "€499.99" [ref=e246] + - row [ref=e247]: + - cell "Classic Cotton T-Shirt" [ref=e248] + - cell "8" [ref=e249] + - cell "€199.92" [ref=e250] + - row [ref=e251]: + - cell "Running Sneakers" [ref=e252] + - cell "1" [ref=e253] + - cell "€119.99" [ref=e254] + - row [ref=e255]: + - cell "Premium Slim Fit Jeans" [ref=e256] + - cell "1" [ref=e257] + - cell "€79.99" [ref=e258] + - row [ref=e259]: + - cell "Chino Shorts" [ref=e260] + - cell "2" [ref=e261] + - cell "€79.98" [ref=e262] + - status \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-08-53-729Z.yml b/.playwright-mcp/page-2026-07-11T11-08-53-729Z.yml new file mode 100644 index 00000000..b71de6a4 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-08-53-729Z.yml @@ -0,0 +1,289 @@ +- generic [ref=e264]: + - complementary [ref=e265]: + - navigation "Admin navigation" [ref=e266]: + - generic [ref=e271]: + - generic [ref=e272]: Shop + - generic [ref=e273]: Commerce platform + - generic [ref=e274]: + - link "Dashboard" [ref=e276] [cursor=pointer]: + - /url: /admin + - generic [ref=e280]: Products + - generic [ref=e281]: + - link "Products" [ref=e282] [cursor=pointer]: + - /url: /admin/products + - link "Collections" [ref=e286] [cursor=pointer]: + - /url: /admin/collections + - link "Inventory" [ref=e290] [cursor=pointer]: + - /url: /admin/inventory + - generic [ref=e294]: Orders + - link "Orders" [ref=e296] [cursor=pointer]: + - /url: /admin/orders + - generic [ref=e300]: Customers + - link "Customers" [ref=e302] [cursor=pointer]: + - /url: /admin/customers + - generic [ref=e306]: Discounts + - link "Discounts" [ref=e308] [cursor=pointer]: + - /url: /admin/discounts + - generic [ref=e313]: Content + - generic [ref=e314]: + - link "Pages" [ref=e315] [cursor=pointer]: + - /url: /admin/pages + - link "Navigation" [ref=e319] [cursor=pointer]: + - /url: /admin/navigation + - link "Themes" [ref=e323] [cursor=pointer]: + - /url: /admin/themes + - generic [ref=e327]: Insights + - generic [ref=e328]: + - link "Analytics" [ref=e329] [cursor=pointer]: + - /url: /admin/analytics + - link "Search" [ref=e334] [cursor=pointer]: + - /url: /admin/settings/search + - generic [ref=e338]: Platform + - generic [ref=e339]: + - link "Settings" [ref=e340] [cursor=pointer]: + - /url: /admin/settings + - link "Apps" [ref=e345] [cursor=pointer]: + - /url: /admin/apps + - link "Developers" [ref=e349] [cursor=pointer]: + - /url: /admin/developers + - generic [ref=e353]: + - banner [ref=e354]: + - generic [ref=e355]: + - button "Acme Fashion" [ref=e358] + - generic [ref=e362]: + - button "Notifications" [ref=e364] + - button "AU Admin User" [ref=e368]: + - generic [ref=e369]: AU + - generic [ref=e372]: Admin User + - main [ref=e376]: + - generic [ref=e377]: + - generic [ref=e378]: + - link "Home" [ref=e380] [cursor=pointer]: + - /url: /admin + - generic [ref=e383]: Products + - generic [ref=e385]: + - generic [ref=e386]: + - heading "Products" [level=1] [ref=e387] + - paragraph [ref=e388]: Manage catalog products, variants, and publishing. + - link "Add product" [ref=e390] [cursor=pointer]: + - /url: /admin/products/create + - generic [ref=e395]: + - textbox "Search products" [ref=e397]: + - /placeholder: Search products… + - combobox "Filter status" [ref=e399]: + - option "All statuses" [selected] + - option "Active" + - option "Draft" + - option "Archived" + - combobox "Filter product type" [ref=e400]: + - option "All types" [selected] + - option "Accessories" + - option "Gift Cards" + - option "Hoodies" + - option "Jackets" + - option "Pants" + - option "Shoes" + - option "T-Shirts" + - generic [ref=e401]: + - table [ref=e403]: + - rowgroup [ref=e404]: + - row [ref=e405]: + - columnheader "Select" [ref=e406] + - columnheader [ref=e408]: + - button "Product" [ref=e409] + - columnheader "Status" [ref=e410] + - columnheader "Inventory" [ref=e411] + - columnheader "Type" [ref=e412] + - columnheader "Vendor" [ref=e413] + - columnheader [ref=e414]: + - button "Updated" [ref=e415] + - rowgroup [ref=e416]: + - row [ref=e417]: + - cell [ref=e418]: + - checkbox "Select Unreleased Winter Jacket" [ref=e419] + - cell "Unreleased Winter Jacket 4 variants" [ref=e421]: + - link "Unreleased Winter Jacket" [ref=e422] [cursor=pointer]: + - /url: /admin/products/15/edit + - generic [ref=e423]: 4 variants + - cell "Draft" [ref=e424] + - cell "0" [ref=e426] + - cell "Jackets" [ref=e427] + - cell "Acme Outerwear" [ref=e428] + - cell "8 minutes ago" [ref=e429] + - row [ref=e430]: + - cell [ref=e431]: + - checkbox "Select Discontinued Raincoat" [ref=e432] + - cell "Discontinued Raincoat 2 variants" [ref=e434]: + - link "Discontinued Raincoat" [ref=e435] [cursor=pointer]: + - /url: /admin/products/16/edit + - generic [ref=e436]: 2 variants + - cell "Archived" [ref=e437] + - cell "6" [ref=e439] + - cell "Jackets" [ref=e440] + - cell "Acme Outerwear" [ref=e441] + - cell "8 minutes ago" [ref=e442] + - row [ref=e443]: + - cell [ref=e444]: + - checkbox "Select Classic Cotton T-Shirt" [ref=e445] + - cell "Classic Cotton T-Shirt 12 variants" [ref=e447]: + - link "Classic Cotton T-Shirt" [ref=e448] [cursor=pointer]: + - /url: /admin/products/1/edit + - generic [ref=e449]: 12 variants + - cell "Active" [ref=e450] + - cell "178" [ref=e452] + - cell "T-Shirts" [ref=e453] + - cell "Acme Basics" [ref=e454] + - cell "8 minutes ago" [ref=e455] + - row [ref=e456]: + - cell [ref=e457]: + - checkbox "Select Premium Slim Fit Jeans" [ref=e458] + - cell "Premium Slim Fit Jeans 10 variants" [ref=e460]: + - link "Premium Slim Fit Jeans" [ref=e461] [cursor=pointer]: + - /url: /admin/products/2/edit + - generic [ref=e462]: 10 variants + - cell "Active" [ref=e463] + - cell "80" [ref=e465] + - cell "Pants" [ref=e466] + - cell "Acme Denim" [ref=e467] + - cell "8 minutes ago" [ref=e468] + - row [ref=e469]: + - cell [ref=e470]: + - checkbox "Select Organic Hoodie" [ref=e471] + - cell "Organic Hoodie 4 variants" [ref=e473]: + - link "Organic Hoodie" [ref=e474] [cursor=pointer]: + - /url: /admin/products/3/edit + - generic [ref=e475]: 4 variants + - cell "Active" [ref=e476] + - cell "80" [ref=e478] + - cell "Hoodies" [ref=e479] + - cell "Acme Basics" [ref=e480] + - cell "8 minutes ago" [ref=e481] + - row [ref=e482]: + - cell [ref=e483]: + - checkbox "Select Leather Belt" [ref=e484] + - cell "Leather Belt 4 variants" [ref=e486]: + - link "Leather Belt" [ref=e487] [cursor=pointer]: + - /url: /admin/products/4/edit + - generic [ref=e488]: 4 variants + - cell "Active" [ref=e489] + - cell "100" [ref=e491] + - cell "Accessories" [ref=e492] + - cell "Acme Accessories" [ref=e493] + - cell "8 minutes ago" [ref=e494] + - row [ref=e495]: + - cell [ref=e496]: + - checkbox "Select Running Sneakers" [ref=e497] + - cell "Running Sneakers 14 variants" [ref=e499]: + - link "Running Sneakers" [ref=e500] [cursor=pointer]: + - /url: /admin/products/5/edit + - generic [ref=e501]: 14 variants + - cell "Active" [ref=e502] + - cell "70" [ref=e504] + - cell "Shoes" [ref=e505] + - cell "Acme Sport" [ref=e506] + - cell "8 minutes ago" [ref=e507] + - row [ref=e508]: + - cell [ref=e509]: + - checkbox "Select Graphic Print Tee" [ref=e510] + - cell "Graphic Print Tee 4 variants" [ref=e512]: + - link "Graphic Print Tee" [ref=e513] [cursor=pointer]: + - /url: /admin/products/6/edit + - generic [ref=e514]: 4 variants + - cell "Active" [ref=e515] + - cell "72" [ref=e517] + - cell "T-Shirts" [ref=e518] + - cell "Acme Basics" [ref=e519] + - cell "8 minutes ago" [ref=e520] + - row [ref=e521]: + - cell [ref=e522]: + - checkbox "Select V-Neck Linen Tee" [ref=e523] + - cell "V-Neck Linen Tee 9 variants" [ref=e525]: + - link "V-Neck Linen Tee" [ref=e526] [cursor=pointer]: + - /url: /admin/products/7/edit + - generic [ref=e527]: 9 variants + - cell "Active" [ref=e528] + - cell "108" [ref=e530] + - cell "T-Shirts" [ref=e531] + - cell "Acme Basics" [ref=e532] + - cell "8 minutes ago" [ref=e533] + - row [ref=e534]: + - cell [ref=e535]: + - checkbox "Select Striped Polo Shirt" [ref=e536] + - cell "Striped Polo Shirt 4 variants" [ref=e538]: + - link "Striped Polo Shirt" [ref=e539] [cursor=pointer]: + - /url: /admin/products/8/edit + - generic [ref=e540]: 4 variants + - cell "Active" [ref=e541] + - cell "40" [ref=e543] + - cell "T-Shirts" [ref=e544] + - cell "Acme Basics" [ref=e545] + - cell "8 minutes ago" [ref=e546] + - row [ref=e547]: + - cell [ref=e548]: + - checkbox "Select Cargo Pants" [ref=e549] + - cell "Cargo Pants 12 variants" [ref=e551]: + - link "Cargo Pants" [ref=e552] [cursor=pointer]: + - /url: /admin/products/9/edit + - generic [ref=e553]: 12 variants + - cell "Active" [ref=e554] + - cell "168" [ref=e556] + - cell "Pants" [ref=e557] + - cell "Acme Workwear" [ref=e558] + - cell "8 minutes ago" [ref=e559] + - row [ref=e560]: + - cell [ref=e561]: + - checkbox "Select Chino Shorts" [ref=e562] + - cell "Chino Shorts 8 variants" [ref=e564]: + - link "Chino Shorts" [ref=e565] [cursor=pointer]: + - /url: /admin/products/10/edit + - generic [ref=e566]: 8 variants + - cell "Active" [ref=e567] + - cell "128" [ref=e569] + - cell "Pants" [ref=e570] + - cell "Acme Basics" [ref=e571] + - cell "8 minutes ago" [ref=e572] + - row [ref=e573]: + - cell [ref=e574]: + - checkbox "Select Wide Leg Trousers" [ref=e575] + - cell "Wide Leg Trousers 3 variants" [ref=e577]: + - link "Wide Leg Trousers" [ref=e578] [cursor=pointer]: + - /url: /admin/products/11/edit + - generic [ref=e579]: 3 variants + - cell "Active" [ref=e580] + - cell "21" [ref=e582] + - cell "Pants" [ref=e583] + - cell "Acme Denim" [ref=e584] + - cell "8 minutes ago" [ref=e585] + - row [ref=e586]: + - cell [ref=e587]: + - checkbox "Select Wool Scarf" [ref=e588] + - cell "Wool Scarf 3 variants" [ref=e590]: + - link "Wool Scarf" [ref=e591] [cursor=pointer]: + - /url: /admin/products/12/edit + - generic [ref=e592]: 3 variants + - cell "Active" [ref=e593] + - cell "90" [ref=e595] + - cell "Accessories" [ref=e596] + - cell "Acme Accessories" [ref=e597] + - cell "8 minutes ago" [ref=e598] + - row [ref=e599]: + - cell [ref=e600]: + - checkbox "Select Canvas Tote Bag" [ref=e601] + - cell "Canvas Tote Bag 2 variants" [ref=e603]: + - link "Canvas Tote Bag" [ref=e604] [cursor=pointer]: + - /url: /admin/products/13/edit + - generic [ref=e605]: 2 variants + - cell "Active" [ref=e606] + - cell "80" [ref=e608] + - cell "Accessories" [ref=e609] + - cell "Acme Accessories" [ref=e610] + - cell "8 minutes ago" [ref=e611] + - navigation "Pagination Navigation" [ref=e614]: + - generic [ref=e615]: + - paragraph [ref=e617]: Showing 1 to 15 of 20 results + - generic [ref=e619]: + - generic "« Previous" [ref=e621] + - generic [ref=e625]: "1" + - button "Go to page 2" [ref=e629]: "2" + - button "Next »" [ref=e631] + - status \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-09-06-483Z.yml b/.playwright-mcp/page-2026-07-11T11-09-06-483Z.yml new file mode 100644 index 00000000..68a52ce2 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-09-06-483Z.yml @@ -0,0 +1,113 @@ +- generic [ref=e264]: + - complementary [ref=e265]: + - navigation "Admin navigation" [ref=e266]: + - generic [ref=e271]: + - generic [ref=e272]: Shop + - generic [ref=e273]: Commerce platform + - generic [ref=e274]: + - link "Dashboard" [ref=e276] [cursor=pointer]: + - /url: /admin + - generic [ref=e280]: Products + - generic [ref=e281]: + - link "Products" [ref=e282] [cursor=pointer]: + - /url: /admin/products + - link "Collections" [ref=e286] [cursor=pointer]: + - /url: /admin/collections + - link "Inventory" [ref=e290] [cursor=pointer]: + - /url: /admin/inventory + - generic [ref=e294]: Orders + - link "Orders" [ref=e296] [cursor=pointer]: + - /url: /admin/orders + - generic [ref=e300]: Customers + - link "Customers" [ref=e302] [cursor=pointer]: + - /url: /admin/customers + - generic [ref=e306]: Discounts + - link "Discounts" [ref=e308] [cursor=pointer]: + - /url: /admin/discounts + - generic [ref=e313]: Content + - generic [ref=e314]: + - link "Pages" [ref=e315] [cursor=pointer]: + - /url: /admin/pages + - link "Navigation" [ref=e319] [cursor=pointer]: + - /url: /admin/navigation + - link "Themes" [ref=e323] [cursor=pointer]: + - /url: /admin/themes + - generic [ref=e327]: Insights + - generic [ref=e328]: + - link "Analytics" [ref=e329] [cursor=pointer]: + - /url: /admin/analytics + - link "Search" [ref=e334] [cursor=pointer]: + - /url: /admin/settings/search + - generic [ref=e338]: Platform + - generic [ref=e339]: + - link "Settings" [ref=e340] [cursor=pointer]: + - /url: /admin/settings + - link "Apps" [ref=e345] [cursor=pointer]: + - /url: /admin/apps + - link "Developers" [ref=e349] [cursor=pointer]: + - /url: /admin/developers + - generic [ref=e353]: + - banner [ref=e354]: + - generic [ref=e355]: + - button "Acme Fashion" [ref=e358] + - generic [ref=e362]: + - button "Notifications" [ref=e364] + - button "AU Admin User" [ref=e368]: + - generic [ref=e369]: AU + - generic [ref=e372]: Admin User + - main [ref=e376]: + - generic [ref=e377]: + - generic [ref=e378]: + - link "Home" [ref=e380] [cursor=pointer]: + - /url: /admin + - generic [ref=e383]: Products + - generic [ref=e385]: + - generic [ref=e386]: + - heading "Products" [level=1] [ref=e387] + - paragraph [ref=e388]: Manage catalog products, variants, and publishing. + - link "Add product" [ref=e390] [cursor=pointer]: + - /url: /admin/products/create + - generic [ref=e395]: + - textbox "Search products" [active] [ref=e397]: + - /placeholder: Search products… + - text: Classic + - combobox "Filter status" [ref=e399]: + - option "All statuses" [selected] + - option "Active" + - option "Draft" + - option "Archived" + - combobox "Filter product type" [ref=e400]: + - option "All types" [selected] + - option "Accessories" + - option "Gift Cards" + - option "Hoodies" + - option "Jackets" + - option "Pants" + - option "Shoes" + - option "T-Shirts" + - table [ref=e403]: + - rowgroup [ref=e404]: + - row [ref=e405]: + - columnheader "Select" [ref=e406] + - columnheader [ref=e408]: + - button "Product" [ref=e409] + - columnheader "Status" [ref=e410] + - columnheader "Inventory" [ref=e411] + - columnheader "Type" [ref=e412] + - columnheader "Vendor" [ref=e413] + - columnheader [ref=e414]: + - button "Updated" [ref=e415] + - rowgroup [ref=e416]: + - row [ref=e634]: + - cell [ref=e444]: + - checkbox "Select Classic Cotton T-Shirt" [ref=e445] + - cell "Classic Cotton T-Shirt 12 variants" [ref=e447]: + - link "Classic Cotton T-Shirt" [ref=e448] [cursor=pointer]: + - /url: /admin/products/1/edit + - generic [ref=e449]: 12 variants + - cell "Active" [ref=e450] + - cell "178" [ref=e452] + - cell "T-Shirts" [ref=e453] + - cell "Acme Basics" [ref=e454] + - cell "9 minutes ago" [ref=e635] + - status \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-09-15-668Z.yml b/.playwright-mcp/page-2026-07-11T11-09-15-668Z.yml new file mode 100644 index 00000000..1564bbce --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-09-15-668Z.yml @@ -0,0 +1,274 @@ +- generic [ref=e637]: + - complementary [ref=e638]: + - navigation "Admin navigation" [ref=e639]: + - generic [ref=e644]: + - generic [ref=e645]: Shop + - generic [ref=e646]: Commerce platform + - generic [ref=e647]: + - link "Dashboard" [ref=e649] [cursor=pointer]: + - /url: /admin + - generic [ref=e653]: Products + - generic [ref=e654]: + - link "Products" [ref=e655] [cursor=pointer]: + - /url: /admin/products + - link "Collections" [ref=e659] [cursor=pointer]: + - /url: /admin/collections + - link "Inventory" [ref=e663] [cursor=pointer]: + - /url: /admin/inventory + - generic [ref=e667]: Orders + - link "Orders" [ref=e669] [cursor=pointer]: + - /url: /admin/orders + - generic [ref=e673]: Customers + - link "Customers" [ref=e675] [cursor=pointer]: + - /url: /admin/customers + - generic [ref=e679]: Discounts + - link "Discounts" [ref=e681] [cursor=pointer]: + - /url: /admin/discounts + - generic [ref=e686]: Content + - generic [ref=e687]: + - link "Pages" [ref=e688] [cursor=pointer]: + - /url: /admin/pages + - link "Navigation" [ref=e692] [cursor=pointer]: + - /url: /admin/navigation + - link "Themes" [ref=e696] [cursor=pointer]: + - /url: /admin/themes + - generic [ref=e700]: Insights + - generic [ref=e701]: + - link "Analytics" [ref=e702] [cursor=pointer]: + - /url: /admin/analytics + - link "Search" [ref=e707] [cursor=pointer]: + - /url: /admin/settings/search + - generic [ref=e711]: Platform + - generic [ref=e712]: + - link "Settings" [ref=e713] [cursor=pointer]: + - /url: /admin/settings + - link "Apps" [ref=e718] [cursor=pointer]: + - /url: /admin/apps + - link "Developers" [ref=e722] [cursor=pointer]: + - /url: /admin/developers + - generic [ref=e726]: + - banner [ref=e727]: + - generic [ref=e728]: + - button "Acme Fashion" [ref=e731] + - generic [ref=e735]: + - button "Notifications" [ref=e737] + - button "AU Admin User" [ref=e741]: + - generic [ref=e742]: AU + - generic [ref=e745]: Admin User + - main [ref=e749]: + - generic [ref=e750]: + - generic [ref=e751]: + - link "Home" [ref=e753] [cursor=pointer]: + - /url: /admin + - generic [ref=e756]: Orders + - generic [ref=e759]: + - heading "Orders" [level=1] [ref=e760] + - paragraph [ref=e761]: Review payments, fulfillment, and customer orders. + - generic [ref=e763]: + - textbox "Search orders" [ref=e765]: + - /placeholder: Search order or email… + - combobox "Filter orders" [ref=e767]: + - option "All orders" [selected] + - option "Paid" + - option "Pending" + - option "Unfulfilled" + - option "Fulfilled" + - option "Refunded" + - table [ref=e770]: + - rowgroup [ref=e771]: + - row [ref=e772]: + - columnheader "Order" [ref=e773] + - columnheader "Date" [ref=e774] + - columnheader "Customer" [ref=e775] + - columnheader "Total" [ref=e776] + - columnheader "Payment" [ref=e777] + - columnheader "Fulfillment" [ref=e778] + - rowgroup [ref=e779]: + - row [ref=e780]: + - cell [ref=e781]: + - link "#1017" [ref=e782] [cursor=pointer]: + - /url: /admin/orders/20 + - cell "Jul 11, 2026 11:08" [ref=e783] + - cell "Guest decline-review@example.test" [ref=e784]: + - generic [ref=e785]: Guest + - generic [ref=e786]: decline-review@example.test + - cell "€29.98" [ref=e787] + - cell "Pending" [ref=e788] + - cell "Unfulfilled" [ref=e790] + - row [ref=e792]: + - cell [ref=e793]: + - link "#1016" [ref=e794] [cursor=pointer]: + - /url: /admin/orders/19 + - cell "Jul 11, 2026 11:06" [ref=e795] + - cell "Guest browser-review@example.test" [ref=e796]: + - generic [ref=e797]: Guest + - generic [ref=e798]: browser-review@example.test + - cell "€54.97" [ref=e799] + - cell "Paid" [ref=e800] + - cell "Unfulfilled" [ref=e802] + - row [ref=e804]: + - cell [ref=e805]: + - link "#1015" [ref=e806] [cursor=pointer]: + - /url: /admin/orders/15 + - cell "Jul 11, 2026 11:00" [ref=e807] + - cell "John Doe customer@acme.test" [ref=e808]: + - generic [ref=e809]: John Doe + - generic [ref=e810]: customer@acme.test + - cell "€54.47" [ref=e811] + - cell "Paid" [ref=e812] + - cell "Unfulfilled" [ref=e814] + - row [ref=e816]: + - cell [ref=e817]: + - link "#1005" [ref=e818] [cursor=pointer]: + - /url: /admin/orders/5 + - cell "Jul 11, 2026 09:00" [ref=e819] + - cell "Jane Smith jane@example.com" [ref=e820]: + - generic [ref=e821]: Jane Smith + - generic [ref=e822]: jane@example.com + - cell "€39.98" [ref=e823] + - cell "Pending" [ref=e824] + - cell "Unfulfilled" [ref=e826] + - row [ref=e828]: + - cell [ref=e829]: + - link "#1013" [ref=e830] [cursor=pointer]: + - /url: /admin/orders/13 + - cell "Jul 10, 2026 11:00" [ref=e831] + - cell "Robert Martinez robert@example.com" [ref=e832]: + - generic [ref=e833]: Robert Martinez + - generic [ref=e834]: robert@example.com + - cell "€84.97" [ref=e835] + - cell "Paid" [ref=e836] + - cell "Unfulfilled" [ref=e838] + - row [ref=e840]: + - cell [ref=e841]: + - link "#1010" [ref=e842] [cursor=pointer]: + - /url: /admin/orders/10 + - cell "Jul 10, 2026 11:00" [ref=e843] + - cell "John Doe customer@acme.test" [ref=e844]: + - generic [ref=e845]: John Doe + - generic [ref=e846]: customer@acme.test + - cell "€504.98" [ref=e847] + - cell "Paid" [ref=e848] + - cell "Unfulfilled" [ref=e850] + - row [ref=e852]: + - cell [ref=e853]: + - link "#1006" [ref=e854] [cursor=pointer]: + - /url: /admin/orders/6 + - cell "Jul 10, 2026 11:00" [ref=e855] + - cell "Michael Brown michael@example.com" [ref=e856]: + - generic [ref=e857]: Michael Brown + - generic [ref=e858]: michael@example.com + - cell "€124.98" [ref=e859] + - cell "Paid" [ref=e860] + - cell "Unfulfilled" [ref=e862] + - row [ref=e864]: + - cell [ref=e865]: + - link "#1001" [ref=e866] [cursor=pointer]: + - /url: /admin/orders/1 + - cell "Jul 9, 2026 11:00" [ref=e867] + - cell "John Doe customer@acme.test" [ref=e868]: + - generic [ref=e869]: John Doe + - generic [ref=e870]: customer@acme.test + - cell "€54.97" [ref=e871] + - cell "Paid" [ref=e872] + - cell "Unfulfilled" [ref=e874] + - row [ref=e876]: + - cell [ref=e877]: + - link "#1009" [ref=e878] [cursor=pointer]: + - /url: /admin/orders/9 + - cell "Jul 8, 2026 11:00" [ref=e879] + - cell "Emma Garcia emma@example.com" [ref=e880]: + - generic [ref=e881]: Emma Garcia + - generic [ref=e882]: emma@example.com + - cell "€49.97" [ref=e883] + - cell "Paid" [ref=e884] + - cell "Unfulfilled" [ref=e886] + - row [ref=e888]: + - cell [ref=e889]: + - link "#1012" [ref=e890] [cursor=pointer]: + - /url: /admin/orders/12 + - cell "Jul 7, 2026 11:00" [ref=e891] + - cell "Lisa Anderson lisa@example.com" [ref=e892]: + - generic [ref=e893]: Lisa Anderson + - generic [ref=e894]: lisa@example.com + - cell "€84.97" [ref=e895] + - cell "Paid" [ref=e896] + - cell "Unfulfilled" [ref=e898] + - row [ref=e900]: + - cell [ref=e901]: + - link "#1003" [ref=e902] [cursor=pointer]: + - /url: /admin/orders/3 + - cell "Jul 6, 2026 11:00" [ref=e903] + - cell "Jane Smith jane@example.com" [ref=e904]: + - generic [ref=e905]: Jane Smith + - generic [ref=e906]: jane@example.com + - cell "€119.97" [ref=e907] + - cell "Paid" [ref=e908] + - cell "Partial" [ref=e910] + - row [ref=e912]: + - cell [ref=e913]: + - link "#1002" [ref=e914] [cursor=pointer]: + - /url: /admin/orders/2 + - cell "Jul 1, 2026 11:00" [ref=e915] + - cell "John Doe customer@acme.test" [ref=e916]: + - generic [ref=e917]: John Doe + - generic [ref=e918]: customer@acme.test + - cell "€89.97" [ref=e919] + - cell "Paid" [ref=e920] + - cell "Fulfilled" [ref=e922] + - row [ref=e924]: + - cell [ref=e925]: + - link "#1008" [ref=e926] [cursor=pointer]: + - /url: /admin/orders/8 + - cell "Jun 29, 2026 11:00" [ref=e927] + - cell "David Lee david@example.com" [ref=e928]: + - generic [ref=e929]: David Lee + - generic [ref=e930]: david@example.com + - cell "€89.97" [ref=e931] + - cell "Partially Refunded" [ref=e932] + - cell "Fulfilled" [ref=e934] + - row [ref=e936]: + - cell [ref=e937]: + - link "#1014" [ref=e938] [cursor=pointer]: + - /url: /admin/orders/14 + - cell "Jun 27, 2026 11:00" [ref=e939] + - cell "Anna Thomas anna@example.com" [ref=e940]: + - generic [ref=e941]: Anna Thomas + - generic [ref=e942]: anna@example.com + - cell "€50.00" [ref=e943] + - cell "Paid" [ref=e944] + - cell "Fulfilled" [ref=e946] + - row [ref=e948]: + - cell [ref=e949]: + - link "#1004" [ref=e950] [cursor=pointer]: + - /url: /admin/orders/4 + - cell "Jun 26, 2026 11:00" [ref=e951] + - cell "John Doe customer@acme.test" [ref=e952]: + - generic [ref=e953]: John Doe + - generic [ref=e954]: customer@acme.test + - cell "€29.98" [ref=e955] + - cell "Refunded" [ref=e956] + - cell "Unfulfilled" [ref=e958] + - row [ref=e960]: + - cell [ref=e961]: + - link "#1007" [ref=e962] [cursor=pointer]: + - /url: /admin/orders/7 + - cell "Jun 21, 2026 11:00" [ref=e963] + - cell "Sarah Wilson sarah@example.com" [ref=e964]: + - generic [ref=e965]: Sarah Wilson + - generic [ref=e966]: sarah@example.com + - cell "€104.96" [ref=e967] + - cell "Paid" [ref=e968] + - cell "Fulfilled" [ref=e970] + - row [ref=e972]: + - cell [ref=e973]: + - link "#1011" [ref=e974] [cursor=pointer]: + - /url: /admin/orders/11 + - cell "Jun 16, 2026 11:00" [ref=e975] + - cell "James Taylor james@example.com" [ref=e976]: + - generic [ref=e977]: James Taylor + - generic [ref=e978]: james@example.com + - cell "€32.98" [ref=e979] + - cell "Paid" [ref=e980] + - cell "Fulfilled" [ref=e982] + - status \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-09-23-818Z.yml b/.playwright-mcp/page-2026-07-11T11-09-23-818Z.yml new file mode 100644 index 00000000..2f92832c --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-09-23-818Z.yml @@ -0,0 +1,127 @@ +- generic [ref=e986]: + - complementary [ref=e987]: + - navigation "Admin navigation" [ref=e988]: + - generic [ref=e993]: + - generic [ref=e994]: Shop + - generic [ref=e995]: Commerce platform + - generic [ref=e996]: + - link "Dashboard" [ref=e998] [cursor=pointer]: + - /url: /admin + - generic [ref=e1002]: Products + - generic [ref=e1003]: + - link "Products" [ref=e1004] [cursor=pointer]: + - /url: /admin/products + - link "Collections" [ref=e1008] [cursor=pointer]: + - /url: /admin/collections + - link "Inventory" [ref=e1012] [cursor=pointer]: + - /url: /admin/inventory + - generic [ref=e1016]: Orders + - link "Orders" [ref=e1018] [cursor=pointer]: + - /url: /admin/orders + - generic [ref=e1022]: Customers + - link "Customers" [ref=e1024] [cursor=pointer]: + - /url: /admin/customers + - generic [ref=e1028]: Discounts + - link "Discounts" [ref=e1030] [cursor=pointer]: + - /url: /admin/discounts + - generic [ref=e1035]: Content + - generic [ref=e1036]: + - link "Pages" [ref=e1037] [cursor=pointer]: + - /url: /admin/pages + - link "Navigation" [ref=e1041] [cursor=pointer]: + - /url: /admin/navigation + - link "Themes" [ref=e1045] [cursor=pointer]: + - /url: /admin/themes + - generic [ref=e1049]: Insights + - generic [ref=e1050]: + - link "Analytics" [ref=e1051] [cursor=pointer]: + - /url: /admin/analytics + - link "Search" [ref=e1056] [cursor=pointer]: + - /url: /admin/settings/search + - generic [ref=e1060]: Platform + - generic [ref=e1061]: + - link "Settings" [ref=e1062] [cursor=pointer]: + - /url: /admin/settings + - link "Apps" [ref=e1067] [cursor=pointer]: + - /url: /admin/apps + - link "Developers" [ref=e1071] [cursor=pointer]: + - /url: /admin/developers + - generic [ref=e1075]: + - banner [ref=e1076]: + - generic [ref=e1077]: + - button "Acme Fashion" [ref=e1080] + - generic [ref=e1084]: + - button "Notifications" [ref=e1086] + - button "AU Admin User" [ref=e1090]: + - generic [ref=e1091]: AU + - generic [ref=e1094]: Admin User + - main [ref=e1098]: + - generic [ref=e1099]: + - generic [ref=e1100]: + - link "Home" [ref=e1102] [cursor=pointer]: + - /url: /admin + - generic [ref=e1105]: "#1005" + - generic [ref=e1107]: + - generic [ref=e1108]: + - heading "#1005" [level=1] [ref=e1109] + - paragraph [ref=e1110]: Placed 2 hours ago + - generic [ref=e1111]: + - button "Confirm payment" [ref=e1112] + - button "Refund" [ref=e1120] + - generic [ref=e1133]: + - generic [ref=e1134]: Payment required + - text: This order must be paid before fulfillment can be created. + - generic [ref=e1135]: + - generic [ref=e1136]: + - generic [ref=e1137]: + - generic [ref=e1138]: Items + - generic [ref=e1141]: + - generic [ref=e1142]: + - generic [ref=e1143]: Leather Belt + - generic [ref=e1144]: SKU ACME-LEATHERB-SM-BLACK-2 · Qty 1 + - generic [ref=e1145]: €34.99 + - generic [ref=e1146]: + - generic [ref=e1147]: + - generic [ref=e1148]: Subtotal + - generic [ref=e1149]: €34.99 + - generic [ref=e1150]: + - generic [ref=e1151]: Discount + - generic [ref=e1152]: €0.00 + - generic [ref=e1153]: + - generic [ref=e1154]: Shipping + - generic [ref=e1155]: €4.99 + - generic [ref=e1156]: + - generic [ref=e1157]: Tax + - generic [ref=e1158]: €5.59 + - generic [ref=e1159]: + - generic [ref=e1160]: Total + - generic [ref=e1161]: €39.98 + - generic [ref=e1162]: + - generic [ref=e1163]: Fulfillment + - paragraph [ref=e1166]: No fulfillments yet. + - generic [ref=e1167]: + - generic [ref=e1168]: Timeline + - list [ref=e1170]: + - listitem [ref=e1171]: + - generic [ref=e1172]: Order created + - generic [ref=e1173]: Jul 11, 2026 09:00 + - generic [ref=e1174]: + - generic [ref=e1175]: + - generic [ref=e1176]: Status + - generic [ref=e1178]: + - generic [ref=e1179]: + - generic [ref=e1180]: Payment + - generic [ref=e1181]: Pending + - generic [ref=e1182]: + - generic [ref=e1183]: Fulfillment + - generic [ref=e1184]: Unfulfilled + - generic [ref=e1185]: + - generic [ref=e1186]: Customer + - generic [ref=e1188]: + - generic [ref=e1189]: Jane Smith + - link "jane@example.com" [ref=e1190] [cursor=pointer]: + - /url: mailto:jane@example.com + - generic [ref=e1191]: + - generic [ref=e1192]: Shipping address + - generic [ref=e1194]: Jane Smith Schillerstrasse 45 Munich Bavaria BY Germany DE 80336 + - status \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-09-37-169Z.yml b/.playwright-mcp/page-2026-07-11T11-09-37-169Z.yml new file mode 100644 index 00000000..aad1099d --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-09-37-169Z.yml @@ -0,0 +1,372 @@ +- generic [active] [ref=e985]: + - dialog [ref=e1195]: + - iframe [ref=e1196]: + - generic [ref=f1e2]: + - generic [ref=f1e4]: + - generic [ref=f1e5]: Internal Server Error + - button "Copy as Markdown" [ref=f1e11] [cursor=pointer] + - generic [ref=f1e18]: + - generic [ref=f1e19]: + - heading "App\\Exceptions\\InvalidInventoryOperationException" [level=1] [ref=f1e20] + - generic [ref=f1e21]: app/Services/InventoryService.php:62 + - paragraph [ref=f1e23]: Cannot commit more inventory than is reserved. + - generic [ref=f1e24]: + - generic [ref=f1e25]: + - generic [ref=f1e26]: + - generic [ref=f1e27]: LARAVEL + - generic [ref=f1e28]: 12.51.0 + - generic [ref=f1e29]: + - generic [ref=f1e30]: PHP + - generic [ref=f1e31]: 8.4.17 + - generic [ref=f1e32]: UNHANDLED + - generic [ref=f1e36]: CODE 0 + - generic [ref=f1e38]: + - generic [ref=f1e39]: "500" + - generic [ref=f1e43]: POST + - generic [ref=f1e47]: http://acme-fashion.test/livewire-6701cc17/update + - button [ref=f1e48] [cursor=pointer] + - generic [ref=f1e53]: + - generic [ref=f1e54]: + - heading "Exception trace" [level=3] [ref=f1e60] + - generic [ref=f1e61]: + - generic [ref=f1e62]: + - generic [ref=f1e63] [cursor=pointer]: + - generic [ref=f1e66]: + - code [ref=f1e70]: + - generic [ref=f1e71]: app/Services/InventoryService.php + - generic [ref=f1e72]: app/Services/InventoryService.php:62 + - button [ref=f1e75] + - code [ref=f1e84]: + - generic [ref=f1e85]: "57" + - generic [ref=f1e86]: "58 DB::transaction(function () use ($item, $quantity): void {" + - generic [ref=f1e87]: 59 $inventory = $this->lock($item); + - generic [ref=f1e88]: "60" + - generic [ref=f1e89]: "61 if ($inventory->quantity_reserved < $quantity) {" + - generic [ref=f1e90]: 62 throw new InvalidInventoryOperationException('Cannot commit more inventory than is reserved.'); + - generic [ref=f1e91]: "63 }" + - generic [ref=f1e92]: "64" + - generic [ref=f1e93]: 65 $inventory->update([ + - generic [ref=f1e94]: 66 'quantity_on_hand' => $inventory->quantity_on_hand - $quantity, + - generic [ref=f1e95]: 67 'quantity_reserved' => $inventory->quantity_reserved - $quantity, + - generic [ref=f1e96]: 68 ]); + - generic [ref=f1e97]: "69" + - generic [ref=f1e98]: 70 $this->syncModel($item, $inventory); + - generic [ref=f1e99]: "71 });" + - generic [ref=f1e100]: "72 }" + - generic [ref=f1e101]: "73" + - generic [ref=f1e102]: "74" + - generic [ref=f1e104] [cursor=pointer]: + - generic [ref=f1e109]: 3 vendor frames + - button [ref=f1e110] + - generic [ref=f1e116] [cursor=pointer]: + - generic [ref=f1e119]: + - code [ref=f1e123]: + - generic [ref=f1e124]: app/Services/InventoryService.php + - generic [ref=f1e125]: app/Services/InventoryService.php:58 + - button [ref=f1e128] + - generic [ref=f1e134] [cursor=pointer]: + - generic [ref=f1e137]: + - code [ref=f1e141]: + - generic [ref=f1e142]: app/Services/PaymentService.php + - generic [ref=f1e143]: app/Services/PaymentService.php:46 + - button [ref=f1e146] + - generic [ref=f1e152] [cursor=pointer]: + - generic [ref=f1e157]: 3 vendor frames + - button [ref=f1e158] + - generic [ref=f1e164] [cursor=pointer]: + - generic [ref=f1e167]: + - code [ref=f1e171]: + - generic [ref=f1e172]: app/Services/PaymentService.php + - generic [ref=f1e173]: app/Services/PaymentService.php:44 + - button [ref=f1e176] + - generic [ref=f1e182] [cursor=pointer]: + - generic [ref=f1e185]: + - code [ref=f1e189]: + - generic [ref=f1e190]: app/Livewire/Admin/Orders/Show.php + - generic [ref=f1e191]: app/Livewire/Admin/Orders/Show.php:48 + - button [ref=f1e194] + - generic [ref=f1e200] [cursor=pointer]: + - generic [ref=f1e205]: 58 vendor frames + - button [ref=f1e206] + - generic [ref=f1e212] [cursor=pointer]: + - generic [ref=f1e215]: + - code [ref=f1e219]: + - generic [ref=f1e220]: public/index.php + - generic [ref=f1e221]: public/index.php:20 + - button [ref=f1e224] + - generic [ref=f1e230] [cursor=pointer]: + - generic [ref=f1e235]: 1 vendor frame + - button [ref=f1e236] + - generic [ref=f1e241]: + - generic [ref=f1e242]: + - heading "Queries" [level=3] [ref=f1e247] + - generic [ref=f1e248]: 1-10 of 16 + - generic [ref=f1e250]: + - generic [ref=f1e251]: + - generic [ref=f1e252]: + - generic [ref=f1e253]: sqlite + - code [ref=f1e260]: + - generic [ref=f1e261]: select * from "users" where "id" = 1 limit 1 + - generic [ref=f1e262]: 4.7ms + - generic [ref=f1e263]: + - generic [ref=f1e264]: + - generic [ref=f1e265]: sqlite + - code [ref=f1e272]: + - generic [ref=f1e273]: select * from "orders" where "id" = '5' limit 1 + - generic [ref=f1e274]: 0.17ms + - generic [ref=f1e275]: + - generic [ref=f1e276]: + - generic [ref=f1e277]: sqlite + - code [ref=f1e284]: + - generic [ref=f1e285]: select * from "stores" where "stores"."id" = 1 limit 1 + - generic [ref=f1e286]: 0.06ms + - generic [ref=f1e287]: + - generic [ref=f1e288]: + - generic [ref=f1e289]: sqlite + - code [ref=f1e296]: + - generic [ref=f1e297]: select exists(select * from "store_users" where "store_users"."user_id" = 1 and "store_users"."user_id" is not null and "store_id" = 1) as "exists" + - generic [ref=f1e298]: 0.2ms + - generic [ref=f1e299]: + - generic [ref=f1e300]: + - generic [ref=f1e301]: sqlite + - code [ref=f1e308]: + - generic [ref=f1e309]: select * from "orders" where "store_id" = 1 and "orders"."id" = 5 and "orders"."store_id" = 1 limit 1 + - generic [ref=f1e310]: 0.06ms + - generic [ref=f1e311]: + - generic [ref=f1e312]: + - generic [ref=f1e313]: sqlite + - code [ref=f1e320]: + - generic [ref=f1e321]: select * from "customers" where "customers"."id" in (2) and "customers"."store_id" = 1 + - generic [ref=f1e322]: 0.14ms + - generic [ref=f1e323]: + - generic [ref=f1e324]: + - generic [ref=f1e325]: sqlite + - code [ref=f1e332]: + - generic [ref=f1e333]: select * from "order_lines" where "order_lines"."order_id" in (5) + - generic [ref=f1e334]: 0.1ms + - generic [ref=f1e335]: + - generic [ref=f1e336]: + - generic [ref=f1e337]: sqlite + - code [ref=f1e344]: + - generic [ref=f1e345]: select * from "payments" where "payments"."order_id" in (5) + - generic [ref=f1e346]: 0.14ms + - generic [ref=f1e347]: + - generic [ref=f1e348]: + - generic [ref=f1e349]: sqlite + - code [ref=f1e356]: + - generic [ref=f1e357]: select * from "refunds" where "refunds"."payment_id" in (5) + - generic [ref=f1e358]: 0.12ms + - generic [ref=f1e359]: + - generic [ref=f1e360]: + - generic [ref=f1e361]: sqlite + - code [ref=f1e368]: + - generic [ref=f1e369]: select * from "refunds" where "refunds"."order_id" in (5) + - generic [ref=f1e370]: 0.02ms + - generic [ref=f1e371]: + - button [disabled] [ref=f1e372] + - button [disabled] [ref=f1e376] + - button "1" [ref=f1e380] [cursor=pointer] + - button "2" [ref=f1e382] [cursor=pointer] + - button [ref=f1e383] [cursor=pointer] + - button [ref=f1e386] [cursor=pointer] + - generic [ref=f1e391]: + - generic [ref=f1e392]: + - heading "Headers" [level=2] [ref=f1e393] + - generic [ref=f1e394]: + - generic [ref=f1e395]: + - generic [ref=f1e396]: cookie + - generic [ref=f1e398]: XSRF-TOKEN=eyJpdiI6IitGSm1id1JIazBDSFlqQTJVTEowZkE9PSIsInZhbHVlIjoiOGxBWC9kcTBEQ2c2RVVuRDgwbTBDWDVHenBBLzljbmhaVkpuOGFwRTJuRUZ2VXBjNXowSDNxRlBPL2p2OEU4MHIzc285dmgzdHkwOUlXVE05Q1I1VWVBa3NPYllrOHNRdkhiNUJyYjhrM1hSQ1B0emxzRStmcUNVdDZCSXlkaEoiLCJtYWMiOiI2MGZhYTY5YmUyN2ExMzYyNzUyNTc4MTQ4ZmM5M2RmMmU4ZjNlOWNmMDY3YjNhMjI1YzNlMDExN2RiMTIwODg1IiwidGFnIjoiIn0%3D; shop_session=eyJpdiI6Im5Db3lQVnpiOGtsWHNTSVp5Si9rK1E9PSIsInZhbHVlIjoiWFNvYlF3SVZ5YWZBUDhEeENFczg4MnREdUI0L2F1YU50akpOK29MdXVPWmlpb1JqdThnZCs1cDhUMzh4cDhTME5Ba2NKU3pGc1ltZ2dCSDFyQWFBbnk5SG80cmJLcGF6eFdydnBhSi9NQlRjWm5VbGxHNHlaN3k2ZnlsakFnWm4iLCJtYWMiOiJhOWRkNjBlZmExODYwMWJhNDk0YzA4OGU0YTNiMWM2YzM5MDU3NjE5YmQxODhlZWJlNzM2ZmFkOWE0NTFkOWFmIiwidGFnIjoiIn0%3D + - generic [ref=f1e399]: + - generic [ref=f1e400]: accept-language + - generic [ref=f1e402]: en-GB,en-US;q=0.9,en;q=0.8 + - generic [ref=f1e403]: + - generic [ref=f1e404]: accept-encoding + - generic [ref=f1e406]: gzip, deflate + - generic [ref=f1e407]: + - generic [ref=f1e408]: referer + - generic [ref=f1e410]: http://acme-fashion.test/admin/orders/5 + - generic [ref=f1e411]: + - generic [ref=f1e412]: origin + - generic [ref=f1e414]: http://acme-fashion.test + - generic [ref=f1e415]: + - generic [ref=f1e416]: accept + - generic [ref=f1e418]: "*/*" + - generic [ref=f1e419]: + - generic [ref=f1e420]: x-livewire + - generic [ref=f1e422]: "1" + - generic [ref=f1e423]: + - generic [ref=f1e424]: content-type + - generic [ref=f1e426]: application/json + - generic [ref=f1e427]: + - generic [ref=f1e428]: user-agent + - generic [ref=f1e430]: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 + - generic [ref=f1e431]: + - generic [ref=f1e432]: content-length + - generic [ref=f1e434]: "730" + - generic [ref=f1e435]: + - generic [ref=f1e436]: connection + - generic [ref=f1e438]: keep-alive + - generic [ref=f1e439]: + - generic [ref=f1e440]: host + - generic [ref=f1e442]: acme-fashion.test + - generic [ref=f1e443]: + - heading "Body" [level=2] [ref=f1e444] + - code [ref=f1e449]: + - generic [ref=f1e450]: "{" + - generic [ref=f1e451]: "\"_token\": \"v1cigc0AO9llddR4D8S9axZfXeOmbJvjtR964N87\"," + - generic [ref=f1e452]: "\"components\": [" + - generic [ref=f1e453]: "{" + - generic [ref=f1e454]: "\"snapshot\": \"{\"data\":{\"orderId\":5,\"showFulfillmentModal\":false,\"showRefundModal\":false,\"trackingCompany\":\"\",\"trackingNumber\":\"\",\"fulfillmentQuantities\":[{\"7\":1},{\"s\":\"arr\"}],\"refundAmount\":\"\",\"refundReason\":\"\",\"restock\":false},\"memo\":{\"id\":\"KP28VETFzacd91TwOUSd\",\"name\":\"admin.orders.show\",\"path\":\"admin/orders/5\",\"method\":\"GET\",\"release\":\"a-a-a\",\"children\":[],\"scripts\":[],\"assets\":[],\"errors\":[],\"locale\":\"en\",\"islands\":[]},\"checksum\":\"9987ae09bf7e180c3f2ad8b7fce034404e98d398409e5e9abd184fdfc0961398\"}\"," + - generic [ref=f1e455]: "\"updates\": []," + - generic [ref=f1e456]: "\"calls\": [" + - generic [ref=f1e457]: "{" + - generic [ref=f1e458]: "\"method\": \"confirmPayment\"," + - generic [ref=f1e459]: "\"params\": []," + - generic [ref=f1e460]: "\"metadata\": []" + - generic [ref=f1e461]: "}" + - generic [ref=f1e462]: "]" + - generic [ref=f1e463]: "}" + - generic [ref=f1e464]: "]" + - generic [ref=f1e465]: "}" + - generic [ref=f1e466]: + - heading "Routing" [level=2] [ref=f1e467] + - generic [ref=f1e468]: + - generic [ref=f1e469]: + - generic [ref=f1e470]: controller + - generic [ref=f1e472]: Livewire\Mechanisms\HandleRequests\HandleRequests@handleUpdate + - generic [ref=f1e473]: + - generic [ref=f1e474]: route name + - generic [ref=f1e476]: default-livewire.update + - generic [ref=f1e477]: + - generic [ref=f1e478]: middleware + - generic [ref=f1e480]: web + - generic [ref=f1e481]: + - heading "Routing parameters" [level=2] [ref=f1e482] + - generic [ref=f1e483]: // No routing parameters + - generic [ref=e986]: + - complementary [ref=e987]: + - navigation "Admin navigation" [ref=e988]: + - generic [ref=e993]: + - generic [ref=e994]: Shop + - generic [ref=e995]: Commerce platform + - generic [ref=e996]: + - link "Dashboard" [ref=e998] [cursor=pointer]: + - /url: /admin + - generic [ref=e1002]: Products + - generic [ref=e1003]: + - link "Products" [ref=e1004] [cursor=pointer]: + - /url: /admin/products + - link "Collections" [ref=e1008] [cursor=pointer]: + - /url: /admin/collections + - link "Inventory" [ref=e1012] [cursor=pointer]: + - /url: /admin/inventory + - generic [ref=e1016]: Orders + - link "Orders" [ref=e1018] [cursor=pointer]: + - /url: /admin/orders + - generic [ref=e1022]: Customers + - link "Customers" [ref=e1024] [cursor=pointer]: + - /url: /admin/customers + - generic [ref=e1028]: Discounts + - link "Discounts" [ref=e1030] [cursor=pointer]: + - /url: /admin/discounts + - generic [ref=e1035]: Content + - generic [ref=e1036]: + - link "Pages" [ref=e1037] [cursor=pointer]: + - /url: /admin/pages + - link "Navigation" [ref=e1041] [cursor=pointer]: + - /url: /admin/navigation + - link "Themes" [ref=e1045] [cursor=pointer]: + - /url: /admin/themes + - generic [ref=e1049]: Insights + - generic [ref=e1050]: + - link "Analytics" [ref=e1051] [cursor=pointer]: + - /url: /admin/analytics + - link "Search" [ref=e1056] [cursor=pointer]: + - /url: /admin/settings/search + - generic [ref=e1060]: Platform + - generic [ref=e1061]: + - link "Settings" [ref=e1062] [cursor=pointer]: + - /url: /admin/settings + - link "Apps" [ref=e1067] [cursor=pointer]: + - /url: /admin/apps + - link "Developers" [ref=e1071] [cursor=pointer]: + - /url: /admin/developers + - generic [ref=e1075]: + - banner [ref=e1076]: + - generic [ref=e1077]: + - button "Acme Fashion" [ref=e1080] + - generic [ref=e1084]: + - button "Notifications" [ref=e1086] + - button "AU Admin User" [ref=e1090]: + - generic [ref=e1091]: AU + - generic [ref=e1094]: Admin User + - main [ref=e1098]: + - generic [ref=e1099]: + - generic [ref=e1100]: + - link "Home" [ref=e1102] [cursor=pointer]: + - /url: /admin + - generic [ref=e1105]: "#1005" + - generic [ref=e1107]: + - generic [ref=e1108]: + - heading "#1005" [level=1] [ref=e1109] + - paragraph [ref=e1110]: Placed 2 hours ago + - generic [ref=e1111]: + - button "Confirm payment" [ref=e1112] + - button "Refund" [ref=e1120] + - generic [ref=e1133]: + - generic [ref=e1134]: Payment required + - text: This order must be paid before fulfillment can be created. + - generic [ref=e1135]: + - generic [ref=e1136]: + - generic [ref=e1137]: + - generic [ref=e1138]: Items + - generic [ref=e1141]: + - generic [ref=e1142]: + - generic [ref=e1143]: Leather Belt + - generic [ref=e1144]: SKU ACME-LEATHERB-SM-BLACK-2 · Qty 1 + - generic [ref=e1145]: €34.99 + - generic [ref=e1146]: + - generic [ref=e1147]: + - generic [ref=e1148]: Subtotal + - generic [ref=e1149]: €34.99 + - generic [ref=e1150]: + - generic [ref=e1151]: Discount + - generic [ref=e1152]: €0.00 + - generic [ref=e1153]: + - generic [ref=e1154]: Shipping + - generic [ref=e1155]: €4.99 + - generic [ref=e1156]: + - generic [ref=e1157]: Tax + - generic [ref=e1158]: €5.59 + - generic [ref=e1159]: + - generic [ref=e1160]: Total + - generic [ref=e1161]: €39.98 + - generic [ref=e1162]: + - generic [ref=e1163]: Fulfillment + - paragraph [ref=e1166]: No fulfillments yet. + - generic [ref=e1167]: + - generic [ref=e1168]: Timeline + - list [ref=e1170]: + - listitem [ref=e1171]: + - generic [ref=e1172]: Order created + - generic [ref=e1173]: Jul 11, 2026 09:00 + - generic [ref=e1174]: + - generic [ref=e1175]: + - generic [ref=e1176]: Status + - generic [ref=e1178]: + - generic [ref=e1179]: + - generic [ref=e1180]: Payment + - generic [ref=e1181]: Pending + - generic [ref=e1182]: + - generic [ref=e1183]: Fulfillment + - generic [ref=e1184]: Unfulfilled + - generic [ref=e1185]: + - generic [ref=e1186]: Customer + - generic [ref=e1188]: + - generic [ref=e1189]: Jane Smith + - link "jane@example.com" [ref=e1190] [cursor=pointer]: + - /url: mailto:jane@example.com + - generic [ref=e1191]: + - generic [ref=e1192]: Shipping address + - generic [ref=e1194]: Jane Smith Schillerstrasse 45 Munich Bavaria BY Germany DE 80336 + - status \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-10-28-347Z.yml b/.playwright-mcp/page-2026-07-11T11-10-28-347Z.yml new file mode 100644 index 00000000..2e89eea3 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-10-28-347Z.yml @@ -0,0 +1,16 @@ +- generic [ref=e2]: + - link "Laravel Starter Kit" [ref=e4] [cursor=pointer]: + - /url: http://acme-fashion.test/dashboard + - navigation [ref=e9]: + - generic [ref=e10]: + - generic [ref=e11]: Platform + - link "Dashboard" [ref=e15] [cursor=pointer]: + - /url: http://acme-fashion.test/dashboard + - navigation [ref=e21]: + - link "Repository" [ref=e23] [cursor=pointer]: + - /url: https://github.com/laravel/livewire-starter-kit + - link "Documentation" [ref=e32] [cursor=pointer]: + - /url: https://laravel.com/docs/starter-kits#livewire + - button "AU Admin User" [ref=e38]: + - generic [ref=e39]: AU + - generic [ref=e42]: Admin User \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-10-28-467Z.yml b/.playwright-mcp/page-2026-07-11T11-10-28-467Z.yml new file mode 100644 index 00000000..a5f8f099 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-10-28-467Z.yml @@ -0,0 +1,127 @@ +- generic [ref=f1e2]: + - complementary [ref=f1e3]: + - navigation "Admin navigation" [ref=f1e4]: + - generic [ref=f1e9]: + - generic [ref=f1e10]: Shop + - generic [ref=f1e11]: Commerce platform + - generic [ref=f1e12]: + - link "Dashboard" [ref=f1e14] [cursor=pointer]: + - /url: /admin + - generic [ref=f1e18]: Products + - generic [ref=f1e19]: + - link "Products" [ref=f1e20] [cursor=pointer]: + - /url: /admin/products + - link "Collections" [ref=f1e24] [cursor=pointer]: + - /url: /admin/collections + - link "Inventory" [ref=f1e28] [cursor=pointer]: + - /url: /admin/inventory + - generic [ref=f1e32]: Orders + - link "Orders" [ref=f1e34] [cursor=pointer]: + - /url: /admin/orders + - generic [ref=f1e38]: Customers + - link "Customers" [ref=f1e40] [cursor=pointer]: + - /url: /admin/customers + - generic [ref=f1e44]: Discounts + - link "Discounts" [ref=f1e46] [cursor=pointer]: + - /url: /admin/discounts + - generic [ref=f1e51]: Content + - generic [ref=f1e52]: + - link "Pages" [ref=f1e53] [cursor=pointer]: + - /url: /admin/pages + - link "Navigation" [ref=f1e57] [cursor=pointer]: + - /url: /admin/navigation + - link "Themes" [ref=f1e61] [cursor=pointer]: + - /url: /admin/themes + - generic [ref=f1e65]: Insights + - generic [ref=f1e66]: + - link "Analytics" [ref=f1e67] [cursor=pointer]: + - /url: /admin/analytics + - link "Search" [ref=f1e72] [cursor=pointer]: + - /url: /admin/settings/search + - generic [ref=f1e76]: Platform + - generic [ref=f1e77]: + - link "Settings" [ref=f1e78] [cursor=pointer]: + - /url: /admin/settings + - link "Apps" [ref=f1e83] [cursor=pointer]: + - /url: /admin/apps + - link "Developers" [ref=f1e87] [cursor=pointer]: + - /url: /admin/developers + - generic [ref=f1e91]: + - banner [ref=f1e92]: + - generic [ref=f1e93]: + - button "Acme Fashion" [ref=f1e96] + - generic [ref=f1e100]: + - button "Notifications" [ref=f1e102] + - button "AU Admin User" [ref=f1e106]: + - generic [ref=f1e107]: AU + - generic [ref=f1e110]: Admin User + - main [ref=f1e114]: + - generic [ref=f1e115]: + - generic [ref=f1e116]: + - link "Home" [ref=f1e118] [cursor=pointer]: + - /url: /admin + - generic [ref=f1e121]: "#1005" + - generic [ref=f1e123]: + - generic [ref=f1e124]: + - heading "#1005" [level=1] [ref=f1e125] + - paragraph [ref=f1e126]: Placed 2 hours ago + - generic [ref=f1e127]: + - button "Confirm payment" [ref=f1e128] + - button "Refund" [ref=f1e136] + - generic [ref=f1e149]: + - generic [ref=f1e150]: Payment required + - text: This order must be paid before fulfillment can be created. + - generic [ref=f1e151]: + - generic [ref=f1e152]: + - generic [ref=f1e153]: + - generic [ref=f1e154]: Items + - generic [ref=f1e157]: + - generic [ref=f1e158]: + - generic [ref=f1e159]: Leather Belt + - generic [ref=f1e160]: SKU ACME-LEATHERB-SM-BLACK-2 · Qty 1 + - generic [ref=f1e161]: €34.99 + - generic [ref=f1e162]: + - generic [ref=f1e163]: + - generic [ref=f1e164]: Subtotal + - generic [ref=f1e165]: €34.99 + - generic [ref=f1e166]: + - generic [ref=f1e167]: Discount + - generic [ref=f1e168]: €0.00 + - generic [ref=f1e169]: + - generic [ref=f1e170]: Shipping + - generic [ref=f1e171]: €4.99 + - generic [ref=f1e172]: + - generic [ref=f1e173]: Tax + - generic [ref=f1e174]: €5.59 + - generic [ref=f1e175]: + - generic [ref=f1e176]: Total + - generic [ref=f1e177]: €39.98 + - generic [ref=f1e178]: + - generic [ref=f1e179]: Fulfillment + - paragraph [ref=f1e182]: No fulfillments yet. + - generic [ref=f1e183]: + - generic [ref=f1e184]: Timeline + - list [ref=f1e186]: + - listitem [ref=f1e187]: + - generic [ref=f1e188]: Order created + - generic [ref=f1e189]: Jul 11, 2026 09:10 + - generic [ref=f1e190]: + - generic [ref=f1e191]: + - generic [ref=f1e192]: Status + - generic [ref=f1e194]: + - generic [ref=f1e195]: + - generic [ref=f1e196]: Payment + - generic [ref=f1e197]: Pending + - generic [ref=f1e198]: + - generic [ref=f1e199]: Fulfillment + - generic [ref=f1e200]: Unfulfilled + - generic [ref=f1e201]: + - generic [ref=f1e202]: Customer + - generic [ref=f1e204]: + - generic [ref=f1e205]: Jane Smith + - link "jane@example.com" [ref=f1e206] [cursor=pointer]: + - /url: mailto:jane@example.com + - generic [ref=f1e207]: + - generic [ref=f1e208]: Shipping address + - generic [ref=f1e210]: Jane Smith Schillerstrasse 45 Munich Bavaria BY Germany DE 80336 + - status \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-10-29-642Z.yml b/.playwright-mcp/page-2026-07-11T11-10-29-642Z.yml new file mode 100644 index 00000000..e6b38c2b --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-10-29-642Z.yml @@ -0,0 +1,125 @@ +- generic [ref=f1e2]: + - complementary [ref=f1e3]: + - navigation "Admin navigation" [ref=f1e4]: + - generic [ref=f1e9]: + - generic [ref=f1e10]: Shop + - generic [ref=f1e11]: Commerce platform + - generic [ref=f1e12]: + - link "Dashboard" [ref=f1e14] [cursor=pointer]: + - /url: /admin + - generic [ref=f1e18]: Products + - generic [ref=f1e19]: + - link "Products" [ref=f1e20] [cursor=pointer]: + - /url: /admin/products + - link "Collections" [ref=f1e24] [cursor=pointer]: + - /url: /admin/collections + - link "Inventory" [ref=f1e28] [cursor=pointer]: + - /url: /admin/inventory + - generic [ref=f1e32]: Orders + - link "Orders" [ref=f1e34] [cursor=pointer]: + - /url: /admin/orders + - generic [ref=f1e38]: Customers + - link "Customers" [ref=f1e40] [cursor=pointer]: + - /url: /admin/customers + - generic [ref=f1e44]: Discounts + - link "Discounts" [ref=f1e46] [cursor=pointer]: + - /url: /admin/discounts + - generic [ref=f1e51]: Content + - generic [ref=f1e52]: + - link "Pages" [ref=f1e53] [cursor=pointer]: + - /url: /admin/pages + - link "Navigation" [ref=f1e57] [cursor=pointer]: + - /url: /admin/navigation + - link "Themes" [ref=f1e61] [cursor=pointer]: + - /url: /admin/themes + - generic [ref=f1e65]: Insights + - generic [ref=f1e66]: + - link "Analytics" [ref=f1e67] [cursor=pointer]: + - /url: /admin/analytics + - link "Search" [ref=f1e72] [cursor=pointer]: + - /url: /admin/settings/search + - generic [ref=f1e76]: Platform + - generic [ref=f1e77]: + - link "Settings" [ref=f1e78] [cursor=pointer]: + - /url: /admin/settings + - link "Apps" [ref=f1e83] [cursor=pointer]: + - /url: /admin/apps + - link "Developers" [ref=f1e87] [cursor=pointer]: + - /url: /admin/developers + - generic [ref=f1e91]: + - banner [ref=f1e92]: + - generic [ref=f1e93]: + - button "Acme Fashion" [ref=f1e96] + - generic [ref=f1e100]: + - button "Notifications" [ref=f1e102] + - button "AU Admin User" [ref=f1e106]: + - generic [ref=f1e107]: AU + - generic [ref=f1e110]: Admin User + - main [ref=f1e114]: + - generic [ref=f1e115]: + - generic [ref=f1e116]: + - link "Home" [ref=f1e118] [cursor=pointer]: + - /url: /admin + - generic [ref=f1e121]: "#1005" + - generic [ref=f1e123]: + - generic [ref=f1e124]: + - heading "#1005" [level=1] [ref=f1e125] + - paragraph [ref=f1e126]: Placed 2 hours ago + - button "Refund" [ref=f1e136] + - generic [ref=f1e151]: + - generic [ref=f1e152]: + - generic [ref=f1e153]: + - generic [ref=f1e154]: Items + - generic [ref=f1e157]: + - generic [ref=f1e158]: + - generic [ref=f1e159]: Leather Belt + - generic [ref=f1e160]: SKU ACME-LEATHERB-SM-BLACK-2 · Qty 1 + - generic [ref=f1e161]: €34.99 + - generic [ref=f1e162]: + - generic [ref=f1e163]: + - generic [ref=f1e164]: Subtotal + - generic [ref=f1e165]: €34.99 + - generic [ref=f1e166]: + - generic [ref=f1e167]: Discount + - generic [ref=f1e168]: €0.00 + - generic [ref=f1e169]: + - generic [ref=f1e170]: Shipping + - generic [ref=f1e171]: €4.99 + - generic [ref=f1e172]: + - generic [ref=f1e173]: Tax + - generic [ref=f1e174]: €5.59 + - generic [ref=f1e175]: + - generic [ref=f1e176]: Total + - generic [ref=f1e177]: €39.98 + - generic [ref=f1e178]: + - generic [ref=f1e179]: Fulfillment + - generic [ref=f1e181]: + - paragraph [ref=f1e182]: No fulfillments yet. + - button "Create fulfillment" [ref=f1e211] + - generic [ref=f1e183]: + - generic [ref=f1e184]: Timeline + - list [ref=f1e186]: + - listitem [ref=f1e187]: + - generic [ref=f1e188]: Order created + - generic [ref=f1e189]: Jul 11, 2026 09:10 + - generic [ref=f1e190]: + - generic [ref=f1e191]: + - generic [ref=f1e192]: Status + - generic [ref=f1e194]: + - generic [ref=f1e195]: + - generic [ref=f1e196]: Payment + - generic [ref=f1e197]: Paid + - generic [ref=f1e198]: + - generic [ref=f1e199]: Fulfillment + - generic [ref=f1e200]: Unfulfilled + - generic [ref=f1e201]: + - generic [ref=f1e202]: Customer + - generic [ref=f1e204]: + - generic [ref=f1e205]: Jane Smith + - link "jane@example.com" [ref=f1e206] [cursor=pointer]: + - /url: mailto:jane@example.com + - generic [ref=f1e207]: + - generic [ref=f1e208]: Shipping address + - generic [ref=f1e210]: Jane Smith Schillerstrasse 45 Munich Bavaria BY Germany DE 80336 + - status [ref=f1e221]: + - generic [ref=f1e222]: Payment confirmed. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-10-39-032Z.yml b/.playwright-mcp/page-2026-07-11T11-10-39-032Z.yml new file mode 100644 index 00000000..20edb2d6 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-10-39-032Z.yml @@ -0,0 +1,54 @@ +- generic [ref=e1]: + - link "Skip to main content" [ref=e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=e3]: Free shipping available with code FREESHIP + - banner [ref=e4]: + - generic [ref=e5]: + - link "Acme Fashion" [ref=e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=e7]: + - link "Home" [ref=e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Sign in" [ref=e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account/login + - generic [ref=e12]: + - button "Open product search" [ref=e14]: + - generic [ref=e19]: Search + - button "Open shopping cart" [ref=e21]: + - generic [ref=e26]: Cart + - main [ref=e27]: + - generic [ref=e29]: + - generic [ref=e30]: Sign in + - paragraph [ref=e31]: Access your orders and saved addresses. + - generic [ref=e32]: + - generic [ref=e33]: + - generic [ref=e34]: Email + - textbox "Email" [active] [ref=e36] + - generic [ref=e37]: + - generic [ref=e38]: Password + - generic [ref=e39]: + - textbox "Password" [ref=e40] + - button "Toggle password visibility" [ref=e42] + - generic [ref=e46]: + - checkbox "Remember me" [ref=e47] + - generic [ref=e49]: Remember me + - button "Sign in" [ref=e50] + - paragraph [ref=e56]: + - text: New here? + - link "Create an account" [ref=e57] [cursor=pointer]: + - /url: http://acme-fashion.test/account/register + - contentinfo [ref=e58]: + - generic [ref=e59]: + - generic [ref=e60]: + - paragraph [ref=e61]: Acme Fashion + - paragraph [ref=e62]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=e63]: + - link "Shop all collections" [ref=e64] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=e65] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=e66]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-10-47-627Z.yml b/.playwright-mcp/page-2026-07-11T11-10-47-627Z.yml new file mode 100644 index 00000000..7142059f --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-10-47-627Z.yml @@ -0,0 +1,71 @@ +- generic [active] [ref=e67]: + - link "Skip to main content" [ref=e68] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=e69]: Free shipping available with code FREESHIP + - banner [ref=e70]: + - generic [ref=e71]: + - link "Acme Fashion" [ref=e72] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=e73]: + - link "Home" [ref=e74] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=e75] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=e76] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Account" [ref=e77] [cursor=pointer]: + - /url: http://acme-fashion.test/account + - generic [ref=e78]: + - button "Open product search" [ref=e80]: + - generic [ref=e85]: Search + - button "Open shopping cart" [ref=e87]: + - generic [ref=e92]: Cart + - main [ref=e93]: + - generic [ref=e94]: + - generic [ref=e95]: + - generic [ref=e96]: + - heading "Welcome, John Doe" [level=1] [ref=e97] + - paragraph [ref=e98]: customer@acme.test + - button "Sign out" [ref=e99] + - generic [ref=e105]: + - link [ref=e106] [cursor=pointer]: + - /url: http://acme-fashion.test/account/orders + - heading "Orders" [level=2] [ref=e107] + - paragraph [ref=e108]: View your order history + - link [ref=e109] [cursor=pointer]: + - /url: http://acme-fashion.test/account/addresses + - heading "Addresses" [level=2] [ref=e110] + - paragraph [ref=e111]: Manage delivery addresses + - heading "Recent orders" [level=2] [ref=e112] + - generic [ref=e113]: + - link "#1015 €54.47" [ref=e114] [cursor=pointer]: + - /url: http://acme-fashion.test/account/orders/#1015 + - generic [ref=e115]: "#1015" + - generic [ref=e116]: €54.47 + - link "#1010 €504.98" [ref=e118] [cursor=pointer]: + - /url: http://acme-fashion.test/account/orders/#1010 + - generic [ref=e119]: "#1010" + - generic [ref=e120]: €504.98 + - link "#1001 €54.97" [ref=e122] [cursor=pointer]: + - /url: http://acme-fashion.test/account/orders/#1001 + - generic [ref=e123]: "#1001" + - generic [ref=e124]: €54.97 + - link "#1002 €89.97" [ref=e126] [cursor=pointer]: + - /url: http://acme-fashion.test/account/orders/#1002 + - generic [ref=e127]: "#1002" + - generic [ref=e128]: €89.97 + - link "#1004 €29.98" [ref=e130] [cursor=pointer]: + - /url: http://acme-fashion.test/account/orders/#1004 + - generic [ref=e131]: "#1004" + - generic [ref=e132]: €29.98 + - contentinfo [ref=e134]: + - generic [ref=e135]: + - generic [ref=e136]: + - paragraph [ref=e137]: Acme Fashion + - paragraph [ref=e138]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=e139]: + - link "Shop all collections" [ref=e140] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=e141] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=e142]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-10-58-585Z.yml b/.playwright-mcp/page-2026-07-11T11-10-58-585Z.yml new file mode 100644 index 00000000..7142059f --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-10-58-585Z.yml @@ -0,0 +1,71 @@ +- generic [active] [ref=e67]: + - link "Skip to main content" [ref=e68] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=e69]: Free shipping available with code FREESHIP + - banner [ref=e70]: + - generic [ref=e71]: + - link "Acme Fashion" [ref=e72] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=e73]: + - link "Home" [ref=e74] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=e75] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=e76] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Account" [ref=e77] [cursor=pointer]: + - /url: http://acme-fashion.test/account + - generic [ref=e78]: + - button "Open product search" [ref=e80]: + - generic [ref=e85]: Search + - button "Open shopping cart" [ref=e87]: + - generic [ref=e92]: Cart + - main [ref=e93]: + - generic [ref=e94]: + - generic [ref=e95]: + - generic [ref=e96]: + - heading "Welcome, John Doe" [level=1] [ref=e97] + - paragraph [ref=e98]: customer@acme.test + - button "Sign out" [ref=e99] + - generic [ref=e105]: + - link [ref=e106] [cursor=pointer]: + - /url: http://acme-fashion.test/account/orders + - heading "Orders" [level=2] [ref=e107] + - paragraph [ref=e108]: View your order history + - link [ref=e109] [cursor=pointer]: + - /url: http://acme-fashion.test/account/addresses + - heading "Addresses" [level=2] [ref=e110] + - paragraph [ref=e111]: Manage delivery addresses + - heading "Recent orders" [level=2] [ref=e112] + - generic [ref=e113]: + - link "#1015 €54.47" [ref=e114] [cursor=pointer]: + - /url: http://acme-fashion.test/account/orders/#1015 + - generic [ref=e115]: "#1015" + - generic [ref=e116]: €54.47 + - link "#1010 €504.98" [ref=e118] [cursor=pointer]: + - /url: http://acme-fashion.test/account/orders/#1010 + - generic [ref=e119]: "#1010" + - generic [ref=e120]: €504.98 + - link "#1001 €54.97" [ref=e122] [cursor=pointer]: + - /url: http://acme-fashion.test/account/orders/#1001 + - generic [ref=e123]: "#1001" + - generic [ref=e124]: €54.97 + - link "#1002 €89.97" [ref=e126] [cursor=pointer]: + - /url: http://acme-fashion.test/account/orders/#1002 + - generic [ref=e127]: "#1002" + - generic [ref=e128]: €89.97 + - link "#1004 €29.98" [ref=e130] [cursor=pointer]: + - /url: http://acme-fashion.test/account/orders/#1004 + - generic [ref=e131]: "#1004" + - generic [ref=e132]: €29.98 + - contentinfo [ref=e134]: + - generic [ref=e135]: + - generic [ref=e136]: + - paragraph [ref=e137]: Acme Fashion + - paragraph [ref=e138]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=e139]: + - link "Shop all collections" [ref=e140] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=e141] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=e142]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-12-12-799Z.yml b/.playwright-mcp/page-2026-07-11T11-12-12-799Z.yml new file mode 100644 index 00000000..58d30b81 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-12-12-799Z.yml @@ -0,0 +1,71 @@ +- generic [active] [ref=e1]: + - link "Skip to main content" [ref=e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=e3]: Free shipping available with code FREESHIP + - banner [ref=e4]: + - generic [ref=e5]: + - link "Acme Fashion" [ref=e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=e7]: + - link "Home" [ref=e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Account" [ref=e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account + - generic [ref=e12]: + - button "Open product search" [ref=e14]: + - generic [ref=e19]: Search + - button "Open shopping cart" [ref=e21]: + - generic [ref=e26]: Cart + - main [ref=e27]: + - generic [ref=e28]: + - generic [ref=e29]: + - generic [ref=e30]: + - heading "Welcome, John Doe" [level=1] [ref=e31] + - paragraph [ref=e32]: customer@acme.test + - button "Sign out" [ref=e33] + - generic [ref=e39]: + - link [ref=e40] [cursor=pointer]: + - /url: http://acme-fashion.test/account/orders + - heading "Orders" [level=2] [ref=e41] + - paragraph [ref=e42]: View your order history + - link [ref=e43] [cursor=pointer]: + - /url: http://acme-fashion.test/account/addresses + - heading "Addresses" [level=2] [ref=e44] + - paragraph [ref=e45]: Manage delivery addresses + - heading "Recent orders" [level=2] [ref=e46] + - generic [ref=e47]: + - link "#1015 €54.47" [ref=e48] [cursor=pointer]: + - /url: http://acme-fashion.test/account/orders/15 + - generic [ref=e49]: "#1015" + - generic [ref=e50]: €54.47 + - link "#1010 €504.98" [ref=e52] [cursor=pointer]: + - /url: http://acme-fashion.test/account/orders/10 + - generic [ref=e53]: "#1010" + - generic [ref=e54]: €504.98 + - link "#1001 €54.97" [ref=e56] [cursor=pointer]: + - /url: http://acme-fashion.test/account/orders/1 + - generic [ref=e57]: "#1001" + - generic [ref=e58]: €54.97 + - link "#1002 €89.97" [ref=e60] [cursor=pointer]: + - /url: http://acme-fashion.test/account/orders/2 + - generic [ref=e61]: "#1002" + - generic [ref=e62]: €89.97 + - link "#1004 €29.98" [ref=e64] [cursor=pointer]: + - /url: http://acme-fashion.test/account/orders/4 + - generic [ref=e65]: "#1004" + - generic [ref=e66]: €29.98 + - contentinfo [ref=e68]: + - generic [ref=e69]: + - generic [ref=e70]: + - paragraph [ref=e71]: Acme Fashion + - paragraph [ref=e72]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=e73]: + - link "Shop all collections" [ref=e74] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=e75] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=e76]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-12-19-324Z.yml b/.playwright-mcp/page-2026-07-11T11-12-19-324Z.yml new file mode 100644 index 00000000..a5a04eef --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-12-19-324Z.yml @@ -0,0 +1,60 @@ +- generic [active] [ref=f1e1]: + - link "Skip to main content" [ref=f1e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=f1e3]: Free shipping available with code FREESHIP + - banner [ref=f1e4]: + - generic [ref=f1e5]: + - link "Acme Fashion" [ref=f1e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=f1e7]: + - link "Home" [ref=f1e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=f1e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f1e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Account" [ref=f1e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account + - generic [ref=f1e12]: + - button "Open product search" [ref=f1e14]: + - generic [ref=f1e19]: Search + - button "Open shopping cart" [ref=f1e21]: + - generic [ref=f1e26]: Cart + - main [ref=f1e27]: + - generic [ref=f1e28]: + - navigation "Breadcrumb" [ref=f1e29]: + - list [ref=f1e30]: + - listitem [ref=f1e31]: + - link "Account" [ref=f1e32] [cursor=pointer]: + - /url: http://acme-fashion.test/account + - generic [ref=f1e33]: / + - listitem [ref=f1e34]: + - link "Orders" [ref=f1e35] [cursor=pointer]: + - /url: http://acme-fashion.test/account/orders + - generic [ref=f1e36]: / + - listitem [ref=f1e37]: + - generic [ref=f1e38]: "#1001" + - generic [ref=f1e39]: + - generic [ref=f1e40]: + - 'heading "Order #1001" [level=1] [ref=f1e41]' + - paragraph [ref=f1e42]: Placed Jul 9, 2026 + - generic [ref=f1e43]: paid + - generic [ref=f1e44]: + - heading "Items" [level=2] [ref=f1e45] + - generic [ref=f1e46]: + - generic [ref=f1e47]: 2 × Classic Cotton T-Shirt + - generic [ref=f1e48]: €49.98 + - generic [ref=f1e50]: + - generic [ref=f1e51]: Total + - generic [ref=f1e52]: €54.97 + - contentinfo [ref=f1e54]: + - generic [ref=f1e55]: + - generic [ref=f1e56]: + - paragraph [ref=f1e57]: Acme Fashion + - paragraph [ref=f1e58]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=f1e59]: + - link "Shop all collections" [ref=f1e60] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f1e61] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=f1e62]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-12-28-195Z.yml b/.playwright-mcp/page-2026-07-11T11-12-28-195Z.yml new file mode 100644 index 00000000..de3ceb79 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-12-28-195Z.yml @@ -0,0 +1,100 @@ +- generic [active] [ref=f2e1]: + - link "Skip to main content" [ref=f2e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=f2e3]: Free shipping available with code FREESHIP + - banner [ref=f2e4]: + - generic [ref=f2e5]: + - link "Acme Electronics" [ref=f2e6] [cursor=pointer]: + - /url: http://acme-electronics.test + - navigation "Main navigation" [ref=f2e7]: + - link "Home" [ref=f2e8] [cursor=pointer]: + - /url: http://acme-electronics.test + - link "Collections" [ref=f2e9] [cursor=pointer]: + - /url: http://acme-electronics.test/collections + - link "Search" [ref=f2e10] [cursor=pointer]: + - /url: http://acme-electronics.test/search + - link "Sign in" [ref=f2e11] [cursor=pointer]: + - /url: http://acme-electronics.test/account/login + - generic [ref=f2e12]: + - button "Open product search" [ref=f2e14]: + - generic [ref=f2e19]: Search + - button "Open shopping cart" [ref=f2e21]: + - generic [ref=f2e26]: Cart + - main [ref=f2e27]: + - generic [ref=f2e28]: + - generic [ref=f2e31]: + - generic [ref=f2e32]: New season + - heading "Find your next favorite." [level=1] [ref=f2e33] + - paragraph [ref=f2e34]: Explore curated essentials from Acme Electronics. + - link "Shop collections" [ref=f2e36] [cursor=pointer]: + - /url: http://acme-electronics.test/collections + - generic [ref=f2e38]: + - generic [ref=f2e39]: + - generic [ref=f2e40]: + - paragraph [ref=f2e41]: Explore + - heading "Featured collections" [level=2] [ref=f2e42] + - link "View all" [ref=f2e43] [cursor=pointer]: + - /url: http://acme-electronics.test/collections + - generic [ref=f2e44]: + - link [ref=f2e45] [cursor=pointer]: + - /url: http://acme-electronics.test/collections/featured + - heading "Featured" [level=3] [ref=f2e46] + - paragraph [ref=f2e47]: 3 products + - link [ref=f2e48] [cursor=pointer]: + - /url: http://acme-electronics.test/collections/accessories + - heading "Accessories" [level=3] [ref=f2e49] + - paragraph [ref=f2e50]: 2 products + - generic [ref=f2e51]: + - heading "Featured products" [level=2] [ref=f2e52] + - generic [ref=f2e53]: + - article [ref=f2e54]: + - link "Monitor Stand image placeholder Monitor Stand €49.99" [ref=f2e55] [cursor=pointer]: + - /url: http://acme-electronics.test/products/monitor-stand + - img "Monitor Stand image placeholder" [ref=f2e56]: + - generic [ref=f2e57]: Monitor Stand + - generic [ref=f2e58]: + - heading "Monitor Stand" [level=2] [ref=f2e59] + - generic [ref=f2e60]: €49.99 + - article [ref=f2e62]: + - link "Mechanical Keyboard image placeholder Mechanical Keyboard €129.99" [ref=f2e63] [cursor=pointer]: + - /url: http://acme-electronics.test/products/mechanical-keyboard + - img "Mechanical Keyboard image placeholder" [ref=f2e64]: + - generic [ref=f2e65]: Mechanical Keyboard + - generic [ref=f2e66]: + - heading "Mechanical Keyboard" [level=2] [ref=f2e67] + - generic [ref=f2e68]: €129.99 + - article [ref=f2e70]: + - link "USB-C Cable 2m image placeholder USB-C Cable 2m €12.99" [ref=f2e71] [cursor=pointer]: + - /url: http://acme-electronics.test/products/usb-c-cable-2m + - img "USB-C Cable 2m image placeholder" [ref=f2e72]: + - generic [ref=f2e73]: USB-C Cable 2m + - generic [ref=f2e74]: + - heading "USB-C Cable 2m" [level=2] [ref=f2e75] + - generic [ref=f2e76]: €12.99 + - article [ref=f2e78]: + - link "Wireless Headphones image placeholder Wireless Headphones €149.99" [ref=f2e79] [cursor=pointer]: + - /url: http://acme-electronics.test/products/wireless-headphones + - img "Wireless Headphones image placeholder" [ref=f2e80]: + - generic [ref=f2e81]: Wireless Headphones + - generic [ref=f2e82]: + - heading "Wireless Headphones" [level=2] [ref=f2e83] + - generic [ref=f2e84]: €149.99 + - article [ref=f2e86]: + - link "Pro Laptop 15 image placeholder Pro Laptop 15 €999.99" [ref=f2e87] [cursor=pointer]: + - /url: http://acme-electronics.test/products/pro-laptop-15 + - img "Pro Laptop 15 image placeholder" [ref=f2e88]: + - generic [ref=f2e89]: Pro Laptop 15 + - generic [ref=f2e90]: + - heading "Pro Laptop 15" [level=2] [ref=f2e91] + - generic [ref=f2e92]: €999.99 + - contentinfo [ref=f2e94]: + - generic [ref=f2e95]: + - generic [ref=f2e96]: + - paragraph [ref=f2e97]: Acme Electronics + - paragraph [ref=f2e98]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=f2e99]: + - link "Shop all collections" [ref=f2e100] [cursor=pointer]: + - /url: http://acme-electronics.test/collections + - link "Search" [ref=f2e101] [cursor=pointer]: + - /url: http://acme-electronics.test/search + - paragraph [ref=f2e102]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-13-17-485Z.yml b/.playwright-mcp/page-2026-07-11T11-13-17-485Z.yml new file mode 100644 index 00000000..69abf61f --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-13-17-485Z.yml @@ -0,0 +1,93 @@ +- generic [active] [ref=f3e1]: + - link "Skip to main content" [ref=f3e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=f3e3]: Free shipping available with code FREESHIP + - banner [ref=f3e4]: + - generic [ref=f3e5]: + - link "Acme Electronics" [ref=f3e6] [cursor=pointer]: + - /url: http://acme-electronics.test + - generic [ref=f3e7]: + - button "Open product search" [ref=f3e9]: + - generic [ref=f3e14]: Search + - button "Open shopping cart" [ref=f3e16]: + - generic [ref=f3e21]: Cart + - group [ref=f3e22]: + - generic "Menu" [ref=f3e23] [cursor=pointer] + - main [ref=f3e24]: + - generic [ref=f3e25]: + - generic [ref=f3e28]: + - generic [ref=f3e29]: New season + - heading "Find your next favorite." [level=1] [ref=f3e30] + - paragraph [ref=f3e31]: Explore curated essentials from Acme Electronics. + - link "Shop collections" [ref=f3e33] [cursor=pointer]: + - /url: http://acme-electronics.test/collections + - generic [ref=f3e35]: + - generic [ref=f3e36]: + - generic [ref=f3e37]: + - paragraph [ref=f3e38]: Explore + - heading "Featured collections" [level=2] [ref=f3e39] + - link "View all" [ref=f3e40] [cursor=pointer]: + - /url: http://acme-electronics.test/collections + - generic [ref=f3e41]: + - link [ref=f3e42] [cursor=pointer]: + - /url: http://acme-electronics.test/collections/featured + - heading "Featured" [level=3] [ref=f3e43] + - paragraph [ref=f3e44]: 3 products + - link [ref=f3e45] [cursor=pointer]: + - /url: http://acme-electronics.test/collections/accessories + - heading "Accessories" [level=3] [ref=f3e46] + - paragraph [ref=f3e47]: 2 products + - generic [ref=f3e48]: + - heading "Featured products" [level=2] [ref=f3e49] + - generic [ref=f3e50]: + - article [ref=f3e51]: + - link "Monitor Stand image placeholder Monitor Stand €49.99" [ref=f3e52] [cursor=pointer]: + - /url: http://acme-electronics.test/products/monitor-stand + - img "Monitor Stand image placeholder" [ref=f3e53]: + - generic [ref=f3e54]: Monitor Stand + - generic [ref=f3e55]: + - heading "Monitor Stand" [level=2] [ref=f3e56] + - generic [ref=f3e57]: €49.99 + - article [ref=f3e59]: + - link "Mechanical Keyboard image placeholder Mechanical Keyboard €129.99" [ref=f3e60] [cursor=pointer]: + - /url: http://acme-electronics.test/products/mechanical-keyboard + - img "Mechanical Keyboard image placeholder" [ref=f3e61]: + - generic [ref=f3e62]: Mechanical Keyboard + - generic [ref=f3e63]: + - heading "Mechanical Keyboard" [level=2] [ref=f3e64] + - generic [ref=f3e65]: €129.99 + - article [ref=f3e67]: + - link "USB-C Cable 2m image placeholder USB-C Cable 2m €12.99" [ref=f3e68] [cursor=pointer]: + - /url: http://acme-electronics.test/products/usb-c-cable-2m + - img "USB-C Cable 2m image placeholder" [ref=f3e69]: + - generic [ref=f3e70]: USB-C Cable 2m + - generic [ref=f3e71]: + - heading "USB-C Cable 2m" [level=2] [ref=f3e72] + - generic [ref=f3e73]: €12.99 + - article [ref=f3e75]: + - link "Wireless Headphones image placeholder Wireless Headphones €149.99" [ref=f3e76] [cursor=pointer]: + - /url: http://acme-electronics.test/products/wireless-headphones + - img "Wireless Headphones image placeholder" [ref=f3e77]: + - generic [ref=f3e78]: Wireless Headphones + - generic [ref=f3e79]: + - heading "Wireless Headphones" [level=2] [ref=f3e80] + - generic [ref=f3e81]: €149.99 + - article [ref=f3e83]: + - link "Pro Laptop 15 image placeholder Pro Laptop 15 €999.99" [ref=f3e84] [cursor=pointer]: + - /url: http://acme-electronics.test/products/pro-laptop-15 + - img "Pro Laptop 15 image placeholder" [ref=f3e85]: + - generic [ref=f3e86]: Pro Laptop 15 + - generic [ref=f3e87]: + - heading "Pro Laptop 15" [level=2] [ref=f3e88] + - generic [ref=f3e89]: €999.99 + - contentinfo [ref=f3e91]: + - generic [ref=f3e92]: + - generic [ref=f3e93]: + - paragraph [ref=f3e94]: Acme Electronics + - paragraph [ref=f3e95]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=f3e96]: + - link "Shop all collections" [ref=f3e97] [cursor=pointer]: + - /url: http://acme-electronics.test/collections + - link "Search" [ref=f3e98] [cursor=pointer]: + - /url: http://acme-electronics.test/search + - paragraph [ref=f3e99]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-13-26-021Z.yml b/.playwright-mcp/page-2026-07-11T11-13-26-021Z.yml new file mode 100644 index 00000000..0cb9698d --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-13-26-021Z.yml @@ -0,0 +1,100 @@ +- generic [ref=f3e1]: + - link "Skip to main content" [ref=f3e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=f3e3]: Free shipping available with code FREESHIP + - banner [ref=f3e4]: + - generic [ref=f3e5]: + - link "Acme Electronics" [ref=f3e6] [cursor=pointer]: + - /url: http://acme-electronics.test + - generic [ref=f3e7]: + - button "Open product search" [ref=f3e9]: + - generic [ref=f3e14]: Search + - button "Open shopping cart" [ref=f3e16]: + - generic [ref=f3e21]: Cart + - group [ref=f3e22]: + - generic "Menu" [active] [ref=f3e23] [cursor=pointer] + - navigation "Mobile navigation" [ref=f3e100]: + - link "Home" [ref=f3e101] [cursor=pointer]: + - /url: http://acme-electronics.test + - link "Collections" [ref=f3e102] [cursor=pointer]: + - /url: http://acme-electronics.test/collections + - link "Account" [ref=f3e103] [cursor=pointer]: + - /url: http://acme-electronics.test/account + - main [ref=f3e24]: + - generic [ref=f3e25]: + - generic [ref=f3e28]: + - generic [ref=f3e29]: New season + - heading "Find your next favorite." [level=1] [ref=f3e30] + - paragraph [ref=f3e31]: Explore curated essentials from Acme Electronics. + - link "Shop collections" [ref=f3e33] [cursor=pointer]: + - /url: http://acme-electronics.test/collections + - generic [ref=f3e35]: + - generic [ref=f3e36]: + - generic [ref=f3e37]: + - paragraph [ref=f3e38]: Explore + - heading "Featured collections" [level=2] [ref=f3e39] + - link "View all" [ref=f3e40] [cursor=pointer]: + - /url: http://acme-electronics.test/collections + - generic [ref=f3e41]: + - link [ref=f3e42] [cursor=pointer]: + - /url: http://acme-electronics.test/collections/featured + - heading "Featured" [level=3] [ref=f3e43] + - paragraph [ref=f3e44]: 3 products + - link [ref=f3e45] [cursor=pointer]: + - /url: http://acme-electronics.test/collections/accessories + - heading "Accessories" [level=3] [ref=f3e46] + - paragraph [ref=f3e47]: 2 products + - generic [ref=f3e48]: + - heading "Featured products" [level=2] [ref=f3e49] + - generic [ref=f3e50]: + - article [ref=f3e51]: + - link "Monitor Stand image placeholder Monitor Stand €49.99" [ref=f3e52] [cursor=pointer]: + - /url: http://acme-electronics.test/products/monitor-stand + - img "Monitor Stand image placeholder" [ref=f3e53]: + - generic [ref=f3e54]: Monitor Stand + - generic [ref=f3e55]: + - heading "Monitor Stand" [level=2] [ref=f3e56] + - generic [ref=f3e57]: €49.99 + - article [ref=f3e59]: + - link "Mechanical Keyboard image placeholder Mechanical Keyboard €129.99" [ref=f3e60] [cursor=pointer]: + - /url: http://acme-electronics.test/products/mechanical-keyboard + - img "Mechanical Keyboard image placeholder" [ref=f3e61]: + - generic [ref=f3e62]: Mechanical Keyboard + - generic [ref=f3e63]: + - heading "Mechanical Keyboard" [level=2] [ref=f3e64] + - generic [ref=f3e65]: €129.99 + - article [ref=f3e67]: + - link "USB-C Cable 2m image placeholder USB-C Cable 2m €12.99" [ref=f3e68] [cursor=pointer]: + - /url: http://acme-electronics.test/products/usb-c-cable-2m + - img "USB-C Cable 2m image placeholder" [ref=f3e69]: + - generic [ref=f3e70]: USB-C Cable 2m + - generic [ref=f3e71]: + - heading "USB-C Cable 2m" [level=2] [ref=f3e72] + - generic [ref=f3e73]: €12.99 + - article [ref=f3e75]: + - link "Wireless Headphones image placeholder Wireless Headphones €149.99" [ref=f3e76] [cursor=pointer]: + - /url: http://acme-electronics.test/products/wireless-headphones + - img "Wireless Headphones image placeholder" [ref=f3e77]: + - generic [ref=f3e78]: Wireless Headphones + - generic [ref=f3e79]: + - heading "Wireless Headphones" [level=2] [ref=f3e80] + - generic [ref=f3e81]: €149.99 + - article [ref=f3e83]: + - link "Pro Laptop 15 image placeholder Pro Laptop 15 €999.99" [ref=f3e84] [cursor=pointer]: + - /url: http://acme-electronics.test/products/pro-laptop-15 + - img "Pro Laptop 15 image placeholder" [ref=f3e85]: + - generic [ref=f3e86]: Pro Laptop 15 + - generic [ref=f3e87]: + - heading "Pro Laptop 15" [level=2] [ref=f3e88] + - generic [ref=f3e89]: €999.99 + - contentinfo [ref=f3e91]: + - generic [ref=f3e92]: + - generic [ref=f3e93]: + - paragraph [ref=f3e94]: Acme Electronics + - paragraph [ref=f3e95]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=f3e96]: + - link "Shop all collections" [ref=f3e97] [cursor=pointer]: + - /url: http://acme-electronics.test/collections + - link "Search" [ref=f3e98] [cursor=pointer]: + - /url: http://acme-electronics.test/search + - paragraph [ref=f3e99]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-24-36-347Z.yml b/.playwright-mcp/page-2026-07-11T11-24-36-347Z.yml new file mode 100644 index 00000000..77aef4f2 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-24-36-347Z.yml @@ -0,0 +1,131 @@ +- generic [active] [ref=f4e1]: + - link "Skip to main content" [ref=f4e2] [cursor=pointer]: + - /url: "#main-content" + - generic [ref=f4e3]: Free shipping available with code FREESHIP + - banner [ref=f4e4]: + - generic [ref=f4e5]: + - link "Acme Fashion" [ref=f4e6] [cursor=pointer]: + - /url: http://acme-fashion.test + - navigation "Main navigation" [ref=f4e7]: + - link "Home" [ref=f4e8] [cursor=pointer]: + - /url: http://acme-fashion.test + - link "Collections" [ref=f4e9] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f4e10] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - link "Account" [ref=f4e11] [cursor=pointer]: + - /url: http://acme-fashion.test/account + - generic [ref=f4e12]: + - button "Open product search" [ref=f4e14]: + - generic [ref=f4e19]: Search + - button "Open shopping cart" [ref=f4e21]: + - generic [ref=f4e26]: Cart + - main [ref=f4e27]: + - generic [ref=f4e28]: + - generic [ref=f4e31]: + - generic [ref=f4e32]: New season + - heading "Find your next favorite." [level=1] [ref=f4e33] + - paragraph [ref=f4e34]: Explore curated essentials from Acme Fashion. + - link "Shop collections" [ref=f4e36] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - generic [ref=f4e38]: + - generic [ref=f4e39]: + - generic [ref=f4e40]: + - paragraph [ref=f4e41]: Explore + - heading "Featured collections" [level=2] [ref=f4e42] + - link "View all" [ref=f4e43] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - generic [ref=f4e44]: + - link [ref=f4e45] [cursor=pointer]: + - /url: http://acme-fashion.test/collections/new-arrivals + - heading "New Arrivals" [level=3] [ref=f4e46] + - paragraph [ref=f4e47]: 7 products + - link [ref=f4e48] [cursor=pointer]: + - /url: http://acme-fashion.test/collections/t-shirts + - heading "T-Shirts" [level=3] [ref=f4e49] + - paragraph [ref=f4e50]: 4 products + - link [ref=f4e51] [cursor=pointer]: + - /url: http://acme-fashion.test/collections/pants-jeans + - heading "Pants & Jeans" [level=3] [ref=f4e52] + - paragraph [ref=f4e53]: 4 products + - generic [ref=f4e54]: + - heading "Featured products" [level=2] [ref=f4e55] + - generic [ref=f4e56]: + - article [ref=f4e57]: + - link "Cashmere Overcoat image placeholder Cashmere Overcoat €499.99" [ref=f4e58] [cursor=pointer]: + - /url: http://acme-fashion.test/products/cashmere-overcoat + - img "Cashmere Overcoat image placeholder" [ref=f4e59]: + - generic [ref=f4e60]: Cashmere Overcoat + - generic [ref=f4e61]: + - heading "Cashmere Overcoat" [level=2] [ref=f4e62] + - generic [ref=f4e63]: €499.99 + - article [ref=f4e65]: + - link "Gift Card image placeholder Gift Card €25.00" [ref=f4e66] [cursor=pointer]: + - /url: http://acme-fashion.test/products/gift-card + - img "Gift Card image placeholder" [ref=f4e67]: + - generic [ref=f4e68]: Gift Card + - generic [ref=f4e69]: + - heading "Gift Card" [level=2] [ref=f4e70] + - generic [ref=f4e71]: €25.00 + - article [ref=f4e73]: + - link "Backorder Denim Jacket image placeholder Backorder Denim Jacket €99.99" [ref=f4e74] [cursor=pointer]: + - /url: http://acme-fashion.test/products/backorder-denim-jacket + - img "Backorder Denim Jacket image placeholder" [ref=f4e75]: + - generic [ref=f4e76]: Backorder Denim Jacket + - generic [ref=f4e77]: + - heading "Backorder Denim Jacket" [level=2] [ref=f4e78] + - generic [ref=f4e79]: €99.99 + - article [ref=f4e81]: + - link "Limited Edition Sneakers image placeholder Limited Edition Sneakers €159.99" [ref=f4e82] [cursor=pointer]: + - /url: http://acme-fashion.test/products/limited-edition-sneakers + - img "Limited Edition Sneakers image placeholder" [ref=f4e83]: + - generic [ref=f4e84]: Limited Edition Sneakers + - generic [ref=f4e85]: + - heading "Limited Edition Sneakers" [level=2] [ref=f4e86] + - generic [ref=f4e87]: €159.99 + - article [ref=f4e89]: + - link "Bucket Hat image placeholder Bucket Hat €24.99" [ref=f4e90] [cursor=pointer]: + - /url: http://acme-fashion.test/products/bucket-hat + - img "Bucket Hat image placeholder" [ref=f4e91]: + - generic [ref=f4e92]: Bucket Hat + - generic [ref=f4e93]: + - heading "Bucket Hat" [level=2] [ref=f4e94] + - generic [ref=f4e95]: €24.99 + - article [ref=f4e97]: + - link "Canvas Tote Bag image placeholder Canvas Tote Bag €19.99" [ref=f4e98] [cursor=pointer]: + - /url: http://acme-fashion.test/products/canvas-tote-bag + - img "Canvas Tote Bag image placeholder" [ref=f4e99]: + - generic [ref=f4e100]: Canvas Tote Bag + - generic [ref=f4e101]: + - heading "Canvas Tote Bag" [level=2] [ref=f4e102] + - generic [ref=f4e103]: €19.99 + - article [ref=f4e105]: + - link "Wool Scarf image placeholder Wool Scarf €29.99" [ref=f4e106] [cursor=pointer]: + - /url: http://acme-fashion.test/products/wool-scarf + - img "Wool Scarf image placeholder" [ref=f4e107]: + - generic [ref=f4e108]: Wool Scarf + - generic [ref=f4e109]: + - heading "Wool Scarf" [level=2] [ref=f4e110] + - generic [ref=f4e111]: €29.99 + - article [ref=f4e113]: + - link "Wide Leg Trousers image placeholder Wide Leg Trousers €49.99 €69.99 Sale price" [ref=f4e114] [cursor=pointer]: + - /url: http://acme-fashion.test/products/wide-leg-trousers + - img "Wide Leg Trousers image placeholder" [ref=f4e115]: + - generic [ref=f4e116]: Wide Leg Trousers + - generic [ref=f4e117]: + - heading "Wide Leg Trousers" [level=2] [ref=f4e118] + - generic [ref=f4e119]: + - generic [ref=f4e120]: €49.99 + - generic [ref=f4e121]: €69.99 + - generic [ref=f4e122]: Sale price + - contentinfo [ref=f4e123]: + - generic [ref=f4e124]: + - generic [ref=f4e125]: + - paragraph [ref=f4e126]: Acme Fashion + - paragraph [ref=f4e127]: Thoughtfully selected products for everyday life. + - navigation "Footer navigation" [ref=f4e128]: + - link "Shop all collections" [ref=f4e129] [cursor=pointer]: + - /url: http://acme-fashion.test/collections + - link "Search" [ref=f4e130] [cursor=pointer]: + - /url: http://acme-fashion.test/search + - paragraph [ref=f4e131]: Secure mock checkout. Prices shown in EUR. \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-24-36-506Z.yml b/.playwright-mcp/page-2026-07-11T11-24-36-506Z.yml new file mode 100644 index 00000000..2e89eea3 --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-24-36-506Z.yml @@ -0,0 +1,16 @@ +- generic [ref=e2]: + - link "Laravel Starter Kit" [ref=e4] [cursor=pointer]: + - /url: http://acme-fashion.test/dashboard + - navigation [ref=e9]: + - generic [ref=e10]: + - generic [ref=e11]: Platform + - link "Dashboard" [ref=e15] [cursor=pointer]: + - /url: http://acme-fashion.test/dashboard + - navigation [ref=e21]: + - link "Repository" [ref=e23] [cursor=pointer]: + - /url: https://github.com/laravel/livewire-starter-kit + - link "Documentation" [ref=e32] [cursor=pointer]: + - /url: https://laravel.com/docs/starter-kits#livewire + - button "AU Admin User" [ref=e38]: + - generic [ref=e39]: AU + - generic [ref=e42]: Admin User \ No newline at end of file diff --git a/.playwright-mcp/page-2026-07-11T11-25-55-023Z.yml b/.playwright-mcp/page-2026-07-11T11-25-55-023Z.yml new file mode 100644 index 00000000..10e76cbd --- /dev/null +++ b/.playwright-mcp/page-2026-07-11T11-25-55-023Z.yml @@ -0,0 +1,158 @@ +- generic [ref=f1e2]: + - complementary [ref=f1e3]: + - navigation "Admin navigation" [ref=f1e4]: + - generic [ref=f1e9]: + - generic [ref=f1e10]: Shop + - generic [ref=f1e11]: Commerce platform + - generic [ref=f1e12]: + - link "Dashboard" [ref=f1e14] [cursor=pointer]: + - /url: /admin + - generic [ref=f1e18]: Products + - generic [ref=f1e19]: + - link "Products" [ref=f1e20] [cursor=pointer]: + - /url: /admin/products + - link "Collections" [ref=f1e24] [cursor=pointer]: + - /url: /admin/collections + - link "Inventory" [ref=f1e28] [cursor=pointer]: + - /url: /admin/inventory + - generic [ref=f1e32]: Orders + - link "Orders" [ref=f1e34] [cursor=pointer]: + - /url: /admin/orders + - generic [ref=f1e38]: Customers + - link "Customers" [ref=f1e40] [cursor=pointer]: + - /url: /admin/customers + - generic [ref=f1e44]: Discounts + - link "Discounts" [ref=f1e46] [cursor=pointer]: + - /url: /admin/discounts + - generic [ref=f1e51]: Content + - generic [ref=f1e52]: + - link "Pages" [ref=f1e53] [cursor=pointer]: + - /url: /admin/pages + - link "Navigation" [ref=f1e57] [cursor=pointer]: + - /url: /admin/navigation + - link "Themes" [ref=f1e61] [cursor=pointer]: + - /url: /admin/themes + - generic [ref=f1e65]: Insights + - generic [ref=f1e66]: + - link "Analytics" [ref=f1e67] [cursor=pointer]: + - /url: /admin/analytics + - link "Search" [ref=f1e72] [cursor=pointer]: + - /url: /admin/settings/search + - generic [ref=f1e76]: Platform + - generic [ref=f1e77]: + - link "Settings" [ref=f1e78] [cursor=pointer]: + - /url: /admin/settings + - link "Apps" [ref=f1e83] [cursor=pointer]: + - /url: /admin/apps + - link "Developers" [ref=f1e87] [cursor=pointer]: + - /url: /admin/developers + - generic [ref=f1e91]: + - banner [ref=f1e92]: + - generic [ref=f1e93]: + - button "Acme Fashion" [ref=f1e96] + - generic [ref=f1e100]: + - button "Notifications" [ref=f1e102] + - button "AU Admin User" [ref=f1e106]: + - generic [ref=f1e107]: AU + - generic [ref=f1e110]: Admin User + - main [ref=f1e114]: + - generic [ref=f1e115]: + - generic [ref=f1e116]: + - link "Home" [ref=f1e118] [cursor=pointer]: + - /url: /admin + - generic [ref=f1e121]: Dashboard + - generic [ref=f1e123]: + - generic [ref=f1e124]: + - heading "Dashboard" [level=1] [ref=f1e125] + - paragraph [ref=f1e126]: A live view of store performance. + - combobox "Date range" [ref=f1e128]: + - option "Today" + - option "Last 7 days" + - option "Last 30 days" [selected] + - option "Custom range" + - generic [ref=f1e129]: + - generic [ref=f1e132]: + - paragraph [ref=f1e133]: Total sales + - generic [ref=f1e134]: €1,517.12 + - generic [ref=f1e139]: + - paragraph [ref=f1e140]: Orders + - generic [ref=f1e141]: "15" + - generic [ref=f1e146]: + - paragraph [ref=f1e147]: Average order value + - generic [ref=f1e148]: €101.14 + - generic [ref=f1e153]: + - paragraph [ref=f1e154]: Visitors + - generic [ref=f1e155]: 3,481 + - generic [ref=f1e158]: + - generic [ref=f1e159]: + - generic [ref=f1e160]: Orders over time + - generic "Orders chart" [ref=f1e162]: + - generic: + - 'generic "2026-06-16: 1"' + - generic: + - 'generic "2026-06-21: 1"' + - generic: + - 'generic "2026-06-26: 1"' + - generic: + - 'generic "2026-06-27: 1"' + - generic: + - 'generic "2026-06-29: 1"' + - generic: + - 'generic "2026-07-01: 1"' + - generic: + - 'generic "2026-07-06: 1"' + - generic: + - 'generic "2026-07-07: 1"' + - generic: + - 'generic "2026-07-08: 1"' + - generic: + - 'generic "2026-07-09: 1"' + - generic: + - 'generic "2026-07-10: 3"' + - generic: + - 'generic "2026-07-11: 2"' + - generic [ref=f1e163]: + - generic [ref=f1e164]: Conversion funnel + - generic [ref=f1e166]: + - generic [ref=f1e168]: + - generic [ref=f1e169]: Visits + - generic [ref=f1e170]: 3,481 + - generic [ref=f1e174]: + - generic [ref=f1e175]: Add To Cart + - generic [ref=f1e176]: "740" + - generic [ref=f1e180]: + - generic [ref=f1e181]: Checkout Started + - generic [ref=f1e182]: "352" + - generic [ref=f1e186]: + - generic [ref=f1e187]: Checkout Completed + - generic [ref=f1e188]: "152" + - generic [ref=f1e191]: + - generic [ref=f1e192]: Top products + - table [ref=f1e195]: + - rowgroup [ref=f1e196]: + - row [ref=f1e197]: + - columnheader "Product" [ref=f1e198] + - columnheader "Units sold" [ref=f1e199] + - columnheader "Revenue" [ref=f1e200] + - rowgroup [ref=f1e201]: + - row [ref=f1e202]: + - cell "Cashmere Overcoat" [ref=f1e203] + - cell "1" [ref=f1e204] + - cell "€499.99" [ref=f1e205] + - row [ref=f1e206]: + - cell "Classic Cotton T-Shirt" [ref=f1e207] + - cell "5" [ref=f1e208] + - cell "€124.95" [ref=f1e209] + - row [ref=f1e210]: + - cell "Running Sneakers" [ref=f1e211] + - cell "1" [ref=f1e212] + - cell "€119.99" [ref=f1e213] + - row [ref=f1e214]: + - cell "Premium Slim Fit Jeans" [ref=f1e215] + - cell "1" [ref=f1e216] + - cell "€79.99" [ref=f1e217] + - row [ref=f1e218]: + - cell "Chino Shorts" [ref=f1e219] + - cell "2" [ref=f1e220] + - cell "€79.98" [ref=f1e221] + - status \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..a5316efa --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +Your mission is to implement an entire shop system based on the specifications im specs/*. You must use sub-agents for role play (e.g. frontend-, backend-deveoper, QA Analyst, QA Engineer, etc). You must do in one go without stopping. You must test everything via Pest (unit, and functional tests). You must also additional simulate user behaviour using the Playwright MPC and confirm that all acceptance criterias are met. If you find bugs, you must fix them. The result is a perfect shop system. All requirements are perfectly implemented. All acceptance criterias are met, tested and confirmed by you. + +Continuously keep track of the progress in specs/progress.md Commit your progress after every relevant iteration with a meaningful message. + +When implementation is fully done, then make a full review meeting with Playwright in Chrome and showcase all features (customer- and admin-side) to me. In case bugs appear, you must fix them all and restart the review meeting. Shop is running at http://shop.test/. + +Don't re-use any existing implementation in another branch. Build it from scratch. diff --git a/app/Actions/SanitizeHtml.php b/app/Actions/SanitizeHtml.php new file mode 100644 index 00000000..25f21842 --- /dev/null +++ b/app/Actions/SanitizeHtml.php @@ -0,0 +1,100 @@ +> */ + private const ALLOWED = [ + 'p' => [], 'br' => [], 'strong' => [], 'em' => [], 'u' => [], + 'ol' => [], 'ul' => [], 'li' => [], 'a' => ['href'], 'img' => ['src', 'alt'], + 'h1' => [], 'h2' => [], 'h3' => [], 'h4' => [], 'h5' => [], 'h6' => [], + 'blockquote' => [], 'table' => [], 'thead' => [], 'tbody' => [], 'tr' => [], + 'th' => [], 'td' => [], 'div' => [], 'span' => [], + ]; + + public function execute(?string $html): ?string + { + if ($html === null) { + return null; + } + + $html = preg_replace('#<(script|style|iframe|object|embed|template|svg|math)\b[^>]*>.*?\1\s*>#is', '', $html) ?? ''; + $html = preg_replace('#<(script|style|iframe|object|embed|template|svg|math)\b[^>]*/?>#is', '', $html) ?? ''; + $allowedTags = '<'.implode('><', array_keys(self::ALLOWED)).'>'; + $html = strip_tags($html, $allowedTags); + if ($html === '' || ! class_exists(DOMDocument::class)) { + return trim($html); + } + + $document = new DOMDocument('1.0', 'UTF-8'); + $previous = libxml_use_internal_errors(true); + $document->loadHTML( + '
'.fake()->sentence().'
', + 'type' => 'manual', + 'status' => 'active', + ]; + } + + public function draft(): static + { + return $this->state(fn (array $attributes) => ['status' => 'draft']); + } + + public function archived(): static + { + return $this->state(fn (array $attributes) => ['status' => 'archived']); + } + + public function automated(): static + { + return $this->state(fn (array $attributes) => ['type' => 'automated']); + } +} diff --git a/database/factories/CustomerAddressFactory.php b/database/factories/CustomerAddressFactory.php new file mode 100644 index 00000000..d7491913 --- /dev/null +++ b/database/factories/CustomerAddressFactory.php @@ -0,0 +1,37 @@ + Customer::factory(), + 'label' => fake()->randomElement(['Home', 'Work', 'Other']), + 'address_json' => [ + 'first_name' => fake()->firstName(), + 'last_name' => fake()->lastName(), + 'company' => '', + 'address1' => fake()->streetAddress(), + 'address2' => '', + 'city' => fake()->city(), + 'province' => '', + 'province_code' => '', + 'country' => 'Germany', + 'country_code' => 'DE', + 'zip' => fake()->postcode(), + 'phone' => fake()->phoneNumber(), + ], + 'is_default' => false, + ]; + } + + public function default(): static + { + return $this->state(fn (array $attributes) => ['is_default' => true]); + } +} diff --git a/database/factories/CustomerFactory.php b/database/factories/CustomerFactory.php new file mode 100644 index 00000000..4cb2a185 --- /dev/null +++ b/database/factories/CustomerFactory.php @@ -0,0 +1,33 @@ + Store::factory(), + 'email' => fake()->unique()->safeEmail(), + 'password_hash' => static::$password ??= Hash::make('password'), + 'name' => fake()->name(), + 'marketing_opt_in' => fake()->boolean(30), + ]; + } + + public function guest(): static + { + return $this->state(fn (array $attributes) => ['password_hash' => null]); + } + + public function optedInMarketing(): static + { + return $this->state(fn (array $attributes) => ['marketing_opt_in' => true]); + } +} diff --git a/database/factories/DiscountFactory.php b/database/factories/DiscountFactory.php new file mode 100644 index 00000000..56f72e9e --- /dev/null +++ b/database/factories/DiscountFactory.php @@ -0,0 +1,63 @@ + Store::factory(), + 'type' => 'code', + 'code' => fake()->unique()->bothify('????##'), + 'value_type' => 'percent', + 'value_amount' => 10, + 'starts_at' => now()->subMonth(), + 'ends_at' => now()->addYear(), + 'usage_limit' => null, + 'usage_count' => 0, + 'rules_json' => [], + 'status' => 'active', + ]; + } + + public function fixed(int $amountCents): static + { + return $this->state(fn (array $attributes) => ['value_type' => 'fixed', 'value_amount' => $amountCents]); + } + + public function freeShipping(): static + { + return $this->state(fn (array $attributes) => ['value_type' => 'free_shipping', 'value_amount' => 0]); + } + + public function expired(): static + { + return $this->state(fn (array $attributes) => [ + 'starts_at' => now()->subYear(), 'ends_at' => now()->subDay(), 'status' => 'expired', + ]); + } + + public function maxedOut(): static + { + return $this->state(fn (array $attributes) => ['usage_limit' => 5, 'usage_count' => 5]); + } + + public function automatic(): static + { + return $this->state(fn (array $attributes) => ['type' => 'automatic', 'code' => null]); + } + + public function draft(): static + { + return $this->state(fn (array $attributes) => ['status' => 'draft']); + } + + public function disabled(): static + { + return $this->state(fn (array $attributes) => ['status' => 'disabled', 'starts_at' => now()->subDay()]); + } +} diff --git a/database/factories/FulfillmentFactory.php b/database/factories/FulfillmentFactory.php new file mode 100644 index 00000000..0ed59df2 --- /dev/null +++ b/database/factories/FulfillmentFactory.php @@ -0,0 +1,35 @@ +bothify('??########'); + + return [ + 'order_id' => Order::factory(), + 'status' => 'shipped', + 'tracking_company' => fake()->randomElement(['DHL', 'UPS', 'FedEx', 'DPD']), + 'tracking_number' => $trackingNumber, + 'tracking_url' => 'https://tracking.example.com/'.$trackingNumber, + 'shipped_at' => now(), + ]; + } + + public function pending(): static + { + return $this->state(fn (array $attributes) => [ + 'status' => 'pending', 'tracking_number' => null, 'tracking_url' => null, 'shipped_at' => null, + ]); + } + + public function delivered(): static + { + return $this->state(fn (array $attributes) => ['status' => 'delivered']); + } +} diff --git a/database/factories/FulfillmentLineFactory.php b/database/factories/FulfillmentLineFactory.php new file mode 100644 index 00000000..08263bea --- /dev/null +++ b/database/factories/FulfillmentLineFactory.php @@ -0,0 +1,19 @@ + Fulfillment::factory(), + 'order_line_id' => OrderLine::factory(), + 'quantity' => 1, + ]; + } +} diff --git a/database/factories/InventoryItemFactory.php b/database/factories/InventoryItemFactory.php new file mode 100644 index 00000000..98b13f25 --- /dev/null +++ b/database/factories/InventoryItemFactory.php @@ -0,0 +1,36 @@ + Store::factory(), + 'variant_id' => ProductVariant::factory(), + 'quantity_on_hand' => fake()->numberBetween(0, 100), + 'quantity_reserved' => 0, + 'policy' => 'deny', + ]; + } + + public function outOfStock(): static + { + return $this->state(fn (array $attributes) => ['quantity_on_hand' => 0]); + } + + public function continuePolicy(): static + { + return $this->state(fn (array $attributes) => ['policy' => 'continue']); + } + + public function lowStock(): static + { + return $this->state(fn (array $attributes) => ['quantity_on_hand' => fake()->numberBetween(1, 3)]); + } +} diff --git a/database/factories/NavigationItemFactory.php b/database/factories/NavigationItemFactory.php new file mode 100644 index 00000000..24d4f68b --- /dev/null +++ b/database/factories/NavigationItemFactory.php @@ -0,0 +1,36 @@ + NavigationMenu::factory(), + 'type' => 'link', + 'label' => fake()->words(2, true), + 'url' => '/', + 'resource_id' => null, + 'position' => 0, + ]; + } + + public function page(int $pageId): static + { + return $this->state(fn (array $attributes) => ['type' => 'page', 'url' => null, 'resource_id' => $pageId]); + } + + public function collection(int $collectionId): static + { + return $this->state(fn (array $attributes) => ['type' => 'collection', 'url' => null, 'resource_id' => $collectionId]); + } + + public function product(int $productId): static + { + return $this->state(fn (array $attributes) => ['type' => 'product', 'url' => null, 'resource_id' => $productId]); + } +} diff --git a/database/factories/NavigationMenuFactory.php b/database/factories/NavigationMenuFactory.php new file mode 100644 index 00000000..a4c8a060 --- /dev/null +++ b/database/factories/NavigationMenuFactory.php @@ -0,0 +1,18 @@ + Store::factory(), + 'handle' => fake()->unique()->slug(2), + 'title' => fake()->words(2, true), + ]; + } +} diff --git a/database/factories/OrderFactory.php b/database/factories/OrderFactory.php new file mode 100644 index 00000000..c0cf8fcb --- /dev/null +++ b/database/factories/OrderFactory.php @@ -0,0 +1,95 @@ + fake()->firstName(), + 'last_name' => fake()->lastName(), + 'company' => '', + 'address1' => fake()->streetAddress(), + 'address2' => '', + 'city' => fake()->city(), + 'province' => '', + 'province_code' => '', + 'country' => 'Germany', + 'country_code' => 'DE', + 'zip' => fake()->postcode(), + 'phone' => fake()->phoneNumber(), + ]; + + return [ + 'store_id' => Store::factory(), + 'customer_id' => Customer::factory(), + 'order_number' => '#'.fake()->unique()->numberBetween(1001, 999999), + 'payment_method' => 'credit_card', + 'status' => 'paid', + 'financial_status' => 'paid', + 'fulfillment_status' => 'unfulfilled', + 'currency' => 'EUR', + 'subtotal_amount' => 4998, + 'discount_amount' => 0, + 'shipping_amount' => 499, + 'tax_amount' => 798, + 'total_amount' => 5497, + 'email' => fake()->safeEmail(), + 'billing_address_json' => $address, + 'shipping_address_json' => $address, + 'placed_at' => now(), + ]; + } + + public function pending(): static + { + return $this->state(fn (array $attributes) => ['status' => 'pending', 'financial_status' => 'pending']); + } + + public function pendingBankTransfer(): static + { + return $this->state(fn (array $attributes) => [ + 'status' => 'pending', 'financial_status' => 'pending', 'payment_method' => 'bank_transfer', + ]); + } + + public function fulfilled(): static + { + return $this->state(fn (array $attributes) => ['status' => 'fulfilled', 'fulfillment_status' => 'fulfilled']); + } + + public function cancelled(): static + { + return $this->state(fn (array $attributes) => ['status' => 'cancelled', 'financial_status' => 'refunded']); + } + + public function refunded(): static + { + return $this->state(fn (array $attributes) => ['status' => 'refunded', 'financial_status' => 'refunded']); + } + + public function partiallyFulfilled(): static + { + return $this->state(fn (array $attributes) => ['fulfillment_status' => 'partial']); + } + + public function creditCard(): static + { + return $this->state(fn (array $attributes) => ['payment_method' => 'credit_card']); + } + + public function paypal(): static + { + return $this->state(fn (array $attributes) => ['payment_method' => 'paypal']); + } + + public function bankTransfer(): static + { + return $this->state(fn (array $attributes) => ['payment_method' => 'bank_transfer']); + } +} diff --git a/database/factories/OrderLineFactory.php b/database/factories/OrderLineFactory.php new file mode 100644 index 00000000..8ae3a737 --- /dev/null +++ b/database/factories/OrderLineFactory.php @@ -0,0 +1,27 @@ + Order::factory(), + 'product_id' => Product::factory(), + 'variant_id' => ProductVariant::factory(), + 'title_snapshot' => fake()->words(3, true), + 'sku_snapshot' => fake()->bothify('SKU-####-???'), + 'quantity' => fake()->numberBetween(1, 3), + 'unit_price_amount' => fake()->numberBetween(999, 19999), + 'total_amount' => fn (array $attributes): int => $attributes['unit_price_amount'] * $attributes['quantity'], + 'tax_lines_json' => [], + 'discount_allocations_json' => [], + ]; + } +} diff --git a/database/factories/OrganizationFactory.php b/database/factories/OrganizationFactory.php new file mode 100644 index 00000000..19afbfdb --- /dev/null +++ b/database/factories/OrganizationFactory.php @@ -0,0 +1,16 @@ + fake()->company(), + 'billing_email' => fake()->unique()->companyEmail(), + ]; + } +} diff --git a/database/factories/PageFactory.php b/database/factories/PageFactory.php new file mode 100644 index 00000000..b96a9e07 --- /dev/null +++ b/database/factories/PageFactory.php @@ -0,0 +1,34 @@ +unique()->words(3, true); + + return [ + 'store_id' => Store::factory(), + 'title' => Str::title($title), + 'handle' => Str::slug($title), + 'body_html' => ''.fake()->paragraphs(3, true).'
', + 'status' => 'published', + 'published_at' => now(), + ]; + } + + public function draft(): static + { + return $this->state(fn (array $attributes) => ['status' => 'draft', 'published_at' => null]); + } + + public function archived(): static + { + return $this->state(fn (array $attributes) => ['status' => 'archived']); + } +} diff --git a/database/factories/PaymentFactory.php b/database/factories/PaymentFactory.php new file mode 100644 index 00000000..2ce36439 --- /dev/null +++ b/database/factories/PaymentFactory.php @@ -0,0 +1,53 @@ + Order::factory(), + 'provider' => 'mock', + 'method' => 'credit_card', + 'provider_payment_id' => 'mock_'.fake()->regexify('[A-Za-z0-9]{20}'), + 'status' => 'captured', + 'amount' => fake()->numberBetween(999, 99999), + 'currency' => 'EUR', + 'raw_json_encrypted' => null, + ]; + } + + public function pending(): static + { + return $this->state(fn (array $attributes) => ['status' => 'pending']); + } + + public function failed(): static + { + return $this->state(fn (array $attributes) => ['status' => 'failed']); + } + + public function refunded(): static + { + return $this->state(fn (array $attributes) => ['status' => 'refunded']); + } + + public function creditCard(): static + { + return $this->state(fn (array $attributes) => ['method' => 'credit_card']); + } + + public function paypal(): static + { + return $this->state(fn (array $attributes) => ['method' => 'paypal']); + } + + public function bankTransfer(): static + { + return $this->state(fn (array $attributes) => ['method' => 'bank_transfer']); + } +} diff --git a/database/factories/ProductFactory.php b/database/factories/ProductFactory.php new file mode 100644 index 00000000..9245df40 --- /dev/null +++ b/database/factories/ProductFactory.php @@ -0,0 +1,53 @@ +unique()->words(3, true); + + return [ + 'store_id' => Store::factory(), + 'title' => Str::title($title), + 'handle' => Str::slug($title), + 'status' => 'active', + 'description_html' => ''.fake()->paragraph().'
'.fake()->paragraph().'
', + 'vendor' => fake()->company(), + 'product_type' => fake()->randomElement(['Shirts', 'Pants', 'Shoes', 'Accessories', 'Electronics', 'Books']), + 'tags' => fake()->randomElements(['new', 'sale', 'trending', 'popular', 'limited'], fake()->numberBetween(1, 3)), + 'published_at' => now(), + ]; + } + + public function draft(): static + { + return $this->state(fn (array $attributes) => ['status' => 'draft', 'published_at' => null]); + } + + public function archived(): static + { + return $this->state(fn (array $attributes) => ['status' => 'archived']); + } + + public function withVariants(int $count = 1): static + { + return $this->afterCreating(function (Product $product) use ($count): void { + ProductVariant::factory()->count($count)->for($product)->create(); + }); + } + + public function withDefaultVariant(int $price = 1000): static + { + return $this->afterCreating(function (Product $product) use ($price): void { + ProductVariant::factory()->default()->for($product)->create(['price_amount' => $price]); + }); + } +} diff --git a/database/factories/ProductVariantFactory.php b/database/factories/ProductVariantFactory.php new file mode 100644 index 00000000..8f98df7a --- /dev/null +++ b/database/factories/ProductVariantFactory.php @@ -0,0 +1,49 @@ + Product::factory(), + 'sku' => fake()->unique()->bothify('SKU-####-???'), + 'barcode' => fake()->ean13(), + 'price_amount' => fake()->numberBetween(999, 19999), + 'compare_at_amount' => null, + 'currency' => 'EUR', + 'weight_g' => fake()->numberBetween(100, 5000), + 'requires_shipping' => true, + 'is_default' => false, + 'position' => 0, + 'status' => 'active', + ]; + } + + public function onSale(): static + { + return $this->state(fn (array $attributes) => [ + 'price_amount' => fake()->numberBetween(9999, 19999), + 'compare_at_amount' => fake()->numberBetween(20000, 39999), + ]); + } + + public function digital(): static + { + return $this->state(fn (array $attributes) => ['requires_shipping' => false, 'weight_g' => 0]); + } + + public function default(): static + { + return $this->state(fn (array $attributes) => ['is_default' => true]); + } + + public function archived(): static + { + return $this->state(fn (array $attributes) => ['status' => 'archived']); + } +} diff --git a/database/factories/RefundFactory.php b/database/factories/RefundFactory.php new file mode 100644 index 00000000..bb9213bb --- /dev/null +++ b/database/factories/RefundFactory.php @@ -0,0 +1,32 @@ + Order::factory(), + 'payment_id' => Payment::factory(), + 'amount' => fake()->numberBetween(999, 19999), + 'reason' => fake()->sentence(), + 'status' => 'processed', + 'provider_refund_id' => 'mock_re_'.fake()->regexify('[A-Za-z0-9]{20}'), + ]; + } + + public function pending(): static + { + return $this->state(fn (array $attributes) => ['status' => 'pending', 'provider_refund_id' => null]); + } + + public function failed(): static + { + return $this->state(fn (array $attributes) => ['status' => 'failed']); + } +} diff --git a/database/factories/ShippingRateFactory.php b/database/factories/ShippingRateFactory.php new file mode 100644 index 00000000..4aebe032 --- /dev/null +++ b/database/factories/ShippingRateFactory.php @@ -0,0 +1,37 @@ + ShippingZone::factory(), + 'name' => fake()->randomElement(['Standard', 'Express', 'Economy']), + 'type' => 'flat', + 'config_json' => ['amount' => 499], + 'is_active' => true, + ]; + } + + public function inactive(): static + { + return $this->state(fn (array $attributes) => ['is_active' => false]); + } + + public function weightBased(): static + { + return $this->state(fn (array $attributes) => [ + 'type' => 'weight', + 'config_json' => ['ranges' => [ + ['min_g' => 0, 'max_g' => 500, 'amount' => 399], + ['min_g' => 501, 'max_g' => 2000, 'amount' => 699], + ['min_g' => 2001, 'amount' => 1299], + ]], + ]); + } +} diff --git a/database/factories/ShippingZoneFactory.php b/database/factories/ShippingZoneFactory.php new file mode 100644 index 00000000..9d95d609 --- /dev/null +++ b/database/factories/ShippingZoneFactory.php @@ -0,0 +1,19 @@ + Store::factory(), + 'name' => fake()->country(), + 'countries_json' => ['DE'], + 'regions_json' => [], + ]; + } +} diff --git a/database/factories/StoreDomainFactory.php b/database/factories/StoreDomainFactory.php new file mode 100644 index 00000000..3911354f --- /dev/null +++ b/database/factories/StoreDomainFactory.php @@ -0,0 +1,35 @@ + Store::factory(), + 'hostname' => fake()->unique()->domainName(), + 'type' => 'storefront', + 'is_primary' => true, + 'tls_mode' => 'managed', + ]; + } + + public function admin(): static + { + return $this->state(fn (array $attributes) => ['type' => 'admin']); + } + + public function api(): static + { + return $this->state(fn (array $attributes) => ['type' => 'api']); + } + + public function secondary(): static + { + return $this->state(fn (array $attributes) => ['is_primary' => false]); + } +} diff --git a/database/factories/StoreFactory.php b/database/factories/StoreFactory.php new file mode 100644 index 00000000..f6c3fa88 --- /dev/null +++ b/database/factories/StoreFactory.php @@ -0,0 +1,27 @@ + Organization::factory(), + 'name' => fake()->company().' Store', + 'handle' => fake()->unique()->slug(2), + 'status' => 'active', + 'default_currency' => 'EUR', + 'default_locale' => 'en', + 'timezone' => 'Europe/Berlin', + ]; + } + + public function suspended(): static + { + return $this->state(fn (array $attributes) => ['status' => 'suspended']); + } +} diff --git a/database/factories/ThemeFactory.php b/database/factories/ThemeFactory.php new file mode 100644 index 00000000..1ccad6d6 --- /dev/null +++ b/database/factories/ThemeFactory.php @@ -0,0 +1,25 @@ + Store::factory(), + 'name' => 'Default Theme', + 'version' => '1.0.0', + 'status' => 'published', + 'published_at' => now(), + ]; + } + + public function draft(): static + { + return $this->state(fn (array $attributes) => ['status' => 'draft', 'published_at' => null]); + } +} diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 80da5ac7..6add344f 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -27,7 +27,9 @@ public function definition(): array 'name' => fake()->name(), 'email' => fake()->unique()->safeEmail(), 'email_verified_at' => now(), - 'password' => static::$password ??= Hash::make('password'), + 'password_hash' => static::$password ??= Hash::make('password'), + 'status' => 'active', + 'last_login_at' => fake()->dateTimeBetween('-30 days'), 'remember_token' => Str::random(10), 'two_factor_secret' => null, 'two_factor_recovery_codes' => null, @@ -56,4 +58,11 @@ public function withTwoFactor(): static 'two_factor_confirmed_at' => now(), ]); } + + public function disabled(): static + { + return $this->state(fn (array $attributes) => [ + 'status' => 'disabled', + ]); + } } diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index 05fb5d9e..7b45db4a 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -13,12 +13,17 @@ public function up(): void { Schema::create('users', function (Blueprint $table) { $table->id(); + $table->string('email'); + $table->string('password_hash'); $table->string('name'); - $table->string('email')->unique(); + $table->enum('status', ['active', 'disabled'])->default('active'); $table->timestamp('email_verified_at')->nullable(); - $table->string('password'); + $table->timestamp('last_login_at')->nullable(); $table->rememberToken(); $table->timestamps(); + + $table->unique('email', 'idx_users_email'); + $table->index('status', 'idx_users_status'); }); Schema::create('password_reset_tokens', function (Blueprint $table) { diff --git a/database/migrations/2025_08_14_170933_add_two_factor_columns_to_users_table.php b/database/migrations/2025_08_14_170933_add_two_factor_columns_to_users_table.php index 187d974d..a008f488 100644 --- a/database/migrations/2025_08_14_170933_add_two_factor_columns_to_users_table.php +++ b/database/migrations/2025_08_14_170933_add_two_factor_columns_to_users_table.php @@ -12,7 +12,7 @@ public function up(): void { Schema::table('users', function (Blueprint $table) { - $table->text('two_factor_secret')->after('password')->nullable(); + $table->text('two_factor_secret')->after('password_hash')->nullable(); $table->text('two_factor_recovery_codes')->after('two_factor_secret')->nullable(); $table->timestamp('two_factor_confirmed_at')->after('two_factor_recovery_codes')->nullable(); }); diff --git a/database/migrations/2026_01_01_000100_create_foundation_tables.php b/database/migrations/2026_01_01_000100_create_foundation_tables.php new file mode 100644 index 00000000..bfd97271 --- /dev/null +++ b/database/migrations/2026_01_01_000100_create_foundation_tables.php @@ -0,0 +1,103 @@ +id(); + $table->string('name'); + $table->string('billing_email'); + $table->timestamps(); + $table->index('billing_email', 'idx_organizations_billing_email'); + }); + + Schema::create('apps', function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->enum('status', ['active', 'disabled'])->default('active'); + $table->timestamp('created_at')->nullable(); + $table->index('status', 'idx_apps_status'); + }); + + Schema::create('stores', function (Blueprint $table) { + $table->id(); + $table->foreignId('organization_id')->constrained()->cascadeOnDelete(); + $table->string('name'); + $table->string('handle'); + $table->enum('status', ['active', 'suspended'])->default('active'); + $table->string('default_currency')->default('USD'); + $table->string('default_locale')->default('en'); + $table->string('timezone')->default('UTC'); + $table->timestamps(); + $table->unique('handle', 'idx_stores_handle'); + $table->index('organization_id', 'idx_stores_organization_id'); + $table->index('status', 'idx_stores_status'); + }); + + Schema::create('store_domains', function (Blueprint $table) { + $table->id(); + $table->foreignId('store_id')->constrained()->cascadeOnDelete(); + $table->string('hostname'); + $table->enum('type', ['storefront', 'admin', 'api'])->default('storefront'); + $table->boolean('is_primary')->default(false); + $table->enum('tls_mode', ['managed', 'bring_your_own'])->default('managed'); + $table->timestamp('created_at')->nullable(); + $table->unique('hostname', 'idx_store_domains_hostname'); + $table->index('store_id', 'idx_store_domains_store_id'); + $table->index(['store_id', 'is_primary'], 'idx_store_domains_store_primary'); + }); + + Schema::create('store_users', function (Blueprint $table) { + $table->foreignId('store_id')->constrained()->cascadeOnDelete(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->enum('role', ['owner', 'admin', 'staff', 'support'])->default('staff'); + $table->timestamp('created_at')->nullable(); + $table->primary(['store_id', 'user_id']); + $table->index('user_id', 'idx_store_users_user_id'); + $table->index(['store_id', 'role'], 'idx_store_users_role'); + }); + + Schema::create('store_settings', function (Blueprint $table) { + $table->unsignedBigInteger('store_id')->primary(); + $table->text('settings_json')->default('{}'); + $table->timestamp('updated_at')->nullable(); + $table->foreign('store_id')->references('id')->on('stores')->cascadeOnDelete(); + }); + + Schema::create('customer_password_reset_tokens', function (Blueprint $table) { + $table->foreignId('store_id')->constrained()->cascadeOnDelete(); + $table->string('email'); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + $table->primary(['store_id', 'email']); + }); + + Schema::create('personal_access_tokens', function (Blueprint $table) { + $table->id(); + $table->morphs('tokenable'); + $table->string('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable()->index(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + Schema::dropIfExists('customer_password_reset_tokens'); + Schema::dropIfExists('store_settings'); + Schema::dropIfExists('store_users'); + Schema::dropIfExists('store_domains'); + Schema::dropIfExists('stores'); + Schema::dropIfExists('apps'); + Schema::dropIfExists('organizations'); + } +}; diff --git a/database/migrations/2026_01_01_000200_create_tenant_root_tables.php b/database/migrations/2026_01_01_000200_create_tenant_root_tables.php new file mode 100644 index 00000000..08c8b230 --- /dev/null +++ b/database/migrations/2026_01_01_000200_create_tenant_root_tables.php @@ -0,0 +1,177 @@ +id(); + $table->foreignId('store_id')->constrained()->cascadeOnDelete(); + $table->string('email'); + $table->string('password_hash')->nullable(); + $table->string('name')->nullable(); + $table->boolean('marketing_opt_in')->default(false); + $table->timestamps(); + $table->unique(['store_id', 'email'], 'idx_customers_store_email'); + $table->index('store_id', 'idx_customers_store_id'); + }); + + Schema::create('themes', function (Blueprint $table) { + $table->id(); + $table->foreignId('store_id')->constrained()->cascadeOnDelete(); + $table->string('name'); + $table->string('version')->nullable(); + $table->enum('status', ['draft', 'published'])->default('draft'); + $table->timestamp('published_at')->nullable(); + $table->timestamps(); + $table->index('store_id', 'idx_themes_store_id'); + $table->index(['store_id', 'status'], 'idx_themes_store_status'); + }); + + Schema::create('pages', function (Blueprint $table) { + $table->id(); + $table->foreignId('store_id')->constrained()->cascadeOnDelete(); + $table->string('title'); + $table->string('handle'); + $table->text('body_html')->nullable(); + $table->enum('status', ['draft', 'published', 'archived'])->default('draft'); + $table->timestamp('published_at')->nullable(); + $table->timestamps(); + $table->unique(['store_id', 'handle'], 'idx_pages_store_handle'); + $table->index('store_id', 'idx_pages_store_id'); + $table->index(['store_id', 'status'], 'idx_pages_store_status'); + }); + + Schema::create('navigation_menus', function (Blueprint $table) { + $table->id(); + $table->foreignId('store_id')->constrained()->cascadeOnDelete(); + $table->string('handle'); + $table->string('title'); + $table->timestamps(); + $table->unique(['store_id', 'handle'], 'idx_navigation_menus_store_handle'); + $table->index('store_id', 'idx_navigation_menus_store_id'); + }); + + Schema::create('search_settings', function (Blueprint $table) { + $table->unsignedBigInteger('store_id')->primary(); + $table->text('synonyms_json')->default('[]'); + $table->text('stop_words_json')->default('[]'); + $table->timestamp('updated_at')->nullable(); + $table->foreign('store_id')->references('id')->on('stores')->cascadeOnDelete(); + }); + + Schema::create('shipping_zones', function (Blueprint $table) { + $table->id(); + $table->foreignId('store_id')->constrained()->cascadeOnDelete(); + $table->string('name'); + $table->text('countries_json')->default('[]'); + $table->text('regions_json')->default('[]'); + $table->index('store_id', 'idx_shipping_zones_store_id'); + }); + + Schema::create('tax_settings', function (Blueprint $table) { + $table->unsignedBigInteger('store_id')->primary(); + $table->enum('mode', ['manual', 'provider'])->default('manual'); + $table->enum('provider', ['stripe_tax', 'none'])->default('none'); + $table->boolean('prices_include_tax')->default(false); + $table->text('config_json')->default('{}'); + $table->foreign('store_id')->references('id')->on('stores')->cascadeOnDelete(); + }); + + Schema::create('discounts', function (Blueprint $table) { + $table->id(); + $table->foreignId('store_id')->constrained()->cascadeOnDelete(); + $table->enum('type', ['code', 'automatic'])->default('code'); + $table->string('code')->nullable(); + $table->enum('value_type', ['fixed', 'percent', 'free_shipping']); + $table->integer('value_amount')->default(0); + $table->timestamp('starts_at'); + $table->timestamp('ends_at')->nullable(); + $table->integer('usage_limit')->nullable(); + $table->integer('usage_count')->default(0); + $table->text('rules_json')->default('{}'); + $table->enum('status', ['draft', 'active', 'expired', 'disabled'])->default('active'); + $table->timestamps(); + $table->unique(['store_id', 'code'], 'idx_discounts_store_code'); + $table->index('store_id', 'idx_discounts_store_id'); + $table->index(['store_id', 'status'], 'idx_discounts_store_status'); + $table->index(['store_id', 'type'], 'idx_discounts_store_type'); + }); + + Schema::create('products', function (Blueprint $table) { + $table->id(); + $table->foreignId('store_id')->constrained()->cascadeOnDelete(); + $table->string('title'); + $table->string('handle'); + $table->enum('status', ['draft', 'active', 'archived'])->default('draft'); + $table->text('description_html')->nullable(); + $table->string('vendor')->nullable(); + $table->string('product_type')->nullable(); + $table->text('tags')->default('[]'); + $table->timestamp('published_at')->nullable(); + $table->timestamps(); + $table->unique(['store_id', 'handle'], 'idx_products_store_handle'); + $table->index('store_id', 'idx_products_store_id'); + $table->index(['store_id', 'status'], 'idx_products_store_status'); + $table->index(['store_id', 'published_at'], 'idx_products_published_at'); + $table->index(['store_id', 'vendor'], 'idx_products_vendor'); + $table->index(['store_id', 'product_type'], 'idx_products_product_type'); + }); + + Schema::create('collections', function (Blueprint $table) { + $table->id(); + $table->foreignId('store_id')->constrained()->cascadeOnDelete(); + $table->string('title'); + $table->string('handle'); + $table->text('description_html')->nullable(); + $table->enum('type', ['manual', 'automated'])->default('manual'); + $table->enum('status', ['draft', 'active', 'archived'])->default('active'); + $table->timestamps(); + $table->unique(['store_id', 'handle'], 'idx_collections_store_handle'); + $table->index('store_id', 'idx_collections_store_id'); + $table->index(['store_id', 'status'], 'idx_collections_store_status'); + }); + + Schema::create('app_installations', function (Blueprint $table) { + $table->id(); + $table->foreignId('store_id')->constrained()->cascadeOnDelete(); + $table->foreignId('app_id')->constrained()->cascadeOnDelete(); + $table->text('scopes_json')->default('[]'); + $table->enum('status', ['active', 'suspended', 'uninstalled'])->default('active'); + $table->timestamp('installed_at')->nullable(); + $table->unique(['store_id', 'app_id'], 'idx_app_installations_store_app'); + $table->index('store_id', 'idx_app_installations_store_id'); + $table->index('app_id', 'idx_app_installations_app_id'); + }); + + Schema::create('oauth_clients', function (Blueprint $table) { + $table->id(); + $table->foreignId('app_id')->constrained()->cascadeOnDelete(); + $table->string('client_id'); + $table->text('client_secret_encrypted'); + $table->text('redirect_uris_json')->default('[]'); + $table->unique('client_id', 'idx_oauth_clients_client_id'); + $table->index('app_id', 'idx_oauth_clients_app_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('oauth_clients'); + Schema::dropIfExists('app_installations'); + Schema::dropIfExists('collections'); + Schema::dropIfExists('products'); + Schema::dropIfExists('discounts'); + Schema::dropIfExists('tax_settings'); + Schema::dropIfExists('shipping_zones'); + Schema::dropIfExists('search_settings'); + Schema::dropIfExists('navigation_menus'); + Schema::dropIfExists('pages'); + Schema::dropIfExists('themes'); + Schema::dropIfExists('customers'); + } +}; diff --git a/database/migrations/2026_01_01_000300_create_dependent_tables.php b/database/migrations/2026_01_01_000300_create_dependent_tables.php new file mode 100644 index 00000000..7738b5ac --- /dev/null +++ b/database/migrations/2026_01_01_000300_create_dependent_tables.php @@ -0,0 +1,185 @@ +id(); + $table->foreignId('product_id')->constrained()->cascadeOnDelete(); + $table->string('name'); + $table->integer('position')->default(0); + $table->index('product_id', 'idx_product_options_product_id'); + $table->unique(['product_id', 'position'], 'idx_product_options_product_position'); + }); + + Schema::create('product_variants', function (Blueprint $table) { + $table->id(); + $table->foreignId('product_id')->constrained()->cascadeOnDelete(); + $table->string('sku')->nullable(); + $table->string('barcode')->nullable(); + $table->integer('price_amount')->default(0); + $table->integer('compare_at_amount')->nullable(); + $table->string('currency')->default('USD'); + $table->integer('weight_g')->nullable(); + $table->boolean('requires_shipping')->default(true); + $table->boolean('is_default')->default(false); + $table->integer('position')->default(0); + $table->enum('status', ['active', 'archived'])->default('active'); + $table->timestamps(); + $table->index('product_id', 'idx_product_variants_product_id'); + $table->index('sku', 'idx_product_variants_sku'); + $table->index('barcode', 'idx_product_variants_barcode'); + $table->index(['product_id', 'position'], 'idx_product_variants_product_position'); + $table->index(['product_id', 'is_default'], 'idx_product_variants_product_default'); + }); + + Schema::create('product_media', function (Blueprint $table) { + $table->id(); + $table->foreignId('product_id')->constrained()->cascadeOnDelete(); + $table->enum('type', ['image', 'video'])->default('image'); + $table->string('storage_key'); + $table->string('alt_text')->nullable(); + $table->integer('width')->nullable(); + $table->integer('height')->nullable(); + $table->string('mime_type')->nullable(); + $table->integer('byte_size')->nullable(); + $table->integer('position')->default(0); + $table->enum('status', ['processing', 'ready', 'failed'])->default('processing'); + $table->timestamp('created_at')->nullable(); + $table->index('product_id', 'idx_product_media_product_id'); + $table->index(['product_id', 'position'], 'idx_product_media_product_position'); + $table->index('status', 'idx_product_media_status'); + }); + + Schema::create('collection_products', function (Blueprint $table) { + $table->foreignId('collection_id')->constrained()->cascadeOnDelete(); + $table->foreignId('product_id')->constrained()->cascadeOnDelete(); + $table->integer('position')->default(0); + $table->primary(['collection_id', 'product_id']); + $table->index('product_id', 'idx_collection_products_product_id'); + $table->index(['collection_id', 'position'], 'idx_collection_products_position'); + }); + + Schema::create('customer_addresses', function (Blueprint $table) { + $table->id(); + $table->foreignId('customer_id')->constrained()->cascadeOnDelete(); + $table->string('label')->nullable(); + $table->text('address_json')->default('{}'); + $table->boolean('is_default')->default(false); + $table->index('customer_id', 'idx_customer_addresses_customer_id'); + $table->index(['customer_id', 'is_default'], 'idx_customer_addresses_default'); + }); + + Schema::create('carts', function (Blueprint $table) { + $table->id(); + $table->foreignId('store_id')->constrained()->cascadeOnDelete(); + $table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete(); + $table->string('currency')->default('USD'); + $table->integer('cart_version')->default(1); + $table->enum('status', ['active', 'converted', 'abandoned'])->default('active'); + $table->timestamps(); + $table->index('store_id', 'idx_carts_store_id'); + $table->index('customer_id', 'idx_carts_customer_id'); + $table->index(['store_id', 'status'], 'idx_carts_store_status'); + }); + + Schema::create('navigation_items', function (Blueprint $table) { + $table->id(); + $table->foreignId('menu_id')->constrained('navigation_menus')->cascadeOnDelete(); + $table->enum('type', ['link', 'page', 'collection', 'product'])->default('link'); + $table->string('label'); + $table->string('url')->nullable(); + $table->unsignedBigInteger('resource_id')->nullable(); + $table->integer('position')->default(0); + $table->index('menu_id', 'idx_navigation_items_menu_id'); + $table->index(['menu_id', 'position'], 'idx_navigation_items_menu_position'); + }); + + Schema::create('theme_files', function (Blueprint $table) { + $table->id(); + $table->foreignId('theme_id')->constrained()->cascadeOnDelete(); + $table->string('path'); + $table->string('storage_key'); + $table->string('sha256'); + $table->integer('byte_size')->default(0); + $table->unique(['theme_id', 'path'], 'idx_theme_files_theme_path'); + $table->index('theme_id', 'idx_theme_files_theme_id'); + }); + + Schema::create('theme_settings', function (Blueprint $table) { + $table->unsignedBigInteger('theme_id')->primary(); + $table->text('settings_json')->default('{}'); + $table->timestamp('updated_at')->nullable(); + $table->foreign('theme_id')->references('id')->on('themes')->cascadeOnDelete(); + }); + + Schema::create('search_queries', function (Blueprint $table) { + $table->id(); + $table->foreignId('store_id')->constrained()->cascadeOnDelete(); + $table->text('query'); + $table->text('filters_json')->nullable(); + $table->integer('results_count')->default(0); + $table->timestamp('created_at')->nullable(); + $table->index('store_id', 'idx_search_queries_store_id'); + $table->index(['store_id', 'created_at'], 'idx_search_queries_store_created'); + $table->index(['store_id', 'query'], 'idx_search_queries_store_query'); + }); + + Schema::create('shipping_rates', function (Blueprint $table) { + $table->id(); + $table->foreignId('zone_id')->constrained('shipping_zones')->cascadeOnDelete(); + $table->string('name'); + $table->enum('type', ['flat', 'weight', 'price', 'carrier'])->default('flat'); + $table->text('config_json')->default('{}'); + $table->boolean('is_active')->default(true); + $table->index('zone_id', 'idx_shipping_rates_zone_id'); + $table->index(['zone_id', 'is_active'], 'idx_shipping_rates_zone_active'); + }); + + Schema::create('oauth_tokens', function (Blueprint $table) { + $table->id(); + $table->foreignId('installation_id')->constrained('app_installations')->cascadeOnDelete(); + $table->string('access_token_hash'); + $table->string('refresh_token_hash')->nullable(); + $table->timestamp('expires_at'); + $table->index('installation_id', 'idx_oauth_tokens_installation_id'); + $table->unique('access_token_hash', 'idx_oauth_tokens_access_hash'); + $table->index('expires_at', 'idx_oauth_tokens_expires_at'); + }); + + Schema::create('webhook_subscriptions', function (Blueprint $table) { + $table->id(); + $table->foreignId('store_id')->constrained()->cascadeOnDelete(); + $table->foreignId('app_installation_id')->nullable()->constrained()->cascadeOnDelete(); + $table->string('event_type'); + $table->string('target_url'); + $table->text('signing_secret_encrypted'); + $table->enum('status', ['active', 'paused', 'disabled'])->default('active'); + $table->index('store_id', 'idx_webhook_subscriptions_store_id'); + $table->index(['store_id', 'event_type'], 'idx_webhook_subscriptions_store_event'); + $table->index('app_installation_id', 'idx_webhook_subscriptions_installation'); + }); + } + + public function down(): void + { + Schema::dropIfExists('webhook_subscriptions'); + Schema::dropIfExists('oauth_tokens'); + Schema::dropIfExists('shipping_rates'); + Schema::dropIfExists('search_queries'); + Schema::dropIfExists('theme_settings'); + Schema::dropIfExists('theme_files'); + Schema::dropIfExists('navigation_items'); + Schema::dropIfExists('carts'); + Schema::dropIfExists('customer_addresses'); + Schema::dropIfExists('collection_products'); + Schema::dropIfExists('product_media'); + Schema::dropIfExists('product_variants'); + Schema::dropIfExists('product_options'); + } +}; diff --git a/database/migrations/2026_01_01_000400_create_transaction_tables.php b/database/migrations/2026_01_01_000400_create_transaction_tables.php new file mode 100644 index 00000000..ab90d79b --- /dev/null +++ b/database/migrations/2026_01_01_000400_create_transaction_tables.php @@ -0,0 +1,155 @@ +id(); + $table->foreignId('product_option_id')->constrained()->cascadeOnDelete(); + $table->string('value'); + $table->integer('position')->default(0); + $table->index('product_option_id', 'idx_product_option_values_option_id'); + $table->unique(['product_option_id', 'position'], 'idx_product_option_values_option_position'); + }); + + Schema::create('inventory_items', function (Blueprint $table) { + $table->id(); + $table->foreignId('store_id')->constrained()->cascadeOnDelete(); + $table->foreignId('variant_id')->constrained('product_variants')->cascadeOnDelete(); + $table->integer('quantity_on_hand')->default(0); + $table->integer('quantity_reserved')->default(0); + $table->enum('policy', ['deny', 'continue'])->default('deny'); + $table->unique('variant_id', 'idx_inventory_items_variant_id'); + $table->index('store_id', 'idx_inventory_items_store_id'); + }); + + Schema::create('cart_lines', function (Blueprint $table) { + $table->id(); + $table->foreignId('cart_id')->constrained()->cascadeOnDelete(); + $table->foreignId('variant_id')->constrained('product_variants')->cascadeOnDelete(); + $table->integer('quantity')->default(1); + $table->integer('unit_price_amount')->default(0); + $table->integer('line_subtotal_amount')->default(0); + $table->integer('line_discount_amount')->default(0); + $table->integer('line_total_amount')->default(0); + $table->index('cart_id', 'idx_cart_lines_cart_id'); + $table->unique(['cart_id', 'variant_id'], 'idx_cart_lines_cart_variant'); + }); + + Schema::create('checkouts', function (Blueprint $table) { + $table->id(); + $table->foreignId('store_id')->constrained()->cascadeOnDelete(); + $table->foreignId('cart_id')->constrained()->cascadeOnDelete(); + $table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete(); + $table->enum('status', ['started', 'addressed', 'shipping_selected', 'payment_selected', 'completed', 'expired'])->default('started'); + $table->enum('payment_method', ['credit_card', 'paypal', 'bank_transfer'])->nullable(); + $table->string('email')->nullable(); + $table->text('shipping_address_json')->nullable(); + $table->text('billing_address_json')->nullable(); + $table->unsignedBigInteger('shipping_method_id')->nullable(); + $table->string('discount_code')->nullable(); + $table->text('tax_provider_snapshot_json')->nullable(); + $table->text('totals_json')->nullable(); + $table->timestamp('expires_at')->nullable(); + $table->timestamps(); + $table->index('store_id', 'idx_checkouts_store_id'); + $table->index('cart_id', 'idx_checkouts_cart_id'); + $table->index('customer_id', 'idx_checkouts_customer_id'); + $table->index(['store_id', 'status'], 'idx_checkouts_status'); + $table->index('expires_at', 'idx_checkouts_expires_at'); + }); + + Schema::create('orders', function (Blueprint $table) { + $table->id(); + $table->foreignId('store_id')->constrained()->cascadeOnDelete(); + $table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete(); + $table->string('order_number'); + $table->enum('payment_method', ['credit_card', 'paypal', 'bank_transfer']); + $table->enum('status', ['pending', 'paid', 'fulfilled', 'cancelled', 'refunded'])->default('pending'); + $table->enum('financial_status', ['pending', 'authorized', 'paid', 'partially_refunded', 'refunded', 'voided'])->default('pending'); + $table->enum('fulfillment_status', ['unfulfilled', 'partial', 'fulfilled'])->default('unfulfilled'); + $table->string('currency')->default('USD'); + $table->integer('subtotal_amount')->default(0); + $table->integer('discount_amount')->default(0); + $table->integer('shipping_amount')->default(0); + $table->integer('tax_amount')->default(0); + $table->integer('total_amount')->default(0); + $table->string('email')->nullable(); + $table->text('billing_address_json')->nullable(); + $table->text('shipping_address_json')->nullable(); + $table->timestamp('placed_at')->nullable(); + $table->timestamps(); + $table->unique(['store_id', 'order_number'], 'idx_orders_store_order_number'); + $table->index('store_id', 'idx_orders_store_id'); + $table->index('customer_id', 'idx_orders_customer_id'); + $table->index(['store_id', 'status'], 'idx_orders_store_status'); + $table->index(['store_id', 'financial_status'], 'idx_orders_store_financial'); + $table->index(['store_id', 'fulfillment_status'], 'idx_orders_store_fulfillment'); + $table->index(['store_id', 'placed_at'], 'idx_orders_placed_at'); + }); + + Schema::create('analytics_events', function (Blueprint $table) { + $table->id(); + $table->foreignId('store_id')->constrained()->cascadeOnDelete(); + $table->string('type'); + $table->string('session_id')->nullable(); + $table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete(); + $table->text('properties_json')->default('{}'); + $table->string('client_event_id')->nullable(); + $table->timestamp('occurred_at')->nullable(); + $table->timestamp('created_at')->nullable(); + $table->index('store_id', 'idx_analytics_events_store_id'); + $table->index(['store_id', 'type'], 'idx_analytics_events_store_type'); + $table->index(['store_id', 'created_at'], 'idx_analytics_events_store_created'); + $table->index('session_id', 'idx_analytics_events_session'); + $table->index('customer_id', 'idx_analytics_events_customer'); + $table->unique(['store_id', 'client_event_id'], 'idx_analytics_events_client_event'); + }); + + Schema::create('analytics_daily', function (Blueprint $table) { + $table->foreignId('store_id')->constrained()->cascadeOnDelete(); + $table->date('date'); + $table->integer('orders_count')->default(0); + $table->integer('revenue_amount')->default(0); + $table->integer('aov_amount')->default(0); + $table->integer('visits_count')->default(0); + $table->integer('add_to_cart_count')->default(0); + $table->integer('checkout_started_count')->default(0); + $table->integer('checkout_completed_count')->default(0); + $table->primary(['store_id', 'date']); + $table->index(['store_id', 'date'], 'idx_analytics_daily_store_date'); + }); + + Schema::create('webhook_deliveries', function (Blueprint $table) { + $table->id(); + $table->foreignId('subscription_id')->constrained('webhook_subscriptions')->cascadeOnDelete(); + $table->string('event_id'); + $table->integer('attempt_count')->default(1); + $table->enum('status', ['pending', 'success', 'failed'])->default('pending'); + $table->timestamp('last_attempt_at')->nullable(); + $table->integer('response_code')->nullable(); + $table->text('response_body_snippet')->nullable(); + $table->index('subscription_id', 'idx_webhook_deliveries_subscription_id'); + $table->index('event_id', 'idx_webhook_deliveries_event_id'); + $table->index('status', 'idx_webhook_deliveries_status'); + $table->index('last_attempt_at', 'idx_webhook_deliveries_last_attempt'); + }); + } + + public function down(): void + { + Schema::dropIfExists('webhook_deliveries'); + Schema::dropIfExists('analytics_daily'); + Schema::dropIfExists('analytics_events'); + Schema::dropIfExists('orders'); + Schema::dropIfExists('checkouts'); + Schema::dropIfExists('cart_lines'); + Schema::dropIfExists('inventory_items'); + Schema::dropIfExists('product_option_values'); + } +}; diff --git a/database/migrations/2026_01_01_000500_create_order_detail_tables.php b/database/migrations/2026_01_01_000500_create_order_detail_tables.php new file mode 100644 index 00000000..69c654e7 --- /dev/null +++ b/database/migrations/2026_01_01_000500_create_order_detail_tables.php @@ -0,0 +1,74 @@ +foreignId('variant_id')->constrained('product_variants')->cascadeOnDelete(); + $table->foreignId('product_option_value_id')->constrained()->cascadeOnDelete(); + $table->primary(['variant_id', 'product_option_value_id']); + $table->index('product_option_value_id', 'idx_variant_option_values_value_id'); + }); + + Schema::create('order_lines', function (Blueprint $table) { + $table->id(); + $table->foreignId('order_id')->constrained()->cascadeOnDelete(); + $table->foreignId('product_id')->nullable()->constrained()->nullOnDelete(); + $table->foreignId('variant_id')->nullable()->constrained('product_variants')->nullOnDelete(); + $table->string('title_snapshot'); + $table->string('sku_snapshot')->nullable(); + $table->integer('quantity')->default(1); + $table->integer('unit_price_amount')->default(0); + $table->integer('total_amount')->default(0); + $table->text('tax_lines_json')->default('[]'); + $table->text('discount_allocations_json')->default('[]'); + $table->index('order_id', 'idx_order_lines_order_id'); + $table->index('product_id', 'idx_order_lines_product_id'); + $table->index('variant_id', 'idx_order_lines_variant_id'); + }); + + Schema::create('payments', function (Blueprint $table) { + $table->id(); + $table->foreignId('order_id')->constrained()->cascadeOnDelete(); + $table->enum('provider', ['mock'])->default('mock'); + $table->enum('method', ['credit_card', 'paypal', 'bank_transfer']); + $table->string('provider_payment_id')->nullable(); + $table->enum('status', ['pending', 'captured', 'failed', 'refunded'])->default('pending'); + $table->integer('amount')->default(0); + $table->string('currency')->default('USD'); + $table->text('raw_json_encrypted')->nullable(); + $table->timestamp('created_at')->nullable(); + $table->index('order_id', 'idx_payments_order_id'); + $table->index(['provider', 'provider_payment_id'], 'idx_payments_provider_id'); + $table->index('method', 'idx_payments_method'); + $table->index('status', 'idx_payments_status'); + }); + + Schema::create('fulfillments', function (Blueprint $table) { + $table->id(); + $table->foreignId('order_id')->constrained()->cascadeOnDelete(); + $table->enum('status', ['pending', 'shipped', 'delivered'])->default('pending'); + $table->string('tracking_company')->nullable(); + $table->string('tracking_number')->nullable(); + $table->string('tracking_url')->nullable(); + $table->timestamp('shipped_at')->nullable(); + $table->timestamp('created_at')->nullable(); + $table->index('order_id', 'idx_fulfillments_order_id'); + $table->index('status', 'idx_fulfillments_status'); + $table->index(['tracking_company', 'tracking_number'], 'idx_fulfillments_tracking'); + }); + } + + public function down(): void + { + Schema::dropIfExists('fulfillments'); + Schema::dropIfExists('payments'); + Schema::dropIfExists('order_lines'); + Schema::dropIfExists('variant_option_values'); + } +}; diff --git a/database/migrations/2026_01_01_000600_create_refund_and_fulfillment_line_tables.php b/database/migrations/2026_01_01_000600_create_refund_and_fulfillment_line_tables.php new file mode 100644 index 00000000..cc4d7e86 --- /dev/null +++ b/database/migrations/2026_01_01_000600_create_refund_and_fulfillment_line_tables.php @@ -0,0 +1,40 @@ +id(); + $table->foreignId('order_id')->constrained()->cascadeOnDelete(); + $table->foreignId('payment_id')->constrained()->cascadeOnDelete(); + $table->integer('amount')->default(0); + $table->text('reason')->nullable(); + $table->enum('status', ['pending', 'processed', 'failed'])->default('pending'); + $table->string('provider_refund_id')->nullable(); + $table->timestamp('created_at')->nullable(); + $table->index('order_id', 'idx_refunds_order_id'); + $table->index('payment_id', 'idx_refunds_payment_id'); + $table->index('status', 'idx_refunds_status'); + }); + + Schema::create('fulfillment_lines', function (Blueprint $table) { + $table->id(); + $table->foreignId('fulfillment_id')->constrained()->cascadeOnDelete(); + $table->foreignId('order_line_id')->constrained()->cascadeOnDelete(); + $table->integer('quantity')->default(1); + $table->index('fulfillment_id', 'idx_fulfillment_lines_fulfillment_id'); + $table->unique(['fulfillment_id', 'order_line_id'], 'idx_fulfillment_lines_fulfillment_order_line'); + }); + } + + public function down(): void + { + Schema::dropIfExists('fulfillment_lines'); + Schema::dropIfExists('refunds'); + } +}; diff --git a/database/migrations/2026_01_01_000700_create_products_fts_table.php b/database/migrations/2026_01_01_000700_create_products_fts_table.php new file mode 100644 index 00000000..4d08daad --- /dev/null +++ b/database/migrations/2026_01_01_000700_create_products_fts_table.php @@ -0,0 +1,27 @@ +id(); + $table->foreignId('store_id')->constrained()->cascadeOnDelete(); + $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); + $table->string('format')->default('csv'); + $table->json('filters_json')->default('{}'); + $table->enum('status', ['queued', 'processing', 'completed', 'failed'])->default('queued'); + $table->unsignedInteger('row_count')->default(0); + $table->string('storage_key')->nullable(); + $table->text('error_message')->nullable(); + $table->timestamp('completed_at')->nullable(); + $table->timestamps(); + $table->index(['store_id', 'status']); + }); + } + + public function down(): void + { + Schema::dropIfExists('order_exports'); + } +}; diff --git a/database/migrations/2026_01_01_000810_enforce_store_owner_uniqueness.php b/database/migrations/2026_01_01_000810_enforce_store_owner_uniqueness.php new file mode 100644 index 00000000..9ca04998 --- /dev/null +++ b/database/migrations/2026_01_01_000810_enforce_store_owner_uniqueness.php @@ -0,0 +1,17 @@ +id(); + $table->foreignId('refund_id')->constrained()->cascadeOnDelete(); + $table->foreignId('order_line_id')->constrained()->cascadeOnDelete(); + $table->unsignedInteger('quantity'); + $table->integer('amount')->default(0); + $table->unique(['refund_id', 'order_line_id']); + $table->index('order_line_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('refund_lines'); + } +}; diff --git a/database/seeders/AnalyticsSeeder.php b/database/seeders/AnalyticsSeeder.php new file mode 100644 index 00000000..29edbaf0 --- /dev/null +++ b/database/seeders/AnalyticsSeeder.php @@ -0,0 +1,25 @@ +where('handle', 'acme-fashion')->firstOrFail(); + foreach (range(29, 0) as $offset) { + $orders = 3 + (($offset * 7) % 10); + $revenue = $orders * (6500 + (($offset * 137) % 4000)); + DB::table('analytics_daily')->updateOrInsert(['store_id' => $store->id, 'date' => now()->subDays($offset)->toDateString()], [ + 'orders_count' => $orders, 'revenue_amount' => $revenue, 'aov_amount' => intdiv($revenue, $orders), + 'visits_count' => 90 + (($offset * 17) % 120), 'add_to_cart_count' => 20 + (($offset * 5) % 35), + 'checkout_started_count' => 10 + (($offset * 3) % 20), 'checkout_completed_count' => $orders, + ]); + } + } +} diff --git a/database/seeders/CollectionSeeder.php b/database/seeders/CollectionSeeder.php new file mode 100644 index 00000000..022ec0d5 --- /dev/null +++ b/database/seeders/CollectionSeeder.php @@ -0,0 +1,31 @@ + [ + ['New Arrivals', 'new-arrivals', 'Discover the latest additions to our store.'], + ['T-Shirts', 't-shirts', 'Premium cotton tees for every occasion.'], + ['Pants & Jeans', 'pants-jeans', 'Find the perfect fit from our denim and trouser range.'], + ['Sale', 'sale', 'Great deals on selected items.'], + ], + 'acme-electronics' => [['Featured', 'featured', 'Premium technology for work and play.'], ['Accessories', 'accessories', 'Essential accessories for every setup.']], + ]; + foreach ($sets as $handle => $collections) { + $store = Store::query()->where('handle', $handle)->firstOrFail(); + foreach ($collections as [$title, $collectionHandle, $description]) { + Collection::withoutGlobalScopes()->updateOrCreate(['store_id' => $store->id, 'handle' => $collectionHandle], [ + 'title' => $title, 'description_html' => "{$description}
", 'type' => 'manual', 'status' => 'active', + ]); + } + } + } +} diff --git a/database/seeders/CustomerSeeder.php b/database/seeders/CustomerSeeder.php new file mode 100644 index 00000000..3bfb0217 --- /dev/null +++ b/database/seeders/CustomerSeeder.php @@ -0,0 +1,64 @@ +where('handle', 'acme-fashion')->firstOrFail(); + $electronics = Store::query()->where('handle', 'acme-electronics')->firstOrFail(); + $fashionCustomers = [ + ['customer@acme.test', 'John Doe', true], ['jane@example.com', 'Jane Smith', false], + ['michael@example.com', 'Michael Brown', true], ['sarah@example.com', 'Sarah Wilson', false], + ['david@example.com', 'David Lee', true], ['emma@example.com', 'Emma Garcia', false], + ['james@example.com', 'James Taylor', false], ['lisa@example.com', 'Lisa Anderson', true], + ['robert@example.com', 'Robert Martinez', false], ['anna@example.com', 'Anna Thomas', true], + ]; + foreach ($fashionCustomers as $index => [$email, $name, $marketing]) { + $customer = $this->customer($fashion, $email, $name, $marketing); + $this->address($customer, 'Home', true, [ + 'first_name' => str($name)->before(' ')->toString(), 'last_name' => str($name)->after(' ')->toString(), + 'company' => null, 'address1' => $index === 0 ? 'Hauptstrasse 1' : ($index === 1 ? 'Schillerstrasse 45' : 'Musterstrasse '.($index + 10)), + 'address2' => null, 'city' => $index === 1 ? 'Munich' : 'Berlin', 'province' => $index === 1 ? 'Bavaria' : 'Berlin', + 'province_code' => $index === 1 ? 'BY' : 'BE', 'country' => 'DE', 'country_code' => 'DE', + 'postal_code' => $index === 1 ? '80336' : '10115', 'zip' => $index === 1 ? '80336' : '10115', + 'phone' => $index === 0 ? '+49 30 12345678' : null, + ]); + if ($index === 0) { + $this->address($customer, 'Work', false, [ + 'first_name' => 'John', 'last_name' => 'Doe', 'company' => 'Acme Corp', 'address1' => 'Friedrichstrasse 100', + 'address2' => '3rd Floor', 'city' => 'Berlin', 'province' => 'Berlin', 'province_code' => 'BE', + 'country' => 'DE', 'country_code' => 'DE', 'postal_code' => '10117', 'zip' => '10117', 'phone' => '+49 30 87654321', + ]); + } + } + + foreach ([['techfan@example.com', 'Tech Fan'], ['gadgetlover@example.com', 'Gadget Lover']] as $index => [$email, $name]) { + $customer = $this->customer($electronics, $email, $name, false); + $this->address($customer, 'Home', true, [ + 'first_name' => str($name)->before(' ')->toString(), 'last_name' => str($name)->after(' ')->toString(), + 'address1' => 'Technikweg '.($index + 1), 'city' => 'Berlin', 'province' => 'Berlin', 'province_code' => 'BE', + 'country' => 'DE', 'country_code' => 'DE', 'postal_code' => '10115', 'zip' => '10115', + ]); + } + } + + private function customer(Store $store, string $email, string $name, bool $marketing): Customer + { + return Customer::withoutGlobalScopes()->updateOrCreate(['store_id' => $store->id, 'email' => $email], [ + 'name' => $name, 'password_hash' => Hash::make('password'), 'marketing_opt_in' => $marketing, + ]); + } + + /** @param arrayAcme Fashion creates modern essentials with purpose from our Berlin studio.
Ethical sourcing, sustainability and fair labour guide every decision.
Our Berlin-based designers build lasting wardrobes, not passing trends.
'], + ['FAQ', 'faq', 'Germany orders arrive in 2-4 days, express in 1-2 days, and EU orders in 5-7 days.
Return unworn items in original packaging within 30 days.
We ship across the EU and to the US, UK, Canada and Australia.
We email your tracking number as soon as it ships.
'], + ['Shipping & Returns', 'shipping-returns', 'Returns are accepted for 30 days. Customers pay return postage unless an item is defective.
'], + ['Privacy Policy', 'privacy-policy', 'We collect the details needed to fulfil your order.
We use data only to provide and improve our service.
Essential cookies keep your cart and account secure.
privacy@acme-fashion.test
'], + ['Terms of Service', 'terms', 'Orders are charged in EUR and prices include tax.
Screen colours may vary slightly from physical items.
Liability is limited as permitted by law.
The laws of the Federal Republic of Germany apply.
'], + ] as [$title, $handle, $body]) { + Page::withoutGlobalScopes()->updateOrCreate(['store_id' => $store->id, 'handle' => $handle], [ + 'title' => $title, 'body_html' => $body, 'status' => 'published', 'published_at' => now()->subMonths(3), + ]); + } + } +} diff --git a/database/seeders/ProductSeeder.php b/database/seeders/ProductSeeder.php new file mode 100644 index 00000000..d0825a72 --- /dev/null +++ b/database/seeders/ProductSeeder.php @@ -0,0 +1,135 @@ +where('handle', 'acme-fashion')->firstOrFail(); + $electronics = Store::query()->where('handle', 'acme-electronics')->firstOrFail(); + + $products = [ + ['Classic Cotton T-Shirt', 'classic-cotton-t-shirt', 'Acme Basics', 'T-Shirts', ['new', 'popular'], 2499, null, 200, 15, 'deny', true, ['Size' => ['S', 'M', 'L', 'XL'], 'Color' => ['White', 'Black', 'Navy']], ['new-arrivals', 't-shirts'], 'A timeless classic cotton t-shirt. Comfortable, breathable, and perfect for everyday wear.'], + ['Premium Slim Fit Jeans', 'premium-slim-fit-jeans', 'Acme Denim', 'Pants', ['new', 'sale'], 7999, 9999, 800, 8, 'deny', true, ['Size' => ['28', '30', '32', '34', '36'], 'Color' => ['Blue', 'Black']], ['new-arrivals', 'pants-jeans', 'sale'], 'Slim fit jeans crafted from premium stretch denim. Comfortable all-day wear with a modern silhouette.'], + ['Organic Hoodie', 'organic-hoodie', 'Acme Basics', 'Hoodies', ['new', 'trending'], 5999, null, 500, 20, 'deny', true, ['Size' => ['S', 'M', 'L', 'XL']], ['new-arrivals'], 'Made from 100% organic cotton. Warm, soft, and sustainably produced.'], + ['Leather Belt', 'leather-belt', 'Acme Accessories', 'Accessories', ['popular'], 3499, null, 150, 25, 'deny', true, ['Size' => ['S/M', 'L/XL'], 'Color' => ['Brown', 'Black']], [], 'Genuine leather belt with brushed metal buckle. A wardrobe essential.'], + ['Running Sneakers', 'running-sneakers', 'Acme Sport', 'Shoes', ['trending'], 11999, null, 600, 5, 'deny', true, ['Size' => ['EU 38', 'EU 39', 'EU 40', 'EU 41', 'EU 42', 'EU 43', 'EU 44'], 'Color' => ['White', 'Black']], ['new-arrivals'], 'Lightweight running sneakers with responsive cushioning and breathable mesh upper.'], + ['Graphic Print Tee', 'graphic-print-tee', 'Acme Basics', 'T-Shirts', ['new'], 2999, null, 210, 18, 'deny', true, ['Size' => ['S', 'M', 'L', 'XL']], ['t-shirts'], 'Bold graphic print on soft cotton. Express yourself with this statement piece.'], + ['V-Neck Linen Tee', 'v-neck-linen-tee', 'Acme Basics', 'T-Shirts', ['popular'], 3499, null, 180, 12, 'deny', true, ['Size' => ['S', 'M', 'L'], 'Color' => ['Beige', 'Olive', 'Sky Blue']], ['t-shirts'], 'Lightweight linen blend v-neck. Perfect for warm summer days.'], + ['Striped Polo Shirt', 'striped-polo-shirt', 'Acme Basics', 'T-Shirts', ['sale'], 2799, 3999, 250, 10, 'deny', true, ['Size' => ['S', 'M', 'L', 'XL']], ['t-shirts', 'sale'], 'Classic striped polo with a modern relaxed fit. Knitted collar and two-button placket.'], + ['Cargo Pants', 'cargo-pants', 'Acme Workwear', 'Pants', ['popular'], 5499, null, 700, 14, 'deny', true, ['Size' => ['30', '32', '34', '36'], 'Color' => ['Khaki', 'Olive', 'Black']], ['pants-jeans'], 'Utility cargo pants with multiple pockets. Durable cotton twill construction.'], + ['Chino Shorts', 'chino-shorts', 'Acme Basics', 'Pants', ['new', 'trending'], 3999, null, 350, 16, 'deny', true, ['Size' => ['30', '32', '34', '36'], 'Color' => ['Navy', 'Sand']], ['pants-jeans', 'new-arrivals'], 'Tailored chino shorts. Comfortable stretch fabric with a clean silhouette.'], + ['Wide Leg Trousers', 'wide-leg-trousers', 'Acme Denim', 'Pants', ['sale'], 4999, 6999, 550, 7, 'deny', true, ['Size' => ['S', 'M', 'L']], ['pants-jeans', 'sale'], 'Relaxed wide leg trousers with a high waist. Flowing drape in premium woven fabric.'], + ['Wool Scarf', 'wool-scarf', 'Acme Accessories', 'Accessories', ['popular'], 2999, null, 120, 30, 'deny', true, ['Color' => ['Grey', 'Burgundy', 'Navy']], [], 'Warm merino wool scarf. Soft hand feel, naturally breathable and temperature regulating.'], + ['Canvas Tote Bag', 'canvas-tote-bag', 'Acme Accessories', 'Accessories', ['trending'], 1999, null, 300, 40, 'deny', true, ['Color' => ['Natural', 'Black']], [], 'Heavy-duty canvas tote bag with reinforced handles. Spacious enough for daily essentials.'], + ['Bucket Hat', 'bucket-hat', 'Acme Accessories', 'Accessories', ['new', 'trending'], 2499, null, 80, 22, 'deny', true, ['Size' => ['S/M', 'L/XL'], 'Color' => ['Beige', 'Black', 'Olive']], ['new-arrivals'], 'Lightweight bucket hat for sun protection. Packable design, washed cotton twill.'], + ['Unreleased Winter Jacket', 'unreleased-winter-jacket', 'Acme Outerwear', 'Jackets', ['limited'], 14999, null, 900, 0, 'deny', true, ['Size' => ['S', 'M', 'L', 'XL']], [], 'Upcoming winter collection piece. Insulated puffer jacket with water-resistant shell.', 'draft'], + ['Discontinued Raincoat', 'discontinued-raincoat', 'Acme Outerwear', 'Jackets', [], 8999, null, 400, 3, 'deny', true, ['Size' => ['M', 'L']], [], 'Lightweight waterproof raincoat. This product has been discontinued.', 'archived'], + ['Limited Edition Sneakers', 'limited-edition-sneakers', 'Acme Sport', 'Shoes', ['limited'], 15999, null, 650, 0, 'deny', true, ['Size' => ['EU 40', 'EU 42', 'EU 44']], [], 'Limited edition collaboration sneakers. Once they are gone, they are gone.'], + ['Backorder Denim Jacket', 'backorder-denim-jacket', 'Acme Denim', 'Jackets', ['popular'], 9999, null, 750, 0, 'continue', true, ['Size' => ['S', 'M', 'L', 'XL']], [], 'Classic denim jacket. Currently on backorder - ships within 2-3 weeks.'], + ['Gift Card', 'gift-card', 'Acme Fashion', 'Gift Cards', ['popular'], [2500, 5000, 10000], null, 0, 9999, 'deny', false, ['Amount' => ['25 EUR', '50 EUR', '100 EUR']], [], 'Digital gift card delivered via email. The perfect gift when you are not sure what to choose.'], + ['Cashmere Overcoat', 'cashmere-overcoat', 'Acme Premium', 'Jackets', ['limited', 'new'], 49999, null, 1200, 3, 'deny', true, ['Size' => ['S', 'M', 'L'], 'Color' => ['Camel', 'Charcoal']], ['new-arrivals'], 'Luxurious cashmere-blend overcoat. Impeccable tailoring with silk lining.'], + ]; + + foreach ($products as $position => $definition) { + $this->seedProduct($fashion, $definition, $position); + } + + foreach ([ + ['Pro Laptop 15', 'pro-laptop-15', 'TechCorp', 'Laptops', ['featured'], [99999, 119999, 149999], null, 1800, 10, 'deny', true, ['Storage' => ['256GB', '512GB', '1TB']], ['featured'], 'Professional laptop with a brilliant 15-inch display.'], + ['Wireless Headphones', 'wireless-headphones', 'AudioMax', 'Audio', ['featured'], 14999, null, 250, 25, 'deny', true, ['Color' => ['Black', 'Silver']], ['featured'], 'Immersive wireless audio with active noise cancellation.'], + ['USB-C Cable 2m', 'usb-c-cable-2m', 'CablePro', 'Cables', ['accessory'], 1299, null, 50, 200, 'deny', true, [], ['accessories'], 'Durable two-metre USB-C charging and data cable.'], + ['Mechanical Keyboard', 'mechanical-keyboard', 'KeyTech', 'Peripherals', ['featured'], 12999, null, 1100, 15, 'deny', true, ['Switch Type' => ['Red', 'Blue', 'Brown']], ['featured'], 'Precision mechanical keyboard for work and gaming.'], + ['Monitor Stand', 'monitor-stand', 'DeskGear', 'Accessories', ['accessory'], 4999, null, 2500, 30, 'deny', true, [], ['accessories'], 'Ergonomic monitor stand with integrated storage.'], + ] as $position => $definition) { + $this->seedProduct($electronics, $definition, $position); + } + } + + /** @param array'.$description.'
', + 'vendor' => $vendor, + 'product_type' => $type, + 'tags' => $tags, + 'published_at' => $status === 'draft' ? null : ($status === 'archived' ? now()->subMonths(6) : now()), + ]); + + if (! $product->variants()->exists()) { + $groups = []; + foreach ($options as $optionPosition => $values) { + $option = $product->options()->create(['name' => $optionPosition, 'position' => count($groups)]); + $groups[] = collect($values)->values()->map(fn (string $value, int $valuePosition) => $option->values()->create(['value' => $value, 'position' => $valuePosition]))->all(); + } + $combinations = $groups === [] ? [[]] : $this->cartesian($groups); + foreach ($combinations as $position => $combination) { + $price = is_array($prices) ? (int) ($prices[$position] ?? end($prices)) : (int) $prices; + $sku = $handle === 'gift-card' + ? ['ACME-GIFT-25', 'ACME-GIFT-50', 'ACME-GIFT-100'][$position] + : strtoupper('ACME-'.substr(preg_replace('/[^a-z0-9]/', '', $handle), 0, 8).'-'.($position + 1)); + $variant = $product->variants()->create([ + 'sku' => $sku, + 'barcode' => null, + 'price_amount' => $price, + 'compare_at_amount' => $compareAt, + 'currency' => 'EUR', + 'weight_g' => $weight, + 'requires_shipping' => $requiresShipping, + 'is_default' => $position === 0, + 'position' => $position, + 'status' => 'active', + ]); + if ($combination !== []) { + $variant->optionValues()->sync(collect($combination)->pluck('id')->all()); + } + InventoryItem::withoutGlobalScopes()->updateOrCreate( + ['variant_id' => $variant->id], + [ + 'store_id' => $store->id, + 'quantity_on_hand' => $inventory, + 'quantity_reserved' => 0, + 'policy' => $policy, + ], + ); + } + } + + $collectionIds = Collection::withoutGlobalScopes()->where('store_id', $store->id)->whereIn('handle', $collections)->pluck('id', 'handle'); + $sync = []; + foreach ($collections as $position => $collectionHandle) { + if (isset($collectionIds[$collectionHandle])) { + $sync[$collectionIds[$collectionHandle]] = ['position' => $productPosition]; + } + } + $product->collections()->sync($sync); + } + + /** @param list{{ number_format($ordersCount) }}
{{ number_format($conversionRate, 2) }}%
No sales data for this period.
@endforelseNo referrer data yet.
@endforelseInstalled {{ $installation->installed_at?->diffForHumans() ?? 'recently' }}
Ready to install
Installed {{ $installation->installed_at?->diffForHumans() ?? 'recently' }}
{{ number_format($this->usage['calls']) }}
Recorded webhook calls
{{ $this->usage['last_call_at'] ? Illuminate\Support\Carbon::parse($this->usage['last_call_at'])->diffForHumans() : 'Never' }}
No scopes granted.
@endforelseEnter your email and we’ll send reset instructions if an account exists.
+ @if($sent) +Manage products, orders, customers, and your storefront.
+ @if(session('status'))/{{ $collection->handle }}
| Order | Date | Payment | Total |
|---|---|---|---|
| {{ $order->order_number }} | {{ ($order->placed_at ?? $order->created_at)->format('M j, Y') }} | ||
| This customer has no orders yet. | |||
{{ $address->label ?: 'Address' }}
@if($address->is_default){{ trim(($a['first_name'] ?? '').' '.($a['last_name'] ?? '')) }}
{{ $a['address1'] ?? '' }}
@if($a['address2'] ?? null){{ $a['address2'] }}
@endif{{ $a['zip'] ?? '' }} {{ $a['city'] ?? '' }}
{{ $a['country'] ?? $a['country_code'] ?? '' }}
@if(!$address->is_default && $adminRole !== 'support')No saved addresses.
@endforelse{{ $kpi['label'] }}
+{{ $kpi['value'] }}
+| Product | Units sold | Revenue |
|---|---|---|
| {{ $product['title'] }} | {{ number_format($product['units_sold']) }} | |
| No sales data for this period. | ||
It will not be shown again.
{{ $generatedToken }}{{ $user?->name }}
{{ $user?->email }}
{{ $item['label'] }}
{{ $item['type'] }}: {{ $item['url'] ?: ($item['resource_id'] ?? '—') }}
This menu has no items.
@endforelse{{ $order->customer?->name ?: 'Guest' }}
{{ $order->email }}
| Product | SKU | Qty | Unit price | Total |
|---|---|---|---|---|
| {{ $line->title_snapshot }} | {{ $line->sku_snapshot ?: '—' }} | {{ $line->quantity }} |
{{ Str::headline($payment->method->value ?? $payment->method) }}
{{ $payment->provider_payment_id ?: 'No provider reference' }}
No payment records.
@endforelse{{ $fulfillment->tracking_company ?: 'No carrier' }} · {{ $fulfillment->tracking_number ?: 'No tracking number' }}
@if($fulfillment->tracking_url)Track shipment@endifNo fulfillments created yet.
@endforelseOrder placed
{{ ($order->placed_at ?? $order->created_at)->format('M j, Y g:i A') }}
Payment {{ Str::headline($payment->status->value ?? $payment->status) }}
{{ $payment->created_at?->format('M j, Y g:i A') }}
Refund {{ Str::headline($refund->status->value ?? $refund->status) }}
{{ $order->customer?->name ?: 'Guest' }}
{{ $order->email }}
@if($order->customer){{ $line }}
@endforeach{{ $line }}
@endforeach{{ $product->variants_count }} {{ Str::plural('variant', $product->variants_count) }}
Last indexed: {{ $lastIndexedAt ? Illuminate\Support\Carbon::parse($lastIndexedAt)->diffForHumans() : 'Never' }}
{{ $reindexProgress }}% complete
Matched zone: {{ $testResult['zone'] }}
No shipping zone matches this address.
@endifVersion {{ $theme->version }}
{{ $description }}
+ @endif +{{ $description }}
+ @endif + + @isset($action) +{{ $error }}
+ @endif +{{ $description }}
+ @endif +{{ $description }}
+ @endif +{{ $title }}
+ + @if (filled($description)) +{{ $description }}
+ @endif + + @isset($action) +{{ $intro }}
@endifAdd an address to make checkout faster. Account recovery Enter your email and we’ll send reset instructions if an account exists. Welcome back Don’t have an account? Create one Join us Already have an account? Log in Account recovery No orders yet {{ optional($order->placed_at)->format('M j, Y') }} SKU {{ $line->sku_snapshot }} Quantity {{ $line->quantity }} {{ str_replace('_',' ', $order->payment_method instanceof \BackedEnum ? $order->payment_method->value : $order->payment_method) }} {{ $fulfillment->tracking_company }} · {{ $fulfillment->tracking_number }} {{ $discountError }} {{ $message }} {{ $line->variant->optionValues->pluck('value')->join(' / ') }} {{ $message }} {{ $message }} {{ $message }} {{ $message }} {{ $message }} {{ $line->variant->product->title }} {{ $line->variant->optionValues->pluck('value')->join(' / ') }} {{ $discountError }} Order {{ $order->order_number }} We’ve sent a confirmation to {{ $order->email }}. We’ll let you know when your order is on its way. Please transfer the total amount to the following account: Please complete your transfer within 7 days. Your order will be processed once payment is confirmed. {{ $line->title_snapshot }} Quantity: {{ $line->quantity }} {{ str_replace('_', ' ', $order->payment_method instanceof \BackedEnum ? $order->payment_method->value : $order->payment_method) }} Step 1 {{ $email }} Enter your contact details to continue. Step 2 Available after contact information. Step 3 {{ data_get($chosenRate, 'name', 'Selected shipping method') }} Available after your address. Step 4 Available after shipping. Browse by story Explore pieces grouped to make finding your next favorite easy. Please check back shortly. {{ $collection->products_count }} {{ str('product')->plural($collection->products_count) }} Collection {{ $products->total() }} {{ str('product')->plural($products->total()) }} Try adjusting your filters or browse the full collection. Your cart is empty {{ $line->variant->optionValues->pluck('value')->join(' / ') }} Products are coming soon. A note from us Subscribe for thoughtful product updates, useful stories, and occasional offers. {{ $message }} Type at least two characters to search. No results for “{{ $query }}” We’re currently performing maintenance. Please check back shortly. {{ $currentStore->name }}
+ {{ data_get($settings, 'home.hero.subheading') }}
+ Curated for you New and noteworthy {{ $currentStore->name }} {{ $product->vendor }} {{ $message }} {{ $results->total() }} results Try a different search term or clear your filters. Created through the API. Answers. Built for long days. Soft cotton.
';
+ $source = strip_tags($source, $allowedTags);
+ $sanitized = e(strip_tags($source));
+
+ if (class_exists(\DOMDocument::class) && $source !== '') {
+ $document = new \DOMDocument('1.0', 'UTF-8');
+ $previousSetting = libxml_use_internal_errors(true);
+ $document->loadHTML(
+ 'No saved addresses
{{ $saved->label ?: 'Address' }}
{{ data_get($data,'first_name') }} {{ data_get($data,'last_name') }}
{{ data_get($data,'address1') }}@if(data_get($data,'address2'))
{{ data_get($data,'address2') }}@endif
{{ data_get($data,'zip',data_get($data,'postal_code')) }} {{ data_get($data,'city') }}
{{ data_get($data,'country_code',data_get($data,'country')) }}{{ $editingId ? 'Edit address' : 'Add address' }}
Reset your password
Log in to your account
Create an account
Choose a new password
Recent Orders
@if($orders->isNotEmpty())View all@endif @foreach($orders as $order)Order Date Status Total @endforeach{{ $order->order_number }} {{ optional($order->placed_at)->format('M j, Y') }} Order {{ $order->order_number }}
@foreach($orders as $order)Order Date Status Total Action @endforeach{{ $order->order_number }} {{ optional($order->placed_at)->format('M j, Y') }} View Items
@endif
{{ $line->title_snapshot }}
@if($line->sku_snapshot)Shipping Address
{{ data_get($shipping,'first_name') }} {{ data_get($shipping,'last_name') }}
{{ data_get($shipping,'address1') }}
{{ data_get($shipping,'zip',data_get($shipping,'postal_code')) }} {{ data_get($shipping,'city') }}
{{ data_get($shipping,'country_code',data_get($shipping,'country')) }}Billing Address
{{ data_get($billing,'first_name') }} {{ data_get($billing,'last_name') }}
{{ data_get($billing,'address1') }}
{{ data_get($billing,'zip',data_get($billing,'postal_code')) }} {{ data_get($billing,'city') }}Payment
+ @if($order->fulfillments->isNotEmpty())Fulfillment
Your Cart
+
+ @if (! $this->cart || $this->cart->lines->isEmpty())
+
+ @else
+ @endif
+
Order Summary
+ @endif{{ $line->quantity }}
+Thank you for your order!
Bank Transfer Instructions
Order Summary
@endif
Shipping Address
{{ data_get($shippingAddress, 'first_name') }} {{ data_get($shippingAddress, 'last_name') }}
{{ data_get($shippingAddress, 'address1') }}
{{ data_get($shippingAddress, 'zip', data_get($shippingAddress, 'postal_code')) }} {{ data_get($shippingAddress, 'city') }}
{{ data_get($shippingAddress, 'country_code', data_get($shippingAddress, 'country')) }}Payment Method
+
+
+Checkout
+
+ Contact information
Shipping address
{{ $shipping['address1'] }}@if($shipping['address2']), {{ $shipping['address2'] }}@endif
{{ $shipping['postal_code'] }} {{ $shipping['city'] }}, {{ $shipping['country'] }}
+ @elseShipping method
Payment method
Filters
@if ($this->hasActiveFilters)@endifCollections
+ Collections are coming soon
+
+ @else
+
{{ $collection->title }}
+ {{ $collection->title }}
+ @if ($collection->description_html)
+ Filters
No products found
+ Your Cart ({{ $this->cartItemCount }})
+
+ @endif
+
+
Stay in the loop
+ Products
@endif
Collections
We’ll be back soon
+ @endif
+
+
+ {{ data_get($settings, 'home.hero.heading') }}
+
+ Shop collections
+
+ @else
+
+ @endif
+
+
{{ $collection->title }}
+ Shop now
+ Featured products
+ {{ $page->title }}
+
+ @endforeach
+
{{ $product->title }}
+
+ Filters
@if($this->hasActiveFilters)@endif{{ $query !== '' ? $results->total().' results for “'.$query.'”' : 'Search products' }}
+
+ @error('query')Filters
No results found for “{{ $query }}”
+
+HTML;
+
+ $product = Product::factory()->for($context['store'])->create([
+ 'description_html' => $malicious,
+ ]);
+ $page = Page::factory()->for($context['store'])->create([
+ 'body_html' => $malicious,
+ ]);
+
+ foreach ([$product->getRawOriginal('description_html'), $page->getRawOriginal('body_html')] as $persisted) {
+ expect($persisted)
+ ->toContain('Safe copy
') + ->toContain('Safe link') + ->toContain('
')
+ ->not->toContain('script')
+ ->not->toContain('iframe')
+ ->not->toContain('javascript')
+ ->not->toContain('data:image')
+ ->not->toContain('onclick')
+ ->not->toContain('onerror')
+ ->not->toContain('style=')
+ ->not->toContain('target=')
+ ->not->toContain('rel=')
+ ->not->toContain('width=');
+ }
+
+ $product->update(['description_html' => 'Quoted']); + + expect($product->refresh()->getRawOriginal('description_html'))->toBe('
Quoted'); + }); +}); + +describe('theme preview isolation', function () { + it('only exposes a draft preview to an authorized admin of that storefront tenant', function () { + $storeA = createStoreContext(); + $published = Theme::factory()->for($storeA['store'])->create([ + 'name' => 'Published A', + 'status' => 'published', + ]); + $published->settings()->create(['settings_json' => [ + 'colors' => ['primary' => '#112233'], + ]]); + $draft = Theme::factory()->draft()->for($storeA['store'])->create(['name' => 'Draft A']); + $draft->settings()->create(['settings_json' => [ + 'colors' => ['primary' => '#abcdef'], + ]]); + + $storeB = createStoreContext(); + $foreignDraft = Theme::factory()->draft()->for($storeB['store'])->create(['name' => 'Draft B']); + $foreignDraft->settings()->create(['settings_json' => [ + 'colors' => ['primary' => '#fedcba'], + ]]); + + $previewUrl = "http://{$storeA['domain']->hostname}/?theme_preview={$draft->id}"; + + $this->withSession(["theme_preview.{$draft->id}" => ['colors' => ['primary' => '#ff0000']]]) + ->get($previewUrl) + ->assertOk() + ->assertSee('--storefront-primary: #112233', false) + ->assertDontSee('#abcdef', false) + ->assertDontSee('#ff0000', false); + + $this->actingAs($storeB['user'], 'web') + ->get($previewUrl) + ->assertOk() + ->assertSee('--storefront-primary: #112233', false) + ->assertDontSee('#abcdef', false); + + $this->actingAs($storeA['user'], 'web') + ->get($previewUrl) + ->assertOk() + ->assertSee('--storefront-primary: #abcdef', false) + ->assertDontSee('--storefront-primary: #112233', false); + + $this->get("http://{$storeA['domain']->hostname}/?theme_preview={$foreignDraft->id}") + ->assertOk() + ->assertSee('--storefront-primary: #112233', false) + ->assertDontSee('#fedcba', false); + }); +}); diff --git a/tests/Feature/Shop/SecurityHardeningTest.php b/tests/Feature/Shop/SecurityHardeningTest.php new file mode 100644 index 00000000..f091a558 --- /dev/null +++ b/tests/Feature/Shop/SecurityHardeningTest.php @@ -0,0 +1,161 @@ +disabled()->create(); + + $this->post(route('login.store'), [ + 'email' => $user->email, + 'password' => 'password', + ])->assertSessionHasErrors('email'); + $this->assertGuest('web'); + + $this->actingAs($user)->get('/dashboard')->assertRedirect('/login'); + $this->assertGuest('web'); + }); + + it('rejects and revokes a disabled users Sanctum token', function () { + $context = createStoreContext(); + $token = $context['user']->createToken('disabled-user', [ + 'store:'.$context['store']->id, + ]); + $context['user']->update(['status' => 'disabled']); + + $this->withToken($token->plainTextToken) + ->getJson('/api/admin/v1/stores/'.$context['store']->id.'/me') + ->assertUnauthorized(); + + expect(DB::table('personal_access_tokens')->where('id', $token->accessToken->id)->exists())->toBeFalse(); + }); + + it('builds reset links from the canonical application URL regardless of Host', function () { + Notification::fake(); + config(['app.url' => 'https://canonical.shop.test']); + request()->headers->set('host', 'attacker.example'); + $user = User::factory()->create(); + + expect(Password::broker('users')->sendResetLink(['email' => $user->email])) + ->toBe(Password::RESET_LINK_SENT); + + Notification::assertSentTo($user, ResetPassword::class, function (ResetPassword $notification) use ($user): bool { + $url = $notification->toMail($user)->actionUrl; + + return str_starts_with($url, 'https://canonical.shop.test/admin/reset-password/') + && ! str_contains($url, 'attacker.example'); + }); + }); +}); + +describe('customer identity hardening', function () { + it('creates a fresh account without claiming a guest customer or historical orders', function () { + $store = createStoreContext()['store']; + $guest = Customer::factory()->guest()->for($store)->create(['email' => 'buyer@example.test']); + $historical = Order::factory()->for($store)->for($guest)->create(['email' => 'buyer@example.test']); + + $account = app(CustomerService::class)->register($store, [ + 'name' => 'Buyer', + 'email' => 'buyer@example.test', + 'password' => 'secure-password', + ]); + + expect($account->id)->not->toBe($guest->id) + ->and($account->email)->toBe('buyer@example.test') + ->and($guest->refresh()->email)->toEndWith('@unclaimed.invalid') + ->and($historical->refresh()->customer_id)->toBe($guest->id) + ->and($account->orders()->count())->toBe(0); + }); + + it('does not restore a customer session or remember token in another store', function () { + $storeA = createStoreContext()['store']; + $storeB = createStoreContext()['store']; + $customer = Customer::factory()->for($storeA)->create(); + bindStore($storeB); + + $provider = Auth::createUserProvider('customers'); + + expect($provider?->retrieveById($customer->id))->toBeNull() + ->and($provider?->retrieveByToken($customer->id, 'remember-me'))->toBeNull(); + }); +}); + +describe('untrusted persisted content hardening', function () { + it('permits relative and HTTP links but drops executable URL schemes', function () { + $store = createStoreContext()['store']; + $menu = NavigationMenu::factory()->for($store)->create(); + $unsafeItem = NavigationItem::factory()->for($menu, 'menu')->create(['url' => 'javascript:alert(1)']); + $safeItem = NavigationItem::factory()->for($menu, 'menu')->create(['url' => '/collections/new']); + $theme = Theme::factory()->for($store)->create(); + $settings = $theme->settings()->create(['settings_json' => [ + 'announcement' => ['url' => 'javascript:alert(1)'], + 'home' => ['hero' => ['cta_url' => 'https://example.test/sale']], + ]]); + $order = Order::factory()->for($store)->create(['customer_id' => null]); + $fulfillment = Fulfillment::factory()->for($order)->create(['tracking_url' => 'data:text/html,pwned']); + + expect($unsafeItem->url)->toBeNull() + ->and(DB::table('navigation_items')->where('id', $unsafeItem->id)->value('url'))->toBeNull() + ->and($safeItem->url)->toBe('/collections/new') + ->and(data_get($settings->settings_json, 'announcement.url'))->toBeNull() + ->and(data_get($settings->settings_json, 'home.hero.cta_url'))->toBe('https://example.test/sale') + ->and($fulfillment->tracking_url)->toBeNull() + ->and(DB::table('fulfillments')->where('id', $fulfillment->id)->value('tracking_url'))->toBeNull(); + }); + + it('never serializes encrypted provider payment payloads', function () { + $payment = Payment::factory()->create(['raw_json_encrypted' => ['secret' => 'provider-data']]); + + expect($payment->toArray())->not->toHaveKey('raw_json_encrypted') + ->and($payment->toJson())->not->toContain('provider-data'); + }); +}); + +it('resolves checkout confirmation only from the immutable order id snapshot', function () { + $store = createStoreContext()['store']; + $cart = Cart::factory()->for($store)->create(); + $expected = Order::factory()->for($store)->create(['customer_id' => null, 'email' => 'guest@example.test']); + $other = Order::factory()->for($store)->create(['customer_id' => null, 'email' => 'guest@example.test']); + $checkout = Checkout::factory()->completed()->for($store)->for($cart)->create([ + 'email' => 'guest@example.test', + 'totals_json' => ['order_id' => $expected->id], + ]); + session([ + 'checkout_access.'.$checkout->id => true, + 'last_order_id' => $other->id, + ]); + + Livewire::test(Confirmation::class, ['checkoutId' => $checkout->id]) + ->assertSet('order.id', $expected->id); +}); + +it('blocks Livewire mutations after a store is suspended', function () { + $context = createStoreContext(); + actingAsAdmin($context['user'], $context['store']); + $context['store']->update(['status' => 'suspended']); + bindStore($context['store']->refresh()); + app()->instance('request', Request::create('/livewire/update', 'POST')); + + expect(fn () => (new AdminSettings)->boot()) + ->toThrow(HttpException::class, 'This store is suspended.'); +}); diff --git a/tests/Feature/Shop/SideEffectsSearchAnalyticsJobsTest.php b/tests/Feature/Shop/SideEffectsSearchAnalyticsJobsTest.php new file mode 100644 index 00000000..85dca711 --- /dev/null +++ b/tests/Feature/Shop/SideEffectsSearchAnalyticsJobsTest.php @@ -0,0 +1,451 @@ +instance(DnsResolver::class, new class implements DnsResolver + { + public function resolve(string $hostname): array + { + return ['8.8.8.8']; + } + }); +}); + +describe('search indexing and discovery', function () { + it('finds active published products by prefix and isolates tenants', function () { + $storeA = createStoreContext()['store']; + $storeB = createStoreContext()['store']; + Product::factory()->for($storeA)->create([ + 'title' => 'Blue Cotton T-Shirt', + 'handle' => 'blue-cotton-shirt', + 'description_html' => '
Organic everyday essential
', + 'vendor' => 'Acme Basics', + ]); + Product::factory()->for($storeA)->create(['title' => 'Red Wool Sweater', 'handle' => 'red-wool-sweater']); + Product::factory()->for($storeA)->draft()->create(['title' => 'Cotton Draft', 'handle' => 'cotton-draft']); + Product::factory()->for($storeA)->archived()->create(['title' => 'Cotton Archive', 'handle' => 'cotton-archive']); + Product::factory()->for($storeB)->create(['title' => 'Cotton From Other Store', 'handle' => 'other-cotton']); + bindStore($storeA); + + $results = app(SearchService::class)->search($storeA, 'cott'); + + expect($results->total())->toBe(1) + ->and($results->items()[0]->title)->toBe('Blue Cotton T-Shirt') + ->and(SearchQuery::query()->latest('id')->first()->query)->toBe('cott') + ->and(SearchQuery::query()->latest('id')->first()->results_count)->toBe(1); + }); + + it('updates and removes FTS records through product observation', function () { + $store = createStoreContext()['store']; + $product = Product::factory()->for($store)->create(['title' => 'Original Search Phrase', 'handle' => 'search-observer']); + $search = app(SearchService::class); + + expect($search->search($store, 'Original')->total())->toBe(1); + $product->update(['title' => 'Replacement Discovery Phrase']); + expect($search->search($store, 'Original')->total())->toBe(0) + ->and($search->search($store, 'Replace')->total())->toBe(1); + $product->delete(); + expect($search->search($store, 'Replace')->total())->toBe(0); + }); + + it('paginates results and limits autocomplete suggestions', function () { + $store = createStoreContext()['store']; + foreach (range(1, 25) as $number) { + Product::factory()->for($store)->create([ + 'title' => "Summer Item {$number}", + 'handle' => "summer-item-{$number}", + ]); + } + $search = app(SearchService::class); + LengthAwarePaginator::currentPageResolver(fn (): int => 2); + $page = $search->search($store, 'Summer', perPage: 12); + LengthAwarePaginator::currentPageResolver(fn (): int => 1); + $suggestions = $search->autocomplete($store, 'Sum', 5); + + expect($page->total())->toBe(25) + ->and($page->currentPage())->toBe(2) + ->and($page->items())->toHaveCount(12) + ->and($suggestions)->toHaveCount(5); + }); + + it('sanitizes FTS operators instead of exposing query syntax errors', function () { + $store = createStoreContext()['store']; + Product::factory()->for($store)->create(['title' => 'Safe Cotton Product', 'handle' => 'safe-cotton']); + + $results = app(SearchService::class)->search($store, 'cotton" OR *'); + + expect($results)->toBeInstanceOf(LengthAwarePaginator::class); + }); +}); + +describe('analytics ingestion and aggregation', function () { + it('deduplicates client events inside a store but not across stores', function () { + $storeA = createStoreContext()['store']; + $storeB = createStoreContext()['store']; + $service = app(AnalyticsService::class); + $first = $service->track($storeA, 'page_view', ['path' => '/'], 'session-a', clientEventId: 'client-1'); + $duplicate = $service->track($storeA, 'page_view', ['path' => '/again'], 'session-b', clientEventId: 'client-1'); + $other = $service->track($storeB, 'page_view', ['path' => '/'], 'session-c', clientEventId: 'client-1'); + + expect($duplicate->id)->toBe($first->id) + ->and($other->id)->not->toBe($first->id) + ->and(AnalyticsEvent::withoutGlobalScopes()->where('client_event_id', 'client-1')->count())->toBe(2); + }); + + it('rejects unsupported event types', function () { + $store = createStoreContext()['store']; + + app(AnalyticsService::class)->track($store, 'execute_script'); + })->throws(InvalidArgumentException::class); + + it('aggregates visits conversions revenue and AOV idempotently', function () { + $store = createStoreContext()['store']; + $date = CarbonImmutable::parse('2026-07-10 12:00:00', 'UTC'); + $analytics = app(AnalyticsService::class); + foreach (['visitor-a', 'visitor-a', 'visitor-b'] as $index => $session) { + $analytics->track($store, 'page_view', [], $session, clientEventId: "page-{$index}", occurredAt: $date); + } + foreach (range(1, 3) as $index) { + $analytics->track($store, 'add_to_cart', [], 'visitor-a', clientEventId: "cart-{$index}", occurredAt: $date); + } + foreach ([1000, 2000, 3000] as $index => $total) { + $analytics->track($store, 'checkout_completed', ['total_amount' => $total], 'buyer', clientEventId: "order-{$index}", occurredAt: $date); + } + $analytics->track($store, 'checkout_started', [], 'buyer', clientEventId: 'checkout-start', occurredAt: $date); + + $first = $analytics->aggregate($store, '2026-07-10'); + $second = $analytics->aggregate($store, '2026-07-10'); + + expect($first->orders_count)->toBe(3) + ->and($first->revenue_amount)->toBe(6000) + ->and($first->aov_amount)->toBe(2000) + ->and($first->visits_count)->toBe(2) + ->and($first->add_to_cart_count)->toBe(3) + ->and($first->checkout_started_count)->toBe(1) + ->and($second->getAttributes())->toMatchArray($first->getAttributes()) + ->and(AnalyticsDaily::withoutGlobalScopes()->where('store_id', $store->id)->count())->toBe(1); + }); + + it('aggregates every active store through the scheduled job', function () { + $storeA = createStoreContext()['store']; + $storeB = createStoreContext()['store']; + $service = app(AnalyticsService::class); + $date = now()->subDay()->startOfDay()->addHour(); + $service->track($storeA, 'checkout_completed', ['total_amount' => 1000], 'a', clientEventId: 'a-order', occurredAt: $date); + $service->track($storeB, 'checkout_completed', ['total_amount' => 2500], 'b', clientEventId: 'b-order', occurredAt: $date); + + (new AggregateAnalytics($date->toDateString()))->handle($service); + + expect(AnalyticsDaily::withoutGlobalScopes()->where('date', $date->toDateString())->count())->toBe(2) + ->and(AnalyticsDaily::withoutGlobalScopes()->where('store_id', $storeA->id)->value('revenue_amount'))->toBe(1000) + ->and(AnalyticsDaily::withoutGlobalScopes()->where('store_id', $storeB->id)->value('revenue_amount'))->toBe(2500); + }); +}); + +describe('webhook dispatch delivery and circuit breaking', function () { + it('queues one delivery for each matching active subscription', function () { + Queue::fake(); + $store = createStoreContext()['store']; + $matching = WebhookSubscription::withoutGlobalScopes()->create([ + 'store_id' => $store->id, + 'event_type' => 'order.created', + 'target_url' => 'https://hooks.example/orders', + 'signing_secret_encrypted' => 'top-secret', + 'status' => 'active', + ]); + WebhookSubscription::withoutGlobalScopes()->create([ + 'store_id' => $store->id, + 'event_type' => 'product.created', + 'target_url' => 'https://hooks.example/products', + 'signing_secret_encrypted' => 'top-secret', + 'status' => 'active', + ]); + WebhookSubscription::withoutGlobalScopes()->create([ + 'store_id' => $store->id, + 'event_type' => 'order.created', + 'target_url' => 'https://hooks.example/paused', + 'signing_secret_encrypted' => 'top-secret', + 'status' => 'paused', + ]); + + app(WebhookService::class)->dispatch($store, 'order.created', ['order_id' => 42]); + + expect($matching->deliveries)->toHaveCount(1) + ->and($matching->deliveries->first()->status->value)->toBe('pending') + ->and($matching->deliveries->first()->attempt_count)->toBe(0); + Queue::assertPushed(DeliverWebhook::class, 1); + Queue::assertPushed(DeliverWebhook::class, fn (DeliverWebhook $job): bool => $job->eventType === 'order.created' && $job->payload === ['order_id' => 42]); + }); + + it('posts signed JSON and records a successful response', function () { + Http::fake(['https://hooks.example/*' => Http::response('accepted', 202)]); + $store = createStoreContext()['store']; + $subscription = WebhookSubscription::withoutGlobalScopes()->create([ + 'store_id' => $store->id, + 'event_type' => 'order.created', + 'target_url' => 'https://hooks.example/orders', + 'signing_secret_encrypted' => 'test-secret', + 'status' => 'active', + ]); + $delivery = $subscription->deliveries()->create(['event_id' => (string) Str::uuid(), 'attempt_count' => 0, 'status' => 'pending']); + $payload = ['order_id' => 42, 'total_amount' => 5000]; + + (new DeliverWebhook($delivery, 'order.created', $payload))->handle(app(WebhookService::class)); + + expect($delivery->refresh()->status->value)->toBe('success') + ->and($delivery->attempt_count)->toBe(1) + ->and($delivery->response_code)->toBe(202) + ->and($delivery->response_body_snippet)->toBe('accepted'); + Http::assertSent(function ($request) use ($delivery, $payload): bool { + $body = json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES); + $timestamp = (string) ($request->header('X-Platform-Timestamp')[0] ?? ''); + + return $request->url() === 'https://hooks.example/orders' + && $request->body() === $body + && ctype_digit($timestamp) + && $request->hasHeader('X-Platform-Event', 'order.created') + && $request->hasHeader('X-Platform-Delivery-Id', $delivery->event_id) + && $request->hasHeader('X-Platform-Signature', hash_hmac('sha256', $timestamp.'.'.$body, 'test-secret')); + }); + }); + + it('records failures and exposes the required retry schedule', function () { + Http::fake(['https://hooks.example/*' => Http::response('broken', 500)]); + $store = createStoreContext()['store']; + $subscription = WebhookSubscription::withoutGlobalScopes()->create([ + 'store_id' => $store->id, + 'event_type' => 'order.created', + 'target_url' => 'https://hooks.example/orders', + 'signing_secret_encrypted' => 'secret', + 'status' => 'active', + ]); + $delivery = $subscription->deliveries()->create(['event_id' => (string) Str::uuid(), 'attempt_count' => 0, 'status' => 'pending']); + $job = new DeliverWebhook($delivery, 'order.created', ['order_id' => 1]); + + expect(fn () => $job->handle(app(WebhookService::class)))->toThrow(RequestException::class) + ->and($delivery->refresh()->status->value)->toBe('failed') + ->and($delivery->attempt_count)->toBe(1) + ->and($delivery->response_code)->toBe(500) + ->and($job->tries)->toBe(6) + ->and($job->backoff)->toBe([60, 300, 1800, 7200, 43200]); + }); + + it('pauses a subscription after five consecutive delivery failures', function () { + $store = createStoreContext()['store']; + $subscription = WebhookSubscription::withoutGlobalScopes()->create([ + 'store_id' => $store->id, + 'event_type' => 'order.created', + 'target_url' => 'https://hooks.example/orders', + 'signing_secret_encrypted' => 'secret', + 'status' => 'active', + ]); + foreach (range(1, 5) as $attempt) { + $delivery = $subscription->deliveries()->create([ + 'event_id' => (string) Str::uuid(), + 'attempt_count' => 6, + 'status' => 'failed', + 'last_attempt_at' => now(), + ]); + } + + app(WebhookService::class)->recordFailure($delivery); + + expect($subscription->refresh()->status->value)->toBe('paused'); + }); + + it('encrypts subscription secrets at rest', function () { + $store = createStoreContext()['store']; + $subscription = WebhookSubscription::withoutGlobalScopes()->create([ + 'store_id' => $store->id, + 'event_type' => 'order.created', + 'target_url' => 'https://hooks.example/orders', + 'signing_secret_encrypted' => 'plain-secret', + 'status' => 'active', + ]); + $raw = DB::table('webhook_subscriptions')->where('id', $subscription->id)->value('signing_secret_encrypted'); + + expect($raw)->not->toContain('plain-secret') + ->and($subscription->signing_secret_encrypted)->toBe('plain-secret'); + }); +}); + +describe('domain side effects audit and media', function () { + it('turns order creation into notification webhook and deduplicated analytics', function () { + Notification::fake(); + Queue::fake(); + $fixture = paidOrderFixture(); + WebhookSubscription::withoutGlobalScopes()->create([ + 'store_id' => $fixture['store']->id, + 'event_type' => 'order.created', + 'target_url' => 'https://hooks.example/orders', + 'signing_secret_encrypted' => 'secret', + 'status' => 'active', + ]); + + event(new OrderCreated($fixture['order'])); + event(new OrderCreated($fixture['order'])); + + Notification::assertSentOnDemand(OrderLifecycleNotification::class, function (OrderLifecycleNotification $notification, array $channels, object $notifiable) use ($fixture): bool { + return $notification->type === 'confirmed' + && $notification->order->is($fixture['order']) + && ($notifiable->routes['mail'] ?? null) === $fixture['order']->email; + }); + expect(AnalyticsEvent::withoutGlobalScopes() + ->where('store_id', $fixture['store']->id) + ->where('client_event_id', "order:{$fixture['order']->id}:completed") + ->count())->toBe(1) + ->and(WebhookDelivery::query()->count())->toBe(2); + Queue::assertPushed(DeliverWebhook::class, 2); + }); + + it('writes structured audit changes with actor tenant and resource', function () { + $context = createStoreContext(); + actingAsAdmin($context['user'], $context['store']); + $product = Product::factory()->for($context['store'])->create(['title' => 'Before Audit', 'handle' => 'audit-product']); + /** @var LoggerInterface&MockInterface $auditChannel */ + $auditChannel = Mockery::mock(LoggerInterface::class); + $auditChannel->shouldReceive('info')->once()->with('product.updated', Mockery::on(function (array $context) use ($product): bool { + return $context['event'] === 'product.updated' + && $context['user_id'] !== null + && $context['store_id'] === $product->store_id + && $context['resource_type'] === 'product' + && $context['resource_id'] === $product->id + && $context['changes']['title'] === ['Before Audit', 'After Audit']; + })); + Log::shouldReceive('channel')->once()->with('audit')->andReturn($auditChannel); + + $product->update(['title' => 'After Audit']); + }); + + it('generates every media rendition and deletes all files with the record', function () { + Storage::fake('public'); + $store = createStoreContext()['store']; + $product = Product::factory()->for($store)->create(); + $file = UploadedFile::fake()->image('catalog.png', 1200, 800); + $source = $file->storeAs('media/originals', 'catalog.png', 'public'); + $media = $product->media()->create([ + 'type' => 'image', + 'storage_key' => $source, + 'alt_text' => 'Catalog image', + 'position' => 0, + 'status' => 'processing', + ]); + + (new ProcessMediaUpload($media))->handle(); + + expect($media->refresh()->status->value)->toBe('ready') + ->and($media->width)->toBe(1200) + ->and($media->height)->toBe(800) + ->and($media->mime_type)->toBe('image/png') + ->and($media->byte_size)->toBeGreaterThan(0); + foreach (['thumbnail', 'small', 'medium', 'large'] as $size) { + Storage::disk('public')->assertExists("media/{$product->id}/{$media->id}/{$size}.png"); + Storage::disk('public')->assertExists("media/{$product->id}/{$media->id}/{$size}.webp"); + } + + $media->delete(); + Storage::disk('public')->assertMissing($source); + foreach (['thumbnail', 'small', 'medium', 'large'] as $size) { + Storage::disk('public')->assertMissing("media/{$product->id}/{$media->id}/{$size}.png"); + Storage::disk('public')->assertMissing("media/{$product->id}/{$media->id}/{$size}.webp"); + } + }); + + it('marks invalid media failed and preserves the original for diagnosis', function () { + Storage::fake('public'); + $store = createStoreContext()['store']; + $product = Product::factory()->for($store)->create(); + $file = UploadedFile::fake()->createWithContent('invalid.png', 'not an image'); + $source = $file->storeAs('media/originals', 'invalid.png', 'public'); + $media = $product->media()->create(['type' => 'image', 'storage_key' => $source, 'position' => 0, 'status' => 'processing']); + + expect(fn () => (new ProcessMediaUpload($media))->handle())->toThrow(RuntimeException::class) + ->and($media->refresh()->status->value)->toBe('failed'); + Storage::disk('public')->assertExists($source); + }); +}); + +describe('maintenance scheduling and seeded acceptance data', function () { + it('abandons stale carts releases checkout reservations and leaves fresh carts active', function () { + $stale = checkoutReadyForPayment(); + DB::table('carts')->where('id', $stale['cart']->id)->update(['updated_at' => now()->subDays(15)]); + $freshStore = createStoreContext()['store']; + $fresh = app(CartService::class)->create($freshStore); + + (new CleanupAbandonedCarts)->handle(app(CheckoutService::class)); + + expect($stale['cart']->refresh()->status->value)->toBe('abandoned') + ->and($stale['checkout']->refresh()->status->value)->toBe('expired') + ->and($stale['variant']->inventoryItem->refresh()->quantity_reserved)->toBe(0) + ->and($fresh->refresh()->status->value)->toBe('active'); + }); + + it('registers all four required recurring jobs', function () { + Artisan::call('schedule:list'); + $output = Artisan::output(); + + expect($output)->toContain('ExpireAbandonedCheckouts') + ->and($output)->toContain('CleanupAbandonedCarts') + ->and($output)->toContain('AggregateAnalytics') + ->and($output)->toContain('CancelUnpaidBankTransferOrders') + ->and($output)->toContain('*/15 * * * *') + ->and($output)->toContain('30 0 * * *') + ->and($output)->toContain('0 1 * * *') + ->and($output)->toContain('0 2 * * *'); + }); + + it('seeds the exact demo tenants credentials catalog and isolation markers', function () { + $this->seed(); + $fashion = Store::query()->where('handle', 'acme-fashion')->firstOrFail(); + $electronics = Store::query()->where('handle', 'acme-electronics')->firstOrFail(); + $admin = User::query()->where('email', 'admin@acme.test')->firstOrFail(); + $customer = Customer::withoutGlobalScopes()->where('store_id', $fashion->id)->where('email', 'customer@acme.test')->firstOrFail(); + + expect(Store::query()->count())->toBe(2) + ->and($fashion->domains()->where('hostname', 'shop.test')->exists())->toBeTrue() + ->and($fashion->products()->count())->toBe(20) + ->and($electronics->products()->count())->toBe(5) + ->and($fashion->products()->where('handle', 'classic-cotton-t-shirt')->exists())->toBeTrue() + ->and($fashion->products()->where('handle', 'unreleased-winter-jacket')->where('status', 'draft')->exists())->toBeTrue() + ->and($fashion->products()->where('handle', 'gift-card')->whereHas('variants', fn ($query) => $query->where('requires_shipping', false))->exists())->toBeTrue() + ->and($electronics->products()->where('handle', 'pro-laptop-15')->exists())->toBeTrue() + ->and(password_verify('password', $admin->password_hash))->toBeTrue() + ->and(password_verify('password', $customer->password_hash))->toBeTrue() + ->and($admin->roleForStore($fashion)?->value)->toBe('owner'); + }); +}); diff --git a/tests/Feature/Shop/StorefrontApiTest.php b/tests/Feature/Shop/StorefrontApiTest.php new file mode 100644 index 00000000..273d802f --- /dev/null +++ b/tests/Feature/Shop/StorefrontApiTest.php @@ -0,0 +1,553 @@ +hostname.'/api/storefront/v1/'.ltrim($path, '/'); +} + +/** @return array{cart_id: int, email: string} */ +function storefrontCheckoutPayload(int $cartId, string $email = 'buyer@example.test'): array +{ + return ['cart_id' => $cartId, 'email' => $email]; +} + +/** @return array{payment_method: string, card_number: string, card_expiry: string, card_cvc: string, card_holder: string} */ +function storefrontCardPayment(string $number): array +{ + return [ + 'payment_method' => 'credit_card', + 'card_number' => $number, + 'card_expiry' => '12/30', + 'card_cvc' => '123', + 'card_holder' => 'Ada Lovelace', + ]; +} + +describe('storefront cart API', function () { + it('creates retrieves mutates and versions a cart', function () { + $context = createStoreContext(); + $sku = makeSellableVariant($context['store'], variantAttributes: ['price_amount' => 2500]); + + $created = $this->postJson(storefrontApiUrl($context, 'carts')) + ->assertCreated() + ->assertJsonPath('cart_version', 1) + ->assertJsonPath('currency', $context['store']->default_currency) + ->assertJsonCount(0, 'lines'); + $cartId = $created->json('id'); + + $added = $this->postJson(storefrontApiUrl($context, "carts/{$cartId}/lines"), [ + 'variant_id' => $sku['variant']->id, + 'quantity' => 2, + 'expected_version' => 1, + ])->assertCreated() + ->assertJsonPath('cart_version', 2) + ->assertJsonPath('totals.subtotal', 5000) + ->assertJsonPath('lines.0.quantity', 2) + ->assertJsonPath('lines.0.line_total_amount', 5000); + $lineId = $added->json('lines.0.id'); + + $this->getJson(storefrontApiUrl($context, "carts/{$cartId}")) + ->assertOk() + ->assertJsonCount(1, 'lines') + ->assertJsonPath('lines.0.product_title', $sku['product']->title); + + $this->putJson(storefrontApiUrl($context, "carts/{$cartId}/lines/{$lineId}"), [ + 'quantity' => 0, + 'cart_version' => 2, + ])->assertUnprocessable()->assertJsonValidationErrors('quantity'); + + $this->putJson(storefrontApiUrl($context, "carts/{$cartId}/lines/{$lineId}"), [ + 'quantity' => 3, + ])->assertUnprocessable()->assertJsonValidationErrors('cart_version'); + + $this->putJson(storefrontApiUrl($context, "carts/{$cartId}/lines/{$lineId}"), [ + 'quantity' => 3, + 'cart_version' => 2, + ])->assertOk() + ->assertJsonPath('cart_version', 3) + ->assertJsonPath('lines.0.quantity', 3) + ->assertJsonPath('lines.0.line_subtotal_amount', 7500); + + $this->putJson(storefrontApiUrl($context, "carts/{$cartId}/lines/{$lineId}"), [ + 'quantity' => 4, + 'cart_version' => 2, + ])->assertConflict() + ->assertJsonPath('data.cart_version', 3) + ->assertJsonPath('data.lines.0.quantity', 3); + + $this->deleteJson(storefrontApiUrl($context, "carts/{$cartId}/lines/{$lineId}"), [ + 'cart_version' => 3, + ])->assertOk() + ->assertJsonPath('cart_version', 4) + ->assertJsonCount(0, 'lines'); + }); + + it('returns validation responses for invalid quantities variants and inventory', function () { + $context = createStoreContext(); + $sku = makeSellableVariant($context['store'], inventoryAttributes: ['quantity_on_hand' => 1]); + $cart = app(CartService::class)->create($context['store']); + $this->withSession(['cart_id' => $cart->id, 'api_cart_ids' => [$cart->id]]); + + $this->postJson(storefrontApiUrl($context, "carts/{$cart->id}/lines"), [ + 'variant_id' => 999999, + 'quantity' => 1, + ])->assertUnprocessable()->assertJsonValidationErrors('variant_id'); + + $this->postJson(storefrontApiUrl($context, "carts/{$cart->id}/lines"), [ + 'variant_id' => $sku['variant']->id, + 'quantity' => 0, + ])->assertUnprocessable()->assertJsonValidationErrors('quantity'); + + $this->postJson(storefrontApiUrl($context, "carts/{$cart->id}/lines"), [ + 'variant_id' => $sku['variant']->id, + 'quantity' => 2, + ])->assertUnprocessable()->assertJsonValidationErrors('quantity'); + }); + + it('does not reveal carts or variants from another tenant', function () { + $contextA = createStoreContext(); + $contextB = createStoreContext(); + $foreignSku = makeSellableVariant($contextB['store']); + $foreignCart = app(CartService::class)->create($contextB['store']); + bindStore($contextA['store']); + $cart = app(CartService::class)->create($contextA['store']); + $this->withSession(['cart_id' => $cart->id, 'api_cart_ids' => [$cart->id]]); + + $this->getJson(storefrontApiUrl($contextA, "carts/{$foreignCart->id}"))->assertNotFound(); + $this->postJson(storefrontApiUrl($contextA, "carts/{$cart->id}/lines"), [ + 'variant_id' => $foreignSku['variant']->id, + 'quantity' => 1, + ])->assertUnprocessable()->assertJsonValidationErrors('variant_id'); + $this->getJson(storefrontApiUrl($contextA, 'carts/999999'))->assertNotFound(); + }); +}); + +describe('storefront cart and checkout ownership', function () { + it('does not let one anonymous session show or mutate another sessions cart or checkout', function () { + $context = createStoreContext(); + $sku = makeSellableVariant($context['store']); + + $cartAId = $this->postJson(storefrontApiUrl($context, 'carts')) + ->assertCreated() + ->json('id'); + $lineAId = $this->postJson(storefrontApiUrl($context, "carts/{$cartAId}/lines"), [ + 'variant_id' => $sku['variant']->id, + 'quantity' => 1, + 'expected_version' => 1, + ])->assertCreated()->json('lines.0.id'); + $checkoutAId = $this->postJson(storefrontApiUrl($context, 'checkouts'), storefrontCheckoutPayload($cartAId, 'session-a@example.test')) + ->assertCreated() + ->assertJsonPath('email', 'session-a@example.test') + ->json('id'); + + $this->flushSession(); + $cartBId = $this->postJson(storefrontApiUrl($context, 'carts')) + ->assertCreated() + ->json('id'); + expect($cartBId)->not->toBe($cartAId); + + $this->getJson(storefrontApiUrl($context, "carts/{$cartAId}"))->assertNotFound(); + $this->postJson(storefrontApiUrl($context, "carts/{$cartAId}/lines"), [ + 'variant_id' => $sku['variant']->id, + 'quantity' => 1, + 'expected_version' => 2, + ])->assertNotFound(); + $this->putJson(storefrontApiUrl($context, "carts/{$cartAId}/lines/{$lineAId}"), [ + 'quantity' => 5, + 'cart_version' => 2, + ])->assertNotFound(); + $this->postJson(storefrontApiUrl($context, 'checkouts'), storefrontCheckoutPayload($cartAId))->assertNotFound(); + $this->getJson(storefrontApiUrl($context, "checkouts/{$checkoutAId}"))->assertNotFound(); + $this->putJson(storefrontApiUrl($context, "checkouts/{$checkoutAId}/address"), validCheckoutAddress()) + ->assertNotFound(); + + $cartA = Cart::withoutGlobalScopes()->with('lines')->findOrFail($cartAId); + $checkoutA = Checkout::withoutGlobalScopes()->findOrFail($checkoutAId); + expect($cartA->cart_version)->toBe(2) + ->and($cartA->lines)->toHaveCount(1) + ->and($cartA->lines->first()->quantity)->toBe(1) + ->and($checkoutA->status->value)->toBe('started') + ->and($checkoutA->email)->toBe('session-a@example.test'); + }); + + it('does not let an authenticated customer access another customers cart or checkout', function () { + $context = createStoreContext(); + $sku = makeSellableVariant($context['store']); + $customerA = Customer::factory()->for($context['store'])->create(); + $customerB = Customer::factory()->for($context['store'])->create(); + $carts = app(CartService::class); + $checkouts = app(CheckoutService::class); + + $cartA = $carts->create($context['store'], $customerA); + $carts->addLine($cartA, $sku['variant'], 1); + $checkoutA = $checkouts->create($cartA); + $cartB = $carts->create($context['store'], $customerB); + $lineB = $carts->addLine($cartB, $sku['variant'], 1); + $checkoutB = $checkouts->create($cartB); + + actingAsCustomer($customerA); + $this->withSession(['cart_id' => $cartB->id]); + + $this->getJson(storefrontApiUrl($context, "carts/{$cartA->id}"))->assertOk(); + $this->getJson(storefrontApiUrl($context, "checkouts/{$checkoutA->id}"))->assertOk(); + $this->getJson(storefrontApiUrl($context, "carts/{$cartB->id}"))->assertNotFound(); + $this->postJson(storefrontApiUrl($context, "carts/{$cartB->id}/lines"), [ + 'variant_id' => $sku['variant']->id, + 'quantity' => 1, + 'expected_version' => 2, + ])->assertNotFound(); + $this->putJson(storefrontApiUrl($context, "carts/{$cartB->id}/lines/{$lineB->id}"), [ + 'quantity' => 5, + 'cart_version' => 2, + ])->assertNotFound(); + $this->postJson(storefrontApiUrl($context, 'checkouts'), storefrontCheckoutPayload($cartB->id))->assertNotFound(); + $this->getJson(storefrontApiUrl($context, "checkouts/{$checkoutB->id}"))->assertNotFound(); + $this->putJson(storefrontApiUrl($context, "checkouts/{$checkoutB->id}/address"), validCheckoutAddress()) + ->assertNotFound(); + + expect($cartB->refresh()->cart_version)->toBe(2) + ->and($lineB->refresh()->quantity)->toBe(1) + ->and($checkoutB->refresh()->status->value)->toBe('started') + ->and($checkoutB->email)->toBeNull(); + }); +}); + +describe('storefront checkout API', function () { + it('keeps one live checkout per cart and refreshes its starting email', function () { + $context = createStoreContext(); + $sku = makeSellableVariant($context['store']); + ['cart' => $cart] = makeCartWithLine($context['store'], $sku['variant']); + $this->withSession(['cart_id' => $cart->id, 'api_cart_ids' => [$cart->id]]); + + $first = $this->postJson( + storefrontApiUrl($context, 'checkouts'), + storefrontCheckoutPayload($cart->id, 'first@example.test'), + )->assertCreated(); + $second = $this->postJson( + storefrontApiUrl($context, 'checkouts'), + storefrontCheckoutPayload($cart->id, 'second@example.test'), + )->assertCreated(); + + expect($second->json('id'))->toBe($first->json('id')) + ->and($second->json('email'))->toBe('second@example.test') + ->and($cart->checkouts()->whereNotIn('status', ['completed', 'expired'])->count())->toBe(1); + }); + + it('rejects every cart mutation after payment selection', function () { + $context = createStoreContext(); + $sku = makeSellableVariant($context['store']); + ['cart' => $cart, 'line' => $line] = makeCartWithLine($context['store'], $sku['variant']); + $this->withSession(['cart_id' => $cart->id, 'api_cart_ids' => [$cart->id]]); + $zone = ShippingZone::factory()->for($context['store'])->create(['countries_json' => ['DE']]); + $rate = ShippingRate::factory()->for($zone, 'zone')->create(); + $checkoutId = $this->postJson(storefrontApiUrl($context, 'checkouts'), storefrontCheckoutPayload($cart->id)) + ->assertCreated()->json('id'); + $this->putJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/address"), validCheckoutAddress())->assertOk(); + $this->putJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/shipping-method"), ['shipping_method_id' => $rate->id])->assertOk(); + $this->putJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/payment-method"), ['payment_method' => 'credit_card']) + ->assertOk()->assertJsonPath('status', 'payment_selected'); + + $this->postJson(storefrontApiUrl($context, "carts/{$cart->id}/lines"), [ + 'variant_id' => $sku['variant']->id, + 'quantity' => 1, + 'expected_version' => 2, + ])->assertUnprocessable()->assertJsonValidationErrors('variant_id'); + $this->putJson(storefrontApiUrl($context, "carts/{$cart->id}/lines/{$line->id}"), [ + 'quantity' => 5, + 'cart_version' => 2, + ])->assertUnprocessable()->assertJsonValidationErrors('cart'); + $this->deleteJson(storefrontApiUrl($context, "carts/{$cart->id}/lines/{$line->id}"), [ + 'cart_version' => 2, + ])->assertUnprocessable()->assertJsonValidationErrors('cart'); + + expect($cart->refresh()->cart_version)->toBe(2) + ->and($line->refresh()->quantity)->toBe(1) + ->and($sku['inventory']->refresh()->quantity_reserved)->toBe(1); + }); + + it('returns gone for an expired checkout', function () { + $context = createStoreContext(); + $sku = makeSellableVariant($context['store']); + ['cart' => $cart] = makeCartWithLine($context['store'], $sku['variant']); + $this->withSession(['cart_id' => $cart->id, 'api_cart_ids' => [$cart->id]]); + $checkoutId = $this->postJson(storefrontApiUrl($context, 'checkouts'), storefrontCheckoutPayload($cart->id)) + ->assertCreated()->json('id'); + Checkout::withoutGlobalScopes()->whereKey($checkoutId)->update(['expires_at' => now()->subMinute()]); + + $this->getJson(storefrontApiUrl($context, "checkouts/{$checkoutId}"))->assertGone(); + $this->putJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/address"), validCheckoutAddress())->assertGone(); + }); + + it('requires complete matching payment fields before charging', function () { + $context = createStoreContext(); + $sku = makeSellableVariant($context['store']); + ['cart' => $cart] = makeCartWithLine($context['store'], $sku['variant']); + $this->withSession(['cart_id' => $cart->id, 'api_cart_ids' => [$cart->id]]); + $zone = ShippingZone::factory()->for($context['store'])->create(['countries_json' => ['DE']]); + $rate = ShippingRate::factory()->for($zone, 'zone')->create(); + $checkoutId = $this->postJson(storefrontApiUrl($context, 'checkouts'), storefrontCheckoutPayload($cart->id)) + ->assertCreated()->json('id'); + $this->putJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/address"), validCheckoutAddress())->assertOk(); + $this->putJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/shipping-method"), ['shipping_method_id' => $rate->id])->assertOk(); + $this->putJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/payment-method"), ['payment_method' => 'credit_card'])->assertOk(); + + $this->postJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/pay"), []) + ->assertUnprocessable()->assertJsonValidationErrors('payment_method'); + $this->postJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/pay"), ['payment_method' => 'credit_card']) + ->assertUnprocessable()->assertJsonValidationErrors(['card_number', 'card_expiry', 'card_cvc', 'card_holder']); + $this->postJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/pay"), ['payment_method' => 'paypal']) + ->assertUnprocessable()->assertJsonValidationErrors('payment_method'); + + expect(Order::withoutGlobalScopes()->where('store_id', $context['store']->id)->count())->toBe(0) + ->and(Payment::query()->count())->toBe(0) + ->and($sku['inventory']->refresh()->quantity_reserved)->toBe(1); + }); + + it('progresses a digital-only checkout without a shipping address or rate', function () { + $context = createStoreContext(); + $sku = makeSellableVariant($context['store'], variantAttributes: [ + 'requires_shipping' => false, + 'weight_g' => 0, + ]); + ['cart' => $cart] = makeCartWithLine($context['store'], $sku['variant']); + $this->withSession(['cart_id' => $cart->id, 'api_cart_ids' => [$cart->id]]); + $checkoutId = $this->postJson(storefrontApiUrl($context, 'checkouts'), storefrontCheckoutPayload($cart->id, 'reader@example.test')) + ->assertCreated()->json('id'); + + $this->putJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/address"), []) + ->assertOk() + ->assertJsonPath('status', 'addressed') + ->assertJsonPath('shipping_address_json', null) + ->assertJsonCount(0, 'available_shipping_methods'); + $this->putJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/shipping-method"), ['shipping_method_id' => null]) + ->assertOk() + ->assertJsonPath('status', 'shipping_selected') + ->assertJsonPath('shipping_method_id', null) + ->assertJsonPath('totals.shipping', 0); + $this->putJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/payment-method"), ['payment_method' => 'paypal']) + ->assertOk(); + $this->postJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/pay"), ['payment_method' => 'paypal']) + ->assertOk() + ->assertJsonPath('order.financial_status', 'paid'); + }); + + it('completes and idempotently repeats a discounted card checkout', function () { + $context = createStoreContext(); + $sku = makeSellableVariant($context['store'], variantAttributes: ['price_amount' => 2500]); + ['cart' => $cart] = makeCartWithLine($context['store'], $sku['variant'], 2); + $this->withSession(['cart_id' => $cart->id, 'api_cart_ids' => [$cart->id]]); + $zone = ShippingZone::factory()->for($context['store'])->create(['countries_json' => ['DE']]); + $rate = ShippingRate::factory()->for($zone, 'zone')->create(['config_json' => ['amount' => 499]]); + Discount::factory()->for($context['store'])->create(['code' => 'API10', 'value_type' => 'percent', 'value_amount' => 10]); + + $created = $this->postJson(storefrontApiUrl($context, 'checkouts'), storefrontCheckoutPayload($cart->id)) + ->assertCreated() + ->assertJsonPath('status', 'started') + ->assertJsonPath('email', 'buyer@example.test'); + $checkoutId = $created->json('id'); + + $this->putJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/address"), validCheckoutAddress()) + ->assertOk() + ->assertJsonPath('status', 'addressed') + ->assertJsonPath('available_shipping_methods.0.id', $rate->id) + ->assertJsonPath('available_shipping_methods.0.price_amount', 499); + + $this->putJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/shipping-method"), ['shipping_method_id' => $rate->id]) + ->assertOk() + ->assertJsonPath('status', 'shipping_selected') + ->assertJsonPath('totals.shipping', 499); + + $this->postJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/apply-discount"), ['code' => 'api10']) + ->assertOk() + ->assertJsonPath('discount_code', 'api10') + ->assertJsonPath('totals.discount', 500); + + $this->putJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/payment-method"), ['payment_method' => 'credit_card']) + ->assertOk() + ->assertJsonPath('status', 'payment_selected'); + + $paid = $this->postJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/pay"), storefrontCardPayment('4242 4242 4242 4242')) + ->assertOk() + ->assertJsonPath('status', 'completed') + ->assertJsonPath('order.financial_status', 'paid') + ->assertJsonPath('order.total_amount', 4999) + ->assertJsonPath('next_cart.status', 'active') + ->assertJsonPath('next_cart.item_count', 0); + $orderId = $paid->json('order.id'); + $nextCartId = $paid->json('next_cart.id'); + + expect($nextCartId)->not->toBe($cart->id); + $this->getJson(storefrontApiUrl($context, "carts/{$nextCartId}")) + ->assertOk() + ->assertJsonPath('status', 'active') + ->assertJsonPath('totals.item_count', 0); + $this->get('http://'.$context['domain']->hostname.'/') + ->assertOk() + ->assertSee('cartCount: 0', false); + + $this->postJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/pay"), storefrontCardPayment('4242 4242 4242 4242')) + ->assertOk() + ->assertJsonPath('order.id', $orderId) + ->assertJsonPath('next_cart.id', $nextCartId) + ->assertJsonPath('next_cart.item_count', 0); + + expect(Order::withoutGlobalScopes()->where('store_id', $context['store']->id)->count())->toBe(1) + ->and(Payment::query()->where('order_id', $orderId)->count())->toBe(1) + ->and($sku['inventory']->refresh()->quantity_on_hand)->toBe(18) + ->and($sku['inventory']->quantity_reserved)->toBe(0); + }); + + it('returns a stable decline code and releases inventory', function () { + $context = createStoreContext(); + $sku = makeSellableVariant($context['store']); + ['cart' => $cart] = makeCartWithLine($context['store'], $sku['variant']); + $this->withSession(['cart_id' => $cart->id, 'api_cart_ids' => [$cart->id]]); + $zone = ShippingZone::factory()->for($context['store'])->create(['countries_json' => ['DE']]); + $rate = ShippingRate::factory()->for($zone, 'zone')->create(); + $checkoutId = $this->postJson(storefrontApiUrl($context, 'checkouts'), storefrontCheckoutPayload($cart->id))->json('id'); + $this->putJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/address"), validCheckoutAddress())->assertOk(); + $this->putJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/shipping-method"), ['shipping_method_id' => $rate->id])->assertOk(); + $this->putJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/payment-method"), ['payment_method' => 'credit_card'])->assertOk(); + + $this->postJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/pay"), storefrontCardPayment('4000 0000 0000 0002')) + ->assertUnprocessable() + ->assertJsonPath('error_code', 'card_declined'); + + expect($sku['inventory']->refresh()->quantity_reserved)->toBe(0) + ->and(Order::withoutGlobalScopes()->where('store_id', $context['store']->id)->count())->toBe(0); + }); + + it('translates invalid checkout domain input into 422 responses', function () { + $context = createStoreContext(); + $empty = app(CartService::class)->create($context['store']); + $this->withSession(['cart_id' => $empty->id, 'api_cart_ids' => [$empty->id]]); + + $this->postJson(storefrontApiUrl($context, 'checkouts'), storefrontCheckoutPayload($empty->id)) + ->assertUnprocessable() + ->assertJsonValidationErrors('cart_id'); + + $sku = makeSellableVariant($context['store']); + ['cart' => $cart] = makeCartWithLine($context['store'], $sku['variant']); + $this->withSession(['cart_id' => $cart->id, 'api_cart_ids' => [$empty->id, $cart->id]]); + $this->postJson(storefrontApiUrl($context, 'checkouts'), ['cart_id' => $cart->id]) + ->assertUnprocessable() + ->assertJsonValidationErrors('email'); + $checkoutId = $this->postJson(storefrontApiUrl($context, 'checkouts'), storefrontCheckoutPayload($cart->id))->json('id'); + $this->putJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/address"), [ + 'email' => 'buyer@example.test', + 'shipping_address' => [ + 'first_name' => 'Ada', 'last_name' => 'Lovelace', 'address1' => 'Street 1', + 'country_code' => 'DE', 'postal_code' => '10115', + ], + ])->assertUnprocessable()->assertJsonValidationErrors('shipping_address.city'); + + $this->putJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/address"), validCheckoutAddress()) + ->assertUnprocessable() + ->assertJsonValidationErrors('shipping_address'); + + $zone = ShippingZone::factory()->for($context['store'])->create(['countries_json' => ['DE']]); + ShippingRate::factory()->for($zone, 'zone')->create(); + $this->putJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/address"), validCheckoutAddress())->assertOk(); + $wrongZone = ShippingZone::factory()->for($context['store'])->create(['countries_json' => ['US']]); + $wrongRate = ShippingRate::factory()->for($wrongZone, 'zone')->create(); + $this->putJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/shipping-method"), ['shipping_method_id' => $wrongRate->id]) + ->assertUnprocessable() + ->assertJsonValidationErrors('shipping_method_id'); + + $this->postJson(storefrontApiUrl($context, "checkouts/{$checkoutId}/apply-discount"), ['code' => 'MISSING']) + ->assertUnprocessable() + ->assertJsonPath('error_code', 'not_found'); + }); +}); + +describe('storefront search analytics and order API', function () { + it('searches suggests validates and rate limits queries', function () { + $context = createStoreContext(); + Product::factory()->for($context['store'])->create(['title' => 'Summer Cotton Dress', 'handle' => 'summer-cotton-dress']); + + $this->getJson(storefrontApiUrl($context, 'search?q=cott')) + ->assertOk() + ->assertJsonPath('pagination.total', 1) + ->assertJsonPath('results.0.title', 'Summer Cotton Dress'); + $this->getJson(storefrontApiUrl($context, 'search/suggest?q=sum')) + ->assertOk() + ->assertJsonPath('suggestions.0.title', 'Summer Cotton Dress'); + $this->getJson(storefrontApiUrl($context, 'search/suggest?q=a'))->assertOk(); + $this->getJson(storefrontApiUrl($context, 'search/suggest?q=')) + ->assertUnprocessable()->assertJsonValidationErrors('q'); + + $this->withServerVariables(['REMOTE_ADDR' => '198.51.100.10']); + foreach (range(1, 30) as $attempt) { + $this->getJson(storefrontApiUrl($context, 'search?q=none'))->assertOk(); + } + $this->getJson(storefrontApiUrl($context, 'search?q=none'))->assertTooManyRequests(); + }); + + it('accepts analytics batches deduplicates events and validates shape', function () { + $context = createStoreContext(); + $customer = Customer::factory()->for($context['store'])->create(); + actingAsCustomer($customer); + $event = [ + 'type' => 'add_to_cart', + 'session_id' => 'session-api', + 'client_event_id' => 'event-api-1', + 'occurred_at' => now()->toIso8601String(), + 'properties' => ['variant_id' => 42], + ]; + + $this->postJson(storefrontApiUrl($context, 'analytics/events'), ['events' => [$event, $event]]) + ->assertStatus(202) + ->assertJsonPath('accepted', 1) + ->assertJsonPath('rejected', 1); + + $stored = AnalyticsEvent::withoutGlobalScopes()->where('store_id', $context['store']->id)->firstOrFail(); + expect(AnalyticsEvent::withoutGlobalScopes()->where('store_id', $context['store']->id)->count())->toBe(1) + ->and($stored->customer_id)->toBe($customer->id) + ->and($stored->properties_json)->toBe(['variant_id' => 42]); + + $this->postJson(storefrontApiUrl($context, 'analytics/events'), ['events' => [[ + 'type' => 'not_supported', + 'session_id' => 'x', + 'client_event_id' => 'bad', + 'occurred_at' => 'not-a-date', + ]]])->assertUnprocessable()->assertJsonValidationErrors(['events.0.type', 'events.0.occurred_at']); + }); + + it('requires a signed order token and scopes the order to its tenant', function () { + $context = createStoreContext(); + $order = Order::factory()->for($context['store'])->create([ + 'customer_id' => null, + 'order_number' => '#API1001', + 'email' => 'buyer@example.test', + ]); + OrderLine::factory()->for($order)->create(['product_id' => null, 'variant_id' => null, 'title_snapshot' => 'Historic Item']); + $token = hash_hmac('sha256', $order->order_number.'|'.$order->email, (string) config('app.key')); + $number = rawurlencode($order->order_number); + + $this->getJson(storefrontApiUrl($context, "orders/{$number}"))->assertUnauthorized(); + $this->getJson(storefrontApiUrl($context, "orders/{$number}?token=wrong"))->assertUnauthorized(); + $this->getJson(storefrontApiUrl($context, "orders/{$number}?token={$token}")) + ->assertOk() + ->assertJsonPath('order_number', '#API1001') + ->assertJsonPath('lines.0.title_snapshot', 'Historic Item'); + + $other = createStoreContext(); + $this->getJson(storefrontApiUrl($other, "orders/{$number}?token={$token}"))->assertNotFound(); + }); +}); diff --git a/tests/Feature/Shop/TenancyAndAuthorizationTest.php b/tests/Feature/Shop/TenancyAndAuthorizationTest.php new file mode 100644 index 00000000..6091d190 --- /dev/null +++ b/tests/Feature/Shop/TenancyAndAuthorizationTest.php @@ -0,0 +1,216 @@ +getJson("http://{$context['domain']->hostname}/api/storefront/v1/search?q=unfindable") + ->assertOk() + ->assertJsonPath('pagination.total', 0); + + expect(app('current_store')->is($context['store']))->toBeTrue() + ->and(Cache::get('store_domain:'.$context['domain']->hostname))->toBe($context['store']->id); + }); + + it('rejects unknown and suspended storefront hosts', function () { + $this->getJson('http://missing-shop.test/api/storefront/v1/search?q=anything') + ->assertNotFound(); + + $context = createStoreContext(storeAttributes: ['status' => 'suspended']); + $this->getJson("http://{$context['domain']->hostname}/api/storefront/v1/search?q=anything") + ->assertStatus(503); + }); + + it('scopes reads creates and direct identifiers to the current store', function () { + $storeA = createStoreContext()['store']; + Product::factory()->count(3)->for($storeA)->create(); + Order::factory()->count(2)->for($storeA)->create(['customer_id' => null]); + + $storeB = Store::factory()->create(); + $foreign = Product::factory()->count(5)->for($storeB)->create()->first(); + Order::factory()->count(4)->for($storeB)->create(['customer_id' => null]); + + bindStore($storeA); + $created = Product::query()->create([ + 'title' => 'Scoped Product', + 'handle' => 'scoped-product', + 'status' => 'draft', + 'tags' => [], + ]); + + expect(Product::query()->count())->toBe(4) + ->and(Order::query()->count())->toBe(2) + ->and($created->store_id)->toBe($storeA->id) + ->and(Product::query()->find($foreign->id))->toBeNull() + ->and(Product::withoutGlobalScope(StoreScope::class)->count())->toBe(9); + }); + + it('allows one customer email per store, not globally', function () { + $storeA = createStoreContext()['store']; + $storeB = createStoreContext()['store']; + $service = app(CustomerService::class); + + $customerA = $service->register($storeA, [ + 'name' => 'Same Buyer', + 'email' => 'same@example.test', + 'password' => 'correct-horse', + ]); + $customerB = $service->register($storeB, [ + 'name' => 'Same Buyer', + 'email' => 'same@example.test', + 'password' => 'correct-horse', + ]); + + expect($customerA->store_id)->not->toBe($customerB->store_id) + ->and(Customer::withoutGlobalScopes()->where('email', 'same@example.test')->count())->toBe(2); + + $service->register($storeA, [ + 'name' => 'Duplicate', + 'email' => 'same@example.test', + 'password' => 'correct-horse', + ]); + })->throws(ValidationException::class); +}); + +describe('role policies', function () { + it('returns the typed role for a store and null elsewhere', function () { + $context = createStoreContext(StoreUserRole::Staff); + $unrelated = Store::factory()->create(); + + expect($context['user']->roleForStore($context['store']))->toBe(StoreUserRole::Staff) + ->and($context['user']->roleForStore($unrelated))->toBeNull(); + }); + + it('enforces product permissions for every store role', function (string $role, bool $create, bool $delete) { + $context = createStoreContext($role); + $product = Product::factory()->for($context['store'])->create(); + $policy = new ProductPolicy; + + expect($policy->view($context['user'], $product))->toBeTrue() + ->and($policy->create($context['user']))->toBe($create) + ->and($policy->update($context['user'], $product))->toBe($create) + ->and($policy->delete($context['user'], $product))->toBe($delete); + })->with([ + 'owner' => ['owner', true, true], + 'admin' => ['admin', true, true], + 'staff' => ['staff', true, false], + 'support' => ['support', false, false], + ]); + + it('enforces order refund and fulfillment permissions for every store role', function (string $role, bool $mutate, bool $refund) { + $context = createStoreContext($role); + $order = Order::factory()->for($context['store'])->create(['customer_id' => null]); + $orders = new OrderPolicy; + $refunds = new RefundPolicy; + + expect($orders->view($context['user'], $order))->toBeTrue() + ->and($orders->update($context['user'], $order))->toBe($mutate) + ->and($orders->fulfill($context['user'], $order))->toBe($mutate) + ->and($orders->refund($context['user'], $order))->toBe($refund) + ->and($refunds->create($context['user'], $order))->toBe($refund); + })->with([ + 'owner' => ['owner', true, true], + 'admin' => ['admin', true, true], + 'staff' => ['staff', true, false], + 'support read-only' => ['support', false, false], + ]); + + it('restricts settings staff management and store deletion', function (string $role, bool $settings, bool $delete) { + $context = createStoreContext($role); + $policy = new StorePolicy; + + expect($policy->view($context['user'], $context['store']))->toBeTrue() + ->and($policy->update($context['user'], $context['store']))->toBe($settings) + ->and($policy->manageStaff($context['user'], $context['store']))->toBe($settings) + ->and($policy->delete($context['user'], $context['store']))->toBe($delete); + })->with([ + 'owner' => ['owner', true, true], + 'admin' => ['admin', true, false], + 'staff' => ['staff', false, false], + 'support' => ['support', false, false], + ]); + + it('never authorizes a resource from another tenant', function () { + $context = createStoreContext('owner'); + $other = Store::factory()->create(); + $foreignProduct = Product::factory()->for($other)->create(); + + expect((new ProductPolicy)->update($context['user'], $foreignProduct))->toBeFalse(); + }); +}); + +describe('Sanctum administration tokens', function () { + it('persists abilities and authenticates a bearer token', function () { + $context = createStoreContext(); + $token = $context['user']->createToken('qa', [ + 'read-products', + 'write-products', + 'store:'.$context['store']->id, + ]); + + expect($token->accessToken->abilities)->toBe([ + 'read-products', + 'write-products', + 'store:'.$context['store']->id, + ]) + ->and($token->plainTextToken)->toContain('|'); + + $this->withToken($token->plainTextToken) + ->getJson("/api/admin/v1/stores/{$context['store']->id}/products") + ->assertOk() + ->assertJsonStructure(['data', 'meta' => ['current_page', 'last_page', 'total']]); + }); + + it('rejects missing invalid revoked and cross-store tokens', function () { + $context = createStoreContext(); + $otherStore = Store::factory()->create(); + + $this->getJson("/api/admin/v1/stores/{$context['store']->id}/products")->assertUnauthorized(); + $this->withToken('definitely-invalid')->getJson("/api/admin/v1/stores/{$context['store']->id}/products")->assertUnauthorized(); + + $token = $context['user']->createToken('revoked', [ + 'read-products', + 'store:'.$context['store']->id, + ]); + $plain = $token->plainTextToken; + $token->accessToken->delete(); + $this->withToken($plain)->getJson("/api/admin/v1/stores/{$context['store']->id}/products")->assertUnauthorized(); + + $valid = $context['user']->createToken('valid', [ + 'read-products', + 'store:'.$context['store']->id, + ]); + $this->withToken($valid->plainTextToken) + ->getJson("/api/admin/v1/stores/{$otherStore->id}/products") + ->assertForbidden(); + }); + + it('enforces token abilities on product mutations', function () { + $context = createStoreContext(); + $readOnly = $context['user']->createToken('reader', [ + 'read-products', + 'store:'.$context['store']->id, + ]); + + $this->withToken($readOnly->plainTextToken) + ->postJson("/api/admin/v1/stores/{$context['store']->id}/products", [ + 'title' => 'Forbidden Product', + 'variant' => ['price_amount' => 1000], + ]) + ->assertForbidden(); + }); +}); diff --git a/tests/Feature/Shop/ThemeArchiveAndSearchStatusTest.php b/tests/Feature/Shop/ThemeArchiveAndSearchStatusTest.php new file mode 100644 index 00000000..b414b9ce --- /dev/null +++ b/tests/Feature/Shop/ThemeArchiveAndSearchStatusTest.php @@ -0,0 +1,355 @@ + $entries + */ +function acceptanceThemeArchive(array $entries): UploadedFile +{ + $path = tempnam(sys_get_temp_dir(), 'acceptance-theme-'); + $zip = new ZipArchive; + $result = $zip->open($path, ZipArchive::OVERWRITE); + expect($result)->toBeTrue(); + + foreach ($entries as $name => $contents) { + $zip->addFromString($name, $contents); + } + + $zip->close(); + + return new UploadedFile($path, 'theme.zip', 'application/zip', null, true); +} + +/** @return array