design-clone 2.1.0 → 2.3.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
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slider Verification Helpers
|
|
3
|
+
*
|
|
4
|
+
* DOM inspection utilities for verify-slider.js:
|
|
5
|
+
* element visibility checks, slide index detection, and autoplay monitoring.
|
|
6
|
+
* Constants live in verify-slider-constants.js.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { SLIDER_PATTERNS, AUTOPLAY_CONFIG } from './verify-slider-constants.js';
|
|
10
|
+
import { SLIDER_PATTERNS, AUTOPLAY_CONFIG } from './verify-slider-constants.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Detect which slider library is used on the page
|
|
14
|
+
* @param {import('playwright').Page} page
|
|
15
|
+
* @returns {Promise<{library: string, patterns: Object}|null>}
|
|
16
|
+
*/
|
|
17
|
+
export async function detectSliderLibrary(page) {
|
|
18
|
+
for (const [name, patterns] of Object.entries(SLIDER_PATTERNS)) {
|
|
19
|
+
try {
|
|
20
|
+
const count = await page.locator(patterns.container).count();
|
|
21
|
+
if (count > 0) {
|
|
22
|
+
return { library: name, patterns };
|
|
23
|
+
}
|
|
24
|
+
} catch (err) { /* continue - selector not found */ }
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check element visibility via computed style and bounding rect
|
|
31
|
+
* @param {import('playwright').Page} page
|
|
32
|
+
* @param {string} selector
|
|
33
|
+
* @returns {Promise<boolean>}
|
|
34
|
+
*/
|
|
35
|
+
export async function isElementVisible(page, selector) {
|
|
36
|
+
try {
|
|
37
|
+
const element = await page.$(selector);
|
|
38
|
+
if (!element) return false;
|
|
39
|
+
|
|
40
|
+
return await page.evaluate((sel) => {
|
|
41
|
+
const el = document.querySelector(sel);
|
|
42
|
+
if (!el) return false;
|
|
43
|
+
|
|
44
|
+
const style = window.getComputedStyle(el);
|
|
45
|
+
const rect = el.getBoundingClientRect();
|
|
46
|
+
return (
|
|
47
|
+
style.display !== 'none' &&
|
|
48
|
+
style.visibility !== 'hidden' &&
|
|
49
|
+
style.opacity !== '0' &&
|
|
50
|
+
rect.width > 0 &&
|
|
51
|
+
rect.height > 0
|
|
52
|
+
);
|
|
53
|
+
}, selector);
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Count visible elements matching a selector
|
|
61
|
+
* @param {import('playwright').Page} page
|
|
62
|
+
* @param {string} selector
|
|
63
|
+
* @returns {Promise<number>}
|
|
64
|
+
*/
|
|
65
|
+
export async function countVisibleElements(page, selector) {
|
|
66
|
+
try {
|
|
67
|
+
return await page.evaluate((sel) => {
|
|
68
|
+
const items = document.querySelectorAll(sel);
|
|
69
|
+
let visible = 0;
|
|
70
|
+
items.forEach(item => {
|
|
71
|
+
const style = window.getComputedStyle(item);
|
|
72
|
+
const rect = item.getBoundingClientRect();
|
|
73
|
+
if (
|
|
74
|
+
style.display !== 'none' &&
|
|
75
|
+
style.visibility !== 'hidden' &&
|
|
76
|
+
style.opacity !== '0' &&
|
|
77
|
+
rect.width > 0 &&
|
|
78
|
+
rect.height > 0
|
|
79
|
+
) visible++;
|
|
80
|
+
});
|
|
81
|
+
return visible;
|
|
82
|
+
}, selector);
|
|
83
|
+
} catch {
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get the index of the currently active slide
|
|
90
|
+
* @param {import('playwright').Page} page
|
|
91
|
+
* @param {Object} patterns - Slider patterns object
|
|
92
|
+
* @returns {Promise<number>} Active slide index or -1
|
|
93
|
+
*/
|
|
94
|
+
export async function getActiveSlideIndex(page, patterns) {
|
|
95
|
+
try {
|
|
96
|
+
return await page.evaluate((selectors) => {
|
|
97
|
+
const active = document.querySelector(selectors.active);
|
|
98
|
+
if (active) {
|
|
99
|
+
const slides = document.querySelectorAll(selectors.slide);
|
|
100
|
+
for (let i = 0; i < slides.length; i++) {
|
|
101
|
+
if (slides[i] === active || slides[i].contains(active)) return i;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const container = document.querySelector(selectors.container);
|
|
106
|
+
if (container) {
|
|
107
|
+
const slides = container.querySelectorAll(selectors.slide);
|
|
108
|
+
for (let i = 0; i < slides.length; i++) {
|
|
109
|
+
const rect = slides[i].getBoundingClientRect();
|
|
110
|
+
const containerRect = container.getBoundingClientRect();
|
|
111
|
+
if (rect.left >= containerRect.left - 10 && rect.left < containerRect.right) return i;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return -1;
|
|
116
|
+
}, patterns);
|
|
117
|
+
} catch {
|
|
118
|
+
return -1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check autoplay by monitoring slide changes over AUTOPLAY_CONFIG.waitTime.
|
|
124
|
+
* Exits early when requiredChanges are detected.
|
|
125
|
+
* @param {import('playwright').Page} page
|
|
126
|
+
* @param {Object} patterns - Slider patterns
|
|
127
|
+
* @param {boolean} verbose
|
|
128
|
+
* @returns {Promise<{hasAutoplay: boolean, changes: number, slideIndices: Array, duration: number}>}
|
|
129
|
+
*/
|
|
130
|
+
export async function checkAutoplay(page, patterns, verbose) {
|
|
131
|
+
const slideIndices = [];
|
|
132
|
+
const startTime = Date.now();
|
|
133
|
+
|
|
134
|
+
const initialIndex = await getActiveSlideIndex(page, patterns);
|
|
135
|
+
slideIndices.push({ time: 0, index: initialIndex });
|
|
136
|
+
|
|
137
|
+
if (verbose) console.error(` Starting autoplay detection (max ${AUTOPLAY_CONFIG.waitTime / 1000}s)...`);
|
|
138
|
+
|
|
139
|
+
while (Date.now() - startTime < AUTOPLAY_CONFIG.waitTime) {
|
|
140
|
+
await new Promise(r => setTimeout(r, AUTOPLAY_CONFIG.checkInterval));
|
|
141
|
+
|
|
142
|
+
const currentIndex = await getActiveSlideIndex(page, patterns);
|
|
143
|
+
const elapsed = Date.now() - startTime;
|
|
144
|
+
|
|
145
|
+
if (currentIndex !== slideIndices[slideIndices.length - 1].index) {
|
|
146
|
+
slideIndices.push({ time: elapsed, index: currentIndex });
|
|
147
|
+
if (verbose) console.error(` Slide changed to ${currentIndex} at ${elapsed}ms`);
|
|
148
|
+
|
|
149
|
+
if (slideIndices.length - 1 >= AUTOPLAY_CONFIG.requiredChanges) {
|
|
150
|
+
if (verbose) console.error(` Early exit: ${AUTOPLAY_CONFIG.requiredChanges} changes detected`);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const changes = slideIndices.length - 1;
|
|
157
|
+
return {
|
|
158
|
+
hasAutoplay: changes >= AUTOPLAY_CONFIG.requiredChanges,
|
|
159
|
+
changes,
|
|
160
|
+
slideIndices,
|
|
161
|
+
duration: Date.now() - startTime
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
@@ -20,375 +20,27 @@
|
|
|
20
20
|
* --verbose Show detailed progress
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import fs from 'fs/promises';
|
|
24
23
|
import path from 'path';
|
|
25
24
|
|
|
26
|
-
import { getBrowser, getPage, closeBrowser, disconnectBrowser
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
}
|
|
25
|
+
import { getBrowser, getPage, closeBrowser, disconnectBrowser } from '../utils/browser.js';
|
|
26
|
+
import { parseArgs, outputJSON, outputError } from '../utils/helpers.js';
|
|
27
|
+
import { VIEWPORTS_HD as VIEWPORTS } from '../shared/viewports.js';
|
|
28
|
+
import { testSliderViewport } from './verify-slider-checks.js';
|
|
108
29
|
|
|
109
30
|
/**
|
|
110
|
-
*
|
|
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
|
|
31
|
+
* Validate HTML file path (security: prevent path traversal)
|
|
249
32
|
*/
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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;
|
|
33
|
+
function validateHtmlPath(htmlPath) {
|
|
34
|
+
const absolutePath = path.resolve(htmlPath);
|
|
35
|
+
const allowedPrefixes = [
|
|
36
|
+
process.cwd(),
|
|
37
|
+
path.join(process.env.HOME || '', '.claude'),
|
|
38
|
+
'/tmp',
|
|
39
|
+
path.join(process.env.HOME || '', 'cloned-designs')
|
|
40
|
+
];
|
|
41
|
+
const isAllowed = allowedPrefixes.some(prefix => absolutePath.startsWith(prefix));
|
|
42
|
+
if (!isAllowed) throw new Error(`Path "${htmlPath}" is outside allowed directories`);
|
|
43
|
+
return absolutePath;
|
|
392
44
|
}
|
|
393
45
|
|
|
394
46
|
/**
|
|
@@ -396,44 +48,16 @@ async function testViewport(page, viewportName, verbose = false) {
|
|
|
396
48
|
*/
|
|
397
49
|
async function captureSliderScreenshot(page, outputDir, viewportName) {
|
|
398
50
|
if (!outputDir) return null;
|
|
399
|
-
|
|
400
|
-
// Try to scroll slider into view
|
|
401
51
|
await page.evaluate(() => {
|
|
402
52
|
const slider = document.querySelector('[class*="swiper"], [class*="slick"], [class*="owl"], [class*="carousel"], [class*="slider"]');
|
|
403
53
|
if (slider) slider.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
404
54
|
});
|
|
405
55
|
await new Promise(r => setTimeout(r, 200));
|
|
406
|
-
|
|
407
56
|
const screenshotPath = path.join(outputDir, `slider-test-${viewportName}.png`);
|
|
408
|
-
await page.screenshot({
|
|
409
|
-
path: screenshotPath,
|
|
410
|
-
fullPage: false
|
|
411
|
-
});
|
|
57
|
+
await page.screenshot({ path: screenshotPath, fullPage: false });
|
|
412
58
|
return screenshotPath;
|
|
413
59
|
}
|
|
414
60
|
|
|
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
61
|
/**
|
|
438
62
|
* Main verification function
|
|
439
63
|
*/
|
|
@@ -454,37 +78,28 @@ async function verifySlider() {
|
|
|
454
78
|
|
|
455
79
|
let targetUrl;
|
|
456
80
|
if (args.html) {
|
|
457
|
-
|
|
458
|
-
targetUrl = `file://${absolutePath}`;
|
|
81
|
+
targetUrl = `file://${validateHtmlPath(args.html)}`;
|
|
459
82
|
} else {
|
|
460
83
|
targetUrl = args.url;
|
|
461
84
|
}
|
|
462
85
|
|
|
463
86
|
if (verbose) console.error(`\n🔍 Verifying slider: ${targetUrl}\n`);
|
|
464
87
|
|
|
465
|
-
await page.goto(targetUrl, {
|
|
466
|
-
waitUntil: 'networkidle',
|
|
467
|
-
timeout: 30000
|
|
468
|
-
});
|
|
88
|
+
await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 30000 });
|
|
469
89
|
|
|
470
90
|
const results = {
|
|
471
91
|
success: true,
|
|
472
92
|
component: 'slider',
|
|
473
93
|
url: targetUrl,
|
|
474
94
|
viewports: {},
|
|
475
|
-
summary: {
|
|
476
|
-
totalTests: 0,
|
|
477
|
-
passed: 0,
|
|
478
|
-
failed: 0,
|
|
479
|
-
warnings: []
|
|
480
|
-
},
|
|
95
|
+
summary: { totalTests: 0, passed: 0, failed: 0, warnings: [] },
|
|
481
96
|
screenshots: [],
|
|
482
97
|
sliderDetected: false,
|
|
483
98
|
sliderLibrary: null
|
|
484
99
|
};
|
|
485
100
|
|
|
486
101
|
for (const viewportName of ['mobile', 'tablet', 'desktop']) {
|
|
487
|
-
const viewportResult = await
|
|
102
|
+
const viewportResult = await testSliderViewport(page, viewportName, VIEWPORTS, verbose);
|
|
488
103
|
results.viewports[viewportName] = viewportResult;
|
|
489
104
|
|
|
490
105
|
results.summary.totalTests += viewportResult.tests.length;
|
|
@@ -505,19 +120,13 @@ async function verifySlider() {
|
|
|
505
120
|
|
|
506
121
|
results.success = results.summary.failed === 0;
|
|
507
122
|
|
|
508
|
-
if (args.close === 'true') {
|
|
509
|
-
await closeBrowser();
|
|
510
|
-
} else {
|
|
511
|
-
await disconnectBrowser();
|
|
512
|
-
}
|
|
123
|
+
if (args.close === 'true') { await closeBrowser(); } else { await disconnectBrowser(); }
|
|
513
124
|
|
|
514
125
|
if (verbose) {
|
|
515
126
|
console.error('\n📊 Summary:');
|
|
516
127
|
console.error(` Slider: ${results.sliderDetected ? results.sliderLibrary : 'Not detected'}`);
|
|
517
128
|
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
|
-
}
|
|
129
|
+
if (results.summary.warnings.length > 0) console.error(` Warnings: ${results.summary.warnings.length}`);
|
|
521
130
|
console.error(` Status: ${results.success ? '✓ PASS' : '✗ FAIL'}\n`);
|
|
522
131
|
}
|
|
523
132
|
|
package/.env.example
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# Design Clone Skill - Environment Variables
|
|
2
|
-
# Copy to .env and fill in your values
|
|
3
|
-
|
|
4
|
-
# Gemini API Key (optional but recommended)
|
|
5
|
-
# Get from: https://aistudio.google.com/apikey
|
|
6
|
-
# Enables AI structure analysis and design token extraction
|
|
7
|
-
GEMINI_API_KEY=your-api-key-here
|
|
8
|
-
|
|
9
|
-
# Chrome Path (optional - auto-detected on most systems)
|
|
10
|
-
# Set if Chrome is installed in non-standard location
|
|
11
|
-
# CHROME_PATH=/path/to/chrome
|
|
12
|
-
|
|
13
|
-
# Puppeteer Options (optional)
|
|
14
|
-
# PUPPETEER_NO_SANDBOX=1 # Required for Docker/CI
|