@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.
- package/.cursor/skills/deco-api-call-dedup/SKILL.md +443 -0
- package/.cursor/skills/deco-apps-architecture/SKILL.md +255 -0
- package/.cursor/skills/deco-apps-architecture/app-pattern.md +288 -0
- package/.cursor/skills/deco-apps-architecture/commerce-types.md +239 -0
- package/.cursor/skills/deco-apps-architecture/new-app-guide.md +268 -0
- package/.cursor/skills/deco-apps-architecture/scripts-codegen.md +148 -0
- package/.cursor/skills/deco-apps-architecture/shared-utils.md +181 -0
- package/.cursor/skills/deco-apps-architecture/vtex-deep-structure.md +253 -0
- package/.cursor/skills/deco-apps-architecture/website-app.md +169 -0
- package/.cursor/skills/deco-apps-vtex-porting/SKILL.md +189 -0
- package/.cursor/skills/deco-apps-vtex-porting/adaptation-patterns.md +335 -0
- package/.cursor/skills/deco-apps-vtex-porting/commerce-porting.md +155 -0
- package/.cursor/skills/deco-apps-vtex-porting/cookie-auth-patterns.md +148 -0
- package/.cursor/skills/deco-apps-vtex-porting/structure-map.md +234 -0
- package/.cursor/skills/deco-apps-vtex-porting/transform-mapping.md +99 -0
- package/.cursor/skills/deco-apps-vtex-porting/website-porting.md +194 -0
- package/.cursor/skills/deco-apps-vtex-review/SKILL.md +234 -0
- package/.cursor/skills/deco-async-rendering-architecture/SKILL.md +270 -0
- package/.cursor/skills/deco-async-rendering-site-guide/SKILL.md +417 -0
- package/.cursor/skills/deco-cms-layout-caching/SKILL.md +293 -0
- package/.cursor/skills/deco-cms-route-config/SKILL.md +388 -0
- package/.cursor/skills/deco-core-architecture/SKILL.md +185 -0
- package/.cursor/skills/deco-core-architecture/blocks.md +196 -0
- package/.cursor/skills/deco-core-architecture/deco-vs-deco-start.md +191 -0
- package/.cursor/skills/deco-core-architecture/engine.md +220 -0
- package/.cursor/skills/deco-core-architecture/hooks-components.md +157 -0
- package/.cursor/skills/deco-core-architecture/plugins-clients.md +136 -0
- package/.cursor/skills/deco-core-architecture/runtime.md +116 -0
- package/.cursor/skills/deco-core-architecture/site-usage.md +165 -0
- package/.cursor/skills/deco-e2e-testing/SKILL.md +372 -0
- package/.cursor/skills/deco-e2e-testing/discovery.md +337 -0
- package/.cursor/skills/deco-e2e-testing/scripts/scaffold.sh +81 -0
- package/.cursor/skills/deco-e2e-testing/selectors.md +175 -0
- package/.cursor/skills/deco-e2e-testing/templates/package.json +18 -0
- package/.cursor/skills/deco-e2e-testing/templates/playwright.config.ts +65 -0
- package/.cursor/skills/deco-e2e-testing/templates/scripts/baseline.ts +279 -0
- package/.cursor/skills/deco-e2e-testing/templates/scripts/run-e2e.ts +194 -0
- package/.cursor/skills/deco-e2e-testing/templates/specs/ecommerce-flow.spec.ts +612 -0
- package/.cursor/skills/deco-e2e-testing/templates/tsconfig.json +12 -0
- package/.cursor/skills/deco-e2e-testing/templates/utils/metrics-collector.ts +918 -0
- package/.cursor/skills/deco-e2e-testing/troubleshooting.md +602 -0
- package/.cursor/skills/deco-edge-caching/SKILL.md +316 -0
- package/.cursor/skills/deco-full-analysis/SKILL.md +898 -0
- package/.cursor/skills/deco-full-analysis/checklists/asset-optimization.md +251 -0
- package/.cursor/skills/deco-full-analysis/checklists/bug-fix.md +189 -0
- package/.cursor/skills/deco-full-analysis/checklists/cache-strategy.md +144 -0
- package/.cursor/skills/deco-full-analysis/checklists/dependency-update.md +150 -0
- package/.cursor/skills/deco-full-analysis/checklists/hydration-fix.md +191 -0
- package/.cursor/skills/deco-full-analysis/checklists/image-optimization.md +180 -0
- package/.cursor/skills/deco-full-analysis/checklists/loader-optimization.md +165 -0
- package/.cursor/skills/deco-full-analysis/checklists/seo-fix.md +183 -0
- package/.cursor/skills/deco-full-analysis/checklists/site-cleanup.md +281 -0
- package/.cursor/skills/deco-full-analysis/discovery.md +548 -0
- package/.cursor/skills/deco-incident-debugging/SKILL.md +378 -0
- package/.cursor/skills/deco-incident-debugging/headless-mode.md +510 -0
- package/.cursor/skills/deco-incident-debugging/learnings-index.md +227 -0
- package/.cursor/skills/deco-incident-debugging/triage-workflow.md +312 -0
- package/.cursor/skills/deco-islands-migration/SKILL.md +251 -0
- package/.cursor/skills/deco-loader-n-plus-1-detector/SKILL.md +275 -0
- package/.cursor/skills/deco-performance-audit/SKILL.md +530 -0
- package/.cursor/skills/deco-performance-audit/tools-reference.md +428 -0
- package/.cursor/skills/deco-performance-audit/workflow.md +457 -0
- package/.cursor/skills/deco-server-functions-invoke/SKILL.md +92 -0
- package/.cursor/skills/deco-server-functions-invoke/architecture.md +166 -0
- package/.cursor/skills/deco-server-functions-invoke/generator.md +122 -0
- package/.cursor/skills/deco-server-functions-invoke/problem.md +98 -0
- package/.cursor/skills/deco-server-functions-invoke/troubleshooting.md +110 -0
- package/.cursor/skills/deco-site-deployment/SKILL.md +396 -0
- package/.cursor/skills/deco-site-memory-debugging/SKILL.md +121 -0
- package/.cursor/skills/deco-site-memory-debugging/cdp-connection.md +222 -0
- package/.cursor/skills/deco-site-memory-debugging/memory-analysis.md +362 -0
- package/.cursor/skills/deco-site-patterns/SKILL.md +124 -0
- package/.cursor/skills/deco-site-patterns/app-composition.md +337 -0
- package/.cursor/skills/deco-site-patterns/client-patterns.md +341 -0
- package/.cursor/skills/deco-site-patterns/cms-wiring.md +230 -0
- package/.cursor/skills/deco-site-patterns/section-patterns.md +340 -0
- package/.cursor/skills/deco-site-scaling-tuning/SKILL.md +240 -0
- package/.cursor/skills/deco-site-scaling-tuning/analysis-scripts.md +267 -0
- package/.cursor/skills/deco-start-architecture/SKILL.md +218 -0
- package/.cursor/skills/deco-start-architecture/admin-protocol.md +156 -0
- package/.cursor/skills/deco-start-architecture/cms-resolution.md +201 -0
- package/.cursor/skills/deco-start-architecture/code-quality.md +158 -0
- package/.cursor/skills/deco-start-architecture/gap-analysis.md +129 -0
- package/.cursor/skills/deco-start-architecture/sdk-utilities.md +197 -0
- package/.cursor/skills/deco-start-architecture/worker-entry-caching.md +154 -0
- package/.cursor/skills/deco-startup-analysis/SKILL.md +248 -0
- package/.cursor/skills/deco-storefront-test-checklist/SKILL.md +369 -0
- package/.cursor/skills/deco-tanstack-hydration-fixes/SKILL.md +468 -0
- package/.cursor/skills/deco-tanstack-navigation/SKILL.md +681 -0
- package/.cursor/skills/deco-tanstack-search/SKILL.md +411 -0
- package/.cursor/skills/deco-tanstack-storefront-patterns/SKILL.md +1013 -0
- package/.cursor/skills/deco-to-tanstack-migration/SKILL.md +518 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/codemod-commands.md +174 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/commerce/README.md +78 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/deco-framework/README.md +128 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/gotchas.md +719 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/imports/README.md +70 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/platform-hooks/README.md +154 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/signals/README.md +220 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/vite-config/README.md +78 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/package-json.md +55 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/root-route.md +110 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/router.md +96 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/setup-ts.md +167 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/vite-config.md +122 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/worker-entry.md +67 -0
- package/.cursor/skills/deco-typescript-fixes/SKILL.md +178 -0
- package/.cursor/skills/deco-typescript-fixes/common-fixes.md +330 -0
- package/.cursor/skills/deco-typescript-fixes/strategy.md +148 -0
- package/.cursor/skills/deco-variant-selection-perf/SKILL.md +272 -0
- package/.cursor/skills/deco-vtex-fetch-cache/SKILL.md +225 -0
- package/.cursor/skills/find-skills/SKILL.md +133 -0
- package/.cursor/skills/incident-report/SKILL.md +179 -0
- package/.cursor/skills/incident-report/references/5-whys.md +75 -0
- package/.cursor/skills/incident-report/templates/client-report.md +187 -0
- package/.cursor/skills/incident-report/templates/internal-report.md +206 -0
- package/.cursor/skills/template-skill/SKILL.md +38 -0
- package/.github/workflows/release.yml +32 -0
- package/.releaserc.json +25 -0
- package/CLAUDE.md +135 -0
- package/GAP_ANALYSIS.md +224 -0
- package/GAP_ANALYSIS_V2.md +1013 -0
- package/biome.json +39 -0
- package/knip.json +5 -0
- package/package.json +87 -0
- package/scripts/generate-blocks.ts +69 -0
- package/scripts/generate-invoke.ts +378 -0
- package/scripts/generate-schema.ts +657 -0
- package/src/admin/cors.ts +29 -0
- package/src/admin/decofile.ts +72 -0
- package/src/admin/index.ts +24 -0
- package/src/admin/invoke.ts +163 -0
- package/src/admin/liveControls.ts +29 -0
- package/src/admin/meta.ts +70 -0
- package/src/admin/render.ts +205 -0
- package/src/admin/schema.ts +686 -0
- package/src/admin/setup.ts +44 -0
- package/src/cms/index.ts +59 -0
- package/src/cms/loader.ts +180 -0
- package/src/cms/registry.ts +162 -0
- package/src/cms/resolve.ts +1005 -0
- package/src/cms/sectionLoaders.ts +294 -0
- package/src/hooks/DecoPageRenderer.tsx +444 -0
- package/src/hooks/LazySection.tsx +109 -0
- package/src/hooks/LiveControls.tsx +108 -0
- package/src/hooks/SectionErrorFallback.tsx +85 -0
- package/src/hooks/index.ts +8 -0
- package/src/index.ts +5 -0
- package/src/matchers/builtins.ts +184 -0
- package/src/matchers/posthog.ts +154 -0
- package/src/middleware/decoState.ts +55 -0
- package/src/middleware/healthMetrics.ts +131 -0
- package/src/middleware/index.ts +80 -0
- package/src/middleware/liveness.ts +21 -0
- package/src/middleware/observability.ts +205 -0
- package/src/routes/adminRoutes.ts +83 -0
- package/src/routes/cmsRoute.ts +302 -0
- package/src/routes/components.tsx +34 -0
- package/src/routes/index.ts +15 -0
- package/src/sdk/analytics.ts +72 -0
- package/src/sdk/cacheHeaders.ts +268 -0
- package/src/sdk/cachedLoader.ts +206 -0
- package/src/sdk/clx.ts +3 -0
- package/src/sdk/cookie.ts +39 -0
- package/src/sdk/createInvoke.ts +57 -0
- package/src/sdk/csp.ts +59 -0
- package/src/sdk/env.ts +27 -0
- package/src/sdk/index.ts +63 -0
- package/src/sdk/instrumentedFetch.ts +137 -0
- package/src/sdk/invoke.ts +133 -0
- package/src/sdk/mergeCacheControl.ts +150 -0
- package/src/sdk/redirects.ts +217 -0
- package/src/sdk/requestContext.ts +184 -0
- package/src/sdk/serverTimings.ts +68 -0
- package/src/sdk/signal.ts +41 -0
- package/src/sdk/sitemap.ts +143 -0
- package/src/sdk/urlUtils.ts +117 -0
- package/src/sdk/useDevice.ts +82 -0
- package/src/sdk/useId.ts +7 -0
- package/src/sdk/useScript.ts +101 -0
- package/src/sdk/workerEntry.ts +703 -0
- package/src/sdk/wrapCaughtErrors.ts +107 -0
- package/src/types/index.ts +39 -0
- package/src/types/widgets.ts +13 -0
- 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
|
+
}
|