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.
Files changed (177) hide show
  1. package/README.md +13 -34
  2. package/SKILL.md +69 -45
  3. package/bin/cli.js +22 -4
  4. package/bin/commands/clone-site.js +31 -171
  5. package/bin/commands/help.js +19 -6
  6. package/bin/commands/init.js +9 -86
  7. package/bin/commands/uninstall.js +105 -0
  8. package/bin/commands/update.js +70 -0
  9. package/bin/commands/verify.js +7 -14
  10. package/bin/utils/paths.js +28 -0
  11. package/bin/utils/validate.js +2 -22
  12. package/bin/utils/version.js +23 -0
  13. package/docs/code-standards.md +789 -0
  14. package/docs/codebase-summary.md +533 -286
  15. package/docs/index.md +74 -0
  16. package/docs/project-overview-pdr.md +797 -0
  17. package/docs/system-architecture.md +718 -0
  18. package/package.json +14 -17
  19. package/src/ai/prompts/design-tokens/basic.md +80 -0
  20. package/src/ai/prompts/design-tokens/section-with-css.md +41 -0
  21. package/src/ai/prompts/design-tokens/section.md +48 -0
  22. package/src/ai/prompts/design-tokens/with-css.md +87 -0
  23. package/src/ai/prompts/structure-analysis/basic.md +55 -0
  24. package/src/ai/prompts/structure-analysis/with-context.md +59 -0
  25. package/src/ai/prompts/structure-analysis/with-dimensions.md +63 -0
  26. package/src/ai/prompts/structure-analysis/with-hierarchy.md +73 -0
  27. package/src/ai/prompts/ux-audit/aggregation.md +42 -0
  28. package/src/ai/prompts/ux-audit/desktop.md +92 -0
  29. package/src/ai/prompts/ux-audit/mobile.md +93 -0
  30. package/src/ai/prompts/ux-audit/tablet.md +92 -0
  31. package/src/core/animation/animation-extractor-ast.js +183 -0
  32. package/src/core/animation/animation-extractor-output.js +152 -0
  33. package/src/core/animation/animation-extractor.js +178 -0
  34. package/src/core/animation/state-capture-detection.js +200 -0
  35. package/src/core/animation/state-capture.js +193 -0
  36. package/src/core/capture/browser-context-pool.js +96 -0
  37. package/src/core/capture/multi-page-screenshot-page.js +110 -0
  38. package/src/core/capture/multi-page-screenshot.js +208 -0
  39. package/src/core/capture/screenshot-extraction.js +186 -0
  40. package/src/core/capture/screenshot-helpers.js +175 -0
  41. package/src/core/capture/screenshot-orchestrator.js +174 -0
  42. package/src/core/capture/screenshot-viewport.js +93 -0
  43. package/src/core/capture/screenshot.js +192 -0
  44. package/src/core/content/content-counter-dom.js +191 -0
  45. package/src/core/content/content-counter.js +76 -0
  46. package/src/core/css/breakpoint-detector.js +66 -0
  47. package/src/core/css/chromium-defaults.json +23 -0
  48. package/src/core/css/computed-style-extractor.js +102 -0
  49. package/src/core/css/css-chunker.js +103 -0
  50. package/src/core/css/filter-css-dead-code.js +120 -0
  51. package/src/core/css/filter-css-html-analyzer.js +110 -0
  52. package/src/core/css/filter-css-selector-matcher.js +172 -0
  53. package/src/core/css/filter-css.js +206 -0
  54. package/src/core/css/merge-css-atrule-processor.js +158 -0
  55. package/src/core/css/merge-css-file-io.js +68 -0
  56. package/src/core/css/merge-css.js +148 -0
  57. package/src/core/detection/framework-detector-routing.js +68 -0
  58. package/src/core/detection/framework-detector-signals.js +65 -0
  59. package/src/core/detection/framework-detector.js +198 -0
  60. package/src/core/dimension/dimension-extractor-card-detector.js +82 -0
  61. package/src/core/dimension/dimension-extractor.js +317 -0
  62. package/src/core/dimension/dimension-output-ai-summary.js +111 -0
  63. package/src/core/dimension/dimension-output.js +173 -0
  64. package/src/core/dimension/dom-tree-analyzer-tree-builders.js +95 -0
  65. package/src/core/dimension/dom-tree-analyzer.js +191 -0
  66. package/src/core/discovery/app-state-snapshot-capture.js +195 -0
  67. package/src/core/discovery/app-state-snapshot-utils.js +178 -0
  68. package/src/core/discovery/app-state-snapshot.js +131 -0
  69. package/src/core/discovery/discover-pages-routes.js +84 -0
  70. package/src/core/discovery/discover-pages-utils.js +177 -0
  71. package/src/core/discovery/discover-pages.js +191 -0
  72. package/src/core/html/html-extractor-inline-styler.js +70 -0
  73. package/src/core/html/html-extractor.js +147 -0
  74. package/src/core/html/semantic-enhancer-mappings.js +200 -0
  75. package/src/core/html/semantic-enhancer-page.js +148 -0
  76. package/src/core/html/semantic-enhancer.js +135 -0
  77. package/src/core/links/rewrite-links-css-rewriter.js +53 -0
  78. package/src/core/links/rewrite-links.js +173 -0
  79. package/src/core/media/asset-validator.js +118 -0
  80. package/src/core/media/extract-assets-downloader.js +187 -0
  81. package/src/core/media/extract-assets-page-scraper.js +115 -0
  82. package/src/core/media/extract-assets.js +159 -0
  83. package/src/core/media/video-capture-convert.js +200 -0
  84. package/src/core/media/video-capture.js +201 -0
  85. package/src/core/{lazy-loader.js → page-prep/lazy-loader.js} +37 -39
  86. package/src/core/section/section-cropper-helpers.js +43 -0
  87. package/src/core/{section-cropper.js → section/section-cropper.js} +11 -88
  88. package/src/core/section/section-detector-strategies.js +139 -0
  89. package/src/core/section/section-detector-utils.js +100 -0
  90. package/src/core/section/section-detector.js +88 -0
  91. package/src/core/tests/test-section-cropper.js +2 -2
  92. package/src/core/tests/test-section-detector.js +2 -2
  93. package/src/post-process/enhance-assets.js +29 -4
  94. package/src/post-process/fetch-images-unsplash-client.js +123 -0
  95. package/src/post-process/fetch-images.js +60 -263
  96. package/src/post-process/inject-gosnap.js +88 -0
  97. package/src/post-process/inject-icons-svg-replacer.js +76 -0
  98. package/src/post-process/inject-icons.js +47 -200
  99. package/src/route-discoverers/base-discoverer-utils.js +137 -0
  100. package/src/route-discoverers/base-discoverer.js +29 -118
  101. package/src/route-discoverers/index.js +1 -1
  102. package/src/shared/config.js +38 -0
  103. package/src/shared/error-codes.js +31 -0
  104. package/src/shared/viewports.js +46 -0
  105. package/src/utils/browser.js +0 -7
  106. package/src/utils/helpers.js +4 -0
  107. package/src/utils/log.js +12 -0
  108. package/src/utils/playwright-loader.js +76 -0
  109. package/src/utils/playwright.js +3 -69
  110. package/src/utils/progress.js +32 -0
  111. package/src/verification/generate-audit-report-css-fixes.js +52 -0
  112. package/src/verification/generate-audit-report-sections.js +158 -0
  113. package/src/verification/generate-audit-report.js +5 -281
  114. package/src/verification/quality-scorer.js +92 -0
  115. package/src/verification/verify-footer-checks.js +103 -0
  116. package/src/verification/verify-footer-helpers.js +178 -0
  117. package/src/verification/verify-footer.js +23 -381
  118. package/src/verification/verify-header-checks.js +104 -0
  119. package/src/verification/verify-header-helpers.js +156 -0
  120. package/src/verification/verify-header.js +23 -365
  121. package/src/verification/verify-layout-report.js +101 -0
  122. package/src/verification/verify-layout.js +13 -259
  123. package/src/verification/verify-menu-checks.js +104 -0
  124. package/src/verification/verify-menu-helpers.js +112 -0
  125. package/src/verification/verify-menu.js +17 -285
  126. package/src/verification/verify-slider-checks.js +115 -0
  127. package/src/verification/verify-slider-constants.js +65 -0
  128. package/src/verification/verify-slider-helpers.js +164 -0
  129. package/src/verification/verify-slider.js +23 -414
  130. package/.env.example +0 -14
  131. package/docs/basic-clone.md +0 -63
  132. package/docs/cli-reference.md +0 -316
  133. package/docs/design-clone-architecture.md +0 -492
  134. package/docs/pixel-perfect.md +0 -117
  135. package/docs/project-roadmap.md +0 -382
  136. package/docs/troubleshooting.md +0 -170
  137. package/requirements.txt +0 -5
  138. package/src/ai/__pycache__/analyze-structure.cpython-313.pyc +0 -0
  139. package/src/ai/__pycache__/extract-design-tokens.cpython-313.pyc +0 -0
  140. package/src/ai/analyze-structure.py +0 -375
  141. package/src/ai/extract-design-tokens.py +0 -782
  142. package/src/ai/prompts/__init__.py +0 -2
  143. package/src/ai/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
  144. package/src/ai/prompts/__pycache__/design_tokens.cpython-313.pyc +0 -0
  145. package/src/ai/prompts/__pycache__/structure_analysis.cpython-313.pyc +0 -0
  146. package/src/ai/prompts/__pycache__/ux_audit.cpython-313.pyc +0 -0
  147. package/src/ai/prompts/design_tokens.py +0 -316
  148. package/src/ai/prompts/structure_analysis.py +0 -592
  149. package/src/ai/prompts/ux_audit.py +0 -198
  150. package/src/ai/ux-audit.js +0 -596
  151. package/src/core/animation-extractor.js +0 -526
  152. package/src/core/app-state-snapshot.js +0 -511
  153. package/src/core/content-counter.js +0 -342
  154. package/src/core/design-tokens.js +0 -103
  155. package/src/core/dimension-extractor.js +0 -438
  156. package/src/core/dimension-output.js +0 -305
  157. package/src/core/discover-pages.js +0 -542
  158. package/src/core/dom-tree-analyzer.js +0 -298
  159. package/src/core/extract-assets.js +0 -468
  160. package/src/core/filter-css.js +0 -499
  161. package/src/core/framework-detector.js +0 -538
  162. package/src/core/html-extractor.js +0 -212
  163. package/src/core/merge-css.js +0 -407
  164. package/src/core/multi-page-screenshot.js +0 -380
  165. package/src/core/rewrite-links.js +0 -226
  166. package/src/core/screenshot.js +0 -701
  167. package/src/core/section-detector.js +0 -386
  168. package/src/core/semantic-enhancer.js +0 -492
  169. package/src/core/state-capture.js +0 -598
  170. package/src/core/video-capture.js +0 -546
  171. package/src/utils/__init__.py +0 -16
  172. package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  173. package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
  174. package/src/utils/env.py +0 -134
  175. /package/src/core/{css-extractor.js → css/css-extractor.js} +0 -0
  176. /package/src/core/{cookie-handler.js → page-prep/cookie-handler.js} +0 -0
  177. /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, parseArgs, outputJSON, outputError } from '../utils/browser.js';
