difflens 0.0.1
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/LICENSE +21 -0
- package/README.md +149 -0
- package/dist/ai-reporter-GPDXP36M.mjs +6 -0
- package/dist/api-server.d.mts +2 -0
- package/dist/api-server.d.ts +2 -0
- package/dist/api-server.js +89 -0
- package/dist/api-server.mjs +65 -0
- package/dist/chunk-4Q2LAHUO.mjs +51 -0
- package/dist/chunk-LVDSHTE4.mjs +433 -0
- package/dist/cli.d.mts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1173 -0
- package/dist/cli.mjs +661 -0
- package/dist/index.d.mts +61 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +537 -0
- package/dist/index.mjs +14 -0
- package/dist/mcp-server.d.mts +2 -0
- package/dist/mcp-server.d.ts +2 -0
- package/dist/mcp-server.js +88 -0
- package/dist/mcp-server.mjs +64 -0
- package/package.json +78 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
15
|
+
var __copyProps = (to, from, except, desc) => {
|
|
16
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
17
|
+
for (let key of __getOwnPropNames(from))
|
|
18
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
19
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
+
mod
|
|
30
|
+
));
|
|
31
|
+
|
|
32
|
+
// src/core/reporters/ai-reporter.ts
|
|
33
|
+
var ai_reporter_exports = {};
|
|
34
|
+
__export(ai_reporter_exports, {
|
|
35
|
+
formatAiReport: () => formatAiReport
|
|
36
|
+
});
|
|
37
|
+
function formatAiReport(results, outDir) {
|
|
38
|
+
let output = "DiffLens Test Report\n====================\n\n";
|
|
39
|
+
const failures = results.filter((r) => r.status === "fail" || r.a11yViolations.length > 0);
|
|
40
|
+
if (failures.length === 0) {
|
|
41
|
+
return output + "All tests passed. No visual regressions or accessibility violations found.\n";
|
|
42
|
+
}
|
|
43
|
+
output += `Found ${failures.length} issues:
|
|
44
|
+
|
|
45
|
+
`;
|
|
46
|
+
failures.forEach((result, index) => {
|
|
47
|
+
output += `Issue #${index + 1}: ${result.scenario}
|
|
48
|
+
`;
|
|
49
|
+
output += `----------------------------------------
|
|
50
|
+
`;
|
|
51
|
+
if (result.status === "fail") {
|
|
52
|
+
if (result.dimensionMismatch) {
|
|
53
|
+
output += `[Dimension Mismatch]
|
|
54
|
+
`;
|
|
55
|
+
output += ` Baseline and current images have different dimensions.
|
|
56
|
+
`;
|
|
57
|
+
} else if (result.diffPixels > 0) {
|
|
58
|
+
output += `[Visual Regression]
|
|
59
|
+
`;
|
|
60
|
+
output += ` Diff Pixels: ${result.diffPixels} (${result.diffPercentage.toFixed(2)}%)
|
|
61
|
+
`;
|
|
62
|
+
if (result.diffPath) {
|
|
63
|
+
output += ` Diff Image: ${import_path3.default.relative(process.cwd(), result.diffPath)}
|
|
64
|
+
`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (result.a11yViolations.length > 0) {
|
|
69
|
+
output += `[Accessibility Violations]
|
|
70
|
+
`;
|
|
71
|
+
result.a11yViolations.forEach((v) => {
|
|
72
|
+
output += ` - [${v.impact}] ${v.help} (${v.id})
|
|
73
|
+
`;
|
|
74
|
+
output += ` Target: ${v.nodes.map((n) => n.target.join(", ")).join(" | ")}
|
|
75
|
+
`;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
output += "\n";
|
|
79
|
+
});
|
|
80
|
+
return output;
|
|
81
|
+
}
|
|
82
|
+
var import_path3;
|
|
83
|
+
var init_ai_reporter = __esm({
|
|
84
|
+
"src/core/reporters/ai-reporter.ts"() {
|
|
85
|
+
"use strict";
|
|
86
|
+
import_path3 = __toESM(require("path"));
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// node_modules/cac/dist/index.mjs
|
|
91
|
+
var import_events = require("events");
|
|
92
|
+
function toArr(any) {
|
|
93
|
+
return any == null ? [] : Array.isArray(any) ? any : [any];
|
|
94
|
+
}
|
|
95
|
+
function toVal(out, key, val, opts) {
|
|
96
|
+
var x, old = out[key], nxt = !!~opts.string.indexOf(key) ? val == null || val === true ? "" : String(val) : typeof val === "boolean" ? val : !!~opts.boolean.indexOf(key) ? val === "false" ? false : val === "true" || (out._.push((x = +val, x * 0 === 0) ? x : val), !!val) : (x = +val, x * 0 === 0) ? x : val;
|
|
97
|
+
out[key] = old == null ? nxt : Array.isArray(old) ? old.concat(nxt) : [old, nxt];
|
|
98
|
+
}
|
|
99
|
+
function mri2(args, opts) {
|
|
100
|
+
args = args || [];
|
|
101
|
+
opts = opts || {};
|
|
102
|
+
var k, arr, arg, name, val, out = { _: [] };
|
|
103
|
+
var i = 0, j = 0, idx = 0, len = args.length;
|
|
104
|
+
const alibi = opts.alias !== void 0;
|
|
105
|
+
const strict = opts.unknown !== void 0;
|
|
106
|
+
const defaults = opts.default !== void 0;
|
|
107
|
+
opts.alias = opts.alias || {};
|
|
108
|
+
opts.string = toArr(opts.string);
|
|
109
|
+
opts.boolean = toArr(opts.boolean);
|
|
110
|
+
if (alibi) {
|
|
111
|
+
for (k in opts.alias) {
|
|
112
|
+
arr = opts.alias[k] = toArr(opts.alias[k]);
|
|
113
|
+
for (i = 0; i < arr.length; i++) {
|
|
114
|
+
(opts.alias[arr[i]] = arr.concat(k)).splice(i, 1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (i = opts.boolean.length; i-- > 0; ) {
|
|
119
|
+
arr = opts.alias[opts.boolean[i]] || [];
|
|
120
|
+
for (j = arr.length; j-- > 0; ) opts.boolean.push(arr[j]);
|
|
121
|
+
}
|
|
122
|
+
for (i = opts.string.length; i-- > 0; ) {
|
|
123
|
+
arr = opts.alias[opts.string[i]] || [];
|
|
124
|
+
for (j = arr.length; j-- > 0; ) opts.string.push(arr[j]);
|
|
125
|
+
}
|
|
126
|
+
if (defaults) {
|
|
127
|
+
for (k in opts.default) {
|
|
128
|
+
name = typeof opts.default[k];
|
|
129
|
+
arr = opts.alias[k] = opts.alias[k] || [];
|
|
130
|
+
if (opts[name] !== void 0) {
|
|
131
|
+
opts[name].push(k);
|
|
132
|
+
for (i = 0; i < arr.length; i++) {
|
|
133
|
+
opts[name].push(arr[i]);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const keys = strict ? Object.keys(opts.alias) : [];
|
|
139
|
+
for (i = 0; i < len; i++) {
|
|
140
|
+
arg = args[i];
|
|
141
|
+
if (arg === "--") {
|
|
142
|
+
out._ = out._.concat(args.slice(++i));
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
for (j = 0; j < arg.length; j++) {
|
|
146
|
+
if (arg.charCodeAt(j) !== 45) break;
|
|
147
|
+
}
|
|
148
|
+
if (j === 0) {
|
|
149
|
+
out._.push(arg);
|
|
150
|
+
} else if (arg.substring(j, j + 3) === "no-") {
|
|
151
|
+
name = arg.substring(j + 3);
|
|
152
|
+
if (strict && !~keys.indexOf(name)) {
|
|
153
|
+
return opts.unknown(arg);
|
|
154
|
+
}
|
|
155
|
+
out[name] = false;
|
|
156
|
+
} else {
|
|
157
|
+
for (idx = j + 1; idx < arg.length; idx++) {
|
|
158
|
+
if (arg.charCodeAt(idx) === 61) break;
|
|
159
|
+
}
|
|
160
|
+
name = arg.substring(j, idx);
|
|
161
|
+
val = arg.substring(++idx) || (i + 1 === len || ("" + args[i + 1]).charCodeAt(0) === 45 || args[++i]);
|
|
162
|
+
arr = j === 2 ? [name] : name;
|
|
163
|
+
for (idx = 0; idx < arr.length; idx++) {
|
|
164
|
+
name = arr[idx];
|
|
165
|
+
if (strict && !~keys.indexOf(name)) return opts.unknown("-".repeat(j) + name);
|
|
166
|
+
toVal(out, name, idx + 1 < arr.length || val, opts);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (defaults) {
|
|
171
|
+
for (k in opts.default) {
|
|
172
|
+
if (out[k] === void 0) {
|
|
173
|
+
out[k] = opts.default[k];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (alibi) {
|
|
178
|
+
for (k in out) {
|
|
179
|
+
arr = opts.alias[k] || [];
|
|
180
|
+
while (arr.length > 0) {
|
|
181
|
+
out[arr.shift()] = out[k];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
var removeBrackets = (v) => v.replace(/[<[].+/, "").trim();
|
|
188
|
+
var findAllBrackets = (v) => {
|
|
189
|
+
const ANGLED_BRACKET_RE_GLOBAL = /<([^>]+)>/g;
|
|
190
|
+
const SQUARE_BRACKET_RE_GLOBAL = /\[([^\]]+)\]/g;
|
|
191
|
+
const res = [];
|
|
192
|
+
const parse = (match) => {
|
|
193
|
+
let variadic = false;
|
|
194
|
+
let value = match[1];
|
|
195
|
+
if (value.startsWith("...")) {
|
|
196
|
+
value = value.slice(3);
|
|
197
|
+
variadic = true;
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
required: match[0].startsWith("<"),
|
|
201
|
+
value,
|
|
202
|
+
variadic
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
let angledMatch;
|
|
206
|
+
while (angledMatch = ANGLED_BRACKET_RE_GLOBAL.exec(v)) {
|
|
207
|
+
res.push(parse(angledMatch));
|
|
208
|
+
}
|
|
209
|
+
let squareMatch;
|
|
210
|
+
while (squareMatch = SQUARE_BRACKET_RE_GLOBAL.exec(v)) {
|
|
211
|
+
res.push(parse(squareMatch));
|
|
212
|
+
}
|
|
213
|
+
return res;
|
|
214
|
+
};
|
|
215
|
+
var getMriOptions = (options) => {
|
|
216
|
+
const result = { alias: {}, boolean: [] };
|
|
217
|
+
for (const [index, option] of options.entries()) {
|
|
218
|
+
if (option.names.length > 1) {
|
|
219
|
+
result.alias[option.names[0]] = option.names.slice(1);
|
|
220
|
+
}
|
|
221
|
+
if (option.isBoolean) {
|
|
222
|
+
if (option.negated) {
|
|
223
|
+
const hasStringTypeOption = options.some((o, i) => {
|
|
224
|
+
return i !== index && o.names.some((name) => option.names.includes(name)) && typeof o.required === "boolean";
|
|
225
|
+
});
|
|
226
|
+
if (!hasStringTypeOption) {
|
|
227
|
+
result.boolean.push(option.names[0]);
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
result.boolean.push(option.names[0]);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return result;
|
|
235
|
+
};
|
|
236
|
+
var findLongest = (arr) => {
|
|
237
|
+
return arr.sort((a, b) => {
|
|
238
|
+
return a.length > b.length ? -1 : 1;
|
|
239
|
+
})[0];
|
|
240
|
+
};
|
|
241
|
+
var padRight = (str, length) => {
|
|
242
|
+
return str.length >= length ? str : `${str}${" ".repeat(length - str.length)}`;
|
|
243
|
+
};
|
|
244
|
+
var camelcase = (input) => {
|
|
245
|
+
return input.replace(/([a-z])-([a-z])/g, (_, p1, p2) => {
|
|
246
|
+
return p1 + p2.toUpperCase();
|
|
247
|
+
});
|
|
248
|
+
};
|
|
249
|
+
var setDotProp = (obj, keys, val) => {
|
|
250
|
+
let i = 0;
|
|
251
|
+
let length = keys.length;
|
|
252
|
+
let t = obj;
|
|
253
|
+
let x;
|
|
254
|
+
for (; i < length; ++i) {
|
|
255
|
+
x = t[keys[i]];
|
|
256
|
+
t = t[keys[i]] = i === length - 1 ? val : x != null ? x : !!~keys[i + 1].indexOf(".") || !(+keys[i + 1] > -1) ? {} : [];
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
var setByType = (obj, transforms) => {
|
|
260
|
+
for (const key of Object.keys(transforms)) {
|
|
261
|
+
const transform = transforms[key];
|
|
262
|
+
if (transform.shouldTransform) {
|
|
263
|
+
obj[key] = Array.prototype.concat.call([], obj[key]);
|
|
264
|
+
if (typeof transform.transformFunction === "function") {
|
|
265
|
+
obj[key] = obj[key].map(transform.transformFunction);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
var getFileName = (input) => {
|
|
271
|
+
const m = /([^\\\/]+)$/.exec(input);
|
|
272
|
+
return m ? m[1] : "";
|
|
273
|
+
};
|
|
274
|
+
var camelcaseOptionName = (name) => {
|
|
275
|
+
return name.split(".").map((v, i) => {
|
|
276
|
+
return i === 0 ? camelcase(v) : v;
|
|
277
|
+
}).join(".");
|
|
278
|
+
};
|
|
279
|
+
var CACError = class extends Error {
|
|
280
|
+
constructor(message) {
|
|
281
|
+
super(message);
|
|
282
|
+
this.name = this.constructor.name;
|
|
283
|
+
if (typeof Error.captureStackTrace === "function") {
|
|
284
|
+
Error.captureStackTrace(this, this.constructor);
|
|
285
|
+
} else {
|
|
286
|
+
this.stack = new Error(message).stack;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
var Option = class {
|
|
291
|
+
constructor(rawName, description, config) {
|
|
292
|
+
this.rawName = rawName;
|
|
293
|
+
this.description = description;
|
|
294
|
+
this.config = Object.assign({}, config);
|
|
295
|
+
rawName = rawName.replace(/\.\*/g, "");
|
|
296
|
+
this.negated = false;
|
|
297
|
+
this.names = removeBrackets(rawName).split(",").map((v) => {
|
|
298
|
+
let name = v.trim().replace(/^-{1,2}/, "");
|
|
299
|
+
if (name.startsWith("no-")) {
|
|
300
|
+
this.negated = true;
|
|
301
|
+
name = name.replace(/^no-/, "");
|
|
302
|
+
}
|
|
303
|
+
return camelcaseOptionName(name);
|
|
304
|
+
}).sort((a, b) => a.length > b.length ? 1 : -1);
|
|
305
|
+
this.name = this.names[this.names.length - 1];
|
|
306
|
+
if (this.negated && this.config.default == null) {
|
|
307
|
+
this.config.default = true;
|
|
308
|
+
}
|
|
309
|
+
if (rawName.includes("<")) {
|
|
310
|
+
this.required = true;
|
|
311
|
+
} else if (rawName.includes("[")) {
|
|
312
|
+
this.required = false;
|
|
313
|
+
} else {
|
|
314
|
+
this.isBoolean = true;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
var processArgs = process.argv;
|
|
319
|
+
var platformInfo = `${process.platform}-${process.arch} node-${process.version}`;
|
|
320
|
+
var Command = class {
|
|
321
|
+
constructor(rawName, description, config = {}, cli2) {
|
|
322
|
+
this.rawName = rawName;
|
|
323
|
+
this.description = description;
|
|
324
|
+
this.config = config;
|
|
325
|
+
this.cli = cli2;
|
|
326
|
+
this.options = [];
|
|
327
|
+
this.aliasNames = [];
|
|
328
|
+
this.name = removeBrackets(rawName);
|
|
329
|
+
this.args = findAllBrackets(rawName);
|
|
330
|
+
this.examples = [];
|
|
331
|
+
}
|
|
332
|
+
usage(text) {
|
|
333
|
+
this.usageText = text;
|
|
334
|
+
return this;
|
|
335
|
+
}
|
|
336
|
+
allowUnknownOptions() {
|
|
337
|
+
this.config.allowUnknownOptions = true;
|
|
338
|
+
return this;
|
|
339
|
+
}
|
|
340
|
+
ignoreOptionDefaultValue() {
|
|
341
|
+
this.config.ignoreOptionDefaultValue = true;
|
|
342
|
+
return this;
|
|
343
|
+
}
|
|
344
|
+
version(version2, customFlags = "-v, --version") {
|
|
345
|
+
this.versionNumber = version2;
|
|
346
|
+
this.option(customFlags, "Display version number");
|
|
347
|
+
return this;
|
|
348
|
+
}
|
|
349
|
+
example(example) {
|
|
350
|
+
this.examples.push(example);
|
|
351
|
+
return this;
|
|
352
|
+
}
|
|
353
|
+
option(rawName, description, config) {
|
|
354
|
+
const option = new Option(rawName, description, config);
|
|
355
|
+
this.options.push(option);
|
|
356
|
+
return this;
|
|
357
|
+
}
|
|
358
|
+
alias(name) {
|
|
359
|
+
this.aliasNames.push(name);
|
|
360
|
+
return this;
|
|
361
|
+
}
|
|
362
|
+
action(callback) {
|
|
363
|
+
this.commandAction = callback;
|
|
364
|
+
return this;
|
|
365
|
+
}
|
|
366
|
+
isMatched(name) {
|
|
367
|
+
return this.name === name || this.aliasNames.includes(name);
|
|
368
|
+
}
|
|
369
|
+
get isDefaultCommand() {
|
|
370
|
+
return this.name === "" || this.aliasNames.includes("!");
|
|
371
|
+
}
|
|
372
|
+
get isGlobalCommand() {
|
|
373
|
+
return this instanceof GlobalCommand;
|
|
374
|
+
}
|
|
375
|
+
hasOption(name) {
|
|
376
|
+
name = name.split(".")[0];
|
|
377
|
+
return this.options.find((option) => {
|
|
378
|
+
return option.names.includes(name);
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
outputHelp() {
|
|
382
|
+
const { name, commands } = this.cli;
|
|
383
|
+
const {
|
|
384
|
+
versionNumber,
|
|
385
|
+
options: globalOptions,
|
|
386
|
+
helpCallback
|
|
387
|
+
} = this.cli.globalCommand;
|
|
388
|
+
let sections = [
|
|
389
|
+
{
|
|
390
|
+
body: `${name}${versionNumber ? `/${versionNumber}` : ""}`
|
|
391
|
+
}
|
|
392
|
+
];
|
|
393
|
+
sections.push({
|
|
394
|
+
title: "Usage",
|
|
395
|
+
body: ` $ ${name} ${this.usageText || this.rawName}`
|
|
396
|
+
});
|
|
397
|
+
const showCommands = (this.isGlobalCommand || this.isDefaultCommand) && commands.length > 0;
|
|
398
|
+
if (showCommands) {
|
|
399
|
+
const longestCommandName = findLongest(commands.map((command) => command.rawName));
|
|
400
|
+
sections.push({
|
|
401
|
+
title: "Commands",
|
|
402
|
+
body: commands.map((command) => {
|
|
403
|
+
return ` ${padRight(command.rawName, longestCommandName.length)} ${command.description}`;
|
|
404
|
+
}).join("\n")
|
|
405
|
+
});
|
|
406
|
+
sections.push({
|
|
407
|
+
title: `For more info, run any command with the \`--help\` flag`,
|
|
408
|
+
body: commands.map((command) => ` $ ${name}${command.name === "" ? "" : ` ${command.name}`} --help`).join("\n")
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
let options = this.isGlobalCommand ? globalOptions : [...this.options, ...globalOptions || []];
|
|
412
|
+
if (!this.isGlobalCommand && !this.isDefaultCommand) {
|
|
413
|
+
options = options.filter((option) => option.name !== "version");
|
|
414
|
+
}
|
|
415
|
+
if (options.length > 0) {
|
|
416
|
+
const longestOptionName = findLongest(options.map((option) => option.rawName));
|
|
417
|
+
sections.push({
|
|
418
|
+
title: "Options",
|
|
419
|
+
body: options.map((option) => {
|
|
420
|
+
return ` ${padRight(option.rawName, longestOptionName.length)} ${option.description} ${option.config.default === void 0 ? "" : `(default: ${option.config.default})`}`;
|
|
421
|
+
}).join("\n")
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
if (this.examples.length > 0) {
|
|
425
|
+
sections.push({
|
|
426
|
+
title: "Examples",
|
|
427
|
+
body: this.examples.map((example) => {
|
|
428
|
+
if (typeof example === "function") {
|
|
429
|
+
return example(name);
|
|
430
|
+
}
|
|
431
|
+
return example;
|
|
432
|
+
}).join("\n")
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
if (helpCallback) {
|
|
436
|
+
sections = helpCallback(sections) || sections;
|
|
437
|
+
}
|
|
438
|
+
console.log(sections.map((section) => {
|
|
439
|
+
return section.title ? `${section.title}:
|
|
440
|
+
${section.body}` : section.body;
|
|
441
|
+
}).join("\n\n"));
|
|
442
|
+
}
|
|
443
|
+
outputVersion() {
|
|
444
|
+
const { name } = this.cli;
|
|
445
|
+
const { versionNumber } = this.cli.globalCommand;
|
|
446
|
+
if (versionNumber) {
|
|
447
|
+
console.log(`${name}/${versionNumber} ${platformInfo}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
checkRequiredArgs() {
|
|
451
|
+
const minimalArgsCount = this.args.filter((arg) => arg.required).length;
|
|
452
|
+
if (this.cli.args.length < minimalArgsCount) {
|
|
453
|
+
throw new CACError(`missing required args for command \`${this.rawName}\``);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
checkUnknownOptions() {
|
|
457
|
+
const { options, globalCommand } = this.cli;
|
|
458
|
+
if (!this.config.allowUnknownOptions) {
|
|
459
|
+
for (const name of Object.keys(options)) {
|
|
460
|
+
if (name !== "--" && !this.hasOption(name) && !globalCommand.hasOption(name)) {
|
|
461
|
+
throw new CACError(`Unknown option \`${name.length > 1 ? `--${name}` : `-${name}`}\``);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
checkOptionValue() {
|
|
467
|
+
const { options: parsedOptions, globalCommand } = this.cli;
|
|
468
|
+
const options = [...globalCommand.options, ...this.options];
|
|
469
|
+
for (const option of options) {
|
|
470
|
+
const value = parsedOptions[option.name.split(".")[0]];
|
|
471
|
+
if (option.required) {
|
|
472
|
+
const hasNegated = options.some((o) => o.negated && o.names.includes(option.name));
|
|
473
|
+
if (value === true || value === false && !hasNegated) {
|
|
474
|
+
throw new CACError(`option \`${option.rawName}\` value is missing`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
var GlobalCommand = class extends Command {
|
|
481
|
+
constructor(cli2) {
|
|
482
|
+
super("@@global@@", "", {}, cli2);
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
var __assign = Object.assign;
|
|
486
|
+
var CAC = class extends import_events.EventEmitter {
|
|
487
|
+
constructor(name = "") {
|
|
488
|
+
super();
|
|
489
|
+
this.name = name;
|
|
490
|
+
this.commands = [];
|
|
491
|
+
this.rawArgs = [];
|
|
492
|
+
this.args = [];
|
|
493
|
+
this.options = {};
|
|
494
|
+
this.globalCommand = new GlobalCommand(this);
|
|
495
|
+
this.globalCommand.usage("<command> [options]");
|
|
496
|
+
}
|
|
497
|
+
usage(text) {
|
|
498
|
+
this.globalCommand.usage(text);
|
|
499
|
+
return this;
|
|
500
|
+
}
|
|
501
|
+
command(rawName, description, config) {
|
|
502
|
+
const command = new Command(rawName, description || "", config, this);
|
|
503
|
+
command.globalCommand = this.globalCommand;
|
|
504
|
+
this.commands.push(command);
|
|
505
|
+
return command;
|
|
506
|
+
}
|
|
507
|
+
option(rawName, description, config) {
|
|
508
|
+
this.globalCommand.option(rawName, description, config);
|
|
509
|
+
return this;
|
|
510
|
+
}
|
|
511
|
+
help(callback) {
|
|
512
|
+
this.globalCommand.option("-h, --help", "Display this message");
|
|
513
|
+
this.globalCommand.helpCallback = callback;
|
|
514
|
+
this.showHelpOnExit = true;
|
|
515
|
+
return this;
|
|
516
|
+
}
|
|
517
|
+
version(version2, customFlags = "-v, --version") {
|
|
518
|
+
this.globalCommand.version(version2, customFlags);
|
|
519
|
+
this.showVersionOnExit = true;
|
|
520
|
+
return this;
|
|
521
|
+
}
|
|
522
|
+
example(example) {
|
|
523
|
+
this.globalCommand.example(example);
|
|
524
|
+
return this;
|
|
525
|
+
}
|
|
526
|
+
outputHelp() {
|
|
527
|
+
if (this.matchedCommand) {
|
|
528
|
+
this.matchedCommand.outputHelp();
|
|
529
|
+
} else {
|
|
530
|
+
this.globalCommand.outputHelp();
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
outputVersion() {
|
|
534
|
+
this.globalCommand.outputVersion();
|
|
535
|
+
}
|
|
536
|
+
setParsedInfo({ args, options }, matchedCommand, matchedCommandName) {
|
|
537
|
+
this.args = args;
|
|
538
|
+
this.options = options;
|
|
539
|
+
if (matchedCommand) {
|
|
540
|
+
this.matchedCommand = matchedCommand;
|
|
541
|
+
}
|
|
542
|
+
if (matchedCommandName) {
|
|
543
|
+
this.matchedCommandName = matchedCommandName;
|
|
544
|
+
}
|
|
545
|
+
return this;
|
|
546
|
+
}
|
|
547
|
+
unsetMatchedCommand() {
|
|
548
|
+
this.matchedCommand = void 0;
|
|
549
|
+
this.matchedCommandName = void 0;
|
|
550
|
+
}
|
|
551
|
+
parse(argv = processArgs, {
|
|
552
|
+
run = true
|
|
553
|
+
} = {}) {
|
|
554
|
+
this.rawArgs = argv;
|
|
555
|
+
if (!this.name) {
|
|
556
|
+
this.name = argv[1] ? getFileName(argv[1]) : "cli";
|
|
557
|
+
}
|
|
558
|
+
let shouldParse = true;
|
|
559
|
+
for (const command of this.commands) {
|
|
560
|
+
const parsed = this.mri(argv.slice(2), command);
|
|
561
|
+
const commandName = parsed.args[0];
|
|
562
|
+
if (command.isMatched(commandName)) {
|
|
563
|
+
shouldParse = false;
|
|
564
|
+
const parsedInfo = __assign(__assign({}, parsed), {
|
|
565
|
+
args: parsed.args.slice(1)
|
|
566
|
+
});
|
|
567
|
+
this.setParsedInfo(parsedInfo, command, commandName);
|
|
568
|
+
this.emit(`command:${commandName}`, command);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (shouldParse) {
|
|
572
|
+
for (const command of this.commands) {
|
|
573
|
+
if (command.name === "") {
|
|
574
|
+
shouldParse = false;
|
|
575
|
+
const parsed = this.mri(argv.slice(2), command);
|
|
576
|
+
this.setParsedInfo(parsed, command);
|
|
577
|
+
this.emit(`command:!`, command);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (shouldParse) {
|
|
582
|
+
const parsed = this.mri(argv.slice(2));
|
|
583
|
+
this.setParsedInfo(parsed);
|
|
584
|
+
}
|
|
585
|
+
if (this.options.help && this.showHelpOnExit) {
|
|
586
|
+
this.outputHelp();
|
|
587
|
+
run = false;
|
|
588
|
+
this.unsetMatchedCommand();
|
|
589
|
+
}
|
|
590
|
+
if (this.options.version && this.showVersionOnExit && this.matchedCommandName == null) {
|
|
591
|
+
this.outputVersion();
|
|
592
|
+
run = false;
|
|
593
|
+
this.unsetMatchedCommand();
|
|
594
|
+
}
|
|
595
|
+
const parsedArgv = { args: this.args, options: this.options };
|
|
596
|
+
if (run) {
|
|
597
|
+
this.runMatchedCommand();
|
|
598
|
+
}
|
|
599
|
+
if (!this.matchedCommand && this.args[0]) {
|
|
600
|
+
this.emit("command:*");
|
|
601
|
+
}
|
|
602
|
+
return parsedArgv;
|
|
603
|
+
}
|
|
604
|
+
mri(argv, command) {
|
|
605
|
+
const cliOptions = [
|
|
606
|
+
...this.globalCommand.options,
|
|
607
|
+
...command ? command.options : []
|
|
608
|
+
];
|
|
609
|
+
const mriOptions = getMriOptions(cliOptions);
|
|
610
|
+
let argsAfterDoubleDashes = [];
|
|
611
|
+
const doubleDashesIndex = argv.indexOf("--");
|
|
612
|
+
if (doubleDashesIndex > -1) {
|
|
613
|
+
argsAfterDoubleDashes = argv.slice(doubleDashesIndex + 1);
|
|
614
|
+
argv = argv.slice(0, doubleDashesIndex);
|
|
615
|
+
}
|
|
616
|
+
let parsed = mri2(argv, mriOptions);
|
|
617
|
+
parsed = Object.keys(parsed).reduce((res, name) => {
|
|
618
|
+
return __assign(__assign({}, res), {
|
|
619
|
+
[camelcaseOptionName(name)]: parsed[name]
|
|
620
|
+
});
|
|
621
|
+
}, { _: [] });
|
|
622
|
+
const args = parsed._;
|
|
623
|
+
const options = {
|
|
624
|
+
"--": argsAfterDoubleDashes
|
|
625
|
+
};
|
|
626
|
+
const ignoreDefault = command && command.config.ignoreOptionDefaultValue ? command.config.ignoreOptionDefaultValue : this.globalCommand.config.ignoreOptionDefaultValue;
|
|
627
|
+
let transforms = /* @__PURE__ */ Object.create(null);
|
|
628
|
+
for (const cliOption of cliOptions) {
|
|
629
|
+
if (!ignoreDefault && cliOption.config.default !== void 0) {
|
|
630
|
+
for (const name of cliOption.names) {
|
|
631
|
+
options[name] = cliOption.config.default;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (Array.isArray(cliOption.config.type)) {
|
|
635
|
+
if (transforms[cliOption.name] === void 0) {
|
|
636
|
+
transforms[cliOption.name] = /* @__PURE__ */ Object.create(null);
|
|
637
|
+
transforms[cliOption.name]["shouldTransform"] = true;
|
|
638
|
+
transforms[cliOption.name]["transformFunction"] = cliOption.config.type[0];
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
for (const key of Object.keys(parsed)) {
|
|
643
|
+
if (key !== "_") {
|
|
644
|
+
const keys = key.split(".");
|
|
645
|
+
setDotProp(options, keys, parsed[key]);
|
|
646
|
+
setByType(options, transforms);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
args,
|
|
651
|
+
options
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
runMatchedCommand() {
|
|
655
|
+
const { args, options, matchedCommand: command } = this;
|
|
656
|
+
if (!command || !command.commandAction)
|
|
657
|
+
return;
|
|
658
|
+
command.checkUnknownOptions();
|
|
659
|
+
command.checkOptionValue();
|
|
660
|
+
command.checkRequiredArgs();
|
|
661
|
+
const actionArgs = [];
|
|
662
|
+
command.args.forEach((arg, index) => {
|
|
663
|
+
if (arg.variadic) {
|
|
664
|
+
actionArgs.push(args.slice(index));
|
|
665
|
+
} else {
|
|
666
|
+
actionArgs.push(args[index]);
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
actionArgs.push(options);
|
|
670
|
+
return command.commandAction.apply(this, actionArgs);
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
var cac = (name = "") => new CAC(name);
|
|
674
|
+
var dist_default = cac;
|
|
675
|
+
|
|
676
|
+
// src/cli.ts
|
|
677
|
+
var import_fs5 = __toESM(require("fs"));
|
|
678
|
+
var import_path5 = __toESM(require("path"));
|
|
679
|
+
|
|
680
|
+
// package.json
|
|
681
|
+
var version = "0.0.1";
|
|
682
|
+
|
|
683
|
+
// src/config.ts
|
|
684
|
+
var import_path = __toESM(require("path"));
|
|
685
|
+
var import_fs = __toESM(require("fs"));
|
|
686
|
+
var import_esbuild = require("esbuild");
|
|
687
|
+
var DEFAULT_CONFIG = {
|
|
688
|
+
scenarios: [],
|
|
689
|
+
threshold: 0.1,
|
|
690
|
+
outDir: ".difflens"
|
|
691
|
+
};
|
|
692
|
+
async function loadConfig(configPath) {
|
|
693
|
+
const searchPlaces = [
|
|
694
|
+
"difflens.config.ts",
|
|
695
|
+
"difflens.config.js",
|
|
696
|
+
"difflens.config.mjs",
|
|
697
|
+
"difflens.config.cjs"
|
|
698
|
+
];
|
|
699
|
+
let resolvedPath;
|
|
700
|
+
if (configPath) {
|
|
701
|
+
resolvedPath = import_path.default.resolve(process.cwd(), configPath);
|
|
702
|
+
} else {
|
|
703
|
+
for (const place of searchPlaces) {
|
|
704
|
+
const p = import_path.default.resolve(process.cwd(), place);
|
|
705
|
+
if (import_fs.default.existsSync(p)) {
|
|
706
|
+
resolvedPath = p;
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if (!resolvedPath) {
|
|
712
|
+
console.warn("No configuration file found. Using default config.");
|
|
713
|
+
return DEFAULT_CONFIG;
|
|
714
|
+
}
|
|
715
|
+
try {
|
|
716
|
+
let importPath = resolvedPath;
|
|
717
|
+
let isTs = resolvedPath.endsWith(".ts");
|
|
718
|
+
let tempFile = null;
|
|
719
|
+
if (isTs) {
|
|
720
|
+
const outfile = resolvedPath.replace(/\.ts$/, ".js.tmp.mjs");
|
|
721
|
+
await (0, import_esbuild.build)({
|
|
722
|
+
entryPoints: [resolvedPath],
|
|
723
|
+
outfile,
|
|
724
|
+
bundle: true,
|
|
725
|
+
platform: "node",
|
|
726
|
+
format: "esm",
|
|
727
|
+
external: ["difflens"]
|
|
728
|
+
// Exclude self if referenced?
|
|
729
|
+
});
|
|
730
|
+
importPath = outfile;
|
|
731
|
+
tempFile = outfile;
|
|
732
|
+
}
|
|
733
|
+
const importUrl = `file://${importPath}`;
|
|
734
|
+
const userConfigModule = await import(importUrl);
|
|
735
|
+
const userConfig = userConfigModule.default || userConfigModule;
|
|
736
|
+
if (tempFile && import_fs.default.existsSync(tempFile)) {
|
|
737
|
+
import_fs.default.unlinkSync(tempFile);
|
|
738
|
+
}
|
|
739
|
+
return {
|
|
740
|
+
...DEFAULT_CONFIG,
|
|
741
|
+
...userConfig
|
|
742
|
+
};
|
|
743
|
+
} catch (error) {
|
|
744
|
+
console.error(`Failed to load configuration from ${resolvedPath}:`, error);
|
|
745
|
+
throw error;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// src/core/runner.ts
|
|
750
|
+
var import_playwright2 = require("playwright");
|
|
751
|
+
var import_path4 = __toESM(require("path"));
|
|
752
|
+
var import_fs4 = __toESM(require("fs"));
|
|
753
|
+
|
|
754
|
+
// src/core/capture.ts
|
|
755
|
+
async function captureScreenshot(page, url, outputPath, options = {}) {
|
|
756
|
+
if (url) {
|
|
757
|
+
await page.goto(url, { waitUntil: "networkidle" });
|
|
758
|
+
}
|
|
759
|
+
await page.evaluate(() => document.fonts.ready);
|
|
760
|
+
await page.addStyleTag({
|
|
761
|
+
content: `
|
|
762
|
+
*, *::before, *::after {
|
|
763
|
+
animation-duration: 0s !important;
|
|
764
|
+
transition-duration: 0s !important;
|
|
765
|
+
animation-iteration-count: 1 !important;
|
|
766
|
+
}
|
|
767
|
+
`
|
|
768
|
+
});
|
|
769
|
+
if (options.hideSelectors) {
|
|
770
|
+
for (const selector of options.hideSelectors) {
|
|
771
|
+
await page.locator(selector).evaluate((el) => el.style.visibility = "hidden");
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (options.maskSelectors) {
|
|
775
|
+
for (const selector of options.maskSelectors) {
|
|
776
|
+
await page.locator(selector).evaluate((el) => el.style.backgroundColor = "#FF00FF");
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
await page.screenshot({
|
|
780
|
+
path: outputPath,
|
|
781
|
+
fullPage: options.fullPage ?? true,
|
|
782
|
+
animations: "disabled"
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/core/compare.ts
|
|
787
|
+
var import_fs2 = __toESM(require("fs"));
|
|
788
|
+
var import_pngjs = require("pngjs");
|
|
789
|
+
var import_pixelmatch = __toESM(require("pixelmatch"));
|
|
790
|
+
function compareImages(img1Path, img2Path, diffPath, threshold = 0.1) {
|
|
791
|
+
const img1 = import_pngjs.PNG.sync.read(import_fs2.default.readFileSync(img1Path));
|
|
792
|
+
const img2 = import_pngjs.PNG.sync.read(import_fs2.default.readFileSync(img2Path));
|
|
793
|
+
const { width, height } = img1;
|
|
794
|
+
const diff = new import_pngjs.PNG({ width, height });
|
|
795
|
+
if (img1.width !== img2.width || img1.height !== img2.height) {
|
|
796
|
+
return {
|
|
797
|
+
diffPixels: -1,
|
|
798
|
+
diffPercentage: -1,
|
|
799
|
+
isSameDimensions: false
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
const diffPixels = (0, import_pixelmatch.default)(
|
|
803
|
+
img1.data,
|
|
804
|
+
img2.data,
|
|
805
|
+
diff.data,
|
|
806
|
+
width,
|
|
807
|
+
height,
|
|
808
|
+
{ threshold }
|
|
809
|
+
);
|
|
810
|
+
import_fs2.default.writeFileSync(diffPath, import_pngjs.PNG.sync.write(diff));
|
|
811
|
+
const totalPixels = width * height;
|
|
812
|
+
const diffPercentage = diffPixels / totalPixels * 100;
|
|
813
|
+
return {
|
|
814
|
+
diffPixels,
|
|
815
|
+
diffPercentage,
|
|
816
|
+
isSameDimensions: true
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// src/core/a11y.ts
|
|
821
|
+
var import_playwright = __toESM(require("@axe-core/playwright"));
|
|
822
|
+
async function runAxeAudit(page) {
|
|
823
|
+
const results = await new import_playwright.default({ page }).analyze();
|
|
824
|
+
if (results.violations.length > 0) {
|
|
825
|
+
console.error(`Found ${results.violations.length} accessibility violations:`);
|
|
826
|
+
results.violations.forEach((violation) => {
|
|
827
|
+
console.error(`- [${violation.impact}] ${violation.help} (${violation.id})`);
|
|
828
|
+
violation.nodes.forEach((node) => {
|
|
829
|
+
console.error(` Target: ${node.target}`);
|
|
830
|
+
});
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
return {
|
|
834
|
+
violations: results.violations
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// src/core/report.ts
|
|
839
|
+
var import_fs3 = __toESM(require("fs"));
|
|
840
|
+
var import_path2 = __toESM(require("path"));
|
|
841
|
+
function generateHtmlReport(data, outDir) {
|
|
842
|
+
const html = `
|
|
843
|
+
<!DOCTYPE html>
|
|
844
|
+
<html lang="en">
|
|
845
|
+
<head>
|
|
846
|
+
<meta charset="UTF-8">
|
|
847
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
848
|
+
<title>DiffLens Report</title>
|
|
849
|
+
<style>
|
|
850
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
|
|
851
|
+
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
852
|
+
h1 { color: #333; border-bottom: 2px solid #eee; padding-bottom: 10px; }
|
|
853
|
+
.summary { margin-bottom: 20px; padding: 15px; background: #eef; border-radius: 4px; }
|
|
854
|
+
.scenario { border: 1px solid #ddd; margin-bottom: 20px; border-radius: 4px; overflow: hidden; }
|
|
855
|
+
.scenario-header { padding: 10px 15px; background: #f9f9f9; border-bottom: 1px solid #ddd; display: flex; justify-content: space-between; align-items: center; }
|
|
856
|
+
.scenario-title { font-weight: bold; font-size: 1.1em; }
|
|
857
|
+
.status { padding: 4px 8px; border-radius: 4px; font-size: 0.9em; font-weight: bold; color: white; }
|
|
858
|
+
.status.pass { background: #28a745; }
|
|
859
|
+
.status.fail { background: #dc3545; }
|
|
860
|
+
.status.new { background: #17a2b8; }
|
|
861
|
+
.scenario-body { padding: 15px; }
|
|
862
|
+
.images { display: flex; gap: 20px; margin-bottom: 15px; overflow-x: auto; }
|
|
863
|
+
.image-container { flex: 0 0 auto; }
|
|
864
|
+
.image-container img { max-width: 300px; border: 1px solid #ddd; border-radius: 4px; }
|
|
865
|
+
.image-label { font-size: 0.9em; color: #666; margin-bottom: 5px; }
|
|
866
|
+
.a11y { margin-top: 15px; border-top: 1px solid #eee; padding-top: 10px; }
|
|
867
|
+
.a11y-title { font-weight: bold; color: #666; margin-bottom: 5px; }
|
|
868
|
+
.violation { margin-bottom: 5px; color: #d63384; }
|
|
869
|
+
</style>
|
|
870
|
+
</head>
|
|
871
|
+
<body>
|
|
872
|
+
<div class="container">
|
|
873
|
+
<h1>DiffLens Report</h1>
|
|
874
|
+
<div class="summary">
|
|
875
|
+
<p><strong>Generated at:</strong> ${new Date(data.timestamp).toLocaleString()}</p>
|
|
876
|
+
<p><strong>Total Scenarios:</strong> ${data.results.length}</p>
|
|
877
|
+
<p><strong>Failed:</strong> ${data.results.filter((r) => r.status === "fail").length}</p>
|
|
878
|
+
</div>
|
|
879
|
+
|
|
880
|
+
${data.results.map((result) => `
|
|
881
|
+
<div class="scenario">
|
|
882
|
+
<div class="scenario-header">
|
|
883
|
+
<span class="scenario-title">${result.scenario}</span>
|
|
884
|
+
<span class="status ${result.status}">${result.status.toUpperCase()}</span>
|
|
885
|
+
</div>
|
|
886
|
+
<div class="scenario-body">
|
|
887
|
+
<div class="images">
|
|
888
|
+
${result.baselinePath ? `
|
|
889
|
+
<div class="image-container">
|
|
890
|
+
<div class="image-label">Baseline</div>
|
|
891
|
+
<img src="${import_path2.default.relative(import_path2.default.resolve(outDir), import_path2.default.resolve(result.baselinePath))}" alt="Baseline">
|
|
892
|
+
</div>
|
|
893
|
+
` : ""}
|
|
894
|
+
<div class="image-container">
|
|
895
|
+
<div class="image-label">Current</div>
|
|
896
|
+
<img src="${import_path2.default.relative(import_path2.default.resolve(outDir), import_path2.default.resolve(result.screenshotPath))}" alt="Current">
|
|
897
|
+
</div>
|
|
898
|
+
${result.diffPath ? `
|
|
899
|
+
<div class="image-container">
|
|
900
|
+
<div class="image-label">Diff</div>
|
|
901
|
+
<img src="${import_path2.default.relative(import_path2.default.resolve(outDir), import_path2.default.resolve(result.diffPath))}" alt="Diff">
|
|
902
|
+
</div>
|
|
903
|
+
` : ""}
|
|
904
|
+
</div>
|
|
905
|
+
|
|
906
|
+
${result.diffPixels === -1 ? `
|
|
907
|
+
<p style="color: #dc3545; font-weight: bold;">[FAIL] Dimension mismatch! Baseline and current images have different sizes.</p>
|
|
908
|
+
` : `
|
|
909
|
+
<p><strong>Diff:</strong> ${result.diffPixels} pixels (${result.diffPercentage.toFixed(2)}%)</p>
|
|
910
|
+
`}
|
|
911
|
+
|
|
912
|
+
${result.a11yViolations.length > 0 ? `
|
|
913
|
+
<div class="a11y">
|
|
914
|
+
<div class="a11y-title">Accessibility Violations (${result.a11yViolations.length})</div>
|
|
915
|
+
${result.a11yViolations.map((v) => `
|
|
916
|
+
<div class="violation">
|
|
917
|
+
[${v.impact}] ${v.help} (${v.id})
|
|
918
|
+
</div>
|
|
919
|
+
`).join("")}
|
|
920
|
+
</div>
|
|
921
|
+
` : '<div class="a11y"><div class="a11y-title">No Accessibility Violations</div></div>'}
|
|
922
|
+
</div>
|
|
923
|
+
</div>
|
|
924
|
+
`).join("")}
|
|
925
|
+
</div>
|
|
926
|
+
</body>
|
|
927
|
+
</html>
|
|
928
|
+
`;
|
|
929
|
+
import_fs3.default.writeFileSync(import_path2.default.join(outDir, "index.html"), html);
|
|
930
|
+
import_fs3.default.writeFileSync(import_path2.default.join(outDir, "report.json"), JSON.stringify(data, null, 2));
|
|
931
|
+
console.log(`Report generated: ${import_path2.default.join(outDir, "index.html")}`);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// src/core/runner.ts
|
|
935
|
+
async function runTests(config) {
|
|
936
|
+
const browser = await import_playwright2.chromium.launch();
|
|
937
|
+
const context = await browser.newContext();
|
|
938
|
+
const page = await context.newPage();
|
|
939
|
+
const outDir = config.outDir || ".difflens";
|
|
940
|
+
const dirs = {
|
|
941
|
+
baseline: import_path4.default.join(outDir, "baseline"),
|
|
942
|
+
current: import_path4.default.join(outDir, "current"),
|
|
943
|
+
diff: import_path4.default.join(outDir, "diff")
|
|
944
|
+
};
|
|
945
|
+
Object.values(dirs).forEach((dir) => {
|
|
946
|
+
if (!import_fs4.default.existsSync(dir)) {
|
|
947
|
+
import_fs4.default.mkdirSync(dir, { recursive: true });
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
const results = [];
|
|
951
|
+
if (!config.scenarios || config.scenarios.length === 0) {
|
|
952
|
+
console.error("Error: No scenarios defined in configuration.");
|
|
953
|
+
return false;
|
|
954
|
+
}
|
|
955
|
+
for (const scenario of config.scenarios) {
|
|
956
|
+
const viewports = scenario.viewports || config.viewports && Object.entries(config.viewports).map(([label, { width, height }]) => ({ label, width, height })) || [{ width: 1280, height: 720, label: "default" }];
|
|
957
|
+
for (const viewport of viewports) {
|
|
958
|
+
const viewportLabel = viewport.label || `${viewport.width}x${viewport.height}`;
|
|
959
|
+
const label = viewports.length > 1 || viewport.label ? `${scenario.label}-${viewportLabel}` : scenario.label;
|
|
960
|
+
console.error(`Running scenario: ${label}`);
|
|
961
|
+
const url = config.baseUrl ? new URL(scenario.path, config.baseUrl).toString() : scenario.path;
|
|
962
|
+
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
|
963
|
+
let retries = 2;
|
|
964
|
+
let navigationSuccess = false;
|
|
965
|
+
while (retries >= 0) {
|
|
966
|
+
try {
|
|
967
|
+
await page.goto(url, { waitUntil: "networkidle" });
|
|
968
|
+
navigationSuccess = true;
|
|
969
|
+
break;
|
|
970
|
+
} catch (e) {
|
|
971
|
+
if (retries === 0) {
|
|
972
|
+
console.error(` [WARN] Navigation failed: ${e}`);
|
|
973
|
+
} else {
|
|
974
|
+
console.error(` [WARN] Navigation failed, retrying... (${retries} attempts left)`);
|
|
975
|
+
retries--;
|
|
976
|
+
await page.waitForTimeout(1e3);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
if (!navigationSuccess) {
|
|
981
|
+
if (config.failOnNavigationError) {
|
|
982
|
+
console.error(` [FAIL] Navigation failed.`);
|
|
983
|
+
results.push({
|
|
984
|
+
scenario: label,
|
|
985
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
986
|
+
status: "fail",
|
|
987
|
+
diffPixels: 0,
|
|
988
|
+
diffPercentage: 0,
|
|
989
|
+
screenshotPath: "",
|
|
990
|
+
a11yViolations: []
|
|
991
|
+
});
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
let actionFailed = false;
|
|
996
|
+
if (scenario.actions) {
|
|
997
|
+
for (const action of scenario.actions) {
|
|
998
|
+
console.error(` Action: ${action.type} ${action.selector || ""}`);
|
|
999
|
+
try {
|
|
1000
|
+
if (action.type === "click" && action.selector) {
|
|
1001
|
+
await page.click(action.selector);
|
|
1002
|
+
} else if (action.type === "type" && action.selector && action.value) {
|
|
1003
|
+
await page.fill(action.selector, action.value);
|
|
1004
|
+
} else if (action.type === "wait" && action.timeout) {
|
|
1005
|
+
await page.waitForTimeout(action.timeout);
|
|
1006
|
+
}
|
|
1007
|
+
} catch (e) {
|
|
1008
|
+
console.error(` [WARN] Action failed: ${e}`);
|
|
1009
|
+
actionFailed = true;
|
|
1010
|
+
if (config.failOnActionError) {
|
|
1011
|
+
break;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
await page.waitForTimeout(500);
|
|
1016
|
+
}
|
|
1017
|
+
const currentPath = import_path4.default.join(dirs.current, `${label}.png`);
|
|
1018
|
+
retries = 2;
|
|
1019
|
+
while (retries >= 0) {
|
|
1020
|
+
try {
|
|
1021
|
+
await captureScreenshot(page, null, currentPath, {
|
|
1022
|
+
fullPage: true,
|
|
1023
|
+
maskSelectors: scenario.maskSelectors,
|
|
1024
|
+
hideSelectors: scenario.hideSelectors
|
|
1025
|
+
});
|
|
1026
|
+
break;
|
|
1027
|
+
} catch (e) {
|
|
1028
|
+
if (retries === 0) throw e;
|
|
1029
|
+
console.error(` [WARN] Screenshot failed, retrying... (${retries} attempts left)`);
|
|
1030
|
+
retries--;
|
|
1031
|
+
await page.waitForTimeout(1e3);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
console.error(` Screenshot captured: ${currentPath}`);
|
|
1035
|
+
const a11yResult = await runAxeAudit(page);
|
|
1036
|
+
if (a11yResult.violations.length > 0) {
|
|
1037
|
+
console.error(` Accessibility violations: ${a11yResult.violations.length}`);
|
|
1038
|
+
}
|
|
1039
|
+
const baselinePath = import_path4.default.join(dirs.baseline, `${label}.png`);
|
|
1040
|
+
const diffPath = import_path4.default.join(dirs.diff, `${label}.png`);
|
|
1041
|
+
let status = "new";
|
|
1042
|
+
let diffPixels = 0;
|
|
1043
|
+
let diffPercentage = 0;
|
|
1044
|
+
let finalDiffPath;
|
|
1045
|
+
let finalBaselinePath;
|
|
1046
|
+
let dimensionMismatch = false;
|
|
1047
|
+
if (import_fs4.default.existsSync(baselinePath)) {
|
|
1048
|
+
finalBaselinePath = baselinePath;
|
|
1049
|
+
console.error(" Comparing with baseline...");
|
|
1050
|
+
const result = compareImages(baselinePath, currentPath, diffPath, config.threshold);
|
|
1051
|
+
diffPixels = result.diffPixels;
|
|
1052
|
+
diffPercentage = result.diffPercentage;
|
|
1053
|
+
dimensionMismatch = !result.isSameDimensions;
|
|
1054
|
+
if (!result.isSameDimensions) {
|
|
1055
|
+
status = "fail";
|
|
1056
|
+
console.error(` [FAIL] Dimension mismatch! Baseline and current images have different sizes.`);
|
|
1057
|
+
} else if (result.diffPixels > 0) {
|
|
1058
|
+
status = "fail";
|
|
1059
|
+
finalDiffPath = diffPath;
|
|
1060
|
+
console.error(` [FAIL] Visual regression detected! Diff pixels: ${result.diffPixels} (${result.diffPercentage.toFixed(2)}%)`);
|
|
1061
|
+
console.error(` Diff image saved to: ${diffPath}`);
|
|
1062
|
+
} else {
|
|
1063
|
+
status = "pass";
|
|
1064
|
+
console.error(" [PASS] No visual regression detected.");
|
|
1065
|
+
}
|
|
1066
|
+
} else {
|
|
1067
|
+
console.error(" Baseline not found. Saving current as baseline.");
|
|
1068
|
+
import_fs4.default.copyFileSync(currentPath, baselinePath);
|
|
1069
|
+
console.error(` Baseline saved to: ${baselinePath}`);
|
|
1070
|
+
finalBaselinePath = baselinePath;
|
|
1071
|
+
}
|
|
1072
|
+
if (a11yResult.violations.length > 0) {
|
|
1073
|
+
status = "fail";
|
|
1074
|
+
console.error(` [FAIL] Accessibility violations detected: ${a11yResult.violations.length}`);
|
|
1075
|
+
}
|
|
1076
|
+
if (actionFailed && config.failOnActionError) {
|
|
1077
|
+
status = "fail";
|
|
1078
|
+
console.error(` [FAIL] Action execution failed.`);
|
|
1079
|
+
}
|
|
1080
|
+
results.push({
|
|
1081
|
+
scenario: label,
|
|
1082
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1083
|
+
status,
|
|
1084
|
+
diffPixels,
|
|
1085
|
+
diffPercentage,
|
|
1086
|
+
screenshotPath: currentPath,
|
|
1087
|
+
diffPath: finalDiffPath,
|
|
1088
|
+
baselinePath: finalBaselinePath,
|
|
1089
|
+
a11yViolations: a11yResult.violations,
|
|
1090
|
+
dimensionMismatch
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
await browser.close();
|
|
1095
|
+
const reportData = {
|
|
1096
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1097
|
+
results
|
|
1098
|
+
};
|
|
1099
|
+
if (config.format === "json") {
|
|
1100
|
+
console.log(JSON.stringify(results, null, 2));
|
|
1101
|
+
} else if (config.format === "ai") {
|
|
1102
|
+
const { formatAiReport: formatAiReport2 } = await Promise.resolve().then(() => (init_ai_reporter(), ai_reporter_exports));
|
|
1103
|
+
console.log(formatAiReport2(results, outDir));
|
|
1104
|
+
} else {
|
|
1105
|
+
generateHtmlReport(reportData, outDir);
|
|
1106
|
+
}
|
|
1107
|
+
const hasFailures = results.some((r) => r.status === "fail");
|
|
1108
|
+
return !hasFailures;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// src/cli.ts
|
|
1112
|
+
var cli = dist_default("difflens");
|
|
1113
|
+
cli.command("init", "Initialize DiffLens configuration").action(() => {
|
|
1114
|
+
const configPath = import_path5.default.resolve(process.cwd(), "difflens.config.ts");
|
|
1115
|
+
if (import_fs5.default.existsSync(configPath)) {
|
|
1116
|
+
console.log("Configuration file already exists: difflens.config.ts");
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
const template = `import { DiffLensConfig } from 'difflens';
|
|
1120
|
+
|
|
1121
|
+
const config: DiffLensConfig = {
|
|
1122
|
+
scenarios: [
|
|
1123
|
+
{
|
|
1124
|
+
label: 'example',
|
|
1125
|
+
path: 'https://example.com',
|
|
1126
|
+
},
|
|
1127
|
+
],
|
|
1128
|
+
outDir: '.difflens',
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
export default config;
|
|
1132
|
+
`;
|
|
1133
|
+
import_fs5.default.writeFileSync(configPath, template);
|
|
1134
|
+
console.log("Created configuration file: difflens.config.ts");
|
|
1135
|
+
});
|
|
1136
|
+
cli.command("test", "Run visual regression tests").option("--format <type>", "Output format (default, json, ai)").option("--url <url>", "URL to test (overrides config file)").option("--label <label>", "Label for the test scenario", { default: "check" }).action(async (options) => {
|
|
1137
|
+
console.error("Running tests...");
|
|
1138
|
+
try {
|
|
1139
|
+
let config;
|
|
1140
|
+
if (options.url) {
|
|
1141
|
+
config = {
|
|
1142
|
+
scenarios: [{ label: options.label, path: options.url }],
|
|
1143
|
+
outDir: ".difflens",
|
|
1144
|
+
format: options.format
|
|
1145
|
+
};
|
|
1146
|
+
} else {
|
|
1147
|
+
config = await loadConfig();
|
|
1148
|
+
if (options.format) {
|
|
1149
|
+
config.format = options.format;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
const success = await runTests(config);
|
|
1153
|
+
if (!success) {
|
|
1154
|
+
console.error("Tests failed.");
|
|
1155
|
+
process.exit(1);
|
|
1156
|
+
}
|
|
1157
|
+
} catch (error) {
|
|
1158
|
+
console.error("Test run failed:", error);
|
|
1159
|
+
process.exit(1);
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
cli.command("report", "Generate HTML report").action(() => {
|
|
1163
|
+
console.error("Generating report...");
|
|
1164
|
+
console.error("Report generation is currently integrated into the test command.");
|
|
1165
|
+
});
|
|
1166
|
+
cli.help();
|
|
1167
|
+
cli.version(version);
|
|
1168
|
+
try {
|
|
1169
|
+
cli.parse();
|
|
1170
|
+
} catch (error) {
|
|
1171
|
+
console.error(error);
|
|
1172
|
+
process.exit(1);
|
|
1173
|
+
}
|