@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.
- package/CHANGELOG.md +18 -1
- package/dist/cli.js +237 -30
- 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
|
|
50536
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
50740
|
+
dismissals.push({ reason: "selector-match", tag: sel, method: "escape", dismissed: true });
|
|
50563
50741
|
} catch {}
|
|
50564
50742
|
}
|
|
50565
|
-
if (
|
|
50566
|
-
dlog2(ctx,
|
|
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
|
|
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.
|
|
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",
|