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
|
@@ -18,124 +18,13 @@
|
|
|
18
18
|
* --verbose Show detailed progress
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import fs from 'fs/promises';
|
|
22
21
|
import path from 'path';
|
|
23
|
-
import { fileURLToPath } from 'url';
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
import {
|
|
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
|
-
// Common menu element selectors
|
|
36
|
-
const MENU_SELECTORS = {
|
|
37
|
-
// Hamburger/toggle buttons
|
|
38
|
-
toggleButtons: [
|
|
39
|
-
'[aria-label*="menu" i]',
|
|
40
|
-
'[aria-label*="nav" i]',
|
|
41
|
-
'button.hamburger',
|
|
42
|
-
'.hamburger',
|
|
43
|
-
'.menu-toggle',
|
|
44
|
-
'.nav-toggle',
|
|
45
|
-
'.mobile-menu-toggle',
|
|
46
|
-
'button[class*="hamburger"]',
|
|
47
|
-
'button[class*="menu"]',
|
|
48
|
-
'[data-toggle="nav"]',
|
|
49
|
-
'[data-menu-toggle]',
|
|
50
|
-
'.header__toggle',
|
|
51
|
-
'.header-toggle',
|
|
52
|
-
'#menu-toggle',
|
|
53
|
-
'.burger',
|
|
54
|
-
'.burger-menu'
|
|
55
|
-
],
|
|
56
|
-
// Navigation containers
|
|
57
|
-
navContainers: [
|
|
58
|
-
'nav',
|
|
59
|
-
'[role="navigation"]',
|
|
60
|
-
'.nav',
|
|
61
|
-
'.navigation',
|
|
62
|
-
'.main-nav',
|
|
63
|
-
'.site-nav',
|
|
64
|
-
'.header-nav',
|
|
65
|
-
'.primary-nav',
|
|
66
|
-
'#nav',
|
|
67
|
-
'#navigation',
|
|
68
|
-
'.menu',
|
|
69
|
-
'.main-menu'
|
|
70
|
-
],
|
|
71
|
-
// Menu items
|
|
72
|
-
menuItems: [
|
|
73
|
-
'nav a',
|
|
74
|
-
'nav li',
|
|
75
|
-
'.nav-item',
|
|
76
|
-
'.menu-item',
|
|
77
|
-
'.nav-link',
|
|
78
|
-
'.menu-link',
|
|
79
|
-
'[role="navigation"] a'
|
|
80
|
-
]
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Check if element is visible using Playwright locator API
|
|
85
|
-
*/
|
|
86
|
-
async function isElementVisible(page, selector) {
|
|
87
|
-
try {
|
|
88
|
-
return await page.locator(selector).isVisible();
|
|
89
|
-
} catch {
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Find first matching selector
|
|
96
|
-
*/
|
|
97
|
-
async function findElement(page, selectors) {
|
|
98
|
-
for (const selector of selectors) {
|
|
99
|
-
const element = await page.$(selector);
|
|
100
|
-
if (element) {
|
|
101
|
-
return { element, selector };
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Count visible menu items
|
|
109
|
-
*/
|
|
110
|
-
async function countVisibleMenuItems(page) {
|
|
111
|
-
for (const selector of MENU_SELECTORS.menuItems) {
|
|
112
|
-
try {
|
|
113
|
-
const count = await page.evaluate((sel) => {
|
|
114
|
-
const items = document.querySelectorAll(sel);
|
|
115
|
-
let visible = 0;
|
|
116
|
-
items.forEach(item => {
|
|
117
|
-
const style = window.getComputedStyle(item);
|
|
118
|
-
const rect = item.getBoundingClientRect();
|
|
119
|
-
if (
|
|
120
|
-
style.display !== 'none' &&
|
|
121
|
-
style.visibility !== 'hidden' &&
|
|
122
|
-
style.opacity !== '0' &&
|
|
123
|
-
rect.width > 0 &&
|
|
124
|
-
rect.height > 0
|
|
125
|
-
) {
|
|
126
|
-
visible++;
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
return visible;
|
|
130
|
-
}, selector);
|
|
131
|
-
|
|
132
|
-
if (count > 0) {
|
|
133
|
-
return { count, selector };
|
|
134
|
-
}
|
|
135
|
-
} catch { /* continue */ }
|
|
136
|
-
}
|
|
137
|
-
return { count: 0, selector: null };
|
|
138
|
-
}
|
|
23
|
+
import { getBrowser, getPage, closeBrowser, disconnectBrowser } from '../utils/browser.js';
|
|
24
|
+
import { parseArgs, outputJSON, outputError } from '../utils/helpers.js';
|
|
25
|
+
import { VIEWPORTS_HD as VIEWPORTS } from '../shared/viewports.js';
|
|
26
|
+
import { MENU_SELECTORS, findElement, countVisibleMenuItems } from './verify-menu-helpers.js';
|
|
27
|
+
import { testDesktopMenu, testMobileMenu } from './verify-menu-checks.js';
|
|
139
28
|
|
|
140
29
|
/**
|
|
141
30
|
* Test menu at specific viewport
|
|
@@ -143,151 +32,28 @@ async function countVisibleMenuItems(page) {
|
|
|
143
32
|
async function testViewport(page, viewportName, verbose = false) {
|
|
144
33
|
const viewport = VIEWPORTS[viewportName];
|
|
145
34
|
await page.setViewportSize(viewport);
|
|
146
|
-
await new Promise(r => setTimeout(r, 500));
|
|
147
|
-
|
|
148
|
-
const result = {
|
|
149
|
-
viewport: viewportName,
|
|
150
|
-
dimensions: viewport,
|
|
151
|
-
tests: [],
|
|
152
|
-
passed: 0,
|
|
153
|
-
failed: 0,
|
|
154
|
-
warnings: []
|
|
155
|
-
};
|
|
35
|
+
await new Promise(r => setTimeout(r, 500));
|
|
156
36
|
|
|
37
|
+
const result = { viewport: viewportName, dimensions: viewport, tests: [], passed: 0, failed: 0, warnings: [] };
|
|
157
38
|
if (verbose) console.error(`\n📱 Testing ${viewportName} (${viewport.width}x${viewport.height})...`);
|
|
158
39
|
|
|
159
|
-
// Test 1: Navigation container exists
|
|
160
40
|
const navResult = await findElement(page, MENU_SELECTORS.navContainers);
|
|
161
41
|
if (navResult) {
|
|
162
|
-
result.tests.push({
|
|
163
|
-
name: 'Navigation container exists',
|
|
164
|
-
passed: true,
|
|
165
|
-
selector: navResult.selector
|
|
166
|
-
});
|
|
42
|
+
result.tests.push({ name: 'Navigation container exists', passed: true, selector: navResult.selector });
|
|
167
43
|
result.passed++;
|
|
168
44
|
if (verbose) console.error(` ✓ Navigation container found: ${navResult.selector}`);
|
|
169
45
|
} else {
|
|
170
|
-
result.tests.push({
|
|
171
|
-
name: 'Navigation container exists',
|
|
172
|
-
passed: false,
|
|
173
|
-
error: 'No navigation container found'
|
|
174
|
-
});
|
|
46
|
+
result.tests.push({ name: 'Navigation container exists', passed: false, error: 'No navigation container found' });
|
|
175
47
|
result.failed++;
|
|
176
48
|
if (verbose) console.error(` ✗ Navigation container not found`);
|
|
177
49
|
}
|
|
178
50
|
|
|
179
|
-
// Test 2: Menu items visibility
|
|
180
51
|
const menuItems = await countVisibleMenuItems(page);
|
|
181
52
|
|
|
182
|
-
// Different expectations based on viewport
|
|
183
53
|
if (viewportName === 'desktop') {
|
|
184
|
-
|
|
185
|
-
if (menuItems.count >= 2) {
|
|
186
|
-
result.tests.push({
|
|
187
|
-
name: 'Desktop menu items visible',
|
|
188
|
-
passed: true,
|
|
189
|
-
count: menuItems.count,
|
|
190
|
-
selector: menuItems.selector
|
|
191
|
-
});
|
|
192
|
-
result.passed++;
|
|
193
|
-
if (verbose) console.error(` ✓ ${menuItems.count} menu items visible`);
|
|
194
|
-
} else {
|
|
195
|
-
result.tests.push({
|
|
196
|
-
name: 'Desktop menu items visible',
|
|
197
|
-
passed: false,
|
|
198
|
-
count: menuItems.count,
|
|
199
|
-
error: 'Expected at least 2 visible menu items on desktop'
|
|
200
|
-
});
|
|
201
|
-
result.failed++;
|
|
202
|
-
if (verbose) console.error(` ✗ Only ${menuItems.count} menu items visible (expected >= 2)`);
|
|
203
|
-
}
|
|
54
|
+
await testDesktopMenu(page, result, menuItems, verbose);
|
|
204
55
|
} else {
|
|
205
|
-
|
|
206
|
-
const toggleResult = await findElement(page, MENU_SELECTORS.toggleButtons);
|
|
207
|
-
|
|
208
|
-
if (toggleResult) {
|
|
209
|
-
const isToggleVisible = await isElementVisible(page, toggleResult.selector);
|
|
210
|
-
|
|
211
|
-
if (isToggleVisible) {
|
|
212
|
-
result.tests.push({
|
|
213
|
-
name: 'Mobile menu toggle visible',
|
|
214
|
-
passed: true,
|
|
215
|
-
selector: toggleResult.selector
|
|
216
|
-
});
|
|
217
|
-
result.passed++;
|
|
218
|
-
if (verbose) console.error(` ✓ Menu toggle visible: ${toggleResult.selector}`);
|
|
219
|
-
|
|
220
|
-
// Test toggle functionality
|
|
221
|
-
try {
|
|
222
|
-
// Get initial menu state
|
|
223
|
-
const initialMenuItems = await countVisibleMenuItems(page);
|
|
224
|
-
|
|
225
|
-
// Click toggle
|
|
226
|
-
await toggleResult.element.click();
|
|
227
|
-
await new Promise(r => setTimeout(r, 500)); // Wait for animation
|
|
228
|
-
|
|
229
|
-
// Check menu state after click
|
|
230
|
-
const afterClickItems = await countVisibleMenuItems(page);
|
|
231
|
-
|
|
232
|
-
// Menu should either show more items or we can detect state change
|
|
233
|
-
if (afterClickItems.count !== initialMenuItems.count || afterClickItems.count >= 2) {
|
|
234
|
-
result.tests.push({
|
|
235
|
-
name: 'Menu toggle functionality',
|
|
236
|
-
passed: true,
|
|
237
|
-
before: initialMenuItems.count,
|
|
238
|
-
after: afterClickItems.count
|
|
239
|
-
});
|
|
240
|
-
result.passed++;
|
|
241
|
-
if (verbose) console.error(` ✓ Toggle works: ${initialMenuItems.count} -> ${afterClickItems.count} items`);
|
|
242
|
-
|
|
243
|
-
// Click again to close
|
|
244
|
-
await toggleResult.element.click();
|
|
245
|
-
await new Promise(r => setTimeout(r, 300));
|
|
246
|
-
} else {
|
|
247
|
-
result.tests.push({
|
|
248
|
-
name: 'Menu toggle functionality',
|
|
249
|
-
passed: false,
|
|
250
|
-
before: initialMenuItems.count,
|
|
251
|
-
after: afterClickItems.count,
|
|
252
|
-
warning: 'Toggle may not be functional - no state change detected'
|
|
253
|
-
});
|
|
254
|
-
result.warnings.push('Menu toggle click did not change visible items');
|
|
255
|
-
if (verbose) console.error(` ⚠ Toggle click had no effect`);
|
|
256
|
-
}
|
|
257
|
-
} catch (err) {
|
|
258
|
-
result.tests.push({
|
|
259
|
-
name: 'Menu toggle functionality',
|
|
260
|
-
passed: false,
|
|
261
|
-
error: err.message
|
|
262
|
-
});
|
|
263
|
-
result.failed++;
|
|
264
|
-
if (verbose) console.error(` ✗ Toggle click failed: ${err.message}`);
|
|
265
|
-
}
|
|
266
|
-
} else {
|
|
267
|
-
result.warnings.push('Menu toggle found but not visible');
|
|
268
|
-
if (verbose) console.error(` ⚠ Menu toggle found but not visible`);
|
|
269
|
-
}
|
|
270
|
-
} else {
|
|
271
|
-
// No hamburger - check if menu items are still visible (maybe it's a small visible menu)
|
|
272
|
-
if (menuItems.count >= 2) {
|
|
273
|
-
result.tests.push({
|
|
274
|
-
name: 'Mobile menu visible without toggle',
|
|
275
|
-
passed: true,
|
|
276
|
-
count: menuItems.count,
|
|
277
|
-
note: 'Menu shows items without hamburger toggle'
|
|
278
|
-
});
|
|
279
|
-
result.passed++;
|
|
280
|
-
if (verbose) console.error(` ✓ ${menuItems.count} menu items visible (no toggle needed)`);
|
|
281
|
-
} else {
|
|
282
|
-
result.tests.push({
|
|
283
|
-
name: 'Mobile menu accessibility',
|
|
284
|
-
passed: false,
|
|
285
|
-
error: 'No hamburger toggle found and menu items hidden'
|
|
286
|
-
});
|
|
287
|
-
result.failed++;
|
|
288
|
-
if (verbose) console.error(` ✗ No hamburger toggle and menu items hidden`);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
56
|
+
await testMobileMenu(page, result, menuItems, verbose);
|
|
291
57
|
}
|
|
292
58
|
|
|
293
59
|
return result;
|
|
@@ -298,12 +64,8 @@ async function testViewport(page, viewportName, verbose = false) {
|
|
|
298
64
|
*/
|
|
299
65
|
async function captureDebugScreenshot(page, outputDir, viewportName) {
|
|
300
66
|
if (!outputDir) return null;
|
|
301
|
-
|
|
302
67
|
const screenshotPath = path.join(outputDir, `menu-test-${viewportName}.png`);
|
|
303
|
-
await page.screenshot({
|
|
304
|
-
path: screenshotPath,
|
|
305
|
-
fullPage: false
|
|
306
|
-
});
|
|
68
|
+
await page.screenshot({ path: screenshotPath, fullPage: false });
|
|
307
69
|
return screenshotPath;
|
|
308
70
|
}
|
|
309
71
|
|
|
@@ -322,38 +84,19 @@ async function verifyMenu() {
|
|
|
322
84
|
const outputDir = args.output;
|
|
323
85
|
|
|
324
86
|
try {
|
|
325
|
-
// Launch browser
|
|
326
87
|
const browser = await getBrowser({ headless: args.headless !== 'false' });
|
|
327
88
|
const page = await getPage(browser);
|
|
328
89
|
|
|
329
|
-
|
|
330
|
-
let targetUrl;
|
|
331
|
-
if (args.html) {
|
|
332
|
-
// Local file
|
|
333
|
-
const absolutePath = path.resolve(args.html);
|
|
334
|
-
targetUrl = `file://${absolutePath}`;
|
|
335
|
-
} else {
|
|
336
|
-
targetUrl = args.url;
|
|
337
|
-
}
|
|
338
|
-
|
|
90
|
+
const targetUrl = args.html ? `file://${path.resolve(args.html)}` : args.url;
|
|
339
91
|
if (verbose) console.error(`\n🔍 Verifying responsive menu: ${targetUrl}\n`);
|
|
340
92
|
|
|
341
|
-
await page.goto(targetUrl, {
|
|
342
|
-
waitUntil: 'networkidle',
|
|
343
|
-
timeout: 30000
|
|
344
|
-
});
|
|
93
|
+
await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 30000 });
|
|
345
94
|
|
|
346
|
-
// Test all viewports
|
|
347
95
|
const results = {
|
|
348
96
|
success: true,
|
|
349
97
|
url: targetUrl,
|
|
350
98
|
viewports: {},
|
|
351
|
-
summary: {
|
|
352
|
-
totalTests: 0,
|
|
353
|
-
passed: 0,
|
|
354
|
-
failed: 0,
|
|
355
|
-
warnings: []
|
|
356
|
-
},
|
|
99
|
+
summary: { totalTests: 0, passed: 0, failed: 0, warnings: [] },
|
|
357
100
|
screenshots: []
|
|
358
101
|
};
|
|
359
102
|
|
|
@@ -366,30 +109,20 @@ async function verifyMenu() {
|
|
|
366
109
|
results.summary.failed += viewportResult.failed;
|
|
367
110
|
results.summary.warnings.push(...viewportResult.warnings);
|
|
368
111
|
|
|
369
|
-
// Capture debug screenshot if output dir provided
|
|
370
112
|
if (outputDir) {
|
|
371
113
|
const screenshotPath = await captureDebugScreenshot(page, outputDir, viewportName);
|
|
372
114
|
if (screenshotPath) results.screenshots.push(screenshotPath);
|
|
373
115
|
}
|
|
374
116
|
}
|
|
375
117
|
|
|
376
|
-
// Determine overall success
|
|
377
118
|
results.success = results.summary.failed === 0;
|
|
378
119
|
|
|
379
|
-
|
|
380
|
-
if (args.close === 'true') {
|
|
381
|
-
await closeBrowser();
|
|
382
|
-
} else {
|
|
383
|
-
await disconnectBrowser();
|
|
384
|
-
}
|
|
120
|
+
if (args.close === 'true') { await closeBrowser(); } else { await disconnectBrowser(); }
|
|
385
121
|
|
|
386
|
-
// Final summary
|
|
387
122
|
if (verbose) {
|
|
388
123
|
console.error('\n📊 Summary:');
|
|
389
124
|
console.error(` Tests: ${results.summary.passed}/${results.summary.totalTests} passed`);
|
|
390
|
-
if (results.summary.warnings.length > 0) {
|
|
391
|
-
console.error(` Warnings: ${results.summary.warnings.length}`);
|
|
392
|
-
}
|
|
125
|
+
if (results.summary.warnings.length > 0) console.error(` Warnings: ${results.summary.warnings.length}`);
|
|
393
126
|
console.error(` Status: ${results.success ? '✓ PASS' : '✗ FAIL'}\n`);
|
|
394
127
|
}
|
|
395
128
|
|
|
@@ -402,5 +135,4 @@ async function verifyMenu() {
|
|
|
402
135
|
}
|
|
403
136
|
}
|
|
404
137
|
|
|
405
|
-
// Run
|
|
406
138
|
verifyMenu();
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slider Viewport Checks
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates all per-viewport slider checks using helpers.
|
|
5
|
+
* Separated from verify-slider-helpers.js to keep each file under 200 lines.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
detectSliderLibrary,
|
|
10
|
+
isElementVisible,
|
|
11
|
+
countVisibleElements,
|
|
12
|
+
getActiveSlideIndex,
|
|
13
|
+
checkAutoplay
|
|
14
|
+
} from './verify-slider-helpers.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Test slider at a specific viewport — runs all slider checks
|
|
18
|
+
* @param {import('playwright').Page} page
|
|
19
|
+
* @param {string} viewportName
|
|
20
|
+
* @param {Object} VIEWPORTS - Viewport map
|
|
21
|
+
* @param {boolean} verbose
|
|
22
|
+
* @returns {Promise<Object>} Viewport result object
|
|
23
|
+
*/
|
|
24
|
+
export async function testSliderViewport(page, viewportName, VIEWPORTS, verbose = false) {
|
|
25
|
+
const viewport = VIEWPORTS[viewportName];
|
|
26
|
+
await page.setViewportSize(viewport);
|
|
27
|
+
await new Promise(r => setTimeout(r, 500));
|
|
28
|
+
|
|
29
|
+
const result = {
|
|
30
|
+
viewport: viewportName,
|
|
31
|
+
dimensions: viewport,
|
|
32
|
+
tests: [],
|
|
33
|
+
passed: 0,
|
|
34
|
+
failed: 0,
|
|
35
|
+
warnings: [],
|
|
36
|
+
sliderInfo: null
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
if (verbose) console.error(`\n📱 Testing ${viewportName} (${viewport.width}x${viewport.height})...`);
|
|
40
|
+
|
|
41
|
+
const sliderDetection = await detectSliderLibrary(page);
|
|
42
|
+
if (!sliderDetection) {
|
|
43
|
+
result.tests.push({ name: 'Slider detection', passed: true, note: 'No slider/carousel detected on page' });
|
|
44
|
+
result.passed++;
|
|
45
|
+
if (verbose) console.error(` ℹ No slider detected`);
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { library, patterns } = sliderDetection;
|
|
50
|
+
result.sliderInfo = { library };
|
|
51
|
+
result.tests.push({ name: 'Slider detection', passed: true, library, selector: patterns.container });
|
|
52
|
+
result.passed++;
|
|
53
|
+
if (verbose) console.error(` ✓ Slider detected: ${library}`);
|
|
54
|
+
|
|
55
|
+
const slideCount = await countVisibleElements(page, patterns.slide);
|
|
56
|
+
if (slideCount > 0) {
|
|
57
|
+
result.tests.push({ name: 'Slides present', passed: true, count: slideCount });
|
|
58
|
+
result.passed++;
|
|
59
|
+
result.sliderInfo.slideCount = slideCount;
|
|
60
|
+
if (verbose) console.error(` ✓ ${slideCount} slides found`);
|
|
61
|
+
} else {
|
|
62
|
+
result.tests.push({ name: 'Slides present', passed: false, error: 'No slides found in slider' });
|
|
63
|
+
result.failed++;
|
|
64
|
+
if (verbose) console.error(` ✗ No slides found`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const hasPrev = await isElementVisible(page, patterns.prev);
|
|
68
|
+
const hasNext = await isElementVisible(page, patterns.next);
|
|
69
|
+
if (hasPrev || hasNext) {
|
|
70
|
+
result.tests.push({ name: 'Navigation arrows', passed: true, hasPrev, hasNext });
|
|
71
|
+
result.passed++;
|
|
72
|
+
if (verbose) console.error(` ✓ Navigation arrows: prev=${hasPrev}, next=${hasNext}`);
|
|
73
|
+
} else {
|
|
74
|
+
result.warnings.push('No navigation arrows visible');
|
|
75
|
+
if (verbose) console.error(` ⚠ No navigation arrows found`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const hasPagination = await isElementVisible(page, patterns.pagination);
|
|
79
|
+
if (hasPagination) {
|
|
80
|
+
result.tests.push({ name: 'Pagination dots', passed: true, selector: patterns.pagination });
|
|
81
|
+
result.passed++;
|
|
82
|
+
if (verbose) console.error(` ✓ Pagination dots found`);
|
|
83
|
+
} else {
|
|
84
|
+
result.warnings.push('No pagination dots visible');
|
|
85
|
+
if (verbose) console.error(` ⚠ No pagination dots found`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const activeIndex = await getActiveSlideIndex(page, patterns);
|
|
89
|
+
if (activeIndex >= 0) {
|
|
90
|
+
result.tests.push({ name: 'Active slide indicator', passed: true, activeIndex });
|
|
91
|
+
result.passed++;
|
|
92
|
+
result.sliderInfo.currentSlide = activeIndex;
|
|
93
|
+
if (verbose) console.error(` ✓ Active slide: ${activeIndex}`);
|
|
94
|
+
} else {
|
|
95
|
+
result.warnings.push('Could not determine active slide');
|
|
96
|
+
if (verbose) console.error(` ⚠ Could not detect active slide`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (viewportName === 'desktop' && slideCount > 1) {
|
|
100
|
+
if (verbose) console.error(` Testing autoplay...`);
|
|
101
|
+
const autoplayResult = await checkAutoplay(page, patterns, verbose);
|
|
102
|
+
result.tests.push({
|
|
103
|
+
name: 'Autoplay functionality',
|
|
104
|
+
passed: true,
|
|
105
|
+
note: autoplayResult.hasAutoplay ? undefined : `No autoplay detected (${autoplayResult.changes} changes in ${autoplayResult.duration}ms)`,
|
|
106
|
+
changes: autoplayResult.changes,
|
|
107
|
+
duration: autoplayResult.duration
|
|
108
|
+
});
|
|
109
|
+
result.passed++;
|
|
110
|
+
result.sliderInfo.hasAutoplay = autoplayResult.hasAutoplay;
|
|
111
|
+
if (verbose) console.error(` ${autoplayResult.hasAutoplay ? '✓ Autoplay detected' : 'ℹ No autoplay'} (${autoplayResult.changes} changes)`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slider Verification Constants
|
|
3
|
+
*
|
|
4
|
+
* CSS selector patterns for known slider libraries and autoplay detection config.
|
|
5
|
+
* Extracted from verify-slider-helpers.js to keep each file under 200 lines.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Slider library CSS selector patterns
|
|
9
|
+
export const SLIDER_PATTERNS = {
|
|
10
|
+
swiper: {
|
|
11
|
+
container: '[class*="swiper"]',
|
|
12
|
+
slide: '.swiper-slide',
|
|
13
|
+
active: '.swiper-slide-active',
|
|
14
|
+
prev: '.swiper-button-prev',
|
|
15
|
+
next: '.swiper-button-next',
|
|
16
|
+
pagination: '.swiper-pagination'
|
|
17
|
+
},
|
|
18
|
+
slick: {
|
|
19
|
+
container: '[class*="slick"]',
|
|
20
|
+
slide: '.slick-slide',
|
|
21
|
+
active: '.slick-active, .slick-current',
|
|
22
|
+
prev: '.slick-prev',
|
|
23
|
+
next: '.slick-next',
|
|
24
|
+
pagination: '.slick-dots'
|
|
25
|
+
},
|
|
26
|
+
owl: {
|
|
27
|
+
container: '[class*="owl"]',
|
|
28
|
+
slide: '.owl-item',
|
|
29
|
+
active: '.owl-item.active',
|
|
30
|
+
prev: '.owl-prev',
|
|
31
|
+
next: '.owl-next',
|
|
32
|
+
pagination: '.owl-dots'
|
|
33
|
+
},
|
|
34
|
+
splide: {
|
|
35
|
+
container: '.splide',
|
|
36
|
+
slide: '.splide__slide',
|
|
37
|
+
active: '.splide__slide.is-active',
|
|
38
|
+
prev: '.splide__arrow--prev',
|
|
39
|
+
next: '.splide__arrow--next',
|
|
40
|
+
pagination: '.splide__pagination'
|
|
41
|
+
},
|
|
42
|
+
glide: {
|
|
43
|
+
container: '.glide',
|
|
44
|
+
slide: '.glide__slide',
|
|
45
|
+
active: '.glide__slide--active',
|
|
46
|
+
prev: '[data-glide-dir="<"]',
|
|
47
|
+
next: '[data-glide-dir=">"]',
|
|
48
|
+
pagination: '.glide__bullets'
|
|
49
|
+
},
|
|
50
|
+
native: {
|
|
51
|
+
container: '[style*="scroll-snap"], [class*="carousel"], [class*="slider"]',
|
|
52
|
+
slide: '[style*="scroll-snap"] > *, .carousel-item, .slider-item',
|
|
53
|
+
active: '.active, [aria-current="true"]',
|
|
54
|
+
prev: '[class*="prev"], [aria-label*="prev" i]',
|
|
55
|
+
next: '[class*="next"], [aria-label*="next" i]',
|
|
56
|
+
pagination: '[class*="indicator"], [class*="dot"], [role="tablist"]'
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Autoplay detection config
|
|
61
|
+
export const AUTOPLAY_CONFIG = {
|
|
62
|
+
waitTime: 6000, // Total wait time in ms
|
|
63
|
+
checkInterval: 1000, // Check every 1s
|
|
64
|
+
requiredChanges: 2 // Require 2 slide changes (per validation)
|
|
65
|
+
};
|