npm.io
0.6.0 • Published 11h agoCLI

@decocms/qa

Licence
MIT
Version
0.6.0
Deps
5
Size
103 kB
Vulns
0
Weekly
181

@decocms/qa

E2E QA test runner for deco.cx ecommerce stores. Runs a deterministic 10-step purchase journey (home → PLP → PDP → cart → /checkout) against a single URL and emits JUnit XML + a JSON report + screenshots. The journey asserts cart state — quantity, variant, price, and that the cart isn't empty — so a broken add-to-cart fails the run instead of passing green.

Selectors are resolved from data-qa-* attributes on the store's JSX, with a configurable override fallback in .qarc.json. No LLM discovery, no visual diff — deterministic by design. The marking is consumed by the setup-deco-ecommerce-qa Claude Code skill that scaffolds GitHub Actions workflows in store repos.

Quickstart

bunx @decocms/qa journey \
  --url https://mystore.example.com \
  --junit junit.xml \
  --github \
  --viewports desktop,mobile

The journey clicks through:

  1. visit-home — load the URL
  2. navigate-plp — click the first visible [data-qa-category-link]. If no visible match exists and [data-qa-menu-trigger] is present, the engine clicks the trigger (e.g. a hamburger button) to open the menu drawer, then clicks the category link. If a blocking popup/overlay intercepts the click, the engine dismisses it and retries (see Dismissing blocking overlays).
  3. enter-pdp — click the first [data-qa-product-card], extract [data-qa-pdp-title] and (optional) [data-qa-pdp-price]
  4. shipping-calc-pdp (optional) — fill [data-qa-cep-input] + click [data-qa-cep-submit]
  5. add-to-cart — click [data-qa-buy-button]. If the click opens a size/variant modal (common on mobile), the engine picks the first in-stock [data-qa-variant-option], clicks [data-qa-variant-confirm] if present, then verifies via [data-qa-cart-count] (soft — never fails the step).
  6. open-minicart — dismiss blocking overlays (see Dismissing blocking overlays), click [data-qa-cart-icon], wait for [data-qa-minicart], then assert the cart line (see "Cart-state assertions" below)
  7. cart-persists-reload (optional) — reload the page, reopen the minicart, assert the cart line still has ≥1 [data-qa-minicart-item]
  8. shipping-calc-cart (optional) — same as step 4 but in the cart context
  9. go-checkout — click [data-qa-minicart-checkout], assert [data-qa-checkout-page], verify the PDP title persisted in the cart DOM (preferring [data-qa-minicart-item-name] when present)
  10. cart-controls (optional) — click [data-qa-quantity-increment] and assert [data-qa-quantity-value] becomes 2, then click [data-qa-minicart-item-remove] and assert the cart empties

Optional steps are skipped silently when their slugs aren't on the page.

After every navigation the engine waits for the network to settle and scrolls the page to trigger IntersectionObserver-driven lazy content (product grids), and polls for each required selector for up to 10s — so lazily-hydrated storefronts don't flake.

The variant slugs (data-qa-variant-option, data-qa-variant-confirm) and the badge slug (data-qa-cart-count) are optional: add them only on stores where buying requires picking a size or where you want add-to-cart verified by the cart badge.

Dismissing blocking overlays

Stores often show a blocking modal/popup (newsletter, cookie-consent, age-gate) before the shopper reaches a product. The engine masks automation (real UA, AutomationControlled disabled) so it sees the real shopper experience — which means the store can't detect the run to suppress the popup. So the engine dismisses overlays itself: proactively before navigating, and reactively whenever a click is intercepted — retrying a normal click once the overlay is gone (it never clicks through an overlay). In order, it tries:

  1. data-qa-dismiss (recommended) — clicks every visible [data-qa-dismiss]. Mark the popup's close control (the ✕) with this attribute and dismissal is deterministic and zero-config — no ?qa=-style store branching needed.
  2. Escape — clears overlays bound to a keydown listener.
  3. Heuristic (opt-in via features.dismissOverlaysHeuristic) — clicks a generic close affordance (a /×/fechar/close button, or [aria-label*="close"]/[aria-label*="fechar"]). Hardened to never follow a link (href) or submit a form, but best-effort and may misfire — off by default so deterministic runs aren't surprised.

Dismissal is bounded (capped rounds) and a no-op when no overlay is present, so stores without popups are unaffected.

WAF-bypass user-agent

By default the engine appends the token deco-qa-bot/1.0 to the browser user-agent on every viewport (desktop, mobile and tablet). This token matches a Cloudflare WAF skip rule scoped to the preview hosts (*.decocdn.com), so the QA bot is let through without weakening the protection for real shopper traffic.

Set QA_USER_AGENT to override the user-agent completely (the token included) — useful for one-off debugging:

QA_USER_AGENT="my-debug-agent/1.0" bunx @decocms/qa journey --url https://mystore.example.com
Cart-state assertions

