@decocms/parity 0.16.0 → 0.17.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 +12 -0
- package/dist/cli.js +244 -54
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ 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
|
+
|
|
10
22
|
## [0.16.0](https://github.com/decocms/parity/compare/v0.15.1...v0.16.0) (2026-07-28)
|
|
11
23
|
|
|
12
24
|
### Added
|
package/dist/cli.js
CHANGED
|
@@ -39020,6 +39020,21 @@ async function capturePage(page, opts) {
|
|
|
39020
39020
|
let xRobotsTag = null;
|
|
39021
39021
|
let vitals = null;
|
|
39022
39022
|
let html3 = "";
|
|
39023
|
+
let _resolveDisconnect;
|
|
39024
|
+
const disconnectPromise = new Promise((r) => {
|
|
39025
|
+
_resolveDisconnect = r;
|
|
39026
|
+
});
|
|
39027
|
+
const browser = page.context().browser();
|
|
39028
|
+
const handleBrowserDisconnect = () => {
|
|
39029
|
+
dlog(opts.side, opts.viewport, " capturePage: browser disconnected — aborting capture");
|
|
39030
|
+
_resolveDisconnect();
|
|
39031
|
+
};
|
|
39032
|
+
const handlePageCrash = () => {
|
|
39033
|
+
dlog(opts.side, opts.viewport, " capturePage: page crashed — aborting capture");
|
|
39034
|
+
_resolveDisconnect();
|
|
39035
|
+
};
|
|
39036
|
+
browser?.on("disconnected", handleBrowserDisconnect);
|
|
39037
|
+
page.on("crash", handlePageCrash);
|
|
39023
39038
|
const inner = async () => {
|
|
39024
39039
|
try {
|
|
39025
39040
|
dlog(opts.side, opts.viewport, ` capturePage: goto(${opts.url}) start`);
|
|
@@ -39130,7 +39145,7 @@ async function capturePage(page, opts) {
|
|
|
39130
39145
|
};
|
|
39131
39146
|
const SAFETY_MARGIN_MS = 1e4;
|
|
39132
39147
|
const outerDeadlineMs = overallBudgetMs + SAFETY_MARGIN_MS;
|
|
39133
|
-
|
|
39148
|
+
const result = await Promise.race([
|
|
39134
39149
|
inner(),
|
|
39135
39150
|
new Promise((resolve) => setTimeout(() => {
|
|
39136
39151
|
state.console.push({
|
|
@@ -39138,8 +39153,18 @@ async function capturePage(page, opts) {
|
|
|
39138
39153
|
text: `[capture-timeout] capturePage exceeded ${outerDeadlineMs}ms outer deadline — returning partial capture`
|
|
39139
39154
|
});
|
|
39140
39155
|
resolve(buildPartial());
|
|
39141
|
-
}, outerDeadlineMs))
|
|
39156
|
+
}, outerDeadlineMs)),
|
|
39157
|
+
disconnectPromise.then(() => {
|
|
39158
|
+
state.console.push({
|
|
39159
|
+
type: "error",
|
|
39160
|
+
text: "[browser-disconnected] browser/page crashed — returning partial capture"
|
|
39161
|
+
});
|
|
39162
|
+
return buildPartial();
|
|
39163
|
+
})
|
|
39142
39164
|
]);
|
|
39165
|
+
browser?.off("disconnected", handleBrowserDisconnect);
|
|
39166
|
+
page.off("crash", handlePageCrash);
|
|
39167
|
+
return result;
|
|
39143
39168
|
}
|
|
39144
39169
|
|
|
39145
39170
|
// src/report/html-template.ts
|
|
@@ -40340,6 +40365,14 @@ var REPORT_CSS = `
|
|
|
40340
40365
|
color: var(--state-warn);
|
|
40341
40366
|
font-style: italic;
|
|
40342
40367
|
}
|
|
40368
|
+
.step-diagnostics {
|
|
40369
|
+
margin-top: 4px;
|
|
40370
|
+
font-size: 10px;
|
|
40371
|
+
color: var(--text-muted);
|
|
40372
|
+
border-left: 2px solid var(--state-warn);
|
|
40373
|
+
padding-left: 6px;
|
|
40374
|
+
}
|
|
40375
|
+
.step-diagnostics code { background: var(--surface-base); padding: 1px 4px; border-radius: 3px; }
|
|
40343
40376
|
|
|
40344
40377
|
/* Network waterfall — per-page SVG bar chart. Issue #78. */
|
|
40345
40378
|
.wf-page {
|
|
@@ -41154,6 +41187,18 @@ var StepCapture = z.object({
|
|
|
41154
41187
|
loginValidation: z.object({
|
|
41155
41188
|
stage: z.enum(["form-loaded", "submitted", "succeeded", "error-shown"]),
|
|
41156
41189
|
errorMessage: z.string().optional()
|
|
41190
|
+
}).optional(),
|
|
41191
|
+
diagnostics: z.object({
|
|
41192
|
+
timedOut: z.boolean().optional(),
|
|
41193
|
+
budgetMs: z.number().optional(),
|
|
41194
|
+
elapsedMs: z.number().optional(),
|
|
41195
|
+
pollCount: z.number().optional(),
|
|
41196
|
+
probes: z.array(z.object({
|
|
41197
|
+
selector: z.string(),
|
|
41198
|
+
present: z.boolean(),
|
|
41199
|
+
visible: z.boolean()
|
|
41200
|
+
})).optional(),
|
|
41201
|
+
toastText: z.string().optional()
|
|
41157
41202
|
}).optional()
|
|
41158
41203
|
});
|
|
41159
41204
|
var FlowCapture = z.object({
|
|
@@ -41436,7 +41481,8 @@ var ParityRc = z.object({
|
|
|
41436
41481
|
serverFnFloodBudget: z.number().optional(),
|
|
41437
41482
|
serverFnPattern: z.string().optional(),
|
|
41438
41483
|
overlaySelectors: z.array(z.string()).optional(),
|
|
41439
|
-
addToCartConfirmMs: z.number().optional()
|
|
41484
|
+
addToCartConfirmMs: z.number().optional(),
|
|
41485
|
+
cartRevealTimeoutMs: z.number().optional()
|
|
41440
41486
|
});
|
|
41441
41487
|
var ParityIgnore = z.object({
|
|
41442
41488
|
ignoreSelectorsVisual: z.array(z.string()).default([]),
|
|
@@ -47516,12 +47562,26 @@ function renderStepCell(s, runDir) {
|
|
|
47516
47562
|
const screenshot = s.screenshotPath ? `<a class="step-shot" href="${escapeHtml(relPath(runDir, s.screenshotPath))}" target="_blank" title="open screenshot">\uD83D\uDCF7</a>` : "";
|
|
47517
47563
|
const selector = s.usedSelector ? `<div class="step-selector"><code>${escapeHtml(s.usedSelector)}</code></div>` : "";
|
|
47518
47564
|
const note = s.note ? `<div class="step-note">${escapeHtml(s.note)}</div>` : "";
|
|
47565
|
+
const diagnostics = renderStepDiagnostics(s.diagnostics);
|
|
47519
47566
|
return `<td class="step-cell ${statusClass}">
|
|
47520
47567
|
<div class="step-cell-head"><span class="step-status">${s.status}</span>${screenshot}<span class="dim">${s.durationMs}ms</span></div>
|
|
47521
47568
|
${selector}
|
|
47522
47569
|
${note}
|
|
47570
|
+
${diagnostics}
|
|
47523
47571
|
</td>`;
|
|
47524
47572
|
}
|
|
47573
|
+
function renderStepDiagnostics(d) {
|
|
47574
|
+
if (!d?.timedOut)
|
|
47575
|
+
return "";
|
|
47576
|
+
const hidden = d.probes?.filter((p) => p.present && !p.visible) ?? [];
|
|
47577
|
+
const missing = d.probes?.filter((p) => !p.present) ?? [];
|
|
47578
|
+
const rows = [
|
|
47579
|
+
`<div>esperou ${d.elapsedMs}ms / orçamento ${d.budgetMs}ms (${d.pollCount} poll(s))</div>`,
|
|
47580
|
+
...hidden.map((p) => `<div>⚠️ presente porém oculto: <code>${escapeHtml(p.selector)}</code></div>`),
|
|
47581
|
+
...missing.map((p) => `<div>não encontrado no DOM: <code>${escapeHtml(p.selector)}</code></div>`)
|
|
47582
|
+
];
|
|
47583
|
+
return `<div class="step-diagnostics">${rows.join("")}</div>`;
|
|
47584
|
+
}
|
|
47525
47585
|
function renderChecksTable(run) {
|
|
47526
47586
|
return `
|
|
47527
47587
|
<div class="card">
|
|
@@ -49313,9 +49373,21 @@ async function cssTraceCommand(opts) {
|
|
|
49313
49373
|
|
|
49314
49374
|
// src/commands/e2e.ts
|
|
49315
49375
|
import { writeFileSync as writeFileSync10 } from "node:fs";
|
|
49376
|
+
import { spawn } from "node:child_process";
|
|
49316
49377
|
import chalk8 from "chalk";
|
|
49317
49378
|
import ora4 from "ora";
|
|
49318
49379
|
|
|
49380
|
+
// src/util/localhost.ts
|
|
49381
|
+
var LOCALHOST_HOSTS = new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"]);
|
|
49382
|
+
function isLocalhost(rawUrl) {
|
|
49383
|
+
try {
|
|
49384
|
+
const url = new URL(rawUrl);
|
|
49385
|
+
return LOCALHOST_HOSTS.has(url.hostname);
|
|
49386
|
+
} catch {
|
|
49387
|
+
return false;
|
|
49388
|
+
}
|
|
49389
|
+
}
|
|
49390
|
+
|
|
49319
49391
|
// src/llm/pick-plp.ts
|
|
49320
49392
|
init_client();
|
|
49321
49393
|
var PATH_BLOCKLIST = [
|
|
@@ -49494,6 +49566,8 @@ REGRAS CRÍTICAS:
|
|
|
49494
49566
|
|
|
49495
49567
|
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
49568
|
|
|
49569
|
+
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.
|
|
49570
|
+
|
|
49497
49571
|
Responda SEMPRE via tool_use suggest_recovery.
|
|
49498
49572
|
`.trim();
|
|
49499
49573
|
async function suggestRecovery(input) {
|
|
@@ -49504,6 +49578,7 @@ async function suggestRecovery(input) {
|
|
|
49504
49578
|
userText: `Step: ${input.stepName}
|
|
49505
49579
|
Ação desejada: ${input.intendedAction}
|
|
49506
49580
|
${input.alreadyTried?.length ? `Já tentei (não repita estes): ${input.alreadyTried.join(", ")}
|
|
49581
|
+
` : ""}${input.diagnostics ? `Diagnóstico: ${input.diagnostics}
|
|
49507
49582
|
` : ""}
|
|
49508
49583
|
HTML compactado da página NESTE momento:
|
|
49509
49584
|
\`\`\`html
|
|
@@ -50383,14 +50458,14 @@ async function findElement(page, ctx, opts) {
|
|
|
50383
50458
|
}
|
|
50384
50459
|
return null;
|
|
50385
50460
|
}
|
|
50386
|
-
async function attemptRecovery(page, _ctx, stepName, intendedAction, alreadyTried) {
|
|
50461
|
+
async function attemptRecovery(page, _ctx, stepName, intendedAction, alreadyTried, diagnostics) {
|
|
50387
50462
|
let html3 = "";
|
|
50388
50463
|
try {
|
|
50389
50464
|
html3 = await page.content();
|
|
50390
50465
|
} catch {
|
|
50391
50466
|
return null;
|
|
50392
50467
|
}
|
|
50393
|
-
const suggestion = await suggestRecovery({ stepName, intendedAction, html: html3, alreadyTried });
|
|
50468
|
+
const suggestion = await suggestRecovery({ stepName, intendedAction, html: html3, alreadyTried, diagnostics });
|
|
50394
50469
|
if (!suggestion)
|
|
50395
50470
|
return null;
|
|
50396
50471
|
try {
|
|
@@ -50866,6 +50941,66 @@ async function isCartRevealed(page, expectedProductTitle, ctx) {
|
|
|
50866
50941
|
}
|
|
50867
50942
|
return isCartUiVisible(page, ctx);
|
|
50868
50943
|
}
|
|
50944
|
+
var CART_REVEAL_BUDGET_MS = 4000;
|
|
50945
|
+
var CART_REVEAL_BUDGET_LOCALHOST_MS = 8000;
|
|
50946
|
+
var CART_REVEAL_POLL_INTERVAL_MS = 200;
|
|
50947
|
+
var CART_REVEAL_HOVER_PROBE_MS = 1500;
|
|
50948
|
+
async function probeCartSelectors(page, ctx) {
|
|
50949
|
+
const selectors = [...new Set([...selFor(ctx, "minicartPanel"), ...selFor(ctx, "cartOpenedIndicator")])];
|
|
50950
|
+
const probes = [];
|
|
50951
|
+
for (const sel of selectors) {
|
|
50952
|
+
try {
|
|
50953
|
+
const loc = page.locator(sel);
|
|
50954
|
+
const present = await withCap(loc.count(), 300, 0) > 0;
|
|
50955
|
+
const visible = present ? await withCap(loc.first().isVisible().catch(() => false), 300, false) : false;
|
|
50956
|
+
probes.push({ selector: sel, present, visible });
|
|
50957
|
+
} catch {
|
|
50958
|
+
probes.push({ selector: sel, present: false, visible: false });
|
|
50959
|
+
}
|
|
50960
|
+
}
|
|
50961
|
+
return probes;
|
|
50962
|
+
}
|
|
50963
|
+
function summarizeCartRevealDiagnostics(d) {
|
|
50964
|
+
const parts = [`waited ${d.elapsedMs}ms/${d.budgetMs}ms (${d.pollCount} poll(s))`];
|
|
50965
|
+
const hidden = d.probes?.filter((p) => p.present && !p.visible) ?? [];
|
|
50966
|
+
const missing = d.probes?.filter((p) => !p.present) ?? [];
|
|
50967
|
+
if (hidden.length) {
|
|
50968
|
+
parts.push(`present-but-hidden: ${hidden.map((p) => p.selector).join(", ")}`);
|
|
50969
|
+
}
|
|
50970
|
+
if (missing.length) {
|
|
50971
|
+
parts.push(`not-in-dom: ${missing.map((p) => p.selector).join(", ")}`);
|
|
50972
|
+
}
|
|
50973
|
+
return parts.join(" — ");
|
|
50974
|
+
}
|
|
50975
|
+
async function waitForCartReveal(page, expectedProductTitle, ctx, timeoutMs) {
|
|
50976
|
+
const start = Date.now();
|
|
50977
|
+
const deadline = start + timeoutMs;
|
|
50978
|
+
let pollCount = 0;
|
|
50979
|
+
for (;; ) {
|
|
50980
|
+
pollCount++;
|
|
50981
|
+
const marker = await isCartRevealed(page, expectedProductTitle, ctx);
|
|
50982
|
+
if (marker) {
|
|
50983
|
+
dlog2(ctx, ` waitForCartReveal: revealed after ${pollCount} poll(s) (${marker})`);
|
|
50984
|
+
return {
|
|
50985
|
+
marker,
|
|
50986
|
+
diagnostics: { timedOut: false, budgetMs: timeoutMs, elapsedMs: Date.now() - start, pollCount }
|
|
50987
|
+
};
|
|
50988
|
+
}
|
|
50989
|
+
if (Date.now() >= deadline) {
|
|
50990
|
+
const probes = await probeCartSelectors(page, ctx);
|
|
50991
|
+
const diagnostics = {
|
|
50992
|
+
timedOut: true,
|
|
50993
|
+
budgetMs: timeoutMs,
|
|
50994
|
+
elapsedMs: Date.now() - start,
|
|
50995
|
+
pollCount,
|
|
50996
|
+
probes
|
|
50997
|
+
};
|
|
50998
|
+
dlog2(ctx, ` waitForCartReveal: ${summarizeCartRevealDiagnostics(diagnostics)}`);
|
|
50999
|
+
return { marker: null, diagnostics };
|
|
51000
|
+
}
|
|
51001
|
+
await page.waitForTimeout(CART_REVEAL_POLL_INTERVAL_MS);
|
|
51002
|
+
}
|
|
51003
|
+
}
|
|
50869
51004
|
async function validateCartContainsTitleQuick(page, expectedTitle, ctx) {
|
|
50870
51005
|
const quickSelectors = [
|
|
50871
51006
|
...ctx ? selFor(ctx, "minicartPanel") : [],
|
|
@@ -50903,7 +51038,9 @@ async function waitForCartHydration(page) {
|
|
|
50903
51038
|
}
|
|
50904
51039
|
async function openMinicart(page, trigger, ctx, expectedProductTitle) {
|
|
50905
51040
|
const beforeUrl = page.url();
|
|
50906
|
-
|
|
51041
|
+
let lastDiagnostics;
|
|
51042
|
+
const revealBudget = ctx.rc.cartRevealTimeoutMs ?? (isLocalhost(beforeUrl) ? CART_REVEAL_BUDGET_LOCALHOST_MS : CART_REVEAL_BUDGET_MS);
|
|
51043
|
+
dlog2(ctx, ` openMinicart: starting — trigger=${trigger.selector} title=${expectedProductTitle?.slice(0, 40) ?? "none"} revealBudget=${revealBudget}ms`);
|
|
50907
51044
|
await Promise.race([
|
|
50908
51045
|
dismissOverlays(page, ctx),
|
|
50909
51046
|
new Promise((resolve) => setTimeout(resolve, 4000))
|
|
@@ -50922,11 +51059,12 @@ async function openMinicart(page, trigger, ctx, expectedProductTitle) {
|
|
|
50922
51059
|
await trigger.locator.hover({ timeout: 3000 }).catch(() => {
|
|
50923
51060
|
return;
|
|
50924
51061
|
});
|
|
50925
|
-
|
|
50926
|
-
const
|
|
50927
|
-
|
|
50928
|
-
|
|
50929
|
-
|
|
51062
|
+
const hoverProbeBudget = Math.min(CART_REVEAL_HOVER_PROBE_MS, revealBudget);
|
|
51063
|
+
const hover1 = await waitForCartReveal(page, expectedProductTitle, ctx, hoverProbeBudget);
|
|
51064
|
+
lastDiagnostics = hover1.diagnostics;
|
|
51065
|
+
if (hover1.marker) {
|
|
51066
|
+
dlog2(ctx, ` openMinicart: hover opened drawer (${hover1.marker})`);
|
|
51067
|
+
return { method: "hover", url: page.url(), visibleMarker: hover1.marker, diagnostics: hover1.diagnostics };
|
|
50930
51068
|
}
|
|
50931
51069
|
}
|
|
50932
51070
|
if (ctx.viewport === "mobile") {
|
|
@@ -50947,11 +51085,11 @@ async function openMinicart(page, trigger, ctx, expectedProductTitle) {
|
|
|
50947
51085
|
dlog2(ctx, ` openMinicart: tap navigated → ${page.url()} (settled)`);
|
|
50948
51086
|
return { method: "click-navigate", url: page.url(), visibleMarker: null };
|
|
50949
51087
|
}
|
|
50950
|
-
await page
|
|
50951
|
-
|
|
50952
|
-
if (
|
|
50953
|
-
dlog2(ctx, ` openMinicart: tap opened drawer (${
|
|
50954
|
-
return { method: "click", url: page.url(), visibleMarker:
|
|
51088
|
+
const tap = await waitForCartReveal(page, expectedProductTitle, ctx, revealBudget);
|
|
51089
|
+
lastDiagnostics = tap.diagnostics;
|
|
51090
|
+
if (tap.marker) {
|
|
51091
|
+
dlog2(ctx, ` openMinicart: tap opened drawer (${tap.marker})`);
|
|
51092
|
+
return { method: "click", url: page.url(), visibleMarker: tap.marker, diagnostics: tap.diagnostics };
|
|
50955
51093
|
}
|
|
50956
51094
|
}
|
|
50957
51095
|
dlog2(ctx, ` openMinicart: trying force-click on ${trigger.selector}${triggerHref ? ` (href=${triggerHref})` : ""}`);
|
|
@@ -50972,22 +51110,35 @@ async function openMinicart(page, trigger, ctx, expectedProductTitle) {
|
|
|
50972
51110
|
dlog2(ctx, ` openMinicart: click navigated → ${page.url()} (settled)`);
|
|
50973
51111
|
return { method: "click-navigate", url: page.url(), visibleMarker: null };
|
|
50974
51112
|
}
|
|
50975
|
-
await page
|
|
50976
|
-
|
|
50977
|
-
if (
|
|
50978
|
-
dlog2(ctx, ` openMinicart: click opened drawer (${
|
|
50979
|
-
return { method: "click", url: afterClickUrl, visibleMarker:
|
|
51113
|
+
const click = await waitForCartReveal(page, expectedProductTitle, ctx, revealBudget);
|
|
51114
|
+
lastDiagnostics = click.diagnostics;
|
|
51115
|
+
if (click.marker) {
|
|
51116
|
+
dlog2(ctx, ` openMinicart: click opened drawer (${click.marker})`);
|
|
51117
|
+
return { method: "click", url: afterClickUrl, visibleMarker: click.marker, diagnostics: click.diagnostics };
|
|
51118
|
+
}
|
|
51119
|
+
const blockingOverlay = await dismissBlockingOverlay(page, ctx, trigger.locator);
|
|
51120
|
+
if (blockingOverlay?.dismissed) {
|
|
51121
|
+
dlog2(ctx, ` openMinicart: cleared blocking overlay (${blockingOverlay.method}) — retrying click`);
|
|
51122
|
+
await trigger.locator.click({ force: true, timeout: 4000 }).catch(() => {
|
|
51123
|
+
return;
|
|
51124
|
+
});
|
|
51125
|
+
const retry = await waitForCartReveal(page, expectedProductTitle, ctx, revealBudget);
|
|
51126
|
+
lastDiagnostics = retry.diagnostics;
|
|
51127
|
+
if (retry.marker) {
|
|
51128
|
+
dlog2(ctx, ` openMinicart: click opened drawer after overlay dismissal (${retry.marker})`);
|
|
51129
|
+
return { method: "click", url: page.url(), visibleMarker: retry.marker, diagnostics: retry.diagnostics };
|
|
51130
|
+
}
|
|
50980
51131
|
}
|
|
50981
51132
|
if (ctx.viewport !== "desktop") {
|
|
50982
51133
|
dlog2(ctx, ` openMinicart: click didn't reveal cart, trying hover (mobile)`);
|
|
50983
51134
|
await trigger.locator.hover({ timeout: 3000 }).catch(() => {
|
|
50984
51135
|
return;
|
|
50985
51136
|
});
|
|
50986
|
-
await page
|
|
50987
|
-
|
|
50988
|
-
if (
|
|
50989
|
-
dlog2(ctx, ` openMinicart: hover opened drawer (${
|
|
50990
|
-
return { method: "hover", url: page.url(), visibleMarker:
|
|
51137
|
+
const hover2 = await waitForCartReveal(page, expectedProductTitle, ctx, revealBudget);
|
|
51138
|
+
lastDiagnostics = hover2.diagnostics;
|
|
51139
|
+
if (hover2.marker) {
|
|
51140
|
+
dlog2(ctx, ` openMinicart: hover opened drawer (${hover2.marker})`);
|
|
51141
|
+
return { method: "hover", url: page.url(), visibleMarker: hover2.marker, diagnostics: hover2.diagnostics };
|
|
50991
51142
|
}
|
|
50992
51143
|
}
|
|
50993
51144
|
if (hrefHasCartTarget && triggerHref) {
|
|
@@ -51010,8 +51161,8 @@ async function openMinicart(page, trigger, ctx, expectedProductTitle) {
|
|
|
51010
51161
|
}
|
|
51011
51162
|
}
|
|
51012
51163
|
}
|
|
51013
|
-
dlog2(ctx,
|
|
51014
|
-
return { method: "failed", url: page.url(), visibleMarker: null };
|
|
51164
|
+
dlog2(ctx, ` openMinicart: failed — no cart revealed by hover/click/goto${lastDiagnostics ? ` (${summarizeCartRevealDiagnostics(lastDiagnostics)})` : ""}`);
|
|
51165
|
+
return { method: "failed", url: page.url(), visibleMarker: null, diagnostics: lastDiagnostics };
|
|
51015
51166
|
}
|
|
51016
51167
|
function normalizeTitle(s) {
|
|
51017
51168
|
return s.toLowerCase().replace(/[®©™]/g, "").replace(/[^\p{L}\p{N}\s]+/gu, " ").replace(/\s+/g, " ").trim();
|
|
@@ -51868,6 +52019,7 @@ async function flowPurchaseJourney(ctx) {
|
|
|
51868
52019
|
usedSelector: buyHit.selector,
|
|
51869
52020
|
recoveredByLlm: buyRecovered || undefined,
|
|
51870
52021
|
note: validation.status === "ok" ? undefined : validation.note,
|
|
52022
|
+
diagnostics: validation.diagnostics,
|
|
51871
52023
|
detail: {
|
|
51872
52024
|
signal: validation.signal,
|
|
51873
52025
|
errorText: validation.errorText,
|
|
@@ -51897,13 +52049,26 @@ async function flowPurchaseJourney(ctx) {
|
|
|
51897
52049
|
let cartOpenMethod = "failed";
|
|
51898
52050
|
let miniText = "";
|
|
51899
52051
|
let cartRevealMode = "unknown";
|
|
52052
|
+
let revealDiagnostics;
|
|
51900
52053
|
const drawerAlreadyOpen = await isCartRevealed(page, expectedProductTitle, ctx) !== null;
|
|
51901
52054
|
if (miniHit) {
|
|
51902
52055
|
miniText = await miniHit.locator.innerText().catch(() => "");
|
|
51903
52056
|
cartRevealMode = await detectCartRevealMode(page, miniHit.locator, drawerAlreadyOpen, ctx.viewport).catch(() => "unknown");
|
|
51904
52057
|
dlog2(ctx, `step 7 open-minicart: cartRevealMode=${cartRevealMode}`);
|
|
51905
|
-
|
|
52058
|
+
let openResult = await openMinicart(page, miniHit, ctx, expectedProductTitle);
|
|
51906
52059
|
cartOpenMethod = openResult.method;
|
|
52060
|
+
revealDiagnostics = openResult.diagnostics;
|
|
52061
|
+
if (cartOpenMethod === "failed" && revealDiagnostics && budget.remaining > 0) {
|
|
52062
|
+
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));
|
|
52063
|
+
if (recovery) {
|
|
52064
|
+
budget.remaining--;
|
|
52065
|
+
miniHit = recovery;
|
|
52066
|
+
miniRecovered = true;
|
|
52067
|
+
openResult = await openMinicart(page, miniHit, ctx, expectedProductTitle);
|
|
52068
|
+
cartOpenMethod = openResult.method;
|
|
52069
|
+
revealDiagnostics = openResult.diagnostics;
|
|
52070
|
+
}
|
|
52071
|
+
}
|
|
51907
52072
|
} else {
|
|
51908
52073
|
if (drawerAlreadyOpen) {
|
|
51909
52074
|
cartOpenMethod = "already-open";
|
|
@@ -51938,6 +52103,7 @@ async function flowPurchaseJourney(ctx) {
|
|
|
51938
52103
|
const isProdCartEmptyQuirk = ctx.acceptProdQuirks === true && ctx.side === "prod" && step7Validation !== undefined && !step7Validation.found && cartEmptyReason.startsWith("cart genuinely empty");
|
|
51939
52104
|
const step7Status = isProdCartEmptyQuirk ? "skipped" : cartOpenMethod === "failed" ? "failed" : step7Validation && !step7Validation.found ? "failed" : "ok";
|
|
51940
52105
|
const step7QuirkNote = isProdCartEmptyQuirk ? "cart-empty-prod-quirk: aceito via --accept-prod-quirks (cartRevealMode validated by separate check)" : undefined;
|
|
52106
|
+
const step7RevealFailureNote = cartOpenMethod === "failed" && revealDiagnostics ? `minicart não revelou — ${summarizeCartRevealDiagnostics(revealDiagnostics)}` : undefined;
|
|
51941
52107
|
steps.push({
|
|
51942
52108
|
step: 7,
|
|
51943
52109
|
name: "open-minicart",
|
|
@@ -51952,13 +52118,14 @@ async function flowPurchaseJourney(ctx) {
|
|
|
51952
52118
|
cartOpenMethod,
|
|
51953
52119
|
cartRevealMode,
|
|
51954
52120
|
cartValidation: step7Validation,
|
|
51955
|
-
|
|
52121
|
+
diagnostics: revealDiagnostics,
|
|
52122
|
+
note: step7QuirkNote ?? step7RevealFailureNote,
|
|
51956
52123
|
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",
|
|
51957
52124
|
selectorKey: miniHit ? "minicartTrigger" : undefined,
|
|
51958
52125
|
usedSelector: miniHit?.selector,
|
|
51959
52126
|
recoveredByLlm: miniRecovered || undefined
|
|
51960
52127
|
});
|
|
51961
|
-
reportEnd(7, "open-minicart", step7Status, Date.now() - t7, step7QuirkNote);
|
|
52128
|
+
reportEnd(7, "open-minicart", step7Status, Date.now() - t7, step7QuirkNote ?? step7RevealFailureNote);
|
|
51962
52129
|
if (isProdCartEmptyQuirk) {
|
|
51963
52130
|
const quirkNote = "cart-empty-prod-quirk: skipped (depende do cart que prod não persistiu)";
|
|
51964
52131
|
reportStart(8, "shipping-calc-cart");
|
|
@@ -52299,9 +52466,22 @@ async function findPlusButtonNear(input) {
|
|
|
52299
52466
|
return null;
|
|
52300
52467
|
}
|
|
52301
52468
|
async function validateAddToCart(page, ctx, cartCountBefore, beforeUrl) {
|
|
52302
|
-
const
|
|
52469
|
+
const startedAt = Date.now();
|
|
52470
|
+
const budgetMs = resolveAddToCartConfirmMs(ctx.rc);
|
|
52471
|
+
const deadline = startedAt + budgetMs;
|
|
52303
52472
|
let lastErrorText;
|
|
52473
|
+
let pollCount = 0;
|
|
52474
|
+
const drawerSelectors = [
|
|
52475
|
+
...selFor(ctx, "cartOpenedIndicator"),
|
|
52476
|
+
"[role='dialog']",
|
|
52477
|
+
"[data-minicart][aria-expanded='true']",
|
|
52478
|
+
"[data-cart-drawer].open",
|
|
52479
|
+
"[data-minicart-drawer]:not([hidden])",
|
|
52480
|
+
".minicart--open",
|
|
52481
|
+
".cart-drawer--open"
|
|
52482
|
+
];
|
|
52304
52483
|
while (Date.now() < deadline) {
|
|
52484
|
+
pollCount++;
|
|
52305
52485
|
const currentUrl = page.url();
|
|
52306
52486
|
if (currentUrl !== beforeUrl && /\/(cart|carrinho|checkout)(\/|$|\?)/i.test(currentUrl)) {
|
|
52307
52487
|
return {
|
|
@@ -52318,15 +52498,6 @@ async function validateAddToCart(page, ctx, cartCountBefore, beforeUrl) {
|
|
|
52318
52498
|
note: `Contador do minicart foi de ${cartCountBefore} para ${cartCountNow}`
|
|
52319
52499
|
};
|
|
52320
52500
|
}
|
|
52321
|
-
const drawerSelectors = [
|
|
52322
|
-
...selFor(ctx, "cartOpenedIndicator"),
|
|
52323
|
-
"[role='dialog']",
|
|
52324
|
-
"[data-minicart][aria-expanded='true']",
|
|
52325
|
-
"[data-cart-drawer].open",
|
|
52326
|
-
"[data-minicart-drawer]:not([hidden])",
|
|
52327
|
-
".minicart--open",
|
|
52328
|
-
".cart-drawer--open"
|
|
52329
|
-
];
|
|
52330
52501
|
for (const sel of drawerSelectors) {
|
|
52331
52502
|
const el = page.locator(sel).first();
|
|
52332
52503
|
if (await el.isVisible({ timeout: 200 }).catch(() => false)) {
|
|
@@ -52359,18 +52530,32 @@ async function validateAddToCart(page, ctx, cartCountBefore, beforeUrl) {
|
|
|
52359
52530
|
} catch {}
|
|
52360
52531
|
await page.waitForTimeout(250);
|
|
52361
52532
|
}
|
|
52533
|
+
const diagnostics = {
|
|
52534
|
+
timedOut: true,
|
|
52535
|
+
budgetMs,
|
|
52536
|
+
elapsedMs: Date.now() - startedAt,
|
|
52537
|
+
pollCount,
|
|
52538
|
+
probes: await Promise.all(drawerSelectors.map(async (selector) => {
|
|
52539
|
+
const loc = page.locator(selector).first();
|
|
52540
|
+
const present = await loc.count().catch(() => 0) > 0;
|
|
52541
|
+
const visible = present ? await loc.isVisible({ timeout: 200 }).catch(() => false) : false;
|
|
52542
|
+
return { selector, present, visible };
|
|
52543
|
+
}))
|
|
52544
|
+
};
|
|
52362
52545
|
if (lastErrorText) {
|
|
52363
52546
|
return {
|
|
52364
52547
|
status: "failed",
|
|
52365
52548
|
signal: "error-text",
|
|
52366
52549
|
note: `add-to-cart silenciosamente falhou: '${lastErrorText}' visível na página`,
|
|
52367
|
-
errorText: lastErrorText
|
|
52550
|
+
errorText: lastErrorText,
|
|
52551
|
+
diagnostics
|
|
52368
52552
|
};
|
|
52369
52553
|
}
|
|
52370
52554
|
return {
|
|
52371
52555
|
status: "failed",
|
|
52372
52556
|
signal: "no-signal",
|
|
52373
|
-
note: "add-to-cart sem confirmação visível (minicart não atualizou, sem drawer, sem mudança de URL)"
|
|
52557
|
+
note: "add-to-cart sem confirmação visível (minicart não atualizou, sem drawer, sem mudança de URL)",
|
|
52558
|
+
diagnostics
|
|
52374
52559
|
};
|
|
52375
52560
|
}
|
|
52376
52561
|
|
|
@@ -54988,6 +55173,19 @@ async function e2eCommand(opts) {
|
|
|
54988
55173
|
quiet: opts.json === true
|
|
54989
55174
|
});
|
|
54990
55175
|
}
|
|
55176
|
+
const E2E_TIMEOUT_MS = 3 * 60 * 1000;
|
|
55177
|
+
const watchdogScript = `setTimeout(()=>{try{process.kill(${process.pid},9)}catch(_){}},${E2E_TIMEOUT_MS})`;
|
|
55178
|
+
const watchdog = spawn(process.execPath, ["-e", watchdogScript], {
|
|
55179
|
+
stdio: "ignore",
|
|
55180
|
+
detached: false
|
|
55181
|
+
});
|
|
55182
|
+
watchdog.unref();
|
|
55183
|
+
const onSignal = () => {
|
|
55184
|
+
watchdog.kill();
|
|
55185
|
+
process.exit(1);
|
|
55186
|
+
};
|
|
55187
|
+
process.once("SIGINT", onSignal);
|
|
55188
|
+
process.once("SIGTERM", onSignal);
|
|
54991
55189
|
const spinner = opts.json ? null : ora4("Lançando browser…").start();
|
|
54992
55190
|
let browser = null;
|
|
54993
55191
|
const allFlows = [];
|
|
@@ -55031,6 +55229,9 @@ async function e2eCommand(opts) {
|
|
|
55031
55229
|
if (spinner)
|
|
55032
55230
|
spinner.succeed(`${allFlows.length} flow capture(s), ${allPages.length} página(s)`);
|
|
55033
55231
|
} finally {
|
|
55232
|
+
watchdog.kill();
|
|
55233
|
+
process.off("SIGINT", onSignal);
|
|
55234
|
+
process.off("SIGTERM", onSignal);
|
|
55034
55235
|
await browser?.close().catch(() => {
|
|
55035
55236
|
return;
|
|
55036
55237
|
});
|
|
@@ -58406,17 +58607,6 @@ function formatSecs(ms) {
|
|
|
58406
58607
|
return `${min}m${sec.toString().padStart(2, "0")}s`;
|
|
58407
58608
|
}
|
|
58408
58609
|
|
|
58409
|
-
// src/util/localhost.ts
|
|
58410
|
-
var LOCALHOST_HOSTS = new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"]);
|
|
58411
|
-
function isLocalhost(rawUrl) {
|
|
58412
|
-
try {
|
|
58413
|
-
const url = new URL(rawUrl);
|
|
58414
|
-
return LOCALHOST_HOSTS.has(url.hostname);
|
|
58415
|
-
} catch {
|
|
58416
|
-
return false;
|
|
58417
|
-
}
|
|
58418
|
-
}
|
|
58419
|
-
|
|
58420
58610
|
// src/util/timing.ts
|
|
58421
58611
|
async function withTiming(fn) {
|
|
58422
58612
|
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.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",
|