design-clone 2.1.0 → 3.0.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,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Footer Verification Helpers
|
|
3
|
+
*
|
|
4
|
+
* Selectors, DOM query utilities, and footer-specific checks extracted
|
|
5
|
+
* from verify-footer.js to keep each file under 200 lines.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Footer element selectors
|
|
9
|
+
export const FOOTER_SELECTORS = {
|
|
10
|
+
container: [
|
|
11
|
+
'footer',
|
|
12
|
+
'[role="contentinfo"]',
|
|
13
|
+
'.footer',
|
|
14
|
+
'#footer',
|
|
15
|
+
'.site-footer',
|
|
16
|
+
'.page-footer'
|
|
17
|
+
],
|
|
18
|
+
columns: [
|
|
19
|
+
'footer [class*="column"]',
|
|
20
|
+
'footer [class*="col-"]',
|
|
21
|
+
'footer .col',
|
|
22
|
+
'.footer-column',
|
|
23
|
+
'.footer-widget',
|
|
24
|
+
'.footer-section',
|
|
25
|
+
'footer > div > div'
|
|
26
|
+
],
|
|
27
|
+
links: [
|
|
28
|
+
'footer a[href]',
|
|
29
|
+
'.footer-links a',
|
|
30
|
+
'.footer-nav a',
|
|
31
|
+
'footer nav a',
|
|
32
|
+
'footer ul a'
|
|
33
|
+
],
|
|
34
|
+
copyright: [
|
|
35
|
+
'footer [class*="copyright"]',
|
|
36
|
+
'.copyright',
|
|
37
|
+
'footer small',
|
|
38
|
+
'footer p:last-child'
|
|
39
|
+
],
|
|
40
|
+
socialIcons: [
|
|
41
|
+
'footer a[href*="facebook"]',
|
|
42
|
+
'footer a[href*="twitter"]',
|
|
43
|
+
'footer a[href*="instagram"]',
|
|
44
|
+
'footer a[href*="linkedin"]',
|
|
45
|
+
'footer a[href*="youtube"]',
|
|
46
|
+
'footer [class*="social"]',
|
|
47
|
+
'.social-links a',
|
|
48
|
+
'.social-icons a'
|
|
49
|
+
]
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Find first matching element from a list of selectors
|
|
54
|
+
* @param {import('playwright').Page} page
|
|
55
|
+
* @param {string[]} selectors
|
|
56
|
+
* @returns {Promise<{element: ElementHandle, selector: string}|null>}
|
|
57
|
+
*/
|
|
58
|
+
export async function findElement(page, selectors) {
|
|
59
|
+
for (const selector of selectors) {
|
|
60
|
+
try {
|
|
61
|
+
const element = await page.$(selector);
|
|
62
|
+
if (element) return { element, selector };
|
|
63
|
+
} catch (err) { /* continue */ }
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Count elements using the selector with the highest match count
|
|
70
|
+
* @param {import('playwright').Page} page
|
|
71
|
+
* @param {string[]} selectors
|
|
72
|
+
* @returns {Promise<{count: number, selector: string|null}>}
|
|
73
|
+
*/
|
|
74
|
+
export async function countElements(page, selectors) {
|
|
75
|
+
let totalCount = 0;
|
|
76
|
+
let matchedSelector = null;
|
|
77
|
+
|
|
78
|
+
for (const selector of selectors) {
|
|
79
|
+
try {
|
|
80
|
+
const count = await page.locator(selector).count();
|
|
81
|
+
if (count > totalCount) {
|
|
82
|
+
totalCount = count;
|
|
83
|
+
matchedSelector = selector;
|
|
84
|
+
}
|
|
85
|
+
} catch (err) { /* continue */ }
|
|
86
|
+
}
|
|
87
|
+
return { count: totalCount, selector: matchedSelector };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Count visible elements from a list of selectors (returns first non-zero match)
|
|
92
|
+
* @param {import('playwright').Page} page
|
|
93
|
+
* @param {string[]} selectors
|
|
94
|
+
* @returns {Promise<{count: number, selector: string|null}>}
|
|
95
|
+
*/
|
|
96
|
+
export async function countVisibleElements(page, selectors) {
|
|
97
|
+
for (const selector of selectors) {
|
|
98
|
+
try {
|
|
99
|
+
const count = await page.evaluate((sel) => {
|
|
100
|
+
const items = document.querySelectorAll(sel);
|
|
101
|
+
let visible = 0;
|
|
102
|
+
items.forEach(item => {
|
|
103
|
+
const style = window.getComputedStyle(item);
|
|
104
|
+
const rect = item.getBoundingClientRect();
|
|
105
|
+
if (
|
|
106
|
+
style.display !== 'none' &&
|
|
107
|
+
style.visibility !== 'hidden' &&
|
|
108
|
+
style.opacity !== '0' &&
|
|
109
|
+
rect.width > 0 &&
|
|
110
|
+
rect.height > 0
|
|
111
|
+
) visible++;
|
|
112
|
+
});
|
|
113
|
+
return visible;
|
|
114
|
+
}, selector);
|
|
115
|
+
|
|
116
|
+
if (count > 0) return { count, selector };
|
|
117
|
+
} catch (err) { /* continue */ }
|
|
118
|
+
}
|
|
119
|
+
return { count: 0, selector: null };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check footer position — should be at bottom of page
|
|
124
|
+
* @param {import('playwright').Page} page
|
|
125
|
+
* @param {string} footerSelector
|
|
126
|
+
* @returns {Promise<Object|null>}
|
|
127
|
+
*/
|
|
128
|
+
export async function checkFooterPosition(page, footerSelector) {
|
|
129
|
+
return await page.evaluate((sel) => {
|
|
130
|
+
const footer = document.querySelector(sel);
|
|
131
|
+
if (!footer) return null;
|
|
132
|
+
|
|
133
|
+
const rect = footer.getBoundingClientRect();
|
|
134
|
+
const scrollHeight = Math.max(
|
|
135
|
+
document.body.scrollHeight,
|
|
136
|
+
document.documentElement.scrollHeight
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
window.scrollTo(0, scrollHeight);
|
|
140
|
+
|
|
141
|
+
const style = window.getComputedStyle(footer);
|
|
142
|
+
const footerBottom = rect.y + window.scrollY + rect.height;
|
|
143
|
+
const tolerance = 50;
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
y: rect.y + window.scrollY,
|
|
147
|
+
height: rect.height,
|
|
148
|
+
width: rect.width,
|
|
149
|
+
pageHeight: scrollHeight,
|
|
150
|
+
isAtBottom: footerBottom >= (scrollHeight - tolerance),
|
|
151
|
+
footerBottom,
|
|
152
|
+
backgroundColor: style.backgroundColor,
|
|
153
|
+
color: style.color
|
|
154
|
+
};
|
|
155
|
+
}, footerSelector);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Check for copyright text in footer
|
|
160
|
+
* @param {import('playwright').Page} page
|
|
161
|
+
* @returns {Promise<{hasCopyright: boolean, hasYear: boolean, hasCurrentYear: boolean}|null>}
|
|
162
|
+
*/
|
|
163
|
+
export async function checkCopyright(page) {
|
|
164
|
+
return await page.evaluate(() => {
|
|
165
|
+
const footer = document.querySelector('footer') || document.querySelector('[role="contentinfo"]');
|
|
166
|
+
if (!footer) return null;
|
|
167
|
+
|
|
168
|
+
const text = footer.textContent || '';
|
|
169
|
+
const currentYear = new Date().getFullYear();
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
hasCopyright: /©|copyright|all rights reserved/i.test(text),
|
|
173
|
+
hasYear: new RegExp(`20[0-9]{2}|${currentYear}`).test(text),
|
|
174
|
+
hasCurrentYear: text.includes(String(currentYear))
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
@@ -21,342 +21,27 @@
|
|
|
21
21
|
* --verbose Show detailed progress
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import fs from 'fs/promises';
|
|
25
24
|
import path from 'path';
|
|
26
25
|
|
|
27
|
-
import { getBrowser, getPage, closeBrowser, disconnectBrowser
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
}
|
|
26
|
+
import { getBrowser, getPage, closeBrowser, disconnectBrowser } from '../utils/browser.js';
|
|
27
|
+
import { parseArgs, outputJSON, outputError } from '../utils/helpers.js';
|
|
28
|
+
import { VIEWPORTS_HD as VIEWPORTS } from '../shared/viewports.js';
|
|
29
|
+
import { testFooterViewport } from './verify-footer-checks.js';
|
|
180
30
|
|
|
181
31
|
/**
|
|
182
|
-
*
|
|
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
|
|
32
|
+
* Validate HTML file path (security: prevent path traversal)
|
|
205
33
|
*/
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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;
|
|
34
|
+
function validateHtmlPath(htmlPath) {
|
|
35
|
+
const absolutePath = path.resolve(htmlPath);
|
|
36
|
+
const allowedPrefixes = [
|
|
37
|
+
process.cwd(),
|
|
38
|
+
path.join(process.env.HOME || '', '.claude'),
|
|
39
|
+
'/tmp',
|
|
40
|
+
path.join(process.env.HOME || '', 'cloned-designs')
|
|
41
|
+
];
|
|
42
|
+
const isAllowed = allowedPrefixes.some(prefix => absolutePath.startsWith(prefix));
|
|
43
|
+
if (!isAllowed) throw new Error(`Path "${htmlPath}" is outside allowed directories`);
|
|
44
|
+
return absolutePath;
|
|
360
45
|
}
|
|
361
46
|
|
|
362
47
|
/**
|
|
@@ -364,44 +49,16 @@ async function testViewport(page, viewportName, verbose = false) {
|
|
|
364
49
|
*/
|
|
365
50
|
async function captureFooterScreenshot(page, outputDir, viewportName) {
|
|
366
51
|
if (!outputDir) return null;
|
|
367
|
-
|
|
368
|
-
// Scroll to footer first
|
|
369
52
|
await page.evaluate(() => {
|
|
370
53
|
const footer = document.querySelector('footer') || document.querySelector('[role="contentinfo"]');
|
|
371
54
|
if (footer) footer.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
372
55
|
});
|
|
373
56
|
await new Promise(r => setTimeout(r, 200));
|
|
374
|
-
|
|
375
57
|
const screenshotPath = path.join(outputDir, `footer-test-${viewportName}.png`);
|
|
376
|
-
await page.screenshot({
|
|
377
|
-
path: screenshotPath,
|
|
378
|
-
fullPage: false
|
|
379
|
-
});
|
|
58
|
+
await page.screenshot({ path: screenshotPath, fullPage: false });
|
|
380
59
|
return screenshotPath;
|
|
381
60
|
}
|
|
382
61
|
|
|
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
62
|
/**
|
|
406
63
|
* Main verification function
|
|
407
64
|
*/
|
|
@@ -422,35 +79,26 @@ async function verifyFooter() {
|
|
|
422
79
|
|
|
423
80
|
let targetUrl;
|
|
424
81
|
if (args.html) {
|
|
425
|
-
|
|
426
|
-
targetUrl = `file://${absolutePath}`;
|
|
82
|
+
targetUrl = `file://${validateHtmlPath(args.html)}`;
|
|
427
83
|
} else {
|
|
428
84
|
targetUrl = args.url;
|
|
429
85
|
}
|
|
430
86
|
|
|
431
87
|
if (verbose) console.error(`\n🔍 Verifying footer: ${targetUrl}\n`);
|
|
432
88
|
|
|
433
|
-
await page.goto(targetUrl, {
|
|
434
|
-
waitUntil: 'networkidle',
|
|
435
|
-
timeout: 30000
|
|
436
|
-
});
|
|
89
|
+
await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 30000 });
|
|
437
90
|
|
|
438
91
|
const results = {
|
|
439
92
|
success: true,
|
|
440
93
|
component: 'footer',
|
|
441
94
|
url: targetUrl,
|
|
442
95
|
viewports: {},
|
|
443
|
-
summary: {
|
|
444
|
-
totalTests: 0,
|
|
445
|
-
passed: 0,
|
|
446
|
-
failed: 0,
|
|
447
|
-
warnings: []
|
|
448
|
-
},
|
|
96
|
+
summary: { totalTests: 0, passed: 0, failed: 0, warnings: [] },
|
|
449
97
|
screenshots: []
|
|
450
98
|
};
|
|
451
99
|
|
|
452
100
|
for (const viewportName of ['mobile', 'tablet', 'desktop']) {
|
|
453
|
-
const viewportResult = await
|
|
101
|
+
const viewportResult = await testFooterViewport(page, viewportName, VIEWPORTS, verbose);
|
|
454
102
|
results.viewports[viewportName] = viewportResult;
|
|
455
103
|
|
|
456
104
|
results.summary.totalTests += viewportResult.tests.length;
|
|
@@ -466,18 +114,12 @@ async function verifyFooter() {
|
|
|
466
114
|
|
|
467
115
|
results.success = results.summary.failed === 0;
|
|
468
116
|
|
|
469
|
-
if (args.close === 'true') {
|
|
470
|
-
await closeBrowser();
|
|
471
|
-
} else {
|
|
472
|
-
await disconnectBrowser();
|
|
473
|
-
}
|
|
117
|
+
if (args.close === 'true') { await closeBrowser(); } else { await disconnectBrowser(); }
|
|
474
118
|
|
|
475
119
|
if (verbose) {
|
|
476
120
|
console.error('\n📊 Summary:');
|
|
477
121
|
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
|
-
}
|
|
122
|
+
if (results.summary.warnings.length > 0) console.error(` Warnings: ${results.summary.warnings.length}`);
|
|
481
123
|
console.error(` Status: ${results.success ? '✓ PASS' : '✗ FAIL'}\n`);
|
|
482
124
|
}
|
|
483
125
|
|