design-clone 1.2.0 ā 2.1.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 +26 -12
- package/bin/commands/clone-site.js +75 -10
- package/bin/commands/init.js +33 -1
- package/bin/commands/verify.js +5 -3
- package/bin/utils/validate.js +24 -8
- package/docs/cli-reference.md +200 -2
- package/docs/codebase-summary.md +309 -0
- package/docs/design-clone-architecture.md +259 -42
- package/docs/pixel-perfect.md +35 -4
- package/docs/project-roadmap.md +382 -0
- package/docs/troubleshooting.md +5 -4
- package/package.json +10 -8
- 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 +73 -3
- package/src/ai/extract-design-tokens.py +356 -13
- 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 +133 -0
- package/src/ai/prompts/structure_analysis.py +329 -10
- package/src/ai/prompts/ux_audit.py +198 -0
- package/src/ai/ux-audit.js +596 -0
- package/src/core/app-state-snapshot.js +511 -0
- package/src/core/content-counter.js +342 -0
- package/src/core/cookie-handler.js +1 -1
- package/src/core/css-extractor.js +4 -4
- package/src/core/dimension-extractor.js +93 -21
- package/src/core/dimension-output.js +103 -6
- package/src/core/discover-pages.js +242 -14
- package/src/core/dom-tree-analyzer.js +298 -0
- package/src/core/extract-assets.js +1 -1
- package/src/core/framework-detector.js +538 -0
- package/src/core/html-extractor.js +45 -4
- package/src/core/lazy-loader.js +7 -7
- package/src/core/multi-page-screenshot.js +9 -6
- package/src/core/page-readiness.js +8 -8
- package/src/core/screenshot.js +138 -9
- package/src/core/section-cropper.js +209 -0
- package/src/core/section-detector.js +386 -0
- package/src/core/semantic-enhancer.js +492 -0
- package/src/core/state-capture.js +18 -22
- package/src/core/tests/test-section-cropper.js +177 -0
- package/src/core/tests/test-section-detector.js +55 -0
- package/src/core/video-capture.js +152 -146
- package/src/route-discoverers/angular-discoverer.js +157 -0
- package/src/route-discoverers/astro-discoverer.js +123 -0
- package/src/route-discoverers/base-discoverer.js +242 -0
- package/src/route-discoverers/index.js +106 -0
- package/src/route-discoverers/next-discoverer.js +130 -0
- package/src/route-discoverers/nuxt-discoverer.js +138 -0
- package/src/route-discoverers/react-discoverer.js +139 -0
- package/src/route-discoverers/svelte-discoverer.js +109 -0
- package/src/route-discoverers/universal-discoverer.js +227 -0
- package/src/route-discoverers/vue-discoverer.js +118 -0
- package/src/utils/__init__.py +1 -1
- package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/utils/browser.js +11 -37
- package/src/utils/playwright.js +213 -0
- package/src/verification/generate-audit-report.js +398 -0
- package/src/verification/verify-footer.js +493 -0
- package/src/verification/verify-header.js +486 -0
- package/src/verification/verify-layout.js +2 -2
- package/src/verification/verify-menu.js +4 -20
- package/src/verification/verify-slider.js +533 -0
- package/src/utils/puppeteer.js +0 -281
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Header Verification Script
|
|
4
|
+
*
|
|
5
|
+
* Tests header components across viewports:
|
|
6
|
+
* - Logo presence and positioning
|
|
7
|
+
* - Navigation links visibility
|
|
8
|
+
* - CTA buttons
|
|
9
|
+
* - Sticky/fixed behavior
|
|
10
|
+
* - Z-index layering
|
|
11
|
+
* - Height consistency
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* node verify-header.js --html <path> [--verbose]
|
|
15
|
+
* node verify-header.js --url <url> [--verbose]
|
|
16
|
+
*
|
|
17
|
+
* Options:
|
|
18
|
+
* --html Path to local HTML file
|
|
19
|
+
* --url URL to test
|
|
20
|
+
* --output Output directory for screenshots
|
|
21
|
+
* --verbose Show detailed progress
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import fs from 'fs/promises';
|
|
25
|
+
import path from 'path';
|
|
26
|
+
|
|
27
|
+
import { getBrowser, getPage, closeBrowser, disconnectBrowser, parseArgs, outputJSON, outputError } from '../utils/browser.js';
|
|
28
|
+
|
|
29
|
+
// Viewport configurations
|
|
30
|
+
const VIEWPORTS = {
|
|
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
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Validate HTML file path (security: prevent path traversal)
|
|
362
|
+
*/
|
|
363
|
+
function validateHtmlPath(htmlPath) {
|
|
364
|
+
const absolutePath = path.resolve(htmlPath);
|
|
365
|
+
const cwd = process.cwd();
|
|
366
|
+
|
|
367
|
+
// Allow paths within CWD or common output directories
|
|
368
|
+
const allowedPrefixes = [
|
|
369
|
+
cwd,
|
|
370
|
+
path.join(process.env.HOME || '', '.claude'),
|
|
371
|
+
'/tmp',
|
|
372
|
+
path.join(process.env.HOME || '', 'cloned-designs')
|
|
373
|
+
];
|
|
374
|
+
|
|
375
|
+
const isAllowed = allowedPrefixes.some(prefix => absolutePath.startsWith(prefix));
|
|
376
|
+
if (!isAllowed) {
|
|
377
|
+
throw new Error(`Path "${htmlPath}" is outside allowed directories`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return absolutePath;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Main verification function
|
|
385
|
+
*/
|
|
386
|
+
async function verifyHeader() {
|
|
387
|
+
const args = parseArgs(process.argv.slice(2));
|
|
388
|
+
|
|
389
|
+
if (!args.html && !args.url) {
|
|
390
|
+
outputError(new Error('Either --html or --url is required'));
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const verbose = args.verbose === 'true';
|
|
395
|
+
const outputDir = args.output;
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
const browser = await getBrowser({ headless: args.headless !== 'false' });
|
|
399
|
+
const page = await getPage(browser);
|
|
400
|
+
|
|
401
|
+
let targetUrl;
|
|
402
|
+
if (args.html) {
|
|
403
|
+
const absolutePath = validateHtmlPath(args.html);
|
|
404
|
+
targetUrl = `file://${absolutePath}`;
|
|
405
|
+
} else {
|
|
406
|
+
targetUrl = args.url;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (verbose) console.error(`\nš Verifying header: ${targetUrl}\n`);
|
|
410
|
+
|
|
411
|
+
await page.goto(targetUrl, {
|
|
412
|
+
waitUntil: 'networkidle',
|
|
413
|
+
timeout: 30000
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const results = {
|
|
417
|
+
success: true,
|
|
418
|
+
component: 'header',
|
|
419
|
+
url: targetUrl,
|
|
420
|
+
viewports: {},
|
|
421
|
+
summary: {
|
|
422
|
+
totalTests: 0,
|
|
423
|
+
passed: 0,
|
|
424
|
+
failed: 0,
|
|
425
|
+
warnings: []
|
|
426
|
+
},
|
|
427
|
+
screenshots: [],
|
|
428
|
+
heightConsistency: {}
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
// Test all viewports
|
|
432
|
+
for (const viewportName of ['mobile', 'tablet', 'desktop']) {
|
|
433
|
+
const viewportResult = await testViewport(page, viewportName, verbose);
|
|
434
|
+
results.viewports[viewportName] = viewportResult;
|
|
435
|
+
|
|
436
|
+
results.summary.totalTests += viewportResult.tests.length;
|
|
437
|
+
results.summary.passed += viewportResult.passed;
|
|
438
|
+
results.summary.failed += viewportResult.failed;
|
|
439
|
+
results.summary.warnings.push(...viewportResult.warnings);
|
|
440
|
+
|
|
441
|
+
if (viewportResult.headerHeight) {
|
|
442
|
+
results.heightConsistency[viewportName] = viewportResult.headerHeight;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (outputDir) {
|
|
446
|
+
const screenshotPath = await captureHeaderScreenshot(page, outputDir, viewportName);
|
|
447
|
+
if (screenshotPath) results.screenshots.push(screenshotPath);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Check height consistency
|
|
452
|
+
const heights = Object.values(results.heightConsistency);
|
|
453
|
+
if (heights.length >= 2) {
|
|
454
|
+
const maxDiff = Math.max(...heights) - Math.min(...heights);
|
|
455
|
+
if (maxDiff > 20) {
|
|
456
|
+
results.summary.warnings.push(`Header height varies by ${maxDiff}px across viewports`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
results.success = results.summary.failed === 0;
|
|
461
|
+
|
|
462
|
+
if (args.close === 'true') {
|
|
463
|
+
await closeBrowser();
|
|
464
|
+
} else {
|
|
465
|
+
await disconnectBrowser();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (verbose) {
|
|
469
|
+
console.error('\nš Summary:');
|
|
470
|
+
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
|
+
}
|
|
474
|
+
console.error(` Status: ${results.success ? 'ā PASS' : 'ā FAIL'}\n`);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
outputJSON(results);
|
|
478
|
+
process.exit(results.success ? 0 : 1);
|
|
479
|
+
|
|
480
|
+
} catch (error) {
|
|
481
|
+
outputError(error);
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
verifyHeader();
|
|
@@ -36,7 +36,7 @@ const VIEWPORTS = {
|
|
|
36
36
|
* Capture screenshot of generated HTML at specific viewport
|
|
37
37
|
*/
|
|
38
38
|
async function captureGeneratedScreenshot(page, viewport, outputPath) {
|
|
39
|
-
await page.
|
|
39
|
+
await page.setViewportSize(viewport);
|
|
40
40
|
await new Promise(r => setTimeout(r, 500)); // Wait for CSS to apply
|
|
41
41
|
|
|
42
42
|
await page.screenshot({
|
|
@@ -328,7 +328,7 @@ async function verifyLayout() {
|
|
|
328
328
|
const targetUrl = `file://${absolutePath}`;
|
|
329
329
|
|
|
330
330
|
await page.goto(targetUrl, {
|
|
331
|
-
waitUntil: '
|
|
331
|
+
waitUntil: 'networkidle',
|
|
332
332
|
timeout: 30000
|
|
333
333
|
});
|
|
334
334
|
|
|
@@ -81,27 +81,11 @@ const MENU_SELECTORS = {
|
|
|
81
81
|
};
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
|
-
* Check if element is visible
|
|
84
|
+
* Check if element is visible using Playwright locator API
|
|
85
85
|
*/
|
|
86
86
|
async function isElementVisible(page, selector) {
|
|
87
87
|
try {
|
|
88
|
-
|
|
89
|
-
if (!element) return false;
|
|
90
|
-
|
|
91
|
-
const isVisible = await page.evaluate(el => {
|
|
92
|
-
const style = window.getComputedStyle(el);
|
|
93
|
-
const rect = el.getBoundingClientRect();
|
|
94
|
-
|
|
95
|
-
return (
|
|
96
|
-
style.display !== 'none' &&
|
|
97
|
-
style.visibility !== 'hidden' &&
|
|
98
|
-
style.opacity !== '0' &&
|
|
99
|
-
rect.width > 0 &&
|
|
100
|
-
rect.height > 0
|
|
101
|
-
);
|
|
102
|
-
}, element);
|
|
103
|
-
|
|
104
|
-
return isVisible;
|
|
88
|
+
return await page.locator(selector).isVisible();
|
|
105
89
|
} catch {
|
|
106
90
|
return false;
|
|
107
91
|
}
|
|
@@ -158,7 +142,7 @@ async function countVisibleMenuItems(page) {
|
|
|
158
142
|
*/
|
|
159
143
|
async function testViewport(page, viewportName, verbose = false) {
|
|
160
144
|
const viewport = VIEWPORTS[viewportName];
|
|
161
|
-
await page.
|
|
145
|
+
await page.setViewportSize(viewport);
|
|
162
146
|
await new Promise(r => setTimeout(r, 500)); // Wait for CSS to apply
|
|
163
147
|
|
|
164
148
|
const result = {
|
|
@@ -355,7 +339,7 @@ async function verifyMenu() {
|
|
|
355
339
|
if (verbose) console.error(`\nš Verifying responsive menu: ${targetUrl}\n`);
|
|
356
340
|
|
|
357
341
|
await page.goto(targetUrl, {
|
|
358
|
-
waitUntil: '
|
|
342
|
+
waitUntil: 'networkidle',
|
|
359
343
|
timeout: 30000
|
|
360
344
|
});
|
|
361
345
|
|