design-clone 2.1.0 → 3.0.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 (177) hide show
  1. package/README.md +13 -34
  2. package/SKILL.md +69 -45
  3. package/bin/cli.js +22 -4
  4. package/bin/commands/clone-site.js +31 -171
  5. package/bin/commands/help.js +19 -6
  6. package/bin/commands/init.js +9 -86
  7. package/bin/commands/uninstall.js +105 -0
  8. package/bin/commands/update.js +70 -0
  9. package/bin/commands/verify.js +7 -14
  10. package/bin/utils/paths.js +28 -0
  11. package/bin/utils/validate.js +2 -22
  12. package/bin/utils/version.js +23 -0
  13. package/docs/code-standards.md +789 -0
  14. package/docs/codebase-summary.md +533 -286
  15. package/docs/index.md +74 -0
  16. package/docs/project-overview-pdr.md +797 -0
  17. package/docs/system-architecture.md +718 -0
  18. package/package.json +14 -17
  19. package/src/ai/prompts/design-tokens/basic.md +80 -0
  20. package/src/ai/prompts/design-tokens/section-with-css.md +41 -0
  21. package/src/ai/prompts/design-tokens/section.md +48 -0
  22. package/src/ai/prompts/design-tokens/with-css.md +87 -0
  23. package/src/ai/prompts/structure-analysis/basic.md +55 -0
  24. package/src/ai/prompts/structure-analysis/with-context.md +59 -0
  25. package/src/ai/prompts/structure-analysis/with-dimensions.md +63 -0
  26. package/src/ai/prompts/structure-analysis/with-hierarchy.md +73 -0
  27. package/src/ai/prompts/ux-audit/aggregation.md +42 -0
  28. package/src/ai/prompts/ux-audit/desktop.md +92 -0
  29. package/src/ai/prompts/ux-audit/mobile.md +93 -0
  30. package/src/ai/prompts/ux-audit/tablet.md +92 -0
  31. package/src/core/animation/animation-extractor-ast.js +183 -0
  32. package/src/core/animation/animation-extractor-output.js +152 -0
  33. package/src/core/animation/animation-extractor.js +178 -0
  34. package/src/core/animation/state-capture-detection.js +200 -0
  35. package/src/core/animation/state-capture.js +193 -0
  36. package/src/core/capture/browser-context-pool.js +96 -0
  37. package/src/core/capture/multi-page-screenshot-page.js +110 -0
  38. package/src/core/capture/multi-page-screenshot.js +208 -0
  39. package/src/core/capture/screenshot-extraction.js +186 -0
  40. package/src/core/capture/screenshot-helpers.js +175 -0
  41. package/src/core/capture/screenshot-orchestrator.js +174 -0
  42. package/src/core/capture/screenshot-viewport.js +93 -0
  43. package/src/core/capture/screenshot.js +192 -0
  44. package/src/core/content/content-counter-dom.js +191 -0
  45. package/src/core/content/content-counter.js +76 -0
  46. package/src/core/css/breakpoint-detector.js +66 -0
  47. package/src/core/css/chromium-defaults.json +23 -0
  48. package/src/core/css/computed-style-extractor.js +102 -0
  49. package/src/core/css/css-chunker.js +103 -0
  50. package/src/core/css/filter-css-dead-code.js +120 -0
  51. package/src/core/css/filter-css-html-analyzer.js +110 -0
  52. package/src/core/css/filter-css-selector-matcher.js +172 -0
  53. package/src/core/css/filter-css.js +206 -0
  54. package/src/core/css/merge-css-atrule-processor.js +158 -0
  55. package/src/core/css/merge-css-file-io.js +68 -0
  56. package/src/core/css/merge-css.js +148 -0
  57. package/src/core/detection/framework-detector-routing.js +68 -0
  58. package/src/core/detection/framework-detector-signals.js +65 -0
  59. package/src/core/detection/framework-detector.js +198 -0
  60. package/src/core/dimension/dimension-extractor-card-detector.js +82 -0
  61. package/src/core/dimension/dimension-extractor.js +317 -0
  62. package/src/core/dimension/dimension-output-ai-summary.js +111 -0
  63. package/src/core/dimension/dimension-output.js +173 -0
  64. package/src/core/dimension/dom-tree-analyzer-tree-builders.js +95 -0
  65. package/src/core/dimension/dom-tree-analyzer.js +191 -0
  66. package/src/core/discovery/app-state-snapshot-capture.js +195 -0
  67. package/src/core/discovery/app-state-snapshot-utils.js +178 -0
  68. package/src/core/discovery/app-state-snapshot.js +131 -0
  69. package/src/core/discovery/discover-pages-routes.js +84 -0
  70. package/src/core/discovery/discover-pages-utils.js +177 -0
  71. package/src/core/discovery/discover-pages.js +191 -0
  72. package/src/core/html/html-extractor-inline-styler.js +70 -0
  73. package/src/core/html/html-extractor.js +147 -0
  74. package/src/core/html/semantic-enhancer-mappings.js +200 -0
  75. package/src/core/html/semantic-enhancer-page.js +148 -0
  76. package/src/core/html/semantic-enhancer.js +135 -0
  77. package/src/core/links/rewrite-links-css-rewriter.js +53 -0
  78. package/src/core/links/rewrite-links.js +173 -0
  79. package/src/core/media/asset-validator.js +118 -0
  80. package/src/core/media/extract-assets-downloader.js +187 -0
  81. package/src/core/media/extract-assets-page-scraper.js +115 -0
  82. package/src/core/media/extract-assets.js +159 -0
  83. package/src/core/media/video-capture-convert.js +200 -0
  84. package/src/core/media/video-capture.js +201 -0
  85. package/src/core/{lazy-loader.js → page-prep/lazy-loader.js} +37 -39
  86. package/src/core/section/section-cropper-helpers.js +43 -0
  87. package/src/core/{section-cropper.js → section/section-cropper.js} +11 -88
  88. package/src/core/section/section-detector-strategies.js +139 -0
  89. package/src/core/section/section-detector-utils.js +100 -0
  90. package/src/core/section/section-detector.js +88 -0
  91. package/src/core/tests/test-section-cropper.js +2 -2
  92. package/src/core/tests/test-section-detector.js +2 -2
  93. package/src/post-process/enhance-assets.js +29 -4
  94. package/src/post-process/fetch-images-unsplash-client.js +123 -0
  95. package/src/post-process/fetch-images.js +60 -263
  96. package/src/post-process/inject-gosnap.js +88 -0
  97. package/src/post-process/inject-icons-svg-replacer.js +76 -0
  98. package/src/post-process/inject-icons.js +47 -200
  99. package/src/route-discoverers/base-discoverer-utils.js +137 -0
  100. package/src/route-discoverers/base-discoverer.js +29 -118
  101. package/src/route-discoverers/index.js +1 -1
  102. package/src/shared/config.js +38 -0
  103. package/src/shared/error-codes.js +31 -0
  104. package/src/shared/viewports.js +46 -0
  105. package/src/utils/browser.js +0 -7
  106. package/src/utils/helpers.js +4 -0
  107. package/src/utils/log.js +12 -0
  108. package/src/utils/playwright-loader.js +76 -0
  109. package/src/utils/playwright.js +3 -69
  110. package/src/utils/progress.js +32 -0
  111. package/src/verification/generate-audit-report-css-fixes.js +52 -0
  112. package/src/verification/generate-audit-report-sections.js +158 -0
  113. package/src/verification/generate-audit-report.js +5 -281
  114. package/src/verification/quality-scorer.js +92 -0
  115. package/src/verification/verify-footer-checks.js +103 -0
  116. package/src/verification/verify-footer-helpers.js +178 -0
  117. package/src/verification/verify-footer.js +23 -381
  118. package/src/verification/verify-header-checks.js +104 -0
  119. package/src/verification/verify-header-helpers.js +156 -0
  120. package/src/verification/verify-header.js +23 -365
  121. package/src/verification/verify-layout-report.js +101 -0
  122. package/src/verification/verify-layout.js +13 -259
  123. package/src/verification/verify-menu-checks.js +104 -0
  124. package/src/verification/verify-menu-helpers.js +112 -0
  125. package/src/verification/verify-menu.js +17 -285
  126. package/src/verification/verify-slider-checks.js +115 -0
  127. package/src/verification/verify-slider-constants.js +65 -0
  128. package/src/verification/verify-slider-helpers.js +164 -0
  129. package/src/verification/verify-slider.js +23 -414
  130. package/.env.example +0 -14
  131. package/docs/basic-clone.md +0 -63
  132. package/docs/cli-reference.md +0 -316
  133. package/docs/design-clone-architecture.md +0 -492
  134. package/docs/pixel-perfect.md +0 -117
  135. package/docs/project-roadmap.md +0 -382
  136. package/docs/troubleshooting.md +0 -170
  137. package/requirements.txt +0 -5
  138. package/src/ai/__pycache__/analyze-structure.cpython-313.pyc +0 -0
  139. package/src/ai/__pycache__/extract-design-tokens.cpython-313.pyc +0 -0
  140. package/src/ai/analyze-structure.py +0 -375
  141. package/src/ai/extract-design-tokens.py +0 -782
  142. package/src/ai/prompts/__init__.py +0 -2
  143. package/src/ai/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
  144. package/src/ai/prompts/__pycache__/design_tokens.cpython-313.pyc +0 -0
  145. package/src/ai/prompts/__pycache__/structure_analysis.cpython-313.pyc +0 -0
  146. package/src/ai/prompts/__pycache__/ux_audit.cpython-313.pyc +0 -0
  147. package/src/ai/prompts/design_tokens.py +0 -316
  148. package/src/ai/prompts/structure_analysis.py +0 -592
  149. package/src/ai/prompts/ux_audit.py +0 -198
  150. package/src/ai/ux-audit.js +0 -596
  151. package/src/core/animation-extractor.js +0 -526
  152. package/src/core/app-state-snapshot.js +0 -511
  153. package/src/core/content-counter.js +0 -342
  154. package/src/core/design-tokens.js +0 -103
  155. package/src/core/dimension-extractor.js +0 -438
  156. package/src/core/dimension-output.js +0 -305
  157. package/src/core/discover-pages.js +0 -542
  158. package/src/core/dom-tree-analyzer.js +0 -298
  159. package/src/core/extract-assets.js +0 -468
  160. package/src/core/filter-css.js +0 -499
  161. package/src/core/framework-detector.js +0 -538
  162. package/src/core/html-extractor.js +0 -212
  163. package/src/core/merge-css.js +0 -407
  164. package/src/core/multi-page-screenshot.js +0 -380
  165. package/src/core/rewrite-links.js +0 -226
  166. package/src/core/screenshot.js +0 -701
  167. package/src/core/section-detector.js +0 -386
  168. package/src/core/semantic-enhancer.js +0 -492
  169. package/src/core/state-capture.js +0 -598
  170. package/src/core/video-capture.js +0 -546
  171. package/src/utils/__init__.py +0 -16
  172. package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  173. package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
  174. package/src/utils/env.py +0 -134
  175. /package/src/core/{css-extractor.js → css/css-extractor.js} +0 -0
  176. /package/src/core/{cookie-handler.js → page-prep/cookie-handler.js} +0 -0
  177. /package/src/core/{page-readiness.js → page-prep/page-readiness.js} +0 -0
