@decocms/parity 0.11.4 → 0.11.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/cli.js +67 -14
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@ 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.11.5](https://github.com/decocms/parity/compare/v0.11.4...v0.11.5) (2026-06-17)
9
+
10
+ ### Fixed
11
+
12
+ * **Variant clicks now wait for navigation.** Deco TanStack PDPs render size/color pickers as `<a href="<product>/p?skuId=N">` links — clicking navigates to a different SKU URL. The previous `click + waitForTimeout(400)` ran add-to-cart against the pre-nav page where the variant was still "unselected". New `clickAndMaybeWait` helper races the click with `page.waitForNavigation` so the flow runs against the post-navigation page when it happens, and falls through when it doesn't (button-radio case).
13
+ * **`validateAddToCart` now uses `cartOpenedIndicator` selectors.** The new selector key from 0.11.4 (`[aria-label='Fechar notificação']` / `[aria-label='Fechar carrinho']` + generic role fallbacks) is now actually wired into the post-click drawer-open probe, so Deco TanStack notifications correctly mark add-to-cart as `ok` via the `drawer-open` signal.
14
+
15
+ ### Added
16
+
17
+ * **Landing-page detection before LLM recovery on missing buy button.** When step 6 (add-to-cart) can't find a buy button, the runner now checks whether the page even looks like a PDP (schema:Product JSON-LD, `<form>` with button, price text, variant inputs). If fewer than 2 PDP signals fire, the step is skipped with `PDP appears to be a landing page (...)` instead of burning LLM recovery budget on a page that has nothing to recover. Reasons are surfaced in the skip note so the user immediately knows why.
18
+
19
+ ### Verification (live against bagaggio)
20
+
21
+ | Version | Steps reached on cand |
22
+ | --- | --- |
23
+ | 0.11.2 and earlier | 3/9 (stops at enter-pdp) |
24
+ | 0.11.3 | 4/9 (stops at select-variant) |
25
+ | 0.11.4 | 6/9 (stops at add-to-cart) |
26
+ | **0.11.5** | **9/9** (reaches go-checkout) |
27
+
8
28
  ## [0.11.4](https://github.com/decocms/parity/compare/v0.11.3...v0.11.4) (2026-06-17)
9
29
 
10
30
  ### Added
package/dist/cli.js CHANGED
@@ -12776,12 +12776,20 @@ async function flowPurchaseJourney(ctx) {
12776
12776
  reportStart(6, "add-to-cart");
12777
12777
  let buyHit = await firstVisibleLocator(page, selFor(ctx, "buyButton"));
12778
12778
  let buyRecovered = false;
12779
- if (!buyHit && budget.remaining > 0) {
12780
- const recovery = await attemptRecovery(page, ctx, "add-to-cart", "Clicar no botão de comprar/adicionar ao carrinho", selFor(ctx, "buyButton"));
12781
- if (recovery) {
12782
- buyHit = recovery;
12783
- buyRecovered = true;
12784
- budget.remaining--;
12779
+ if (!buyHit) {
12780
+ const landingCheck = await detectLandingPage(page);
12781
+ if (landingCheck.isLanding) {
12782
+ steps.push(makeSkipStep(6, "add-to-cart", ctx, `PDP appears to be a landing page (${landingCheck.reasons.join("; ")}) — no buy form to test`));
12783
+ reportEnd(6, "add-to-cart", "skipped", 0, "landing page detected");
12784
+ return { pages, steps };
12785
+ }
12786
+ if (budget.remaining > 0) {
12787
+ const recovery = await attemptRecovery(page, ctx, "add-to-cart", "Clicar no botão de comprar/adicionar ao carrinho", selFor(ctx, "buyButton"));
12788
+ if (recovery) {
12789
+ buyHit = recovery;
12790
+ buyRecovered = true;
12791
+ budget.remaining--;
12792
+ }
12785
12793
  }
12786
12794
  }
12787
12795
  if (!buyHit) {
@@ -13232,6 +13240,56 @@ async function fillCep(page, selector, cep) {
13232
13240
  return false;
13233
13241
  }
13234
13242
  }
13243
+ async function detectLandingPage(page) {
13244
+ const reasons = [];
13245
+ let pdpSignalCount = 0;
13246
+ try {
13247
+ const hasSchema = await page.locator(`script[type='application/ld+json']:has-text('"@type":"Product"')`).first().count().catch(() => 0);
13248
+ if (hasSchema > 0)
13249
+ pdpSignalCount++;
13250
+ else
13251
+ reasons.push("no schema:Product JSON-LD");
13252
+ } catch {
13253
+ reasons.push("no schema:Product JSON-LD");
13254
+ }
13255
+ try {
13256
+ const hasItemtype = await page.locator("[itemtype*='Product']").first().count();
13257
+ if (hasItemtype > 0)
13258
+ pdpSignalCount++;
13259
+ } catch {}
13260
+ try {
13261
+ const hasForm = await page.locator("form:has(button)").first().count();
13262
+ if (hasForm > 0)
13263
+ pdpSignalCount++;
13264
+ else
13265
+ reasons.push("no <form> with button");
13266
+ } catch {
13267
+ reasons.push("no <form> with button");
13268
+ }
13269
+ try {
13270
+ const bodyText = await page.locator("body").innerText({ timeout: 500 });
13271
+ if (/R\$\s*\d+|\$\s*\d+\.\d{2}|€\s*\d+|\bUSD\s*\d+/i.test(bodyText)) {
13272
+ pdpSignalCount++;
13273
+ } else {
13274
+ reasons.push("no price text (R$ / $ / €)");
13275
+ }
13276
+ } catch {
13277
+ reasons.push("no price text (R$ / $ / €)");
13278
+ }
13279
+ try {
13280
+ const hasVariantInput = await page.locator("select, input[type='number'], input[type='radio']").first().count();
13281
+ if (hasVariantInput > 0)
13282
+ pdpSignalCount++;
13283
+ } catch {}
13284
+ return { isLanding: pdpSignalCount < 2, reasons };
13285
+ }
13286
+ async function clickAndMaybeWait(page, locator, _label) {
13287
+ await Promise.allSettled([
13288
+ page.waitForNavigation({ timeout: 5000, waitUntil: "domcontentloaded" }),
13289
+ locator.click({ timeout: 2000 })
13290
+ ]);
13291
+ await page.waitForTimeout(600);
13292
+ }
13235
13293
  async function selectVariant(page, ctx) {
13236
13294
  const actions = [];
13237
13295
  let primarySelectorKey;
@@ -13275,10 +13333,7 @@ async function selectVariant(page, ctx) {
13275
13333
  const sizeHit = await firstVisibleLocator(page, selFor(ctx, "sizeSwatch"));
13276
13334
  if (sizeHit && !await sizeHit.locator.isDisabled().catch(() => false)) {
13277
13335
  const sizeText = (await sizeHit.locator.innerText().catch(() => "")).slice(0, 20).trim();
13278
- await sizeHit.locator.click({ timeout: 2000 }).catch(() => {
13279
- return;
13280
- });
13281
- await page.waitForTimeout(400);
13336
+ await clickAndMaybeWait(page, sizeHit.locator, "sizeSwatch");
13282
13337
  actions.push(`Selecionou tamanho${sizeText ? ` '${sizeText}'` : ""} (\`${sizeHit.selector}\`)`);
13283
13338
  trackPrimary("sizeSwatch", sizeHit.selector);
13284
13339
  }
@@ -13287,10 +13342,7 @@ async function selectVariant(page, ctx) {
13287
13342
  if (colorHit && !await colorHit.locator.isDisabled().catch(() => false)) {
13288
13343
  const colorText = (await colorHit.locator.innerText().catch(() => "")).slice(0, 20).trim();
13289
13344
  const colorLabel = colorText || await colorHit.locator.getAttribute("aria-label").catch(() => null) || "";
13290
- await colorHit.locator.click({ timeout: 2000 }).catch(() => {
13291
- return;
13292
- });
13293
- await page.waitForTimeout(400);
13345
+ await clickAndMaybeWait(page, colorHit.locator, "colorSwatch");
13294
13346
  actions.push(`Selecionou cor${colorLabel ? ` '${colorLabel}'` : ""} (\`${colorHit.selector}\`)`);
13295
13347
  trackPrimary("colorSwatch", colorHit.selector);
13296
13348
  }
@@ -13441,6 +13493,7 @@ async function validateAddToCart(page, ctx, cartCountBefore, beforeUrl) {
13441
13493
  };
13442
13494
  }
13443
13495
  const drawerSelectors = [
13496
+ ...selFor(ctx, "cartOpenedIndicator"),
13444
13497
  "[role='dialog']",
13445
13498
  "[data-minicart][aria-expanded='true']",
13446
13499
  "[data-cart-drawer].open",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/parity",
3
- "version": "0.11.4",
3
+ "version": "0.11.5",
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",