@decocms/parity 0.14.0 → 0.15.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/dist/cli.js +237 -30
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -9,9 +9,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
9
9
 
10
10
  ### Added
11
11
 
12
+ * **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.
13
+
14
+ ## [0.15.0](https://github.com/decocms/parity/compare/v0.14.0...v0.15.0) (2026-07-27)
15
+
16
+ ### Added
17
+
18
+ * **Structural overlay interception detection + configurable overlay selectors (issues #145, #146).** A "no-signal" add-to-cart failure is often not a broken cart but a newsletter/discount modal intercepting the click — classically one that pops on the very `mousemove` Playwright's `.click()` dispatches, in a cookie-less session (e.g. a `div.fixed bg-black/50 z-9999` backdrop over the buy button). Instead of name-matching every popup shape (whack-a-mole), parity now detects interception **structurally**: before retrying, it checks whether the buy button is still the topmost element at its click point (`document.elementFromPoint`) and, if something covers it, dismisses it least-destructive-first — Escape → a named close button → an icon/geometry close control inside the overlay (catches unnamed `<button><svg/></button>` X's) → a corner backdrop click → and, guarded, hiding the confirmed interceptor as a last resort (never an ancestor of the target, and it can't fake success — the step still needs a real confirmation signal) — then retries once. What was detected/dismissed is recorded in the step's `detail.overlayDismissed` and surfaced in the action text, so a report shows *why* a click was intercepted even when dismissal succeeded. Separately, `.parityrc.json` `overlaySelectors` adds explicit site-specific overlay selectors, merged with the built-in cookie/toast/newsletter defaults (never replacing them), as a fast-path override.
19
+
20
+ ## [0.14.0](https://github.com/decocms/parity/compare/v0.13.0...v0.14.0) (2026-07-27)
21
+
22
+ ### Added
23
+
24
+ * **Configurable add-to-cart confirmation deadline (issue #143).** The purchase-journey / `e2e` add-to-cart step polled a hardcoded 3000ms for a success signal (URL→cart, minicart count increase, drawer open, success toast). On sites whose success toast is short-lived, or with slow TTFB / popup overlays, that could race the deadline and report a false "no signal" failure on an add-to-cart that actually worked. Now tunable via `.parityrc.json` `addToCartConfirmMs` or `--add-to-cart-timeout <ms>` (on `parity run` and `parity e2e`); a non-positive/NaN override is ignored and falls back to the 3000ms default.
25
+
26
+ ## [0.13.0](https://github.com/decocms/parity/compare/v0.12.0...v0.13.0) (2026-07-27)
27
+
28
+ ### Added
29
+
12
30
  * **`parity e2e` now automates selectors like `parity run` (issue #141).** Single-site runs previously saw only `DEFAULT_SELECTORS` + hand-written `.parityrc.json` — `e2e` never detected the platform, never ran LLM selector discovery, and never learned from its flow runs (so `learned-selectors.json`, keyed by platform, never applied). It now detects the platform, runs the same grounded LLM discovery + live-validation pass, threads the platform into every flow, and promotes selectors learned from real successful interactions. New flags mirror `run`: `--no-auto-selectors`, `--refresh-selectors`, `--no-learn`. The discovery pass is now shared code (`src/engine/selector-discovery-pass.ts`) used by both commands.
13
31
  * **Discovery covers the journey variant/quantity keys.** LLM selector discovery now infers `variantRow`, `quantityIncrement`, `quantityInput`, `sizeSwatch`, and `colorSwatch` (grounded on the real PDP and live-validated) — exactly the keys single-site users kept hand-writing in `.parityrc.json`.
14
- * **Configurable add-to-cart confirmation deadline (issue #143).** The purchase-journey / `e2e` add-to-cart step polled a hardcoded 3000ms for a success signal (URL→cart, minicart count increase, drawer open, success toast). On sites whose success toast is short-lived, or with slow TTFB / popup overlays, that could race the deadline and report a false "no signal" failure on an add-to-cart that actually worked. Now tunable via `.parityrc.json` `addToCartConfirmMs` or `--add-to-cart-timeout <ms>` (on `parity run` and `parity e2e`); a non-positive/NaN override is ignored and falls back to the 3000ms default.
15
32
 
16
33
  ### Fixed
17
34
 
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(),
@@ -41434,6 +41435,7 @@ var ParityRc = z.object({
41434
41435
  }).optional(),
41435
41436
  serverFnFloodBudget: z.number().optional(),
41436
41437
  serverFnPattern: z.string().optional(),
41438
+ overlaySelectors: z.array(z.string()).optional(),
41437
41439
  addToCartConfirmMs: z.number().optional()
41438
41440
  });
41439
41441
  var ParityIgnore = z.object({
@@ -49544,6 +49546,7 @@ var SelectorKey = z2.enum([
49544
49546
  "quantityInput",
49545
49547
  "minicartCount",
49546
49548
  "cartOpenedIndicator",
49549
+ "minicartPanel",
49547
49550
  "searchTrigger",
49548
49551
  "searchInput",
49549
49552
  "searchSuggestions",
@@ -49948,6 +49951,17 @@ var DEFAULT_SELECTORS = {
49948
49951
  "[role='dialog']:has-text('adicionado')",
49949
49952
  "[role='dialog']:has-text('added')"
49950
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
+ ],
49951
49965
  cartItemRow: [
49952
49966
  "[data-cart-item]",
49953
49967
  "[data-testid='cart-item']",
@@ -50531,41 +50545,206 @@ async function extractProductTitle(page) {
50531
50545
  return docTitle.trim();
50532
50546
  return null;
50533
50547
  }
50548
+ var DEFAULT_OVERLAY_SELECTORS = [
50549
+ "[class*='cookie' i][class*='banner' i]",
50550
+ "[class*='cookie' i][class*='consent' i]",
50551
+ "[id*='cookie' i][class*='banner' i]",
50552
+ "[role='alertdialog']:visible",
50553
+ "[class*='toast' i]:visible",
50554
+ "[class*='snackbar' i]:visible",
50555
+ "[class*='added-to-cart' i]:visible",
50556
+ "[class*='product-added' i]:visible",
50557
+ "[id*='newsletter' i]:visible",
50558
+ "[class*='newsletter' i]:visible",
50559
+ "[id*='signup-popup' i]:visible",
50560
+ "[class*='signup' i][class*='popup' i]:visible"
50561
+ ];
50562
+ var CLOSE_BUTTON_SELECTOR = "button[aria-label*='close' i], [aria-label*='close' i], button[aria-label*='fechar' i], [aria-label*='fechar' i], [data-close], [data-dismiss], [data-qa-dismiss], button[class*='close' i], button:has-text('×'), button:has-text('✕')";
50563
+ function overlaySelectorsFor(rc) {
50564
+ const extra = rc?.overlaySelectors ?? [];
50565
+ return [...new Set([...DEFAULT_OVERLAY_SELECTORS, ...extra])];
50566
+ }
50567
+ async function blockerAtTarget(target) {
50568
+ return await withCap(target.evaluate((el) => {
50569
+ const node2 = el;
50570
+ const rect = node2.getBoundingClientRect();
50571
+ if (rect.width === 0 || rect.height === 0)
50572
+ return null;
50573
+ const cx = rect.left + rect.width / 2;
50574
+ const cy = rect.top + rect.height / 2;
50575
+ const top = document.elementFromPoint(cx, cy);
50576
+ if (!top || top === node2 || node2.contains(top))
50577
+ return null;
50578
+ let cur = top;
50579
+ let overlay = top;
50580
+ while (cur && cur !== document.body) {
50581
+ const pos = getComputedStyle(cur).position;
50582
+ if (pos === "fixed" || pos === "sticky" || pos === "absolute")
50583
+ overlay = cur;
50584
+ cur = cur.parentElement;
50585
+ }
50586
+ const r = overlay.getBoundingClientRect();
50587
+ const coverage = r.width * r.height / (window.innerWidth * window.innerHeight);
50588
+ const cls = typeof overlay.className === "string" ? overlay.className : "";
50589
+ return {
50590
+ tag: overlay.tagName.toLowerCase(),
50591
+ id: overlay.id || undefined,
50592
+ className: cls ? cls.slice(0, 120) : undefined,
50593
+ fullViewport: coverage > 0.6
50594
+ };
50595
+ }).catch(() => null), 2000, null);
50596
+ }
50597
+ async function findOverlayCloseControl(target) {
50598
+ return await withCap(target.evaluate((el) => {
50599
+ const node2 = el;
50600
+ const rect = node2.getBoundingClientRect();
50601
+ if (rect.width === 0 || rect.height === 0)
50602
+ return null;
50603
+ const cx = rect.left + rect.width / 2;
50604
+ const cy = rect.top + rect.height / 2;
50605
+ const top = document.elementFromPoint(cx, cy);
50606
+ if (!top || top === node2 || node2.contains(top))
50607
+ return null;
50608
+ let cur = top;
50609
+ let overlay = top;
50610
+ while (cur && cur !== document.body) {
50611
+ const pos = getComputedStyle(cur).position;
50612
+ if (pos === "fixed" || pos === "sticky" || pos === "absolute")
50613
+ overlay = cur;
50614
+ cur = cur.parentElement;
50615
+ }
50616
+ const orect = overlay.getBoundingClientRect();
50617
+ const clickables = Array.from(overlay.querySelectorAll("button, a, [role='button']"));
50618
+ let best = null;
50619
+ for (const c of clickables) {
50620
+ const r = c.getBoundingClientRect();
50621
+ if (r.width === 0 || r.height === 0 || r.width > 72 || r.height > 72)
50622
+ continue;
50623
+ const label = `${c.getAttribute("aria-label") ?? ""} ${typeof c.className === "string" ? c.className : ""} ${c.textContent ?? ""}`.toLowerCase();
50624
+ const named = /close|fechar|dismiss|×|✕/.test(label);
50625
+ const distTopRight = Math.hypot(orect.right - r.right, r.top - orect.top);
50626
+ const score = (named ? 0 : 1e5) + distTopRight;
50627
+ if (!best || score < best.score) {
50628
+ best = { x: r.left + r.width / 2, y: r.top + r.height / 2, score };
50629
+ }
50630
+ }
50631
+ return best ? { x: best.x, y: best.y } : null;
50632
+ }).catch(() => null), 2000, null);
50633
+ }
50634
+ async function neutralizeBlockerAtTarget(target) {
50635
+ return await withCap(target.evaluate((el) => {
50636
+ const node2 = el;
50637
+ const rect = node2.getBoundingClientRect();
50638
+ if (rect.width === 0 || rect.height === 0)
50639
+ return false;
50640
+ const cx = rect.left + rect.width / 2;
50641
+ const cy = rect.top + rect.height / 2;
50642
+ const top = document.elementFromPoint(cx, cy);
50643
+ if (!top || top === node2 || node2.contains(top))
50644
+ return false;
50645
+ let cur = top;
50646
+ let overlay = top;
50647
+ while (cur && cur !== document.body) {
50648
+ const pos = getComputedStyle(cur).position;
50649
+ if (pos === "fixed" || pos === "sticky" || pos === "absolute")
50650
+ overlay = cur;
50651
+ cur = cur.parentElement;
50652
+ }
50653
+ if (overlay.contains(node2))
50654
+ return false;
50655
+ overlay.style.setProperty("display", "none", "important");
50656
+ return true;
50657
+ }).catch(() => false), 2000, false);
50658
+ }
50659
+ async function dismissBlockingOverlay(page, ctx, target) {
50660
+ const blocker = await blockerAtTarget(target);
50661
+ if (!blocker)
50662
+ return null;
50663
+ const base = {
50664
+ reason: "click-point-intercepted",
50665
+ tag: blocker.tag,
50666
+ id: blocker.id,
50667
+ className: blocker.className,
50668
+ fullViewport: blocker.fullViewport
50669
+ };
50670
+ const cleared = async () => await blockerAtTarget(target) === null;
50671
+ await page.keyboard.press("Escape").catch(() => {
50672
+ return;
50673
+ });
50674
+ await page.waitForTimeout(300);
50675
+ if (await cleared()) {
50676
+ dlog2(ctx, ` overlay: dismissed via Escape (${describe(blocker)})`);
50677
+ return { ...base, method: "escape", dismissed: true };
50678
+ }
50679
+ const closer = page.locator(CLOSE_BUTTON_SELECTOR).first();
50680
+ if (await withCap(closer.isVisible({ timeout: 300 }).catch(() => false), 500, false)) {
50681
+ await closer.click({ timeout: 1500 }).catch(() => {
50682
+ return;
50683
+ });
50684
+ await page.waitForTimeout(300);
50685
+ if (await cleared()) {
50686
+ dlog2(ctx, ` overlay: dismissed via close button (${describe(blocker)})`);
50687
+ return { ...base, method: "close-button", dismissed: true };
50688
+ }
50689
+ }
50690
+ const closeXY = await findOverlayCloseControl(target);
50691
+ if (closeXY) {
50692
+ await page.mouse.click(closeXY.x, closeXY.y).catch(() => {
50693
+ return;
50694
+ });
50695
+ await page.waitForTimeout(300);
50696
+ if (await cleared()) {
50697
+ dlog2(ctx, ` overlay: dismissed via icon close control (${describe(blocker)})`);
50698
+ return { ...base, method: "close-button", dismissed: true };
50699
+ }
50700
+ }
50701
+ await page.mouse.click(5, 5).catch(() => {
50702
+ return;
50703
+ });
50704
+ await page.waitForTimeout(300);
50705
+ if (await cleared()) {
50706
+ dlog2(ctx, ` overlay: dismissed via backdrop click (${describe(blocker)})`);
50707
+ return { ...base, method: "backdrop-click", dismissed: true };
50708
+ }
50709
+ if (await neutralizeBlockerAtTarget(target)) {
50710
+ await page.waitForTimeout(150);
50711
+ if (await cleared()) {
50712
+ dlog2(ctx, ` overlay: neutralized (hidden) as last resort (${describe(blocker)})`);
50713
+ return { ...base, method: "neutralized", dismissed: true };
50714
+ }
50715
+ }
50716
+ dlog2(ctx, ` overlay: detected but NOT dismissed (${describe(blocker)})`);
50717
+ return { ...base, method: "backdrop-click", dismissed: false };
50718
+ }
50719
+ function describe(b) {
50720
+ return `${b.tag}${b.id ? `#${b.id}` : ""}${b.className ? `.${b.className.split(/\s+/)[0]}` : ""}`;
50721
+ }
50534
50722
  async function dismissOverlays(page, ctx) {
50535
- const overlaySelectors = [
50536
- "[class*='cookie' i][class*='banner' i]",
50537
- "[class*='cookie' i][class*='consent' i]",
50538
- "[id*='cookie' i][class*='banner' i]",
50539
- "[role='alertdialog']:visible",
50540
- "[class*='toast' i]:visible",
50541
- "[class*='snackbar' i]:visible",
50542
- "[class*='added-to-cart' i]:visible",
50543
- "[class*='product-added' i]:visible"
50544
- ];
50545
- let dismissedAny = false;
50546
- for (const sel of overlaySelectors) {
50723
+ const dismissals = [];
50724
+ for (const sel of overlaySelectorsFor(ctx.rc)) {
50547
50725
  try {
50548
50726
  const overlay = page.locator(sel).first();
50549
50727
  if (!await withCap(overlay.isVisible({ timeout: 200 }).catch(() => false), 400, false))
50550
50728
  continue;
50551
- const closer = overlay.locator("button[aria-label*='close' i], button[aria-label*='fechar' i], button[class*='close' i], [data-close], [aria-label='Close']").first();
50729
+ const closer = overlay.locator(CLOSE_BUTTON_SELECTOR).first();
50552
50730
  if (await withCap(closer.isVisible({ timeout: 200 }).catch(() => false), 400, false)) {
50553
50731
  await closer.click({ timeout: 1500 }).catch(() => {
50554
50732
  return;
50555
50733
  });
50556
- dismissedAny = true;
50734
+ dismissals.push({ reason: "selector-match", tag: sel, method: "close-button", dismissed: true });
50557
50735
  continue;
50558
50736
  }
50559
50737
  await page.keyboard.press("Escape").catch(() => {
50560
50738
  return;
50561
50739
  });
50562
- dismissedAny = true;
50740
+ dismissals.push({ reason: "selector-match", tag: sel, method: "escape", dismissed: true });
50563
50741
  } catch {}
50564
50742
  }
50565
- if (dismissedAny) {
50566
- dlog2(ctx, " openMinicart: dismissed overlay(s) before interacting");
50743
+ if (dismissals.length > 0) {
50744
+ dlog2(ctx, ` dismissed ${dismissals.length} overlay(s) before interacting`);
50567
50745
  await page.waitForTimeout(500);
50568
50746
  }
50747
+ return dismissals;
50569
50748
  }
50570
50749
 
50571
50750
  // src/engine/flows/cart-helpers.ts
@@ -50584,8 +50763,10 @@ async function readCartCount(page, ctx) {
50584
50763
  }
50585
50764
  return 0;
50586
50765
  }
50587
- async function isCartUiVisible(page) {
50766
+ async function isCartUiVisible(page, ctx) {
50767
+ const configured = ctx ? [...selFor(ctx, "minicartPanel"), ...selFor(ctx, "cartOpenedIndicator")] : [];
50588
50768
  return firstVisible(page, [
50769
+ ...configured,
50589
50770
  "[role='dialog']:visible",
50590
50771
  "[aria-modal='true']:visible",
50591
50772
  "[data-minicart][aria-hidden='false']",
@@ -50671,16 +50852,17 @@ async function detectCartRevealMode(page, trigger, drawerAlreadyOpen, viewport)
50671
50852
  return "click-drawer";
50672
50853
  return "unknown";
50673
50854
  }
50674
- async function isCartRevealed(page, expectedProductTitle) {
50855
+ async function isCartRevealed(page, expectedProductTitle, ctx) {
50675
50856
  if (expectedProductTitle) {
50676
- const v = await validateCartContainsTitleQuick(page, expectedProductTitle);
50857
+ const v = await validateCartContainsTitleQuick(page, expectedProductTitle, ctx);
50677
50858
  if (v)
50678
50859
  return `title-found:${v}`;
50679
50860
  }
50680
- return isCartUiVisible(page);
50861
+ return isCartUiVisible(page, ctx);
50681
50862
  }
50682
- async function validateCartContainsTitleQuick(page, expectedTitle) {
50863
+ async function validateCartContainsTitleQuick(page, expectedTitle, ctx) {
50683
50864
  const quickSelectors = [
50865
+ ...ctx ? selFor(ctx, "minicartPanel") : [],
50684
50866
  "[role='dialog']",
50685
50867
  "[class*='minicart' i]",
50686
50868
  "[class*='cart' i]",
@@ -50717,7 +50899,7 @@ async function openMinicart(page, trigger, ctx, expectedProductTitle) {
50717
50899
  const beforeUrl = page.url();
50718
50900
  await dismissOverlays(page, ctx);
50719
50901
  await page.waitForTimeout(800);
50720
- const alreadyOpen = await isCartRevealed(page, expectedProductTitle);
50902
+ const alreadyOpen = await isCartRevealed(page, expectedProductTitle, ctx);
50721
50903
  if (alreadyOpen) {
50722
50904
  dlog2(ctx, ` openMinicart: already-open (matched ${alreadyOpen})`);
50723
50905
  return { method: "already-open", url: beforeUrl, visibleMarker: alreadyOpen };
@@ -50730,7 +50912,7 @@ async function openMinicart(page, trigger, ctx, expectedProductTitle) {
50730
50912
  return;
50731
50913
  });
50732
50914
  await page.waitForTimeout(1500);
50733
- const hoverOpened = await isCartRevealed(page, expectedProductTitle);
50915
+ const hoverOpened = await isCartRevealed(page, expectedProductTitle, ctx);
50734
50916
  if (hoverOpened) {
50735
50917
  dlog2(ctx, ` openMinicart: hover opened drawer (${hoverOpened})`);
50736
50918
  return { method: "hover", url: page.url(), visibleMarker: hoverOpened };
@@ -50755,7 +50937,7 @@ async function openMinicart(page, trigger, ctx, expectedProductTitle) {
50755
50937
  return { method: "click-navigate", url: page.url(), visibleMarker: null };
50756
50938
  }
50757
50939
  await page.waitForTimeout(800);
50758
- const tapOpened = await isCartRevealed(page, expectedProductTitle);
50940
+ const tapOpened = await isCartRevealed(page, expectedProductTitle, ctx);
50759
50941
  if (tapOpened) {
50760
50942
  dlog2(ctx, ` openMinicart: tap opened drawer (${tapOpened})`);
50761
50943
  return { method: "click", url: page.url(), visibleMarker: tapOpened };
@@ -50780,7 +50962,7 @@ async function openMinicart(page, trigger, ctx, expectedProductTitle) {
50780
50962
  return { method: "click-navigate", url: page.url(), visibleMarker: null };
50781
50963
  }
50782
50964
  await page.waitForTimeout(1500);
50783
- const clickOpened = await isCartRevealed(page, expectedProductTitle);
50965
+ const clickOpened = await isCartRevealed(page, expectedProductTitle, ctx);
50784
50966
  if (clickOpened) {
50785
50967
  dlog2(ctx, ` openMinicart: click opened drawer (${clickOpened})`);
50786
50968
  return { method: "click", url: afterClickUrl, visibleMarker: clickOpened };
@@ -50791,7 +50973,7 @@ async function openMinicart(page, trigger, ctx, expectedProductTitle) {
50791
50973
  return;
50792
50974
  });
50793
50975
  await page.waitForTimeout(1500);
50794
- const hoverOpened = await isCartRevealed(page, expectedProductTitle);
50976
+ const hoverOpened = await isCartRevealed(page, expectedProductTitle, ctx);
50795
50977
  if (hoverOpened) {
50796
50978
  dlog2(ctx, ` openMinicart: hover opened drawer (${hoverOpened})`);
50797
50979
  return { method: "hover", url: page.url(), visibleMarker: hoverOpened };
@@ -50837,7 +51019,16 @@ function titlesMatch(observed, expected) {
50837
51019
  return false;
50838
51020
  }
50839
51021
  async function validateCartContainsTitle(page, expectedTitle, ctx) {
51022
+ const panelScopedTitleSelectors = selFor(ctx, "minicartPanel").flatMap((p) => [
51023
+ `${p} a[href*='/p']`,
51024
+ `${p} [class*='name' i]`,
51025
+ `${p} [class*='title' i]`,
51026
+ `${p} [class*='product' i]`,
51027
+ `${p} [data-cart-item-name]`,
51028
+ `${p} [data-qa-product-name]`
51029
+ ]);
50840
51030
  const titleSelectors = [
51031
+ ...panelScopedTitleSelectors,
50841
51032
  "[data-cart-item-name]",
50842
51033
  "[data-cart-item] [class*='title' i]",
50843
51034
  "[data-cart-item] [class*='name' i]",
@@ -51631,10 +51822,25 @@ async function flowPurchaseJourney(ctx) {
51631
51822
  validation = await validateAddToCart(page, ctx, cartCountBefore, beforeUrl6);
51632
51823
  }
51633
51824
  }
51825
+ let overlayDismissed;
51826
+ if (validation.status === "failed" && validation.signal === "no-signal") {
51827
+ const od = await dismissBlockingOverlay(page, ctx, buyHit.locator);
51828
+ if (od) {
51829
+ overlayDismissed = od;
51830
+ if (od.dismissed) {
51831
+ await page.waitForTimeout(300);
51832
+ await buyHit.locator.click({ timeout: 5000 }).catch(() => {
51833
+ return;
51834
+ });
51835
+ validation = await validateAddToCart(page, ctx, cartCountBefore, beforeUrl6);
51836
+ }
51837
+ }
51838
+ }
51634
51839
  const sp6 = screenshotPath(ctx, "pj-6-add-cart");
51635
51840
  await screenshotStable(page, { path: sp6, fullPage: false });
51636
51841
  const buyActionDesc = `Clicou no botão${buyText ? ` '${buyText.slice(0, 40).trim()}'` : ""} (\`${buyHit.selector}\`)${buyRecovered ? " — selector veio de recovery LLM" : ""}`;
51637
- const fullActionDesc = variantRetryNote ? `${buyActionDesc} — ${variantRetryNote} ${validation.note}` : `${buyActionDesc} ${validation.note}`;
51842
+ const overlayNote = overlayDismissed ? `overlay ${overlayDismissed.tag}${overlayDismissed.id ? `#${overlayDismissed.id}` : ""} ${overlayDismissed.dismissed ? `dismissed (${overlayDismissed.method}), click retried` : "detected but NOT dismissed"}` : "";
51843
+ const fullActionDesc = variantRetryNote ? `${buyActionDesc} — ${variantRetryNote}${overlayNote} — ${validation.note}` : `${buyActionDesc}${overlayNote} — ${validation.note}`;
51638
51844
  steps.push({
51639
51845
  step: 6,
51640
51846
  name: "add-to-cart",
@@ -51654,7 +51860,8 @@ async function flowPurchaseJourney(ctx) {
51654
51860
  detail: {
51655
51861
  signal: validation.signal,
51656
51862
  errorText: validation.errorText,
51657
- variantRetry: variantRetryNote
51863
+ variantRetry: variantRetryNote,
51864
+ overlayDismissed
51658
51865
  }
51659
51866
  });
51660
51867
  reportEnd(6, "add-to-cart", validation.status, Date.now() - t6, validation.status === "ok" ? undefined : validation.note);
@@ -51679,7 +51886,7 @@ async function flowPurchaseJourney(ctx) {
51679
51886
  let cartOpenMethod = "failed";
51680
51887
  let miniText = "";
51681
51888
  let cartRevealMode = "unknown";
51682
- const drawerAlreadyOpen = await isCartRevealed(page, expectedProductTitle) !== null;
51889
+ const drawerAlreadyOpen = await isCartRevealed(page, expectedProductTitle, ctx) !== null;
51683
51890
  if (miniHit) {
51684
51891
  miniText = await miniHit.locator.innerText().catch(() => "");
51685
51892
  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.14.0",
3
+ "version": "0.15.1",
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",