@decocms/start 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/.cursor/skills/deco-api-call-dedup/SKILL.md +443 -0
  2. package/.cursor/skills/deco-apps-architecture/SKILL.md +255 -0
  3. package/.cursor/skills/deco-apps-architecture/app-pattern.md +288 -0
  4. package/.cursor/skills/deco-apps-architecture/commerce-types.md +239 -0
  5. package/.cursor/skills/deco-apps-architecture/new-app-guide.md +268 -0
  6. package/.cursor/skills/deco-apps-architecture/scripts-codegen.md +148 -0
  7. package/.cursor/skills/deco-apps-architecture/shared-utils.md +181 -0
  8. package/.cursor/skills/deco-apps-architecture/vtex-deep-structure.md +253 -0
  9. package/.cursor/skills/deco-apps-architecture/website-app.md +169 -0
  10. package/.cursor/skills/deco-apps-vtex-porting/SKILL.md +189 -0
  11. package/.cursor/skills/deco-apps-vtex-porting/adaptation-patterns.md +335 -0
  12. package/.cursor/skills/deco-apps-vtex-porting/commerce-porting.md +155 -0
  13. package/.cursor/skills/deco-apps-vtex-porting/cookie-auth-patterns.md +148 -0
  14. package/.cursor/skills/deco-apps-vtex-porting/structure-map.md +234 -0
  15. package/.cursor/skills/deco-apps-vtex-porting/transform-mapping.md +99 -0
  16. package/.cursor/skills/deco-apps-vtex-porting/website-porting.md +194 -0
  17. package/.cursor/skills/deco-apps-vtex-review/SKILL.md +234 -0
  18. package/.cursor/skills/deco-async-rendering-architecture/SKILL.md +270 -0
  19. package/.cursor/skills/deco-async-rendering-site-guide/SKILL.md +417 -0
  20. package/.cursor/skills/deco-cms-layout-caching/SKILL.md +293 -0
  21. package/.cursor/skills/deco-cms-route-config/SKILL.md +388 -0
  22. package/.cursor/skills/deco-core-architecture/SKILL.md +185 -0
  23. package/.cursor/skills/deco-core-architecture/blocks.md +196 -0
  24. package/.cursor/skills/deco-core-architecture/deco-vs-deco-start.md +191 -0
  25. package/.cursor/skills/deco-core-architecture/engine.md +220 -0
  26. package/.cursor/skills/deco-core-architecture/hooks-components.md +157 -0
  27. package/.cursor/skills/deco-core-architecture/plugins-clients.md +136 -0
  28. package/.cursor/skills/deco-core-architecture/runtime.md +116 -0
  29. package/.cursor/skills/deco-core-architecture/site-usage.md +165 -0
  30. package/.cursor/skills/deco-e2e-testing/SKILL.md +372 -0
  31. package/.cursor/skills/deco-e2e-testing/discovery.md +337 -0
  32. package/.cursor/skills/deco-e2e-testing/scripts/scaffold.sh +81 -0
  33. package/.cursor/skills/deco-e2e-testing/selectors.md +175 -0
  34. package/.cursor/skills/deco-e2e-testing/templates/package.json +18 -0
  35. package/.cursor/skills/deco-e2e-testing/templates/playwright.config.ts +65 -0
  36. package/.cursor/skills/deco-e2e-testing/templates/scripts/baseline.ts +279 -0
  37. package/.cursor/skills/deco-e2e-testing/templates/scripts/run-e2e.ts +194 -0
  38. package/.cursor/skills/deco-e2e-testing/templates/specs/ecommerce-flow.spec.ts +612 -0
  39. package/.cursor/skills/deco-e2e-testing/templates/tsconfig.json +12 -0
  40. package/.cursor/skills/deco-e2e-testing/templates/utils/metrics-collector.ts +918 -0
  41. package/.cursor/skills/deco-e2e-testing/troubleshooting.md +602 -0
  42. package/.cursor/skills/deco-edge-caching/SKILL.md +316 -0
  43. package/.cursor/skills/deco-full-analysis/SKILL.md +898 -0
  44. package/.cursor/skills/deco-full-analysis/checklists/asset-optimization.md +251 -0
  45. package/.cursor/skills/deco-full-analysis/checklists/bug-fix.md +189 -0
  46. package/.cursor/skills/deco-full-analysis/checklists/cache-strategy.md +144 -0
  47. package/.cursor/skills/deco-full-analysis/checklists/dependency-update.md +150 -0
  48. package/.cursor/skills/deco-full-analysis/checklists/hydration-fix.md +191 -0
  49. package/.cursor/skills/deco-full-analysis/checklists/image-optimization.md +180 -0
  50. package/.cursor/skills/deco-full-analysis/checklists/loader-optimization.md +165 -0
  51. package/.cursor/skills/deco-full-analysis/checklists/seo-fix.md +183 -0
  52. package/.cursor/skills/deco-full-analysis/checklists/site-cleanup.md +281 -0
  53. package/.cursor/skills/deco-full-analysis/discovery.md +548 -0
  54. package/.cursor/skills/deco-incident-debugging/SKILL.md +378 -0
  55. package/.cursor/skills/deco-incident-debugging/headless-mode.md +510 -0
  56. package/.cursor/skills/deco-incident-debugging/learnings-index.md +227 -0
  57. package/.cursor/skills/deco-incident-debugging/triage-workflow.md +312 -0
  58. package/.cursor/skills/deco-islands-migration/SKILL.md +251 -0
  59. package/.cursor/skills/deco-loader-n-plus-1-detector/SKILL.md +275 -0
  60. package/.cursor/skills/deco-performance-audit/SKILL.md +530 -0
  61. package/.cursor/skills/deco-performance-audit/tools-reference.md +428 -0
  62. package/.cursor/skills/deco-performance-audit/workflow.md +457 -0
  63. package/.cursor/skills/deco-server-functions-invoke/SKILL.md +92 -0
  64. package/.cursor/skills/deco-server-functions-invoke/architecture.md +166 -0
  65. package/.cursor/skills/deco-server-functions-invoke/generator.md +122 -0
  66. package/.cursor/skills/deco-server-functions-invoke/problem.md +98 -0
  67. package/.cursor/skills/deco-server-functions-invoke/troubleshooting.md +110 -0
  68. package/.cursor/skills/deco-site-deployment/SKILL.md +396 -0
  69. package/.cursor/skills/deco-site-memory-debugging/SKILL.md +121 -0
  70. package/.cursor/skills/deco-site-memory-debugging/cdp-connection.md +222 -0
  71. package/.cursor/skills/deco-site-memory-debugging/memory-analysis.md +362 -0
  72. package/.cursor/skills/deco-site-patterns/SKILL.md +124 -0
  73. package/.cursor/skills/deco-site-patterns/app-composition.md +337 -0
  74. package/.cursor/skills/deco-site-patterns/client-patterns.md +341 -0
  75. package/.cursor/skills/deco-site-patterns/cms-wiring.md +230 -0
  76. package/.cursor/skills/deco-site-patterns/section-patterns.md +340 -0
  77. package/.cursor/skills/deco-site-scaling-tuning/SKILL.md +240 -0
  78. package/.cursor/skills/deco-site-scaling-tuning/analysis-scripts.md +267 -0
  79. package/.cursor/skills/deco-start-architecture/SKILL.md +218 -0
  80. package/.cursor/skills/deco-start-architecture/admin-protocol.md +156 -0
  81. package/.cursor/skills/deco-start-architecture/cms-resolution.md +201 -0
  82. package/.cursor/skills/deco-start-architecture/code-quality.md +158 -0
  83. package/.cursor/skills/deco-start-architecture/gap-analysis.md +129 -0
  84. package/.cursor/skills/deco-start-architecture/sdk-utilities.md +197 -0
  85. package/.cursor/skills/deco-start-architecture/worker-entry-caching.md +154 -0
  86. package/.cursor/skills/deco-startup-analysis/SKILL.md +248 -0
  87. package/.cursor/skills/deco-storefront-test-checklist/SKILL.md +369 -0
  88. package/.cursor/skills/deco-tanstack-hydration-fixes/SKILL.md +468 -0
  89. package/.cursor/skills/deco-tanstack-navigation/SKILL.md +681 -0
  90. package/.cursor/skills/deco-tanstack-search/SKILL.md +411 -0
  91. package/.cursor/skills/deco-tanstack-storefront-patterns/SKILL.md +1013 -0
  92. package/.cursor/skills/deco-to-tanstack-migration/SKILL.md +518 -0
  93. package/.cursor/skills/deco-to-tanstack-migration/references/codemod-commands.md +174 -0
  94. package/.cursor/skills/deco-to-tanstack-migration/references/commerce/README.md +78 -0
  95. package/.cursor/skills/deco-to-tanstack-migration/references/deco-framework/README.md +128 -0
  96. package/.cursor/skills/deco-to-tanstack-migration/references/gotchas.md +719 -0
  97. package/.cursor/skills/deco-to-tanstack-migration/references/imports/README.md +70 -0
  98. package/.cursor/skills/deco-to-tanstack-migration/references/platform-hooks/README.md +154 -0
  99. package/.cursor/skills/deco-to-tanstack-migration/references/signals/README.md +220 -0
  100. package/.cursor/skills/deco-to-tanstack-migration/references/vite-config/README.md +78 -0
  101. package/.cursor/skills/deco-to-tanstack-migration/templates/package-json.md +55 -0
  102. package/.cursor/skills/deco-to-tanstack-migration/templates/root-route.md +110 -0
  103. package/.cursor/skills/deco-to-tanstack-migration/templates/router.md +96 -0
  104. package/.cursor/skills/deco-to-tanstack-migration/templates/setup-ts.md +167 -0
  105. package/.cursor/skills/deco-to-tanstack-migration/templates/vite-config.md +122 -0
  106. package/.cursor/skills/deco-to-tanstack-migration/templates/worker-entry.md +67 -0
  107. package/.cursor/skills/deco-typescript-fixes/SKILL.md +178 -0
  108. package/.cursor/skills/deco-typescript-fixes/common-fixes.md +330 -0
  109. package/.cursor/skills/deco-typescript-fixes/strategy.md +148 -0
  110. package/.cursor/skills/deco-variant-selection-perf/SKILL.md +272 -0
  111. package/.cursor/skills/deco-vtex-fetch-cache/SKILL.md +225 -0
  112. package/.cursor/skills/find-skills/SKILL.md +133 -0
  113. package/.cursor/skills/incident-report/SKILL.md +179 -0
  114. package/.cursor/skills/incident-report/references/5-whys.md +75 -0
  115. package/.cursor/skills/incident-report/templates/client-report.md +187 -0
  116. package/.cursor/skills/incident-report/templates/internal-report.md +206 -0
  117. package/.cursor/skills/template-skill/SKILL.md +38 -0
  118. package/.github/workflows/release.yml +32 -0
  119. package/.releaserc.json +25 -0
  120. package/CLAUDE.md +135 -0
  121. package/GAP_ANALYSIS.md +224 -0
  122. package/GAP_ANALYSIS_V2.md +1013 -0
  123. package/biome.json +39 -0
  124. package/knip.json +5 -0
  125. package/package.json +87 -0
  126. package/scripts/generate-blocks.ts +69 -0
  127. package/scripts/generate-invoke.ts +378 -0
  128. package/scripts/generate-schema.ts +657 -0
  129. package/src/admin/cors.ts +29 -0
  130. package/src/admin/decofile.ts +72 -0
  131. package/src/admin/index.ts +24 -0
  132. package/src/admin/invoke.ts +163 -0
  133. package/src/admin/liveControls.ts +29 -0
  134. package/src/admin/meta.ts +70 -0
  135. package/src/admin/render.ts +205 -0
  136. package/src/admin/schema.ts +686 -0
  137. package/src/admin/setup.ts +44 -0
  138. package/src/cms/index.ts +59 -0
  139. package/src/cms/loader.ts +180 -0
  140. package/src/cms/registry.ts +162 -0
  141. package/src/cms/resolve.ts +1005 -0
  142. package/src/cms/sectionLoaders.ts +294 -0
  143. package/src/hooks/DecoPageRenderer.tsx +444 -0
  144. package/src/hooks/LazySection.tsx +109 -0
  145. package/src/hooks/LiveControls.tsx +108 -0
  146. package/src/hooks/SectionErrorFallback.tsx +85 -0
  147. package/src/hooks/index.ts +8 -0
  148. package/src/index.ts +5 -0
  149. package/src/matchers/builtins.ts +184 -0
  150. package/src/matchers/posthog.ts +154 -0
  151. package/src/middleware/decoState.ts +55 -0
  152. package/src/middleware/healthMetrics.ts +131 -0
  153. package/src/middleware/index.ts +80 -0
  154. package/src/middleware/liveness.ts +21 -0
  155. package/src/middleware/observability.ts +205 -0
  156. package/src/routes/adminRoutes.ts +83 -0
  157. package/src/routes/cmsRoute.ts +302 -0
  158. package/src/routes/components.tsx +34 -0
  159. package/src/routes/index.ts +15 -0
  160. package/src/sdk/analytics.ts +72 -0
  161. package/src/sdk/cacheHeaders.ts +268 -0
  162. package/src/sdk/cachedLoader.ts +206 -0
  163. package/src/sdk/clx.ts +3 -0
  164. package/src/sdk/cookie.ts +39 -0
  165. package/src/sdk/createInvoke.ts +57 -0
  166. package/src/sdk/csp.ts +59 -0
  167. package/src/sdk/env.ts +27 -0
  168. package/src/sdk/index.ts +63 -0
  169. package/src/sdk/instrumentedFetch.ts +137 -0
  170. package/src/sdk/invoke.ts +133 -0
  171. package/src/sdk/mergeCacheControl.ts +150 -0
  172. package/src/sdk/redirects.ts +217 -0
  173. package/src/sdk/requestContext.ts +184 -0
  174. package/src/sdk/serverTimings.ts +68 -0
  175. package/src/sdk/signal.ts +41 -0
  176. package/src/sdk/sitemap.ts +143 -0
  177. package/src/sdk/urlUtils.ts +117 -0
  178. package/src/sdk/useDevice.ts +82 -0
  179. package/src/sdk/useId.ts +7 -0
  180. package/src/sdk/useScript.ts +101 -0
  181. package/src/sdk/workerEntry.ts +703 -0
  182. package/src/sdk/wrapCaughtErrors.ts +107 -0
  183. package/src/types/index.ts +39 -0
  184. package/src/types/widgets.ts +13 -0
  185. package/tsconfig.json +13 -0
