@drip-apex/sdk 0.1.0 → 0.2.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/README.md +5 -0
- package/dist/bundle-report.json +584 -226
- package/dist/define.cjs +123 -0
- package/dist/define.cjs.map +7 -0
- package/dist/define.mjs +102 -0
- package/dist/define.mjs.map +7 -0
- package/dist/drip.cjs +19262 -0
- package/dist/drip.cjs.map +7 -0
- package/dist/drip.debug.js +1 -1
- package/dist/drip.debug.js.map +1 -1
- package/dist/drip.heatmap.js +1 -1
- package/dist/drip.heatmap.js.map +1 -1
- package/dist/drip.js +1 -1
- package/dist/drip.js.map +1 -1
- package/dist/drip.live.js +1 -1
- package/dist/drip.live.js.map +1 -1
- package/dist/drip.mjs +19246 -0
- package/dist/drip.mjs.map +7 -0
- package/dist/drip.replay-recorder.js +2 -0
- package/dist/drip.replay-recorder.js.map +1 -0
- package/dist/editor.js +147 -142
- package/dist/editor.js.map +4 -4
- package/dist/flags.cjs +1788 -0
- package/dist/flags.cjs.map +7 -0
- package/dist/flags.mjs +1765 -0
- package/dist/flags.mjs.map +7 -0
- package/dist/index.mjs +12 -0
- package/dist/index.mjs.map +2 -2
- package/dist/metafiles/define.cjs.meta.json +28 -0
- package/dist/metafiles/define.mjs.meta.json +32 -0
- package/dist/metafiles/drip.cjs.meta.json +3193 -0
- package/dist/metafiles/drip.debug.meta.json +2859 -574
- package/dist/metafiles/drip.heatmap.meta.json +2732 -460
- package/dist/metafiles/drip.live.meta.json +2732 -460
- package/dist/metafiles/drip.meta.json +2859 -574
- package/dist/metafiles/drip.mjs.meta.json +3235 -0
- package/dist/metafiles/drip.replay-recorder.meta.json +82 -0
- package/dist/metafiles/editor.meta.json +67 -27
- package/dist/metafiles/flags.cjs.meta.json +220 -0
- package/dist/metafiles/flags.mjs.meta.json +225 -0
- package/dist/metafiles/index.meta.json +2717 -491
- package/dist/metafiles/index.mjs.meta.json +14 -4
- package/dist/react/index.js +16621 -13539
- package/dist/react/index.js.map +4 -4
- package/dist/sri.json +20 -15
- package/dist/types/activation-runtime.d.ts +91 -0
- package/dist/types/analytics-runtime.d.ts +166 -0
- package/dist/types/antiflicker.d.ts +9 -3
- package/dist/types/assignment-cookie-sync.d.ts +31 -0
- package/dist/types/assignment-runtime.d.ts +257 -0
- package/dist/types/assignment-store.d.ts +4 -8
- package/dist/types/bucket.d.ts +1 -2
- package/dist/types/compact-store.d.ts +1 -0
- package/dist/types/config-runtime.d.ts +313 -0
- package/dist/types/consent-evaluation.d.ts +27 -0
- package/dist/types/css.d.ts +5 -3
- package/dist/types/debug-loader.d.ts +2 -0
- package/dist/types/decide-assignments.d.ts +31 -0
- package/dist/types/delivery-timing.d.ts +8 -0
- package/dist/types/early-interaction-capture.d.ts +79 -0
- package/dist/types/early-reassert.d.ts +32 -0
- package/dist/types/edge-runtime.d.ts +11 -0
- package/dist/types/editor/api.d.ts +3 -0
- package/dist/types/editor/controller.d.ts +3 -1
- package/dist/types/editor/publication-result.d.ts +11 -0
- package/dist/types/editor/types.d.ts +2 -1
- package/dist/types/editor/view-kit.d.ts +1 -1
- package/dist/types/endpoint.d.ts +8 -0
- package/dist/types/exclusion-groups.d.ts +4 -7
- package/dist/types/flags.d.ts +2 -0
- package/dist/types/goal-eligibility.d.ts +4 -0
- package/dist/types/goals.d.ts +10 -1
- package/dist/types/guards.d.ts +7 -7
- package/dist/types/handover.d.ts +12 -1
- package/dist/types/hash.d.ts +8 -4
- package/dist/types/hydration.d.ts +2 -1
- package/dist/types/index.d.ts +24 -128
- package/dist/types/init-test-helpers.d.ts +48 -0
- package/dist/types/inline-bootstrap.d.ts +26 -0
- package/dist/types/js.d.ts +3 -1
- package/dist/types/money.d.ts +12 -0
- package/dist/types/mutate.d.ts +45 -6
- package/dist/types/performance.d.ts +2 -0
- package/dist/types/persistence-decoders.d.ts +12 -0
- package/dist/types/public-api.d.ts +209 -0
- package/dist/types/qa-assistant-state.d.ts +34 -0
- package/dist/types/react/index.d.ts +28 -3
- package/dist/types/reassert-sentinel.d.ts +6 -0
- package/dist/types/redirect.d.ts +48 -0
- package/dist/types/replay-recorder-entry.d.ts +1 -0
- package/dist/types/replay-signal-state.d.ts +6 -0
- package/dist/types/replay-signals.d.ts +13 -0
- package/dist/types/replay.d.ts +110 -0
- package/dist/types/router.d.ts +12 -2
- package/dist/types/runtime-state.d.ts +166 -0
- package/dist/types/sanitize.d.ts +1 -0
- package/dist/types/shadow.d.ts +3 -0
- package/dist/types/shopify-cart.d.ts +25 -1
- package/dist/types/storefront-vitals.d.ts +24 -0
- package/dist/types/targeting-diagnostics.d.ts +1 -0
- package/dist/types/targeting.d.ts +16 -2
- package/dist/types/track.d.ts +15 -1
- package/dist/types/types.d.ts +115 -9
- package/dist/types/wait.d.ts +14 -1
- package/dist/types/weekday-schedule.d.ts +3 -0
- package/package.json +17 -8
- package/src/activation-runtime.ts +2737 -0
- package/src/analytics-runtime.ts +2175 -0
- package/src/antiflicker.test.ts +216 -44
- package/src/antiflicker.ts +103 -28
- package/src/artifact-concurrency.test.ts +1091 -0
- package/src/assignment-cookie-sync.test.ts +202 -0
- package/src/assignment-cookie-sync.ts +189 -0
- package/src/assignment-runtime.ts +3204 -0
- package/src/assignment-store.test.ts +56 -0
- package/src/assignment-store.ts +7 -38
- package/src/bucket.ts +3 -18
- package/src/commerce-analytics-lifecycle.test.ts +75 -15
- package/src/commerce-config-lifecycle.test.ts +81 -1
- package/src/commerce-price-preserve.test.ts +133 -0
- package/src/commerce-price.test.ts +97 -0
- package/src/commerce-price.ts +74 -20
- package/src/commerce-shipping.test.ts +53 -0
- package/src/commerce-shipping.ts +5 -11
- package/src/compact-store.ts +15 -0
- package/src/config-fetch.test.ts +480 -9
- package/src/config-runtime.ts +2070 -0
- package/src/consent-evaluation-cookiescript.test.ts +50 -0
- package/src/consent-evaluation.ts +138 -0
- package/src/css.test.ts +30 -0
- package/src/css.ts +120 -6
- package/src/cwv.test.ts +60 -77
- package/src/debug-loader.test.ts +214 -0
- package/src/debug-loader.ts +41 -0
- package/src/decide-assignments.test.ts +196 -0
- package/src/decide-assignments.ts +204 -0
- package/src/delivery-timing.test.ts +104 -0
- package/src/delivery-timing.ts +89 -0
- package/src/early-interaction-capture.test.ts +451 -0
- package/src/early-interaction-capture.ts +230 -0
- package/src/early-reassert.test.ts +207 -0
- package/src/early-reassert.ts +152 -0
- package/src/edge-adoption.test.ts +435 -0
- package/src/edge-runtime.ts +30 -0
- package/src/editor/api.test.ts +109 -0
- package/src/editor/api.ts +34 -3
- package/src/editor/controller.test.ts +297 -14
- package/src/editor/controller.ts +69 -5
- package/src/editor/index.test.ts +2 -0
- package/src/editor/index.ts +1 -0
- package/src/editor/publication-result.ts +58 -0
- package/src/editor/types.ts +10 -1
- package/src/editor/view-kit.ts +2 -1
- package/src/editor/view.test.ts +108 -0
- package/src/editor/view.ts +49 -7
- package/src/endpoint.test.ts +15 -0
- package/src/endpoint.ts +10 -0
- package/src/exclusion-groups.ts +8 -18
- package/src/flags.test.ts +88 -1
- package/src/flags.ts +29 -29
- package/src/global-runtime-reconciler.test.ts +271 -0
- package/src/goal-eligibility.test.ts +29 -0
- package/src/goal-eligibility.ts +17 -0
- package/src/goals.test.ts +144 -1
- package/src/goals.ts +97 -18
- package/src/guards.test.ts +99 -1
- package/src/guards.ts +34 -15
- package/src/handover-cache-order.test.ts +6 -3
- package/src/handover.test.ts +154 -8
- package/src/handover.ts +122 -12
- package/src/hash.ts +29 -54
- package/src/holdout.test.ts +26 -0
- package/src/hydration.test.ts +73 -2
- package/src/hydration.ts +18 -9
- package/src/index.ts +130 -7287
- package/src/init-analytics.test.ts +1694 -0
- package/src/init-anti-flicker.test.ts +1211 -0
- package/src/init-cart-attributes.test.ts +2003 -0
- package/src/init-config-resolution.test.ts +999 -0
- package/src/init-consent.test.ts +971 -0
- package/src/init-empty.test.ts +11 -5406
- package/src/init-integrations.test.ts +846 -0
- package/src/init-page-activation.test.ts +5064 -0
- package/src/init-qa-loading.test.ts +740 -0
- package/src/init-redirects.test.ts +2156 -0
- package/src/init-test-helpers.ts +287 -0
- package/src/inline-bootstrap.test.ts +184 -0
- package/src/inline-bootstrap.ts +73 -0
- package/src/js.test.ts +51 -1
- package/src/js.ts +86 -2
- package/src/money.test.ts +55 -1
- package/src/money.ts +104 -2
- package/src/money.web-parity.contract.test.ts +85 -0
- package/src/mutate.test.ts +474 -10
- package/src/mutate.ts +739 -230
- package/src/page-trigger.test.ts +218 -8
- package/src/performance.ts +10 -0
- package/src/persistence-decoders.ts +18 -0
- package/src/public-api.ts +1828 -0
- package/src/qa-assistant-state.test.ts +249 -3
- package/src/qa-assistant-state.ts +146 -6
- package/src/qa-assistant-view.test.ts +220 -2
- package/src/qa-assistant-view.ts +102 -5
- package/src/react/index.test.tsx +37 -0
- package/src/react/index.tsx +19 -3
- package/src/reassert-sentinel.test.ts +155 -1
- package/src/reassert-sentinel.ts +168 -8
- package/src/redirect.test.ts +541 -0
- package/src/redirect.ts +572 -0
- package/src/replay-recorder-entry.ts +115 -0
- package/src/replay-signal-state.ts +100 -0
- package/src/replay-signals.test.ts +249 -0
- package/src/replay-signals.ts +151 -0
- package/src/replay.test.ts +764 -0
- package/src/replay.ts +514 -0
- package/src/router.test.ts +61 -2
- package/src/router.ts +50 -12
- package/src/runtime-state.ts +507 -0
- package/src/runtime.test.ts +174 -7
- package/src/runtime.ts +88 -14
- package/src/sanitize.test.ts +34 -0
- package/src/sanitize.ts +23 -12
- package/src/shadow.test.ts +187 -0
- package/src/shadow.ts +99 -0
- package/src/shopify-cart.test.ts +733 -23
- package/src/shopify-cart.ts +402 -49
- package/src/storefront-vitals.test.ts +217 -0
- package/src/storefront-vitals.ts +206 -0
- package/src/surface-signals.ts +6 -0
- package/src/targeting-conformance.test.ts +9 -0
- package/src/targeting-diagnostics.ts +38 -8
- package/src/targeting-parity-corpus.json +559 -0
- package/src/targeting-parity-corpus.test.ts +18 -0
- package/src/targeting.detailed.test.ts +105 -0
- package/src/targeting.test.ts +211 -0
- package/src/targeting.ts +88 -207
- package/src/track.test.ts +147 -0
- package/src/track.ts +125 -26
- package/src/types.ts +120 -13
- package/src/wait.test.ts +23 -0
- package/src/wait.ts +129 -13
- package/src/weekday-delivery.test.ts +56 -0
- package/src/weekday-schedule-parity-corpus.json +92 -0
- package/src/weekday-schedule.test.ts +69 -0
- package/src/weekday-schedule.ts +20 -0
|
@@ -118,6 +118,62 @@ describe("persisted assignment store", () => {
|
|
|
118
118
|
expect(localStorage.getItem("drip_assignments_v1")).toBeNull();
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
+
it("restores a persisted redirect control assignment after navigation off the source URL", async () => {
|
|
122
|
+
localStorage.setItem(
|
|
123
|
+
"drip_assignments_v1",
|
|
124
|
+
JSON.stringify({ exp_1: ["exp_1_control", 1, 123] }),
|
|
125
|
+
);
|
|
126
|
+
const { evaluate } = await import("./index");
|
|
127
|
+
const redirect = experiment("exp_1", [1, 0]);
|
|
128
|
+
redirect.experimentType = "redirect";
|
|
129
|
+
redirect.targeting = {
|
|
130
|
+
url: [{ type: "simple", pattern: "/products/source" }],
|
|
131
|
+
};
|
|
132
|
+
redirect.variations[1].redirectUrl = "https://store.test/landing";
|
|
133
|
+
|
|
134
|
+
expect(evaluate([redirect], {
|
|
135
|
+
url: "https://store.test/checkout",
|
|
136
|
+
userId: "visitor-1",
|
|
137
|
+
})).toEqual([
|
|
138
|
+
expect.objectContaining({
|
|
139
|
+
experimentId: "exp_1",
|
|
140
|
+
variationId: "exp_1_control",
|
|
141
|
+
}),
|
|
142
|
+
]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("drops a persisted redirect control assignment when its segment gate fails", async () => {
|
|
146
|
+
localStorage.setItem(
|
|
147
|
+
"drip_assignments_v1",
|
|
148
|
+
JSON.stringify({ exp_1: ["exp_1_control", 1, 123] }),
|
|
149
|
+
);
|
|
150
|
+
const { evaluate } = await import("./index");
|
|
151
|
+
const redirect = experiment("exp_1", [1, 0]);
|
|
152
|
+
redirect.experimentType = "redirect";
|
|
153
|
+
redirect.targeting = {
|
|
154
|
+
url: [{ type: "simple", pattern: "/products/source" }],
|
|
155
|
+
segmentRules: [{
|
|
156
|
+
id: "vip-segment",
|
|
157
|
+
rules: {
|
|
158
|
+
version: 1,
|
|
159
|
+
operator: "and",
|
|
160
|
+
conditions: [{
|
|
161
|
+
signal: "customer.tier",
|
|
162
|
+
operator: "equals",
|
|
163
|
+
value: "vip",
|
|
164
|
+
}],
|
|
165
|
+
},
|
|
166
|
+
}],
|
|
167
|
+
};
|
|
168
|
+
redirect.variations[1].redirectUrl = "https://store.test/landing";
|
|
169
|
+
|
|
170
|
+
expect(evaluate([redirect], {
|
|
171
|
+
attributes: { "customer.tier": "standard" },
|
|
172
|
+
url: "https://store.test/checkout",
|
|
173
|
+
userId: "visitor-1",
|
|
174
|
+
})).toEqual([]);
|
|
175
|
+
});
|
|
176
|
+
|
|
121
177
|
it("does not restore when non-page audience targeting is no longer eligible", async () => {
|
|
122
178
|
localStorage.setItem(
|
|
123
179
|
"drip_assignments_v1",
|
package/src/assignment-store.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { Experiment, Variation } from "./types";
|
|
2
|
+
import { decodeCompactStore } from "./compact-store";
|
|
3
|
+
import { decodePersistedAssignments, type PersistedAssignmentRecord } from "./persistence-decoders";
|
|
4
|
+
export { decodePersistedAssignments, type PersistedAssignmentRecord } from "./persistence-decoders";
|
|
2
5
|
|
|
3
6
|
const STORAGE_KEY = "drip_assignments_v1"; // gitleaks:allow — public storage key name, not a credential
|
|
4
7
|
const COOKIE_NAME = "drip_assignments_v1";
|
|
@@ -6,12 +9,6 @@ const MAX_ENTRIES = 80;
|
|
|
6
9
|
const MAX_COOKIE_ENTRIES = 20;
|
|
7
10
|
const COOKIE_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;
|
|
8
11
|
|
|
9
|
-
export type PersistedAssignmentRecord = {
|
|
10
|
-
variationId: string;
|
|
11
|
-
epoch: number;
|
|
12
|
-
assignedAt: number;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
12
|
type CompactRecord = [string, number, number];
|
|
16
13
|
type CompactStore = Record<string, CompactRecord>;
|
|
17
14
|
|
|
@@ -72,28 +69,8 @@ function clearCookie(): void {
|
|
|
72
69
|
}
|
|
73
70
|
}
|
|
74
71
|
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
const parsed = JSON.parse(raw) as unknown;
|
|
79
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
80
|
-
const out: CompactStore = {};
|
|
81
|
-
for (const [experimentId, value] of Object.entries(parsed)) {
|
|
82
|
-
if (
|
|
83
|
-
typeof experimentId !== "string" ||
|
|
84
|
-
!Array.isArray(value) ||
|
|
85
|
-
typeof value[0] !== "string" ||
|
|
86
|
-
typeof value[1] !== "number" ||
|
|
87
|
-
typeof value[2] !== "number"
|
|
88
|
-
) {
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
out[experimentId] = [value[0], value[1], value[2]];
|
|
92
|
-
}
|
|
93
|
-
return out;
|
|
94
|
-
} catch {
|
|
95
|
-
return {};
|
|
96
|
-
}
|
|
72
|
+
export function decodeCompactAssignmentStore(raw: string | null): CompactStore {
|
|
73
|
+
return decodeCompactStore(raw, 3) as CompactStore;
|
|
97
74
|
}
|
|
98
75
|
|
|
99
76
|
function serializeStore(store: CompactStore, maxEntries = MAX_ENTRIES): string {
|
|
@@ -109,15 +86,7 @@ function serializeStore(store: CompactStore, maxEntries = MAX_ENTRIES): string {
|
|
|
109
86
|
}
|
|
110
87
|
|
|
111
88
|
export function readPersistedAssignments(): Record<string, PersistedAssignmentRecord> {
|
|
112
|
-
|
|
113
|
-
...parseStore(readCookie()),
|
|
114
|
-
...parseStore(readLocalStorage()),
|
|
115
|
-
};
|
|
116
|
-
const out: Record<string, PersistedAssignmentRecord> = {};
|
|
117
|
-
for (const [experimentId, [variationId, epoch, assignedAt]] of Object.entries(compact)) {
|
|
118
|
-
out[experimentId] = { variationId, epoch, assignedAt };
|
|
119
|
-
}
|
|
120
|
-
return out;
|
|
89
|
+
return decodePersistedAssignments(readLocalStorage(), readCookie());
|
|
121
90
|
}
|
|
122
91
|
|
|
123
92
|
export function writePersistedAssignments(
|
|
@@ -203,6 +172,6 @@ export const __assignmentStoreInternals = {
|
|
|
203
172
|
STORAGE_KEY,
|
|
204
173
|
COOKIE_NAME,
|
|
205
174
|
assignmentEpoch,
|
|
206
|
-
parseStore,
|
|
175
|
+
parseStore: decodeCompactAssignmentStore,
|
|
207
176
|
serializeStore,
|
|
208
177
|
};
|
package/src/bucket.ts
CHANGED
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
return hashValue < allocation;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function chooseVariation(hashValue: number, weights: number[]): number {
|
|
8
|
-
if (!weights.length) return -1;
|
|
9
|
-
const total = weights.reduce((sum, w) => sum + (w > 0 ? w : 0), 0);
|
|
10
|
-
if (total <= 0) return 0;
|
|
11
|
-
const n = hashValue * total;
|
|
12
|
-
let acc = 0;
|
|
13
|
-
for (let i = 0; i < weights.length; i++) {
|
|
14
|
-
acc += weights[i] > 0 ? weights[i] : 0;
|
|
15
|
-
if (n < acc) return i;
|
|
16
|
-
}
|
|
17
|
-
return weights.length - 1;
|
|
18
|
-
}
|
|
1
|
+
// The bucketing kernel lives in @drip/shared-runtime/bucketing (#2476). This
|
|
2
|
+
// module keeps re-exporting it so SDK call sites stay unchanged.
|
|
3
|
+
export { chooseVariation, inTrafficAllocation } from "@drip/shared-runtime/bucketing";
|
|
@@ -19,6 +19,7 @@ describe("commerce analytics lifecycle", () => {
|
|
|
19
19
|
delete (globalThis as any).Apex;
|
|
20
20
|
localStorage.clear();
|
|
21
21
|
sessionStorage.clear();
|
|
22
|
+
document.cookie = "drip_assignments_v1=; Max-Age=0; Path=/; SameSite=Lax";
|
|
22
23
|
installCommerceProductPage();
|
|
23
24
|
fetchMock = vi.fn(async (input: RequestInfo | URL) =>
|
|
24
25
|
String(input).endsWith("/cart.js")
|
|
@@ -29,7 +30,9 @@ describe("commerce analytics lifecycle", () => {
|
|
|
29
30
|
globalThis.fetch = fetchMock as typeof fetch;
|
|
30
31
|
});
|
|
31
32
|
|
|
32
|
-
afterEach(() => {
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
const { cleanupCommerceShipping } = await import("./commerce-shipping");
|
|
35
|
+
cleanupCommerceShipping();
|
|
33
36
|
vi.clearAllTimers();
|
|
34
37
|
vi.useRealTimers();
|
|
35
38
|
vi.restoreAllMocks();
|
|
@@ -157,7 +160,7 @@ describe("commerce analytics lifecycle", () => {
|
|
|
157
160
|
});
|
|
158
161
|
});
|
|
159
162
|
|
|
160
|
-
it("keeps an off-target
|
|
163
|
+
it("keeps an off-target persisted price pin attributable without rendering it", async () => {
|
|
161
164
|
delete (window as Window & { ShopifyAnalytics?: unknown }).ShopifyAnalytics;
|
|
162
165
|
document.body.insertAdjacentHTML(
|
|
163
166
|
"beforeend",
|
|
@@ -202,6 +205,10 @@ describe("commerce analytics lifecycle", () => {
|
|
|
202
205
|
})
|
|
203
206
|
: new Response("ok", { status: 202 }));
|
|
204
207
|
globalThis.fetch = fetchMock as typeof fetch;
|
|
208
|
+
localStorage.setItem(
|
|
209
|
+
"drip_assignments_v1",
|
|
210
|
+
JSON.stringify({ exp_price: ["treatment", 1, 123] }),
|
|
211
|
+
);
|
|
205
212
|
const onExperimentViewed = vi.fn();
|
|
206
213
|
const { flush, getAssignments, init, trackRevenue } = await import("./index");
|
|
207
214
|
|
|
@@ -222,24 +229,44 @@ describe("commerce analytics lifecycle", () => {
|
|
|
222
229
|
});
|
|
223
230
|
expect(document.querySelector(".price")?.textContent).toBe("€1.299,00");
|
|
224
231
|
expect(document.querySelector(".companion")?.textContent).toBe("Control companion");
|
|
225
|
-
expect(getAssignments()).toEqual([
|
|
232
|
+
expect(getAssignments()).toEqual([
|
|
233
|
+
expect.objectContaining({
|
|
234
|
+
experimentId: "exp_price",
|
|
235
|
+
variationId: "treatment",
|
|
236
|
+
pageActive: false,
|
|
237
|
+
}),
|
|
238
|
+
]);
|
|
226
239
|
expect(onExperimentViewed).not.toHaveBeenCalled();
|
|
227
240
|
expect(localStorage.getItem("drip_exposures")).toBeNull();
|
|
228
241
|
|
|
229
242
|
trackRevenue(19.99, { orderId: "off-target-order" });
|
|
230
243
|
await flush();
|
|
231
244
|
const trackedEvents = fetchMock.mock.calls
|
|
232
|
-
.filter(([url]) => String(url) === "https://events.test/events")
|
|
245
|
+
.filter(([url]) => String(url) === "https://events.test/v1/events")
|
|
233
246
|
.flatMap(([, options]) => JSON.parse(String(options?.body)).events);
|
|
234
247
|
expect(trackedEvents.some((event: { type: string }) =>
|
|
235
|
-
event.type === "experiment_viewed"
|
|
248
|
+
event.type === "experiment_viewed"
|
|
236
249
|
)).toBe(false);
|
|
250
|
+
expect(trackedEvents).toContainEqual(expect.objectContaining({
|
|
251
|
+
type: "goal",
|
|
252
|
+
experimentId: "exp_price",
|
|
253
|
+
variationId: "treatment",
|
|
254
|
+
data: expect.objectContaining({
|
|
255
|
+
goalId: "__revenue__",
|
|
256
|
+
revenue: 19.99,
|
|
257
|
+
orderId: "off-target-order",
|
|
258
|
+
}),
|
|
259
|
+
}));
|
|
260
|
+
expect(JSON.parse(localStorage.getItem("drip_assignments_v1") ?? "{}"))
|
|
261
|
+
.toHaveProperty("exp_price", ["treatment", 1, 123]);
|
|
237
262
|
const cartUpdate = fetchMock.mock.calls.find(([url]) => url === "/cart/update.js");
|
|
238
|
-
|
|
239
|
-
|
|
263
|
+
const cartAttributes = JSON.parse(String(cartUpdate?.[1]?.body)).attributes;
|
|
264
|
+
expect(cartAttributes).toMatchObject({
|
|
240
265
|
apex_assignment_authorization: pinnedToken,
|
|
241
266
|
});
|
|
242
|
-
|
|
267
|
+
expect(JSON.parse(cartAttributes.apex_assignments)).toEqual([
|
|
268
|
+
{ e: "exp_price", v: "treatment", ep: 1 },
|
|
269
|
+
]);
|
|
243
270
|
});
|
|
244
271
|
|
|
245
272
|
it("keeps off-target offer pins charge-only while applying targeted offers", async () => {
|
|
@@ -318,8 +345,12 @@ describe("commerce analytics lifecycle", () => {
|
|
|
318
345
|
})
|
|
319
346
|
: new Response("ok", { status: 202 }));
|
|
320
347
|
globalThis.fetch = fetchMock as typeof fetch;
|
|
348
|
+
localStorage.setItem(
|
|
349
|
+
"drip_assignments_v1",
|
|
350
|
+
JSON.stringify({ exp_offer_off_target: ["off_target_treatment", 1, 123] }),
|
|
351
|
+
);
|
|
321
352
|
const onExperimentViewed = vi.fn();
|
|
322
|
-
const { getAssignments, init } = await import("./index");
|
|
353
|
+
const { flush, getAssignments, init, trackRevenue } = await import("./index");
|
|
323
354
|
|
|
324
355
|
init({
|
|
325
356
|
antiFlickerMode: "off",
|
|
@@ -350,23 +381,52 @@ describe("commerce analytics lifecycle", () => {
|
|
|
350
381
|
.toBe("Off-target control");
|
|
351
382
|
expect(document.querySelector(".targeted-offer")?.textContent)
|
|
352
383
|
.toBe("Targeted treatment");
|
|
353
|
-
expect(getAssignments()).toEqual([
|
|
354
|
-
expect.objectContaining({
|
|
355
|
-
|
|
384
|
+
expect(getAssignments()).toEqual(expect.arrayContaining([
|
|
385
|
+
expect.objectContaining({
|
|
386
|
+
experimentId: "exp_offer_off_target",
|
|
387
|
+
variationId: "off_target_treatment",
|
|
388
|
+
pageActive: false,
|
|
389
|
+
}),
|
|
390
|
+
expect.objectContaining({
|
|
391
|
+
experimentId: "exp_offer_targeted",
|
|
392
|
+
variationId: "targeted_treatment",
|
|
393
|
+
pageActive: true,
|
|
394
|
+
}),
|
|
395
|
+
]));
|
|
356
396
|
expect(onExperimentViewed).toHaveBeenCalledTimes(1);
|
|
357
397
|
expect(onExperimentViewed).toHaveBeenCalledWith(
|
|
358
398
|
expect.objectContaining({ experimentId: "exp_offer_targeted" }),
|
|
359
399
|
);
|
|
360
400
|
expect(JSON.parse(localStorage.getItem("drip_assignments_v1") ?? "{}"))
|
|
361
|
-
.
|
|
401
|
+
.toHaveProperty("exp_offer_off_target", ["off_target_treatment", 1, expect.any(Number)]);
|
|
362
402
|
expect(JSON.parse(localStorage.getItem("drip_assignments_v1") ?? "{}"))
|
|
363
403
|
.toHaveProperty("exp_offer_targeted");
|
|
404
|
+
trackRevenue(24.99, { orderId: "off-target-offer-order" });
|
|
405
|
+
await flush();
|
|
406
|
+
const trackedEvents = fetchMock.mock.calls
|
|
407
|
+
.filter(([url]) => String(url) === "https://events.test/v1/events")
|
|
408
|
+
.flatMap(([, options]) => JSON.parse(String(options?.body)).events);
|
|
409
|
+
expect(trackedEvents).toContainEqual(expect.objectContaining({
|
|
410
|
+
type: "goal",
|
|
411
|
+
experimentId: "exp_offer_off_target",
|
|
412
|
+
variationId: "off_target_treatment",
|
|
413
|
+
data: expect.objectContaining({
|
|
414
|
+
goalId: "__revenue__",
|
|
415
|
+
revenue: 24.99,
|
|
416
|
+
orderId: "off-target-offer-order",
|
|
417
|
+
}),
|
|
418
|
+
}));
|
|
364
419
|
const cartUpdate = fetchMock.mock.calls.find(([url]) => url === "/cart/update.js");
|
|
365
|
-
|
|
366
|
-
.
|
|
420
|
+
const stampedAssignments = JSON.parse(
|
|
421
|
+
JSON.parse(String(cartUpdate?.[1]?.body)).attributes.apex_assignments,
|
|
422
|
+
);
|
|
423
|
+
expect(stampedAssignments).toEqual(expect.arrayContaining([
|
|
367
424
|
{ e: "exp_offer_off_target", v: "off_target_treatment", ep: 1 },
|
|
368
425
|
{ e: "exp_offer_targeted", v: "targeted_treatment", ep: 1 },
|
|
369
426
|
]));
|
|
427
|
+
expect(stampedAssignments.filter(
|
|
428
|
+
(assignment: { e: string }) => assignment.e === "exp_offer_off_target",
|
|
429
|
+
)).toHaveLength(1);
|
|
370
430
|
await expect(window.fetch("/cart/add.js", { method: "POST" }))
|
|
371
431
|
.resolves.toMatchObject({ status: 202 });
|
|
372
432
|
});
|
|
@@ -54,7 +54,11 @@ describe("commerce config lifecycle", () => {
|
|
|
54
54
|
}) as typeof fetch;
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
afterEach(() => {
|
|
57
|
+
afterEach(async () => {
|
|
58
|
+
const { cleanupCommercePrice } = await import("./commerce-price");
|
|
59
|
+
const { cleanupCommerceShipping } = await import("./commerce-shipping");
|
|
60
|
+
cleanupCommercePrice();
|
|
61
|
+
cleanupCommerceShipping();
|
|
58
62
|
vi.restoreAllMocks();
|
|
59
63
|
delete (globalThis as typeof globalThis & { __drip?: unknown }).__drip;
|
|
60
64
|
globalThis.fetch = originalFetch;
|
|
@@ -211,6 +215,82 @@ describe("commerce config lifecycle", () => {
|
|
|
211
215
|
});
|
|
212
216
|
});
|
|
213
217
|
|
|
218
|
+
it("cannot reassert a deactivated Page price while a non-commerce assignment remains", async () => {
|
|
219
|
+
history.replaceState({}, "", "/products/current-product");
|
|
220
|
+
globalThis.fetch = vi.fn(async (input: RequestInfo | URL) => {
|
|
221
|
+
const url = String(input);
|
|
222
|
+
if (url.endsWith("/cart.js")) {
|
|
223
|
+
return Response.json({ token: "cart-page-price?key=runtime-secret" });
|
|
224
|
+
}
|
|
225
|
+
if (url.includes("/v1/assign")) {
|
|
226
|
+
return Response.json(commerceAssignmentResponseFixture);
|
|
227
|
+
}
|
|
228
|
+
return new Response("ok", { status: 200 });
|
|
229
|
+
}) as typeof fetch;
|
|
230
|
+
const targetedExperiments = [
|
|
231
|
+
{
|
|
232
|
+
...experiments[0]!,
|
|
233
|
+
targeting: {
|
|
234
|
+
pageRules: [{
|
|
235
|
+
id: "page_price_product",
|
|
236
|
+
includeRules: [{ type: "simple" as const, pattern: "/products/*" }],
|
|
237
|
+
trigger: { deactivationMode: "reset" as const },
|
|
238
|
+
}],
|
|
239
|
+
},
|
|
240
|
+
variations: [{
|
|
241
|
+
...experiments[0]!.variations[0]!,
|
|
242
|
+
js: "window.__pricePageApplied=true;",
|
|
243
|
+
}],
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
id: "exp_non_commerce_sibling",
|
|
247
|
+
status: "running" as const,
|
|
248
|
+
trafficAllocation: 1,
|
|
249
|
+
variations: [{
|
|
250
|
+
id: "var_non_commerce_sibling",
|
|
251
|
+
weight: 1,
|
|
252
|
+
mutations: [],
|
|
253
|
+
js: "window.__nonCommerceSiblingApplied=true;",
|
|
254
|
+
}],
|
|
255
|
+
},
|
|
256
|
+
];
|
|
257
|
+
const disconnect = vi.spyOn(MutationObserver.prototype, "disconnect");
|
|
258
|
+
const { getAssignments, init, reInit } = await import("./index");
|
|
259
|
+
|
|
260
|
+
init({
|
|
261
|
+
antiFlickerMode: "off",
|
|
262
|
+
commerce,
|
|
263
|
+
endpoint: "https://events.test",
|
|
264
|
+
experiments: targetedExperiments,
|
|
265
|
+
ingestSignature: "ingest-token",
|
|
266
|
+
shopId: "shop_123",
|
|
267
|
+
trackingStart: "immediate",
|
|
268
|
+
userId: "visitor_page_price",
|
|
269
|
+
});
|
|
270
|
+
await waitForPrice("€1.149,95");
|
|
271
|
+
|
|
272
|
+
history.replaceState({}, "", "/collections/all");
|
|
273
|
+
reInit();
|
|
274
|
+
expect(getAssignments()).toEqual(expect.arrayContaining([
|
|
275
|
+
expect.objectContaining({
|
|
276
|
+
experimentId: "exp_price",
|
|
277
|
+
pageActive: false,
|
|
278
|
+
}),
|
|
279
|
+
expect.objectContaining({
|
|
280
|
+
experimentId: "exp_non_commerce_sibling",
|
|
281
|
+
pageActive: true,
|
|
282
|
+
}),
|
|
283
|
+
]));
|
|
284
|
+
await waitForPrice("€1.299,00");
|
|
285
|
+
expect(disconnect).toHaveBeenCalled();
|
|
286
|
+
|
|
287
|
+
const price = document.querySelector(".price")!;
|
|
288
|
+
price.textContent = "€1.399,00";
|
|
289
|
+
await Promise.resolve();
|
|
290
|
+
await Promise.resolve();
|
|
291
|
+
expect(price.textContent).toBe("€1.399,00");
|
|
292
|
+
});
|
|
293
|
+
|
|
214
294
|
it("clears a stale shipping authorization after the Worker confirms the assignment disappeared", async () => {
|
|
215
295
|
sessionStorage.setItem("drip_cart_attr", JSON.stringify({
|
|
216
296
|
assignments: [["exp_shipping", "treatment", 6]],
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { applyMutations, reassertNonStructuralMutations } from "./mutate";
|
|
4
|
+
import { applyMutationWithWait } from "./wait";
|
|
5
|
+
|
|
6
|
+
describe("content treatment price preservation", () => {
|
|
7
|
+
beforeEach(() => { document.body.innerHTML = ""; });
|
|
8
|
+
|
|
9
|
+
function apply(text: string, replacement: string, options: Record<string, unknown> = {}) {
|
|
10
|
+
document.body.innerHTML = `<div class="target">${text}</div>`;
|
|
11
|
+
const onViolation = vi.fn();
|
|
12
|
+
applyMutations([{ selector: ".target", action: "text", text: replacement }], {
|
|
13
|
+
preservePrices: true,
|
|
14
|
+
onPricePreservationViolation: onViolation,
|
|
15
|
+
...options,
|
|
16
|
+
});
|
|
17
|
+
return { element: document.querySelector(".target")!, onViolation };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
it("applies content when every displayed price survives", () => {
|
|
21
|
+
const result = apply("Now $1,299.95", "Only $1,299.95 today");
|
|
22
|
+
expect(result.element.textContent).toBe("Only $1,299.95 today");
|
|
23
|
+
expect(result.onViolation).not.toHaveBeenCalled();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("reverts and reports content that drops a price", () => {
|
|
27
|
+
const result = apply("Now €1.299,95", "On sale");
|
|
28
|
+
expect(result.element.textContent).toBe("Now €1.299,95");
|
|
29
|
+
expect(result.onViolation).toHaveBeenCalledWith(expect.objectContaining({
|
|
30
|
+
selector: ".target",
|
|
31
|
+
originalPriceTokens: [expect.objectContaining({ raw: "€1.299,95" })],
|
|
32
|
+
}));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("reverts a changed price value", () => {
|
|
36
|
+
expect(apply("£99.00", "£79.00").element.textContent).toBe("£99.00");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it.each([
|
|
40
|
+
["Now 1.299,95 \u20ac", "1.299,95 \u20ac"],
|
|
41
|
+
["Now CHF 99.00", "CHF 99.00"],
|
|
42
|
+
["Now 99.00 EUR", "99.00 EUR"],
|
|
43
|
+
["Now 99 kr", "99 kr"],
|
|
44
|
+
])("reverts and reports dropped suffix/code price %s", (text, raw) => {
|
|
45
|
+
const result = apply(text, "On sale");
|
|
46
|
+
expect(result.element.textContent).toBe(text);
|
|
47
|
+
expect(result.onViolation).toHaveBeenCalledWith(expect.objectContaining({
|
|
48
|
+
originalPriceTokens: [expect.objectContaining({ raw })],
|
|
49
|
+
}));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("does not treat bare numbers as money", () => {
|
|
53
|
+
const result = apply("Model SKU 1299", "Model SKU 1200");
|
|
54
|
+
expect(result.element.textContent).toBe("Model SKU 1200");
|
|
55
|
+
expect(result.onViolation).not.toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("keeps a rejected mutation blocked during reassertion", () => {
|
|
59
|
+
const mutation = { selector: ".target", action: "text" as const, text: "On sale" };
|
|
60
|
+
document.body.innerHTML = '<div class="target">Now $19.00</div>';
|
|
61
|
+
applyMutations([mutation], { preservePrices: true });
|
|
62
|
+
|
|
63
|
+
reassertNonStructuralMutations([mutation]);
|
|
64
|
+
|
|
65
|
+
expect(document.querySelector(".target")?.textContent).toBe("Now $19.00");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("reverts and reports setText content that drops a price", () => {
|
|
69
|
+
document.body.innerHTML = '<div class="target">Now $29.00</div>';
|
|
70
|
+
const onViolation = vi.fn();
|
|
71
|
+
applyMutations([{ selector: ".target", action: "setText", text: "On sale" }], {
|
|
72
|
+
preservePrices: true,
|
|
73
|
+
onPricePreservationViolation: onViolation,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(document.querySelector(".target")?.textContent).toBe("Now $29.00");
|
|
77
|
+
expect(onViolation).toHaveBeenCalledWith(expect.objectContaining({
|
|
78
|
+
selector: ".target",
|
|
79
|
+
originalPriceTokens: [expect.objectContaining({ raw: "$29.00" })],
|
|
80
|
+
}));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("guards setText content applied through the wait path", () => {
|
|
84
|
+
document.body.innerHTML = '<div class="target">Now $39.00</div>';
|
|
85
|
+
const onViolation = vi.fn();
|
|
86
|
+
applyMutationWithWait(
|
|
87
|
+
{ selector: ".target", action: "setText", text: "On sale" },
|
|
88
|
+
0,
|
|
89
|
+
undefined,
|
|
90
|
+
undefined,
|
|
91
|
+
[],
|
|
92
|
+
undefined,
|
|
93
|
+
0,
|
|
94
|
+
{ enabled: true, onViolation },
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
expect(document.querySelector(".target")?.textContent).toBe("Now $39.00");
|
|
98
|
+
expect(onViolation).toHaveBeenCalledWith(expect.objectContaining({
|
|
99
|
+
selector: ".target",
|
|
100
|
+
originalPriceTokens: [expect.objectContaining({ raw: "$39.00" })],
|
|
101
|
+
}));
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("resolves observer reveal accounting immediately when the guard rejects", () => {
|
|
105
|
+
document.body.innerHTML = '<div class="target">Now $39.00</div>';
|
|
106
|
+
const onApplied = vi.fn();
|
|
107
|
+
const onMiss = vi.fn();
|
|
108
|
+
applyMutationWithWait(
|
|
109
|
+
{ selector: ".target", action: "setText", text: "On sale" },
|
|
110
|
+
0,
|
|
111
|
+
onApplied,
|
|
112
|
+
onMiss,
|
|
113
|
+
[],
|
|
114
|
+
undefined,
|
|
115
|
+
0,
|
|
116
|
+
{ enabled: true, onViolation: vi.fn() },
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
expect(onMiss).toHaveBeenCalledTimes(1);
|
|
120
|
+
expect(onApplied).not.toHaveBeenCalled();
|
|
121
|
+
expect(document.querySelector(".target")?.textContent).toBe("Now $39.00");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("preserves multiple currencies and values", () => {
|
|
125
|
+
const result = apply("¥1000 / ₹1,000 / $10", "Compare ¥1000, ₹1,000 and $10");
|
|
126
|
+
expect(result.element.textContent).toContain("Compare");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("keeps the no-price fast path and intentional price tests exempt", () => {
|
|
130
|
+
expect(apply("Hello", "Goodbye").element.textContent).toBe("Goodbye");
|
|
131
|
+
expect(apply("$10", "$12", { priceTestExempt: true }).element.textContent).toBe("$12");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -87,6 +87,103 @@ describe("commerce price display swap", () => {
|
|
|
87
87
|
);
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
it.each([
|
|
91
|
+
{
|
|
92
|
+
currency: "JPY",
|
|
93
|
+
published: 129900,
|
|
94
|
+
treatment: "1149",
|
|
95
|
+
before: "¥1,299",
|
|
96
|
+
after: "¥1,149",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
currency: "BHD",
|
|
100
|
+
published: 128.2,
|
|
101
|
+
treatment: "1.123",
|
|
102
|
+
before: "KD 1.282",
|
|
103
|
+
after: "KD 1.123",
|
|
104
|
+
},
|
|
105
|
+
])("swaps $currency prices at their ISO-4217 scale", ({
|
|
106
|
+
currency,
|
|
107
|
+
published,
|
|
108
|
+
treatment,
|
|
109
|
+
before,
|
|
110
|
+
after,
|
|
111
|
+
}) => {
|
|
112
|
+
shopify("product", currency);
|
|
113
|
+
Object.assign((window as any).ShopifyAnalytics.meta.product, {
|
|
114
|
+
variants: [{ id: 123, price: published }],
|
|
115
|
+
});
|
|
116
|
+
document.body.innerHTML = `<div class="price">${before}</div>`;
|
|
117
|
+
const scaledSettings: CommercePriceSettings = {
|
|
118
|
+
...settings,
|
|
119
|
+
experiments: [{
|
|
120
|
+
...settings.experiments[0],
|
|
121
|
+
currency,
|
|
122
|
+
variants: {
|
|
123
|
+
treatment: [{
|
|
124
|
+
variant_gid: "gid://shopify/ProductVariant/123",
|
|
125
|
+
fixedPricePerUnit: treatment,
|
|
126
|
+
}],
|
|
127
|
+
},
|
|
128
|
+
}],
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
applyCommercePrice(scaledSettings, assignments);
|
|
132
|
+
|
|
133
|
+
expect(document.querySelector(".price")?.textContent).toBe(after);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("fails closed when a Shopify stored price cannot represent whole minor units", () => {
|
|
137
|
+
shopify("product", "BHD");
|
|
138
|
+
Object.assign((window as any).ShopifyAnalytics.meta.product, {
|
|
139
|
+
variants: [{ id: 123, price: 128.25 }],
|
|
140
|
+
});
|
|
141
|
+
document.body.innerHTML = '<div class="price">KD 1.282</div>';
|
|
142
|
+
const bhdSettings: CommercePriceSettings = {
|
|
143
|
+
...settings,
|
|
144
|
+
experiments: [{
|
|
145
|
+
...settings.experiments[0],
|
|
146
|
+
currency: "BHD",
|
|
147
|
+
variants: {
|
|
148
|
+
treatment: [{
|
|
149
|
+
variant_gid: "gid://shopify/ProductVariant/123",
|
|
150
|
+
fixedPricePerUnit: "1.123",
|
|
151
|
+
}],
|
|
152
|
+
},
|
|
153
|
+
}],
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
applyCommercePrice(bhdSettings, assignments);
|
|
157
|
+
|
|
158
|
+
expect(document.querySelector(".price")?.textContent).toBe("KD 1.282");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("fails closed when the active currency has unknown precision", () => {
|
|
162
|
+
shopify("product", "ZZZ");
|
|
163
|
+
Object.assign((window as any).ShopifyAnalytics.meta.product, {
|
|
164
|
+
variants: [{ id: 123, price: 1000 }],
|
|
165
|
+
});
|
|
166
|
+
document.body.innerHTML = '<div class="price">ZZ 10.00</div>';
|
|
167
|
+
const unknownSettings: CommercePriceSettings = {
|
|
168
|
+
...settings,
|
|
169
|
+
experiments: [{
|
|
170
|
+
...settings.experiments[0],
|
|
171
|
+
currency: "ZZZ",
|
|
172
|
+
variants: {
|
|
173
|
+
treatment: [{
|
|
174
|
+
variant_gid: "gid://shopify/ProductVariant/123",
|
|
175
|
+
fixedPricePerUnit: "9.00",
|
|
176
|
+
}],
|
|
177
|
+
},
|
|
178
|
+
}],
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
applyCommercePrice(unknownSettings, assignments);
|
|
182
|
+
|
|
183
|
+
expect(document.querySelector(".price")?.textContent).toBe("ZZ 10.00");
|
|
184
|
+
expect(isCommercePriceActive(unknownSettings, assignments)).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
|
|
90
187
|
it("keeps the published price when the active plane does not authorize the assignment", () => {
|
|
91
188
|
document.body.innerHTML = '<div class="price">€1.299,00</div>';
|
|
92
189
|
const authorizeNone = () => false;
|