@decocms/parity 0.2.0 → 0.3.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 +30 -0
- package/dist/cli.js +123 -43
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
7
|
|
|
8
|
+
## [0.3.0](https://github.com/decocms/parity/compare/v0.2.0...v0.3.0) (2026-05-26)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
* **journey:** retry `go-checkout` via LLM recovery when the default selector clicks the wrong element and the URL never reaches `/checkout` ([5826d30](https://github.com/decocms/parity/commit/5826d309c6a910f8cf8017667cdcdcebd65f65d9))
|
|
14
|
+
* **journey:** LLM recovery on `cep-pdp` + `cep-cart` when defaults miss the CEP input ([624b0f5](https://github.com/decocms/parity/commit/624b0f5acbe1c50fce2bea69faa6906e29d36e64))
|
|
15
|
+
* **journey:** per-flow hard deadline so a single hung flow can't freeze the whole crawl ([e2d4a68](https://github.com/decocms/parity/commit/e2d4a681a5d4f0a9627251eceb1e733d12bdc68d))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
* **journey:** abort in-flight Playwright ops when the deadline fires, instead of letting them mutate the next flow's shared BrowserContext ([a21aa78](https://github.com/decocms/parity/commit/a21aa78a4431e7a2cff9aac26ac34c4ca2fa768c))
|
|
21
|
+
* **journey:** seal the timeout FlowCapture synchronously so Promise.race can't pick up the inner rejection caused by closing pages ([d2a0f1e](https://github.com/decocms/parity/commit/d2a0f1e7a08bf95a28a951bace322c5baf062bea))
|
|
22
|
+
* **journey:** await timeout cleanup before runFlow returns so the next flow on the same context isn't racing in-flight close()s ([a2fd6c2](https://github.com/decocms/parity/commit/a2fd6c2e7e7a9eb493d2cb75d08f3d5cc7cc3f91))
|
|
23
|
+
|
|
24
|
+
## [0.2.0](https://github.com/decocms/parity/compare/v0.1.1...v0.2.0) (2026-05-26)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
* **css-trace:** inspect CSS rules affecting a DOM element ([1e323fa](https://github.com/decocms/parity/commit/1e323fa17564045d5f55bfbc5cbf2fcdb0112877))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
* **capture:** hard outer deadline so capturePage cannot exceed budget+10s ([966b9a5](https://github.com/decocms/parity/commit/966b9a59851536262b88510da6175a215c90d0b2))
|
|
35
|
+
* **capture:** hard outer deadline so capturePage cannot exceed budget+10s ([ea61116](https://github.com/decocms/parity/commit/ea61116c81fb1ec86e86ddb24216214380a81c81))
|
|
36
|
+
* **lint:** replace 3 template literals without interpolation with strings ([c60f19c](https://github.com/decocms/parity/commit/c60f19c4a56c8441ead829fca0998ee36a671872))
|
|
37
|
+
|
|
8
38
|
## [Unreleased]
|
|
9
39
|
|
|
10
40
|
## [0.1.1] — 2026-05-22
|
package/dist/cli.js
CHANGED
|
@@ -5059,19 +5059,60 @@ var PURCHASE_JOURNEY_TOTAL_STEPS = 8;
|
|
|
5059
5059
|
function selFor(ctx, key) {
|
|
5060
5060
|
return selectorsFor(key, { rc: ctx.rc, learned: ctx.learned, platform: ctx.platform });
|
|
5061
5061
|
}
|
|
5062
|
+
var FLOW_DEADLINE_MS = {
|
|
5063
|
+
homepage: 90000,
|
|
5064
|
+
plp: 180000,
|
|
5065
|
+
pdp: 240000,
|
|
5066
|
+
"purchase-journey": 360000
|
|
5067
|
+
};
|
|
5062
5068
|
async function runFlow(flow, ctx) {
|
|
5063
5069
|
const start = Date.now();
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5070
|
+
const deadlineMs = FLOW_DEADLINE_MS[flow];
|
|
5071
|
+
const inner = async () => {
|
|
5072
|
+
switch (flow) {
|
|
5073
|
+
case "homepage":
|
|
5074
|
+
return finalize(flow, ctx, await flowHomepage(ctx), [], start);
|
|
5075
|
+
case "plp":
|
|
5076
|
+
return finalize(flow, ctx, await flowPlp(ctx), [], start);
|
|
5077
|
+
case "pdp":
|
|
5078
|
+
return finalize(flow, ctx, await flowPdp(ctx), [], start);
|
|
5079
|
+
case "purchase-journey": {
|
|
5080
|
+
const { pages, steps } = await flowPurchaseJourney(ctx);
|
|
5081
|
+
return finalize(flow, ctx, pages, steps, start);
|
|
5082
|
+
}
|
|
5074
5083
|
}
|
|
5084
|
+
};
|
|
5085
|
+
const innerPromise = inner();
|
|
5086
|
+
innerPromise.catch(() => {
|
|
5087
|
+
return;
|
|
5088
|
+
});
|
|
5089
|
+
let timer;
|
|
5090
|
+
let cleanup = Promise.resolve();
|
|
5091
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
5092
|
+
timer = setTimeout(() => {
|
|
5093
|
+
const pages = ctx.ctx.pages();
|
|
5094
|
+
resolve(finalize(flow, ctx, [], [
|
|
5095
|
+
{
|
|
5096
|
+
step: 0,
|
|
5097
|
+
name: "visit-home",
|
|
5098
|
+
side: ctx.side,
|
|
5099
|
+
viewport: ctx.viewport,
|
|
5100
|
+
status: "failed",
|
|
5101
|
+
durationMs: deadlineMs,
|
|
5102
|
+
screenshotPath: "",
|
|
5103
|
+
actionDescription: `[flow-timeout] flow "${flow}" excedeu ${deadlineMs}ms — captura abortada pela safety net externa, ${pages.length} page(s) fechada(s) para liberar o contexto. Step interno provavelmente travou em uma operação Playwright que não respeitou seu timeout declarado.`
|
|
5104
|
+
}
|
|
5105
|
+
], start));
|
|
5106
|
+
cleanup = Promise.allSettled(pages.map((p) => p.close()));
|
|
5107
|
+
}, deadlineMs);
|
|
5108
|
+
});
|
|
5109
|
+
try {
|
|
5110
|
+
const result = await Promise.race([innerPromise, timeoutPromise]);
|
|
5111
|
+
await cleanup;
|
|
5112
|
+
return result;
|
|
5113
|
+
} finally {
|
|
5114
|
+
if (timer !== undefined)
|
|
5115
|
+
clearTimeout(timer);
|
|
5075
5116
|
}
|
|
5076
5117
|
}
|
|
5077
5118
|
function finalize(flow, ctx, pages, steps, start) {
|
|
@@ -5264,7 +5305,16 @@ async function flowPurchaseJourney(ctx) {
|
|
|
5264
5305
|
steps[steps.length - 1].actionDescription = `Abriu PDP \`${pdpHit.url}\` (via \`${pdpHit.selector}\`)`;
|
|
5265
5306
|
steps[steps.length - 1].beforeUrl = plpHit.url;
|
|
5266
5307
|
reportStart(4, "shipping-calc-pdp");
|
|
5267
|
-
|
|
5308
|
+
let cepInputPdp = await firstVisible(page, selFor(ctx, "cepInputPdp"));
|
|
5309
|
+
let cepPdpRecovered = false;
|
|
5310
|
+
if (!cepInputPdp && recoveryBudget > 0) {
|
|
5311
|
+
const recovery = await attemptRecovery(page, ctx, "shipping-calc-pdp", "Achar o input de CEP / código postal nesta PDP (deve ser um input visível com label/placeholder relacionado a frete, entrega ou CEP)", selFor(ctx, "cepInputPdp"));
|
|
5312
|
+
if (recovery) {
|
|
5313
|
+
cepInputPdp = recovery.selector;
|
|
5314
|
+
cepPdpRecovered = true;
|
|
5315
|
+
recoveryBudget--;
|
|
5316
|
+
}
|
|
5317
|
+
}
|
|
5268
5318
|
if (cepInputPdp) {
|
|
5269
5319
|
const t4 = Date.now();
|
|
5270
5320
|
const beforeUrl4 = page.url();
|
|
@@ -5289,14 +5339,15 @@ async function flowPurchaseJourney(ctx) {
|
|
|
5289
5339
|
screenshotPath: sp,
|
|
5290
5340
|
screenshotBeforePath: spBefore4,
|
|
5291
5341
|
beforeUrl: beforeUrl4,
|
|
5292
|
-
actionDescription: `Preencheu CEP '${ctx.rc.cep}' no input \`${cepInputPdp}\` e disparou cálculo de frete`,
|
|
5342
|
+
actionDescription: `Preencheu CEP '${ctx.rc.cep}' no input \`${cepInputPdp}\` e disparou cálculo de frete${cepPdpRecovered ? " (selector via LLM recovery)" : ""}`,
|
|
5293
5343
|
detail: { cepUsed: ctx.rc.cep },
|
|
5294
5344
|
selectorKey: "cepInputPdp",
|
|
5295
|
-
usedSelector: cepInputPdp
|
|
5345
|
+
usedSelector: cepInputPdp,
|
|
5346
|
+
recoveredByLlm: cepPdpRecovered || undefined
|
|
5296
5347
|
});
|
|
5297
5348
|
reportEnd(4, "shipping-calc-pdp", step4Status, Date.now() - t4);
|
|
5298
5349
|
} else {
|
|
5299
|
-
steps.push(makeSkipStep(4, "shipping-calc-pdp", ctx, "no CEP input on PDP"));
|
|
5350
|
+
steps.push(makeSkipStep(4, "shipping-calc-pdp", ctx, "no CEP input on PDP (recovery exhausted)"));
|
|
5300
5351
|
reportEnd(4, "shipping-calc-pdp", "skipped", 0, "no CEP input on PDP");
|
|
5301
5352
|
}
|
|
5302
5353
|
reportStart(5, "add-to-cart");
|
|
@@ -5394,7 +5445,16 @@ async function flowPurchaseJourney(ctx) {
|
|
|
5394
5445
|
});
|
|
5395
5446
|
reportEnd(6, "open-minicart", "ok", Date.now() - t6);
|
|
5396
5447
|
reportStart(7, "shipping-calc-cart");
|
|
5397
|
-
|
|
5448
|
+
let cepInputCart = await firstVisible(page, selFor(ctx, "cepInputCart"));
|
|
5449
|
+
let cepCartRecovered = false;
|
|
5450
|
+
if (!cepInputCart && recoveryBudget > 0) {
|
|
5451
|
+
const recovery = await attemptRecovery(page, ctx, "shipping-calc-cart", "Achar o input de CEP / código postal dentro do carrinho ou minicart aberto agora (input visível com label/placeholder de frete, entrega ou CEP)", selFor(ctx, "cepInputCart"));
|
|
5452
|
+
if (recovery) {
|
|
5453
|
+
cepInputCart = recovery.selector;
|
|
5454
|
+
cepCartRecovered = true;
|
|
5455
|
+
recoveryBudget--;
|
|
5456
|
+
}
|
|
5457
|
+
}
|
|
5398
5458
|
if (cepInputCart) {
|
|
5399
5459
|
const t7 = Date.now();
|
|
5400
5460
|
const beforeUrl7 = page.url();
|
|
@@ -5419,14 +5479,15 @@ async function flowPurchaseJourney(ctx) {
|
|
|
5419
5479
|
screenshotPath: sp7,
|
|
5420
5480
|
screenshotBeforePath: spBefore7,
|
|
5421
5481
|
beforeUrl: beforeUrl7,
|
|
5422
|
-
actionDescription: `Preencheu CEP '${ctx.rc.cep}' no carrinho (\`${cepInputCart}\`)`,
|
|
5482
|
+
actionDescription: `Preencheu CEP '${ctx.rc.cep}' no carrinho (\`${cepInputCart}\`)${cepCartRecovered ? " (selector via LLM recovery)" : ""}`,
|
|
5423
5483
|
detail: { cepUsed: ctx.rc.cep },
|
|
5424
5484
|
selectorKey: "cepInputCart",
|
|
5425
|
-
usedSelector: cepInputCart
|
|
5485
|
+
usedSelector: cepInputCart,
|
|
5486
|
+
recoveredByLlm: cepCartRecovered || undefined
|
|
5426
5487
|
});
|
|
5427
5488
|
reportEnd(7, "shipping-calc-cart", step7Status, Date.now() - t7);
|
|
5428
5489
|
} else {
|
|
5429
|
-
steps.push(makeSkipStep(7, "shipping-calc-cart", ctx, "no CEP input in cart"));
|
|
5490
|
+
steps.push(makeSkipStep(7, "shipping-calc-cart", ctx, "no CEP input in cart (recovery exhausted)"));
|
|
5430
5491
|
reportEnd(7, "shipping-calc-cart", "skipped", 0, "no CEP input in cart");
|
|
5431
5492
|
}
|
|
5432
5493
|
reportStart(8, "go-checkout");
|
|
@@ -5445,28 +5506,47 @@ async function flowPurchaseJourney(ctx) {
|
|
|
5445
5506
|
reportEnd(8, "go-checkout", "skipped", 0, "no checkout button found");
|
|
5446
5507
|
return { pages, steps };
|
|
5447
5508
|
}
|
|
5448
|
-
const
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
await page.screenshot({ path: spBefore8, fullPage: false }).catch(() => {
|
|
5452
|
-
return;
|
|
5453
|
-
});
|
|
5454
|
-
const checkoutText = await checkoutHit.locator.innerText().catch(() => "");
|
|
5455
|
-
await Promise.all([
|
|
5456
|
-
page.waitForURL(/checkout/i, { timeout: 1e4 }).catch(() => {
|
|
5509
|
+
const tryCheckoutClick = async (hit, attempt2) => {
|
|
5510
|
+
const spBefore = screenshotPath(ctx, `pj-8-checkout-before-${attempt2}`);
|
|
5511
|
+
await page.screenshot({ path: spBefore, fullPage: false }).catch(() => {
|
|
5457
5512
|
return;
|
|
5458
|
-
})
|
|
5459
|
-
|
|
5513
|
+
});
|
|
5514
|
+
const clickedText2 = await hit.locator.innerText().catch(() => "");
|
|
5515
|
+
await Promise.all([
|
|
5516
|
+
page.waitForURL(/checkout/i, { timeout: 1e4 }).catch(() => {
|
|
5517
|
+
return;
|
|
5518
|
+
}),
|
|
5519
|
+
hit.locator.click({ timeout: 5000 }).catch(() => {
|
|
5520
|
+
return;
|
|
5521
|
+
})
|
|
5522
|
+
]);
|
|
5523
|
+
await page.waitForTimeout(1500);
|
|
5524
|
+
const spAfter = screenshotPath(ctx, `pj-8-checkout-reached-${attempt2}`);
|
|
5525
|
+
await page.screenshot({ path: spAfter, fullPage: false }).catch(() => {
|
|
5460
5526
|
return;
|
|
5461
|
-
})
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
const
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5527
|
+
});
|
|
5528
|
+
return { url: page.url(), spBefore, spAfter, clickedText: clickedText2 };
|
|
5529
|
+
};
|
|
5530
|
+
const t8 = Date.now();
|
|
5531
|
+
const beforeUrl8 = page.url();
|
|
5532
|
+
let attempt = 1;
|
|
5533
|
+
let result = await tryCheckoutClick(checkoutHit, attempt);
|
|
5534
|
+
let reachedCheckout = /\/checkout/i.test(result.url);
|
|
5535
|
+
let usedSelector = checkoutHit.selector;
|
|
5536
|
+
let clickedText = result.clickedText;
|
|
5537
|
+
if (!reachedCheckout && recoveryBudget > 0 && !/\/checkout/i.test(beforeUrl8)) {
|
|
5538
|
+
const retrySuggestion = await attemptRecovery(page, ctx, "go-checkout-retry", `Cliquei em '${clickedText.slice(0, 40).trim()}' (selector \`${usedSelector}\`), mas a URL ficou em ${result.url} e não foi pra /checkout. Achar o botão que de fato navega pra /checkout neste cart/minicart aberto.`, [usedSelector, ...selFor(ctx, "checkoutButton")]);
|
|
5539
|
+
if (retrySuggestion) {
|
|
5540
|
+
recoveryBudget--;
|
|
5541
|
+
checkoutRecovered = true;
|
|
5542
|
+
attempt++;
|
|
5543
|
+
const retryResult = await tryCheckoutClick(retrySuggestion, attempt);
|
|
5544
|
+
result = retryResult;
|
|
5545
|
+
usedSelector = retrySuggestion.selector;
|
|
5546
|
+
clickedText = retryResult.clickedText;
|
|
5547
|
+
reachedCheckout = /\/checkout/i.test(retryResult.url);
|
|
5548
|
+
}
|
|
5549
|
+
}
|
|
5470
5550
|
const step8Status = reachedCheckout ? "ok" : "failed";
|
|
5471
5551
|
steps.push({
|
|
5472
5552
|
step: 8,
|
|
@@ -5475,13 +5555,13 @@ async function flowPurchaseJourney(ctx) {
|
|
|
5475
5555
|
viewport: ctx.viewport,
|
|
5476
5556
|
status: step8Status,
|
|
5477
5557
|
durationMs: Date.now() - t8,
|
|
5478
|
-
url:
|
|
5479
|
-
screenshotPath:
|
|
5480
|
-
screenshotBeforePath:
|
|
5558
|
+
url: result.url,
|
|
5559
|
+
screenshotPath: result.spAfter,
|
|
5560
|
+
screenshotBeforePath: result.spBefore,
|
|
5481
5561
|
beforeUrl: beforeUrl8,
|
|
5482
|
-
actionDescription: `Clicou em${
|
|
5562
|
+
actionDescription: `Clicou em${clickedText ? ` '${clickedText.slice(0, 30).trim()}'` : ""} (\`${usedSelector}\`); URL final: ${result.url}${reachedCheckout ? " ✓ atingiu /checkout" : " ✗ não foi pra checkout"}${attempt > 1 ? ` (após ${attempt} tentativas com recovery LLM)` : ""}`,
|
|
5483
5563
|
selectorKey: "checkoutButton",
|
|
5484
|
-
usedSelector
|
|
5564
|
+
usedSelector,
|
|
5485
5565
|
recoveredByLlm: checkoutRecovered || undefined
|
|
5486
5566
|
});
|
|
5487
5567
|
reportEnd(8, "go-checkout", step8Status, Date.now() - t8);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/parity",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|