Once the minicart is open (step 6), the engine validates the cart line — but only for the markers a store has actually added. A missing marker means "not verified" (skipped), never a failure, so stores adopt the checks incrementally:

Marker Assertion
data-qa-minicart-items The cart-line list wrapper (rendered even when empty). When present and it contains zero data-qa-minicart-item rows → fail (empty cart — add-to-cart didn't persist). Also scopes the line queries below, so a recommendation carousel can't be mistaken for a cart line.
data-qa-minicart-item One element per cart line.
data-qa-quantity-value Must read 1 after a single add. Put the marker on whatever displays the quantity — a text node (<span>1</span>), a form field (<input value="1">), a wrapper around the field (<div data-qa-quantity-value><input value="1"></div>), or carry the value on the attribute itself (data-qa-quantity-value="1"). The engine reads all of these — no mirror <span> needed for input-based quantities. If you declare the value on the attribute, bind it reactively (step 10 increments and expects it to become 2).
data-qa-minicart-item-variant Must contain the size picked in step 5 (exact token match).
data-qa-minicart-item-price + data-qa-pdp-price Line price must be > 0 and the PDP "from" price.
data-qa-minicart-item-name Used at go-checkout to confirm the right product reached the checkout; a mismatch here (and only here) escalates step 9 to a failure.

Additional optional markers: data-qa-minicart-subtotal, data-qa-minicart-total, data-qa-quantity-increment, data-qa-quantity-decrement, data-qa-minicart-item-remove (the last three drive the cart-controls step).

Commands

Command Purpose
qa journey --url <url> Run the 10-step purchase journey
qa doctor --url <url> Report which data-qa-* slugs are present on a URL
qa list-slugs Print the 28 canonical slugs as JSON
qa journey flags
Flag Type Purpose
--url <url> string Target URL (required)
--junit <file> string Emit JUnit XML to this path
--github boolean Emit GitHub Actions ::group:: / ::error:: annotations
--viewports <list> string Comma-separated: desktop,mobile,tablet. Default: desktop.
--cep <cep> string Override the CEP used in shipping-calc steps
--smoke boolean Run only steps 1,2,3,5 (skips shipping, minicart, checkout)
--headed boolean Run with a visible browser (local debug)
--debug boolean Pause for inspection (implies --headed)

.qarc.json config

Lives at the root of the store repo. All fields except url are optional.

{
  "url": "https://farmrio.com.br",
  "cep": "01310-100",
  "viewports": ["desktop", "mobile"],
  "selectors": {
    "data-qa-buy-button": "button.custom-add-to-cart"
  },
  "features": {
    "checkoutUrlPattern": "/checkout"
  }
}
  • selectors — per-slug CSS override used when [data-qa-<slug>] is missing. Keys are restricted to the 28 canonical slug names (Zod-enforced).
  • features.checkoutUrlPattern — a substring the post-click checkout URL must contain (e.g. /checkout), with */** as wildcards. When set, the engine drops the [data-qa-checkout-page] assertion and validates by URL instead — works for same-origin, cross-origin (external VTEX), and hash-route SPA checkouts (…/checkout#/email). Because it's a contains match, a plain /checkout covers all of these; the legacy glob form (**/checkout**) still works. On a local base (localhost/127.0.0.1) where the click never reaches a matching URL, the step is skipped (verdict stays pass) rather than failing. Takes precedence over checkoutCrossOrigin.
  • features.checkoutCrossOrigin — set to true for VTEX legacy stores where the checkout opens on checkout.vtex.com.br. The engine drops the [data-qa-checkout-page] assertion and validates by URL change instead; the PDP-title persistence sweep still runs on the VTEX DOM. Prefer checkoutUrlPattern for new configs.

CLI flags take precedence over .qarc.json.

Output

qa-output/<runId>/
├── report.json            # full structured report (validated against the v0.1 Zod schema)
└── screenshots/
    ├── 1-visit-home.png
    ├── 2-navigate-plp.png
    └── ...

When --junit <file> is passed, a separate JUnit XML file is written for dorny/test-reporter@v2 or similar CI parsers.

Exit codes

Code Meaning
0 Journey passed (all required steps OK; optional steps OK or deliberately skipped)
1 Assertion failure (a required step failed — the standard red-test case)
2 Setup failure (URL invalid, .qarc.json broken, browser couldn't launch)
3 Global timeout (journey exceeded its deadline; pages were force-closed)

Development

bun install
bunx playwright install chromium
bun run check    # tsc --noEmit
bun run lint     # biome lint
bun run test     # vitest run
bun run build    # bundle dist/cli.js for npm

Tests use a local HTTP server (tests/harness/server.ts) plus HTML fixtures. They run against real Chromium — no mocks for browser interactions.

License

MIT.

Keywords