@@ -3,7 +3,7 @@
3
3
  * Layout Verification Script
4
4
  *
5
5
  * Compares generated HTML against original website screenshots
6
- * using Gemini Vision to identify layout discrepancies.
6
+ * to identify layout discrepancies.
7
7
  *
8
8
  * Usage:
9
9
  * node verify-layout.js --html <path> --original <dir> [--output <dir>] [--verbose]
@@ -17,151 +17,31 @@
17
17
 
18
18
  import fs from 'fs/promises';
19
19
  import path from 'path';
20
- import { fileURLToPath } from 'url';
21
20
 
22
- // Import browser abstraction (auto-detects chrome-devtools or standalone)
23
- import { getBrowser, getPage, closeBrowser, disconnectBrowser, parseArgs, outputJSON, outputError } from '../utils/browser.js';
24
-
25
- // Import Gemini for vision comparison
26
- const GEMINI_API_KEY = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
27
-
28
- // Viewport configurations matching original screenshots
29
- const VIEWPORTS = {
30
- desktop: { width: 1920, height: 1080, deviceScaleFactor: 1 },
31
- tablet: { width: 768, height: 1024, deviceScaleFactor: 1 },
32
- mobile: { width: 375, height: 812, deviceScaleFactor: 2 }
33
- };
21
+ import { getBrowser, getPage, closeBrowser, disconnectBrowser } from '../utils/browser.js';
22
+ import { parseArgs, outputJSON, outputError } from '../utils/helpers.js';
23
+ import { VIEWPORTS_HD as VIEWPORTS } from '../shared/viewports.js';
24
+ import { generateCSSFixes, writeReport } from './verify-layout-report.js';
34
25
 
