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,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit Report Section Generators
|
|
3
|
+
*
|
|
4
|
+
* Pure functions for generating markdown table and section content
|
|
5
|
+
* extracted from generate-audit-report.js to keep each file under 200 lines.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { generateCSSFixes } from './generate-audit-report-css-fixes.js';
|
|
10
|
+
|
|
11
|
+
// Re-export so callers only need this one module
|
|
12
|
+
export { generateCSSFixes };
|
|
13
|
+
|
|
14
|
+
// Status icons
|
|
15
|
+
export const STATUS_ICONS = {
|
|
16
|
+
pass: '✅',
|
|
17
|
+
warn: '⚠️',
|
|
18
|
+
fail: '❌',
|
|
19
|
+
info: 'ℹ️'
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Calculate component status from verification result
|
|
24
|
+
* @param {Object|null} result
|
|
25
|
+
* @returns {{status: string, icon: string, label: string}}
|
|
26
|
+
*/
|
|
27
|
+
export function getComponentStatus(result) {
|
|
28
|
+
if (!result) return { status: 'skip', icon: STATUS_ICONS.info, label: 'Not tested' };
|
|
29
|
+
const { summary } = result;
|
|
30
|
+
if (!summary) return { status: 'skip', icon: STATUS_ICONS.info, label: 'No data' };
|
|
31
|
+
if (summary.failed > 0) return { status: 'fail', icon: STATUS_ICONS.fail, label: `${summary.failed} failed` };
|
|
32
|
+
if (summary.warnings?.length > 0) return { status: 'warn', icon: STATUS_ICONS.warn, label: `${summary.warnings.length} warnings` };
|
|
33
|
+
return { status: 'pass', icon: STATUS_ICONS.pass, label: 'Passed' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generate summary table markdown
|
|
38
|
+
* @param {Object} results - Map of component name to result
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
41
|
+
export function generateSummaryTable(results) {
|
|
42
|
+
let table = `| Component | Status | Tests | Details |\n|-----------|--------|-------|----------|\n`;
|
|
43
|
+
for (const [component, result] of Object.entries(results)) {
|
|
44
|
+
const status = getComponentStatus(result);
|
|
45
|
+
const tests = result?.summary ? `${result.summary.passed}/${result.summary.totalTests}` : '-';
|
|
46
|
+
const label = component.charAt(0).toUpperCase() + component.slice(1);
|
|
47
|
+
table += `| ${label} | ${status.icon} ${status.label} | ${tests} | ${result?.url || '-'} |\n`;
|
|
48
|
+
}
|
|
49
|
+
return table;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Generate responsive viewport breakdown table
|
|
54
|
+
* @param {Object} results
|
|
55
|
+
* @returns {string}
|
|
56
|
+
*/
|
|
57
|
+
export function generateViewportTable(results) {
|
|
58
|
+
const viewports = ['mobile', 'tablet', 'desktop'];
|
|
59
|
+
const components = Object.keys(results).filter(c => results[c]?.viewports);
|
|
60
|
+
if (components.length === 0) return '';
|
|
61
|
+
|
|
62
|
+
let table = `| Component | Mobile | Tablet | Desktop |\n|-----------|--------|--------|----------|\n`;
|
|
63
|
+
for (const component of components) {
|
|
64
|
+
const result = results[component];
|
|
65
|
+
const row = [component.charAt(0).toUpperCase() + component.slice(1)];
|
|
66
|
+
for (const vp of viewports) {
|
|
67
|
+
const vpResult = result.viewports?.[vp];
|
|
68
|
+
if (vpResult) {
|
|
69
|
+
const icon = vpResult.failed > 0 ? STATUS_ICONS.fail
|
|
70
|
+
: vpResult.warnings?.length > 0 ? STATUS_ICONS.warn
|
|
71
|
+
: STATUS_ICONS.pass;
|
|
72
|
+
row.push(`${icon} ${vpResult.passed}/${vpResult.tests?.length || 0}`);
|
|
73
|
+
} else {
|
|
74
|
+
row.push('-');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
table += `| ${row.join(' | ')} |\n`;
|
|
78
|
+
}
|
|
79
|
+
return table;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Generate a markdown section for a single component
|
|
84
|
+
* @param {string} component
|
|
85
|
+
* @param {Object|null} result
|
|
86
|
+
* @returns {string}
|
|
87
|
+
*/
|
|
88
|
+
export function generateComponentSection(component, result) {
|
|
89
|
+
const label = component.charAt(0).toUpperCase() + component.slice(1);
|
|
90
|
+
if (!result) return `### ${label}\n\n${STATUS_ICONS.info} Not tested\n\n---\n\n`;
|
|
91
|
+
|
|
92
|
+
const status = getComponentStatus(result);
|
|
93
|
+
let section = `### ${label} ${status.icon}\n\n`;
|
|
94
|
+
|
|
95
|
+
if (component === 'slider' && result.sliderLibrary) {
|
|
96
|
+
section += `**Library:** ${result.sliderLibrary}\n\n`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (result.viewports) {
|
|
100
|
+
for (const [viewport, vpResult] of Object.entries(result.viewports)) {
|
|
101
|
+
const vpLabel = viewport.charAt(0).toUpperCase() + viewport.slice(1);
|
|
102
|
+
section += `#### ${vpLabel} (${vpResult.dimensions?.width}x${vpResult.dimensions?.height})\n\n`;
|
|
103
|
+
|
|
104
|
+
if (vpResult.tests?.length > 0) {
|
|
105
|
+
for (const test of vpResult.tests) {
|
|
106
|
+
const icon = test.passed ? STATUS_ICONS.pass : STATUS_ICONS.fail;
|
|
107
|
+
section += `- ${icon} **${test.name}**`;
|
|
108
|
+
if (test.selector) section += ` - \`${test.selector}\``;
|
|
109
|
+
if (test.count !== undefined) section += ` (${test.count} found)`;
|
|
110
|
+
if (test.note) section += ` - ${test.note}`;
|
|
111
|
+
if (test.error) section += ` - ⚠️ ${test.error}`;
|
|
112
|
+
section += '\n';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (vpResult.warnings?.length > 0) {
|
|
117
|
+
section += '\n**Warnings:**\n';
|
|
118
|
+
for (const warning of vpResult.warnings) {
|
|
119
|
+
section += `- ${STATUS_ICONS.warn} ${warning}\n`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
section += '\n';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (result.screenshots?.length > 0) {
|
|
127
|
+
section += `#### Screenshots\n\n| Viewport | Screenshot |\n|----------|------------|\n`;
|
|
128
|
+
for (const screenshot of result.screenshots) {
|
|
129
|
+
const name = path.basename(screenshot);
|
|
130
|
+
const viewport = name.replace(/^[a-z]+-test-/, '').replace('.png', '');
|
|
131
|
+
section += `| ${viewport} | [${name}](${screenshot}) |\n`;
|
|
132
|
+
}
|
|
133
|
+
section += '\n';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
section += '---\n\n';
|
|
137
|
+
return section;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Generate the full markdown audit report string
|
|
142
|
+
* @param {Object} results
|
|
143
|
+
* @param {string|undefined} url
|
|
144
|
+
* @returns {string}
|
|
145
|
+
*/
|
|
146
|
+
export function generateMarkdownReport(results, url) {
|
|
147
|
+
const timestamp = new Date().toISOString();
|
|
148
|
+
let report = `# Component Audit Report\n\n**Generated:** ${timestamp}\n**URL:** ${url || 'N/A'}\n\n`;
|
|
149
|
+
report += `## Summary\n\n${generateSummaryTable(results)}\n`;
|
|
150
|
+
report += `## Responsive Breakdown\n\n${generateViewportTable(results)}\n`;
|
|
151
|
+
report += `## Component Details\n\n`;
|
|
152
|
+
for (const [component, result] of Object.entries(results)) {
|
|
153
|
+
report += generateComponentSection(component, result);
|
|
154
|
+
}
|
|
155
|
+
report += generateCSSFixes(results);
|
|
156
|
+
report += `---\n\n*Report generated by design-clone verification suite*\n`;
|
|
157
|
+
return report;
|
|
158
|
+
}
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
|
|
21
21
|
import fs from 'fs/promises';
|
|
22
22
|
import path from 'path';
|
|
23
|
-
import { parseArgs, outputJSON, outputError } from '../utils/
|
|
23
|
+
import { parseArgs, outputJSON, outputError } from '../utils/helpers.js';
|
|
24
|
+
import { generateMarkdownReport } from './generate-audit-report-sections.js';
|
|
24
25
|
|
|
25
26
|
// Component types and their result files
|
|
26
27
|
const COMPONENT_FILES = {
|
|
@@ -31,16 +32,10 @@ const COMPONENT_FILES = {
|
|
|
31
32
|
layout: 'layout-results.json'
|
|
32
33
|
};
|
|
33
34
|
|
|
34
|
-
// Status icons
|
|
35
|
-
const STATUS_ICONS = {
|
|
36
|
-
pass: '✅',
|
|
37
|
-
warn: '⚠️',
|
|
38
|
-
fail: '❌',
|
|
39
|
-
info: 'ℹ️'
|
|
40
|
-
};
|
|
41
|
-
|
|
42
35
|
/**
|
|
43
36
|
* Load verification results from directory
|
|
37
|
+
* @param {string} dir
|
|
38
|
+
* @returns {Promise<Object>} Map of component name to parsed result or null
|
|
44
39
|
*/
|
|
45
40
|
async function loadVerificationResults(dir) {
|
|
46
41
|
const results = {};
|
|
@@ -61,264 +56,6 @@ async function loadVerificationResults(dir) {
|
|
|
61
56
|
return results;
|
|
62
57
|
}
|
|
63
58
|
|
|
64
|
-
/**
|
|
65
|
-
* Calculate component status
|
|
66
|
-
*/
|
|
67
|
-
function getComponentStatus(result) {
|
|
68
|
-
if (!result) return { status: 'skip', icon: STATUS_ICONS.info, label: 'Not tested' };
|
|
69
|
-
|
|
70
|
-
const { summary } = result;
|
|
71
|
-
if (!summary) return { status: 'skip', icon: STATUS_ICONS.info, label: 'No data' };
|
|
72
|
-
|
|
73
|
-
if (summary.failed > 0) {
|
|
74
|
-
return { status: 'fail', icon: STATUS_ICONS.fail, label: `${summary.failed} failed` };
|
|
75
|
-
}
|
|
76
|
-
if (summary.warnings?.length > 0) {
|
|
77
|
-
return { status: 'warn', icon: STATUS_ICONS.warn, label: `${summary.warnings.length} warnings` };
|
|
78
|
-
}
|
|
79
|
-
return { status: 'pass', icon: STATUS_ICONS.pass, label: 'Passed' };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Generate summary table
|
|
84
|
-
*/
|
|
85
|
-
function generateSummaryTable(results) {
|
|
86
|
-
let table = `| Component | Status | Tests | Details |
|
|
87
|
-
|-----------|--------|-------|---------|
|
|
88
|
-
`;
|
|
89
|
-
|
|
90
|
-
for (const [component, result] of Object.entries(results)) {
|
|
91
|
-
const status = getComponentStatus(result);
|
|
92
|
-
const tests = result?.summary
|
|
93
|
-
? `${result.summary.passed}/${result.summary.totalTests}`
|
|
94
|
-
: '-';
|
|
95
|
-
table += `| ${component.charAt(0).toUpperCase() + component.slice(1)} | ${status.icon} ${status.label} | ${tests} | ${result?.url || '-'} |\n`;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return table;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Generate viewport breakdown table
|
|
103
|
-
*/
|
|
104
|
-
function generateViewportTable(results) {
|
|
105
|
-
const viewports = ['mobile', 'tablet', 'desktop'];
|
|
106
|
-
const components = Object.keys(results).filter(c => results[c]?.viewports);
|
|
107
|
-
|
|
108
|
-
if (components.length === 0) return '';
|
|
109
|
-
|
|
110
|
-
let table = `| Component | Mobile | Tablet | Desktop |
|
|
111
|
-
|-----------|--------|--------|---------|
|
|
112
|
-
`;
|
|
113
|
-
|
|
114
|
-
for (const component of components) {
|
|
115
|
-
const result = results[component];
|
|
116
|
-
const row = [component.charAt(0).toUpperCase() + component.slice(1)];
|
|
117
|
-
|
|
118
|
-
for (const vp of viewports) {
|
|
119
|
-
const vpResult = result.viewports?.[vp];
|
|
120
|
-
if (vpResult) {
|
|
121
|
-
const icon = vpResult.failed > 0 ? STATUS_ICONS.fail
|
|
122
|
-
: vpResult.warnings?.length > 0 ? STATUS_ICONS.warn
|
|
123
|
-
: STATUS_ICONS.pass;
|
|
124
|
-
row.push(`${icon} ${vpResult.passed}/${vpResult.tests?.length || 0}`);
|
|
125
|
-
} else {
|
|
126
|
-
row.push('-');
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
table += `| ${row.join(' | ')} |\n`;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return table;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Generate component section
|
|
138
|
-
*/
|
|
139
|
-
function generateComponentSection(component, result) {
|
|
140
|
-
if (!result) {
|
|
141
|
-
return `### ${component.charAt(0).toUpperCase() + component.slice(1)}
|
|
142
|
-
|
|
143
|
-
${STATUS_ICONS.info} Not tested
|
|
144
|
-
|
|
145
|
-
---
|
|
146
|
-
|
|
147
|
-
`;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const status = getComponentStatus(result);
|
|
151
|
-
let section = `### ${component.charAt(0).toUpperCase() + component.slice(1)} ${status.icon}
|
|
152
|
-
|
|
153
|
-
`;
|
|
154
|
-
|
|
155
|
-
// Add component-specific info
|
|
156
|
-
if (component === 'slider' && result.sliderLibrary) {
|
|
157
|
-
section += `**Library:** ${result.sliderLibrary}\n\n`;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Test results by viewport
|
|
161
|
-
if (result.viewports) {
|
|
162
|
-
for (const [viewport, vpResult] of Object.entries(result.viewports)) {
|
|
163
|
-
section += `#### ${viewport.charAt(0).toUpperCase() + viewport.slice(1)} (${vpResult.dimensions?.width}x${vpResult.dimensions?.height})\n\n`;
|
|
164
|
-
|
|
165
|
-
// List tests
|
|
166
|
-
if (vpResult.tests?.length > 0) {
|
|
167
|
-
for (const test of vpResult.tests) {
|
|
168
|
-
const icon = test.passed ? STATUS_ICONS.pass : STATUS_ICONS.fail;
|
|
169
|
-
section += `- ${icon} **${test.name}**`;
|
|
170
|
-
if (test.selector) section += ` - \`${test.selector}\``;
|
|
171
|
-
if (test.count !== undefined) section += ` (${test.count} found)`;
|
|
172
|
-
if (test.note) section += ` - ${test.note}`;
|
|
173
|
-
if (test.error) section += ` - ⚠️ ${test.error}`;
|
|
174
|
-
section += '\n';
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Warnings
|
|
179
|
-
if (vpResult.warnings?.length > 0) {
|
|
180
|
-
section += '\n**Warnings:**\n';
|
|
181
|
-
for (const warning of vpResult.warnings) {
|
|
182
|
-
section += `- ${STATUS_ICONS.warn} ${warning}\n`;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
section += '\n';
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Screenshots
|
|
191
|
-
if (result.screenshots?.length > 0) {
|
|
192
|
-
section += `#### Screenshots\n\n`;
|
|
193
|
-
section += `| Viewport | Screenshot |\n|----------|------------|\n`;
|
|
194
|
-
for (const screenshot of result.screenshots) {
|
|
195
|
-
const name = path.basename(screenshot);
|
|
196
|
-
const viewport = name.replace(/^[a-z]+-test-/, '').replace('.png', '');
|
|
197
|
-
section += `| ${viewport} | [${name}](${screenshot}) |\n`;
|
|
198
|
-
}
|
|
199
|
-
section += '\n';
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
section += '---\n\n';
|
|
203
|
-
return section;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Generate CSS fixes section
|
|
208
|
-
*/
|
|
209
|
-
function generateCSSFixes(results) {
|
|
210
|
-
const fixes = [];
|
|
211
|
-
|
|
212
|
-
// Collect issues that might need CSS fixes
|
|
213
|
-
for (const [component, result] of Object.entries(results)) {
|
|
214
|
-
if (!result?.viewports) continue;
|
|
215
|
-
|
|
216
|
-
for (const [viewport, vpResult] of Object.entries(result.viewports)) {
|
|
217
|
-
// Check for layout issues
|
|
218
|
-
if (component === 'header' && vpResult.headerHeight) {
|
|
219
|
-
// Height consistency check already done in verify-header
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (component === 'footer') {
|
|
223
|
-
const positionTest = vpResult.tests?.find(t => t.name === 'Footer at page bottom');
|
|
224
|
-
if (positionTest && !positionTest.passed) {
|
|
225
|
-
fixes.push({
|
|
226
|
-
component,
|
|
227
|
-
viewport,
|
|
228
|
-
issue: 'Footer not at page bottom',
|
|
229
|
-
suggestion: `/* Ensure footer sticks to bottom */
|
|
230
|
-
footer {
|
|
231
|
-
margin-top: auto;
|
|
232
|
-
}
|
|
233
|
-
body {
|
|
234
|
-
min-height: 100vh;
|
|
235
|
-
display: flex;
|
|
236
|
-
flex-direction: column;
|
|
237
|
-
}`
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Check warnings
|
|
243
|
-
if (vpResult.warnings) {
|
|
244
|
-
for (const warning of vpResult.warnings) {
|
|
245
|
-
if (warning.includes('z-index')) {
|
|
246
|
-
fixes.push({
|
|
247
|
-
component,
|
|
248
|
-
viewport,
|
|
249
|
-
issue: warning,
|
|
250
|
-
suggestion: `/* Increase header z-index */
|
|
251
|
-
header, .header, [role="banner"] {
|
|
252
|
-
z-index: 1000;
|
|
253
|
-
}`
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (fixes.length === 0) return '';
|
|
262
|
-
|
|
263
|
-
let section = `## Suggested CSS Fixes
|
|
264
|
-
|
|
265
|
-
`;
|
|
266
|
-
|
|
267
|
-
for (const fix of fixes) {
|
|
268
|
-
section += `### ${fix.component} (${fix.viewport})
|
|
269
|
-
|
|
270
|
-
**Issue:** ${fix.issue}
|
|
271
|
-
|
|
272
|
-
\`\`\`css
|
|
273
|
-
${fix.suggestion}
|
|
274
|
-
\`\`\`
|
|
275
|
-
|
|
276
|
-
`;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
return section;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Generate full markdown report
|
|
284
|
-
*/
|
|
285
|
-
function generateMarkdownReport(results, url) {
|
|
286
|
-
const timestamp = new Date().toISOString();
|
|
287
|
-
|
|
288
|
-
let report = `# Component Audit Report
|
|
289
|
-
|
|
290
|
-
**Generated:** ${timestamp}
|
|
291
|
-
**URL:** ${url || 'N/A'}
|
|
292
|
-
|
|
293
|
-
## Summary
|
|
294
|
-
|
|
295
|
-
${generateSummaryTable(results)}
|
|
296
|
-
|
|
297
|
-
## Responsive Breakdown
|
|
298
|
-
|
|
299
|
-
${generateViewportTable(results)}
|
|
300
|
-
|
|
301
|
-
## Component Details
|
|
302
|
-
|
|
303
|
-
`;
|
|
304
|
-
|
|
305
|
-
// Add each component section
|
|
306
|
-
for (const [component, result] of Object.entries(results)) {
|
|
307
|
-
report += generateComponentSection(component, result);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Add CSS fixes if any
|
|
311
|
-
report += generateCSSFixes(results);
|
|
312
|
-
|
|
313
|
-
// Footer
|
|
314
|
-
report += `---
|
|
315
|
-
|
|
316
|
-
*Report generated by design-clone verification suite*
|
|
317
|
-
`;
|
|
318
|
-
|
|
319
|
-
return report;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
59
|
/**
|
|
323
60
|
* Main function
|
|
324
61
|
*/
|
|
@@ -336,24 +73,17 @@ async function generateAuditReport() {
|
|
|
336
73
|
try {
|
|
337
74
|
if (verbose) console.error(`\n📊 Generating audit report from ${args.dir}\n`);
|
|
338
75
|
|
|
339
|
-
// Load all verification results
|
|
340
76
|
const results = await loadVerificationResults(args.dir);
|
|
341
|
-
|
|
342
|
-
// Get URL from first available result
|
|
343
77
|
const url = Object.values(results).find(r => r?.url)?.url;
|
|
344
78
|
|
|
345
|
-
// Count loaded components
|
|
346
79
|
const loadedCount = Object.values(results).filter(r => r !== null).length;
|
|
347
80
|
if (verbose) console.error(` Loaded ${loadedCount}/${Object.keys(COMPONENT_FILES).length} component results`);
|
|
348
81
|
|
|
349
|
-
// Generate report
|
|
350
82
|
const report = generateMarkdownReport(results, url);
|
|
351
83
|
|
|
352
|
-
// Write report
|
|
353
84
|
await fs.writeFile(outputPath, report, 'utf-8');
|
|
354
85
|
if (verbose) console.error(` ✓ Report written to ${outputPath}`);
|
|
355
86
|
|
|
356
|
-
// Calculate overall stats
|
|
357
87
|
let totalTests = 0, totalPassed = 0, totalFailed = 0, totalWarnings = 0;
|
|
358
88
|
for (const result of Object.values(results)) {
|
|
359
89
|
if (result?.summary) {
|
|
@@ -368,13 +98,7 @@ async function generateAuditReport() {
|
|
|
368
98
|
success: totalFailed === 0,
|
|
369
99
|
reportPath: outputPath,
|
|
370
100
|
url,
|
|
371
|
-
summary: {
|
|
372
|
-
components: loadedCount,
|
|
373
|
-
totalTests,
|
|
374
|
-
passed: totalPassed,
|
|
375
|
-
failed: totalFailed,
|
|
376
|
-
warnings: totalWarnings
|
|
377
|
-
}
|
|
101
|
+
summary: { components: loadedCount, totalTests, passed: totalPassed, failed: totalFailed, warnings: totalWarnings }
|
|
378
102
|
};
|
|
379
103
|
|
|
380
104
|
if (verbose) {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output Quality Scorer
|
|
3
|
+
*
|
|
4
|
+
* Scores the quality of a design clone capture on 5 metrics (0-100 scale).
|
|
5
|
+
* Auto-runs for clone-px mode, opt-in for basic clone.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
|
|
10
|
+
const WEIGHTS = {
|
|
11
|
+
cssCoverage: 0.30,
|
|
12
|
+
assetCompleteness: 0.25,
|
|
13
|
+
responsiveFidelity: 0.20,
|
|
14
|
+
htmlSemantics: 0.15,
|
|
15
|
+
accessibility: 0.10
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const SEMANTIC_TAGS = new Set([
|
|
19
|
+
'header', 'footer', 'nav', 'main', 'article', 'section', 'aside', 'figure', 'figcaption'
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
function scoreCssCoverage(extraction) {
|
|
23
|
+
if (!extraction?.css || !extraction?.filtered) return 0;
|
|
24
|
+
const reduction = parseInt(extraction.filtered.reduction) || 0;
|
|
25
|
+
const kept = 100 - reduction;
|
|
26
|
+
if (kept >= 80) return 100;
|
|
27
|
+
if (kept >= 50) return 80;
|
|
28
|
+
return 60;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function scoreAssetCompleteness(stats) {
|
|
32
|
+
if (!stats || stats.total === 0) return 100;
|
|
33
|
+
const ratio = stats.downloaded / stats.total;
|
|
34
|
+
return Math.round(ratio * 100);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function scoreResponsiveFidelity(screenshots) {
|
|
38
|
+
if (!screenshots) return 0;
|
|
39
|
+
const count = Array.isArray(screenshots) ? screenshots.length : 0;
|
|
40
|
+
if (count >= 3) return 100;
|
|
41
|
+
if (count >= 2) return 70;
|
|
42
|
+
return 40;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function scoreHtmlSemantics(htmlContent) {
|
|
46
|
+
if (!htmlContent) return 0;
|
|
47
|
+
let found = 0;
|
|
48
|
+
for (const tag of SEMANTIC_TAGS) {
|
|
49
|
+
if (htmlContent.includes(`<${tag}`)) found++;
|
|
50
|
+
}
|
|
51
|
+
return Math.min(100, Math.round((found / SEMANTIC_TAGS.size) * 100));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function scoreAccessibility(htmlContent) {
|
|
55
|
+
if (!htmlContent) return 0;
|
|
56
|
+
let score = 0;
|
|
57
|
+
const imgCount = (htmlContent.match(/<img/g) || []).length;
|
|
58
|
+
const altCount = (htmlContent.match(/<img[^>]+alt=/g) || []).length;
|
|
59
|
+
if (imgCount === 0) score += 50;
|
|
60
|
+
else score += Math.round((altCount / imgCount) * 50);
|
|
61
|
+
if (htmlContent.includes('<h1')) score += 25;
|
|
62
|
+
if (/<html[^>]+lang=/.test(htmlContent)) score += 25;
|
|
63
|
+
return Math.min(100, score);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Score the quality of a design clone capture
|
|
68
|
+
* @param {{ extraction: Object, screenshots: Array, assetStats: Object, outputDir: string }} params
|
|
69
|
+
* @returns {Promise<{overall: number, metrics: Object, weights: Object, maxScore: number}>}
|
|
70
|
+
*/
|
|
71
|
+
export async function scoreCapture({ extraction, screenshots, assetStats, outputDir }) {
|
|
72
|
+
let htmlContent = '';
|
|
73
|
+
if (extraction?.html?.path) {
|
|
74
|
+
try { htmlContent = await fs.readFile(extraction.html.path, 'utf-8'); } catch { /* ok */ }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const metrics = {
|
|
78
|
+
cssCoverage: scoreCssCoverage(extraction),
|
|
79
|
+
assetCompleteness: scoreAssetCompleteness(assetStats),
|
|
80
|
+
responsiveFidelity: scoreResponsiveFidelity(screenshots),
|
|
81
|
+
htmlSemantics: scoreHtmlSemantics(htmlContent),
|
|
82
|
+
accessibility: scoreAccessibility(htmlContent)
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const overall = Math.round(
|
|
86
|
+
Object.entries(WEIGHTS).reduce(
|
|
87
|
+
(sum, [key, weight]) => sum + (metrics[key] * weight), 0
|
|
88
|
+
)
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return { overall, metrics, weights: WEIGHTS, maxScore: 100 };
|
|
92
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Footer Viewport Checks
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates all per-viewport footer checks using helpers.
|
|
5
|
+
* Separated from verify-footer-helpers.js to keep each file under 200 lines.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
FOOTER_SELECTORS,
|
|
10
|
+
findElement,
|
|
11
|
+
countElements,
|
|
12
|
+
countVisibleElements,
|
|
13
|
+
checkFooterPosition,
|
|
14
|
+
checkCopyright
|
|
15
|
+
} from './verify-footer-helpers.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Test footer at a specific viewport — runs all footer checks
|
|
19
|
+
* @param {import('playwright').Page} page
|
|
20
|
+
* @param {string} viewportName
|
|
21
|
+
* @param {Object} VIEWPORTS - Viewport map
|
|
22
|
+
* @param {boolean} verbose
|
|
23
|
+
* @returns {Promise<Object>} Viewport result object
|
|
24
|
+
*/
|
|
25
|
+
export async function testFooterViewport(page, viewportName, VIEWPORTS, verbose = false) {
|
|
26
|
+
const viewport = VIEWPORTS[viewportName];
|
|
27
|
+
await page.setViewportSize(viewport);
|
|
28
|
+
await new Promise(r => setTimeout(r, 500));
|
|
29
|
+
await page.evaluate(() => { window.scrollTo(0, document.body.scrollHeight); });
|
|
30
|
+
await new Promise(r => setTimeout(r, 300));
|
|
31
|
+
|
|
32
|
+
const result = { viewport: viewportName, dimensions: viewport, tests: [], passed: 0, failed: 0, warnings: [] };
|
|
33
|
+
if (verbose) console.error(`\n📱 Testing ${viewportName} (${viewport.width}x${viewport.height})...`);
|
|
34
|
+
|
|
35
|
+
const footerResult = await findElement(page, FOOTER_SELECTORS.container);
|
|
36
|
+
if (!footerResult) {
|
|
37
|
+
result.tests.push({ name: 'Footer container exists', passed: false, error: 'No footer container found' });
|
|
38
|
+
result.failed++;
|
|
39
|
+
if (verbose) console.error(` ✗ Footer not found`);
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
result.tests.push({ name: 'Footer container exists', passed: true, selector: footerResult.selector });
|
|
44
|
+
result.passed++;
|
|
45
|
+
if (verbose) console.error(` ✓ Footer found: ${footerResult.selector}`);
|
|
46
|
+
|
|
47
|
+
const positionInfo = await checkFooterPosition(page, footerResult.selector);
|
|
48
|
+
if (positionInfo) {
|
|
49
|
+
if (positionInfo.isAtBottom) {
|
|
50
|
+
result.tests.push({ name: 'Footer at page bottom', passed: true, y: positionInfo.y, pageHeight: positionInfo.pageHeight });
|
|
51
|
+
result.passed++;
|
|
52
|
+
if (verbose) console.error(` ✓ Footer at bottom (y: ${Math.round(positionInfo.y)})`);
|
|
53
|
+
} else {
|
|
54
|
+
result.tests.push({ name: 'Footer at page bottom', passed: false, y: positionInfo.y, footerBottom: positionInfo.footerBottom, pageHeight: positionInfo.pageHeight, error: 'Footer not at page bottom' });
|
|
55
|
+
result.failed++;
|
|
56
|
+
if (verbose) console.error(` ✗ Footer not at bottom (gap: ${positionInfo.pageHeight - positionInfo.footerBottom}px)`);
|
|
57
|
+
}
|
|
58
|
+
result.footerDimensions = { height: positionInfo.height, width: positionInfo.width, backgroundColor: positionInfo.backgroundColor, color: positionInfo.color };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (viewportName !== 'mobile') {
|
|
62
|
+
const columns = await countElements(page, FOOTER_SELECTORS.columns);
|
|
63
|
+
if (columns.count >= 1) {
|
|
64
|
+
result.tests.push({ name: 'Multi-column layout', passed: true, count: columns.count, selector: columns.selector, note: columns.count === 1 ? 'Single column layout' : undefined });
|
|
65
|
+
result.passed++;
|
|
66
|
+
if (verbose) console.error(` ✓ ${columns.count} column(s) found`);
|
|
67
|
+
} else {
|
|
68
|
+
result.warnings.push('No clear column structure detected');
|
|
69
|
+
if (verbose) console.error(` ⚠ No column structure detected`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const links = await countVisibleElements(page, FOOTER_SELECTORS.links);
|
|
74
|
+
if (links.count >= 1) {
|
|
75
|
+
result.tests.push({ name: 'Footer links present', passed: true, count: links.count, selector: links.selector });
|
|
76
|
+
result.passed++;
|
|
77
|
+
if (verbose) console.error(` ✓ ${links.count} links found`);
|
|
78
|
+
} else {
|
|
79
|
+
result.warnings.push('No links found in footer');
|
|
80
|
+
if (verbose) console.error(` ⚠ No links found`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const copyrightInfo = await checkCopyright(page);
|
|
84
|
+
if (copyrightInfo && (copyrightInfo.hasCopyright || copyrightInfo.hasYear)) {
|
|
85
|
+
result.tests.push({ name: 'Copyright text present', passed: true, hasCopyright: copyrightInfo.hasCopyright, hasCurrentYear: copyrightInfo.hasCurrentYear });
|
|
86
|
+
result.passed++;
|
|
87
|
+
if (verbose) console.error(` ✓ Copyright found (current year: ${copyrightInfo.hasCurrentYear})`);
|
|
88
|
+
} else {
|
|
89
|
+
result.warnings.push('No copyright text found');
|
|
90
|
+
if (verbose) console.error(` ⚠ No copyright text`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const socialIcons = await countVisibleElements(page, FOOTER_SELECTORS.socialIcons);
|
|
94
|
+
if (socialIcons.count > 0) {
|
|
95
|
+
result.tests.push({ name: 'Social icons present', passed: true, count: socialIcons.count });
|
|
96
|
+
result.passed++;
|
|
97
|
+
if (verbose) console.error(` ✓ ${socialIcons.count} social icons found`);
|
|
98
|
+
} else {
|
|
99
|
+
if (verbose) console.error(` ℹ No social icons found`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return result;
|
|
103
|
+
}
|