@nextcommerce/campaigns-os 1.33.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/AGENTS.md +204 -0
- package/CHANGELOG.md +5002 -0
- package/CONTEXT.md +685 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +368 -0
- package/agents/claude/CLAUDE.md +32 -0
- package/agents/codex/AGENTS.md +27 -0
- package/agents/copilot/copilot-instructions.md +14 -0
- package/agents/cursor/campaigns-os.mdc +13 -0
- package/bin/campaigns-os.mjs +38 -0
- package/campaign-spec/README.md +138 -0
- package/campaign-spec/dist/analytics-vocabulary.d.ts +47 -0
- package/campaign-spec/dist/analytics-vocabulary.js +74 -0
- package/campaign-spec/dist/index.d.ts +40 -0
- package/campaign-spec/dist/index.js +77 -0
- package/campaign-spec/dist/normalize.d.ts +22 -0
- package/campaign-spec/dist/normalize.js +41 -0
- package/campaign-spec/dist/routing.d.ts +190 -0
- package/campaign-spec/dist/routing.js +263 -0
- package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +42 -0
- package/campaign-spec/dist/rules/analytics-contract-shape.js +303 -0
- package/campaign-spec/dist/rules/assembly-hints-shape.d.ts +42 -0
- package/campaign-spec/dist/rules/assembly-hints-shape.js +191 -0
- package/campaign-spec/dist/rules/campaign-metadata.d.ts +14 -0
- package/campaign-spec/dist/rules/campaign-metadata.js +40 -0
- package/campaign-spec/dist/rules/checkout-has-success-url.d.ts +31 -0
- package/campaign-spec/dist/rules/checkout-has-success-url.js +66 -0
- package/campaign-spec/dist/rules/cycle-detection.d.ts +12 -0
- package/campaign-spec/dist/rules/cycle-detection.js +141 -0
- package/campaign-spec/dist/rules/design-source-shape.d.ts +29 -0
- package/campaign-spec/dist/rules/design-source-shape.js +142 -0
- package/campaign-spec/dist/rules/downsell-without-upsell.d.ts +11 -0
- package/campaign-spec/dist/rules/downsell-without-upsell.js +40 -0
- package/campaign-spec/dist/rules/exit-intent-validation.d.ts +23 -0
- package/campaign-spec/dist/rules/exit-intent-validation.js +147 -0
- package/campaign-spec/dist/rules/funnel-count.d.ts +9 -0
- package/campaign-spec/dist/rules/funnel-count.js +37 -0
- package/campaign-spec/dist/rules/funnel-hypothesis-length.d.ts +22 -0
- package/campaign-spec/dist/rules/funnel-hypothesis-length.js +60 -0
- package/campaign-spec/dist/rules/funnel-identity.d.ts +15 -0
- package/campaign-spec/dist/rules/funnel-identity.js +57 -0
- package/campaign-spec/dist/rules/funnel-weight-sum.d.ts +20 -0
- package/campaign-spec/dist/rules/funnel-weight-sum.js +66 -0
- package/campaign-spec/dist/rules/index.d.ts +69 -0
- package/campaign-spec/dist/rules/index.js +131 -0
- package/campaign-spec/dist/rules/offer-ref-integrity.d.ts +13 -0
- package/campaign-spec/dist/rules/offer-ref-integrity.js +59 -0
- package/campaign-spec/dist/rules/package-pricing-sanity.d.ts +12 -0
- package/campaign-spec/dist/rules/package-pricing-sanity.js +42 -0
- package/campaign-spec/dist/rules/page-count.d.ts +11 -0
- package/campaign-spec/dist/rules/page-count.js +31 -0
- package/campaign-spec/dist/rules/page-id-uniqueness.d.ts +14 -0
- package/campaign-spec/dist/rules/page-id-uniqueness.js +47 -0
- package/campaign-spec/dist/rules/promo-code-input-validation.d.ts +8 -0
- package/campaign-spec/dist/rules/promo-code-input-validation.js +126 -0
- package/campaign-spec/dist/rules/promo-codes-shape.d.ts +30 -0
- package/campaign-spec/dist/rules/promo-codes-shape.js +187 -0
- package/campaign-spec/dist/rules/route-field-ignored-for-page-type.d.ts +33 -0
- package/campaign-spec/dist/rules/route-field-ignored-for-page-type.js +81 -0
- package/campaign-spec/dist/rules/route-target-resolves.d.ts +32 -0
- package/campaign-spec/dist/rules/route-target-resolves.js +112 -0
- package/campaign-spec/dist/rules/schema-version.d.ts +21 -0
- package/campaign-spec/dist/rules/schema-version.js +53 -0
- package/campaign-spec/dist/rules/sdk-version.d.ts +26 -0
- package/campaign-spec/dist/rules/sdk-version.js +96 -0
- package/campaign-spec/dist/rules/shipping-countries-shape.d.ts +10 -0
- package/campaign-spec/dist/rules/shipping-countries-shape.js +30 -0
- package/campaign-spec/dist/rules/shipping-methods-present.d.ts +10 -0
- package/campaign-spec/dist/rules/shipping-methods-present.js +26 -0
- package/campaign-spec/dist/rules/store-profile-shape.d.ts +30 -0
- package/campaign-spec/dist/rules/store-profile-shape.js +127 -0
- package/campaign-spec/dist/rules/thank-you-requirement.d.ts +16 -0
- package/campaign-spec/dist/rules/thank-you-requirement.js +50 -0
- package/campaign-spec/dist/rules/unknown-top-level-fields.d.ts +28 -0
- package/campaign-spec/dist/rules/unknown-top-level-fields.js +114 -0
- package/campaign-spec/dist/rules/upsell-has-packages.d.ts +9 -0
- package/campaign-spec/dist/rules/upsell-has-packages.js +35 -0
- package/campaign-spec/dist/rules/upsell-routing-complete.d.ts +10 -0
- package/campaign-spec/dist/rules/upsell-routing-complete.js +45 -0
- package/campaign-spec/dist/rules/upsell-without-checkout.d.ts +11 -0
- package/campaign-spec/dist/rules/upsell-without-checkout.js +44 -0
- package/campaign-spec/dist/rules/variant-labels-shape.d.ts +28 -0
- package/campaign-spec/dist/rules/variant-labels-shape.js +86 -0
- package/campaign-spec/dist/sdk-version-parse.d.ts +43 -0
- package/campaign-spec/dist/sdk-version-parse.js +62 -0
- package/campaign-spec/dist/types.d.ts +674 -0
- package/campaign-spec/dist/types.js +40 -0
- package/campaign-spec/package.json +12 -0
- package/compatibility.json +25 -0
- package/contracts/agent-relevant-change-policy.v1.json +111 -0
- package/contracts/brand-theme-source-defaults.figma-sections-export.v0.json +32 -0
- package/contracts/brand-theme-target-tokens.next-core.v0.json +65 -0
- package/contracts/campaign-cart-checkout-field-contract.v0.json +45 -0
- package/contracts/campaign-cart-sdk-support-policy.v0.json +11 -0
- package/contracts/commerce-surface-catalog.json +2452 -0
- package/contracts/fixtures/orientation/canonicalization/v1.json +34 -0
- package/contracts/fixtures/orientation/envelope/current.json +95 -0
- package/contracts/fixtures/orientation/envelope/freshness_unknown.json +97 -0
- package/contracts/fixtures/orientation/envelope/legacy_baseline.json +95 -0
- package/contracts/fixtures/orientation/envelope/orientation_available.json +136 -0
- package/contracts/fixtures/orientation/envelope/recovered_interrupted_update.json +136 -0
- package/contracts/fixtures/orientation/envelope/refused.json +100 -0
- package/contracts/fixtures/orientation/envelope/restart_required.json +137 -0
- package/contracts/fixtures/orientation/envelope/updated.json +135 -0
- package/contracts/fixtures/orientation/hostile-target/README.md +61 -0
- package/contracts/fixtures/orientation/hostile-target/manifest.json +82 -0
- package/contracts/fixtures/orientation/hostile-target/repo/CHANGELOG.md +18 -0
- package/contracts/fixtures/orientation/hostile-target/repo/bin/intended.mjs +12 -0
- package/contracts/fixtures/orientation/hostile-target/repo/bin/tripwire.mjs +15 -0
- package/contracts/fixtures/orientation/hostile-target/repo/contracts/release-ledger.json +48 -0
- package/contracts/fixtures/orientation/hostile-target/repo/contracts/supported-surface.json +17 -0
- package/contracts/fixtures/orientation/hostile-target/repo/docs/example-contract.md +13 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-checkout +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-merge +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/pre-commit +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/package.json +15 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/tripwire.mjs +9 -0
- package/contracts/fixtures/orientation/hostile-target/repo/package.json +20 -0
- package/contracts/fixtures/orientation/hostile-target/repo/schemas/example.v0.schema.json +15 -0
- package/contracts/fixtures/orientation/release-gate/cases.json +1073 -0
- package/contracts/fixtures/runtime-recipe/accept/current.json +299 -0
- package/contracts/fixtures/runtime-recipe/accept/minimal.json +294 -0
- package/contracts/fixtures/runtime-recipe/dist-states.json +51 -0
- package/contracts/fixtures/runtime-recipe/manifest.json +85 -0
- package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +297 -0
- package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/missing-required-field.json +251 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +310 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +299 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/assembly-report.json +180 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/build-context.json +115 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/doctor-output.json +29 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/qa-verdict.json +27 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/campaign-runtime.build.json +141 -0
- package/contracts/migration-sidecar-bundle.v0.json +149 -0
- package/contracts/orientation-limits.v1.json +41 -0
- package/contracts/orientation-reason-codes.v1.json +196 -0
- package/contracts/private-template-sources.json +8 -0
- package/contracts/release-ledger.json +4598 -0
- package/contracts/reserved-skill-names.json +13 -0
- package/contracts/runtime-recipe.campaigns-os-node-v1.json +299 -0
- package/contracts/supported-surface.json +180 -0
- package/contracts/template-brand-contract.apollo-mv-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.apollo.v0.json +27 -0
- package/contracts/template-brand-contract.demeter.v0.json +27 -0
- package/contracts/template-brand-contract.olympus-mv-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.olympus-mv-two-step.v0.json +28 -0
- package/contracts/template-brand-contract.olympus.v0.json +27 -0
- package/contracts/template-brand-contract.shared-commerce.v0.json +190 -0
- package/contracts/template-brand-contract.shop-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.shop-three-step.v0.json +29 -0
- package/contracts/template-slot-manifest.apollo-mv-single-step.v0.json +14 -0
- package/contracts/template-slot-manifest.apollo.v0.json +12 -0
- package/contracts/template-slot-manifest.demeter.v0.json +30 -0
- package/contracts/template-slot-manifest.olympus-mv-single-step.v0.json +14 -0
- package/contracts/template-slot-manifest.olympus-mv-two-step.v0.json +15 -0
- package/contracts/template-slot-manifest.olympus.v0.json +12 -0
- package/contracts/template-slot-manifest.shared-content-core.v0.json +4254 -0
- package/contracts/template-slot-manifest.shop-single-step.v0.json +47 -0
- package/contracts/template-slot-manifest.shop-three-step.v0.json +33 -0
- package/docs/brand-theme-bridge.md +159 -0
- package/docs/build-packet.md +1300 -0
- package/docs/campaign-build-brief.md +145 -0
- package/docs/campaign-standardization-report.md +329 -0
- package/docs/campaigns-os-build-flow.md +117 -0
- package/docs/design-source-package.md +784 -0
- package/docs/legacy-migration.md +58 -0
- package/docs/migration-sidecar-bundle.md +139 -0
- package/docs/orientation-contract-reference.md +1220 -0
- package/docs/polish-evidence.md +502 -0
- package/docs/qa-and-test-orders.md +1691 -0
- package/docs/release-ledger-authoring-guide.md +274 -0
- package/docs/runtime-readiness.md +211 -0
- package/docs/supported-surface.md +83 -0
- package/docs/versioning.md +55 -0
- package/docs/workflow-findings-sidecar.md +588 -0
- package/package.json +135 -0
- package/prompts/first-build.md +27 -0
- package/prompts/friction-log.md +30 -0
- package/schemas/campaign-build-brief.v1.schema.json +149 -0
- package/schemas/campaign-design-source-package.v0.schema.json +697 -0
- package/schemas/campaign-runtime-assembly-report.v0.schema.json +390 -0
- package/schemas/campaign-runtime-build-context.v0.schema.json +339 -0
- package/schemas/campaign-runtime-build-packet.v0.schema.json +452 -0
- package/schemas/campaign-spec.v4.schema.json +582 -0
- package/schemas/campaigns-os-doctor-output.v0.schema.json +43 -0
- package/schemas/campaigns-os-legacy-migration-inventory.v0.schema.json +112 -0
- package/schemas/campaigns-os-legacy-provisioning-plan.v0.schema.json +38 -0
- package/schemas/campaigns-os-legacy-provisioning-receipt.v0.schema.json +57 -0
- package/schemas/campaigns-os-qa-verdict-sidecar.v0.schema.json +150 -0
- package/schemas/campaigns-os-qa-verdict.v0.schema.json +438 -0
- package/schemas/campaigns-os-release-ledger.v1.schema.json +162 -0
- package/schemas/campaigns-os-run-record.v0.schema.json +343 -0
- package/schemas/campaigns-os-runtime-recipe.v1.schema.json +313 -0
- package/schemas/campaigns-os-sidecar-bundle-conformance.v0.schema.json +78 -0
- package/schemas/campaigns-os-tooling-orientation.v1.schema.json +381 -0
- package/schemas/campaigns-os-workflow-finding.v0.schema.json +134 -0
- package/schemas/source-html-manifest.v0.schema.json +252 -0
- package/skills/next-campaigns-build/SKILL.md +73 -0
- package/skills/next-campaigns-os/SKILL.md +101 -0
- package/skills/next-campaigns-os/references/session-intake.md +160 -0
- package/skills/next-campaigns-os-setup/SKILL.md +22 -0
- package/skills/next-campaigns-polish/SKILL.md +145 -0
- package/skills/next-campaigns-qa/SKILL.md +92 -0
- package/skills.json +56 -0
- package/skills.sh +64 -0
- package/src/adapter-decision-contract.mjs +333 -0
- package/src/brand-theme.mjs +1151 -0
- package/src/browser-launch.mjs +79 -0
- package/src/build-brief.mjs +781 -0
- package/src/built-site-scope.mjs +312 -0
- package/src/campaign-ecosystem.mjs +734 -0
- package/src/campaign-identity.mjs +405 -0
- package/src/campaign-workspace.mjs +127 -0
- package/src/checkpoint-waiver.mjs +302 -0
- package/src/cli.mjs +13442 -0
- package/src/commercial-journey.mjs +1119 -0
- package/src/commercial-parity.mjs +965 -0
- package/src/consent.mjs +347 -0
- package/src/content-residue.mjs +322 -0
- package/src/deadline.mjs +81 -0
- package/src/design-source-package.mjs +2604 -0
- package/src/deviation.mjs +107 -0
- package/src/doctor-check-registry.mjs +49 -0
- package/src/doctor-sidecar.mjs +106 -0
- package/src/finding-cause.mjs +557 -0
- package/src/findings.mjs +326 -0
- package/src/fs-identity.mjs +68 -0
- package/src/gate-actions.mjs +105 -0
- package/src/html-scan.mjs +53 -0
- package/src/install-mode.mjs +272 -0
- package/src/legacy-migration.d.ts +128 -0
- package/src/legacy-migration.mjs +510 -0
- package/src/lifecycle.mjs +338 -0
- package/src/local-proof.mjs +401 -0
- package/src/map-pin-writeback.mjs +210 -0
- package/src/orchestration-stage-contract.mjs +81 -0
- package/src/package-install-fixture.mjs +33 -0
- package/src/page-kit-build-summary.mjs +175 -0
- package/src/page-kit-campaign-config.mjs +57 -0
- package/src/page-kit-sdk-version.mjs +392 -0
- package/src/page-kit-store-profile.mjs +369 -0
- package/src/page-kit-sync.mjs +162 -0
- package/src/polish-browser.mjs +867 -0
- package/src/polish-capture.mjs +1094 -0
- package/src/polish-deadline.mjs +51 -0
- package/src/polish-gate.mjs +739 -0
- package/src/polish-node.mjs +639 -0
- package/src/polish-page-load.mjs +1100 -0
- package/src/private-template-source.mjs +237 -0
- package/src/proof-policy.mjs +82 -0
- package/src/qa-analytics-correctness.mjs +307 -0
- package/src/qa-analytics-errors.mjs +38 -0
- package/src/qa-analytics-parity.mjs +699 -0
- package/src/qa-binding-evidence.mjs +140 -0
- package/src/qa-browser.mjs +6608 -0
- package/src/qa-cart-entry.mjs +406 -0
- package/src/qa-commercial-parity.mjs +641 -0
- package/src/qa-node.mjs +3620 -0
- package/src/qa-order-bump.mjs +381 -0
- package/src/qa-parity-capture.mjs +428 -0
- package/src/qa-parity-fixture.mjs +359 -0
- package/src/qa-publish.mjs +362 -0
- package/src/qa-purchase-data-layer.mjs +263 -0
- package/src/qa-route-probe.mjs +272 -0
- package/src/qa-sidecar.mjs +188 -0
- package/src/qa-test-order-topology.mjs +207 -0
- package/src/qa-url-privacy.mjs +13 -0
- package/src/qa-verdict-discovery.mjs +192 -0
- package/src/qa-verdict-publish.mjs +105 -0
- package/src/qa-verdict.mjs +287 -0
- package/src/remit.mjs +388 -0
- package/src/repo-scan.mjs +83 -0
- package/src/route-identity.mjs +133 -0
- package/src/run-record-closeout.mjs +229 -0
- package/src/run-record.mjs +839 -0
- package/src/run-session.mjs +226 -0
- package/src/runtime-state-ignore.mjs +113 -0
- package/src/sdk-attribute-index.mjs +212 -0
- package/src/sdk-markup.mjs +358 -0
- package/src/sdk-meta-tags.mjs +52 -0
- package/src/shell-token.mjs +7 -0
- package/src/sidecar-bundle.mjs +399 -0
- package/src/source-asset-crawl.mjs +469 -0
- package/src/source-html-intake.mjs +627 -0
- package/src/source-html-manifest.mjs +276 -0
- package/src/source-prep.mjs +284 -0
- package/src/spec-derive-store.mjs +431 -0
- package/src/spec-derive.mjs +514 -0
- package/src/spec-fetch.mjs +66 -0
- package/src/spec-hash.mjs +48 -0
- package/src/spec-identity.mjs +27 -0
- package/src/stage-ledger.mjs +509 -0
- package/src/standardization-report.mjs +1297 -0
- package/src/template-brand-contract.mjs +473 -0
- package/src/template-freshness.mjs +196 -0
- package/src/template-reference.mjs +81 -0
- package/src/template-slot-manifest.mjs +150 -0
- package/src/text-safety.mjs +62 -0
- package/src/theme-gate.mjs +185 -0
- package/src/upsell-selector-scope.mjs +299 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
// Order-bump state evidence for browser QA (campaigns-os#323).
|
|
2
|
+
//
|
|
3
|
+
// The `browser-order-bump-state` assertion asks one question: does what the
|
|
4
|
+
// buyer can see on a bump toggle agree with what the SDK thinks is in the cart?
|
|
5
|
+
// It reads three things off each toggle — the SDK's own active/in-cart state,
|
|
6
|
+
// the toggle's checkbox input if it has one, and the rendered state marker —
|
|
7
|
+
// and fails when they disagree.
|
|
8
|
+
//
|
|
9
|
+
// The marker half used to be a single `querySelector` over a selector list that
|
|
10
|
+
// ended in a bare `[aria-hidden]`. Two things went wrong with that:
|
|
11
|
+
//
|
|
12
|
+
// 1. `aria-hidden` is decoration vocabulary, not state vocabulary. A bump
|
|
13
|
+
// toggle that carries its own visually-hidden `<input type="checkbox"
|
|
14
|
+
// aria-hidden="true">` has that input match the list, and `querySelector`
|
|
15
|
+
// returns document order, not selector order — so on every toggle whose
|
|
16
|
+
// input precedes its tick, the "marker" resolved to the input. An input is
|
|
17
|
+
// not a rendered marker: it has no `::after`, no glyph, no fill, so
|
|
18
|
+
// `markerChecked` could never read true and an accepted bump reported
|
|
19
|
+
// misaligned on every single run. That is the whole of #323.
|
|
20
|
+
// 2. The same clause matched purely decorative nodes — an arrow icon, a
|
|
21
|
+
// switch slider — whose rendering says nothing about the toggle's state,
|
|
22
|
+
// so the check would fail a correctly rendered toggle for the same reason.
|
|
23
|
+
//
|
|
24
|
+
// So the marker vocabulary here is only the four families that mean "this is
|
|
25
|
+
// the tick", the families are tried in order (rather than being collapsed into
|
|
26
|
+
// one document-order query), and form controls and `[hidden]` subtrees can
|
|
27
|
+
// never be a marker.
|
|
28
|
+
//
|
|
29
|
+
// A rendered marker is then read for a positive state signal, and the signals
|
|
30
|
+
// are alternatives rather than one replacing another, because the families
|
|
31
|
+
// express state in genuinely different ways:
|
|
32
|
+
//
|
|
33
|
+
// - `pseudo` — the marker is a persistent box and its `::after` carries the
|
|
34
|
+
// tick. The box renders in both states, so its visibility says nothing;
|
|
35
|
+
// the state is whether the pseudo-element is rendered. A marker whose
|
|
36
|
+
// `::after` has non-empty content belongs to this family by definition,
|
|
37
|
+
// and its checked reading comes from that pseudo-element alone.
|
|
38
|
+
// - `glyph` — the tick is literal text in the marker.
|
|
39
|
+
// - `fill` — the marker is filled with the accepted colour.
|
|
40
|
+
// - `display_toggled` — the marker *is* the tick and the CSS shows and hides
|
|
41
|
+
// it: `[data-next-toggle-card] [os-component="check"] { display: none }`
|
|
42
|
+
// with the active or in-cart card restoring `display: flex`. Only here is
|
|
43
|
+
// the marker's own rendering the state affordance.
|
|
44
|
+
//
|
|
45
|
+
// The last one is a claim about the page's CSS, so it is tested against the
|
|
46
|
+
// page's CSS (`hiddenByAMatchingRule`) rather than assumed from the family
|
|
47
|
+
// name. That is what keeps a persistent box out of it: a box nothing hides is
|
|
48
|
+
// not display-toggled, and reading its visibility as "checked" would fail a
|
|
49
|
+
// correctly declined bump. For the same reason that walk counts only rules
|
|
50
|
+
// that hide outright: a rule dimming a marker to `opacity: 0.4` leaves it on
|
|
51
|
+
// screen, so it is a style and not a state affordance. The rendered read still
|
|
52
|
+
// treats a faded marker as too faint to read a tick off; the two questions
|
|
53
|
+
// differ, and only the rule walk demands an exact `opacity: 0`.
|
|
54
|
+
//
|
|
55
|
+
// A rendered marker carrying no positive signal is reported as unresolved, not
|
|
56
|
+
// as checked and not as unchecked. The harness cannot read that page's state
|
|
57
|
+
// vocabulary, and saying so is honest; claiming a disagreement is not.
|
|
58
|
+
|
|
59
|
+
// Bump toggle roots. Matches the SDK's toggle card and the older bump root.
|
|
60
|
+
export const ORDER_BUMP_TOGGLE_SELECTOR = "[data-next-toggle-card], [data-next-bump]";
|
|
61
|
+
|
|
62
|
+
// State-marker families, most specific first. Tried in order, so a generic
|
|
63
|
+
// match can never outrank a dedicated one just by appearing earlier in the
|
|
64
|
+
// document. Deliberately does NOT include a bare `[aria-hidden]`.
|
|
65
|
+
export const ORDER_BUMP_MARKER_FAMILIES = Object.freeze([
|
|
66
|
+
".bump-check",
|
|
67
|
+
"[data-next-toggle-check]",
|
|
68
|
+
"[os-component='check']",
|
|
69
|
+
".checkbox__icon",
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
// Containers a marker may be nested in; the container must render too, so a tick
|
|
73
|
+
// inside a collapsed wrapper is not read as shown. A marker's wrapper is drawn
|
|
74
|
+
// from the same vocabulary as the marker itself — a `.checkbox__icon` box
|
|
75
|
+
// holding an `[os-component="check"]` tick is the shape this exists for — so
|
|
76
|
+
// this is the family list, not a second copy of it: two copies drifted apart
|
|
77
|
+
// once already, and a family added to one but not the other would resolve a
|
|
78
|
+
// nested tick and then judge it by the wrong box. Order is immaterial here,
|
|
79
|
+
// unlike in the family list: the selectors are joined into one `closest()`
|
|
80
|
+
// query, which resolves by tree position rather than by selector order.
|
|
81
|
+
export const ORDER_BUMP_MARKER_CONTAINERS = ORDER_BUMP_MARKER_FAMILIES;
|
|
82
|
+
|
|
83
|
+
// Elements that can never be a state marker, whatever they match: the toggle's
|
|
84
|
+
// own form control (read separately as `inputChecked`) and anything the author
|
|
85
|
+
// removed from rendering outright.
|
|
86
|
+
export const ORDER_BUMP_MARKER_EXCLUDED = "input, select, textarea, option";
|
|
87
|
+
|
|
88
|
+
// The accepted-state fill used by the shared checkout CSS, as a computed
|
|
89
|
+
// `background-color` string — `getComputedStyle` always reports an opaque
|
|
90
|
+
// colour in this `rgb(r, g, b)` form, so the comparison is a string equality
|
|
91
|
+
// and the constant has to be written the way the browser serialises it. It is
|
|
92
|
+
// the `fill` family's whole vocabulary: a marker painted this colour is a
|
|
93
|
+
// ticked marker. Deliberately a constant of this module rather than a value
|
|
94
|
+
// read from a brand contract — the check is asking what the checkout CSS in
|
|
95
|
+
// front of the buyer actually paints, and sourcing it from a theme would make
|
|
96
|
+
// the answer move whenever the theme does.
|
|
97
|
+
export const ORDER_BUMP_ACCEPTED_FILL_COLOR = "rgb(45, 148, 127)";
|
|
98
|
+
|
|
99
|
+
export const ORDER_BUMP_PROBE_INPUT = Object.freeze({
|
|
100
|
+
toggleSelector: ORDER_BUMP_TOGGLE_SELECTOR,
|
|
101
|
+
markerFamilies: ORDER_BUMP_MARKER_FAMILIES,
|
|
102
|
+
markerContainers: ORDER_BUMP_MARKER_CONTAINERS,
|
|
103
|
+
markerExcluded: ORDER_BUMP_MARKER_EXCLUDED,
|
|
104
|
+
acceptedFillColor: ORDER_BUMP_ACCEPTED_FILL_COLOR,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Returns the `page.evaluate` body. Exported as a factory so the browser proof
|
|
108
|
+
// can drive the same function the QA runner does, instead of a copy of it.
|
|
109
|
+
export function orderBumpEvidenceScript() {
|
|
110
|
+
return ({ toggleSelector, markerFamilies, markerContainers, markerExcluded, acceptedFillColor }) => {
|
|
111
|
+
// The evaluate body is serialised into the page, so it cannot read the
|
|
112
|
+
// module constant; the colour arrives through the probe input. A caller
|
|
113
|
+
// that omits it would compare every fill against undefined and read each
|
|
114
|
+
// fill-family marker as unresolved with no signal, so refuse instead.
|
|
115
|
+
if (typeof acceptedFillColor !== "string" || !acceptedFillColor) {
|
|
116
|
+
throw new Error("order-bump evidence script needs acceptedFillColor in its input (use ORDER_BUMP_PROBE_INPUT)");
|
|
117
|
+
}
|
|
118
|
+
const hasContent = (value) => Boolean(value) && !["none", "normal", '""', "''"].includes(value);
|
|
119
|
+
|
|
120
|
+
// The one list of declarations that remove an element from view. Both the
|
|
121
|
+
// computed-style test and the stylesheet-rule walk ask through this, so a
|
|
122
|
+
// way of hiding a tick can never be understood by one and missed by the
|
|
123
|
+
// other — `visibility: collapse` was rejected by the computed-style test
|
|
124
|
+
// and invisible to the rule walk, which left a tick hidden that way
|
|
125
|
+
// unreadable instead of state-toggled. Reads through getPropertyValue
|
|
126
|
+
// because a rule's own style leaves unset properties empty, which every
|
|
127
|
+
// test below treats as "not hidden".
|
|
128
|
+
//
|
|
129
|
+
// `opacity` is the one declaration the two callers must weigh differently,
|
|
130
|
+
// so it is passed in rather than fixed here. `display` and `visibility`
|
|
131
|
+
// have no middle ground; opacity does, and the two callers are asking
|
|
132
|
+
// different questions about it (see the two wrappers below).
|
|
133
|
+
const hiddenBy = (style, opacityHides) => style.getPropertyValue("display") === "none"
|
|
134
|
+
|| ["hidden", "collapse"].includes(style.getPropertyValue("visibility"))
|
|
135
|
+
|| opacityHides(Number.parseFloat(style.getPropertyValue("opacity") || "1"));
|
|
136
|
+
|
|
137
|
+
// What the buyer can see. An element faded to half its colour or less is
|
|
138
|
+
// not something a buyer reads a tick off, so the rendered test treats
|
|
139
|
+
// anything at or below half opacity as hidden. The threshold is a judgement about
|
|
140
|
+
// legibility, which is why it belongs only here.
|
|
141
|
+
const FADED_OUT_AT_OR_BELOW = 0.5;
|
|
142
|
+
const hiddenFromView = (style) => hiddenBy(style, (value) => value <= FADED_OUT_AT_OR_BELOW);
|
|
143
|
+
|
|
144
|
+
// What a stylesheet rule *declares*. The rule walk is not asking whether
|
|
145
|
+
// the buyer can see the element; it is asking whether a rule removes the
|
|
146
|
+
// element from rendering, because that is the display-toggled family's
|
|
147
|
+
// signature. `opacity: 0.4` is a dimmed style, not a hiding one — a
|
|
148
|
+
// greyed-out but perfectly visible marker would otherwise be read as a
|
|
149
|
+
// state-toggled tick and report a correctly declined bump as misaligned.
|
|
150
|
+
// Only `opacity: 0` removes a tick the way `display: none` does, so only
|
|
151
|
+
// an exact zero counts here.
|
|
152
|
+
const hiddenByDeclaration = (style) => hiddenBy(style, (value) => value === 0);
|
|
153
|
+
|
|
154
|
+
// The marker's `::after`, read once for both jobs it does here. A tick that
|
|
155
|
+
// is absolutely positioned can render while its host box measures zero, so
|
|
156
|
+
// a zero-sized host is not on its own proof that the tick is hidden — the
|
|
157
|
+
// pseudo-element has to be looked at before the size test disqualifies the
|
|
158
|
+
// marker. Returns null when the element has no generated content at all.
|
|
159
|
+
const pseudoTick = (element) => {
|
|
160
|
+
const after = getComputedStyle(element, "::after");
|
|
161
|
+
if (!hasContent(after.content)) return null;
|
|
162
|
+
// An unstyled tick is sized by its own content, and its computed width
|
|
163
|
+
// and height come back as `auto` rather than a length. Only an explicit
|
|
164
|
+
// zero means the tick occupies nothing; `auto` means the content decides,
|
|
165
|
+
// and the content is non-empty or we would not be here.
|
|
166
|
+
const boxed = (value) => {
|
|
167
|
+
const raw = String(value ?? "").trim();
|
|
168
|
+
if (raw === "" || raw === "auto") return true;
|
|
169
|
+
const parsed = Number.parseFloat(raw);
|
|
170
|
+
return Number.isFinite(parsed) ? parsed > 0 : true;
|
|
171
|
+
};
|
|
172
|
+
return {
|
|
173
|
+
shown: !hiddenFromView(after),
|
|
174
|
+
boxed: boxed(after.width) && boxed(after.height),
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// The one visibility test, in two halves that every call site shares: is the
|
|
179
|
+
// element styled to be seen, and does it occupy a box. Both the toggle
|
|
180
|
+
// filter and the marker read go through these, so a toggle and its own
|
|
181
|
+
// marker can never be judged by different rules — `visibility: collapse`
|
|
182
|
+
// was rejected for a marker and accepted for the toggle containing it.
|
|
183
|
+
// Returns the computed style so a caller that needs it does not read twice.
|
|
184
|
+
const visibleStyle = (element) => {
|
|
185
|
+
if (!(element instanceof Element) || element.hidden) return null;
|
|
186
|
+
if (element.closest("[hidden]")) return null;
|
|
187
|
+
const style = getComputedStyle(element);
|
|
188
|
+
return hiddenFromView(style) ? null : style;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const hasBox = (element) => {
|
|
192
|
+
const rect = element.getBoundingClientRect();
|
|
193
|
+
return rect.width > 0 && rect.height > 0 && element.getClientRects().length > 0;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const displayed = (element) => Boolean(visibleStyle(element)) && hasBox(element);
|
|
197
|
+
|
|
198
|
+
// A marker is held to the same visibility rules, with one addition: a
|
|
199
|
+
// zero-sized host still renders when its own tick does.
|
|
200
|
+
const rendered = (element) => {
|
|
201
|
+
if (!visibleStyle(element)) return false;
|
|
202
|
+
if (hasBox(element)) return true;
|
|
203
|
+
const tick = pseudoTick(element);
|
|
204
|
+
return Boolean(tick && tick.shown && tick.boxed);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// A candidate must be a plausible marker at all: not the toggle's own form
|
|
208
|
+
// control, not inside a subtree the author removed from rendering.
|
|
209
|
+
const eligible = (element) => !element.matches(markerExcluded) && !element.closest("[hidden]");
|
|
210
|
+
|
|
211
|
+
// Does any stylesheet rule that *currently applies* and matches this element
|
|
212
|
+
// remove it from rendering? That is the display-toggled family's signature:
|
|
213
|
+
// a base rule hides the tick and a state-scoped rule restores it, so the
|
|
214
|
+
// element matches the hiding rule in both states. A persistent box that
|
|
215
|
+
// nothing hides matches nothing here, which is exactly why its visibility
|
|
216
|
+
// must not be read as its state.
|
|
217
|
+
//
|
|
218
|
+
// "Currently applies" is load-bearing. A rule inside `@media print`, or
|
|
219
|
+
// inside an `@supports` block for a feature this browser lacks, says
|
|
220
|
+
// nothing about what the buyer sees on screen; counting it would read a
|
|
221
|
+
// visible, unchecked box as a hidden tick and fail a correctly declined
|
|
222
|
+
// bump. So a conditional group is descended into only while its condition
|
|
223
|
+
// holds, a stylesheet whose own media attribute does not match is skipped,
|
|
224
|
+
// and so is a disabled one. A stylesheet the page cannot read
|
|
225
|
+
// (cross-origin, no CORS) is not evidence either way and is skipped too.
|
|
226
|
+
const hiddenByAMatchingRule = (element) => {
|
|
227
|
+
const mediaApplies = (query) => {
|
|
228
|
+
if (!query || query === "all") return true;
|
|
229
|
+
try {
|
|
230
|
+
return window.matchMedia(query).matches;
|
|
231
|
+
} catch {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
const groupApplies = (rule) => {
|
|
236
|
+
// A container query is evaluated against a specific element's nearest
|
|
237
|
+
// container, and there is no API that answers that question for an
|
|
238
|
+
// arbitrary element — `CSS.supports` rejects a container condition
|
|
239
|
+
// outright. So an `@container` block is skipped deliberately rather
|
|
240
|
+
// than by accident: a tick hidden only inside one reads as unresolved,
|
|
241
|
+
// which is quiet, instead of being guessed at. Recorded as a known
|
|
242
|
+
// limit in fixtures/qa-order-bump/README.md.
|
|
243
|
+
if (typeof CSSContainerRule !== "undefined" && rule instanceof CSSContainerRule) return false;
|
|
244
|
+
// CSSMediaRule carries `.media`; CSSSupportsRule carries only a
|
|
245
|
+
// condition. Anything else grouping (a layer, a nested style rule) has
|
|
246
|
+
// no condition to fail and applies.
|
|
247
|
+
if (rule.media && typeof rule.media.mediaText === "string") {
|
|
248
|
+
return mediaApplies(rule.conditionText || rule.media.mediaText);
|
|
249
|
+
}
|
|
250
|
+
if (typeof rule.conditionText === "string") {
|
|
251
|
+
try {
|
|
252
|
+
return CSS.supports(rule.conditionText);
|
|
253
|
+
} catch {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return true;
|
|
258
|
+
};
|
|
259
|
+
const walk = (rules) => {
|
|
260
|
+
for (const rule of rules) {
|
|
261
|
+
if (rule.cssRules) {
|
|
262
|
+
if (!groupApplies(rule)) continue;
|
|
263
|
+
if (walk(Array.from(rule.cssRules))) return true;
|
|
264
|
+
}
|
|
265
|
+
if (!rule.selectorText || !rule.style || !hiddenByDeclaration(rule.style)) continue;
|
|
266
|
+
try {
|
|
267
|
+
// Pseudo-element selectors throw here; they are not this family.
|
|
268
|
+
if (element.matches(rule.selectorText)) return true;
|
|
269
|
+
} catch {
|
|
270
|
+
// unsupported selector: no evidence
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return false;
|
|
274
|
+
};
|
|
275
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
276
|
+
if (sheet.disabled) continue;
|
|
277
|
+
if (!mediaApplies(sheet.media?.mediaText)) continue;
|
|
278
|
+
let rules;
|
|
279
|
+
try {
|
|
280
|
+
rules = Array.from(sheet.cssRules || []);
|
|
281
|
+
} catch {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (walk(rules)) return true;
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
// The positive signals, in the order that settles which vocabulary the
|
|
291
|
+
// marker speaks. Returns null when a rendered marker carries none.
|
|
292
|
+
const checkedSignal = (marker) => {
|
|
293
|
+
const style = getComputedStyle(marker);
|
|
294
|
+
// A marker with pseudo-element content belongs to the pseudo family
|
|
295
|
+
// whatever else is true of it, and its state is that pseudo-element's
|
|
296
|
+
// rendering — never the box's.
|
|
297
|
+
const tick = pseudoTick(marker);
|
|
298
|
+
if (tick) return { signal: "pseudo", checked: tick.shown };
|
|
299
|
+
if (/check|\u2713/.test(marker.textContent || "")) return { signal: "glyph", checked: true };
|
|
300
|
+
if (style.backgroundColor === acceptedFillColor) return { signal: "fill", checked: true };
|
|
301
|
+
if (hiddenByAMatchingRule(marker)) return { signal: "display_toggled", checked: true };
|
|
302
|
+
return null;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// Families in order. A rendered candidate wins outright; otherwise the
|
|
306
|
+
// first eligible candidate is kept so an off toggle still reports a marker
|
|
307
|
+
// (which then correctly reads unchecked) rather than reporting none.
|
|
308
|
+
const resolveMarker = (toggle) => {
|
|
309
|
+
let fallback = null;
|
|
310
|
+
for (const family of markerFamilies) {
|
|
311
|
+
const candidates = Array.from(toggle.querySelectorAll(family)).filter(eligible);
|
|
312
|
+
if (!candidates.length) continue;
|
|
313
|
+
const shown = candidates.find(rendered);
|
|
314
|
+
if (shown) return { element: shown, family };
|
|
315
|
+
if (!fallback) fallback = { element: candidates[0], family };
|
|
316
|
+
}
|
|
317
|
+
return fallback;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const toggles = Array.from(document.querySelectorAll(toggleSelector)).filter(displayed).map((toggle, index) => {
|
|
321
|
+
const input = toggle.querySelector('input[type="checkbox"]');
|
|
322
|
+
const resolved = resolveMarker(toggle);
|
|
323
|
+
const marker = resolved?.element || null;
|
|
324
|
+
const markerContainer = marker?.closest(markerContainers.join(", ")) || marker;
|
|
325
|
+
const markerVisible = Boolean(marker)
|
|
326
|
+
&& rendered(marker)
|
|
327
|
+
&& Boolean(markerContainer)
|
|
328
|
+
&& rendered(markerContainer);
|
|
329
|
+
// A hidden marker is unchecked in every family, so it needs no signal.
|
|
330
|
+
// A rendered one is read for a positive signal, and a rendered marker
|
|
331
|
+
// with none is `unresolved`: it exists and renders, but nothing on it
|
|
332
|
+
// says which state it is in. That is distinct from finding no marker at
|
|
333
|
+
// all, which is a null signal, so the two are never conflated by a reader
|
|
334
|
+
// of the evidence.
|
|
335
|
+
const signal = marker && markerVisible ? checkedSignal(marker) : null;
|
|
336
|
+
const markerSignal = !marker
|
|
337
|
+
? null
|
|
338
|
+
: (markerVisible ? signal?.signal || "unresolved" : "not_rendered");
|
|
339
|
+
const markerChecked = Boolean(markerVisible && signal?.checked);
|
|
340
|
+
// Readable: a marker was found AND its state can be read. An unreadable
|
|
341
|
+
// marker is read like an absent one, never as a disagreement.
|
|
342
|
+
const markerReadable = markerSignal !== null && markerSignal !== "unresolved";
|
|
343
|
+
const active = toggle.classList.contains("next-active")
|
|
344
|
+
|| toggle.classList.contains("next-in-cart")
|
|
345
|
+
|| toggle.classList.contains("next-selected")
|
|
346
|
+
|| toggle.getAttribute("aria-pressed") === "true";
|
|
347
|
+
const inputChecked = input ? input.checked : null;
|
|
348
|
+
const inputAgrees = inputChecked === null || inputChecked === active;
|
|
349
|
+
const markerAgrees = !markerReadable || markerChecked === active;
|
|
350
|
+
return {
|
|
351
|
+
index,
|
|
352
|
+
packageId: toggle.getAttribute("data-next-package-id") || null,
|
|
353
|
+
active,
|
|
354
|
+
inputChecked,
|
|
355
|
+
// Was a marker element found at all. Unchanged in meaning: whether the
|
|
356
|
+
// harness located something to read.
|
|
357
|
+
markerResolved: Boolean(marker),
|
|
358
|
+
// Whether that marker's state could actually be read. A found-but-
|
|
359
|
+
// unreadable marker is resolved and not readable, and only `markerAgrees`
|
|
360
|
+
// depends on this one.
|
|
361
|
+
markerReadable,
|
|
362
|
+
// Which family matched and what it resolved to, so an operator reading
|
|
363
|
+
// a misaligned verdict can see whether the harness found the right
|
|
364
|
+
// element before concluding the page is wrong.
|
|
365
|
+
markerFamily: resolved?.family || null,
|
|
366
|
+
markerTag: marker ? marker.tagName.toLowerCase() : null,
|
|
367
|
+
// How the marker was read: "pseudo", "glyph", "fill" or
|
|
368
|
+
// "display_toggled" when a state vocabulary was recognised,
|
|
369
|
+
// "not_rendered" when the marker is on the page but hidden,
|
|
370
|
+
// "unresolved" when it renders but carries no state signal, and null
|
|
371
|
+
// when no marker was found at all.
|
|
372
|
+
markerSignal,
|
|
373
|
+
markerChecked,
|
|
374
|
+
inputAgrees,
|
|
375
|
+
markerAgrees,
|
|
376
|
+
statesAgree: inputAgrees && markerAgrees,
|
|
377
|
+
};
|
|
378
|
+
});
|
|
379
|
+
return { toggles };
|
|
380
|
+
};
|
|
381
|
+
}
|