28
-
29
- // Viewport configurations
30
- const VIEWPORTS = {
31
- mobile: { width: 375, height: 812, deviceScaleFactor: 2 },
32
- tablet: { width: 768, height: 1024, deviceScaleFactor: 1 },
33
- desktop: { width: 1920, height: 1080, deviceScaleFactor: 1 }
34
- };
35
-
36
- // Footer element selectors
37
- const FOOTER_SELECTORS = {
38
- container: [
39
- 'footer',
40
- '[role="contentinfo"]',
41
- '.footer',
42
- '#footer',
43
- '.site-footer',
44
- '.page-footer'
45
- ],
46
- columns: [
47
- 'footer [class*="column"]',
48
- 'footer [class*="col-"]',
49
- 'footer .col',
50
- '.footer-column',
51
- '.footer-widget',
52
- '.footer-section',
53
- 'footer > div > div'
54
- ],
55
- links: [
56
- 'footer a[href]',
57
- '.footer-links a',
58
- '.footer-nav a',
59
- 'footer nav a',
60
- 'footer ul a'
61
- ],
62
- copyright: [
63
- 'footer [class*="copyright"]',
64
- '.copyright',
65
- 'footer small',
66
- 'footer p:last-child'
67
- ],
68
- socialIcons: [
69
- 'footer a[href*="facebook"]',
70
- 'footer a[href*="twitter"]',
71
- 'footer a[href*="instagram"]',
72
- 'footer a[href*="linkedin"]',
73
- 'footer a[href*="youtube"]',
74
- 'footer [class*="social"]',
75
- '.social-links a',
76
- '.social-icons a'
77
- ]
78
- };
79
-
80
- /**
81
- * Find first matching element
82
- */
83
- async function findElement(page, selectors) {
84
- for (const selector of selectors) {
85
- try {
86
- const element = await page.$(selector);
87
- if (element) {
88
- return { element, selector };
89
- }
90
- } catch (err) { /* continue - selector not found */ }
91
- }
92
- return null;
93
- }
94
-
95
- /**
96
- * Count matching elements
97
- */
98
- async function countElements(page, selectors) {
99
- let totalCount = 0;
100
- let matchedSelector = null;
101
-
102
- for (const selector of selectors) {
103
- try {
104
- const count = await page.locator(selector).count();
105
- if (count > totalCount) {
106
- totalCount = count;
107
- matchedSelector = selector;
108
- }
109
- } catch (err) { /* continue - selector not found */ }
110
- }
111
- return { count: totalCount, selector: matchedSelector };
112
- }
113
-
114
- /**
115
- * Count visible elements
116
- */
117
- async function countVisibleElements(page, selectors) {
118
- for (const selector of selectors) {
119
- try {
120
- const count = await page.evaluate((sel) => {
121
- const items = document.querySelectorAll(sel);
122
- let visible = 0;
123
- items.forEach(item => {
124
- const style = window.getComputedStyle(item);
125
- const rect = item.getBoundingClientRect();
126
- if (
127
- style.display !== 'none' &&
128
- style.visibility !== 'hidden' &&
129
- style.opacity !== '0' &&
130
- rect.width > 0 &&
131
- rect.height > 0
132
- ) {
133
- visible++;
134
- }
135
- });
136
- return visible;
137
- }, selector);
138
-
139
- if (count > 0) {
140
- return { count, selector };
141
- }
142
- } catch (err) { /* continue - selector not found */ }
143
- }
144
- return { count: 0, selector: null };
145
- }
146
-
147
- /**
148
- * Check footer position (should be at bottom)
149
- */
150
- async function checkFooterPosition(page, footerSelector) {
151
- return await page.evaluate((sel) => {
152
- const footer = document.querySelector(sel);
153
- if (!footer) return null;
154
-
155
- const rect = footer.getBoundingClientRect();
156
- const scrollHeight = Math.max(
157
- document.body.scrollHeight,
158
- document.documentElement.scrollHeight
159
- );
160
-
161
- // Scroll to bottom to get accurate position
162
- window.scrollTo(0, scrollHeight);
163
-
164
- const style = window.getComputedStyle(footer);
165
- const footerBottom = rect.y + window.scrollY + rect.height;
166
- const tolerance = 50; // Allow 50px tolerance
167
-
168
- return {
169
- y: rect.y + window.scrollY,
170
- height: rect.height,
171
- width: rect.width,
172
- pageHeight: scrollHeight,
173
- isAtBottom: footerBottom >= (scrollHeight - tolerance),
174
- footerBottom,
175
- backgroundColor: style.backgroundColor,
176
- color: style.color
177
- };
178
- }, footerSelector);
179
- }
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
- * Check for copyright text
183
- */
184
- async function checkCopyright(page) {
185
- return await page.evaluate(() => {
186
- const footer = document.querySelector('footer') || document.querySelector('[role="contentinfo"]');
187
- if (!footer) return null;
188
-
189
- const text = footer.textContent || '';
190
- const currentYear = new Date().getFullYear();
191
-
192
- const hasCopyright = /©|copyright|all rights reserved/i.test(text);
193
- const hasYear = new RegExp(`20[0-9]{2}|${currentYear}`).test(text);
194
-
195
- return {
196
- hasCopyright,
197
- hasYear,
198
- hasCurrentYear: text.includes(String(currentYear))
199
- };
200
- });
201
- }
202
-
203
- /**
204
- * Test footer at specific viewport
32
+ * Validate HTML file path (security: prevent path traversal)
205
33
  */
