@keboola/validate-ui 0.2.0 → 0.4.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/dist/cli.cjs +544 -87
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +544 -87
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +410 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +196 -5
- package/dist/index.d.ts +196 -5
- package/dist/index.js +397 -3
- package/dist/index.js.map +1 -1
- package/package.json +7 -5
package/dist/index.cjs
CHANGED
|
@@ -10,6 +10,7 @@ var zod = require('zod');
|
|
|
10
10
|
var Anthropic = require('@anthropic-ai/sdk');
|
|
11
11
|
var pixelmatch = require('pixelmatch');
|
|
12
12
|
var pngjs = require('pngjs');
|
|
13
|
+
var brandRegistry = require('@keboola/brand-registry');
|
|
13
14
|
|
|
14
15
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
15
16
|
|
|
@@ -341,6 +342,302 @@ ${domExcerpt}`
|
|
|
341
342
|
}
|
|
342
343
|
};
|
|
343
344
|
|
|
345
|
+
// src/compare/diff.ts
|
|
346
|
+
var matchesGlob = (pattern, value) => {
|
|
347
|
+
if (!pattern.includes("*")) return pattern === value;
|
|
348
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
349
|
+
return new RegExp(`^${escaped}$`).test(value);
|
|
350
|
+
};
|
|
351
|
+
var multisetMissing = (from, to) => {
|
|
352
|
+
const remaining = /* @__PURE__ */ new Map();
|
|
353
|
+
for (const value of to) remaining.set(value, (remaining.get(value) ?? 0) + 1);
|
|
354
|
+
const missing = [];
|
|
355
|
+
for (const value of from) {
|
|
356
|
+
const count = remaining.get(value) ?? 0;
|
|
357
|
+
if (count > 0) remaining.set(value, count - 1);
|
|
358
|
+
else missing.push(value);
|
|
359
|
+
}
|
|
360
|
+
return missing;
|
|
361
|
+
};
|
|
362
|
+
var signature = (regionKey, tokens) => `${regionKey}::${[...tokens].sort().join("|")}`;
|
|
363
|
+
var regionRemovalRule = (rules, key) => rules.find(
|
|
364
|
+
(rule) => rule.value === void 0 && rule.region !== void 0 && matchesGlob(rule.region, key)
|
|
365
|
+
);
|
|
366
|
+
var valueRuleMatches = (rule, key, value) => rule.value !== void 0 && matchesGlob(rule.value, value) && (rule.region === void 0 || matchesGlob(rule.region, key));
|
|
367
|
+
var removedFinding = (region, rule) => ({
|
|
368
|
+
regionKey: region.key,
|
|
369
|
+
kind: "region-removed",
|
|
370
|
+
valueSignature: signature(region.key, region.values),
|
|
371
|
+
message: rule === void 0 ? `region "${region.label}" was removed` : `region "${region.label}" removed as expected`,
|
|
372
|
+
severity: rule === void 0 ? "serious" : "minor",
|
|
373
|
+
...rule !== void 0 && { suppressed: { reason: rule.reason } }
|
|
374
|
+
});
|
|
375
|
+
var degradedFinding = (region) => ({
|
|
376
|
+
regionKey: region.key,
|
|
377
|
+
kind: "region-degraded",
|
|
378
|
+
valueSignature: signature(region.key, [region.state]),
|
|
379
|
+
message: `region "${region.label}" degraded to ${region.state} state \u2014 not compared as value loss`,
|
|
380
|
+
severity: "minor"
|
|
381
|
+
});
|
|
382
|
+
var addedFinding = (region) => ({
|
|
383
|
+
regionKey: region.key,
|
|
384
|
+
kind: "region-added",
|
|
385
|
+
valueSignature: signature(region.key, []),
|
|
386
|
+
message: `region "${region.label}" is new`,
|
|
387
|
+
severity: "minor"
|
|
388
|
+
});
|
|
389
|
+
var diffRegionValues = (oldRegion, newRegion, rules) => {
|
|
390
|
+
const missing = multisetMissing(oldRegion.values, newRegion.values);
|
|
391
|
+
if (missing.length === 0) return [];
|
|
392
|
+
const active = [];
|
|
393
|
+
const suppressed = [];
|
|
394
|
+
const reasons = /* @__PURE__ */ new Set();
|
|
395
|
+
for (const value of missing) {
|
|
396
|
+
const rule = rules.find((r) => valueRuleMatches(r, newRegion.key, value));
|
|
397
|
+
if (rule === void 0) active.push(value);
|
|
398
|
+
else {
|
|
399
|
+
suppressed.push(value);
|
|
400
|
+
reasons.add(rule.reason);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const findings = [];
|
|
404
|
+
if (active.length > 0) {
|
|
405
|
+
findings.push({
|
|
406
|
+
regionKey: newRegion.key,
|
|
407
|
+
kind: "value-missing",
|
|
408
|
+
valueSignature: signature(newRegion.key, active),
|
|
409
|
+
message: `region "${newRegion.label}" is missing ${active.length} value(s)`,
|
|
410
|
+
severity: "moderate",
|
|
411
|
+
detail: active.join(", ")
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
if (suppressed.length > 0) {
|
|
415
|
+
findings.push({
|
|
416
|
+
regionKey: newRegion.key,
|
|
417
|
+
kind: "value-missing",
|
|
418
|
+
valueSignature: signature(newRegion.key, suppressed),
|
|
419
|
+
message: `region "${newRegion.label}" is missing ${suppressed.length} expected value(s)`,
|
|
420
|
+
severity: "minor",
|
|
421
|
+
detail: suppressed.join(", "),
|
|
422
|
+
suppressed: { reason: [...reasons].join("; ") }
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
return findings;
|
|
426
|
+
};
|
|
427
|
+
var diffSnapshots = (oldSnap, newSnap, config = {}) => {
|
|
428
|
+
const rules = config.expectedAbsent ?? [];
|
|
429
|
+
const newByKey = new Map(newSnap.regions.map((region) => [region.key, region]));
|
|
430
|
+
const oldKeys = new Set(oldSnap.regions.map((region) => region.key));
|
|
431
|
+
const findings = [];
|
|
432
|
+
for (const oldRegion of oldSnap.regions) {
|
|
433
|
+
const newRegion = newByKey.get(oldRegion.key);
|
|
434
|
+
if (newRegion === void 0) {
|
|
435
|
+
findings.push(removedFinding(oldRegion, regionRemovalRule(rules, oldRegion.key)));
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (oldRegion.state === "ok" && newRegion.state !== "ok") {
|
|
439
|
+
findings.push(degradedFinding(newRegion));
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
findings.push(...diffRegionValues(oldRegion, newRegion, rules));
|
|
443
|
+
}
|
|
444
|
+
for (const newRegion of newSnap.regions) {
|
|
445
|
+
if (!oldKeys.has(newRegion.key)) findings.push(addedFinding(newRegion));
|
|
446
|
+
}
|
|
447
|
+
return findings;
|
|
448
|
+
};
|
|
449
|
+
var comparePass = (findings) => !findings.some(
|
|
450
|
+
(f) => f.suppressed === void 0 && (f.severity === "serious" || f.severity === "critical")
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
// src/compare/regions.ts
|
|
454
|
+
var CANDIDATE_SELECTOR = [
|
|
455
|
+
"[data-region]",
|
|
456
|
+
"main",
|
|
457
|
+
"nav",
|
|
458
|
+
"header",
|
|
459
|
+
"aside",
|
|
460
|
+
"footer",
|
|
461
|
+
"section[aria-label]",
|
|
462
|
+
"[role=main]",
|
|
463
|
+
"[role=navigation]",
|
|
464
|
+
"[role=banner]",
|
|
465
|
+
"[role=complementary]",
|
|
466
|
+
"[role=contentinfo]",
|
|
467
|
+
"[role=region]"
|
|
468
|
+
].join(",");
|
|
469
|
+
var DEFAULT_EMPTY_MARKERS = [
|
|
470
|
+
"no data",
|
|
471
|
+
"no results",
|
|
472
|
+
"nothing to show",
|
|
473
|
+
"nothing here",
|
|
474
|
+
"get started"
|
|
475
|
+
];
|
|
476
|
+
var DEFAULT_ERROR_MARKERS = [
|
|
477
|
+
"something went wrong",
|
|
478
|
+
"failed to load",
|
|
479
|
+
"could not load",
|
|
480
|
+
"unavailable",
|
|
481
|
+
"try again"
|
|
482
|
+
];
|
|
483
|
+
var extractRegions = async (page, config = {}) => {
|
|
484
|
+
const emptyMarkers = [...DEFAULT_EMPTY_MARKERS, ...config.emptyStateMarkers ?? []];
|
|
485
|
+
const errorMarkers = [...DEFAULT_ERROR_MARKERS, ...config.errorStateMarkers ?? []];
|
|
486
|
+
return page.evaluate(
|
|
487
|
+
({ selector, emptyMarkers: emptyMarkers2, errorMarkers: errorMarkers2 }) => {
|
|
488
|
+
const STATES = ["ok", "empty", "error", "loading"];
|
|
489
|
+
const isState = (value) => STATES.includes(value);
|
|
490
|
+
const slugify = (raw) => raw.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "region";
|
|
491
|
+
const roleOf = (el) => {
|
|
492
|
+
const explicit = el.getAttribute("role");
|
|
493
|
+
if (explicit !== null && explicit !== "") return explicit;
|
|
494
|
+
const tag = el.tagName.toLowerCase();
|
|
495
|
+
if (tag === "main") return "main";
|
|
496
|
+
if (tag === "nav") return "navigation";
|
|
497
|
+
if (tag === "aside") return "complementary";
|
|
498
|
+
if (tag === "header" || tag === "footer") {
|
|
499
|
+
if (el.closest("main,nav,section,article,aside") !== null) return void 0;
|
|
500
|
+
return tag === "header" ? "banner" : "contentinfo";
|
|
501
|
+
}
|
|
502
|
+
return void 0;
|
|
503
|
+
};
|
|
504
|
+
const headingOf = (el) => {
|
|
505
|
+
const heading = el.querySelector("h1,h2,h3,h4,h5,h6");
|
|
506
|
+
const text = heading?.textContent?.trim();
|
|
507
|
+
return text !== void 0 && text !== "" ? text : void 0;
|
|
508
|
+
};
|
|
509
|
+
const keyOf = (el) => {
|
|
510
|
+
const region = el.getAttribute("data-region");
|
|
511
|
+
if (region !== null && region !== "") return slugify(region);
|
|
512
|
+
const testid = el.getAttribute("data-testid");
|
|
513
|
+
if (testid !== null && testid !== "") return slugify(testid);
|
|
514
|
+
const role = roleOf(el);
|
|
515
|
+
if (role !== void 0) return role;
|
|
516
|
+
const label = el.getAttribute("aria-label");
|
|
517
|
+
if (label !== null && label !== "") return slugify(label);
|
|
518
|
+
const heading = headingOf(el);
|
|
519
|
+
if (heading !== void 0) return slugify(heading);
|
|
520
|
+
return slugify(el.tagName);
|
|
521
|
+
};
|
|
522
|
+
const labelOf = (el, key) => {
|
|
523
|
+
const label = el.getAttribute("aria-label");
|
|
524
|
+
if (label !== null && label.trim() !== "") return label.trim();
|
|
525
|
+
const heading = headingOf(el);
|
|
526
|
+
if (heading !== void 0) return heading;
|
|
527
|
+
const testid = el.getAttribute("data-testid");
|
|
528
|
+
if (testid !== null && testid !== "") return testid;
|
|
529
|
+
return key;
|
|
530
|
+
};
|
|
531
|
+
const textOf = (el) => (el.innerText || el.textContent || "").trim();
|
|
532
|
+
const stateOf = (el, text) => {
|
|
533
|
+
const explicit = (el.getAttribute("data-state") ?? "").toLowerCase();
|
|
534
|
+
if (isState(explicit)) return explicit;
|
|
535
|
+
const lower = text.toLowerCase();
|
|
536
|
+
if (errorMarkers2.some((marker) => lower.includes(marker))) return "error";
|
|
537
|
+
if (lower.includes("loading")) return "loading";
|
|
538
|
+
if (emptyMarkers2.some((marker) => lower.includes(marker))) return "empty";
|
|
539
|
+
return "ok";
|
|
540
|
+
};
|
|
541
|
+
const valuesOf = (text) => (text.match(/-?\$?\s?\d[\d,]*(?:\.\d+)?%?/g) ?? []).map((token) => token.replace(/[$\s,]/g, "")).filter((token) => token !== "" && token !== "-");
|
|
542
|
+
const qualifies = (el) => {
|
|
543
|
+
const dataRegion = el.getAttribute("data-region");
|
|
544
|
+
if (dataRegion !== null && dataRegion !== "") {
|
|
545
|
+
const parent = el.parentElement;
|
|
546
|
+
return parent === null || parent.closest("[data-region]") === null;
|
|
547
|
+
}
|
|
548
|
+
return el.querySelector(selector) === null && el.closest("[data-region]") === null;
|
|
549
|
+
};
|
|
550
|
+
const candidates = Array.from(document.querySelectorAll(selector));
|
|
551
|
+
const regions = candidates.filter(qualifies);
|
|
552
|
+
const elements = regions.length > 0 ? regions : [document.body];
|
|
553
|
+
const used = {};
|
|
554
|
+
const uniqueKey = (key) => {
|
|
555
|
+
const count = (used[key] ?? 0) + 1;
|
|
556
|
+
used[key] = count;
|
|
557
|
+
return count === 1 ? key : `${key}-${count}`;
|
|
558
|
+
};
|
|
559
|
+
return elements.map((el) => {
|
|
560
|
+
const text = textOf(el);
|
|
561
|
+
const key = uniqueKey(keyOf(el));
|
|
562
|
+
return {
|
|
563
|
+
key,
|
|
564
|
+
label: labelOf(el, key),
|
|
565
|
+
state: stateOf(el, text),
|
|
566
|
+
values: valuesOf(text)
|
|
567
|
+
};
|
|
568
|
+
});
|
|
569
|
+
},
|
|
570
|
+
{ selector: CANDIDATE_SELECTOR, emptyMarkers, errorMarkers }
|
|
571
|
+
);
|
|
572
|
+
};
|
|
573
|
+
var REGION_STATES = ["ok", "empty", "error", "loading"];
|
|
574
|
+
var Region = zod.z.object({
|
|
575
|
+
/** Stable id: data-region → data-testid → role → aria-label → heading slug. */
|
|
576
|
+
key: zod.z.string(),
|
|
577
|
+
/** Human label for the report. */
|
|
578
|
+
label: zod.z.string(),
|
|
579
|
+
state: zod.z.enum(REGION_STATES),
|
|
580
|
+
/** Normalized numeric / currency / percent tokens found in the region. */
|
|
581
|
+
values: zod.z.array(zod.z.string())
|
|
582
|
+
});
|
|
583
|
+
var CompareSnapshot = zod.z.object({
|
|
584
|
+
/** Route the snapshot was captured at, e.g. `/financials`. */
|
|
585
|
+
route: zod.z.string(),
|
|
586
|
+
regions: zod.z.array(Region)
|
|
587
|
+
}).refine(
|
|
588
|
+
(snap) => new Set(snap.regions.map((region) => region.key)).size === snap.regions.length,
|
|
589
|
+
{
|
|
590
|
+
message: "region keys must be unique"
|
|
591
|
+
}
|
|
592
|
+
);
|
|
593
|
+
var ExpectedAbsentRule = zod.z.object({
|
|
594
|
+
region: zod.z.string().optional(),
|
|
595
|
+
value: zod.z.string().optional(),
|
|
596
|
+
reason: zod.z.string().min(1)
|
|
597
|
+
}).refine((rule) => rule.region !== void 0 || rule.value !== void 0, {
|
|
598
|
+
message: "expectedAbsent rule needs at least one of `region` or `value`"
|
|
599
|
+
});
|
|
600
|
+
var CompareConfig = zod.z.object({
|
|
601
|
+
expectedAbsent: zod.z.array(ExpectedAbsentRule).optional(),
|
|
602
|
+
emptyStateMarkers: zod.z.array(zod.z.string()).optional(),
|
|
603
|
+
errorStateMarkers: zod.z.array(zod.z.string()).optional()
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
// src/compare/snapshot.ts
|
|
607
|
+
var captureSnapshot = async (page, route, config) => ({
|
|
608
|
+
route,
|
|
609
|
+
regions: await extractRegions(page, config)
|
|
610
|
+
});
|
|
611
|
+
var saveSnapshot = async (snapshot, path) => {
|
|
612
|
+
await promises.writeFile(path, JSON.stringify(snapshot, null, 2));
|
|
613
|
+
};
|
|
614
|
+
var loadSnapshot = async (path) => {
|
|
615
|
+
const raw = await promises.readFile(path, "utf8");
|
|
616
|
+
return CompareSnapshot.parse(JSON.parse(raw));
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
// src/axes/compare.ts
|
|
620
|
+
var skip2 = (message) => ({
|
|
621
|
+
axis: "compare",
|
|
622
|
+
pass: true,
|
|
623
|
+
findings: [{ message, severity: "minor" }]
|
|
624
|
+
});
|
|
625
|
+
var compareAxis = {
|
|
626
|
+
name: "compare",
|
|
627
|
+
run: async (context) => {
|
|
628
|
+
const { comparison, compareConfig, page } = context;
|
|
629
|
+
if (comparison === void 0) {
|
|
630
|
+
return skip2("no comparison snapshot provided; old-vs-new not assessed");
|
|
631
|
+
}
|
|
632
|
+
if (page === void 0) {
|
|
633
|
+
return skip2("compare needs a live page to extract the new snapshot");
|
|
634
|
+
}
|
|
635
|
+
const newSnapshot = await captureSnapshot(page, comparison.route, compareConfig);
|
|
636
|
+
const findings = diffSnapshots(comparison, newSnapshot, compareConfig);
|
|
637
|
+
return { axis: "compare", pass: comparePass(findings), findings };
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
|
|
344
641
|
// src/axes/runtime-health.ts
|
|
345
642
|
var ERROR_MARKERS = [
|
|
346
643
|
"something went wrong",
|
|
@@ -479,6 +776,11 @@ var altBrandFinding = (screenshot, altScreenshot, brand) => {
|
|
|
479
776
|
}
|
|
480
777
|
return null;
|
|
481
778
|
};
|
|
779
|
+
var interactiveContrastFindings = (brand) => brandRegistry.checkInteractiveContrast(brand).map((warning) => ({
|
|
780
|
+
message: `brand '${brand.id}' ${warning.message}`,
|
|
781
|
+
severity: "moderate",
|
|
782
|
+
detail: `${warning.foreground} on ${warning.background} (${warning.palette})`
|
|
783
|
+
}));
|
|
482
784
|
var overflowFinding = async (page) => {
|
|
483
785
|
const overflows = await page.evaluate(
|
|
484
786
|
() => document.documentElement.scrollWidth > document.documentElement.clientWidth
|
|
@@ -505,6 +807,8 @@ var visualBrandAxis = {
|
|
|
505
807
|
DEFAULT_ALT_BRAND
|
|
506
808
|
);
|
|
507
809
|
if (finding !== null) findings.push(finding);
|
|
810
|
+
const altBrand = brandRegistry.brands.find((brand) => brand.id === DEFAULT_ALT_BRAND);
|
|
811
|
+
if (altBrand !== void 0) findings.push(...interactiveContrastFindings(altBrand));
|
|
508
812
|
}
|
|
509
813
|
if (page !== void 0) {
|
|
510
814
|
const finding = await overflowFinding(page);
|
|
@@ -520,7 +824,8 @@ var ALL_AXES = [
|
|
|
520
824
|
runtimeHealthAxis,
|
|
521
825
|
accessibilityAxis,
|
|
522
826
|
visualBrandAxis,
|
|
523
|
-
briefConformanceAxis
|
|
827
|
+
briefConformanceAxis,
|
|
828
|
+
compareAxis
|
|
524
829
|
];
|
|
525
830
|
|
|
526
831
|
// src/aggregate.ts
|
|
@@ -564,6 +869,8 @@ var validate = async (options) => {
|
|
|
564
869
|
browser,
|
|
565
870
|
brief: options.brief,
|
|
566
871
|
baseline: options.baseline,
|
|
872
|
+
comparison: options.comparison,
|
|
873
|
+
compareConfig: options.compareConfig,
|
|
567
874
|
captureUnderBrand: async (brand) => {
|
|
568
875
|
const brandContext = await browser.newContext({
|
|
569
876
|
viewport: { width: viewport.width, height: viewport.height }
|
|
@@ -589,19 +896,121 @@ var validate = async (options) => {
|
|
|
589
896
|
}
|
|
590
897
|
};
|
|
591
898
|
|
|
899
|
+
// src/compare/dedup.ts
|
|
900
|
+
var groupKey = (finding) => `${finding.kind}::${finding.valueSignature}`;
|
|
901
|
+
var dedupeSharedChrome = (perRoute) => {
|
|
902
|
+
const routesByGroup = /* @__PURE__ */ new Map();
|
|
903
|
+
for (const { route, findings } of perRoute) {
|
|
904
|
+
for (const finding of findings) {
|
|
905
|
+
const key = groupKey(finding);
|
|
906
|
+
const routes = routesByGroup.get(key) ?? /* @__PURE__ */ new Set();
|
|
907
|
+
routes.add(route);
|
|
908
|
+
routesByGroup.set(key, routes);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
const sharedGroups = new Set(
|
|
912
|
+
[...routesByGroup].filter(([, routes]) => routes.size >= 2).map(([key]) => key)
|
|
913
|
+
);
|
|
914
|
+
const sharedChrome = [];
|
|
915
|
+
const seenShared = /* @__PURE__ */ new Set();
|
|
916
|
+
const trimmedPerRoute = perRoute.map(({ route, findings }) => ({
|
|
917
|
+
route,
|
|
918
|
+
findings: findings.filter((finding) => !sharedGroups.has(groupKey(finding)))
|
|
919
|
+
}));
|
|
920
|
+
for (const { findings } of perRoute) {
|
|
921
|
+
for (const finding of findings) {
|
|
922
|
+
const key = groupKey(finding);
|
|
923
|
+
if (!sharedGroups.has(key) || seenShared.has(key)) continue;
|
|
924
|
+
seenShared.add(key);
|
|
925
|
+
const routes = [...routesByGroup.get(key) ?? []].sort();
|
|
926
|
+
sharedChrome.push({
|
|
927
|
+
...finding,
|
|
928
|
+
message: `${finding.message} (shared across ${routes.length} routes: ${routes.join(", ")})`
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return { sharedChrome, perRoute: trimmedPerRoute };
|
|
933
|
+
};
|
|
934
|
+
var DEFAULT_TIMEOUT_MS2 = 3e4;
|
|
935
|
+
var joinUrl = (base, route) => `${base.replace(/\/$/, "")}/${route.replace(/^\//, "")}`;
|
|
936
|
+
var routeSnapshotFile = (route) => `${route.replace(/^\//, "").replace(/\/+$/, "").replace(/\//g, "_") || "index"}.json`;
|
|
937
|
+
var compareRoutes = async (options) => {
|
|
938
|
+
if (options.oldBaseUrl === void 0 === (options.oldSnapshotDir === void 0)) {
|
|
939
|
+
throw new Error("compareRoutes: provide exactly one of oldBaseUrl or oldSnapshotDir");
|
|
940
|
+
}
|
|
941
|
+
const viewport = options.viewport ?? DESKTOP_VIEWPORT;
|
|
942
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
943
|
+
const browser = options.browser ?? await playwright.chromium.launch();
|
|
944
|
+
const ownsBrowser = options.browser === void 0;
|
|
945
|
+
const { oldBaseUrl, oldSnapshotDir } = options;
|
|
946
|
+
const snapshotUrl = async (url, route) => {
|
|
947
|
+
const context = await browser.newContext({
|
|
948
|
+
viewport: { width: viewport.width, height: viewport.height }
|
|
949
|
+
});
|
|
950
|
+
const page = await context.newPage();
|
|
951
|
+
try {
|
|
952
|
+
await page.goto(url, { waitUntil: "networkidle", timeout: timeoutMs });
|
|
953
|
+
await prepareForScreenshot(page);
|
|
954
|
+
return await captureSnapshot(page, route, options.config);
|
|
955
|
+
} finally {
|
|
956
|
+
await context.close();
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
const oldSnapshotFor = (route) => {
|
|
960
|
+
if (oldBaseUrl !== void 0) return snapshotUrl(joinUrl(oldBaseUrl, route), route);
|
|
961
|
+
if (oldSnapshotDir !== void 0) {
|
|
962
|
+
return loadSnapshot(path.join(oldSnapshotDir, routeSnapshotFile(route)));
|
|
963
|
+
}
|
|
964
|
+
throw new Error("compareRoutes: provide exactly one of oldBaseUrl or oldSnapshotDir");
|
|
965
|
+
};
|
|
966
|
+
try {
|
|
967
|
+
const perRoute = [];
|
|
968
|
+
for (const route of options.routes) {
|
|
969
|
+
const oldSnapshot = await oldSnapshotFor(route);
|
|
970
|
+
const newSnapshot = await snapshotUrl(joinUrl(options.newBaseUrl, route), route);
|
|
971
|
+
perRoute.push({ route, findings: diffSnapshots(oldSnapshot, newSnapshot, options.config) });
|
|
972
|
+
}
|
|
973
|
+
const { sharedChrome, perRoute: trimmed } = dedupeSharedChrome(perRoute);
|
|
974
|
+
const routes = trimmed.map(({ route, findings }) => ({
|
|
975
|
+
route,
|
|
976
|
+
findings,
|
|
977
|
+
pass: comparePass(findings)
|
|
978
|
+
}));
|
|
979
|
+
const pass = comparePass(sharedChrome) && routes.every((route) => route.pass);
|
|
980
|
+
return { routes, sharedChrome, pass };
|
|
981
|
+
} finally {
|
|
982
|
+
if (ownsBrowser) await browser.close();
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
|
|
592
986
|
exports.ALL_AXES = ALL_AXES;
|
|
987
|
+
exports.CompareConfig = CompareConfig;
|
|
988
|
+
exports.CompareSnapshot = CompareSnapshot;
|
|
593
989
|
exports.DEFAULT_VIEWPORTS = DEFAULT_VIEWPORTS;
|
|
594
990
|
exports.DESKTOP_VIEWPORT = DESKTOP_VIEWPORT;
|
|
991
|
+
exports.ExpectedAbsentRule = ExpectedAbsentRule;
|
|
595
992
|
exports.MOBILE_VIEWPORT = MOBILE_VIEWPORT;
|
|
993
|
+
exports.REGION_STATES = REGION_STATES;
|
|
994
|
+
exports.Region = Region;
|
|
596
995
|
exports.SEVERITIES = SEVERITIES;
|
|
597
996
|
exports.accessibilityAxis = accessibilityAxis;
|
|
598
997
|
exports.aggregatePass = aggregatePass;
|
|
599
998
|
exports.briefConformanceAxis = briefConformanceAxis;
|
|
600
999
|
exports.capture = capture;
|
|
601
1000
|
exports.capturePage = capturePage;
|
|
1001
|
+
exports.captureSnapshot = captureSnapshot;
|
|
1002
|
+
exports.compareAxis = compareAxis;
|
|
1003
|
+
exports.comparePass = comparePass;
|
|
1004
|
+
exports.compareRoutes = compareRoutes;
|
|
1005
|
+
exports.dedupeSharedChrome = dedupeSharedChrome;
|
|
1006
|
+
exports.diffSnapshots = diffSnapshots;
|
|
1007
|
+
exports.extractRegions = extractRegions;
|
|
1008
|
+
exports.loadSnapshot = loadSnapshot;
|
|
602
1009
|
exports.prepareForScreenshot = prepareForScreenshot;
|
|
1010
|
+
exports.routeSnapshotFile = routeSnapshotFile;
|
|
603
1011
|
exports.runAxes = runAxes;
|
|
604
1012
|
exports.runtimeHealthAxis = runtimeHealthAxis;
|
|
1013
|
+
exports.saveSnapshot = saveSnapshot;
|
|
605
1014
|
exports.serveStatic = serveStatic;
|
|
606
1015
|
exports.validate = validate;
|
|
607
1016
|
exports.visualBrandAxis = visualBrandAxis;
|