@decocms/parity 0.11.5 → 0.11.6
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/CHANGELOG.md +7 -0
- package/dist/cli.js +223 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
7
|
|
|
8
|
+
## [0.11.6](https://github.com/decocms/parity/compare/v0.11.5...v0.11.6) (2026-06-17)
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
* **New `plp-pagination` check.** Tests that `?page=2` and `?page=3` of every captured PLP return 200 AND show different products than page 1. Catches the classic migration regression: the new site silently ignores `?page=N` and returns the first page on every request (or caps pagination). Live tested against bagaggio TanStack — surfaces a real critical bug (`page=2` shows the same 10 products as `page=1`, 100% overlap). Falls back to scraping the home page for a category link when running standalone (`parity check plp-pagination`).
|
|
13
|
+
* **Cross-side count divergence detection.** When prod and cand both serve a paginated PLP but cand's `?page=2` product count differs from prod's by more than 30%, flag as medium-severity (likely sort-order or index pruning regression).
|
|
14
|
+
|
|
8
15
|
## [0.11.5](https://github.com/decocms/parity/compare/v0.11.4...v0.11.5) (2026-06-17)
|
|
9
16
|
|
|
10
17
|
### Fixed
|
package/dist/cli.js
CHANGED
|
@@ -8534,6 +8534,225 @@ function hasReservedSpace($img) {
|
|
|
8534
8534
|
return false;
|
|
8535
8535
|
}
|
|
8536
8536
|
|
|
8537
|
+
// src/checks/plp-pagination.ts
|
|
8538
|
+
async function plpPagination(ctx) {
|
|
8539
|
+
const start = Date.now();
|
|
8540
|
+
const issues = [];
|
|
8541
|
+
let prodPlp = pickPlpUrl(ctx.prodFlows);
|
|
8542
|
+
let candPlp = pickPlpUrl(ctx.candFlows);
|
|
8543
|
+
if (!prodPlp && ctx.prodPages.length > 0) {
|
|
8544
|
+
prodPlp = await discoverPlpFromHome(ctx.prodPages[0].url);
|
|
8545
|
+
}
|
|
8546
|
+
if (!candPlp && ctx.candPages.length > 0) {
|
|
8547
|
+
candPlp = await discoverPlpFromHome(ctx.candPages[0].url);
|
|
8548
|
+
}
|
|
8549
|
+
if (!prodPlp && !candPlp) {
|
|
8550
|
+
return {
|
|
8551
|
+
name: "plp-pagination",
|
|
8552
|
+
status: "skipped",
|
|
8553
|
+
severity: "medium",
|
|
8554
|
+
durationMs: Date.now() - start,
|
|
8555
|
+
summary: "no PLP captured by purchase-journey step 2 AND no home page to discover one from — skipping",
|
|
8556
|
+
issues: []
|
|
8557
|
+
};
|
|
8558
|
+
}
|
|
8559
|
+
const data = {};
|
|
8560
|
+
for (const [side, plp] of [
|
|
8561
|
+
["prod", prodPlp],
|
|
8562
|
+
["cand", candPlp]
|
|
8563
|
+
]) {
|
|
8564
|
+
if (!plp)
|
|
8565
|
+
continue;
|
|
8566
|
+
const page1 = await fetchPlpProducts(plp);
|
|
8567
|
+
const page2 = await fetchPlpProducts(withPage(plp, 2));
|
|
8568
|
+
const page3 = await fetchPlpProducts(withPage(plp, 3));
|
|
8569
|
+
data[`${side}_page_counts`] = {
|
|
8570
|
+
page1: { status: page1.status, products: page1.productPaths.length },
|
|
8571
|
+
page2: { status: page2.status, products: page2.productPaths.length },
|
|
8572
|
+
page3: { status: page3.status, products: page3.productPaths.length }
|
|
8573
|
+
};
|
|
8574
|
+
for (const [n, result] of [
|
|
8575
|
+
[2, page2],
|
|
8576
|
+
[3, page3]
|
|
8577
|
+
]) {
|
|
8578
|
+
if (result.status !== 200) {
|
|
8579
|
+
issues.push({
|
|
8580
|
+
id: `plp-pagination:${side}:page${n}:status-${result.status}`,
|
|
8581
|
+
severity: "high",
|
|
8582
|
+
category: "functional",
|
|
8583
|
+
check: "plp-pagination",
|
|
8584
|
+
summary: `[${side}] page=${n} returned HTTP ${result.status} (expected 200) — pagination breaks under ?page=${n}`,
|
|
8585
|
+
page: withPage(plp, n)
|
|
8586
|
+
});
|
|
8587
|
+
}
|
|
8588
|
+
}
|
|
8589
|
+
if (page1.productPaths.length > 0 && page2.status === 200) {
|
|
8590
|
+
const overlap = setOverlap(page1.productPaths, page2.productPaths);
|
|
8591
|
+
if (overlap > 0.9) {
|
|
8592
|
+
issues.push({
|
|
8593
|
+
id: `plp-pagination:${side}:page2-identical`,
|
|
8594
|
+
severity: "critical",
|
|
8595
|
+
category: "functional",
|
|
8596
|
+
check: "plp-pagination",
|
|
8597
|
+
summary: `[${side}] page=2 shows the same ${page1.productPaths.length} products as page=1 (${(overlap * 100).toFixed(0)}% overlap) — ?page=N is being ignored`,
|
|
8598
|
+
page: withPage(plp, 2),
|
|
8599
|
+
details: `Page 1 first product: ${page1.productPaths[0] ?? "(none)"}
|
|
8600
|
+
Page 2 first product: ${page2.productPaths[0] ?? "(none)"}`
|
|
8601
|
+
});
|
|
8602
|
+
}
|
|
8603
|
+
}
|
|
8604
|
+
if (page2.productPaths.length > 0 && page3.status === 200) {
|
|
8605
|
+
const overlap = setOverlap(page2.productPaths, page3.productPaths);
|
|
8606
|
+
if (overlap > 0.9) {
|
|
8607
|
+
issues.push({
|
|
8608
|
+
id: `plp-pagination:${side}:page3-identical`,
|
|
8609
|
+
severity: "critical",
|
|
8610
|
+
category: "functional",
|
|
8611
|
+
check: "plp-pagination",
|
|
8612
|
+
summary: `[${side}] page=3 shows the same products as page=2 (${(overlap * 100).toFixed(0)}% overlap) — pagination capped or broken`,
|
|
8613
|
+
page: withPage(plp, 3)
|
|
8614
|
+
});
|
|
8615
|
+
}
|
|
8616
|
+
}
|
|
8617
|
+
}
|
|
8618
|
+
if (prodPlp && candPlp) {
|
|
8619
|
+
const prodCounts = data.prod_page_counts;
|
|
8620
|
+
const candCounts = data.cand_page_counts;
|
|
8621
|
+
if (prodCounts && candCounts) {
|
|
8622
|
+
const p = prodCounts.page2.products;
|
|
8623
|
+
const c = candCounts.page2.products;
|
|
8624
|
+
if (p > 0 && Math.abs(p - c) / p > 0.3) {
|
|
8625
|
+
issues.push({
|
|
8626
|
+
id: "plp-pagination:cross-side-count-divergence",
|
|
8627
|
+
severity: "medium",
|
|
8628
|
+
category: "functional",
|
|
8629
|
+
check: "plp-pagination",
|
|
8630
|
+
summary: `page=2 product count diverges: prod=${p}, cand=${c} (Δ ${Math.abs(p - c)}, >30%)`
|
|
8631
|
+
});
|
|
8632
|
+
}
|
|
8633
|
+
}
|
|
8634
|
+
}
|
|
8635
|
+
const status = issues.some((i) => i.severity === "critical") ? "fail" : issues.length > 0 ? "fail" : "pass";
|
|
8636
|
+
return {
|
|
8637
|
+
name: "plp-pagination",
|
|
8638
|
+
status,
|
|
8639
|
+
severity: "high",
|
|
8640
|
+
durationMs: Date.now() - start,
|
|
8641
|
+
summary: issues.length === 0 ? `PLP pagination working on both sides (tested page=2 + page=3)` : `${issues.length} pagination issue(s) — see details`,
|
|
8642
|
+
issues,
|
|
8643
|
+
data
|
|
8644
|
+
};
|
|
8645
|
+
}
|
|
8646
|
+
async function fetchPlpProducts(url) {
|
|
8647
|
+
try {
|
|
8648
|
+
const res = await fetch(url, {
|
|
8649
|
+
headers: {
|
|
8650
|
+
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"
|
|
8651
|
+
},
|
|
8652
|
+
signal: AbortSignal.timeout(15000)
|
|
8653
|
+
});
|
|
8654
|
+
if (res.status !== 200) {
|
|
8655
|
+
return { status: res.status, productPaths: [] };
|
|
8656
|
+
}
|
|
8657
|
+
const html = await res.text();
|
|
8658
|
+
const paths = new Set;
|
|
8659
|
+
const re = /href="([^"]+\/p(?:\?[^"]*|\/[^"]+|))"/gi;
|
|
8660
|
+
let m;
|
|
8661
|
+
while (m = re.exec(html)) {
|
|
8662
|
+
const path = m[1];
|
|
8663
|
+
if (!path)
|
|
8664
|
+
continue;
|
|
8665
|
+
const normalized = path.split("?")[0].replace(/\/$/, "");
|
|
8666
|
+
paths.add(normalized);
|
|
8667
|
+
}
|
|
8668
|
+
const re2 = /href="([^"]+\/products\/[^"]+)"/gi;
|
|
8669
|
+
while (m = re2.exec(html)) {
|
|
8670
|
+
const path = m[1];
|
|
8671
|
+
if (path)
|
|
8672
|
+
paths.add(path.split("?")[0].replace(/\/$/, ""));
|
|
8673
|
+
}
|
|
8674
|
+
return { status: 200, productPaths: Array.from(paths) };
|
|
8675
|
+
} catch (err) {
|
|
8676
|
+
return { status: 0, productPaths: [] };
|
|
8677
|
+
}
|
|
8678
|
+
}
|
|
8679
|
+
function withPage(url, n) {
|
|
8680
|
+
try {
|
|
8681
|
+
const u = new URL(url);
|
|
8682
|
+
u.searchParams.set("page", String(n));
|
|
8683
|
+
return u.toString();
|
|
8684
|
+
} catch {
|
|
8685
|
+
const sep = url.includes("?") ? "&" : "?";
|
|
8686
|
+
return `${url}${sep}page=${n}`;
|
|
8687
|
+
}
|
|
8688
|
+
}
|
|
8689
|
+
async function discoverPlpFromHome(homeUrl) {
|
|
8690
|
+
try {
|
|
8691
|
+
const res = await fetch(homeUrl, {
|
|
8692
|
+
headers: {
|
|
8693
|
+
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"
|
|
8694
|
+
},
|
|
8695
|
+
signal: AbortSignal.timeout(15000)
|
|
8696
|
+
});
|
|
8697
|
+
if (!res.ok)
|
|
8698
|
+
return null;
|
|
8699
|
+
const html = await res.text();
|
|
8700
|
+
const candidates = new Set;
|
|
8701
|
+
const re = /href="([^"]+)"/gi;
|
|
8702
|
+
let m;
|
|
8703
|
+
while (m = re.exec(html)) {
|
|
8704
|
+
const href = m[1];
|
|
8705
|
+
if (!href)
|
|
8706
|
+
continue;
|
|
8707
|
+
if (/^https?:\/\//.test(href) || href.startsWith("#") || href === "/" || /\/(p|cart|carrinho|checkout|account|conta|login|wishlist|favoritos)(\/|$|\?)/i.test(href) || /\.(jpg|png|webp|css|js|svg|ico|woff)/i.test(href)) {
|
|
8708
|
+
continue;
|
|
8709
|
+
}
|
|
8710
|
+
const segments = href.split("?")[0].split("/").filter(Boolean);
|
|
8711
|
+
if (segments.length === 0 || segments.length > 3)
|
|
8712
|
+
continue;
|
|
8713
|
+
candidates.add(href.split("?")[0]);
|
|
8714
|
+
}
|
|
8715
|
+
const sorted = Array.from(candidates).sort((a, b) => a.length - b.length);
|
|
8716
|
+
const home = new URL(homeUrl);
|
|
8717
|
+
const pick = sorted[0];
|
|
8718
|
+
if (!pick)
|
|
8719
|
+
return null;
|
|
8720
|
+
return new URL(pick, home).toString();
|
|
8721
|
+
} catch {
|
|
8722
|
+
return null;
|
|
8723
|
+
}
|
|
8724
|
+
}
|
|
8725
|
+
function pickPlpUrl(flows) {
|
|
8726
|
+
for (const fc of flows) {
|
|
8727
|
+
if (fc.flow !== "purchase-journey")
|
|
8728
|
+
continue;
|
|
8729
|
+
const plpStep = fc.steps?.find((s) => s.name === "navigate-plp" && s.status === "ok");
|
|
8730
|
+
if (plpStep?.url)
|
|
8731
|
+
return plpStep.url;
|
|
8732
|
+
const plpPage = fc.pages.find((p) => {
|
|
8733
|
+
try {
|
|
8734
|
+
const u = new URL(p.url);
|
|
8735
|
+
return u.pathname.split("/").filter(Boolean).length <= 2 && !/\/p$|\/products\//.test(u.pathname);
|
|
8736
|
+
} catch {
|
|
8737
|
+
return false;
|
|
8738
|
+
}
|
|
8739
|
+
});
|
|
8740
|
+
if (plpPage)
|
|
8741
|
+
return plpPage.url;
|
|
8742
|
+
}
|
|
8743
|
+
return null;
|
|
8744
|
+
}
|
|
8745
|
+
function setOverlap(a, b) {
|
|
8746
|
+
if (a.length === 0 || b.length === 0)
|
|
8747
|
+
return 0;
|
|
8748
|
+
const sa = new Set(a);
|
|
8749
|
+
let common = 0;
|
|
8750
|
+
for (const x of b)
|
|
8751
|
+
if (sa.has(x))
|
|
8752
|
+
common++;
|
|
8753
|
+
return common / Math.max(a.length, b.length);
|
|
8754
|
+
}
|
|
8755
|
+
|
|
8537
8756
|
// src/checks/purchase-journey-flow.ts
|
|
8538
8757
|
var STEP_LABELS3 = {
|
|
8539
8758
|
"visit-home": "Visitar home",
|
|
@@ -10515,7 +10734,8 @@ var ALL_CHECKS = [
|
|
|
10515
10734
|
pdpGalleryRelated,
|
|
10516
10735
|
footerLinksHealth,
|
|
10517
10736
|
loginFlow,
|
|
10518
|
-
pictureMissingDims
|
|
10737
|
+
pictureMissingDims,
|
|
10738
|
+
plpPagination
|
|
10519
10739
|
];
|
|
10520
10740
|
var ALL_CHECKS_BY_NAME = {
|
|
10521
10741
|
"http-status-parity": httpStatusParity,
|
|
@@ -10542,7 +10762,8 @@ var ALL_CHECKS_BY_NAME = {
|
|
|
10542
10762
|
"pdp-gallery-related": pdpGalleryRelated,
|
|
10543
10763
|
"footer-links-health": footerLinksHealth,
|
|
10544
10764
|
"login-flow": loginFlow,
|
|
10545
|
-
"picture-missing-dims": pictureMissingDims
|
|
10765
|
+
"picture-missing-dims": pictureMissingDims,
|
|
10766
|
+
"plp-pagination": plpPagination
|
|
10546
10767
|
};
|
|
10547
10768
|
var FLOW_DEPENDENT_CHECKS = new Set([
|
|
10548
10769
|
"purchase-journey-flow",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/parity",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.6",
|
|
4
4
|
"description": "E2E parity validator for site migrations. Compares prod vs cand and reports UI, functional, SEO, visual, and Web Vitals deltas with an LLM-ranked HTML report.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|