206
- async function testViewport(page, viewportName, verbose = false) {
207
- const viewport = VIEWPORTS[viewportName];
208
- await page.setViewportSize(viewport);
209
- await new Promise(r => setTimeout(r, 500));
210
-
211
- // Scroll to bottom to ensure footer is loaded
212
- await page.evaluate(() => {
213
- window.scrollTo(0, document.body.scrollHeight);
214
- });
215
- await new Promise(r => setTimeout(r, 300));
216
-
217
- const result = {
218
- viewport: viewportName,
219
- dimensions: viewport,
220
- tests: [],
221
- passed: 0,
222
- failed: 0,
223
- warnings: []
224
- };
225
-
226
- if (verbose) console.error(`\n📱 Testing ${viewportName} (${viewport.width}x${viewport.height})...`);
227
-
228
- // Test 1: Footer container exists
229
- const footerResult = await findElement(page, FOOTER_SELECTORS.container);
230
- if (footerResult) {
231
- result.tests.push({
232
- name: 'Footer container exists',
233
- passed: true,
234
- selector: footerResult.selector
235
- });
236
- result.passed++;
237
- if (verbose) console.error(` ✓ Footer found: ${footerResult.selector}`);
238
-
239
- // Test 2: Footer position (at bottom)
240
- const positionInfo = await checkFooterPosition(page, footerResult.selector);
241
- if (positionInfo) {
242
- if (positionInfo.isAtBottom) {
243
- result.tests.push({
244
- name: 'Footer at page bottom',
245
- passed: true,
246
- y: positionInfo.y,
247
- pageHeight: positionInfo.pageHeight
248
- });
249
- result.passed++;
250
- if (verbose) console.error(` ✓ Footer at bottom (y: ${Math.round(positionInfo.y)})`);
251
- } else {
252
- result.tests.push({
253
- name: 'Footer at page bottom',
254
- passed: false,
255
- y: positionInfo.y,
256
- footerBottom: positionInfo.footerBottom,
257
- pageHeight: positionInfo.pageHeight,
258
- error: 'Footer not at page bottom'
259
- });
260
- result.failed++;
261
- if (verbose) console.error(` ✗ Footer not at bottom (gap: ${positionInfo.pageHeight - positionInfo.footerBottom}px)`);
262
- }
263
-
264
- // Store dimensions for report
265
- result.footerDimensions = {
266
- height: positionInfo.height,
267
- width: positionInfo.width,
268
- backgroundColor: positionInfo.backgroundColor,
269
- color: positionInfo.color
270
- };
271
- }
272
-
273
- // Test 3: Multi-column layout (desktop/tablet)
274
- if (viewportName !== 'mobile') {
275
- const columns = await countElements(page, FOOTER_SELECTORS.columns);
276
- if (columns.count >= 2) {
277
- result.tests.push({
278
- name: 'Multi-column layout',
279
- passed: true,
280
- count: columns.count,
281
- selector: columns.selector
282
- });
283
- result.passed++;
284
- if (verbose) console.error(` ✓ ${columns.count} columns found`);
285
- } else if (columns.count === 1) {
286
- result.tests.push({
287
- name: 'Multi-column layout',
288
- passed: true,
289
- count: columns.count,
290
- note: 'Single column layout'
291
- });
292
- result.passed++;
293
- if (verbose) console.error(` ✓ Single column layout`);
294
- } else {
295
- result.warnings.push('No clear column structure detected');
296
- if (verbose) console.error(` ⚠ No column structure detected`);
297
- }
298
- }
299
-
300
- // Test 4: Links present
301
- const links = await countVisibleElements(page, FOOTER_SELECTORS.links);
302
- if (links.count >= 1) {
303
- result.tests.push({
304
- name: 'Footer links present',
305
- passed: true,
306
- count: links.count,
307
- selector: links.selector
308
- });
309
- result.passed++;
310
- if (verbose) console.error(` ✓ ${links.count} links found`);
311
- } else {
312
- result.warnings.push('No links found in footer');
313
- if (verbose) console.error(` ⚠ No links found`);
314
- }
315
-
316
- // Test 5: Copyright text
317
- const copyrightInfo = await checkCopyright(page);
318
- if (copyrightInfo) {
319
- if (copyrightInfo.hasCopyright || copyrightInfo.hasYear) {
320
- result.tests.push({
321
- name: 'Copyright text present',
322
- passed: true,
323
- hasCopyright: copyrightInfo.hasCopyright,
324
- hasCurrentYear: copyrightInfo.hasCurrentYear
325
- });
326
- result.passed++;
327
- if (verbose) console.error(` ✓ Copyright found (current year: ${copyrightInfo.hasCurrentYear})`);
328
- } else {
329
- result.warnings.push('No copyright text found');
330
- if (verbose) console.error(` ⚠ No copyright text`);
331
- }
332
- }
333
-
334
- // Test 6: Social icons (optional)
335
- const socialIcons = await countVisibleElements(page, FOOTER_SELECTORS.socialIcons);
336
- if (socialIcons.count > 0) {
337
- result.tests.push({
338
- name: 'Social icons present',
339
- passed: true,
340
- count: socialIcons.count
341
- });
342
- result.passed++;
343
- if (verbose) console.error(` ✓ ${socialIcons.count} social icons found`);
344
- } else {
345
- // Not a failure, just informational
346
- if (verbose) console.error(` ℹ No social icons found`);
347
- }
348
-
349
- } else {
350
- result.tests.push({
351
- name: 'Footer container exists',
352
- passed: false,
353
- error: 'No footer container found'
354
- });
355
- result.failed++;
356
- if (verbose) console.error(` ✗ Footer not found`);
357
- }
358
-
359
- return result;
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
- const absolutePath = validateHtmlPath(args.html);
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 testViewport(page, viewportName, verbose);
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