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,533 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Slider/Carousel Verification Script
|
|
4
|
+
*
|
|
5
|
+
* Tests slider components:
|
|
6
|
+
* - Library detection (Swiper, Slick, Owl, native)
|
|
7
|
+
* - Arrow/navigation visibility
|
|
8
|
+
* - Pagination dots presence
|
|
9
|
+
* - Autoplay detection (requires 2 slide changes in 6s)
|
|
10
|
+
* - Current slide indicator
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* node verify-slider.js --html <path> [--verbose]
|
|
14
|
+
* node verify-slider.js --url <url> [--verbose]
|
|
15
|
+
*
|
|
16
|
+
* Options:
|
|
17
|
+
* --html Path to local HTML file
|
|
18
|
+
* --url URL to test
|
|
19
|
+
* --output Output directory for screenshots
|
|
20
|
+
* --verbose Show detailed progress
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import fs from 'fs/promises';
|
|
24
|
+
import path from 'path';
|
|
25
|
+
|
|
26
|
+
import { getBrowser, getPage, closeBrowser, disconnectBrowser, parseArgs, outputJSON, outputError } from '../utils/browser.js';
|
|
27
|
+
|
|
28
|
+
// Viewport configurations
|
|
29
|
+
const VIEWPORTS = {
|
|
30
|
+
mobile: { width: 375, height: 812, deviceScaleFactor: 2 },
|
|
31
|
+
tablet: { width: 768, height: 1024, deviceScaleFactor: 1 },
|
|
32
|
+
desktop: { width: 1920, height: 1080, deviceScaleFactor: 1 }
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Slider library patterns
|
|
36
|
+
const SLIDER_PATTERNS = {
|
|
37
|
+
swiper: {
|
|
38
|
+
container: '[class*="swiper"]',
|
|
39
|
+
slide: '.swiper-slide',
|
|
40
|
+
active: '.swiper-slide-active',
|
|
41
|
+
prev: '.swiper-button-prev',
|
|
42
|
+
next: '.swiper-button-next',
|
|
43
|
+
pagination: '.swiper-pagination'
|
|
44
|
+
},
|
|
45
|
+
slick: {
|
|
46
|
+
container: '[class*="slick"]',
|
|
47
|
+
slide: '.slick-slide',
|
|
48
|
+
active: '.slick-active, .slick-current',
|
|
49
|
+
prev: '.slick-prev',
|
|
50
|
+
next: '.slick-next',
|
|
51
|
+
pagination: '.slick-dots'
|
|
52
|
+
},
|
|
53
|
+
owl: {
|
|
54
|
+
container: '[class*="owl"]',
|
|
55
|
+
slide: '.owl-item',
|
|
56
|
+
active: '.owl-item.active',
|
|
57
|
+
prev: '.owl-prev',
|
|
58
|
+
next: '.owl-next',
|
|
59
|
+
pagination: '.owl-dots'
|
|
60
|
+
},
|
|
61
|
+
splide: {
|
|
62
|
+
container: '.splide',
|
|
63
|
+
slide: '.splide__slide',
|
|
64
|
+
active: '.splide__slide.is-active',
|
|
65
|
+
prev: '.splide__arrow--prev',
|
|
66
|
+
next: '.splide__arrow--next',
|
|
67
|
+
pagination: '.splide__pagination'
|
|
68
|
+
},
|
|
69
|
+
glide: {
|
|
70
|
+
container: '.glide',
|
|
71
|
+
slide: '.glide__slide',
|
|
72
|
+
active: '.glide__slide--active',
|
|
73
|
+
prev: '[data-glide-dir="<"]',
|
|
74
|
+
next: '[data-glide-dir=">"]',
|
|
75
|
+
pagination: '.glide__bullets'
|
|
76
|
+
},
|
|
77
|
+
native: {
|
|
78
|
+
container: '[style*="scroll-snap"], [class*="carousel"], [class*="slider"]',
|
|
79
|
+
slide: '[style*="scroll-snap"] > *, .carousel-item, .slider-item',
|
|
80
|
+
active: '.active, [aria-current="true"]',
|
|
81
|
+
prev: '[class*="prev"], [aria-label*="prev" i]',
|
|
82
|
+
next: '[class*="next"], [aria-label*="next" i]',
|
|
83
|
+
pagination: '[class*="indicator"], [class*="dot"], [role="tablist"]'
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Autoplay detection config
|
|
88
|
+
const AUTOPLAY_CONFIG = {
|
|
89
|
+
waitTime: 6000, // Total wait time in ms
|
|
90
|
+
checkInterval: 1000, // Check every 1s
|
|
91
|
+
requiredChanges: 2 // Require 2 slide changes (per validation)
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Detect which slider library is used
|
|
96
|
+
*/
|
|
97
|
+
async function detectSliderLibrary(page) {
|
|
98
|
+
for (const [name, patterns] of Object.entries(SLIDER_PATTERNS)) {
|
|
99
|
+
try {
|
|
100
|
+
const count = await page.locator(patterns.container).count();
|
|
101
|
+
if (count > 0) {
|
|
102
|
+
return { library: name, patterns };
|
|
103
|
+
}
|
|
104
|
+
} catch (err) { /* continue - selector not found */ }
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check element visibility
|
|
111
|
+
*/
|
|
112
|
+
async function isElementVisible(page, selector) {
|
|
113
|
+
try {
|
|
114
|
+
const element = await page.$(selector);
|
|
115
|
+
if (!element) return false;
|
|
116
|
+
|
|
117
|
+
return await page.evaluate((sel) => {
|
|
118
|
+
const el = document.querySelector(sel);
|
|
119
|
+
if (!el) return false;
|
|
120
|
+
|
|
121
|
+
const style = window.getComputedStyle(el);
|
|
122
|
+
const rect = el.getBoundingClientRect();
|
|
123
|
+
return (
|
|
124
|
+
style.display !== 'none' &&
|
|
125
|
+
style.visibility !== 'hidden' &&
|
|
126
|
+
style.opacity !== '0' &&
|
|
127
|
+
rect.width > 0 &&
|
|
128
|
+
rect.height > 0
|
|
129
|
+
);
|
|
130
|
+
}, selector);
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Count visible elements
|
|
138
|
+
*/
|
|
139
|
+
async function countVisibleElements(page, selector) {
|
|
140
|
+
try {
|
|
141
|
+
return await page.evaluate((sel) => {
|
|
142
|
+
const items = document.querySelectorAll(sel);
|
|
143
|
+
let visible = 0;
|
|
144
|
+
items.forEach(item => {
|
|
145
|
+
const style = window.getComputedStyle(item);
|
|
146
|
+
const rect = item.getBoundingClientRect();
|
|
147
|
+
if (
|
|
148
|
+
style.display !== 'none' &&
|
|
149
|
+
style.visibility !== 'hidden' &&
|
|
150
|
+
style.opacity !== '0' &&
|
|
151
|
+
rect.width > 0 &&
|
|
152
|
+
rect.height > 0
|
|
153
|
+
) {
|
|
154
|
+
visible++;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
return visible;
|
|
158
|
+
}, selector);
|
|
159
|
+
} catch {
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get current active slide index
|
|
166
|
+
*/
|
|
167
|
+
async function getActiveSlideIndex(page, patterns) {
|
|
168
|
+
try {
|
|
169
|
+
return await page.evaluate((selectors) => {
|
|
170
|
+
// Try active selector
|
|
171
|
+
const active = document.querySelector(selectors.active);
|
|
172
|
+
if (active) {
|
|
173
|
+
const slides = document.querySelectorAll(selectors.slide);
|
|
174
|
+
for (let i = 0; i < slides.length; i++) {
|
|
175
|
+
if (slides[i] === active || slides[i].contains(active)) {
|
|
176
|
+
return i;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Fallback: check transform or scroll position
|
|
182
|
+
const container = document.querySelector(selectors.container);
|
|
183
|
+
if (container) {
|
|
184
|
+
const slides = container.querySelectorAll(selectors.slide);
|
|
185
|
+
for (let i = 0; i < slides.length; i++) {
|
|
186
|
+
const rect = slides[i].getBoundingClientRect();
|
|
187
|
+
const containerRect = container.getBoundingClientRect();
|
|
188
|
+
// Check if slide is in view
|
|
189
|
+
if (rect.left >= containerRect.left - 10 && rect.left < containerRect.right) {
|
|
190
|
+
return i;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return -1;
|
|
196
|
+
}, patterns);
|
|
197
|
+
} catch {
|
|
198
|
+
return -1;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Check autoplay by monitoring slide changes
|
|
204
|
+
* Requires 2 changes in 6 seconds (per validated decision)
|
|
205
|
+
* Early exit when required changes detected (performance optimization)
|
|
206
|
+
*/
|
|
207
|
+
async function checkAutoplay(page, patterns, verbose) {
|
|
208
|
+
const slideIndices = [];
|
|
209
|
+
const startTime = Date.now();
|
|
210
|
+
|
|
211
|
+
// Get initial slide
|
|
212
|
+
const initialIndex = await getActiveSlideIndex(page, patterns);
|
|
213
|
+
slideIndices.push({ time: 0, index: initialIndex });
|
|
214
|
+
|
|
215
|
+
if (verbose) console.error(` Starting autoplay detection (max ${AUTOPLAY_CONFIG.waitTime / 1000}s)...`);
|
|
216
|
+
|
|
217
|
+
// Monitor for changes with early exit
|
|
218
|
+
while (Date.now() - startTime < AUTOPLAY_CONFIG.waitTime) {
|
|
219
|
+
await new Promise(r => setTimeout(r, AUTOPLAY_CONFIG.checkInterval));
|
|
220
|
+
|
|
221
|
+
const currentIndex = await getActiveSlideIndex(page, patterns);
|
|
222
|
+
const elapsed = Date.now() - startTime;
|
|
223
|
+
|
|
224
|
+
if (currentIndex !== slideIndices[slideIndices.length - 1].index) {
|
|
225
|
+
slideIndices.push({ time: elapsed, index: currentIndex });
|
|
226
|
+
if (verbose) console.error(` Slide changed to ${currentIndex} at ${elapsed}ms`);
|
|
227
|
+
|
|
228
|
+
// Early exit: if we have required changes, no need to wait longer
|
|
229
|
+
if (slideIndices.length - 1 >= AUTOPLAY_CONFIG.requiredChanges) {
|
|
230
|
+
if (verbose) console.error(` Early exit: ${AUTOPLAY_CONFIG.requiredChanges} changes detected`);
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const changes = slideIndices.length - 1;
|
|
237
|
+
const actualDuration = Date.now() - startTime;
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
hasAutoplay: changes >= AUTOPLAY_CONFIG.requiredChanges,
|
|
241
|
+
changes,
|
|
242
|
+
slideIndices,
|
|
243
|
+
duration: actualDuration
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Test slider at specific viewport
|
|
249
|
+
*/
|
|
250
|
+
async function testViewport(page, viewportName, verbose = false) {
|
|
251
|
+
const viewport = VIEWPORTS[viewportName];
|
|
252
|
+
await page.setViewportSize(viewport);
|
|
253
|
+
await new Promise(r => setTimeout(r, 500));
|
|
254
|
+
|
|
255
|
+
const result = {
|
|
256
|
+
viewport: viewportName,
|
|
257
|
+
dimensions: viewport,
|
|
258
|
+
tests: [],
|
|
259
|
+
passed: 0,
|
|
260
|
+
failed: 0,
|
|
261
|
+
warnings: [],
|
|
262
|
+
sliderInfo: null
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
if (verbose) console.error(`\nš± Testing ${viewportName} (${viewport.width}x${viewport.height})...`);
|
|
266
|
+
|
|
267
|
+
// Test 1: Detect slider library
|
|
268
|
+
const sliderDetection = await detectSliderLibrary(page);
|
|
269
|
+
|
|
270
|
+
if (!sliderDetection) {
|
|
271
|
+
result.tests.push({
|
|
272
|
+
name: 'Slider detection',
|
|
273
|
+
passed: true,
|
|
274
|
+
note: 'No slider/carousel detected on page'
|
|
275
|
+
});
|
|
276
|
+
result.passed++;
|
|
277
|
+
if (verbose) console.error(` ā¹ No slider detected`);
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const { library, patterns } = sliderDetection;
|
|
282
|
+
result.sliderInfo = { library };
|
|
283
|
+
|
|
284
|
+
result.tests.push({
|
|
285
|
+
name: 'Slider detection',
|
|
286
|
+
passed: true,
|
|
287
|
+
library,
|
|
288
|
+
selector: patterns.container
|
|
289
|
+
});
|
|
290
|
+
result.passed++;
|
|
291
|
+
if (verbose) console.error(` ā Slider detected: ${library}`);
|
|
292
|
+
|
|
293
|
+
// Test 2: Slides present
|
|
294
|
+
const slideCount = await countVisibleElements(page, patterns.slide);
|
|
295
|
+
if (slideCount > 0) {
|
|
296
|
+
result.tests.push({
|
|
297
|
+
name: 'Slides present',
|
|
298
|
+
passed: true,
|
|
299
|
+
count: slideCount
|
|
300
|
+
});
|
|
301
|
+
result.passed++;
|
|
302
|
+
result.sliderInfo.slideCount = slideCount;
|
|
303
|
+
if (verbose) console.error(` ā ${slideCount} slides found`);
|
|
304
|
+
} else {
|
|
305
|
+
result.tests.push({
|
|
306
|
+
name: 'Slides present',
|
|
307
|
+
passed: false,
|
|
308
|
+
error: 'No slides found in slider'
|
|
309
|
+
});
|
|
310
|
+
result.failed++;
|
|
311
|
+
if (verbose) console.error(` ā No slides found`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Test 3: Navigation arrows
|
|
315
|
+
const hasPrev = await isElementVisible(page, patterns.prev);
|
|
316
|
+
const hasNext = await isElementVisible(page, patterns.next);
|
|
317
|
+
|
|
318
|
+
if (hasPrev || hasNext) {
|
|
319
|
+
result.tests.push({
|
|
320
|
+
name: 'Navigation arrows',
|
|
321
|
+
passed: true,
|
|
322
|
+
hasPrev,
|
|
323
|
+
hasNext
|
|
324
|
+
});
|
|
325
|
+
result.passed++;
|
|
326
|
+
if (verbose) console.error(` ā Navigation arrows: prev=${hasPrev}, next=${hasNext}`);
|
|
327
|
+
} else {
|
|
328
|
+
result.warnings.push('No navigation arrows visible');
|
|
329
|
+
if (verbose) console.error(` ā No navigation arrows found`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Test 4: Pagination dots
|
|
333
|
+
const hasPagination = await isElementVisible(page, patterns.pagination);
|
|
334
|
+
if (hasPagination) {
|
|
335
|
+
result.tests.push({
|
|
336
|
+
name: 'Pagination dots',
|
|
337
|
+
passed: true,
|
|
338
|
+
selector: patterns.pagination
|
|
339
|
+
});
|
|
340
|
+
result.passed++;
|
|
341
|
+
if (verbose) console.error(` ā Pagination dots found`);
|
|
342
|
+
} else {
|
|
343
|
+
result.warnings.push('No pagination dots visible');
|
|
344
|
+
if (verbose) console.error(` ā No pagination dots found`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Test 5: Active slide indicator
|
|
348
|
+
const activeIndex = await getActiveSlideIndex(page, patterns);
|
|
349
|
+
if (activeIndex >= 0) {
|
|
350
|
+
result.tests.push({
|
|
351
|
+
name: 'Active slide indicator',
|
|
352
|
+
passed: true,
|
|
353
|
+
activeIndex
|
|
354
|
+
});
|
|
355
|
+
result.passed++;
|
|
356
|
+
result.sliderInfo.currentSlide = activeIndex;
|
|
357
|
+
if (verbose) console.error(` ā Active slide: ${activeIndex}`);
|
|
358
|
+
} else {
|
|
359
|
+
result.warnings.push('Could not determine active slide');
|
|
360
|
+
if (verbose) console.error(` ā Could not detect active slide`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Test 6: Autoplay detection (only on desktop to save time)
|
|
364
|
+
if (viewportName === 'desktop' && slideCount > 1) {
|
|
365
|
+
if (verbose) console.error(` Testing autoplay...`);
|
|
366
|
+
const autoplayResult = await checkAutoplay(page, patterns, verbose);
|
|
367
|
+
|
|
368
|
+
if (autoplayResult.hasAutoplay) {
|
|
369
|
+
result.tests.push({
|
|
370
|
+
name: 'Autoplay functionality',
|
|
371
|
+
passed: true,
|
|
372
|
+
changes: autoplayResult.changes,
|
|
373
|
+
duration: autoplayResult.duration
|
|
374
|
+
});
|
|
375
|
+
result.passed++;
|
|
376
|
+
result.sliderInfo.hasAutoplay = true;
|
|
377
|
+
if (verbose) console.error(` ā Autoplay detected (${autoplayResult.changes} changes)`);
|
|
378
|
+
} else {
|
|
379
|
+
result.tests.push({
|
|
380
|
+
name: 'Autoplay functionality',
|
|
381
|
+
passed: true,
|
|
382
|
+
note: `No autoplay detected (${autoplayResult.changes} changes in ${autoplayResult.duration}ms)`,
|
|
383
|
+
changes: autoplayResult.changes
|
|
384
|
+
});
|
|
385
|
+
result.passed++;
|
|
386
|
+
result.sliderInfo.hasAutoplay = false;
|
|
387
|
+
if (verbose) console.error(` ā¹ No autoplay (${autoplayResult.changes} changes)`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return result;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Capture slider screenshot
|
|
396
|
+
*/
|
|
397
|
+
async function captureSliderScreenshot(page, outputDir, viewportName) {
|
|
398
|
+
if (!outputDir) return null;
|
|
399
|
+
|
|
400
|
+
// Try to scroll slider into view
|
|
401
|
+
await page.evaluate(() => {
|
|
402
|
+
const slider = document.querySelector('[class*="swiper"], [class*="slick"], [class*="owl"], [class*="carousel"], [class*="slider"]');
|
|
403
|
+
if (slider) slider.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
404
|
+
});
|
|
405
|
+
await new Promise(r => setTimeout(r, 200));
|
|
406
|
+
|
|
407
|
+
const screenshotPath = path.join(outputDir, `slider-test-${viewportName}.png`);
|
|
408
|
+
await page.screenshot({
|
|
409
|
+
path: screenshotPath,
|
|
410
|
+
fullPage: false
|
|
411
|
+
});
|
|
412
|
+
return screenshotPath;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Validate HTML file path (security: prevent path traversal)
|
|
417
|
+
*/
|
|
418
|
+
function validateHtmlPath(htmlPath) {
|
|
419
|
+
const absolutePath = path.resolve(htmlPath);
|
|
420
|
+
const cwd = process.cwd();
|
|
421
|
+
|
|
422
|
+
const allowedPrefixes = [
|
|
423
|
+
cwd,
|
|
424
|
+
path.join(process.env.HOME || '', '.claude'),
|
|
425
|
+
'/tmp',
|
|
426
|
+
path.join(process.env.HOME || '', 'cloned-designs')
|
|
427
|
+
];
|
|
428
|
+
|
|
429
|
+
const isAllowed = allowedPrefixes.some(prefix => absolutePath.startsWith(prefix));
|
|
430
|
+
if (!isAllowed) {
|
|
431
|
+
throw new Error(`Path "${htmlPath}" is outside allowed directories`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return absolutePath;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Main verification function
|
|
439
|
+
*/
|
|
440
|
+
async function verifySlider() {
|
|
441
|
+
const args = parseArgs(process.argv.slice(2));
|
|
442
|
+
|
|
443
|
+
if (!args.html && !args.url) {
|
|
444
|
+
outputError(new Error('Either --html or --url is required'));
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const verbose = args.verbose === 'true';
|
|
449
|
+
const outputDir = args.output;
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
const browser = await getBrowser({ headless: args.headless !== 'false' });
|
|
453
|
+
const page = await getPage(browser);
|
|
454
|
+
|
|
455
|
+
let targetUrl;
|
|
456
|
+
if (args.html) {
|
|
457
|
+
const absolutePath = validateHtmlPath(args.html);
|
|
458
|
+
targetUrl = `file://${absolutePath}`;
|
|
459
|
+
} else {
|
|
460
|
+
targetUrl = args.url;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (verbose) console.error(`\nš Verifying slider: ${targetUrl}\n`);
|
|
464
|
+
|
|
465
|
+
await page.goto(targetUrl, {
|
|
466
|
+
waitUntil: 'networkidle',
|
|
467
|
+
timeout: 30000
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
const results = {
|
|
471
|
+
success: true,
|
|
472
|
+
component: 'slider',
|
|
473
|
+
url: targetUrl,
|
|
474
|
+
viewports: {},
|
|
475
|
+
summary: {
|
|
476
|
+
totalTests: 0,
|
|
477
|
+
passed: 0,
|
|
478
|
+
failed: 0,
|
|
479
|
+
warnings: []
|
|
480
|
+
},
|
|
481
|
+
screenshots: [],
|
|
482
|
+
sliderDetected: false,
|
|
483
|
+
sliderLibrary: null
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
for (const viewportName of ['mobile', 'tablet', 'desktop']) {
|
|
487
|
+
const viewportResult = await testViewport(page, viewportName, verbose);
|
|
488
|
+
results.viewports[viewportName] = viewportResult;
|
|
489
|
+
|
|
490
|
+
results.summary.totalTests += viewportResult.tests.length;
|
|
491
|
+
results.summary.passed += viewportResult.passed;
|
|
492
|
+
results.summary.failed += viewportResult.failed;
|
|
493
|
+
results.summary.warnings.push(...viewportResult.warnings);
|
|
494
|
+
|
|
495
|
+
if (viewportResult.sliderInfo) {
|
|
496
|
+
results.sliderDetected = true;
|
|
497
|
+
results.sliderLibrary = viewportResult.sliderInfo.library;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (outputDir) {
|
|
501
|
+
const screenshotPath = await captureSliderScreenshot(page, outputDir, viewportName);
|
|
502
|
+
if (screenshotPath) results.screenshots.push(screenshotPath);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
results.success = results.summary.failed === 0;
|
|
507
|
+
|
|
508
|
+
if (args.close === 'true') {
|
|
509
|
+
await closeBrowser();
|
|
510
|
+
} else {
|
|
511
|
+
await disconnectBrowser();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (verbose) {
|
|
515
|
+
console.error('\nš Summary:');
|
|
516
|
+
console.error(` Slider: ${results.sliderDetected ? results.sliderLibrary : 'Not detected'}`);
|
|
517
|
+
console.error(` Tests: ${results.summary.passed}/${results.summary.totalTests} passed`);
|
|
518
|
+
if (results.summary.warnings.length > 0) {
|
|
519
|
+
console.error(` Warnings: ${results.summary.warnings.length}`);
|
|
520
|
+
}
|
|
521
|
+
console.error(` Status: ${results.success ? 'ā PASS' : 'ā FAIL'}\n`);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
outputJSON(results);
|
|
525
|
+
process.exit(results.success ? 0 : 1);
|
|
526
|
+
|
|
527
|
+
} catch (error) {
|
|
528
|
+
outputError(error);
|
|
529
|
+
process.exit(1);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
verifySlider();
|