35
26
  /**
36
27
  * Capture screenshot of generated HTML at specific viewport
37
28
  */
38
29
  async function captureGeneratedScreenshot(page, viewport, outputPath) {
39
30
  await page.setViewportSize(viewport);
40
- await new Promise(r => setTimeout(r, 500)); // Wait for CSS to apply
41
-
42
- await page.screenshot({
43
- path: outputPath,
44
- fullPage: true
45
- });
46
-
31
+ await new Promise(r => setTimeout(r, 500));
32
+ await page.screenshot({ path: outputPath, fullPage: true });
47
33
  return outputPath;
48
34
  }
49
35
 
50
36
  /**
51
- * Compare two screenshots using Gemini Vision
52
- */
53
- async function compareWithGemini(originalPath, generatedPath, viewportName) {
54
- if (!GEMINI_API_KEY) {
55
- return {
56
- success: false,
57
- error: 'GEMINI_API_KEY not set',
58
- discrepancies: [],
59
- similarity: 0
60
- };
61
- }
62
-
63
- try {
64
- const { GoogleGenerativeAI } = await import('@google/generative-ai');
65
- const genai = new GoogleGenerativeAI(GEMINI_API_KEY);
66
-
67
- // Read both images as base64
68
- const originalBuffer = await fs.readFile(originalPath);
69
- const generatedBuffer = await fs.readFile(generatedPath);
70
-
71
- const originalBase64 = originalBuffer.toString('base64');
72
- const generatedBase64 = generatedBuffer.toString('base64');
73
-
74
- const prompt = `You are a UI/UX expert comparing two website screenshots for layout accuracy.
75
-
76
- IMAGE 1 (left/first): Original website screenshot
77
- IMAGE 2 (right/second): Generated HTML clone screenshot
78
-
79
- Viewport: ${viewportName} (${VIEWPORTS[viewportName].width}x${VIEWPORTS[viewportName].height})
80
-
81
- Analyze and compare these two images. Focus on:
82
- 1. **Layout Structure** - Are sections positioned correctly? Any misalignment?
83
- 2. **Spacing** - Are margins, padding, gaps correct?
84
- 3. **Typography** - Font sizes, line heights, text alignment
85
- 4. **Colors** - Background colors, text colors, borders
86
- 5. **Responsive Elements** - Menu, grid layouts, card widths
87
- 6. **Components** - Buttons, forms, icons positioning
88
-
89
- Return a JSON object with this exact structure:
90
- {
91
- "similarity_score": <0-100 number>,
92
- "overall_assessment": "<brief assessment>",
93
- "discrepancies": [
94
- {
95
- "section": "<section name>",
96
- "severity": "<critical|major|minor>",
97
- "issue": "<description of the issue>",
98
- "css_fix": "<suggested CSS fix or null>"
99
- }
100
- ],
101
- "recommendations": ["<actionable fix 1>", "<actionable fix 2>"]
102
- }
103
-
104
- Be specific about CSS selectors and property values when suggesting fixes.
105
- If similarity is >90%, discrepancies array can be empty.`;
106
-
107
- const model = genai.getGenerativeModel({ model: 'gemini-2.5-flash' });
108
-
109
- const response = await model.generateContent([
110
- prompt,
111
- {
112
- inlineData: {
113
- mimeType: 'image/png',
114
- data: originalBase64
115
- }
116
- },
117
- {
118
- inlineData: {
119
- mimeType: 'image/png',
120
- data: generatedBase64
121
- }
122
- }
123
- ]);
124
-
125
- const text = response.response.text();
126
-
127
- // Extract JSON from response
128
- const jsonMatch = text.match(/\{[\s\S]*\}/);
129
- if (!jsonMatch) {
130
- return {
131
- success: false,
132
- error: 'Could not parse Gemini response',
133
- raw: text
134
- };
135
- }
136
-
137
- const result = JSON.parse(jsonMatch[0]);
138
- return {
139
- success: true,
140
- viewport: viewportName,
141
- ...result
142
- };
143
-
144
- } catch (error) {
145
- return {
146
- success: false,
147
- error: error.message,
148
- discrepancies: []
149
- };
150
- }
151
- }
152
-
153
- /**
154
- * Alternative: Use image comparison without API
155
- * Basic pixel difference calculation
37
+ * Basic image comparison by file size difference.
38
+ * For accurate comparison, use Claude Code vision directly.
156
39
  */
