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.
- package/README.md +13 -34
- package/SKILL.md +69 -45
- package/bin/cli.js +22 -4
- package/bin/commands/clone-site.js +31 -171
- package/bin/commands/help.js +19 -6
- package/bin/commands/init.js +9 -86
- package/bin/commands/uninstall.js +105 -0
- package/bin/commands/update.js +70 -0
- package/bin/commands/verify.js +7 -14
- package/bin/utils/paths.js +28 -0
- package/bin/utils/validate.js +2 -22
- package/bin/utils/version.js +23 -0
- package/docs/code-standards.md +789 -0
- package/docs/codebase-summary.md +533 -286
- package/docs/index.md +74 -0
- package/docs/project-overview-pdr.md +797 -0
- package/docs/system-architecture.md +718 -0
- package/package.json +14 -17
- package/src/ai/prompts/design-tokens/basic.md +80 -0
- package/src/ai/prompts/design-tokens/section-with-css.md +41 -0
- package/src/ai/prompts/design-tokens/section.md +48 -0
- package/src/ai/prompts/design-tokens/with-css.md +87 -0
- package/src/ai/prompts/structure-analysis/basic.md +55 -0
- package/src/ai/prompts/structure-analysis/with-context.md +59 -0
- package/src/ai/prompts/structure-analysis/with-dimensions.md +63 -0
- package/src/ai/prompts/structure-analysis/with-hierarchy.md +73 -0
- package/src/ai/prompts/ux-audit/aggregation.md +42 -0
- package/src/ai/prompts/ux-audit/desktop.md +92 -0
- package/src/ai/prompts/ux-audit/mobile.md +93 -0
- package/src/ai/prompts/ux-audit/tablet.md +92 -0
- package/src/core/animation/animation-extractor-ast.js +183 -0
- package/src/core/animation/animation-extractor-output.js +152 -0
- package/src/core/animation/animation-extractor.js +178 -0
- package/src/core/animation/state-capture-detection.js +200 -0
- package/src/core/animation/state-capture.js +193 -0
- package/src/core/capture/browser-context-pool.js +96 -0
- package/src/core/capture/multi-page-screenshot-page.js +110 -0
- package/src/core/capture/multi-page-screenshot.js +208 -0
- package/src/core/capture/screenshot-extraction.js +186 -0
- package/src/core/capture/screenshot-helpers.js +175 -0
- package/src/core/capture/screenshot-orchestrator.js +174 -0
- package/src/core/capture/screenshot-viewport.js +93 -0
- package/src/core/capture/screenshot.js +192 -0
- package/src/core/content/content-counter-dom.js +191 -0
- package/src/core/content/content-counter.js +76 -0
- package/src/core/css/breakpoint-detector.js +66 -0
- package/src/core/css/chromium-defaults.json +23 -0
- package/src/core/css/computed-style-extractor.js +102 -0
- package/src/core/css/css-chunker.js +103 -0
- package/src/core/css/filter-css-dead-code.js +120 -0
- package/src/core/css/filter-css-html-analyzer.js +110 -0
- package/src/core/css/filter-css-selector-matcher.js +172 -0
- package/src/core/css/filter-css.js +206 -0
- package/src/core/css/merge-css-atrule-processor.js +158 -0
- package/src/core/css/merge-css-file-io.js +68 -0
- package/src/core/css/merge-css.js +148 -0
- package/src/core/detection/framework-detector-routing.js +68 -0
- package/src/core/detection/framework-detector-signals.js +65 -0
- package/src/core/detection/framework-detector.js +198 -0
- package/src/core/dimension/dimension-extractor-card-detector.js +82 -0
- package/src/core/dimension/dimension-extractor.js +317 -0
- package/src/core/dimension/dimension-output-ai-summary.js +111 -0
- package/src/core/dimension/dimension-output.js +173 -0
- package/src/core/dimension/dom-tree-analyzer-tree-builders.js +95 -0
- package/src/core/dimension/dom-tree-analyzer.js +191 -0
- package/src/core/discovery/app-state-snapshot-capture.js +195 -0
- package/src/core/discovery/app-state-snapshot-utils.js +178 -0
- package/src/core/discovery/app-state-snapshot.js +131 -0
- package/src/core/discovery/discover-pages-routes.js +84 -0
- package/src/core/discovery/discover-pages-utils.js +177 -0
- package/src/core/discovery/discover-pages.js +191 -0
- package/src/core/html/html-extractor-inline-styler.js +70 -0
- package/src/core/html/html-extractor.js +147 -0
- package/src/core/html/semantic-enhancer-mappings.js +200 -0
- package/src/core/html/semantic-enhancer-page.js +148 -0
- package/src/core/html/semantic-enhancer.js +135 -0
- package/src/core/links/rewrite-links-css-rewriter.js +53 -0
- package/src/core/links/rewrite-links.js +173 -0
- package/src/core/media/asset-validator.js +118 -0
- package/src/core/media/extract-assets-downloader.js +187 -0
- package/src/core/media/extract-assets-page-scraper.js +115 -0
- package/src/core/media/extract-assets.js +159 -0
- package/src/core/media/video-capture-convert.js +200 -0
- package/src/core/media/video-capture.js +201 -0
- package/src/core/{lazy-loader.js → page-prep/lazy-loader.js} +37 -39
- package/src/core/section/section-cropper-helpers.js +43 -0
- package/src/core/{section-cropper.js → section/section-cropper.js} +11 -88
- package/src/core/section/section-detector-strategies.js +139 -0
- package/src/core/section/section-detector-utils.js +100 -0
- package/src/core/section/section-detector.js +88 -0
- package/src/core/tests/test-section-cropper.js +2 -2
- package/src/core/tests/test-section-detector.js +2 -2
- package/src/post-process/enhance-assets.js +29 -4
- package/src/post-process/fetch-images-unsplash-client.js +123 -0
- package/src/post-process/fetch-images.js +60 -263
- package/src/post-process/inject-gosnap.js +88 -0
- package/src/post-process/inject-icons-svg-replacer.js +76 -0
- package/src/post-process/inject-icons.js +47 -200
- package/src/route-discoverers/base-discoverer-utils.js +137 -0
- package/src/route-discoverers/base-discoverer.js +29 -118
- package/src/route-discoverers/index.js +1 -1
- package/src/shared/config.js +38 -0
- package/src/shared/error-codes.js +31 -0
- package/src/shared/viewports.js +46 -0
- package/src/utils/browser.js +0 -7
- package/src/utils/helpers.js +4 -0
- package/src/utils/log.js +12 -0
- package/src/utils/playwright-loader.js +76 -0
- package/src/utils/playwright.js +3 -69
- package/src/utils/progress.js +32 -0
- package/src/verification/generate-audit-report-css-fixes.js +52 -0
- package/src/verification/generate-audit-report-sections.js +158 -0
- package/src/verification/generate-audit-report.js +5 -281
- package/src/verification/quality-scorer.js +92 -0
- package/src/verification/verify-footer-checks.js +103 -0
- package/src/verification/verify-footer-helpers.js +178 -0
- package/src/verification/verify-footer.js +23 -381
- package/src/verification/verify-header-checks.js +104 -0
- package/src/verification/verify-header-helpers.js +156 -0
- package/src/verification/verify-header.js +23 -365
- package/src/verification/verify-layout-report.js +101 -0
- package/src/verification/verify-layout.js +13 -259
- package/src/verification/verify-menu-checks.js +104 -0
- package/src/verification/verify-menu-helpers.js +112 -0
- package/src/verification/verify-menu.js +17 -285
- package/src/verification/verify-slider-checks.js +115 -0
- package/src/verification/verify-slider-constants.js +65 -0
- package/src/verification/verify-slider-helpers.js +164 -0
- package/src/verification/verify-slider.js +23 -414
- package/.env.example +0 -14
- package/docs/basic-clone.md +0 -63
- package/docs/cli-reference.md +0 -316
- package/docs/design-clone-architecture.md +0 -492
- package/docs/pixel-perfect.md +0 -117
- package/docs/project-roadmap.md +0 -382
- package/docs/troubleshooting.md +0 -170
- package/requirements.txt +0 -5
- package/src/ai/__pycache__/analyze-structure.cpython-313.pyc +0 -0
- package/src/ai/__pycache__/extract-design-tokens.cpython-313.pyc +0 -0
- package/src/ai/analyze-structure.py +0 -375
- package/src/ai/extract-design-tokens.py +0 -782
- package/src/ai/prompts/__init__.py +0 -2
- package/src/ai/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/ai/prompts/__pycache__/design_tokens.cpython-313.pyc +0 -0
- package/src/ai/prompts/__pycache__/structure_analysis.cpython-313.pyc +0 -0
- package/src/ai/prompts/__pycache__/ux_audit.cpython-313.pyc +0 -0
- package/src/ai/prompts/design_tokens.py +0 -316
- package/src/ai/prompts/structure_analysis.py +0 -592
- package/src/ai/prompts/ux_audit.py +0 -198
- package/src/ai/ux-audit.js +0 -596
- package/src/core/animation-extractor.js +0 -526
- package/src/core/app-state-snapshot.js +0 -511
- package/src/core/content-counter.js +0 -342
- package/src/core/design-tokens.js +0 -103
- package/src/core/dimension-extractor.js +0 -438
- package/src/core/dimension-output.js +0 -305
- package/src/core/discover-pages.js +0 -542
- package/src/core/dom-tree-analyzer.js +0 -298
- package/src/core/extract-assets.js +0 -468
- package/src/core/filter-css.js +0 -499
- package/src/core/framework-detector.js +0 -538
- package/src/core/html-extractor.js +0 -212
- package/src/core/merge-css.js +0 -407
- package/src/core/multi-page-screenshot.js +0 -380
- package/src/core/rewrite-links.js +0 -226
- package/src/core/screenshot.js +0 -701
- package/src/core/section-detector.js +0 -386
- package/src/core/semantic-enhancer.js +0 -492
- package/src/core/state-capture.js +0 -598
- package/src/core/video-capture.js +0 -546
- package/src/utils/__init__.py +0 -16
- package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
- package/src/utils/env.py +0 -134
- /package/src/core/{css-extractor.js → css/css-extractor.js} +0 -0
- /package/src/core/{cookie-handler.js → page-prep/cookie-handler.js} +0 -0
- /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
|
-
*
|
|
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
|
-
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
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));
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
|