@decocms/parity 0.15.1 → 0.17.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/CHANGELOG.md +18 -0
- package/dist/cli.js +214 -55
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,10 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.17.0](https://github.com/decocms/parity/compare/v0.16.0...v0.17.0) (2026-07-28)
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
* **`open-minicart`/`add-to-cart` reveal-failure diagnostics fed to LLM recovery.** A poll-based step (waiting for the minicart drawer or an add-to-cart confirmation to appear) previously reported a bare "not found"/"no signal" on timeout, with no evidence of *why*. Failed polls now attach a structured `diagnostics` object to the step (`timedOut`, `budgetMs`, `elapsedMs`, `pollCount`, and a per-selector `probes` snapshot distinguishing "present in the DOM but stayed hidden" from "never matched at all") — surfaced in `report.json` and rendered in the HTML report's step cell. When `open-minicart` fails with this evidence available, parity now spends one LLM recovery attempt passing the diagnostics as concrete context (`RecoverInput.diagnostics`) instead of a bare failure, and the recovery prompt is told not to re-suggest a selector already confirmed present-but-hidden.
|
|
15
|
+
* New `cartRevealTimeoutMs` (issue #149 follow-up) tunes how long `waitForCartReveal` polls for the drawer before giving up. Defaults to 4000ms (8000ms on localhost dev servers, which are slower).
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
* **`waitForCartReveal` polls instead of snapshotting once.** `openMinicart`'s reveal check (via `isCartRevealed`/Playwright `isVisible()`) was a one-shot read of the drawer's *current* state right after a fixed wait, not an actual wait. Drawers that reveal asynchronously — a CSS `allow-discrete` visibility/opacity transition, a data-gated render (react-query cart), or a slow click handler — can stay `visibility:hidden` (often positioned off-screen) for well over a second after the click, so the single snapshot landed inside the hidden window and wrongly reported the cart never opened, even with the correct selector, no overlay, and no console error. `openMinicart` now polls every 200ms up to an adaptive budget instead.
|
|
20
|
+
* **`open-minicart` re-checks for a blocking overlay before giving up.** `dismissOverlays` only ran once at the very top of `openMinicart`, before the trigger was clicked. Confirmed live against a production deploy: a newsletter popup can appear *after* the add-to-cart click and silently intercept the minicart-trigger click (`force: true` clicks whatever is topmost at the coordinates, not necessarily the intended target), so the drawer never opens even though the selector and reveal-polling logic are both correct. `openMinicart` now detects this structurally (mirroring `dismissBlockingOverlay`, issues #145/#146) and retries the click once after clearing it.
|
|
21
|
+
|
|
22
|
+
## [0.16.0](https://github.com/decocms/parity/compare/v0.15.1...v0.16.0) (2026-07-28)
|
|
23
|
+
|
|
10
24
|
### Added
|
|
11
25
|
|
|
12
26
|
* **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
27
|
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
* **`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.
|
|
31
|
+
|
|
14
32
|
## [0.15.0](https://github.com/decocms/parity/compare/v0.14.0...v0.15.0) (2026-07-27)
|
|
15
33
|
|
|
16
34
|
### Added
|
package/dist/cli.js
CHANGED
|
@@ -40340,6 +40340,14 @@ var REPORT_CSS = `
|
|
|
40340
40340
|
color: var(--state-warn);
|
|
40341
40341
|
font-style: italic;
|
|
40342
40342
|
}
|
|
40343
|
+
.step-diagnostics {
|
|
40344
|
+
margin-top: 4px;
|
|
40345
|
+
font-size: 10px;
|
|
40346
|
+
color: var(--text-muted);
|
|
40347
|
+
border-left: 2px solid var(--state-warn);
|
|
40348
|
+
padding-left: 6px;
|
|
40349
|
+
}
|
|
40350
|
+
.step-diagnostics code { background: var(--surface-base); padding: 1px 4px; border-radius: 3px; }
|
|
40343
40351
|
|
|
40344
40352
|
/* Network waterfall — per-page SVG bar chart. Issue #78. */
|
|
40345
40353
|
.wf-page {
|
|
@@ -41154,6 +41162,18 @@ var StepCapture = z.object({
|
|
|
41154
41162
|
loginValidation: z.object({
|
|
41155
41163
|
stage: z.enum(["form-loaded", "submitted", "succeeded", "error-shown"]),
|
|
41156
41164
|
errorMessage: z.string().optional()
|
|
41165
|
+
}).optional(),
|
|
41166
|
+
diagnostics: z.object({
|
|
41167
|
+
timedOut: z.boolean().optional(),
|
|
41168
|
+
budgetMs: z.number().optional(),
|
|
41169
|
+
elapsedMs: z.number().optional(),
|
|
41170
|
+
pollCount: z.number().optional(),
|
|
41171
|
+
probes: z.array(z.object({
|
|
41172
|
+
selector: z.string(),
|
|
41173
|
+
present: z.boolean(),
|
|
41174
|
+
visible: z.boolean()
|
|
41175
|
+
})).optional(),
|
|
41176
|
+
toastText: z.string().optional()
|
|
41157
41177
|
}).optional()
|
|
41158
41178
|
});
|
|
41159
41179
|
var FlowCapture = z.object({
|
|
@@ -41436,7 +41456,8 @@ var ParityRc = z.object({
|
|
|
41436
41456
|
serverFnFloodBudget: z.number().optional(),
|
|
41437
41457
|
serverFnPattern: z.string().optional(),
|
|
41438
41458
|
overlaySelectors: z.array(z.string()).optional(),
|
|
41439
|
-
addToCartConfirmMs: z.number().optional()
|
|
41459
|
+
addToCartConfirmMs: z.number().optional(),
|
|
41460
|
+
cartRevealTimeoutMs: z.number().optional()
|
|
41440
41461
|
});
|
|
41441
41462
|
var ParityIgnore = z.object({
|
|
41442
41463
|
ignoreSelectorsVisual: z.array(z.string()).default([]),
|
|
@@ -47516,12 +47537,26 @@ function renderStepCell(s, runDir) {
|
|
|
47516
47537
|
const screenshot = s.screenshotPath ? `<a class="step-shot" href="${escapeHtml(relPath(runDir, s.screenshotPath))}" target="_blank" title="open screenshot">\uD83D\uDCF7</a>` : "";
|
|
47517
47538
|
const selector = s.usedSelector ? `<div class="step-selector"><code>${escapeHtml(s.usedSelector)}</code></div>` : "";
|
|
47518
47539
|
const note = s.note ? `<div class="step-note">${escapeHtml(s.note)}</div>` : "";
|
|
47540
|
+
const diagnostics = renderStepDiagnostics(s.diagnostics);
|
|
47519
47541
|
return `<td class="step-cell ${statusClass}">
|
|
47520
47542
|
<div class="step-cell-head"><span class="step-status">${s.status}</span>${screenshot}<span class="dim">${s.durationMs}ms</span></div>
|
|
47521
47543
|
${selector}
|
|
47522
47544
|
${note}
|
|
47545
|
+
${diagnostics}
|
|
47523
47546
|
</td>`;
|
|
47524
47547
|
}
|
|
47548
|
+
function renderStepDiagnostics(d) {
|
|
47549
|
+
if (!d?.timedOut)
|
|
47550
|
+
return "";
|
|
47551
|
+
const hidden = d.probes?.filter((p) => p.present && !p.visible) ?? [];
|
|
47552
|
+
const missing = d.probes?.filter((p) => !p.present) ?? [];
|
|
47553
|
+
const rows = [
|
|
47554
|
+
`<div>esperou ${d.elapsedMs}ms / orçamento ${d.budgetMs}ms (${d.pollCount} poll(s))</div>`,
|
|
47555
|
+
...hidden.map((p) => `<div>⚠️ presente porém oculto: <code>${escapeHtml(p.selector)}</code></div>`),
|
|
47556
|
+
...missing.map((p) => `<div>não encontrado no DOM: <code>${escapeHtml(p.selector)}</code></div>`)
|
|
47557
|
+
];
|
|
47558
|
+
return `<div class="step-diagnostics">${rows.join("")}</div>`;
|
|
47559
|
+
}
|
|
47525
47560
|
function renderChecksTable(run) {
|
|
47526
47561
|
return `
|
|
47527
47562
|
<div class="card">
|
|
@@ -49316,6 +49351,17 @@ import { writeFileSync as writeFileSync10 } from "node:fs";
|
|
|
49316
49351
|
import chalk8 from "chalk";
|
|
49317
49352
|
import ora4 from "ora";
|
|
49318
49353
|
|
|
49354
|
+
// src/util/localhost.ts
|
|
49355
|
+
var LOCALHOST_HOSTS = new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"]);
|
|
49356
|
+
function isLocalhost(rawUrl) {
|
|
49357
|
+
try {
|
|
49358
|
+
const url = new URL(rawUrl);
|
|
49359
|
+
return LOCALHOST_HOSTS.has(url.hostname);
|
|
49360
|
+
} catch {
|
|
49361
|
+
return false;
|
|
49362
|
+
}
|
|
49363
|
+
}
|
|
49364
|
+
|
|
49319
49365
|
// src/llm/pick-plp.ts
|
|
49320
49366
|
init_client();
|
|
49321
49367
|
var PATH_BLOCKLIST = [
|
|
@@ -49494,6 +49540,8 @@ REGRAS CRÍTICAS:
|
|
|
49494
49540
|
|
|
49495
49541
|
9. **Quando incerto, retorne selector="" (string vazia).** É preferível que o flow declare a falha honestamente a chutar um seletor errado que apenas clica em coisa aleatória. Em especial: se a página parece ser uma LANDING PAGE (sem buy form, sem schema:Product, sem preço) e o step pede um buy/variant/cep selector, retorne string vazia — não force uma sugestão.
|
|
49496
49542
|
|
|
49543
|
+
10. **Se receber "Diagnóstico" com "present-but-hidden: <seletor>"**, isso significa que ESSE seletor específico já casa o elemento certo no DOM, mas ele ficou invisível pelo tempo orçado — é um problema de timing/animação/dados assíncronos, NÃO um seletor errado. NÃO sugira esse mesmo seletor de novo. Considere: (a) um seletor de TRIGGER diferente que realmente dispara a revelação (ex: o clique caiu num elemento errado), ou (b) se nada mais plausível existe, retorne selector="" — não invente um seletor alternativo só para dar uma resposta.
|
|
49544
|
+
|
|
49497
49545
|
Responda SEMPRE via tool_use suggest_recovery.
|
|
49498
49546
|
`.trim();
|
|
49499
49547
|
async function suggestRecovery(input) {
|
|
@@ -49504,6 +49552,7 @@ async function suggestRecovery(input) {
|
|
|
49504
49552
|
userText: `Step: ${input.stepName}
|
|
49505
49553
|
Ação desejada: ${input.intendedAction}
|
|
49506
49554
|
${input.alreadyTried?.length ? `Já tentei (não repita estes): ${input.alreadyTried.join(", ")}
|
|
49555
|
+
` : ""}${input.diagnostics ? `Diagnóstico: ${input.diagnostics}
|
|
49507
49556
|
` : ""}
|
|
49508
49557
|
HTML compactado da página NESTE momento:
|
|
49509
49558
|
\`\`\`html
|
|
@@ -50383,14 +50432,14 @@ async function findElement(page, ctx, opts) {
|
|
|
50383
50432
|
}
|
|
50384
50433
|
return null;
|
|
50385
50434
|
}
|
|
50386
|
-
async function attemptRecovery(page, _ctx, stepName, intendedAction, alreadyTried) {
|
|
50435
|
+
async function attemptRecovery(page, _ctx, stepName, intendedAction, alreadyTried, diagnostics) {
|
|
50387
50436
|
let html3 = "";
|
|
50388
50437
|
try {
|
|
50389
50438
|
html3 = await page.content();
|
|
50390
50439
|
} catch {
|
|
50391
50440
|
return null;
|
|
50392
50441
|
}
|
|
50393
|
-
const suggestion = await suggestRecovery({ stepName, intendedAction, html: html3, alreadyTried });
|
|
50442
|
+
const suggestion = await suggestRecovery({ stepName, intendedAction, html: html3, alreadyTried, diagnostics });
|
|
50394
50443
|
if (!suggestion)
|
|
50395
50444
|
return null;
|
|
50396
50445
|
try {
|
|
@@ -50719,15 +50768,21 @@ async function dismissBlockingOverlay(page, ctx, target) {
|
|
|
50719
50768
|
function describe(b) {
|
|
50720
50769
|
return `${b.tag}${b.id ? `#${b.id}` : ""}${b.className ? `.${b.className.split(/\s+/)[0]}` : ""}`;
|
|
50721
50770
|
}
|
|
50771
|
+
var OVERLAY_PROBE_CAP_MS = 80;
|
|
50722
50772
|
async function dismissOverlays(page, ctx) {
|
|
50773
|
+
const sels = overlaySelectorsFor(ctx.rc);
|
|
50774
|
+
dlog2(ctx, ` dismissOverlays: checking ${sels.length} selector(s)…`);
|
|
50723
50775
|
const dismissals = [];
|
|
50724
|
-
for (const sel of
|
|
50776
|
+
for (const sel of sels) {
|
|
50725
50777
|
try {
|
|
50726
50778
|
const overlay = page.locator(sel).first();
|
|
50727
|
-
|
|
50779
|
+
const cnt = await withCap(overlay.count(), OVERLAY_PROBE_CAP_MS, 0);
|
|
50780
|
+
if (cnt === 0)
|
|
50781
|
+
continue;
|
|
50782
|
+
if (!await withCap(overlay.isVisible({ timeout: OVERLAY_PROBE_CAP_MS }).catch(() => false), OVERLAY_PROBE_CAP_MS, false))
|
|
50728
50783
|
continue;
|
|
50729
50784
|
const closer = overlay.locator(CLOSE_BUTTON_SELECTOR).first();
|
|
50730
|
-
if (await withCap(closer.isVisible({ timeout:
|
|
50785
|
+
if (await withCap(closer.isVisible({ timeout: OVERLAY_PROBE_CAP_MS }).catch(() => false), OVERLAY_PROBE_CAP_MS, false)) {
|
|
50731
50786
|
await closer.click({ timeout: 1500 }).catch(() => {
|
|
50732
50787
|
return;
|
|
50733
50788
|
});
|
|
@@ -50860,6 +50915,66 @@ async function isCartRevealed(page, expectedProductTitle, ctx) {
|
|
|
50860
50915
|
}
|
|
50861
50916
|
return isCartUiVisible(page, ctx);
|
|
50862
50917
|
}
|
|
50918
|
+
var CART_REVEAL_BUDGET_MS = 4000;
|
|
50919
|
+
var CART_REVEAL_BUDGET_LOCALHOST_MS = 8000;
|
|
50920
|
+
var CART_REVEAL_POLL_INTERVAL_MS = 200;
|
|
50921
|
+
var CART_REVEAL_HOVER_PROBE_MS = 1500;
|
|
50922
|
+
async function probeCartSelectors(page, ctx) {
|
|
50923
|
+
const selectors = [...new Set([...selFor(ctx, "minicartPanel"), ...selFor(ctx, "cartOpenedIndicator")])];
|
|
50924
|
+
const probes = [];
|
|
50925
|
+
for (const sel of selectors) {
|
|
50926
|
+
try {
|
|
50927
|
+
const loc = page.locator(sel);
|
|
50928
|
+
const present = await withCap(loc.count(), 300, 0) > 0;
|
|
50929
|
+
const visible = present ? await withCap(loc.first().isVisible().catch(() => false), 300, false) : false;
|
|
50930
|
+
probes.push({ selector: sel, present, visible });
|
|
50931
|
+
} catch {
|
|
50932
|
+
probes.push({ selector: sel, present: false, visible: false });
|
|
50933
|
+
}
|
|
50934
|
+
}
|
|
50935
|
+
return probes;
|
|
50936
|
+
}
|
|
50937
|
+
function summarizeCartRevealDiagnostics(d) {
|
|
50938
|
+
const parts = [`waited ${d.elapsedMs}ms/${d.budgetMs}ms (${d.pollCount} poll(s))`];
|
|
50939
|
+
const hidden = d.probes?.filter((p) => p.present && !p.visible) ?? [];
|
|
50940
|
+
const missing = d.probes?.filter((p) => !p.present) ?? [];
|
|
50941
|
+
if (hidden.length) {
|
|
50942
|
+
parts.push(`present-but-hidden: ${hidden.map((p) => p.selector).join(", ")}`);
|
|
50943
|
+
}
|
|
50944
|
+
if (missing.length) {
|
|
50945
|
+
parts.push(`not-in-dom: ${missing.map((p) => p.selector).join(", ")}`);
|
|
50946
|
+
}
|
|
50947
|
+
return parts.join(" — ");
|
|
50948
|
+
}
|
|
50949
|
+
async function waitForCartReveal(page, expectedProductTitle, ctx, timeoutMs) {
|
|
50950
|
+
const start = Date.now();
|
|
50951
|
+
const deadline = start + timeoutMs;
|
|
50952
|
+
let pollCount = 0;
|
|
50953
|
+
for (;; ) {
|
|
50954
|
+
pollCount++;
|
|
50955
|
+
const marker = await isCartRevealed(page, expectedProductTitle, ctx);
|
|
50956
|
+
if (marker) {
|
|
50957
|
+
dlog2(ctx, ` waitForCartReveal: revealed after ${pollCount} poll(s) (${marker})`);
|
|
50958
|
+
return {
|
|
50959
|
+
marker,
|
|
50960
|
+
diagnostics: { timedOut: false, budgetMs: timeoutMs, elapsedMs: Date.now() - start, pollCount }
|
|
50961
|
+
};
|
|
50962
|
+
}
|
|
50963
|
+
if (Date.now() >= deadline) {
|
|
50964
|
+
const probes = await probeCartSelectors(page, ctx);
|
|
50965
|
+
const diagnostics = {
|
|
50966
|
+
timedOut: true,
|
|
50967
|
+
budgetMs: timeoutMs,
|
|
50968
|
+
elapsedMs: Date.now() - start,
|
|
50969
|
+
pollCount,
|
|
50970
|
+
probes
|
|
50971
|
+
};
|
|
50972
|
+
dlog2(ctx, ` waitForCartReveal: ${summarizeCartRevealDiagnostics(diagnostics)}`);
|
|
50973
|
+
return { marker: null, diagnostics };
|
|
50974
|
+
}
|
|
50975
|
+
await page.waitForTimeout(CART_REVEAL_POLL_INTERVAL_MS);
|
|
50976
|
+
}
|
|
50977
|
+
}
|
|
50863
50978
|
async function validateCartContainsTitleQuick(page, expectedTitle, ctx) {
|
|
50864
50979
|
const quickSelectors = [
|
|
50865
50980
|
...ctx ? selFor(ctx, "minicartPanel") : [],
|
|
@@ -50897,7 +51012,14 @@ async function waitForCartHydration(page) {
|
|
|
50897
51012
|
}
|
|
50898
51013
|
async function openMinicart(page, trigger, ctx, expectedProductTitle) {
|
|
50899
51014
|
const beforeUrl = page.url();
|
|
50900
|
-
|
|
51015
|
+
let lastDiagnostics;
|
|
51016
|
+
const revealBudget = ctx.rc.cartRevealTimeoutMs ?? (isLocalhost(beforeUrl) ? CART_REVEAL_BUDGET_LOCALHOST_MS : CART_REVEAL_BUDGET_MS);
|
|
51017
|
+
dlog2(ctx, ` openMinicart: starting — trigger=${trigger.selector} title=${expectedProductTitle?.slice(0, 40) ?? "none"} revealBudget=${revealBudget}ms`);
|
|
51018
|
+
await Promise.race([
|
|
51019
|
+
dismissOverlays(page, ctx),
|
|
51020
|
+
new Promise((resolve) => setTimeout(resolve, 4000))
|
|
51021
|
+
]);
|
|
51022
|
+
dlog2(ctx, " openMinicart: dismissOverlays done — checking alreadyOpen…");
|
|
50901
51023
|
await page.waitForTimeout(800);
|
|
50902
51024
|
const alreadyOpen = await isCartRevealed(page, expectedProductTitle, ctx);
|
|
50903
51025
|
if (alreadyOpen) {
|
|
@@ -50911,11 +51033,12 @@ async function openMinicart(page, trigger, ctx, expectedProductTitle) {
|
|
|
50911
51033
|
await trigger.locator.hover({ timeout: 3000 }).catch(() => {
|
|
50912
51034
|
return;
|
|
50913
51035
|
});
|
|
50914
|
-
|
|
50915
|
-
const
|
|
50916
|
-
|
|
50917
|
-
|
|
50918
|
-
|
|
51036
|
+
const hoverProbeBudget = Math.min(CART_REVEAL_HOVER_PROBE_MS, revealBudget);
|
|
51037
|
+
const hover1 = await waitForCartReveal(page, expectedProductTitle, ctx, hoverProbeBudget);
|
|
51038
|
+
lastDiagnostics = hover1.diagnostics;
|
|
51039
|
+
if (hover1.marker) {
|
|
51040
|
+
dlog2(ctx, ` openMinicart: hover opened drawer (${hover1.marker})`);
|
|
51041
|
+
return { method: "hover", url: page.url(), visibleMarker: hover1.marker, diagnostics: hover1.diagnostics };
|
|
50919
51042
|
}
|
|
50920
51043
|
}
|
|
50921
51044
|
if (ctx.viewport === "mobile") {
|
|
@@ -50936,11 +51059,11 @@ async function openMinicart(page, trigger, ctx, expectedProductTitle) {
|
|
|
50936
51059
|
dlog2(ctx, ` openMinicart: tap navigated → ${page.url()} (settled)`);
|
|
50937
51060
|
return { method: "click-navigate", url: page.url(), visibleMarker: null };
|
|
50938
51061
|
}
|
|
50939
|
-
await page
|
|
50940
|
-
|
|
50941
|
-
if (
|
|
50942
|
-
dlog2(ctx, ` openMinicart: tap opened drawer (${
|
|
50943
|
-
return { method: "click", url: page.url(), visibleMarker:
|
|
51062
|
+
const tap = await waitForCartReveal(page, expectedProductTitle, ctx, revealBudget);
|
|
51063
|
+
lastDiagnostics = tap.diagnostics;
|
|
51064
|
+
if (tap.marker) {
|
|
51065
|
+
dlog2(ctx, ` openMinicart: tap opened drawer (${tap.marker})`);
|
|
51066
|
+
return { method: "click", url: page.url(), visibleMarker: tap.marker, diagnostics: tap.diagnostics };
|
|
50944
51067
|
}
|
|
50945
51068
|
}
|
|
50946
51069
|
dlog2(ctx, ` openMinicart: trying force-click on ${trigger.selector}${triggerHref ? ` (href=${triggerHref})` : ""}`);
|
|
@@ -50961,22 +51084,35 @@ async function openMinicart(page, trigger, ctx, expectedProductTitle) {
|
|
|
50961
51084
|
dlog2(ctx, ` openMinicart: click navigated → ${page.url()} (settled)`);
|
|
50962
51085
|
return { method: "click-navigate", url: page.url(), visibleMarker: null };
|
|
50963
51086
|
}
|
|
50964
|
-
await page
|
|
50965
|
-
|
|
50966
|
-
if (
|
|
50967
|
-
dlog2(ctx, ` openMinicart: click opened drawer (${
|
|
50968
|
-
return { method: "click", url: afterClickUrl, visibleMarker:
|
|
51087
|
+
const click = await waitForCartReveal(page, expectedProductTitle, ctx, revealBudget);
|
|
51088
|
+
lastDiagnostics = click.diagnostics;
|
|
51089
|
+
if (click.marker) {
|
|
51090
|
+
dlog2(ctx, ` openMinicart: click opened drawer (${click.marker})`);
|
|
51091
|
+
return { method: "click", url: afterClickUrl, visibleMarker: click.marker, diagnostics: click.diagnostics };
|
|
51092
|
+
}
|
|
51093
|
+
const blockingOverlay = await dismissBlockingOverlay(page, ctx, trigger.locator);
|
|
51094
|
+
if (blockingOverlay?.dismissed) {
|
|
51095
|
+
dlog2(ctx, ` openMinicart: cleared blocking overlay (${blockingOverlay.method}) — retrying click`);
|
|
51096
|
+
await trigger.locator.click({ force: true, timeout: 4000 }).catch(() => {
|
|
51097
|
+
return;
|
|
51098
|
+
});
|
|
51099
|
+
const retry = await waitForCartReveal(page, expectedProductTitle, ctx, revealBudget);
|
|
51100
|
+
lastDiagnostics = retry.diagnostics;
|
|
51101
|
+
if (retry.marker) {
|
|
51102
|
+
dlog2(ctx, ` openMinicart: click opened drawer after overlay dismissal (${retry.marker})`);
|
|
51103
|
+
return { method: "click", url: page.url(), visibleMarker: retry.marker, diagnostics: retry.diagnostics };
|
|
51104
|
+
}
|
|
50969
51105
|
}
|
|
50970
51106
|
if (ctx.viewport !== "desktop") {
|
|
50971
51107
|
dlog2(ctx, ` openMinicart: click didn't reveal cart, trying hover (mobile)`);
|
|
50972
51108
|
await trigger.locator.hover({ timeout: 3000 }).catch(() => {
|
|
50973
51109
|
return;
|
|
50974
51110
|
});
|
|
50975
|
-
await page
|
|
50976
|
-
|
|
50977
|
-
if (
|
|
50978
|
-
dlog2(ctx, ` openMinicart: hover opened drawer (${
|
|
50979
|
-
return { method: "hover", url: page.url(), visibleMarker:
|
|
51111
|
+
const hover2 = await waitForCartReveal(page, expectedProductTitle, ctx, revealBudget);
|
|
51112
|
+
lastDiagnostics = hover2.diagnostics;
|
|
51113
|
+
if (hover2.marker) {
|
|
51114
|
+
dlog2(ctx, ` openMinicart: hover opened drawer (${hover2.marker})`);
|
|
51115
|
+
return { method: "hover", url: page.url(), visibleMarker: hover2.marker, diagnostics: hover2.diagnostics };
|
|
50980
51116
|
}
|
|
50981
51117
|
}
|
|
50982
51118
|
if (hrefHasCartTarget && triggerHref) {
|
|
@@ -50999,8 +51135,8 @@ async function openMinicart(page, trigger, ctx, expectedProductTitle) {
|
|
|
50999
51135
|
}
|
|
51000
51136
|
}
|
|
51001
51137
|
}
|
|
51002
|
-
dlog2(ctx,
|
|
51003
|
-
return { method: "failed", url: page.url(), visibleMarker: null };
|
|
51138
|
+
dlog2(ctx, ` openMinicart: failed — no cart revealed by hover/click/goto${lastDiagnostics ? ` (${summarizeCartRevealDiagnostics(lastDiagnostics)})` : ""}`);
|
|
51139
|
+
return { method: "failed", url: page.url(), visibleMarker: null, diagnostics: lastDiagnostics };
|
|
51004
51140
|
}
|
|
51005
51141
|
function normalizeTitle(s) {
|
|
51006
51142
|
return s.toLowerCase().replace(/[®©™]/g, "").replace(/[^\p{L}\p{N}\s]+/gu, " ").replace(/\s+/g, " ").trim();
|
|
@@ -51857,6 +51993,7 @@ async function flowPurchaseJourney(ctx) {
|
|
|
51857
51993
|
usedSelector: buyHit.selector,
|
|
51858
51994
|
recoveredByLlm: buyRecovered || undefined,
|
|
51859
51995
|
note: validation.status === "ok" ? undefined : validation.note,
|
|
51996
|
+
diagnostics: validation.diagnostics,
|
|
51860
51997
|
detail: {
|
|
51861
51998
|
signal: validation.signal,
|
|
51862
51999
|
errorText: validation.errorText,
|
|
@@ -51886,13 +52023,26 @@ async function flowPurchaseJourney(ctx) {
|
|
|
51886
52023
|
let cartOpenMethod = "failed";
|
|
51887
52024
|
let miniText = "";
|
|
51888
52025
|
let cartRevealMode = "unknown";
|
|
52026
|
+
let revealDiagnostics;
|
|
51889
52027
|
const drawerAlreadyOpen = await isCartRevealed(page, expectedProductTitle, ctx) !== null;
|
|
51890
52028
|
if (miniHit) {
|
|
51891
52029
|
miniText = await miniHit.locator.innerText().catch(() => "");
|
|
51892
52030
|
cartRevealMode = await detectCartRevealMode(page, miniHit.locator, drawerAlreadyOpen, ctx.viewport).catch(() => "unknown");
|
|
51893
52031
|
dlog2(ctx, `step 7 open-minicart: cartRevealMode=${cartRevealMode}`);
|
|
51894
|
-
|
|
52032
|
+
let openResult = await openMinicart(page, miniHit, ctx, expectedProductTitle);
|
|
51895
52033
|
cartOpenMethod = openResult.method;
|
|
52034
|
+
revealDiagnostics = openResult.diagnostics;
|
|
52035
|
+
if (cartOpenMethod === "failed" && revealDiagnostics && budget.remaining > 0) {
|
|
52036
|
+
const recovery = await attemptRecovery(page, ctx, "open-minicart", "Abrir o minicart/drawer do carrinho — o trigger foi clicado mas o drawer não revelou", [miniHit.selector], summarizeCartRevealDiagnostics(revealDiagnostics));
|
|
52037
|
+
if (recovery) {
|
|
52038
|
+
budget.remaining--;
|
|
52039
|
+
miniHit = recovery;
|
|
52040
|
+
miniRecovered = true;
|
|
52041
|
+
openResult = await openMinicart(page, miniHit, ctx, expectedProductTitle);
|
|
52042
|
+
cartOpenMethod = openResult.method;
|
|
52043
|
+
revealDiagnostics = openResult.diagnostics;
|
|
52044
|
+
}
|
|
52045
|
+
}
|
|
51896
52046
|
} else {
|
|
51897
52047
|
if (drawerAlreadyOpen) {
|
|
51898
52048
|
cartOpenMethod = "already-open";
|
|
@@ -51927,6 +52077,7 @@ async function flowPurchaseJourney(ctx) {
|
|
|
51927
52077
|
const isProdCartEmptyQuirk = ctx.acceptProdQuirks === true && ctx.side === "prod" && step7Validation !== undefined && !step7Validation.found && cartEmptyReason.startsWith("cart genuinely empty");
|
|
51928
52078
|
const step7Status = isProdCartEmptyQuirk ? "skipped" : cartOpenMethod === "failed" ? "failed" : step7Validation && !step7Validation.found ? "failed" : "ok";
|
|
51929
52079
|
const step7QuirkNote = isProdCartEmptyQuirk ? "cart-empty-prod-quirk: aceito via --accept-prod-quirks (cartRevealMode validated by separate check)" : undefined;
|
|
52080
|
+
const step7RevealFailureNote = cartOpenMethod === "failed" && revealDiagnostics ? `minicart não revelou — ${summarizeCartRevealDiagnostics(revealDiagnostics)}` : undefined;
|
|
51930
52081
|
steps.push({
|
|
51931
52082
|
step: 7,
|
|
51932
52083
|
name: "open-minicart",
|
|
@@ -51941,13 +52092,14 @@ async function flowPurchaseJourney(ctx) {
|
|
|
51941
52092
|
cartOpenMethod,
|
|
51942
52093
|
cartRevealMode,
|
|
51943
52094
|
cartValidation: step7Validation,
|
|
51944
|
-
|
|
52095
|
+
diagnostics: revealDiagnostics,
|
|
52096
|
+
note: step7QuirkNote ?? step7RevealFailureNote,
|
|
51945
52097
|
actionDescription: miniHit ? `Abriu minicart via ${cartOpenMethod}${miniText ? ` em '${miniText.slice(0, 30).trim()}'` : ""} (\`${miniHit.selector}\`)${miniRecovered ? " — selector via recovery LLM" : ""}${step7Validation ? step7Validation.found ? " ✓ produto encontrado no cart" : ` ✗ produto ESPERADO não encontrado (${step7Validation.observedTitles?.length ?? 0} itens observados)` : ""}` : cartOpenMethod === "already-open" ? "Minicart já estava aberto após add-to-cart" : "Minicart não pôde ser aberto",
|
|
51946
52098
|
selectorKey: miniHit ? "minicartTrigger" : undefined,
|
|
51947
52099
|
usedSelector: miniHit?.selector,
|
|
51948
52100
|
recoveredByLlm: miniRecovered || undefined
|
|
51949
52101
|
});
|
|
51950
|
-
reportEnd(7, "open-minicart", step7Status, Date.now() - t7, step7QuirkNote);
|
|
52102
|
+
reportEnd(7, "open-minicart", step7Status, Date.now() - t7, step7QuirkNote ?? step7RevealFailureNote);
|
|
51951
52103
|
if (isProdCartEmptyQuirk) {
|
|
51952
52104
|
const quirkNote = "cart-empty-prod-quirk: skipped (depende do cart que prod não persistiu)";
|
|
51953
52105
|
reportStart(8, "shipping-calc-cart");
|
|
@@ -52288,9 +52440,22 @@ async function findPlusButtonNear(input) {
|
|
|
52288
52440
|
return null;
|
|
52289
52441
|
}
|
|
52290
52442
|
async function validateAddToCart(page, ctx, cartCountBefore, beforeUrl) {
|
|
52291
|
-
const
|
|
52443
|
+
const startedAt = Date.now();
|
|
52444
|
+
const budgetMs = resolveAddToCartConfirmMs(ctx.rc);
|
|
52445
|
+
const deadline = startedAt + budgetMs;
|
|
52292
52446
|
let lastErrorText;
|
|
52447
|
+
let pollCount = 0;
|
|
52448
|
+
const drawerSelectors = [
|
|
52449
|
+
...selFor(ctx, "cartOpenedIndicator"),
|
|
52450
|
+
"[role='dialog']",
|
|
52451
|
+
"[data-minicart][aria-expanded='true']",
|
|
52452
|
+
"[data-cart-drawer].open",
|
|
52453
|
+
"[data-minicart-drawer]:not([hidden])",
|
|
52454
|
+
".minicart--open",
|
|
52455
|
+
".cart-drawer--open"
|
|
52456
|
+
];
|
|
52293
52457
|
while (Date.now() < deadline) {
|
|
52458
|
+
pollCount++;
|
|
52294
52459
|
const currentUrl = page.url();
|
|
52295
52460
|
if (currentUrl !== beforeUrl && /\/(cart|carrinho|checkout)(\/|$|\?)/i.test(currentUrl)) {
|
|
52296
52461
|
return {
|
|
@@ -52307,15 +52472,6 @@ async function validateAddToCart(page, ctx, cartCountBefore, beforeUrl) {
|
|
|
52307
52472
|
note: `Contador do minicart foi de ${cartCountBefore} para ${cartCountNow}`
|
|
52308
52473
|
};
|
|
52309
52474
|
}
|
|
52310
|
-
const drawerSelectors = [
|
|
52311
|
-
...selFor(ctx, "cartOpenedIndicator"),
|
|
52312
|
-
"[role='dialog']",
|
|
52313
|
-
"[data-minicart][aria-expanded='true']",
|
|
52314
|
-
"[data-cart-drawer].open",
|
|
52315
|
-
"[data-minicart-drawer]:not([hidden])",
|
|
52316
|
-
".minicart--open",
|
|
52317
|
-
".cart-drawer--open"
|
|
52318
|
-
];
|
|
52319
52475
|
for (const sel of drawerSelectors) {
|
|
52320
52476
|
const el = page.locator(sel).first();
|
|
52321
52477
|
if (await el.isVisible({ timeout: 200 }).catch(() => false)) {
|
|
@@ -52348,18 +52504,32 @@ async function validateAddToCart(page, ctx, cartCountBefore, beforeUrl) {
|
|
|
52348
52504
|
} catch {}
|
|
52349
52505
|
await page.waitForTimeout(250);
|
|
52350
52506
|
}
|
|
52507
|
+
const diagnostics = {
|
|
52508
|
+
timedOut: true,
|
|
52509
|
+
budgetMs,
|
|
52510
|
+
elapsedMs: Date.now() - startedAt,
|
|
52511
|
+
pollCount,
|
|
52512
|
+
probes: await Promise.all(drawerSelectors.map(async (selector) => {
|
|
52513
|
+
const loc = page.locator(selector).first();
|
|
52514
|
+
const present = await loc.count().catch(() => 0) > 0;
|
|
52515
|
+
const visible = present ? await loc.isVisible({ timeout: 200 }).catch(() => false) : false;
|
|
52516
|
+
return { selector, present, visible };
|
|
52517
|
+
}))
|
|
52518
|
+
};
|
|
52351
52519
|
if (lastErrorText) {
|
|
52352
52520
|
return {
|
|
52353
52521
|
status: "failed",
|
|
52354
52522
|
signal: "error-text",
|
|
52355
52523
|
note: `add-to-cart silenciosamente falhou: '${lastErrorText}' visível na página`,
|
|
52356
|
-
errorText: lastErrorText
|
|
52524
|
+
errorText: lastErrorText,
|
|
52525
|
+
diagnostics
|
|
52357
52526
|
};
|
|
52358
52527
|
}
|
|
52359
52528
|
return {
|
|
52360
52529
|
status: "failed",
|
|
52361
52530
|
signal: "no-signal",
|
|
52362
|
-
note: "add-to-cart sem confirmação visível (minicart não atualizou, sem drawer, sem mudança de URL)"
|
|
52531
|
+
note: "add-to-cart sem confirmação visível (minicart não atualizou, sem drawer, sem mudança de URL)",
|
|
52532
|
+
diagnostics
|
|
52363
52533
|
};
|
|
52364
52534
|
}
|
|
52365
52535
|
|
|
@@ -58395,17 +58565,6 @@ function formatSecs(ms) {
|
|
|
58395
58565
|
return `${min}m${sec.toString().padStart(2, "0")}s`;
|
|
58396
58566
|
}
|
|
58397
58567
|
|
|
58398
|
-
// src/util/localhost.ts
|
|
58399
|
-
var LOCALHOST_HOSTS = new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"]);
|
|
58400
|
-
function isLocalhost(rawUrl) {
|
|
58401
|
-
try {
|
|
58402
|
-
const url = new URL(rawUrl);
|
|
58403
|
-
return LOCALHOST_HOSTS.has(url.hostname);
|
|
58404
|
-
} catch {
|
|
58405
|
-
return false;
|
|
58406
|
-
}
|
|
58407
|
-
}
|
|
58408
|
-
|
|
58409
58568
|
// src/util/timing.ts
|
|
58410
58569
|
async function withTiming(fn) {
|
|
58411
58570
|
const start = performance.now();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/parity",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.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",
|