@@ -0,0 +1,612 @@
1
+ import { test, expect, type Page, type TestInfo } from '@playwright/test'
2
+ import { MetricsCollector, formatLoaderTimings, formatLazyRenderAnalysis, type PageMetrics } from '../utils/metrics-collector'
3
+ import * as fs from 'fs'
4
+
5
+ /**
6
+ * Site Configuration
7
+ * ==================
8
+ * CUSTOMIZE THESE VALUES FOR YOUR SITE
9
+ * See discovery.md for how to find each value
10
+ */
11
+ const SITE_CONFIG = {
12
+ // URLs - REPLACE THESE
13
+ baseUrl: process.env.SITE_URL || 'https://localhost--{{SITE_NAME}}.deco.site',
14
+ plpPath: '{{PLP_PATH}}', // e.g., '/feminino' or '/utilidades-domesticas'
15
+ fallbackPdpPath: '{{FALLBACK_PDP_PATH}}', // e.g., '/product-name/p'
16
+
17
+ // Deco framework endpoints
18
+ livenessPath: '/deco/_liveness',
19
+
20
+ // Debug mode - ALWAYS enabled to get Server-Timing headers with loader info
21
+ debugParam: '?__d',
22
+
23
+ // Selectors - REPLACE THESE
24
+ productCard: '{{PRODUCT_CARD_SELECTOR}}', // e.g., '[data-deco="view-product"]'
25
+ productCardFallback: 'a:has-text("R$")',
26
+ pdpUrlPattern: /\/p/,
27
+ buyButton: '{{BUY_BUTTON_SELECTOR}}', // e.g., 'button:has-text("Comprar agora")'
28
+ buyButtonFallback: 'button:has-text("Comprar")',
29
+ minicartText: '{{MINICART_TEXT}}', // e.g., 'Produtos Adicionados'
30
+
31
+ // Size selection (fashion stores)
32
+ sizes: {{SIZES_ARRAY}}, // e.g., ['P', 'M', 'G', 'GG']
33
+ sizeButton: (size: string) => `li button:has-text("${size}")`,
34
+
35
+ // Voltage selection (electronics stores)
36
+ voltages: ['110V', '127V', '220V', 'Bivolt'],
37
+ voltageSelector: (voltage: string) => `button:has-text("${voltage}")`,
38
+
39
+ // Thresholds (ms) - adjust based on site performance
40
+ thresholds: {
41
+ coldTTFB: 5000,
42
+ warmTTFB: 2000,
43
+ homeTTFB: 3000,
44
+ homeWarmTTFB: 1500,
45
+ },
46
+
47
+ // Server warmup settings
48
+ warmup: {
49
+ livenessRetries: 30,
50
+ livenessRetryDelay: 1000,
51
+ warmupTimeout: 60000,
52
+ },
53
+ }
54
+
55
+ /**
56
+ * Server Warmup Utility
57
+ */
58
+ async function waitForServerReady(baseUrl: string): Promise<{ livenessTime: number; warmupTime: number }> {
59
+ const { livenessRetries, livenessRetryDelay, warmupTimeout } = SITE_CONFIG.warmup
60
+
61
+ console.log('\n⏳ Waiting for server liveness...')
62
+ const livenessStart = Date.now()
63
+ let livenessOk = false
64
+
65
+ for (let i = 0; i < livenessRetries; i++) {
66
+ try {
67
+ const res = await fetch(`${baseUrl}${SITE_CONFIG.livenessPath}`, {
68
+ signal: AbortSignal.timeout(5000),
69
+ })
70
+ if (res.ok) {
71
+ livenessOk = true
72
+ break
73
+ }
74
+ } catch {
75
+ // Server not ready yet
76
+ }
77
+ await new Promise(r => setTimeout(r, livenessRetryDelay))
78
+ }
79
+
80
+ if (!livenessOk) {
81
+ throw new Error(`Server liveness check failed after ${livenessRetries} attempts`)
82
+ }
83
+
84
+ const livenessTime = Date.now() - livenessStart
85
+ console.log(` ✅ Server alive (${livenessTime}ms)`)
86
+
87
+ console.log(' 🔥 Warming up server (triggering lazy imports)...')
88
+ const warmupStart = Date.now()
89
+
90
+ try {
91
+ const res = await fetch(`${baseUrl}/${SITE_CONFIG.debugParam}`, {
92
+ signal: AbortSignal.timeout(warmupTimeout),
93
+ })
94
+ await res.text()
95
+ } catch (err) {
96
+ console.log(` ⚠️ Warmup request failed: ${err}`)
97
+ }
98
+
99
+ const warmupTime = Date.now() - warmupStart
100
+ console.log(` ✅ Warmup complete (${warmupTime}ms)`)
101
+
102
+ return { livenessTime, warmupTime }
103
+ }
104
+
105
+ /**
106
+ * Page Actions - Reusable browser interactions
107
+ */
108
+ class PageActions {
109
+ constructor(private page: Page) {}
110
+
111
+ private get isLocalhost(): boolean {
112
+ return SITE_CONFIG.baseUrl.includes('localhost:') || SITE_CONFIG.baseUrl.includes('127.0.0.1')
113
+ }
114
+
115
+ private withDebug(path: string): string {
116
+ const hasQuery = path.includes('?')
117
+ return path + (hasQuery ? '&__d' : SITE_CONFIG.debugParam)
118
+ }
119
+
120
+ async goto(path: string) {
121
+ await this.page.goto(this.withDebug(path), { waitUntil: 'domcontentloaded' })
122
+ const timeout = this.isLocalhost ? 5000 : 30000
123
+ await this.page.waitForLoadState('networkidle', { timeout }).catch(() => {})
124
+ }
125
+
126
+ async reload() {
127
+ await this.page.reload({ waitUntil: 'domcontentloaded' })
128
+ const timeout = this.isLocalhost ? 5000 : 30000
129
+ await this.page.waitForLoadState('networkidle', { timeout }).catch(() => {})
130
+ }
131
+
132
+ async waitForProducts(): Promise<boolean> {
133
+ let el = await this.page.waitForSelector(SITE_CONFIG.productCard, { timeout: 15000 }).catch(() => null)
134
+ if (el) return true
135
+ el = await this.page.waitForSelector(SITE_CONFIG.productCardFallback, { timeout: 5000 }).catch(() => null)
136
+ return el !== null
137
+ }
138
+
139
+ async waitForBuyButton(): Promise<boolean> {
140
+ let el = await this.page.waitForSelector(SITE_CONFIG.buyButton, { timeout: 15000 }).catch(() => null)
141
+ if (el) return true
142
+ el = await this.page.waitForSelector(SITE_CONFIG.buyButtonFallback, { timeout: 5000 }).catch(() => null)
143
+ return el !== null
144
+ }
145
+
146
+ async clickFirstProduct(): Promise<boolean> {
147
+ let products = this.page.locator(SITE_CONFIG.productCard)
148
+ let count = await products.count()
149
+
150
+ if (count === 0) {
151
+ products = this.page.locator(SITE_CONFIG.productCardFallback)
152
+ count = await products.count()
153
+ }
154
+
155
+ if (count === 0) return false
156
+
157
+ const href = await products.first().getAttribute('href')
158
+ if (href) {
159
+ await this.page.goto(this.withDebug(href), { waitUntil: 'domcontentloaded' })
160
+ } else {
161
+ await products.first().click()
162
+ }
163
+ await this.page.waitForURL(SITE_CONFIG.pdpUrlPattern, { timeout: 10000 }).catch(() => {})
164
+ const timeout = this.isLocalhost ? 5000 : 30000
165
+ await this.page.waitForLoadState('networkidle', { timeout }).catch(() => {})
166
+ return true
167
+ }
168
+
169
+ async selectSize(): Promise<string | null> {
170
+ for (const size of SITE_CONFIG.sizes) {
171
+ const btn = this.page.locator(SITE_CONFIG.sizeButton(size)).first()
172
+ if (await btn.count() > 0) {
173
+ try {
174
+ await btn.click({ timeout: 2000 })
175
+ await this.page.waitForTimeout(300)
176
+ return size
177
+ } catch { continue }
178
+ }
179
+ }
180
+ return null
181
+ }
182
+
183
+ async selectVoltage(): Promise<string | null> {
184
+ for (const voltage of SITE_CONFIG.voltages) {
185
+ const btn = this.page.locator(SITE_CONFIG.voltageSelector(voltage)).first()
186
+ if (await btn.count() > 0) {
187
+ try {
188
+ await btn.click({ timeout: 2000 })
189
+ await this.page.waitForTimeout(300)
190
+ return voltage
191
+ } catch { continue }
192
+ }
193
+ }
194
+ return null
195
+ }
196
+
197
+ async addToCart(): Promise<number> {
198
+ const start = Date.now()
199
+ let buyBtn = this.page.locator(SITE_CONFIG.buyButton).first()
200
+ if (await buyBtn.count() === 0) {
201
+ buyBtn = this.page.locator(SITE_CONFIG.buyButtonFallback).first()
202
+ }
203
+
204
+ if (await buyBtn.count() === 0) return -1
205
+
206
+ await buyBtn.click()
207
+ await Promise.race([
208
+ this.page.waitForResponse(r =>
209
+ r.url().includes('orderForm') || r.url().includes('cart') || r.url().includes('items'),
210
+ { timeout: 10000 }
211
+ ),
212
+ this.page.waitForTimeout(3000),
213
+ ]).catch(() => {})
214
+
215
+ return Date.now() - start
216
+ }
217
+
218
+ async isMinicartOpen(): Promise<boolean> {
219
+ // Multiple selectors with retry logic for robustness
220
+ const selectors = [
221
+ `text=${SITE_CONFIG.minicartText}`,
222
+ '[data-testid="minicart"]',
223
+ '.minicart',
224
+ '[class*="minicart"]',
225
+ '[class*="Minicart"]',
226
+ '[class*="cart-drawer"]',
227
+ '[class*="drawer"][class*="open"]',
228
+ ]
229
+
230
+ for (let attempt = 0; attempt < 3; attempt++) {
231
+ const timeout = 2000 + (attempt * 1000)
232
+
233
+ for (const selector of selectors) {
234
+ try {
235
+ const visible = await this.page.locator(selector).first()
236
+ .isVisible({ timeout }).catch(() => false)
237
+ if (visible) return true
238
+ } catch {}
239
+ }
240
+
241
+ if (attempt < 2) {
242
+ await this.page.waitForTimeout(500)
243
+ }
244
+ }
245
+
246
+ return false
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Test Suite
252
+ */
253
+ test.describe('E-commerce User Journey - {{SITE_NAME}}', () => {
254
+ const metrics: PageMetrics[] = []
255
+ let collector: MetricsCollector
256
+ let actions: PageActions
257
+ let projectName = 'unknown'
258
+
259
+ // biome-ignore lint/correctness/noEmptyPattern: Playwright requires this pattern
260
+ test.beforeAll(async ({}, testInfo) => {
261
+ projectName = testInfo.project.name || 'default'
262
+ const baseUrl = SITE_CONFIG.baseUrl
263
+ console.log(`\n🚀 Preparing test run against: ${baseUrl}`)
264
+
265
+ try {
266
+ const { livenessTime, warmupTime } = await waitForServerReady(baseUrl)
267
+ console.log(` 📊 Total warmup: ${livenessTime + warmupTime}ms`)
268
+ } catch (err) {
269
+ console.error(` ❌ Server warmup failed: ${err}`)
270
+ throw err
271
+ }
272
+ })
273
+
274
+ test.beforeEach(async ({ page }) => {
275
+ collector = new MetricsCollector(page)
276
+ actions = new PageActions(page)
277
+ await collector.init()
278
+ })
279
+
280
+ test.afterEach(async () => {
281
+ await collector.cleanup()
282
+ })
283
+
284
+ test.afterAll(async () => {
285
+ if (metrics.length > 0) {
286
+ const summary = {
287
+ totalPages: metrics.length,
288
+ avgTTFB: Math.round(metrics.reduce((s, m) => s + (m.performance.TTFB || 0), 0) / metrics.length),
289
+ avgFCP: Math.round(metrics.reduce((s, m) => s + (m.performance.FCP || 0), 0) / metrics.length),
290
+ totalLazyRenders: metrics.reduce((s, m) => s + m.cacheAnalysis.lazyRenders.length, 0),
291
+ totalLoaders: metrics.reduce((s, m) => s + m.serverTiming.loaders.length, 0),
292
+ cacheHits: metrics.reduce((s, m) => s + m.serverTiming.cacheStats.hit, 0),
293
+ cacheMisses: metrics.reduce((s, m) => s + m.serverTiming.cacheStats.miss, 0),
294
+ pages: metrics.map(m => ({
295
+ name: m.pageName,
296
+ ttfb: m.performance.TTFB,
297
+ fcp: m.performance.FCP,
298
+ lazyRenders: m.cacheAnalysis.lazyRenders.length,
299
+ loaders: m.serverTiming.loaders.length,
300
+ serverTime: m.serverTiming.totalServerTime,
301
+ })),
302
+ }
303
+
304
+ const report = {
305
+ project: projectName,
306
+ timestamp: new Date().toISOString(),
307
+ summary,
308
+ metrics,
309
+ }
310
+ fs.mkdirSync('./reports', { recursive: true })
311
+
312
+ const reportFile = `./reports/report-${projectName}.json`
313
+ fs.writeFileSync(reportFile, JSON.stringify(report, null, 2))
314
+ console.log(`\n📄 Report saved to ${reportFile}`)
315
+
316
+ const deviceType = projectName.includes('mobile') ? 'mobile' : 'desktop'
317
+ fs.writeFileSync(`./reports/report-latest-${deviceType}.json`, JSON.stringify(report, null, 2))
318
+ }
319
+ })
320
+
321
+ test('Home -> PLP -> PDP -> Add to Cart', async ({ page }, testInfo) => {
322
+ const device = testInfo.project.name.includes('mobile') ? '📱 Mobile' : '🖥️ Desktop'
323
+ console.log('\n' + '═'.repeat(70))
324
+ console.log(`${device} (${testInfo.project.name})`)
325
+ console.log('═'.repeat(70))
326
+
327
+ // ═══════════════════════════════════════════════════════════════════
328
+ // HOMEPAGE (Cold Cache)
329
+ // ═══════════════════════════════════════════════════════════════════
330
+ console.log('\n' + '═'.repeat(70))
331
+ console.log('🏠 HOMEPAGE (cold cache)')
332
+ console.log('═'.repeat(70))
333
+ collector.startMeasurement()
334
+ await actions.goto('/')
335
+
336
+ console.log(' 📜 Scrolling to trigger lazy renders (full)...')
337
+ const homeRendersTriggered = await collector.scrollPage({ full: true, maxTime: 20000 })
338
+ console.log(` 📜 Triggered ${homeRendersTriggered} lazy renders`)
339
+
340
+ const homeCold = await collector.collectPageMetrics('Homepage Cold')
341
+ metrics.push(homeCold)
342
+ logMetrics(homeCold)
343
+
344
+ // ═══════════════════════════════════════════════════════════════════
345
+ // HOMEPAGE (Warm Cache)
346
+ // ═══════════════════════════════════════════════════════════════════
347
+ console.log('\n' + '═'.repeat(70))
348
+ console.log('🏠 HOMEPAGE (warm cache)')
349
+ console.log('═'.repeat(70))
350
+ collector.startMeasurement()
351
+ await actions.reload()
352
+ await collector.scrollPage({ full: true, maxTime: 15000 })
353
+
354
+ const homeWarm = await collector.collectPageMetrics('Homepage Warm')
355
+ metrics.push(homeWarm)
356
+ logMetrics(homeWarm)
357
+ logCacheImprovement('Homepage', homeCold, homeWarm)
358
+
359
+ // ═══════════════════════════════════════════════════════════════════
360
+ // PLP (Cold Cache)
361
+ // ═══════════════════════════════════════════════════════════════════
362
+ console.log('\n' + '═'.repeat(70))
363
+ console.log('📋 PLP - ' + SITE_CONFIG.plpPath + ' (cold cache)')
364
+ console.log('═'.repeat(70))
365
+ collector.startMeasurement()
366
+ await actions.goto(SITE_CONFIG.plpPath)
367
+ const hasProducts = await actions.waitForProducts()
368
+ if (!hasProducts) console.log(' ⚠️ No products loaded')
369
+
370
+ console.log(' 📜 Scrolling PLP...')
371
+ const plpRendersTriggered = await collector.scrollPage({ full: false })
372
+ console.log(` 📜 Triggered ${plpRendersTriggered} lazy renders`)
373
+
374
+ const plpCold = await collector.collectPageMetrics('PLP Cold')
375
+ metrics.push(plpCold)
376
+ logMetrics(plpCold)
377
+
378
+ // ═══════════════════════════════════════════════════════════════════
379
+ // PLP (Warm Cache)
380
+ // ═══════════════════════════════════════════════════════════════════
381
+ console.log('\n' + '═'.repeat(70))
382
+ console.log('📋 PLP - ' + SITE_CONFIG.plpPath + ' (warm cache)')
383
+ console.log('═'.repeat(70))
384
+ collector.startMeasurement()
385
+ await actions.reload()
386
+ await actions.waitForProducts()
387
+ await collector.scrollPage({ full: false })
388
+
389
+ const plpWarm = await collector.collectPageMetrics('PLP Warm')
390
+ metrics.push(plpWarm)
391
+ logMetrics(plpWarm)
392
+ logCacheImprovement('PLP', plpCold, plpWarm)
393
+
394
+ // ═══════════════════════════════════════════════════════════════════
395
+ // PDP (Cold Cache)
396
+ // ═══════════════════════════════════════════════════════════════════
397
+ console.log('\n' + '═'.repeat(70))
398
+ console.log('📦 PDP (cold cache)')
399
+ console.log('═'.repeat(70))
400
+ collector.startMeasurement()
401
+ const clickedProduct = await actions.clickFirstProduct()
402
+ if (!clickedProduct) {
403
+ console.log(' ⚠️ No products, going to fallback PDP')
404
+ await actions.goto(SITE_CONFIG.fallbackPdpPath)
405
+ }
406
+ await actions.waitForBuyButton()
407
+
408
+ console.log(' 📜 Scrolling PDP...')
409
+ const pdpRendersTriggered = await collector.scrollPage({ full: false })
410
+ console.log(` 📜 Triggered ${pdpRendersTriggered} lazy renders`)
411
+
412
+ const pdpCold = await collector.collectPageMetrics('PDP Cold')
413
+ metrics.push(pdpCold)
414
+ logMetrics(pdpCold)
415
+
416
+ // ═══════════════════════════════════════════════════════════════════
417
+ // PDP (Warm Cache)
418
+ // ═══════════════════════════════════════════════════════════════════
419
+ console.log('\n' + '═'.repeat(70))
420
+ console.log('📦 PDP (warm cache)')
421
+ console.log('═'.repeat(70))
422
+ collector.startMeasurement()
423
+ await actions.reload()
424
+ await actions.waitForBuyButton()
425
+ await collector.scrollPage({ full: false })
426
+
427
+ const pdpWarm = await collector.collectPageMetrics('PDP Warm')
428
+ metrics.push(pdpWarm)
429
+ logMetrics(pdpWarm)
430
+ logCacheImprovement('PDP', pdpCold, pdpWarm)
431
+
432
+ // ═══════════════════════════════════════════════════════════════════
433
+ // ADD TO CART
434
+ // ═══════════════════════════════════════════════════════════════════
435
+ console.log('\n' + '═'.repeat(70))
436
+ console.log('🛒 ADD TO CART')
437
+ console.log('═'.repeat(70))
438
+ collector.startMeasurement()
439
+
440
+ // Try size (fashion) or voltage (electronics)
441
+ const selectedSize = await actions.selectSize()
442
+ if (selectedSize) {
443
+ console.log(` 📐 Size selected: ${selectedSize}`)
444
+ } else {
445
+ const selectedVoltage = await actions.selectVoltage()
446
+ if (selectedVoltage) {
447
+ console.log(` ⚡ Voltage selected: ${selectedVoltage}`)
448
+ }
449
+ }
450
+
451
+ const cartTime = await actions.addToCart()
452
+ console.log(cartTime > 0 ? ` ⏱️ Cart response: ${cartTime}ms` : ' ⚠️ Buy button not found')
453
+
454
+ const cartMetrics = await collector.collectPageMetrics('Add to Cart')
455
+ metrics.push(cartMetrics)
456
+
457
+ // ═══════════════════════════════════════════════════════════════════
458
+ // MINICART VERIFICATION
459
+ // ═══════════════════════════════════════════════════════════════════
460
+ console.log('\n' + '─'.repeat(70))
461
+ console.log('🛍️ Minicart Verification')
462
+ console.log('─'.repeat(70))
463
+ const minicartOpen = await actions.isMinicartOpen()
464
+ console.log(minicartOpen ? ' ✅ Minicart opened successfully' : ' ❌ Minicart not visible')
465
+
466
+ // ═══════════════════════════════════════════════════════════════════
467
+ // PERFORMANCE SUMMARY
468
+ // ═══════════════════════════════════════════════════════════════════
469
+ console.log('\n' + '═'.repeat(70))
470
+ console.log('📊 PERFORMANCE SUMMARY')
471
+ console.log('═'.repeat(70))
472
+ printSummaryTable(metrics)
473
+
474
+ // ═══════════════════════════════════════════════════════════════════
475
+ // CACHE ANALYSIS SUMMARY
476
+ // ═══════════════════════════════════════════════════════════════════
477
+ console.log('\n' + '═'.repeat(70))
478
+ console.log('🗄️ CACHE ANALYSIS')
479
+ console.log('═'.repeat(70))
480
+ printCacheAnalysis(metrics)
481
+
482
+ // ═══════════════════════════════════════════════════════════════════
483
+ // ASSERTIONS
484
+ // ═══════════════════════════════════════════════════════════════════
485
+ console.log('\n' + '─'.repeat(70))
486
+ console.log('✅ Performance Assertions')
487
+ console.log('─'.repeat(70))
488
+
489
+ expect(homeCold.performance.TTFB).toBeLessThan(SITE_CONFIG.thresholds.homeTTFB)
490
+ expect(plpCold.performance.TTFB).toBeLessThan(SITE_CONFIG.thresholds.coldTTFB)
491
+ expect(pdpCold.performance.TTFB).toBeLessThan(SITE_CONFIG.thresholds.coldTTFB)
492
+ console.log(' ✅ Cold cache within thresholds')
493
+
494
+ expect(homeWarm.performance.TTFB).toBeLessThan(SITE_CONFIG.thresholds.homeWarmTTFB)
495
+ expect(plpWarm.performance.TTFB).toBeLessThan(SITE_CONFIG.thresholds.warmTTFB)
496
+ expect(pdpWarm.performance.TTFB).toBeLessThan(SITE_CONFIG.thresholds.warmTTFB)
497
+ console.log(' ✅ Warm cache within thresholds')
498
+
499
+ expect(minicartOpen, 'Minicart should be visible after adding product to cart').toBe(true)
500
+ console.log(' ✅ Minicart functionality verified')
501
+
502
+ console.log('\n' + '═'.repeat(70))
503
+ })
504
+ })
505
+
506
+ // ═══════════════════════════════════════════════════════════════════════════
507
+ // HELPERS
508
+ // ═══════════════════════════════════════════════════════════════════════════
509
+
510
+ function logMetrics(m: PageMetrics) {
511
+ if (m.decoHeaders?.page || m.decoHeaders?.route) {
512
+ console.log(` 📦 Block: ${m.decoHeaders.page || 'unknown'} │ Route: ${m.decoHeaders.route || 'unknown'}`)
513
+ }
514
+
515
+ console.log('')
516
+ const ttfbIcon = (m.performance.TTFB || 0) < 500 ? '🟢' : (m.performance.TTFB || 0) < 800 ? '🟡' : '🔴'
517
+ const fcpIcon = (m.performance.FCP || 0) < 1000 ? '🟢' : (m.performance.FCP || 0) < 1800 ? '🟡' : '🔴'
518
+ console.log(` ${ttfbIcon} TTFB: ${formatMs(m.performance.TTFB)} ${fcpIcon} FCP: ${formatMs(m.performance.FCP)} │ 🌐 ${m.network.totalRequests} requests (${m.network.totalBytesFormatted})`)
519
+
520
+ console.log('')
521
+ const loaderLines = formatLoaderTimings(m.serverTiming)
522
+ for (const line of loaderLines) {
523
+ console.log(line)
524
+ }
525
+
526
+ console.log('')
527
+ const cacheLines = formatLazyRenderAnalysis(m.cacheAnalysis)
528
+ for (const line of cacheLines) {
529
+ console.log(line)
530
+ }
531
+ }
532
+
533
+ function formatMs(value: number | null): string {
534
+ if (value === null) return 'N/A'.padStart(7)
535
+ return `${value.toFixed(0)}ms`.padStart(7)
536
+ }
537
+
538
+ function logCacheImprovement(name: string, cold: PageMetrics, warm: PageMetrics) {
539
+ const ttfbDiff = (cold.performance.TTFB || 0) - (warm.performance.TTFB || 0)
540
+ const serverDiff = cold.serverTiming.totalServerTime - warm.serverTiming.totalServerTime
541
+ const ttfbPercent = cold.performance.TTFB ? ((ttfbDiff / cold.performance.TTFB) * 100).toFixed(0) : '0'
542
+ const serverPercent = cold.serverTiming.totalServerTime ? ((serverDiff / cold.serverTiming.totalServerTime) * 100).toFixed(0) : '0'
543
+
544
+ console.log('')
545
+ console.log(' 🔄 Cache Improvement')
546
+ console.log(' ─────────────────────────────────────────────────────────────')
547
+ console.log(` │ TTFB: ${cold.performance.TTFB?.toFixed(0)}ms → ${warm.performance.TTFB?.toFixed(0)}ms (${ttfbDiff > 0 ? '-' : '+'}${Math.abs(ttfbDiff).toFixed(0)}ms / ${ttfbPercent}%)`)
548
+ console.log(` │ Server: ${cold.serverTiming.totalServerTime.toFixed(0)}ms → ${warm.serverTiming.totalServerTime.toFixed(0)}ms (${serverDiff > 0 ? '-' : '+'}${Math.abs(serverDiff).toFixed(0)}ms / ${serverPercent}%)`)
549
+
550
+ const coldLazy = cold.cacheAnalysis.lazyRenders.length
551
+ const warmLazyCached = warm.cacheAnalysis.lazyRendersCached
552
+ if (coldLazy > 0) {
553
+ console.log(` │ Lazy: ${coldLazy} renders → ${warmLazyCached} cached on reload`)
554
+ }
555
+ }
556
+
557
+ function printSummaryTable(metrics: PageMetrics[]) {
558
+ console.log('')
559
+ console.log(' ┌──────────────────┬─────────────┬─────────────┬────────┐')
560
+ console.log(' │ Page │ TTFB │ FCP │ Lazy │')
561
+ console.log(' ├──────────────────┼─────────────┼─────────────┼────────┤')
562
+
563
+ for (const m of metrics) {
564
+ if (m.pageName === 'Add to Cart') continue
565
+ const name = m.pageName.padEnd(16)
566
+ const ttfbVal = m.performance.TTFB || 0
567
+ const fcpVal = m.performance.FCP || 0
568
+ const ttfbIcon = ttfbVal < 500 ? '🟢' : ttfbVal < 800 ? '🟡' : '🔴'
569
+ const fcpIcon = fcpVal < 1000 ? '🟢' : fcpVal < 1800 ? '🟡' : '🔴'
570
+ const ttfb = `${ttfbIcon} ${formatMs(m.performance.TTFB)}`
571
+ const fcp = `${fcpIcon} ${formatMs(m.performance.FCP)}`
572
+ const lazy = `${m.cacheAnalysis.lazyRenders.length}`.padStart(6)
573
+ console.log(` │ ${name} │ ${ttfb} │ ${fcp} │ ${lazy} │`)
574
+ }
575
+
576
+ console.log(' └──────────────────┴─────────────┴─────────────┴────────┘')
577
+ console.log('')
578
+ console.log(' Legend: 🟢 Good 🟡 Needs Work 🔴 Poor')
579
+ console.log(' Thresholds: TTFB <500ms good, <800ms ok | FCP <1000ms good, <1800ms ok')
580
+ }
581
+
582
+ function printCacheAnalysis(metrics: PageMetrics[]) {
583
+ console.log('')
584
+ console.log(' 📄 Page Cache Status:')
585
+ console.log(' ─────────────────────────────────────────────────────────────')
586
+ for (const m of metrics) {
587
+ if (m.pageName === 'Add to Cart') continue
588
+ const icon = m.cacheAnalysis.pageCached ? '✅' : '❌'
589
+ const name = m.pageName.padEnd(18)
590
+ const status = m.cacheAnalysis.pageCached ? 'CACHED' : 'NOT CACHED'
591
+ console.log(` │ ${icon} ${name} ${status}`)
592
+ }
593
+
594
+ console.log('')
595
+ console.log(' 🔄 Lazy Render (/deco/render) Summary:')
596
+ console.log(' ─────────────────────────────────────────────────────────────')
597
+ for (const m of metrics) {
598
+ if (m.pageName === 'Add to Cart') continue
599
+ if (m.cacheAnalysis.lazyRenders.length === 0) continue
600
+
601
+ const name = m.pageName.padEnd(18)
602
+ const total = m.cacheAnalysis.lazyRenders.length
603
+ const cached = m.cacheAnalysis.lazyRendersCached
604
+ const uncached = m.cacheAnalysis.lazyRendersUncached
605
+ const icon = uncached === 0 ? '✅' : uncached < total / 2 ? '⚠️' : '❌'
606
+
607
+ console.log(` │ ${icon} ${name} ${total} renders (${cached} cached, ${uncached} uncached)`)
608
+ }
609
+
610
+ console.log('')
611
+ console.log(' ✅ No cache warnings detected!')
612
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "types": ["node"]
10
+ },
11
+ "include": ["**/*.ts"]
12
+ }