laxy-verify 1.2.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/dist/audit/broken-links.d.ts +25 -25
- package/dist/audit/broken-links.js +97 -97
- package/dist/badge.d.ts +2 -1
- package/dist/badge.js +18 -14
- package/dist/cli.js +1246 -1233
- package/dist/config.d.ts +102 -102
- package/dist/config.js +360 -360
- package/dist/entitlement.d.ts +15 -13
- package/dist/entitlement.js +98 -94
- package/dist/init.js +132 -132
- package/dist/lighthouse.d.ts +37 -37
- package/dist/lighthouse.js +231 -231
- package/dist/report-markdown.d.ts +53 -53
- package/dist/report-markdown.js +407 -407
- package/dist/security-audit.d.ts +17 -17
- package/dist/security-audit.js +127 -127
- package/dist/verification-core/report.js +526 -526
- package/dist/verification-core/types.d.ts +164 -164
- package/dist/visual-diff.d.ts +33 -33
- package/dist/visual-diff.js +213 -213
- package/package.json +1 -1
- package/dist/ai-analysis.d.ts +0 -28
- package/dist/ai-analysis.js +0 -32
- package/dist/compare-env.d.ts +0 -23
- package/dist/compare-env.js +0 -55
- package/dist/init-analysis.d.ts +0 -6
- package/dist/init-analysis.js +0 -302
- package/dist/route-discovery.d.ts +0 -7
- package/dist/route-discovery.js +0 -108
package/dist/visual-diff.js
CHANGED
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
-
};
|
|
38
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.formatVisualDiffSummary = formatVisualDiffSummary;
|
|
40
|
-
exports.runVisualDiff = runVisualDiff;
|
|
41
|
-
const fs = __importStar(require("node:fs"));
|
|
42
|
-
const path = __importStar(require("node:path"));
|
|
43
|
-
const puppeteer_1 = __importDefault(require("puppeteer"));
|
|
44
|
-
const pngjs_1 = require("pngjs");
|
|
45
|
-
const VISUAL_DIFF_VIEWPORTS = [
|
|
46
|
-
{ viewport: "desktop", width: 1280, height: 720 },
|
|
47
|
-
{ viewport: "tablet", width: 768, height: 1024 },
|
|
48
|
-
{ viewport: "mobile", width: 375, height: 812, isMobile: true, deviceScaleFactor: 2 },
|
|
49
|
-
];
|
|
50
|
-
function formatViewportResult(viewport) {
|
|
51
|
-
return viewport.hasBaseline
|
|
52
|
-
? `${viewport.viewport} ${viewport.diffPercentage}% (${viewport.verdict})`
|
|
53
|
-
: `${viewport.viewport} baseline seeded`;
|
|
54
|
-
}
|
|
55
|
-
function formatVisualDiffSummary(result) {
|
|
56
|
-
return result.viewports.map(formatViewportResult).join(", ");
|
|
57
|
-
}
|
|
58
|
-
function ensureDir(dir) {
|
|
59
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
60
|
-
}
|
|
61
|
-
async function captureScreenshot(url, outputPath, viewport, options) {
|
|
62
|
-
const browser = await puppeteer_1.default.launch({ headless: true, args: ["--no-sandbox", "--disable-setuid-sandbox"] });
|
|
63
|
-
try {
|
|
64
|
-
const page = await browser.newPage();
|
|
65
|
-
await page.setViewport({
|
|
66
|
-
width: viewport.width,
|
|
67
|
-
height: viewport.height,
|
|
68
|
-
isMobile: viewport.isMobile ?? false,
|
|
69
|
-
deviceScaleFactor: viewport.deviceScaleFactor ?? 1,
|
|
70
|
-
});
|
|
71
|
-
await page.goto(url, { waitUntil: "networkidle2", timeout: 20000 });
|
|
72
|
-
await page.waitForSelector("body", { timeout: 5000 });
|
|
73
|
-
await page.evaluate(({ disableAnimations, ignoreSelectors }) => {
|
|
74
|
-
if (disableAnimations) {
|
|
75
|
-
for (const node of Array.from(document.querySelectorAll("[data-animate]"))) {
|
|
76
|
-
const el = node;
|
|
77
|
-
el.style.setProperty("animation", "none", "important");
|
|
78
|
-
el.style.setProperty("transition", "none", "important");
|
|
79
|
-
el.getAnimations?.().forEach((animation) => animation.cancel());
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
for (const selector of ignoreSelectors) {
|
|
83
|
-
for (const node of Array.from(document.querySelectorAll(selector))) {
|
|
84
|
-
const el = node;
|
|
85
|
-
el.setAttribute("data-laxy-visual-ignore", "true");
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
if (!document.getElementById("laxy-visual-diff-style")) {
|
|
89
|
-
const style = document.createElement("style");
|
|
90
|
-
style.id = "laxy-visual-diff-style";
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.formatVisualDiffSummary = formatVisualDiffSummary;
|
|
40
|
+
exports.runVisualDiff = runVisualDiff;
|
|
41
|
+
const fs = __importStar(require("node:fs"));
|
|
42
|
+
const path = __importStar(require("node:path"));
|
|
43
|
+
const puppeteer_1 = __importDefault(require("puppeteer"));
|
|
44
|
+
const pngjs_1 = require("pngjs");
|
|
45
|
+
const VISUAL_DIFF_VIEWPORTS = [
|
|
46
|
+
{ viewport: "desktop", width: 1280, height: 720 },
|
|
47
|
+
{ viewport: "tablet", width: 768, height: 1024 },
|
|
48
|
+
{ viewport: "mobile", width: 375, height: 812, isMobile: true, deviceScaleFactor: 2 },
|
|
49
|
+
];
|
|
50
|
+
function formatViewportResult(viewport) {
|
|
51
|
+
return viewport.hasBaseline
|
|
52
|
+
? `${viewport.viewport} ${viewport.diffPercentage}% (${viewport.verdict})`
|
|
53
|
+
: `${viewport.viewport} baseline seeded`;
|
|
54
|
+
}
|
|
55
|
+
function formatVisualDiffSummary(result) {
|
|
56
|
+
return result.viewports.map(formatViewportResult).join(", ");
|
|
57
|
+
}
|
|
58
|
+
function ensureDir(dir) {
|
|
59
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
async function captureScreenshot(url, outputPath, viewport, options) {
|
|
62
|
+
const browser = await puppeteer_1.default.launch({ headless: true, args: ["--no-sandbox", "--disable-setuid-sandbox"] });
|
|
63
|
+
try {
|
|
64
|
+
const page = await browser.newPage();
|
|
65
|
+
await page.setViewport({
|
|
66
|
+
width: viewport.width,
|
|
67
|
+
height: viewport.height,
|
|
68
|
+
isMobile: viewport.isMobile ?? false,
|
|
69
|
+
deviceScaleFactor: viewport.deviceScaleFactor ?? 1,
|
|
70
|
+
});
|
|
71
|
+
await page.goto(url, { waitUntil: "networkidle2", timeout: 20000 });
|
|
72
|
+
await page.waitForSelector("body", { timeout: 5000 });
|
|
73
|
+
await page.evaluate(({ disableAnimations, ignoreSelectors }) => {
|
|
74
|
+
if (disableAnimations) {
|
|
75
|
+
for (const node of Array.from(document.querySelectorAll("[data-animate]"))) {
|
|
76
|
+
const el = node;
|
|
77
|
+
el.style.setProperty("animation", "none", "important");
|
|
78
|
+
el.style.setProperty("transition", "none", "important");
|
|
79
|
+
el.getAnimations?.().forEach((animation) => animation.cancel());
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const selector of ignoreSelectors) {
|
|
83
|
+
for (const node of Array.from(document.querySelectorAll(selector))) {
|
|
84
|
+
const el = node;
|
|
85
|
+
el.setAttribute("data-laxy-visual-ignore", "true");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (!document.getElementById("laxy-visual-diff-style")) {
|
|
89
|
+
const style = document.createElement("style");
|
|
90
|
+
style.id = "laxy-visual-diff-style";
|
|
91
91
|
style.textContent = `
|
|
92
92
|
[data-animate] {
|
|
93
93
|
animation: none !important;
|
|
@@ -98,126 +98,126 @@ async function captureScreenshot(url, outputPath, viewport, options) {
|
|
|
98
98
|
animation: none !important;
|
|
99
99
|
transition: none !important;
|
|
100
100
|
}
|
|
101
|
-
`;
|
|
102
|
-
document.head.appendChild(style);
|
|
103
|
-
}
|
|
104
|
-
}, {
|
|
105
|
-
disableAnimations: options.disableAnimations,
|
|
106
|
-
ignoreSelectors: options.ignoreSelectors,
|
|
107
|
-
});
|
|
108
|
-
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
109
|
-
await page.screenshot({ path: outputPath, fullPage: true, type: "png" });
|
|
110
|
-
}
|
|
111
|
-
finally {
|
|
112
|
-
await browser.close();
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
async function loadPixelmatch() {
|
|
116
|
-
const dynamicImport = new Function("specifier", "return import(specifier)");
|
|
117
|
-
const mod = await dynamicImport("pixelmatch");
|
|
118
|
-
return mod.default;
|
|
119
|
-
}
|
|
120
|
-
async function compareImages(baselinePath, currentPath, diffOutputPath, options) {
|
|
121
|
-
const baselinePng = pngjs_1.PNG.sync.read(fs.readFileSync(baselinePath));
|
|
122
|
-
const currentPng = pngjs_1.PNG.sync.read(fs.readFileSync(currentPath));
|
|
123
|
-
const width = Math.min(baselinePng.width, currentPng.width);
|
|
124
|
-
const height = Math.min(baselinePng.height, currentPng.height);
|
|
125
|
-
const cropData = (png, w, h) => {
|
|
126
|
-
if (png.width === w && png.height === h)
|
|
127
|
-
return png.data;
|
|
128
|
-
const cropped = Buffer.alloc(w * h * 4);
|
|
129
|
-
for (let y = 0; y < h; y++) {
|
|
130
|
-
png.data.copy(cropped, y * w * 4, y * png.width * 4, y * png.width * 4 + w * 4);
|
|
131
|
-
}
|
|
132
|
-
return cropped;
|
|
133
|
-
};
|
|
134
|
-
const baseData = cropData(baselinePng, width, height);
|
|
135
|
-
const currData = cropData(currentPng, width, height);
|
|
136
|
-
const diff = new pngjs_1.PNG({ width, height });
|
|
137
|
-
const pixelmatch = await loadPixelmatch();
|
|
138
|
-
const diffPixels = pixelmatch(baseData, currData, diff.data, width, height, {
|
|
139
|
-
threshold: options.pixelmatchThreshold,
|
|
140
|
-
});
|
|
141
|
-
ensureDir(path.dirname(diffOutputPath));
|
|
142
|
-
fs.writeFileSync(diffOutputPath, pngjs_1.PNG.sync.write(diff));
|
|
143
|
-
const totalPixels = width * height;
|
|
144
|
-
const diffPercentage = Math.round((diffPixels / totalPixels) * 10000) / 100;
|
|
145
|
-
return { diffPixels, totalPixels, diffPercentage };
|
|
146
|
-
}
|
|
147
|
-
async function runVisualDiff(projectDir, url, label = "current", options = {}) {
|
|
148
|
-
const dir = path.join(projectDir, ".laxy-verify", "visual");
|
|
149
|
-
ensureDir(dir);
|
|
150
|
-
const normalizedOptions = {
|
|
151
|
-
pixelmatchThreshold: options.pixelmatchThreshold ?? 0.1,
|
|
152
|
-
warnThreshold: options.warnThreshold ?? 30,
|
|
153
|
-
rollbackThreshold: options.rollbackThreshold ?? 60,
|
|
154
|
-
ignoreSelectors: options.ignoreSelectors ?? [],
|
|
155
|
-
disableAnimations: options.disableAnimations ?? true,
|
|
156
|
-
};
|
|
157
|
-
const viewportResults = [];
|
|
158
|
-
for (const viewport of VISUAL_DIFF_VIEWPORTS) {
|
|
159
|
-
const baselinePath = path.join(dir, `${viewport.viewport}.baseline.png`);
|
|
160
|
-
const currentPath = path.join(dir, `${label}.${viewport.viewport}.png`);
|
|
161
|
-
const diffPath = path.join(dir, `${label}.${viewport.viewport}.diff.png`);
|
|
162
|
-
await captureScreenshot(url, currentPath, viewport, normalizedOptions);
|
|
163
|
-
if (!fs.existsSync(baselinePath)) {
|
|
164
|
-
fs.copyFileSync(currentPath, baselinePath);
|
|
165
|
-
viewportResults.push({
|
|
166
|
-
viewport: viewport.viewport,
|
|
167
|
-
hasBaseline: false,
|
|
168
|
-
diffPercentage: 0,
|
|
169
|
-
verdict: "pass",
|
|
170
|
-
diffPixels: 0,
|
|
171
|
-
totalPixels: 0,
|
|
172
|
-
baselinePath,
|
|
173
|
-
currentPath,
|
|
174
|
-
diffPath: "",
|
|
175
|
-
});
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
const comparison = await compareImages(baselinePath, currentPath, diffPath, normalizedOptions);
|
|
179
|
-
let verdict = "pass";
|
|
180
|
-
if (comparison.diffPercentage >= normalizedOptions.rollbackThreshold) {
|
|
181
|
-
verdict = "rollback";
|
|
182
|
-
}
|
|
183
|
-
else if (comparison.diffPercentage >= normalizedOptions.warnThreshold) {
|
|
184
|
-
verdict = "warn";
|
|
185
|
-
}
|
|
186
|
-
if (verdict === "pass") {
|
|
187
|
-
fs.copyFileSync(currentPath, baselinePath);
|
|
188
|
-
}
|
|
189
|
-
viewportResults.push({
|
|
190
|
-
viewport: viewport.viewport,
|
|
191
|
-
hasBaseline: true,
|
|
192
|
-
diffPercentage: comparison.diffPercentage,
|
|
193
|
-
verdict,
|
|
194
|
-
diffPixels: comparison.diffPixels,
|
|
195
|
-
totalPixels: comparison.totalPixels,
|
|
196
|
-
baselinePath,
|
|
197
|
-
currentPath,
|
|
198
|
-
diffPath,
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
const comparableResults = viewportResults.filter((viewport) => viewport.hasBaseline);
|
|
202
|
-
const summary = viewportResults.map(formatViewportResult).join(", ");
|
|
203
|
-
const worstVerdict = comparableResults.some((viewport) => viewport.verdict === "rollback")
|
|
204
|
-
? "rollback"
|
|
205
|
-
: comparableResults.some((viewport) => viewport.verdict === "warn")
|
|
206
|
-
? "warn"
|
|
207
|
-
: "pass";
|
|
208
|
-
const maxDiffPercentage = comparableResults.reduce((max, viewport) => Math.max(max, viewport.diffPercentage), 0);
|
|
209
|
-
const totalDiffPixels = comparableResults.reduce((sum, viewport) => sum + viewport.diffPixels, 0);
|
|
210
|
-
const totalPixels = comparableResults.reduce((sum, viewport) => sum + viewport.totalPixels, 0);
|
|
211
|
-
return {
|
|
212
|
-
hasBaseline: comparableResults.length === viewportResults.length,
|
|
213
|
-
diffPercentage: Math.round(maxDiffPercentage * 100) / 100,
|
|
214
|
-
verdict: worstVerdict,
|
|
215
|
-
diffPixels: totalDiffPixels,
|
|
216
|
-
totalPixels,
|
|
217
|
-
baselinePath: viewportResults[0]?.baselinePath ?? "",
|
|
218
|
-
currentPath: viewportResults[0]?.currentPath ?? "",
|
|
219
|
-
diffPath: viewportResults[0]?.diffPath ?? "",
|
|
220
|
-
viewports: viewportResults,
|
|
221
|
-
summary,
|
|
222
|
-
};
|
|
223
|
-
}
|
|
101
|
+
`;
|
|
102
|
+
document.head.appendChild(style);
|
|
103
|
+
}
|
|
104
|
+
}, {
|
|
105
|
+
disableAnimations: options.disableAnimations,
|
|
106
|
+
ignoreSelectors: options.ignoreSelectors,
|
|
107
|
+
});
|
|
108
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
109
|
+
await page.screenshot({ path: outputPath, fullPage: true, type: "png" });
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
await browser.close();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function loadPixelmatch() {
|
|
116
|
+
const dynamicImport = new Function("specifier", "return import(specifier)");
|
|
117
|
+
const mod = await dynamicImport("pixelmatch");
|
|
118
|
+
return mod.default;
|
|
119
|
+
}
|
|
120
|
+
async function compareImages(baselinePath, currentPath, diffOutputPath, options) {
|
|
121
|
+
const baselinePng = pngjs_1.PNG.sync.read(fs.readFileSync(baselinePath));
|
|
122
|
+
const currentPng = pngjs_1.PNG.sync.read(fs.readFileSync(currentPath));
|
|
123
|
+
const width = Math.min(baselinePng.width, currentPng.width);
|
|
124
|
+
const height = Math.min(baselinePng.height, currentPng.height);
|
|
125
|
+
const cropData = (png, w, h) => {
|
|
126
|
+
if (png.width === w && png.height === h)
|
|
127
|
+
return png.data;
|
|
128
|
+
const cropped = Buffer.alloc(w * h * 4);
|
|
129
|
+
for (let y = 0; y < h; y++) {
|
|
130
|
+
png.data.copy(cropped, y * w * 4, y * png.width * 4, y * png.width * 4 + w * 4);
|
|
131
|
+
}
|
|
132
|
+
return cropped;
|
|
133
|
+
};
|
|
134
|
+
const baseData = cropData(baselinePng, width, height);
|
|
135
|
+
const currData = cropData(currentPng, width, height);
|
|
136
|
+
const diff = new pngjs_1.PNG({ width, height });
|
|
137
|
+
const pixelmatch = await loadPixelmatch();
|
|
138
|
+
const diffPixels = pixelmatch(baseData, currData, diff.data, width, height, {
|
|
139
|
+
threshold: options.pixelmatchThreshold,
|
|
140
|
+
});
|
|
141
|
+
ensureDir(path.dirname(diffOutputPath));
|
|
142
|
+
fs.writeFileSync(diffOutputPath, pngjs_1.PNG.sync.write(diff));
|
|
143
|
+
const totalPixels = width * height;
|
|
144
|
+
const diffPercentage = Math.round((diffPixels / totalPixels) * 10000) / 100;
|
|
145
|
+
return { diffPixels, totalPixels, diffPercentage };
|
|
146
|
+
}
|
|
147
|
+
async function runVisualDiff(projectDir, url, label = "current", options = {}) {
|
|
148
|
+
const dir = path.join(projectDir, ".laxy-verify", "visual");
|
|
149
|
+
ensureDir(dir);
|
|
150
|
+
const normalizedOptions = {
|
|
151
|
+
pixelmatchThreshold: options.pixelmatchThreshold ?? 0.1,
|
|
152
|
+
warnThreshold: options.warnThreshold ?? 30,
|
|
153
|
+
rollbackThreshold: options.rollbackThreshold ?? 60,
|
|
154
|
+
ignoreSelectors: options.ignoreSelectors ?? [],
|
|
155
|
+
disableAnimations: options.disableAnimations ?? true,
|
|
156
|
+
};
|
|
157
|
+
const viewportResults = [];
|
|
158
|
+
for (const viewport of VISUAL_DIFF_VIEWPORTS) {
|
|
159
|
+
const baselinePath = path.join(dir, `${viewport.viewport}.baseline.png`);
|
|
160
|
+
const currentPath = path.join(dir, `${label}.${viewport.viewport}.png`);
|
|
161
|
+
const diffPath = path.join(dir, `${label}.${viewport.viewport}.diff.png`);
|
|
162
|
+
await captureScreenshot(url, currentPath, viewport, normalizedOptions);
|
|
163
|
+
if (!fs.existsSync(baselinePath)) {
|
|
164
|
+
fs.copyFileSync(currentPath, baselinePath);
|
|
165
|
+
viewportResults.push({
|
|
166
|
+
viewport: viewport.viewport,
|
|
167
|
+
hasBaseline: false,
|
|
168
|
+
diffPercentage: 0,
|
|
169
|
+
verdict: "pass",
|
|
170
|
+
diffPixels: 0,
|
|
171
|
+
totalPixels: 0,
|
|
172
|
+
baselinePath,
|
|
173
|
+
currentPath,
|
|
174
|
+
diffPath: "",
|
|
175
|
+
});
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const comparison = await compareImages(baselinePath, currentPath, diffPath, normalizedOptions);
|
|
179
|
+
let verdict = "pass";
|
|
180
|
+
if (comparison.diffPercentage >= normalizedOptions.rollbackThreshold) {
|
|
181
|
+
verdict = "rollback";
|
|
182
|
+
}
|
|
183
|
+
else if (comparison.diffPercentage >= normalizedOptions.warnThreshold) {
|
|
184
|
+
verdict = "warn";
|
|
185
|
+
}
|
|
186
|
+
if (verdict === "pass") {
|
|
187
|
+
fs.copyFileSync(currentPath, baselinePath);
|
|
188
|
+
}
|
|
189
|
+
viewportResults.push({
|
|
190
|
+
viewport: viewport.viewport,
|
|
191
|
+
hasBaseline: true,
|
|
192
|
+
diffPercentage: comparison.diffPercentage,
|
|
193
|
+
verdict,
|
|
194
|
+
diffPixels: comparison.diffPixels,
|
|
195
|
+
totalPixels: comparison.totalPixels,
|
|
196
|
+
baselinePath,
|
|
197
|
+
currentPath,
|
|
198
|
+
diffPath,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
const comparableResults = viewportResults.filter((viewport) => viewport.hasBaseline);
|
|
202
|
+
const summary = viewportResults.map(formatViewportResult).join(", ");
|
|
203
|
+
const worstVerdict = comparableResults.some((viewport) => viewport.verdict === "rollback")
|
|
204
|
+
? "rollback"
|
|
205
|
+
: comparableResults.some((viewport) => viewport.verdict === "warn")
|
|
206
|
+
? "warn"
|
|
207
|
+
: "pass";
|
|
208
|
+
const maxDiffPercentage = comparableResults.reduce((max, viewport) => Math.max(max, viewport.diffPercentage), 0);
|
|
209
|
+
const totalDiffPixels = comparableResults.reduce((sum, viewport) => sum + viewport.diffPixels, 0);
|
|
210
|
+
const totalPixels = comparableResults.reduce((sum, viewport) => sum + viewport.totalPixels, 0);
|
|
211
|
+
return {
|
|
212
|
+
hasBaseline: comparableResults.length === viewportResults.length,
|
|
213
|
+
diffPercentage: Math.round(maxDiffPercentage * 100) / 100,
|
|
214
|
+
verdict: worstVerdict,
|
|
215
|
+
diffPixels: totalDiffPixels,
|
|
216
|
+
totalPixels,
|
|
217
|
+
baselinePath: viewportResults[0]?.baselinePath ?? "",
|
|
218
|
+
currentPath: viewportResults[0]?.currentPath ?? "",
|
|
219
|
+
diffPath: viewportResults[0]?.diffPath ?? "",
|
|
220
|
+
viewports: viewportResults,
|
|
221
|
+
summary,
|
|
222
|
+
};
|
|
223
|
+
}
|
package/package.json
CHANGED
package/dist/ai-analysis.d.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
export interface AiFailureContext {
|
|
2
|
-
grade: string;
|
|
3
|
-
blockers: Array<{
|
|
4
|
-
title: string;
|
|
5
|
-
action: string;
|
|
6
|
-
}>;
|
|
7
|
-
lighthouseScores: {
|
|
8
|
-
performance: number;
|
|
9
|
-
accessibility: number;
|
|
10
|
-
seo: number;
|
|
11
|
-
bestPractices: number;
|
|
12
|
-
} | null;
|
|
13
|
-
thresholds: {
|
|
14
|
-
performance: number;
|
|
15
|
-
accessibility: number;
|
|
16
|
-
seo: number;
|
|
17
|
-
bestPractices: number;
|
|
18
|
-
};
|
|
19
|
-
buildErrors: string[];
|
|
20
|
-
e2eFailed: number;
|
|
21
|
-
e2eTotal: number;
|
|
22
|
-
securitySummary?: string;
|
|
23
|
-
}
|
|
24
|
-
export interface AiFailureAnalysis {
|
|
25
|
-
rootCause: string;
|
|
26
|
-
topFixes: string[];
|
|
27
|
-
}
|
|
28
|
-
export declare function requestAiFailureAnalysis(context: AiFailureContext): Promise<AiFailureAnalysis | null>;
|
package/dist/ai-analysis.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.requestAiFailureAnalysis = requestAiFailureAnalysis;
|
|
4
|
-
/**
|
|
5
|
-
* AI failure analysis client (Pro feature).
|
|
6
|
-
*
|
|
7
|
-
* Sends verification failure context to the Laxy API, which returns
|
|
8
|
-
* a root-cause summary and top fixes generated by Claude.
|
|
9
|
-
*/
|
|
10
|
-
const auth_js_1 = require("./auth.js");
|
|
11
|
-
async function requestAiFailureAnalysis(context) {
|
|
12
|
-
const token = (0, auth_js_1.loadToken)();
|
|
13
|
-
if (!token)
|
|
14
|
-
return null;
|
|
15
|
-
try {
|
|
16
|
-
const res = await fetch(`${auth_js_1.LAXY_API_URL}/api/v1/analyze-failure`, {
|
|
17
|
-
method: "POST",
|
|
18
|
-
headers: {
|
|
19
|
-
Authorization: `Bearer ${token}`,
|
|
20
|
-
"Content-Type": "application/json",
|
|
21
|
-
},
|
|
22
|
-
body: JSON.stringify(context),
|
|
23
|
-
signal: AbortSignal.timeout(30_000),
|
|
24
|
-
});
|
|
25
|
-
if (!res.ok)
|
|
26
|
-
return null;
|
|
27
|
-
return (await res.json());
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
}
|
package/dist/compare-env.d.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Multi-environment Lighthouse comparison (Pro feature).
|
|
3
|
-
*
|
|
4
|
-
* Compares Lighthouse scores between the local build and a reference
|
|
5
|
-
* environment URL (e.g. staging, production) to detect regressions
|
|
6
|
-
* introduced by the current PR.
|
|
7
|
-
*/
|
|
8
|
-
import type { LighthouseScores } from "./grade.js";
|
|
9
|
-
export interface EnvComparisonResult {
|
|
10
|
-
localUrl: string;
|
|
11
|
-
compareUrl: string;
|
|
12
|
-
local: LighthouseScores | null;
|
|
13
|
-
compare: LighthouseScores | null;
|
|
14
|
-
/** Positive = local is better, negative = compare env is better */
|
|
15
|
-
delta: {
|
|
16
|
-
performance: number | null;
|
|
17
|
-
accessibility: number | null;
|
|
18
|
-
seo: number | null;
|
|
19
|
-
bestPractices: number | null;
|
|
20
|
-
} | null;
|
|
21
|
-
}
|
|
22
|
-
export declare function runEnvComparison(localPort: number, compareUrl: string, runs?: number): Promise<EnvComparisonResult>;
|
|
23
|
-
export declare function printEnvComparison(result: EnvComparisonResult): void;
|
package/dist/compare-env.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.runEnvComparison = runEnvComparison;
|
|
4
|
-
exports.printEnvComparison = printEnvComparison;
|
|
5
|
-
const lighthouse_js_1 = require("./lighthouse.js");
|
|
6
|
-
function scoreDelta(local, compare) {
|
|
7
|
-
if (!local || !compare)
|
|
8
|
-
return null;
|
|
9
|
-
return {
|
|
10
|
-
performance: local.performance - compare.performance,
|
|
11
|
-
accessibility: local.accessibility - compare.accessibility,
|
|
12
|
-
seo: local.seo - compare.seo,
|
|
13
|
-
bestPractices: local.bestPractices - compare.bestPractices,
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
async function runEnvComparison(localPort, compareUrl, runs = 1) {
|
|
17
|
-
const localUrl = `http://127.0.0.1:${localPort}/`;
|
|
18
|
-
console.log(`\n [compare] Running env comparison: local vs ${compareUrl}`);
|
|
19
|
-
const [localResult, compareResult] = await Promise.all([
|
|
20
|
-
(0, lighthouse_js_1.runLighthouseOnUrl)(localUrl, runs),
|
|
21
|
-
(0, lighthouse_js_1.runLighthouseOnUrl)(compareUrl, runs),
|
|
22
|
-
]);
|
|
23
|
-
return {
|
|
24
|
-
localUrl,
|
|
25
|
-
compareUrl,
|
|
26
|
-
local: localResult.scores,
|
|
27
|
-
compare: compareResult.scores,
|
|
28
|
-
delta: scoreDelta(localResult.scores, compareResult.scores),
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
function printEnvComparison(result) {
|
|
32
|
-
console.log("\n Environment comparison:");
|
|
33
|
-
console.log(` Local: ${result.localUrl}`);
|
|
34
|
-
console.log(` Compare: ${result.compareUrl}`);
|
|
35
|
-
if (!result.local && !result.compare) {
|
|
36
|
-
console.log(" Both environments returned no Lighthouse data.");
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
const fmt = (v) => (v === null ? "—" : String(v));
|
|
40
|
-
const fmtDelta = (d) => {
|
|
41
|
-
if (d === null)
|
|
42
|
-
return "";
|
|
43
|
-
if (d > 0)
|
|
44
|
-
return ` (+${d})`;
|
|
45
|
-
if (d < 0)
|
|
46
|
-
return ` (${d})`;
|
|
47
|
-
return " (=)";
|
|
48
|
-
};
|
|
49
|
-
const d = result.delta;
|
|
50
|
-
console.log(" Metric Local Compare Delta");
|
|
51
|
-
console.log(` Performance ${fmt(result.local?.performance ?? null).padEnd(6)} ${fmt(result.compare?.performance ?? null).padEnd(8)}${fmtDelta(d?.performance ?? null)}`);
|
|
52
|
-
console.log(` Accessibility ${fmt(result.local?.accessibility ?? null).padEnd(6)} ${fmt(result.compare?.accessibility ?? null).padEnd(8)}${fmtDelta(d?.accessibility ?? null)}`);
|
|
53
|
-
console.log(` SEO ${fmt(result.local?.seo ?? null).padEnd(6)} ${fmt(result.compare?.seo ?? null).padEnd(8)}${fmtDelta(d?.seo ?? null)}`);
|
|
54
|
-
console.log(` Best Practices ${fmt(result.local?.bestPractices ?? null).padEnd(6)} ${fmt(result.compare?.bestPractices ?? null).padEnd(8)}${fmtDelta(d?.bestPractices ?? null)}`);
|
|
55
|
-
}
|