157
40
  async function basicImageCompare(originalPath, generatedPath) {
158
- // This is a fallback that just checks file sizes
159
- // Real comparison should use Gemini
160
41
  try {
161
42
  const originalStats = await fs.stat(originalPath);
162
43
  const generatedStats = await fs.stat(generatedPath);
163
44
 
164
- // Crude estimation based on file size difference
165
45
  const sizeDiff = Math.abs(originalStats.size - generatedStats.size) / originalStats.size;
166
46
  const similarity = Math.max(0, 100 - (sizeDiff * 100));
167
47
 
@@ -169,111 +49,13 @@ async function basicImageCompare(originalPath, generatedPath) {
169
49
  success: true,
170
50
  method: 'basic',
171
51
  similarity_score: Math.round(similarity),
172
- note: 'Basic comparison - use Gemini for accurate analysis'
52
+ note: 'Basic comparison - use Claude Code vision for accurate analysis'
173
53
  };
174
54
  } catch (error) {
175
55
  return { success: false, error: error.message };
176
56
  }
177
57
  }
178
58
 
179
- /**
180
- * Generate CSS fix suggestions based on discrepancies
181
- */
182
- function generateCSSFixes(discrepancies) {
183
- const fixes = [];
184
-
185
- for (const disc of discrepancies) {
186
- if (disc.css_fix) {
187
- fixes.push({
188
- section: disc.section,
189
- severity: disc.severity,
190
- issue: disc.issue,
191
- fix: disc.css_fix
192
- });
193
- }
194
- }
195
-
196
- return fixes;
197
- }
198
-
199
- /**
200
- * Write comparison report
201
- */
202
- async function writeReport(outputDir, results) {
203
- const reportPath = path.join(outputDir, 'layout-verification.md');
204
-
205
- let report = `# Layout Verification Report
206
-
207
- Generated: ${new Date().toISOString()}
208
-
209
- ## Summary
210
-
211
- | Viewport | Similarity | Issues |
212
- |----------|------------|--------|
213
- `;
214
-
215
- for (const [viewport, result] of Object.entries(results.viewports)) {
216
- const score = result.similarity_score || 0;
217
- const issues = result.discrepancies?.length || 0;
218
- const status = score >= 90 ? '✅' : score >= 70 ? '⚠️' : '❌';
219
- report += `| ${viewport} | ${status} ${score}% | ${issues} |\n`;
220
- }
221
-
222
- report += `\n## Overall Score: ${results.overall_score}%\n\n`;
223
-
224
- // Detail each viewport
225
- for (const [viewport, result] of Object.entries(results.viewports)) {
226
- report += `## ${viewport.charAt(0).toUpperCase() + viewport.slice(1)} (${VIEWPORTS[viewport].width}x${VIEWPORTS[viewport].height})\n\n`;
227
-
228
- if (result.overall_assessment) {
229
- report += `**Assessment:** ${result.overall_assessment}\n\n`;
230
- }
231
-
232
- if (result.discrepancies?.length > 0) {
233
- report += `### Discrepancies\n\n`;
234
- for (const disc of result.discrepancies) {
235
- const icon = disc.severity === 'critical' ? '🔴' : disc.severity === 'major' ? '🟠' : '🟡';
236
- report += `${icon} **${disc.section}** (${disc.severity})\n`;
237
- report += ` - Issue: ${disc.issue}\n`;
238
- if (disc.css_fix) {
239
- report += ` - Fix: \`${disc.css_fix}\`\n`;
240
- }
241
- report += '\n';
242
- }
243
- } else {
244
- report += `✅ No significant discrepancies found.\n\n`;
245
- }
246
-
247
- if (result.recommendations?.length > 0) {
248
- report += `### Recommendations\n\n`;
249
- for (const rec of result.recommendations) {
250
- report += `- ${rec}\n`;
251
- }
252
- report += '\n';
253
- }
254
- }
255
-
256
- // Consolidated CSS fixes
257
- const allFixes = [];
258
- for (const result of Object.values(results.viewports)) {
259
- if (result.discrepancies) {
260
- allFixes.push(...generateCSSFixes(result.discrepancies));
261
- }
262
- }
263
-
264
- if (allFixes.length > 0) {
265
- report += `## Suggested CSS Fixes\n\n\`\`\`css\n`;
266
- for (const fix of allFixes) {
267
- report += `/* ${fix.section}: ${fix.issue} */\n`;
268
- report += `${fix.fix}\n\n`;
269
- }
270
- report += `\`\`\`\n`;
271
- }
272
-
273
- await fs.writeFile(reportPath, report);
274
- return reportPath;
275
- }
276
-
277
59
  /**
278
60
  * Main verification function
279
61
  */
