@decocms/parity 0.15.0 → 0.16.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/cli.js +51 -15
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.16.0](https://github.com/decocms/parity/compare/v0.15.1...v0.16.0) (2026-07-28)
11
+
12
+ ### Added
13
+
14
+ * **Configurable `minicartPanel` selector for open-minicart reveal detection (issue #149).** `isCartUiVisible` and the title-scope sweep in `validateCartContainsTitleQuick` used a hardcoded list of name-based patterns (`[role='dialog']`, `[class*='minicart']`, etc.) to detect that the cart drawer was open. Any site built with utility-CSS (Tailwind) and `data-qa-*` testing attributes — a common modern stack — has no class/attribute those patterns can match, so detection always returns `null` and the step reports "failed" even when the drawer is genuinely open. The new `minicartPanel` selector key (`.parityrc.json`) is tried first, ahead of all hardcoded patterns. Built-in defaults cover `[data-qa-minicart]`, `[data-minicart]`, `[data-minicart-drawer]`, `[data-testid='minicart']` and common class patterns — so the `data-qa-*` case is handled out-of-the-box. Override with a single selector that matches the drawer root for your specific site.
15
+
16
+ ### Fixed
17
+
18
+ * **`dismissOverlays` stall in `openMinicart` (issue #151).** The named-selector sweep inside `dismissOverlays` used a 400ms per-selector cap; with 12+ selectors none of which match, that's ~5s minimum before `openMinicart` could proceed — and on heavy pages the CDP calls could stall much longer, causing 50-100s gaps with zero debug output. Fixed by: (1) tightening the per-selector probe to 80ms (fast-failing `count()=0` before any `isVisible` call), (2) adding `dlog` at the entry of `dismissOverlays` and `openMinicart` so slow sweeps show up in `DEBUG_PARITY=1` output, and (3) wrapping the `dismissOverlays` call inside `openMinicart` with a 4s hard cap so it degrades gracefully instead of silently consuming the step budget.
19
+
10
20
  ## [0.15.0](https://github.com/decocms/parity/compare/v0.14.0...v0.15.0) (2026-07-27)
11
21
 
12
22
  ### Added
package/dist/cli.js CHANGED
@@ -41389,6 +41389,7 @@ var ParityRc = z.object({
41389
41389
  quantityInput: z.string().optional(),
41390
41390
  minicartCount: z.string().optional(),
41391
41391
  cartOpenedIndicator: z.string().optional(),
41392
+ minicartPanel: z.string().optional(),
41392
41393
  searchTrigger: z.string().optional(),
41393
41394
  searchInput: z.string().optional(),
41394
41395
  searchSuggestions: z.string().optional(),
@@ -49545,6 +49546,7 @@ var SelectorKey = z2.enum([
49545
49546
  "quantityInput",
49546
49547
  "minicartCount",
49547
49548
  "cartOpenedIndicator",
49549
+ "minicartPanel",
49548
49550
  "searchTrigger",
49549
49551
  "searchInput",
49550
49552
  "searchSuggestions",
@@ -49949,6 +49951,17 @@ var DEFAULT_SELECTORS = {
49949
49951
  "[role='dialog']:has-text('adicionado')",
49950
49952
  "[role='dialog']:has-text('added')"
49951
49953
  ],
49954
+ minicartPanel: [
49955
+ "[data-qa-minicart]",
49956
+ "[data-minicart]",
49957
+ "[data-minicart-drawer]",
49958
+ "[data-drawer='minicart']",
49959
+ "[data-testid='minicart']",
49960
+ "[data-testid='cart-drawer']",
49961
+ "[class*='minicart' i]",
49962
+ "[class*='cart-drawer' i]",
49963
+ "[class*='drawer-cart' i]"
49964
+ ],
49952
49965
  cartItemRow: [
49953
49966
  "[data-cart-item]",
49954
49967
  "[data-testid='cart-item']",
@@ -50706,15 +50719,21 @@ async function dismissBlockingOverlay(page, ctx, target) {
50706
50719
  function describe(b) {
50707
50720
  return `${b.tag}${b.id ? `#${b.id}` : ""}${b.className ? `.${b.className.split(/\s+/)[0]}` : ""}`;
50708
50721
  }
50722
+ var OVERLAY_PROBE_CAP_MS = 80;
50709
50723
  async function dismissOverlays(page, ctx) {
50724
+ const sels = overlaySelectorsFor(ctx.rc);
50725
+ dlog2(ctx, ` dismissOverlays: checking ${sels.length} selector(s)…`);
50710
50726
  const dismissals = [];
50711
- for (const sel of overlaySelectorsFor(ctx.rc)) {
50727
+ for (const sel of sels) {
50712
50728
  try {
50713
50729
  const overlay = page.locator(sel).first();
50714
- if (!await withCap(overlay.isVisible({ timeout: 200 }).catch(() => false), 400, false))
50730
+ const cnt = await withCap(overlay.count(), OVERLAY_PROBE_CAP_MS, 0);
50731
+ if (cnt === 0)
50732
+ continue;
50733
+ if (!await withCap(overlay.isVisible({ timeout: OVERLAY_PROBE_CAP_MS }).catch(() => false), OVERLAY_PROBE_CAP_MS, false))
50715
50734
  continue;
50716
50735
  const closer = overlay.locator(CLOSE_BUTTON_SELECTOR).first();
50717
- if (await withCap(closer.isVisible({ timeout: 200 }).catch(() => false), 400, false)) {
50736
+ if (await withCap(closer.isVisible({ timeout: OVERLAY_PROBE_CAP_MS }).catch(() => false), OVERLAY_PROBE_CAP_MS, false)) {
50718
50737
  await closer.click({ timeout: 1500 }).catch(() => {
50719
50738
  return;
50720
50739
  });
@@ -50750,8 +50769,10 @@ async function readCartCount(page, ctx) {
50750
50769
  }
50751
50770
  return 0;
50752
50771
  }
50753
- async function isCartUiVisible(page) {
50772
+ async function isCartUiVisible(page, ctx) {
50773
+ const configured = ctx ? [...selFor(ctx, "minicartPanel"), ...selFor(ctx, "cartOpenedIndicator")] : [];
50754
50774
  return firstVisible(page, [
50775
+ ...configured,
50755
50776
  "[role='dialog']:visible",
50756
50777
  "[aria-modal='true']:visible",
50757
50778
  "[data-minicart][aria-hidden='false']",
@@ -50837,16 +50858,17 @@ async function detectCartRevealMode(page, trigger, drawerAlreadyOpen, viewport)
50837
50858
  return "click-drawer";
50838
50859
  return "unknown";
50839
50860
  }
50840
- async function isCartRevealed(page, expectedProductTitle) {
50861
+ async function isCartRevealed(page, expectedProductTitle, ctx) {
50841
50862
  if (expectedProductTitle) {
50842
- const v = await validateCartContainsTitleQuick(page, expectedProductTitle);
50863
+ const v = await validateCartContainsTitleQuick(page, expectedProductTitle, ctx);
50843
50864
  if (v)
50844
50865
  return `title-found:${v}`;
50845
50866
  }
50846
- return isCartUiVisible(page);
50867
+ return isCartUiVisible(page, ctx);
50847
50868
  }
50848
- async function validateCartContainsTitleQuick(page, expectedTitle) {
50869
+ async function validateCartContainsTitleQuick(page, expectedTitle, ctx) {
50849
50870
  const quickSelectors = [
50871
+ ...ctx ? selFor(ctx, "minicartPanel") : [],
50850
50872
  "[role='dialog']",
50851
50873
  "[class*='minicart' i]",
50852
50874
  "[class*='cart' i]",
@@ -50881,9 +50903,14 @@ async function waitForCartHydration(page) {
50881
50903
  }
50882
50904
  async function openMinicart(page, trigger, ctx, expectedProductTitle) {
50883
50905
  const beforeUrl = page.url();
50884
- await dismissOverlays(page, ctx);
50906
+ dlog2(ctx, ` openMinicart: starting — trigger=${trigger.selector} title=${expectedProductTitle?.slice(0, 40) ?? "none"}`);
50907
+ await Promise.race([
50908
+ dismissOverlays(page, ctx),
50909
+ new Promise((resolve) => setTimeout(resolve, 4000))
50910
+ ]);
50911
+ dlog2(ctx, " openMinicart: dismissOverlays done — checking alreadyOpen…");
50885
50912
  await page.waitForTimeout(800);
50886
- const alreadyOpen = await isCartRevealed(page, expectedProductTitle);
50913
+ const alreadyOpen = await isCartRevealed(page, expectedProductTitle, ctx);
50887
50914
  if (alreadyOpen) {
50888
50915
  dlog2(ctx, ` openMinicart: already-open (matched ${alreadyOpen})`);
50889
50916
  return { method: "already-open", url: beforeUrl, visibleMarker: alreadyOpen };
@@ -50896,7 +50923,7 @@ async function openMinicart(page, trigger, ctx, expectedProductTitle) {
50896
50923
  return;
50897
50924
  });
50898
50925
  await page.waitForTimeout(1500);
50899
- const hoverOpened = await isCartRevealed(page, expectedProductTitle);
50926
+ const hoverOpened = await isCartRevealed(page, expectedProductTitle, ctx);
50900
50927
  if (hoverOpened) {
50901
50928
  dlog2(ctx, ` openMinicart: hover opened drawer (${hoverOpened})`);
50902
50929
  return { method: "hover", url: page.url(), visibleMarker: hoverOpened };
@@ -50921,7 +50948,7 @@ async function openMinicart(page, trigger, ctx, expectedProductTitle) {
50921
50948
  return { method: "click-navigate", url: page.url(), visibleMarker: null };
50922
50949
  }
50923
50950
  await page.waitForTimeout(800);
50924
- const tapOpened = await isCartRevealed(page, expectedProductTitle);
50951
+ const tapOpened = await isCartRevealed(page, expectedProductTitle, ctx);
50925
50952
  if (tapOpened) {
50926
50953
  dlog2(ctx, ` openMinicart: tap opened drawer (${tapOpened})`);
50927
50954
  return { method: "click", url: page.url(), visibleMarker: tapOpened };
@@ -50946,7 +50973,7 @@ async function openMinicart(page, trigger, ctx, expectedProductTitle) {
50946
50973
  return { method: "click-navigate", url: page.url(), visibleMarker: null };
50947
50974
  }
50948
50975
  await page.waitForTimeout(1500);
50949
- const clickOpened = await isCartRevealed(page, expectedProductTitle);
50976
+ const clickOpened = await isCartRevealed(page, expectedProductTitle, ctx);
50950
50977
  if (clickOpened) {
50951
50978
  dlog2(ctx, ` openMinicart: click opened drawer (${clickOpened})`);
50952
50979
  return { method: "click", url: afterClickUrl, visibleMarker: clickOpened };
@@ -50957,7 +50984,7 @@ async function openMinicart(page, trigger, ctx, expectedProductTitle) {
50957
50984
  return;
50958
50985
  });
50959
50986
  await page.waitForTimeout(1500);
50960
- const hoverOpened = await isCartRevealed(page, expectedProductTitle);
50987
+ const hoverOpened = await isCartRevealed(page, expectedProductTitle, ctx);
50961
50988
  if (hoverOpened) {
50962
50989
  dlog2(ctx, ` openMinicart: hover opened drawer (${hoverOpened})`);
50963
50990
  return { method: "hover", url: page.url(), visibleMarker: hoverOpened };
@@ -51003,7 +51030,16 @@ function titlesMatch(observed, expected) {
51003
51030
  return false;
51004
51031
  }
51005
51032
  async function validateCartContainsTitle(page, expectedTitle, ctx) {
51033
+ const panelScopedTitleSelectors = selFor(ctx, "minicartPanel").flatMap((p) => [
51034
+ `${p} a[href*='/p']`,
51035
+ `${p} [class*='name' i]`,
51036
+ `${p} [class*='title' i]`,
51037
+ `${p} [class*='product' i]`,
51038
+ `${p} [data-cart-item-name]`,
51039
+ `${p} [data-qa-product-name]`
51040
+ ]);
51006
51041
  const titleSelectors = [
51042
+ ...panelScopedTitleSelectors,
51007
51043
  "[data-cart-item-name]",
51008
51044
  "[data-cart-item] [class*='title' i]",
51009
51045
  "[data-cart-item] [class*='name' i]",
@@ -51861,7 +51897,7 @@ async function flowPurchaseJourney(ctx) {
51861
51897
  let cartOpenMethod = "failed";
51862
51898
  let miniText = "";
51863
51899
  let cartRevealMode = "unknown";
51864
- const drawerAlreadyOpen = await isCartRevealed(page, expectedProductTitle) !== null;
51900
+ const drawerAlreadyOpen = await isCartRevealed(page, expectedProductTitle, ctx) !== null;
51865
51901
  if (miniHit) {
51866
51902
  miniText = await miniHit.locator.innerText().catch(() => "");
51867
51903
  cartRevealMode = await detectCartRevealMode(page, miniHit.locator, drawerAlreadyOpen, ctx.viewport).catch(() => "unknown");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/parity",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
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",