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,493 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Footer Verification Script
|
|
4
|
+
*
|
|
5
|
+
* Tests footer components across viewports:
|
|
6
|
+
* - Position at bottom of page
|
|
7
|
+
* - Multi-column layout detection
|
|
8
|
+
* - Link sections completeness
|
|
9
|
+
* - Copyright text presence
|
|
10
|
+
* - Social icons
|
|
11
|
+
* - Background contrast
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* node verify-footer.js --html <path> [--verbose]
|
|
15
|
+
* node verify-footer.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
|
+
// Footer element selectors
|
|
37
|
+
const FOOTER_SELECTORS = {
|
|
38
|
+
container: [
|
|
39
|
+
'footer',
|
|
40
|
+
'[role="contentinfo"]',
|
|
41
|
+
'.footer',
|
|
42
|
+
'#footer',
|
|
43
|
+
'.site-footer',
|
|
44
|
+
'.page-footer'
|
|
45
|
+
],
|
|
46
|
+
columns: [
|
|
47
|
+
'footer [class*="column"]',
|
|
48
|
+
'footer [class*="col-"]',
|
|
49
|
+
'footer .col',
|
|
50
|
+
'.footer-column',
|
|
51
|
+
'.footer-widget',
|
|
52
|
+
'.footer-section',
|
|
53
|
+
'footer > div > div'
|
|
54
|
+
],
|
|
55
|
+
links: [
|
|
56
|
+
'footer a[href]',
|
|
57
|
+
'.footer-links a',
|
|
58
|
+
'.footer-nav a',
|
|
59
|
+
'footer nav a',
|
|
60
|
+
'footer ul a'
|
|
61
|
+
],
|
|
62
|
+
copyright: [
|
|
63
|
+
'footer [class*="copyright"]',
|
|
64
|
+
'.copyright',
|
|
65
|
+
'footer small',
|
|
66
|
+
'footer p:last-child'
|
|
67
|
+
],
|
|
68
|
+
socialIcons: [
|
|
69
|
+
'footer a[href*="facebook"]',
|
|
70
|
+
'footer a[href*="twitter"]',
|
|
71
|
+
'footer a[href*="instagram"]',
|
|
72
|
+
'footer a[href*="linkedin"]',
|
|
73
|
+
'footer a[href*="youtube"]',
|
|
74
|
+
'footer [class*="social"]',
|
|
75
|
+
'.social-links a',
|
|
76
|
+
'.social-icons a'
|
|
77
|
+
]
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Find first matching element
|
|
82
|
+
*/
|
|
83
|
+
async function findElement(page, selectors) {
|
|
84
|
+
for (const selector of selectors) {
|
|
85
|
+
try {
|
|
86
|
+
const element = await page.$(selector);
|
|
87
|
+
if (element) {
|
|
88
|
+
return { element, selector };
|
|
89
|
+
}
|
|
90
|
+
} catch (err) { /* continue - selector not found */ }
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Count matching elements
|
|
97
|
+
*/
|
|
98
|
+
async function countElements(page, selectors) {
|
|
99
|
+
let totalCount = 0;
|
|
100
|
+
let matchedSelector = null;
|
|
101
|
+
|
|
102
|
+
for (const selector of selectors) {
|
|
103
|
+
try {
|
|
104
|
+
const count = await page.locator(selector).count();
|
|
105
|
+
if (count > totalCount) {
|
|
106
|
+
totalCount = count;
|
|
107
|
+
matchedSelector = selector;
|
|
108
|
+
}
|
|
109
|
+
} catch (err) { /* continue - selector not found */ }
|
|
110
|
+
}
|
|
111
|
+
return { count: totalCount, selector: matchedSelector };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Count visible elements
|
|
116
|
+
*/
|
|
117
|
+
async function countVisibleElements(page, selectors) {
|
|
118
|
+
for (const selector of selectors) {
|
|
119
|
+
try {
|
|
120
|
+
const count = await page.evaluate((sel) => {
|
|
121
|
+
const items = document.querySelectorAll(sel);
|
|
122
|
+
let visible = 0;
|
|
123
|
+
items.forEach(item => {
|
|
124
|
+
const style = window.getComputedStyle(item);
|
|
125
|
+
const rect = item.getBoundingClientRect();
|
|
126
|
+
if (
|
|
127
|
+
style.display !== 'none' &&
|
|
128
|
+
style.visibility !== 'hidden' &&
|
|
129
|
+
style.opacity !== '0' &&
|
|
130
|
+
rect.width > 0 &&
|
|
131
|
+
rect.height > 0
|
|
132
|
+
) {
|
|
133
|
+
visible++;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return visible;
|
|
137
|
+
}, selector);
|
|
138
|
+
|
|
139
|
+
if (count > 0) {
|
|
140
|
+
return { count, selector };
|
|
141
|
+
}
|
|
142
|
+
} catch (err) { /* continue - selector not found */ }
|
|
143
|
+
}
|
|
144
|
+
return { count: 0, selector: null };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Check footer position (should be at bottom)
|
|
149
|
+
*/
|
|
150
|
+
async function checkFooterPosition(page, footerSelector) {
|
|
151
|
+
return await page.evaluate((sel) => {
|
|
152
|
+
const footer = document.querySelector(sel);
|
|
153
|
+
if (!footer) return null;
|
|
154
|
+
|
|
155
|
+
const rect = footer.getBoundingClientRect();
|
|
156
|
+
const scrollHeight = Math.max(
|
|
157
|
+
document.body.scrollHeight,
|
|
158
|
+
document.documentElement.scrollHeight
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// Scroll to bottom to get accurate position
|
|
162
|
+
window.scrollTo(0, scrollHeight);
|
|
163
|
+
|
|
164
|
+
const style = window.getComputedStyle(footer);
|
|
165
|
+
const footerBottom = rect.y + window.scrollY + rect.height;
|
|
166
|
+
const tolerance = 50; // Allow 50px tolerance
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
y: rect.y + window.scrollY,
|
|
170
|
+
height: rect.height,
|
|
171
|
+
width: rect.width,
|
|
172
|
+
pageHeight: scrollHeight,
|
|
173
|
+
isAtBottom: footerBottom >= (scrollHeight - tolerance),
|
|
174
|
+
footerBottom,
|
|
175
|
+
backgroundColor: style.backgroundColor,
|
|
176
|
+
color: style.color
|
|
177
|
+
};
|
|
178
|
+
}, footerSelector);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Check for copyright text
|
|
183
|
+
*/
|
|
184
|
+
async function checkCopyright(page) {
|
|
185
|
+
return await page.evaluate(() => {
|
|
186
|
+
const footer = document.querySelector('footer') || document.querySelector('[role="contentinfo"]');
|
|
187
|
+
if (!footer) return null;
|
|
188
|
+
|
|
189
|
+
const text = footer.textContent || '';
|
|
190
|
+
const currentYear = new Date().getFullYear();
|
|
191
|
+
|
|
192
|
+
const hasCopyright = /©|copyright|all rights reserved/i.test(text);
|
|
193
|
+
const hasYear = new RegExp(`20[0-9]{2}|${currentYear}`).test(text);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
hasCopyright,
|
|
197
|
+
hasYear,
|
|
198
|
+
hasCurrentYear: text.includes(String(currentYear))
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Test footer at specific viewport
|
|
205
|
+
*/
|
|
206
|
+
async function testViewport(page, viewportName, verbose = false) {
|
|
207
|
+
const viewport = VIEWPORTS[viewportName];
|
|
208
|
+
await page.setViewportSize(viewport);
|
|
209
|
+
await new Promise(r => setTimeout(r, 500));
|
|
210
|
+
|
|
211
|
+
// Scroll to bottom to ensure footer is loaded
|
|
212
|
+
await page.evaluate(() => {
|
|
213
|
+
window.scrollTo(0, document.body.scrollHeight);
|
|
214
|
+
});
|
|
215
|
+
await new Promise(r => setTimeout(r, 300));
|
|
216
|
+
|
|
217
|
+
const result = {
|
|
218
|
+
viewport: viewportName,
|
|
219
|
+
dimensions: viewport,
|
|
220
|
+
tests: [],
|
|
221
|
+
passed: 0,
|
|
222
|
+
failed: 0,
|
|
223
|
+
warnings: []
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
if (verbose) console.error(`\n📱 Testing ${viewportName} (${viewport.width}x${viewport.height})...`);
|
|
227
|
+
|
|
228
|
+
// Test 1: Footer container exists
|
|
229
|
+
const footerResult = await findElement(page, FOOTER_SELECTORS.container);
|
|
230
|
+
if (footerResult) {
|
|
231
|
+
result.tests.push({
|
|
232
|
+
name: 'Footer container exists',
|
|
233
|
+
passed: true,
|
|
234
|
+
selector: footerResult.selector
|
|
235
|
+
});
|
|
236
|
+
result.passed++;
|
|
237
|
+
if (verbose) console.error(` ✓ Footer found: ${footerResult.selector}`);
|
|
238
|
+
|
|
239
|
+
// Test 2: Footer position (at bottom)
|
|
240
|
+
const positionInfo = await checkFooterPosition(page, footerResult.selector);
|
|
241
|
+
if (positionInfo) {
|
|
242
|
+
if (positionInfo.isAtBottom) {
|
|
243
|
+
result.tests.push({
|
|
244
|
+
name: 'Footer at page bottom',
|
|
245
|
+
passed: true,
|
|
246
|
+
y: positionInfo.y,
|
|
247
|
+
pageHeight: positionInfo.pageHeight
|
|
248
|
+
});
|
|
249
|
+
result.passed++;
|
|
250
|
+
if (verbose) console.error(` ✓ Footer at bottom (y: ${Math.round(positionInfo.y)})`);
|
|
251
|
+
} else {
|
|
252
|
+
result.tests.push({
|
|
253
|
+
name: 'Footer at page bottom',
|
|
254
|
+
passed: false,
|
|
255
|
+
y: positionInfo.y,
|
|
256
|
+
footerBottom: positionInfo.footerBottom,
|
|
257
|
+
pageHeight: positionInfo.pageHeight,
|
|
258
|
+
error: 'Footer not at page bottom'
|
|
259
|
+
});
|
|
260
|
+
result.failed++;
|
|
261
|
+
if (verbose) console.error(` ✗ Footer not at bottom (gap: ${positionInfo.pageHeight - positionInfo.footerBottom}px)`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Store dimensions for report
|
|
265
|
+
result.footerDimensions = {
|
|
266
|
+
height: positionInfo.height,
|
|
267
|
+
width: positionInfo.width,
|
|
268
|
+
backgroundColor: positionInfo.backgroundColor,
|
|
269
|
+
color: positionInfo.color
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Test 3: Multi-column layout (desktop/tablet)
|
|
274
|
+
if (viewportName !== 'mobile') {
|
|
275
|
+
const columns = await countElements(page, FOOTER_SELECTORS.columns);
|
|
276
|
+
if (columns.count >= 2) {
|
|
277
|
+
result.tests.push({
|
|
278
|
+
name: 'Multi-column layout',
|
|
279
|
+
passed: true,
|
|
280
|
+
count: columns.count,
|
|
281
|
+
selector: columns.selector
|
|
282
|
+
});
|
|
283
|
+
result.passed++;
|
|
284
|
+
if (verbose) console.error(` ✓ ${columns.count} columns found`);
|
|
285
|
+
} else if (columns.count === 1) {
|
|
286
|
+
result.tests.push({
|
|
287
|
+
name: 'Multi-column layout',
|
|
288
|
+
passed: true,
|
|
289
|
+
count: columns.count,
|
|
290
|
+
note: 'Single column layout'
|
|
291
|
+
});
|
|
292
|
+
result.passed++;
|
|
293
|
+
if (verbose) console.error(` ✓ Single column layout`);
|
|
294
|
+
} else {
|
|
295
|
+
result.warnings.push('No clear column structure detected');
|
|
296
|
+
if (verbose) console.error(` ⚠ No column structure detected`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Test 4: Links present
|
|
301
|
+
const links = await countVisibleElements(page, FOOTER_SELECTORS.links);
|
|
302
|
+
if (links.count >= 1) {
|
|
303
|
+
result.tests.push({
|
|
304
|
+
name: 'Footer links present',
|
|
305
|
+
passed: true,
|
|
306
|
+
count: links.count,
|
|
307
|
+
selector: links.selector
|
|
308
|
+
});
|
|
309
|
+
result.passed++;
|
|
310
|
+
if (verbose) console.error(` ✓ ${links.count} links found`);
|
|
311
|
+
} else {
|
|
312
|
+
result.warnings.push('No links found in footer');
|
|
313
|
+
if (verbose) console.error(` ⚠ No links found`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Test 5: Copyright text
|
|
317
|
+
const copyrightInfo = await checkCopyright(page);
|
|
318
|
+
if (copyrightInfo) {
|
|
319
|
+
if (copyrightInfo.hasCopyright || copyrightInfo.hasYear) {
|
|
320
|
+
result.tests.push({
|
|
321
|
+
name: 'Copyright text present',
|
|
322
|
+
passed: true,
|
|
323
|
+
hasCopyright: copyrightInfo.hasCopyright,
|
|
324
|
+
hasCurrentYear: copyrightInfo.hasCurrentYear
|
|
325
|
+
});
|
|
326
|
+
result.passed++;
|
|
327
|
+
if (verbose) console.error(` ✓ Copyright found (current year: ${copyrightInfo.hasCurrentYear})`);
|
|
328
|
+
} else {
|
|
329
|
+
result.warnings.push('No copyright text found');
|
|
330
|
+
if (verbose) console.error(` ⚠ No copyright text`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Test 6: Social icons (optional)
|
|
335
|
+
const socialIcons = await countVisibleElements(page, FOOTER_SELECTORS.socialIcons);
|
|
336
|
+
if (socialIcons.count > 0) {
|
|
337
|
+
result.tests.push({
|
|
338
|
+
name: 'Social icons present',
|
|
339
|
+
passed: true,
|
|
340
|
+
count: socialIcons.count
|
|
341
|
+
});
|
|
342
|
+
result.passed++;
|
|
343
|
+
if (verbose) console.error(` ✓ ${socialIcons.count} social icons found`);
|
|
344
|
+
} else {
|
|
345
|
+
// Not a failure, just informational
|
|
346
|
+
if (verbose) console.error(` ℹ No social icons found`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
} else {
|
|
350
|
+
result.tests.push({
|
|
351
|
+
name: 'Footer container exists',
|
|
352
|
+
passed: false,
|
|
353
|
+
error: 'No footer container found'
|
|
354
|
+
});
|
|
355
|
+
result.failed++;
|
|
356
|
+
if (verbose) console.error(` ✗ Footer not found`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return result;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Capture footer screenshot
|
|
364
|
+
*/
|
|
365
|
+
async function captureFooterScreenshot(page, outputDir, viewportName) {
|
|
366
|
+
if (!outputDir) return null;
|
|
367
|
+
|
|
368
|
+
// Scroll to footer first
|
|
369
|
+
await page.evaluate(() => {
|
|
370
|
+
const footer = document.querySelector('footer') || document.querySelector('[role="contentinfo"]');
|
|
371
|
+
if (footer) footer.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
372
|
+
});
|
|
373
|
+
await new Promise(r => setTimeout(r, 200));
|
|
374
|
+
|
|
375
|
+
const screenshotPath = path.join(outputDir, `footer-test-${viewportName}.png`);
|
|
376
|
+
await page.screenshot({
|
|
377
|
+
path: screenshotPath,
|
|
378
|
+
fullPage: false
|
|
379
|
+
});
|
|
380
|
+
return screenshotPath;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Validate HTML file path (security: prevent path traversal)
|
|
385
|
+
*/
|
|
386
|
+
function validateHtmlPath(htmlPath) {
|
|
387
|
+
const absolutePath = path.resolve(htmlPath);
|
|
388
|
+
const cwd = process.cwd();
|
|
389
|
+
|
|
390
|
+
const allowedPrefixes = [
|
|
391
|
+
cwd,
|
|
392
|
+
path.join(process.env.HOME || '', '.claude'),
|
|
393
|
+
'/tmp',
|
|
394
|
+
path.join(process.env.HOME || '', 'cloned-designs')
|
|
395
|
+
];
|
|
396
|
+
|
|
397
|
+
const isAllowed = allowedPrefixes.some(prefix => absolutePath.startsWith(prefix));
|
|
398
|
+
if (!isAllowed) {
|
|
399
|
+
throw new Error(`Path "${htmlPath}" is outside allowed directories`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return absolutePath;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Main verification function
|
|
407
|
+
*/
|
|
408
|
+
async function verifyFooter() {
|
|
409
|
+
const args = parseArgs(process.argv.slice(2));
|
|
410
|
+
|
|
411
|
+
if (!args.html && !args.url) {
|
|
412
|
+
outputError(new Error('Either --html or --url is required'));
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const verbose = args.verbose === 'true';
|
|
417
|
+
const outputDir = args.output;
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
const browser = await getBrowser({ headless: args.headless !== 'false' });
|
|
421
|
+
const page = await getPage(browser);
|
|
422
|
+
|
|
423
|
+
let targetUrl;
|
|
424
|
+
if (args.html) {
|
|
425
|
+
const absolutePath = validateHtmlPath(args.html);
|
|
426
|
+
targetUrl = `file://${absolutePath}`;
|
|
427
|
+
} else {
|
|
428
|
+
targetUrl = args.url;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (verbose) console.error(`\n🔍 Verifying footer: ${targetUrl}\n`);
|
|
432
|
+
|
|
433
|
+
await page.goto(targetUrl, {
|
|
434
|
+
waitUntil: 'networkidle',
|
|
435
|
+
timeout: 30000
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const results = {
|
|
439
|
+
success: true,
|
|
440
|
+
component: 'footer',
|
|
441
|
+
url: targetUrl,
|
|
442
|
+
viewports: {},
|
|
443
|
+
summary: {
|
|
444
|
+
totalTests: 0,
|
|
445
|
+
passed: 0,
|
|
446
|
+
failed: 0,
|
|
447
|
+
warnings: []
|
|
448
|
+
},
|
|
449
|
+
screenshots: []
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
for (const viewportName of ['mobile', 'tablet', 'desktop']) {
|
|
453
|
+
const viewportResult = await testViewport(page, viewportName, verbose);
|
|
454
|
+
results.viewports[viewportName] = viewportResult;
|
|
455
|
+
|
|
456
|
+
results.summary.totalTests += viewportResult.tests.length;
|
|
457
|
+
results.summary.passed += viewportResult.passed;
|
|
458
|
+
results.summary.failed += viewportResult.failed;
|
|
459
|
+
results.summary.warnings.push(...viewportResult.warnings);
|
|
460
|
+
|
|
461
|
+
if (outputDir) {
|
|
462
|
+
const screenshotPath = await captureFooterScreenshot(page, outputDir, viewportName);
|
|
463
|
+
if (screenshotPath) results.screenshots.push(screenshotPath);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
results.success = results.summary.failed === 0;
|
|
468
|
+
|
|
469
|
+
if (args.close === 'true') {
|
|
470
|
+
await closeBrowser();
|
|
471
|
+
} else {
|
|
472
|
+
await disconnectBrowser();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (verbose) {
|
|
476
|
+
console.error('\n📊 Summary:');
|
|
477
|
+
console.error(` Tests: ${results.summary.passed}/${results.summary.totalTests} passed`);
|
|
478
|
+
if (results.summary.warnings.length > 0) {
|
|
479
|
+
console.error(` Warnings: ${results.summary.warnings.length}`);
|
|
480
|
+
}
|
|
481
|
+
console.error(` Status: ${results.success ? '✓ PASS' : '✗ FAIL'}\n`);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
outputJSON(results);
|
|
485
|
+
process.exit(results.success ? 0 : 1);
|
|
486
|
+
|
|
487
|
+
} catch (error) {
|
|
488
|
+
outputError(error);
|
|
489
|
+
process.exit(1);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
verifyFooter();
|