@@ -293,7 +75,6 @@ async function verifyLayout() {
293
75
  const verbose = args.verbose === 'true';
294
76
  const outputDir = args.output || path.dirname(args.html);
295
77
 
296
- // Ensure output directory exists
297
78
  await fs.mkdir(outputDir, { recursive: true });
298
79
 
299
80
  try {
@@ -317,22 +98,14 @@ async function verifyLayout() {
317
98
  process.exit(1);
318
99
  }
319
100
 
320
- // Launch browser and capture generated screenshots
321
101
  if (verbose) console.error('\n📸 Capturing generated screenshots...\n');
322
102
 
323
103
  const browser = await getBrowser({ headless: args.headless !== 'false' });
324
104
  const page = await getPage(browser);
325
105
 
326
- // Navigate to generated HTML
327
106
  const absolutePath = path.resolve(args.html);
328
- const targetUrl = `file://${absolutePath}`;
107
+ await page.goto(`file://${absolutePath}`, { waitUntil: 'networkidle', timeout: 30000 });
329
108
 
330
- await page.goto(targetUrl, {
331
- waitUntil: 'networkidle',
332
- timeout: 30000
333
- });
334
-
335
- // Capture screenshots at each viewport
336
109
  const generatedScreenshots = {};
337
110
  for (const [viewport, config] of Object.entries(VIEWPORTS)) {
338
111
  if (originalScreenshots[viewport]) {
@@ -343,24 +116,15 @@ async function verifyLayout() {
343
116
  }
344
117
  }
345
118
 
346
- // Close browser
347
119
  if (args.close === 'true') {
348
120
  await closeBrowser();
349
121
  } else {
350
122
  await disconnectBrowser();
351
123
  }
352
124
 
353
- // Compare screenshots
354
125
  if (verbose) console.error('\n🔬 Comparing layouts...\n');
355
126
 
356
- const results = {
357
- success: true,
358
- html: args.html,
359
- viewports: {},
360
- overall_score: 0,
361
- all_fixes: []
362
- };
363
-
127
+ const results = { success: true, html: args.html, viewports: {}, overall_score: 0, all_fixes: [] };
364
128
  let totalScore = 0;
365
129
  let viewportCount = 0;
366
130
 
@@ -370,14 +134,7 @@ async function verifyLayout() {
370
134
 
371
135
  if (verbose) console.error(` Comparing ${viewport}...`);
372
136
 
373
- let comparison;
374
- if (GEMINI_API_KEY) {
375
- comparison = await compareWithGemini(originalPath, generatedPath, viewport);
376
- } else {
377
- comparison = await basicImageCompare(originalPath, generatedPath);
378
- if (verbose) console.error(' ⚠ Using basic comparison (set GEMINI_API_KEY for accurate analysis)');
379
- }
380
-
137
+ const comparison = await basicImageCompare(originalPath, generatedPath);
381
138
  results.viewports[viewport] = comparison;
382
139
 
383
140
  if (comparison.success && comparison.similarity_score !== undefined) {
@@ -399,11 +156,9 @@ async function verifyLayout() {
399
156
  results.overall_score = viewportCount > 0 ? Math.round(totalScore / viewportCount) : 0;
400
157
  results.success = results.overall_score >= 70;
401
158
 
402
- // Write report
403
159
  const reportPath = await writeReport(outputDir, results);
404
160
  results.report = reportPath;
405
161
 
406
- // Final summary
407
162
  if (verbose) {
408
163
  console.error('\n📊 Summary:');
409
164
  console.error(` Overall Score: ${results.overall_score}%`);
@@ -420,5 +175,4 @@ async function verifyLayout() {
420
175
  }
421
176
  }
422
177
 
423
- // Run
424
178
  verifyLayout();
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Menu Viewport Checks
3
+ *
4
+ * Desktop/mobile menu test orchestration using helpers.
5
+ * Separated from verify-menu-helpers.js to keep each file under 200 lines.
6
+ */
7
+
8
+ import {
9
+ MENU_SELECTORS,
10
+ isElementVisible,
11
+ findElement,
12
+ countVisibleMenuItems
13
+ } from './verify-menu-helpers.js';
14
+
15
+ /**
16
+ * Test desktop menu — requires at least 2 visible items
17
+ * @param {import('playwright').Page} page
18
+ * @param {Object} result - Viewport result object (mutated)
19
+ * @param {{count: number, selector: string|null}} menuItems
20
+ * @param {boolean} verbose
21
+ */
22
+ export async function testDesktopMenu(page, result, menuItems, verbose) {
23
+ if (menuItems.count >= 2) {
24
+ result.tests.push({ name: 'Desktop menu items visible', passed: true, count: menuItems.count, selector: menuItems.selector });
25
+ result.passed++;
26
+ if (verbose) console.error(` ✓ ${menuItems.count} menu items visible`);
27
+ } else {
28
+ result.tests.push({ name: 'Desktop menu items visible', passed: false, count: menuItems.count, error: 'Expected at least 2 visible menu items on desktop' });
29
+ result.failed++;
30
+ if (verbose) console.error(` ✗ Only ${menuItems.count} menu items visible (expected >= 2)`);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Test toggle button click and resulting state change
36
+ * @param {import('playwright').Page} page
37
+ * @param {ElementHandle} toggleElement
38
+ * @param {Object} result - Viewport result object (mutated)
39
+ * @param {boolean} verbose
40
+ */
41
+ export async function testToggleFunctionality(page, toggleElement, result, verbose) {
42
+ try {
43
+ const initialMenuItems = await countVisibleMenuItems(page);
44
+ await toggleElement.click();
45
+ await new Promise(r => setTimeout(r, 500));
46
+ const afterClickItems = await countVisibleMenuItems(page);
47
+
48
+ const stateChanged = afterClickItems.count !== initialMenuItems.count;
49
+ const hasEnoughItems = afterClickItems.count >= 2;
50
+
51
+ if (stateChanged || hasEnoughItems) {
52
+ result.tests.push({ name: 'Menu toggle functionality', passed: true, before: initialMenuItems.count, after: afterClickItems.count });
53
+ result.passed++;
54
+ if (verbose) console.error(` ✓ Toggle works: ${initialMenuItems.count} -> ${afterClickItems.count} items`);
55
+ await toggleElement.click();
56
+ await new Promise(r => setTimeout(r, 300));
57
+ } else {
58
+ result.tests.push({ name: 'Menu toggle functionality', passed: false, before: initialMenuItems.count, after: afterClickItems.count, warning: 'Toggle may not be functional - no state change detected' });
59
+ result.warnings.push('Menu toggle click did not change visible items');
60
+ if (verbose) console.error(` ⚠ Toggle click had no effect`);
61
+ }
62
+ } catch (err) {
63
+ result.tests.push({ name: 'Menu toggle functionality', passed: false, error: err.message });
64
+ result.failed++;
65
+ if (verbose) console.error(` ✗ Toggle click failed: ${err.message}`);
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Test mobile/tablet hamburger menu behaviour
71
+ * @param {import('playwright').Page} page
72
+ * @param {Object} result - Viewport result object (mutated)
73
+ * @param {{count: number, selector: string|null}} menuItems
74
+ * @param {boolean} verbose
75
+ */
76
+ export async function testMobileMenu(page, result, menuItems, verbose) {
77
+ const toggleResult = await findElement(page, MENU_SELECTORS.toggleButtons);
78
+
79
+ if (!toggleResult) {
80
+ if (menuItems.count >= 2) {
81
+ result.tests.push({ name: 'Mobile menu visible without toggle', passed: true, count: menuItems.count, note: 'Menu shows items without hamburger toggle' });
82
+ result.passed++;
83
+ if (verbose) console.error(` ✓ ${menuItems.count} menu items visible (no toggle needed)`);
84
+ } else {
85
+ result.tests.push({ name: 'Mobile menu accessibility', passed: false, error: 'No hamburger toggle found and menu items hidden' });
86
+ result.failed++;
87
+ if (verbose) console.error(` ✗ No hamburger toggle and menu items hidden`);
88
+ }
89
+ return;
90
+ }
91
+
92
+ const isToggleVisible = await isElementVisible(page, toggleResult.selector);
93
+ if (!isToggleVisible) {
94
+ result.warnings.push('Menu toggle found but not visible');
95
+ if (verbose) console.error(` ⚠ Menu toggle found but not visible`);
96
+ return;
97
+ }
98
+
99
+ result.tests.push({ name: 'Mobile menu toggle visible', passed: true, selector: toggleResult.selector });
100
+ result.passed++;
101
+ if (verbose) console.error(` ✓ Menu toggle visible: ${toggleResult.selector}`);
102
+
103
+ await testToggleFunctionality(page, toggleResult.element, result, verbose);
104
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Menu Verification Helpers
3
+ *
4
+ * Selectors and DOM query utilities for verify-menu.js.
5
+ * Includes toggle button/nav container selectors and visible item counting.
6
+ */
7
+
8
+ // Common menu element selectors
9
+ export const MENU_SELECTORS = {
10
+ toggleButtons: [
11
+ '[aria-label*="menu" i]',
12
+ '[aria-label*="nav" i]',
13
+ 'button.hamburger',
14
+ '.hamburger',
15
+ '.menu-toggle',
16
+ '.nav-toggle',
17
+ '.mobile-menu-toggle',
18
+ 'button[class*="hamburger"]',
19
+ 'button[class*="menu"]',
20
+ '[data-toggle="nav"]',
21
+ '[data-menu-toggle]',
22
+ '.header__toggle',
23
+ '.header-toggle',
24
+ '#menu-toggle',
25
+ '.burger',
26
+ '.burger-menu'
27
+ ],
28
+ navContainers: [
29
+ 'nav',
30
+ '[role="navigation"]',
31
+ '.nav',
32
+ '.navigation',
33
+ '.main-nav',
34
+ '.site-nav',
35
+ '.header-nav',
36
+ '.primary-nav',
37
+ '#nav',
38
+ '#navigation',
39
+ '.menu',
40
+ '.main-menu'
41
+ ],
42
+ menuItems: [
43
+ 'nav a',
44
+ 'nav li',
45
+ '.nav-item',
46
+ '.menu-item',
47
+ '.nav-link',
48
+ '.menu-link',
49
+ '[role="navigation"] a'
50
+ ]
51
+ };
52
+
53
+ /**
54
+ * Check if element is visible using Playwright locator API
55
+ * @param {import('playwright').Page} page
56
+ * @param {string} selector
57
+ * @returns {Promise<boolean>}
58
+ */
59
+ export async function isElementVisible(page, selector) {
60
+ try {
61
+ return await page.locator(selector).isVisible();
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Find first matching selector from a list
69
+ * @param {import('playwright').Page} page
70
+ * @param {string[]} selectors
71
+ * @returns {Promise<{element: ElementHandle, selector: string}|null>}
72
+ */
73
+ export async function findElement(page, selectors) {
74
+ for (const selector of selectors) {
75
+ const element = await page.$(selector);
76
+ if (element) return { element, selector };
77
+ }
78
+ return null;
79
+ }
80
+
81
+ /**
82
+ * Count visible menu items across MENU_SELECTORS.menuItems
83
+ * Returns count from first selector that yields visible items
84
+ * @param {import('playwright').Page} page
85
+ * @returns {Promise<{count: number, selector: string|null}>}
86
+ */
87
+ export async function countVisibleMenuItems(page) {
88
+ for (const selector of MENU_SELECTORS.menuItems) {
89
+ try {
90
+ const count = await page.evaluate((sel) => {
91
+ const items = document.querySelectorAll(sel);
92
+ let visible = 0;
93
+ items.forEach(item => {
94
+ const style = window.getComputedStyle(item);
95
+ const rect = item.getBoundingClientRect();
96
+ if (
97
+ style.display !== 'none' &&
98
+ style.visibility !== 'hidden' &&
99
+ style.opacity !== '0' &&
100
+ rect.width > 0 &&
101
+ rect.height > 0
102
+ ) visible++;
103
+ });
104
+ return visible;
105
+ }, selector);
106
+
107
+ if (count > 0) return { count, selector };
108
+ } catch { /* continue */ }
109
+ }
110
+ return { count: 0, selector: null };
111
+ }
112
+