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
|
@@ -21,365 +21,39 @@
|
|
|
21
21
|
* --verbose Show detailed progress
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import fs from 'fs/promises';
|
|
25
24
|
import path from 'path';
|
|
26
25
|
|
|
27
|
-
import { getBrowser, getPage, closeBrowser, disconnectBrowser
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
mobile: { width: 375, height: 812, deviceScaleFactor: 2 },
|
|
32
|
-
tablet: { width: 768, height: 1024, deviceScaleFactor: 1 },
|
|
33
|
-
desktop: { width: 1920, height: 1080, deviceScaleFactor: 1 }
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
// Header element selectors
|
|
37
|
-
const HEADER_SELECTORS = {
|
|
38
|
-
container: [
|
|
39
|
-
'header',
|
|
40
|
-
'[role="banner"]',
|
|
41
|
-
'.header',
|
|
42
|
-
'#header',
|
|
43
|
-
'.site-header',
|
|
44
|
-
'.page-header',
|
|
45
|
-
'.masthead'
|
|
46
|
-
],
|
|
47
|
-
logo: [
|
|
48
|
-
'header img[alt*="logo" i]',
|
|
49
|
-
'[role="banner"] img',
|
|
50
|
-
'.logo img',
|
|
51
|
-
'.site-logo img',
|
|
52
|
-
'.logo',
|
|
53
|
-
'.site-logo',
|
|
54
|
-
'header a[href="/"] img',
|
|
55
|
-
'.brand img',
|
|
56
|
-
'.navbar-brand img'
|
|
57
|
-
],
|
|
58
|
-
nav: [
|
|
59
|
-
'header nav',
|
|
60
|
-
'header [role="navigation"]',
|
|
61
|
-
'.header-nav',
|
|
62
|
-
'.main-navigation',
|
|
63
|
-
'.primary-nav',
|
|
64
|
-
'.site-nav',
|
|
65
|
-
'.navbar-nav'
|
|
66
|
-
],
|
|
67
|
-
cta: [
|
|
68
|
-
'header button.cta',
|
|
69
|
-
'header a[class*="button"]',
|
|
70
|
-
'header a[class*="btn"]',
|
|
71
|
-
'.header-action',
|
|
72
|
-
'.nav-cta',
|
|
73
|
-
'header .btn-primary',
|
|
74
|
-
'header a[href*="contact"]',
|
|
75
|
-
'header a[href*="signup"]',
|
|
76
|
-
'header a[href*="login"]'
|
|
77
|
-
],
|
|
78
|
-
navLinks: [
|
|
79
|
-
'header nav a',
|
|
80
|
-
'header [role="navigation"] a',
|
|
81
|
-
'.main-navigation a',
|
|
82
|
-
'.nav-item a',
|
|
83
|
-
'.menu-item a'
|
|
84
|
-
]
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Find first matching element from selectors
|
|
89
|
-
*/
|
|
90
|
-
async function findElement(page, selectors) {
|
|
91
|
-
for (const selector of selectors) {
|
|
92
|
-
try {
|
|
93
|
-
const element = await page.$(selector);
|
|
94
|
-
if (element) {
|
|
95
|
-
return { element, selector };
|
|
96
|
-
}
|
|
97
|
-
} catch (err) { /* continue - selector not found */ }
|
|
98
|
-
}
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Count visible elements
|
|
104
|
-
*/
|
|
105
|
-
async function countVisibleElements(page, selectors) {
|
|
106
|
-
for (const selector of selectors) {
|
|
107
|
-
try {
|
|
108
|
-
const count = await page.evaluate((sel) => {
|
|
109
|
-
const items = document.querySelectorAll(sel);
|
|
110
|
-
let visible = 0;
|
|
111
|
-
items.forEach(item => {
|
|
112
|
-
const style = window.getComputedStyle(item);
|
|
113
|
-
const rect = item.getBoundingClientRect();
|
|
114
|
-
if (
|
|
115
|
-
style.display !== 'none' &&
|
|
116
|
-
style.visibility !== 'hidden' &&
|
|
117
|
-
style.opacity !== '0' &&
|
|
118
|
-
rect.width > 0 &&
|
|
119
|
-
rect.height > 0
|
|
120
|
-
) {
|
|
121
|
-
visible++;
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
return visible;
|
|
125
|
-
}, selector);
|
|
126
|
-
|
|
127
|
-
if (count > 0) {
|
|
128
|
-
return { count, selector };
|
|
129
|
-
}
|
|
130
|
-
} catch (err) { /* continue - selector not found */ }
|
|
131
|
-
}
|
|
132
|
-
return { count: 0, selector: null };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Check header position properties
|
|
137
|
-
*/
|
|
138
|
-
async function checkHeaderPosition(page, headerSelector) {
|
|
139
|
-
return await page.evaluate((sel) => {
|
|
140
|
-
const header = document.querySelector(sel);
|
|
141
|
-
if (!header) return null;
|
|
142
|
-
|
|
143
|
-
const style = window.getComputedStyle(header);
|
|
144
|
-
const rect = header.getBoundingClientRect();
|
|
145
|
-
|
|
146
|
-
return {
|
|
147
|
-
position: style.position,
|
|
148
|
-
isSticky: style.position === 'sticky',
|
|
149
|
-
isFixed: style.position === 'fixed',
|
|
150
|
-
zIndex: parseInt(style.zIndex) || 'auto',
|
|
151
|
-
top: rect.top,
|
|
152
|
-
height: rect.height,
|
|
153
|
-
width: rect.width
|
|
154
|
-
};
|
|
155
|
-
}, headerSelector);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Check logo position (typically left or center)
|
|
160
|
-
*/
|
|
161
|
-
async function checkLogoPosition(page, logoSelector, headerWidth) {
|
|
162
|
-
return await page.evaluate((sel, width) => {
|
|
163
|
-
const logo = document.querySelector(sel);
|
|
164
|
-
if (!logo) return null;
|
|
165
|
-
|
|
166
|
-
const rect = logo.getBoundingClientRect();
|
|
167
|
-
const centerThreshold = width * 0.35;
|
|
168
|
-
|
|
169
|
-
let position = 'unknown';
|
|
170
|
-
if (rect.left < centerThreshold) {
|
|
171
|
-
position = 'left';
|
|
172
|
-
} else if (rect.left > width - centerThreshold) {
|
|
173
|
-
position = 'right';
|
|
174
|
-
} else {
|
|
175
|
-
position = 'center';
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
position,
|
|
180
|
-
x: rect.left,
|
|
181
|
-
y: rect.top,
|
|
182
|
-
width: rect.width,
|
|
183
|
-
height: rect.height
|
|
184
|
-
};
|
|
185
|
-
}, logoSelector, headerWidth);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Test header at specific viewport
|
|
190
|
-
*/
|
|
191
|
-
async function testViewport(page, viewportName, verbose = false) {
|
|
192
|
-
const viewport = VIEWPORTS[viewportName];
|
|
193
|
-
await page.setViewportSize(viewport);
|
|
194
|
-
await new Promise(r => setTimeout(r, 500));
|
|
195
|
-
|
|
196
|
-
const result = {
|
|
197
|
-
viewport: viewportName,
|
|
198
|
-
dimensions: viewport,
|
|
199
|
-
tests: [],
|
|
200
|
-
passed: 0,
|
|
201
|
-
failed: 0,
|
|
202
|
-
warnings: []
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
if (verbose) console.error(`\n📱 Testing ${viewportName} (${viewport.width}x${viewport.height})...`);
|
|
206
|
-
|
|
207
|
-
// Test 1: Header container exists
|
|
208
|
-
const headerResult = await findElement(page, HEADER_SELECTORS.container);
|
|
209
|
-
if (headerResult) {
|
|
210
|
-
result.tests.push({
|
|
211
|
-
name: 'Header container exists',
|
|
212
|
-
passed: true,
|
|
213
|
-
selector: headerResult.selector
|
|
214
|
-
});
|
|
215
|
-
result.passed++;
|
|
216
|
-
if (verbose) console.error(` ✓ Header found: ${headerResult.selector}`);
|
|
217
|
-
|
|
218
|
-
// Get header position info
|
|
219
|
-
const positionInfo = await checkHeaderPosition(page, headerResult.selector);
|
|
220
|
-
|
|
221
|
-
// Test 2: Logo presence
|
|
222
|
-
const logoResult = await findElement(page, HEADER_SELECTORS.logo);
|
|
223
|
-
if (logoResult) {
|
|
224
|
-
const logoPosition = await checkLogoPosition(page, logoResult.selector, viewport.width);
|
|
225
|
-
result.tests.push({
|
|
226
|
-
name: 'Logo present',
|
|
227
|
-
passed: true,
|
|
228
|
-
selector: logoResult.selector,
|
|
229
|
-
position: logoPosition?.position || 'unknown'
|
|
230
|
-
});
|
|
231
|
-
result.passed++;
|
|
232
|
-
if (verbose) console.error(` ✓ Logo found: ${logoResult.selector} (${logoPosition?.position})`);
|
|
233
|
-
} else {
|
|
234
|
-
result.tests.push({
|
|
235
|
-
name: 'Logo present',
|
|
236
|
-
passed: false,
|
|
237
|
-
error: 'No logo found'
|
|
238
|
-
});
|
|
239
|
-
result.failed++;
|
|
240
|
-
if (verbose) console.error(` ✗ Logo not found`);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Test 3: Navigation links
|
|
244
|
-
const navLinks = await countVisibleElements(page, HEADER_SELECTORS.navLinks);
|
|
245
|
-
const expectedLinks = viewportName === 'desktop' ? 2 : 0;
|
|
246
|
-
|
|
247
|
-
if (navLinks.count >= expectedLinks) {
|
|
248
|
-
result.tests.push({
|
|
249
|
-
name: 'Navigation links visible',
|
|
250
|
-
passed: true,
|
|
251
|
-
count: navLinks.count,
|
|
252
|
-
selector: navLinks.selector
|
|
253
|
-
});
|
|
254
|
-
result.passed++;
|
|
255
|
-
if (verbose) console.error(` ✓ ${navLinks.count} nav links visible`);
|
|
256
|
-
} else if (viewportName !== 'desktop' && navLinks.count === 0) {
|
|
257
|
-
// Mobile/tablet may hide links behind hamburger
|
|
258
|
-
result.tests.push({
|
|
259
|
-
name: 'Navigation links (may be in hamburger)',
|
|
260
|
-
passed: true,
|
|
261
|
-
count: navLinks.count,
|
|
262
|
-
note: 'Links may be hidden in mobile menu'
|
|
263
|
-
});
|
|
264
|
-
result.passed++;
|
|
265
|
-
if (verbose) console.error(` ✓ Nav links hidden (expected on ${viewportName})`);
|
|
266
|
-
} else {
|
|
267
|
-
result.tests.push({
|
|
268
|
-
name: 'Navigation links visible',
|
|
269
|
-
passed: false,
|
|
270
|
-
count: navLinks.count,
|
|
271
|
-
error: `Expected at least ${expectedLinks} links on ${viewportName}`
|
|
272
|
-
});
|
|
273
|
-
result.failed++;
|
|
274
|
-
if (verbose) console.error(` ✗ Only ${navLinks.count} nav links (expected >= ${expectedLinks})`);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Test 4: CTA buttons (desktop only)
|
|
278
|
-
if (viewportName === 'desktop') {
|
|
279
|
-
const ctaResult = await findElement(page, HEADER_SELECTORS.cta);
|
|
280
|
-
if (ctaResult) {
|
|
281
|
-
result.tests.push({
|
|
282
|
-
name: 'CTA button present',
|
|
283
|
-
passed: true,
|
|
284
|
-
selector: ctaResult.selector
|
|
285
|
-
});
|
|
286
|
-
result.passed++;
|
|
287
|
-
if (verbose) console.error(` ✓ CTA found: ${ctaResult.selector}`);
|
|
288
|
-
} else {
|
|
289
|
-
result.warnings.push('No CTA button found in header');
|
|
290
|
-
if (verbose) console.error(` ⚠ No CTA button found`);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Test 5: Sticky/fixed behavior
|
|
295
|
-
if (positionInfo) {
|
|
296
|
-
if (positionInfo.isSticky || positionInfo.isFixed) {
|
|
297
|
-
result.tests.push({
|
|
298
|
-
name: 'Header sticky/fixed behavior',
|
|
299
|
-
passed: true,
|
|
300
|
-
position: positionInfo.position
|
|
301
|
-
});
|
|
302
|
-
result.passed++;
|
|
303
|
-
if (verbose) console.error(` ✓ Header is ${positionInfo.position}`);
|
|
304
|
-
} else {
|
|
305
|
-
result.tests.push({
|
|
306
|
-
name: 'Header sticky/fixed behavior',
|
|
307
|
-
passed: true,
|
|
308
|
-
position: positionInfo.position,
|
|
309
|
-
note: 'Header uses static/relative positioning'
|
|
310
|
-
});
|
|
311
|
-
result.passed++;
|
|
312
|
-
if (verbose) console.error(` ✓ Header position: ${positionInfo.position}`);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Test 6: Z-index check (should be high for sticky/fixed)
|
|
316
|
-
if ((positionInfo.isSticky || positionInfo.isFixed) && positionInfo.zIndex !== 'auto') {
|
|
317
|
-
const zIndexOk = positionInfo.zIndex >= 100;
|
|
318
|
-
result.tests.push({
|
|
319
|
-
name: 'Z-index layering',
|
|
320
|
-
passed: zIndexOk,
|
|
321
|
-
zIndex: positionInfo.zIndex,
|
|
322
|
-
note: zIndexOk ? 'Header on top layer' : 'Z-index may be too low'
|
|
323
|
-
});
|
|
324
|
-
if (zIndexOk) result.passed++;
|
|
325
|
-
else result.warnings.push(`Header z-index (${positionInfo.zIndex}) may be too low`);
|
|
326
|
-
if (verbose) console.error(` ${zIndexOk ? '✓' : '⚠'} Z-index: ${positionInfo.zIndex}`);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Store height for consistency check
|
|
330
|
-
result.headerHeight = positionInfo.height;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
} else {
|
|
334
|
-
result.tests.push({
|
|
335
|
-
name: 'Header container exists',
|
|
336
|
-
passed: false,
|
|
337
|
-
error: 'No header container found'
|
|
338
|
-
});
|
|
339
|
-
result.failed++;
|
|
340
|
-
if (verbose) console.error(` ✗ Header not found`);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
return result;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Capture component screenshot
|
|
348
|
-
*/
|
|
349
|
-
async function captureHeaderScreenshot(page, outputDir, viewportName) {
|
|
350
|
-
if (!outputDir) return null;
|
|
351
|
-
|
|
352
|
-
const screenshotPath = path.join(outputDir, `header-test-${viewportName}.png`);
|
|
353
|
-
await page.screenshot({
|
|
354
|
-
path: screenshotPath,
|
|
355
|
-
fullPage: false
|
|
356
|
-
});
|
|
357
|
-
return screenshotPath;
|
|
358
|
-
}
|
|
26
|
+
import { getBrowser, getPage, closeBrowser, disconnectBrowser } from '../utils/browser.js';
|
|
27
|
+
import { parseArgs, outputJSON, outputError } from '../utils/helpers.js';
|
|
28
|
+
import { VIEWPORTS_HD as VIEWPORTS } from '../shared/viewports.js';
|
|
29
|
+
import { testHeaderViewport } from './verify-header-checks.js';
|
|
359
30
|
|
|
360
31
|
/**
|
|
361
32
|
* Validate HTML file path (security: prevent path traversal)
|
|
362
33
|
*/
|
|
363
34
|
function validateHtmlPath(htmlPath) {
|
|
364
35
|
const absolutePath = path.resolve(htmlPath);
|
|
365
|
-
const cwd = process.cwd();
|
|
366
|
-
|
|
367
|
-
// Allow paths within CWD or common output directories
|
|
368
36
|
const allowedPrefixes = [
|
|
369
|
-
cwd,
|
|
37
|
+
process.cwd(),
|
|
370
38
|
path.join(process.env.HOME || '', '.claude'),
|
|
371
39
|
'/tmp',
|
|
372
40
|
path.join(process.env.HOME || '', 'cloned-designs')
|
|
373
41
|
];
|
|
374
|
-
|
|
375
42
|
const isAllowed = allowedPrefixes.some(prefix => absolutePath.startsWith(prefix));
|
|
376
|
-
if (!isAllowed) {
|
|
377
|
-
throw new Error(`Path "${htmlPath}" is outside allowed directories`);
|
|
378
|
-
}
|
|
379
|
-
|
|
43
|
+
if (!isAllowed) throw new Error(`Path "${htmlPath}" is outside allowed directories`);
|
|
380
44
|
return absolutePath;
|
|
381
45
|
}
|
|
382
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Capture header screenshot
|
|
49
|
+
*/
|
|
50
|
+
async function captureHeaderScreenshot(page, outputDir, viewportName) {
|
|
51
|
+
if (!outputDir) return null;
|
|
52
|
+
const screenshotPath = path.join(outputDir, `header-test-${viewportName}.png`);
|
|
53
|
+
await page.screenshot({ path: screenshotPath, fullPage: false });
|
|
54
|
+
return screenshotPath;
|
|
55
|
+
}
|
|
56
|
+
|
|
383
57
|
/**
|
|
384
58
|
* Main verification function
|
|
385
59
|
*/
|
|
@@ -400,37 +74,27 @@ async function verifyHeader() {
|
|
|
400
74
|
|
|
401
75
|
let targetUrl;
|
|
402
76
|
if (args.html) {
|
|
403
|
-
|
|
404
|
-
targetUrl = `file://${absolutePath}`;
|
|
77
|
+
targetUrl = `file://${validateHtmlPath(args.html)}`;
|
|
405
78
|
} else {
|
|
406
79
|
targetUrl = args.url;
|
|
407
80
|
}
|
|
408
81
|
|
|
409
82
|
if (verbose) console.error(`\n🔍 Verifying header: ${targetUrl}\n`);
|
|
410
83
|
|
|
411
|
-
await page.goto(targetUrl, {
|
|
412
|
-
waitUntil: 'networkidle',
|
|
413
|
-
timeout: 30000
|
|
414
|
-
});
|
|
84
|
+
await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 30000 });
|
|
415
85
|
|
|
416
86
|
const results = {
|
|
417
87
|
success: true,
|
|
418
88
|
component: 'header',
|
|
419
89
|
url: targetUrl,
|
|
420
90
|
viewports: {},
|
|
421
|
-
summary: {
|
|
422
|
-
totalTests: 0,
|
|
423
|
-
passed: 0,
|
|
424
|
-
failed: 0,
|
|
425
|
-
warnings: []
|
|
426
|
-
},
|
|
91
|
+
summary: { totalTests: 0, passed: 0, failed: 0, warnings: [] },
|
|
427
92
|
screenshots: [],
|
|
428
93
|
heightConsistency: {}
|
|
429
94
|
};
|
|
430
95
|
|
|
431
|
-
// Test all viewports
|
|
432
96
|
for (const viewportName of ['mobile', 'tablet', 'desktop']) {
|
|
433
|
-
const viewportResult = await
|
|
97
|
+
const viewportResult = await testHeaderViewport(page, viewportName, VIEWPORTS, verbose);
|
|
434
98
|
results.viewports[viewportName] = viewportResult;
|
|
435
99
|
|
|
436
100
|
results.summary.totalTests += viewportResult.tests.length;
|
|
@@ -448,7 +112,7 @@ async function verifyHeader() {
|
|
|
448
112
|
}
|
|
449
113
|
}
|
|
450
114
|
|
|
451
|
-
//
|
|
115
|
+
// Height consistency check
|
|
452
116
|
const heights = Object.values(results.heightConsistency);
|
|
453
117
|
if (heights.length >= 2) {
|
|
454
118
|
const maxDiff = Math.max(...heights) - Math.min(...heights);
|
|
@@ -459,18 +123,12 @@ async function verifyHeader() {
|
|
|
459
123
|
|
|
460
124
|
results.success = results.summary.failed === 0;
|
|
461
125
|
|
|
462
|
-
if (args.close === 'true') {
|
|
463
|
-
await closeBrowser();
|
|
464
|
-
} else {
|
|
465
|
-
await disconnectBrowser();
|
|
466
|
-
}
|
|
126
|
+
if (args.close === 'true') { await closeBrowser(); } else { await disconnectBrowser(); }
|
|
467
127
|
|
|
468
128
|
if (verbose) {
|
|
469
129
|
console.error('\n📊 Summary:');
|
|
470
130
|
console.error(` Tests: ${results.summary.passed}/${results.summary.totalTests} passed`);
|
|
471
|
-
if (results.summary.warnings.length > 0) {
|
|
472
|
-
console.error(` Warnings: ${results.summary.warnings.length}`);
|
|
473
|
-
}
|
|
131
|
+
if (results.summary.warnings.length > 0) console.error(` Warnings: ${results.summary.warnings.length}`);
|
|
474
132
|
console.error(` Status: ${results.success ? '✓ PASS' : '✗ FAIL'}\n`);
|
|
475
133
|
}
|
|
476
134
|
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout Verification Report Generator
|
|
3
|
+
*
|
|
4
|
+
* Markdown report writing and CSS fix suggestion utilities
|
|
5
|
+
* extracted from verify-layout.js to keep each file under 200 lines.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { VIEWPORTS_HD as VIEWPORTS } from '../shared/viewports.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generate CSS fix suggestions from discrepancy objects
|
|
14
|
+
* @param {Array} discrepancies
|
|
15
|
+
* @returns {Array<{section: string, severity: string, issue: string, fix: string}>}
|
|
16
|
+
*/
|
|
17
|
+
export function generateCSSFixes(discrepancies) {
|
|
18
|
+
const fixes = [];
|
|
19
|
+
for (const disc of discrepancies) {
|
|
20
|
+
if (disc.css_fix) {
|
|
21
|
+
fixes.push({
|
|
22
|
+
section: disc.section,
|
|
23
|
+
severity: disc.severity,
|
|
24
|
+
issue: disc.issue,
|
|
25
|
+
fix: disc.css_fix
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return fixes;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Write a markdown comparison report to outputDir/layout-verification.md
|
|
34
|
+
* @param {string} outputDir
|
|
35
|
+
* @param {Object} results - Verification results object with viewports map
|
|
36
|
+
* @returns {Promise<string>} Path to written report
|
|
37
|
+
*/
|
|
38
|
+
export async function writeReport(outputDir, results) {
|
|
39
|
+
const reportPath = path.join(outputDir, 'layout-verification.md');
|
|
40
|
+
|
|
41
|
+
let report = `# Layout Verification Report\n\nGenerated: ${new Date().toISOString()}\n\n## Summary\n\n`;
|
|
42
|
+
report += `| Viewport | Similarity | Issues |\n|----------|------------|--------|\n`;
|
|
43
|
+
|
|
44
|
+
for (const [viewport, result] of Object.entries(results.viewports)) {
|
|
45
|
+
const score = result.similarity_score || 0;
|
|
46
|
+
const issues = result.discrepancies?.length || 0;
|
|
47
|
+
const status = score >= 90 ? '✅' : score >= 70 ? '⚠️' : '❌';
|
|
48
|
+
report += `| ${viewport} | ${status} ${score}% | ${issues} |\n`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
report += `\n## Overall Score: ${results.overall_score}%\n\n`;
|
|
52
|
+
|
|
53
|
+
for (const [viewport, result] of Object.entries(results.viewports)) {
|
|
54
|
+
const vp = VIEWPORTS[viewport];
|
|
55
|
+
report += `## ${viewport.charAt(0).toUpperCase() + viewport.slice(1)} (${vp.width}x${vp.height})\n\n`;
|
|
56
|
+
|
|
57
|
+
if (result.overall_assessment) {
|
|
58
|
+
report += `**Assessment:** ${result.overall_assessment}\n\n`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (result.discrepancies?.length > 0) {
|
|
62
|
+
report += `### Discrepancies\n\n`;
|
|
63
|
+
for (const disc of result.discrepancies) {
|
|
64
|
+
const icon = disc.severity === 'critical' ? '🔴' : disc.severity === 'major' ? '🟠' : '🟡';
|
|
65
|
+
report += `${icon} **${disc.section}** (${disc.severity})\n`;
|
|
66
|
+
report += ` - Issue: ${disc.issue}\n`;
|
|
67
|
+
if (disc.css_fix) report += ` - Fix: \`${disc.css_fix}\`\n`;
|
|
68
|
+
report += '\n';
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
report += `✅ No significant discrepancies found.\n\n`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (result.recommendations?.length > 0) {
|
|
75
|
+
report += `### Recommendations\n\n`;
|
|
76
|
+
for (const rec of result.recommendations) {
|
|
77
|
+
report += `- ${rec}\n`;
|
|
78
|
+
}
|
|
79
|
+
report += '\n';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Consolidated CSS fixes
|
|
84
|
+
const allFixes = [];
|
|
85
|
+
for (const result of Object.values(results.viewports)) {
|
|
86
|
+
if (result.discrepancies) {
|
|
87
|
+
allFixes.push(...generateCSSFixes(result.discrepancies));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (allFixes.length > 0) {
|
|
92
|
+
report += `## Suggested CSS Fixes\n\n\`\`\`css\n`;
|
|
93
|
+
for (const fix of allFixes) {
|
|
94
|
+
report += `/* ${fix.section}: ${fix.issue} */\n${fix.fix}\n\n`;
|
|
95
|
+
}
|
|
96
|
+
report += `\`\`\`\n`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await fs.writeFile(reportPath, report);
|
|
100
|
+
return reportPath;
|
|
101
|
+
}
|