@drip-apex/sdk 0.1.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.
Files changed (227) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -0
  3. package/dist/_headers +3 -0
  4. package/dist/bundle-report.json +564 -0
  5. package/dist/drip.debug.js +2 -0
  6. package/dist/drip.debug.js.map +1 -0
  7. package/dist/drip.heatmap.js +2 -0
  8. package/dist/drip.heatmap.js.map +1 -0
  9. package/dist/drip.js +2 -0
  10. package/dist/drip.js.map +1 -0
  11. package/dist/drip.live.js +2 -0
  12. package/dist/drip.live.js.map +1 -0
  13. package/dist/editor.js +480 -0
  14. package/dist/editor.js.map +7 -0
  15. package/dist/index.mjs +645 -0
  16. package/dist/index.mjs.map +7 -0
  17. package/dist/metafiles/drip.debug.meta.json +973 -0
  18. package/dist/metafiles/drip.heatmap.meta.json +918 -0
  19. package/dist/metafiles/drip.live.meta.json +918 -0
  20. package/dist/metafiles/drip.meta.json +973 -0
  21. package/dist/metafiles/editor.meta.json +321 -0
  22. package/dist/metafiles/index.meta.json +1039 -0
  23. package/dist/metafiles/index.mjs.meta.json +78 -0
  24. package/dist/react/index.js +16973 -0
  25. package/dist/react/index.js.map +7 -0
  26. package/dist/sri.json +27 -0
  27. package/dist/types/analytics.d.ts +15 -0
  28. package/dist/types/antiflicker.d.ts +14 -0
  29. package/dist/types/assignment-store.d.ts +30 -0
  30. package/dist/types/audit.d.ts +9 -0
  31. package/dist/types/bucket.d.ts +2 -0
  32. package/dist/types/bucketing-cleanup.d.ts +5 -0
  33. package/dist/types/commerce-gift.d.ts +41 -0
  34. package/dist/types/commerce-price.d.ts +22 -0
  35. package/dist/types/commerce-shipping.d.ts +28 -0
  36. package/dist/types/css.d.ts +5 -0
  37. package/dist/types/cwv.d.ts +10 -0
  38. package/dist/types/data-layer-normalize.d.ts +1 -0
  39. package/dist/types/debug.d.ts +23 -0
  40. package/dist/types/define.d.ts +87 -0
  41. package/dist/types/devtools.d.ts +32 -0
  42. package/dist/types/dom-trigger.d.ts +3 -0
  43. package/dist/types/editor/api.d.ts +37 -0
  44. package/dist/types/editor/artifacts.d.ts +15 -0
  45. package/dist/types/editor/command-runtime.d.ts +48 -0
  46. package/dist/types/editor/controller.d.ts +97 -0
  47. package/dist/types/editor/device-preview.d.ts +23 -0
  48. package/dist/types/editor/index.d.ts +9 -0
  49. package/dist/types/editor/operator.d.ts +39 -0
  50. package/dist/types/editor/picker.d.ts +23 -0
  51. package/dist/types/editor/selector.d.ts +2 -0
  52. package/dist/types/editor/types.d.ts +55 -0
  53. package/dist/types/editor/view-kit.d.ts +33 -0
  54. package/dist/types/editor/view.d.ts +5 -0
  55. package/dist/types/editor-bootstrap.d.ts +37 -0
  56. package/dist/types/exclusion-groups.d.ts +59 -0
  57. package/dist/types/exposure-signals.d.ts +7 -0
  58. package/dist/types/flags.d.ts +65 -0
  59. package/dist/types/goals.d.ts +16 -0
  60. package/dist/types/guards.d.ts +39 -0
  61. package/dist/types/handover.d.ts +10 -0
  62. package/dist/types/hash.d.ts +19 -0
  63. package/dist/types/heatmap.d.ts +8 -0
  64. package/dist/types/hideflicker.d.ts +15 -0
  65. package/dist/types/hydration.d.ts +11 -0
  66. package/dist/types/index.d.ts +144 -0
  67. package/dist/types/js.d.ts +6 -0
  68. package/dist/types/module.d.ts +4 -0
  69. package/dist/types/money.d.ts +14 -0
  70. package/dist/types/mutate.d.ts +34 -0
  71. package/dist/types/observer-mute.d.ts +3 -0
  72. package/dist/types/pii-keys.d.ts +1 -0
  73. package/dist/types/prerequisites.d.ts +12 -0
  74. package/dist/types/qa-assistant-state.d.ts +68 -0
  75. package/dist/types/qa-assistant-view.d.ts +6 -0
  76. package/dist/types/qa-assistant.d.ts +9 -0
  77. package/dist/types/qa-toolbar.d.ts +19 -0
  78. package/dist/types/react/index.d.ts +84 -0
  79. package/dist/types/reassert-sentinel.d.ts +6 -0
  80. package/dist/types/router.d.ts +9 -0
  81. package/dist/types/runtime-tools.d.ts +50 -0
  82. package/dist/types/runtime.d.ts +22 -0
  83. package/dist/types/safe-regex.d.ts +6 -0
  84. package/dist/types/sanitize.d.ts +5 -0
  85. package/dist/types/semantic.d.ts +7 -0
  86. package/dist/types/shopify-cart-events.d.ts +7 -0
  87. package/dist/types/shopify-cart.d.ts +62 -0
  88. package/dist/types/signals.d.ts +31 -0
  89. package/dist/types/surface-signals.d.ts +6 -0
  90. package/dist/types/targeting-diagnostics.d.ts +5 -0
  91. package/dist/types/targeting.d.ts +50 -0
  92. package/dist/types/test-fixtures/commerce.d.ts +12 -0
  93. package/dist/types/track.d.ts +79 -0
  94. package/dist/types/types.d.ts +859 -0
  95. package/dist/types/wait.d.ts +7 -0
  96. package/package.json +74 -0
  97. package/src/analytics.ts +146 -0
  98. package/src/antiflicker.test.ts +127 -0
  99. package/src/antiflicker.ts +147 -0
  100. package/src/assignment-store.test.ts +171 -0
  101. package/src/assignment-store.ts +208 -0
  102. package/src/audit.test.ts +17 -0
  103. package/src/audit.ts +362 -0
  104. package/src/bucket.test.ts +65 -0
  105. package/src/bucket.ts +18 -0
  106. package/src/bucketing-cleanup.test.ts +30 -0
  107. package/src/bucketing-cleanup.ts +72 -0
  108. package/src/bucketing-parity.test.ts +41 -0
  109. package/src/commerce-analytics-lifecycle.test.ts +1416 -0
  110. package/src/commerce-config-lifecycle.test.ts +274 -0
  111. package/src/commerce-epoch-lifecycle.test.ts +62 -0
  112. package/src/commerce-gift.test.ts +1293 -0
  113. package/src/commerce-gift.ts +979 -0
  114. package/src/commerce-offers-display.test.ts +154 -0
  115. package/src/commerce-price.test.ts +1145 -0
  116. package/src/commerce-price.ts +941 -0
  117. package/src/commerce-revocation-lifecycle.test.ts +78 -0
  118. package/src/commerce-shipping.test.ts +630 -0
  119. package/src/commerce-shipping.ts +447 -0
  120. package/src/config-fetch.test.ts +325 -0
  121. package/src/css.ts +54 -0
  122. package/src/cwv.test.ts +95 -0
  123. package/src/cwv.ts +129 -0
  124. package/src/data-layer-normalize.ts +21 -0
  125. package/src/debug.test.ts +83 -0
  126. package/src/debug.ts +138 -0
  127. package/src/define.test.ts +327 -0
  128. package/src/define.ts +208 -0
  129. package/src/devtools.ts +582 -0
  130. package/src/dom-trigger.test.ts +70 -0
  131. package/src/dom-trigger.ts +55 -0
  132. package/src/editor/api.test.ts +154 -0
  133. package/src/editor/api.ts +185 -0
  134. package/src/editor/artifacts.test.ts +57 -0
  135. package/src/editor/artifacts.ts +115 -0
  136. package/src/editor/command-runtime.test.ts +225 -0
  137. package/src/editor/command-runtime.ts +572 -0
  138. package/src/editor/controller.test.ts +371 -0
  139. package/src/editor/controller.ts +596 -0
  140. package/src/editor/device-preview.test.ts +60 -0
  141. package/src/editor/device-preview.ts +120 -0
  142. package/src/editor/index.test.ts +161 -0
  143. package/src/editor/index.ts +169 -0
  144. package/src/editor/operator.test.ts +148 -0
  145. package/src/editor/operator.ts +256 -0
  146. package/src/editor/picker.test.ts +58 -0
  147. package/src/editor/picker.ts +152 -0
  148. package/src/editor/selector.test.ts +44 -0
  149. package/src/editor/selector.ts +110 -0
  150. package/src/editor/types.ts +92 -0
  151. package/src/editor/view-kit.ts +162 -0
  152. package/src/editor/view.test.ts +535 -0
  153. package/src/editor/view.ts +1395 -0
  154. package/src/editor-bootstrap.test.ts +116 -0
  155. package/src/editor-bootstrap.ts +213 -0
  156. package/src/event-schema-contract.test.ts +35 -0
  157. package/src/exclusion-groups.test.ts +383 -0
  158. package/src/exclusion-groups.ts +270 -0
  159. package/src/exposure-signals.test.ts +182 -0
  160. package/src/exposure-signals.ts +110 -0
  161. package/src/flags.test.ts +322 -0
  162. package/src/flags.ts +201 -0
  163. package/src/goals.test.ts +42 -0
  164. package/src/goals.ts +200 -0
  165. package/src/guards.test.ts +249 -0
  166. package/src/guards.ts +250 -0
  167. package/src/handover-cache-order.test.ts +32 -0
  168. package/src/handover.test.ts +349 -0
  169. package/src/handover.ts +309 -0
  170. package/src/hash.ts +214 -0
  171. package/src/heatmap.test.ts +305 -0
  172. package/src/heatmap.ts +373 -0
  173. package/src/hideflicker.ts +21 -0
  174. package/src/hydration.test.ts +252 -0
  175. package/src/hydration.ts +203 -0
  176. package/src/index.ts +7513 -0
  177. package/src/init-empty.test.ts +5572 -0
  178. package/src/js.test.ts +26 -0
  179. package/src/js.ts +61 -0
  180. package/src/module.ts +50 -0
  181. package/src/money.test.ts +35 -0
  182. package/src/money.ts +53 -0
  183. package/src/mutate.test.ts +304 -0
  184. package/src/mutate.ts +469 -0
  185. package/src/observer-mute.test.ts +88 -0
  186. package/src/observer-mute.ts +45 -0
  187. package/src/page-trigger.test.ts +84 -0
  188. package/src/personalization.test.ts +68 -0
  189. package/src/pii-keys.ts +17 -0
  190. package/src/prerequisites.test.ts +39 -0
  191. package/src/prerequisites.ts +94 -0
  192. package/src/qa-assistant-state.test.ts +471 -0
  193. package/src/qa-assistant-state.ts +505 -0
  194. package/src/qa-assistant-view.test.ts +185 -0
  195. package/src/qa-assistant-view.ts +768 -0
  196. package/src/qa-assistant.ts +40 -0
  197. package/src/qa-toolbar.ts +444 -0
  198. package/src/react/index.test.tsx +177 -0
  199. package/src/react/index.tsx +252 -0
  200. package/src/reassert-sentinel.test.ts +314 -0
  201. package/src/reassert-sentinel.ts +191 -0
  202. package/src/router.test.ts +78 -0
  203. package/src/router.ts +92 -0
  204. package/src/runtime-tools.test.ts +112 -0
  205. package/src/runtime-tools.ts +403 -0
  206. package/src/runtime.test.ts +217 -0
  207. package/src/runtime.ts +268 -0
  208. package/src/safe-regex.ts +35 -0
  209. package/src/sanitize.ts +34 -0
  210. package/src/semantic.ts +245 -0
  211. package/src/shopify-cart-events.test.ts +76 -0
  212. package/src/shopify-cart-events.ts +115 -0
  213. package/src/shopify-cart.test.ts +2823 -0
  214. package/src/shopify-cart.ts +1256 -0
  215. package/src/signals.ts +226 -0
  216. package/src/surface-signals.test.ts +83 -0
  217. package/src/surface-signals.ts +165 -0
  218. package/src/targeting-diagnostics.ts +560 -0
  219. package/src/targeting.detailed.test.ts +443 -0
  220. package/src/targeting.test.ts +1054 -0
  221. package/src/targeting.ts +1024 -0
  222. package/src/test-fixtures/commerce.ts +65 -0
  223. package/src/track.test.ts +817 -0
  224. package/src/track.ts +519 -0
  225. package/src/types.ts +991 -0
  226. package/src/wait.test.ts +96 -0
  227. package/src/wait.ts +238 -0
@@ -0,0 +1,274 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import {
4
+ commerceAssignmentResponseFixture,
5
+ commercePriceFixture as commerce,
6
+ installCommerceProductPage,
7
+ priceExperimentsFixture as experiments,
8
+ } from "./test-fixtures/commerce";
9
+
10
+ function makeStorage(): Storage {
11
+ const values = new Map<string, string>();
12
+ return {
13
+ get length() { return values.size; },
14
+ clear: () => values.clear(),
15
+ getItem: (key) => values.get(key) ?? null,
16
+ key: (index) => Array.from(values.keys())[index] ?? null,
17
+ removeItem: (key) => { values.delete(key); },
18
+ setItem: (key, value) => { values.set(key, value); },
19
+ };
20
+ }
21
+
22
+ async function waitForPrice(expected: string) {
23
+ for (let attempt = 0; attempt < 500; attempt += 1) {
24
+ if (document.querySelector(".price")?.textContent === expected) return;
25
+ await new Promise((resolve) => setTimeout(resolve, 1));
26
+ }
27
+ expect(document.querySelector(".price")?.textContent).toBe(expected);
28
+ }
29
+
30
+ describe("commerce config lifecycle", () => {
31
+ const originalFetch = globalThis.fetch;
32
+
33
+ beforeEach(() => {
34
+ vi.resetModules();
35
+ delete (globalThis as typeof globalThis & { __drip?: unknown }).__drip;
36
+ Object.defineProperty(globalThis, "localStorage", {
37
+ configurable: true,
38
+ value: makeStorage(),
39
+ });
40
+ Object.defineProperty(globalThis, "sessionStorage", {
41
+ configurable: true,
42
+ value: makeStorage(),
43
+ });
44
+ installCommerceProductPage();
45
+ globalThis.fetch = vi.fn(async (input: RequestInfo | URL) => {
46
+ const url = String(input);
47
+ if (url.includes("/config")) {
48
+ return new Response(JSON.stringify({ experiments }), { status: 200 });
49
+ }
50
+ if (url.includes("/v1/assign")) {
51
+ return new Response(JSON.stringify(commerceAssignmentResponseFixture), { status: 200 });
52
+ }
53
+ return new Response("ok", { status: 202 });
54
+ }) as typeof fetch;
55
+ });
56
+
57
+ afterEach(() => {
58
+ vi.restoreAllMocks();
59
+ delete (globalThis as typeof globalThis & { __drip?: unknown }).__drip;
60
+ globalThis.fetch = originalFetch;
61
+ delete (window as Window & { Shopify?: unknown }).Shopify;
62
+ delete (window as Window & { ShopifyAnalytics?: unknown }).ShopifyAnalytics;
63
+ document.head.innerHTML = "";
64
+ document.body.innerHTML = "";
65
+ });
66
+
67
+ it("tears down a cached price plane before an authoritative removal can write stale cart state", async () => {
68
+ const hash = JSON.stringify({
69
+ experiments,
70
+ profile: null,
71
+ runtime: null,
72
+ signals: [],
73
+ commerce,
74
+ });
75
+ localStorage.setItem("drip_cfg:shop_123:production", JSON.stringify({
76
+ fetchedAt: Date.now(),
77
+ hash,
78
+ experiments,
79
+ commerce,
80
+ }));
81
+ let resolveConfig!: (response: Response) => void;
82
+ let resolveStaleAuthorization!: (response: Response) => void;
83
+ let authorizationCalls = 0;
84
+ const fetchMock = vi.fn((input: RequestInfo | URL) => {
85
+ const url = String(input);
86
+ if (url.includes("/config")) {
87
+ return new Promise<Response>((resolve) => { resolveConfig = resolve; });
88
+ }
89
+ if (url.endsWith("/cart.js")) {
90
+ return Promise.resolve(Response.json({ token: "cart-runtime?key=runtime-secret" }));
91
+ }
92
+ if (url.includes("/v1/assign")) {
93
+ authorizationCalls += 1;
94
+ if (authorizationCalls === 1) {
95
+ return Promise.resolve(Response.json(commerceAssignmentResponseFixture));
96
+ }
97
+ return new Promise<Response>((resolve) => { resolveStaleAuthorization = resolve; });
98
+ }
99
+ return Promise.resolve(new Response("ok", { status: 200 }));
100
+ });
101
+ globalThis.fetch = fetchMock as typeof fetch;
102
+ const disconnect = vi.spyOn(MutationObserver.prototype, "disconnect");
103
+ const { getDiagnostics, init } = await import("./index");
104
+
105
+ init({
106
+ antiFlickerMode: "off",
107
+ endpoint: "https://events.test",
108
+ ingestSignature: "ingest-token",
109
+ lateConfigPolicy: "apply",
110
+ shopId: "shop_123",
111
+ });
112
+ await vi.waitFor(() => {
113
+ expect(document.querySelector(".price")?.textContent).toBe("€1.149,95");
114
+ expect(getDiagnostics().spa.lastShopifyCartSync?.status).toBe("sent");
115
+ });
116
+
117
+ const cartWritesBeforeRemoval = fetchMock.mock.calls.filter(
118
+ ([url]) => url === "/cart/update.js",
119
+ ).length;
120
+ await window.fetch("/cart/add.js", { method: "POST" });
121
+ await vi.waitFor(() => {
122
+ expect(authorizationCalls).toBe(2);
123
+ expect(getDiagnostics().spa.lastShopifyCartSync?.status).toBe("pending");
124
+ });
125
+
126
+ resolveConfig(Response.json({ experiments }));
127
+
128
+ await vi.waitFor(
129
+ () => expect(getDiagnostics().configSource).toBe("network"),
130
+ { timeout: 5_000, interval: 10 },
131
+ );
132
+ await waitForPrice("€1.299,00");
133
+ expect(disconnect).toHaveBeenCalled();
134
+
135
+ resolveStaleAuthorization(Response.json(commerceAssignmentResponseFixture));
136
+ await vi.waitFor(() => {
137
+ expect(fetchMock.mock.calls.filter(
138
+ ([url]) => url === "/cart/update.js",
139
+ )).toHaveLength(cartWritesBeforeRemoval);
140
+ });
141
+ const cached = JSON.parse(localStorage.getItem("drip_cfg:shop_123:production") ?? "{}");
142
+ expect(cached).not.toHaveProperty("commerce");
143
+ expect(cached.hash).toContain('"commerce":null');
144
+ });
145
+
146
+ it("keeps the no-state, no-plane lazy path free of price cleanup", async () => {
147
+ const commercePrice = await import("./commerce-price");
148
+ const cleanup = vi.spyOn(commercePrice, "cleanupCommercePrice");
149
+ globalThis.fetch = vi.fn(async (input: RequestInfo | URL) =>
150
+ String(input).includes("/config")
151
+ ? Response.json({ experiments: [] })
152
+ : new Response("ok", { status: 202 })) as typeof fetch;
153
+ const { getDiagnostics, init } = await import("./index");
154
+
155
+ init({
156
+ antiFlickerMode: "off",
157
+ endpoint: "https://events.test",
158
+ ingestSignature: "ingest-token",
159
+ lateConfigPolicy: "apply",
160
+ shopId: "shop_123",
161
+ });
162
+
163
+ await vi.waitFor(() => expect(getDiagnostics().configSource).toBe("network"));
164
+ expect(cleanup).not.toHaveBeenCalled();
165
+ });
166
+
167
+ it("keeps an active cached price plane applied when the authoritative plane remains", async () => {
168
+ const hash = JSON.stringify({
169
+ experiments,
170
+ profile: null,
171
+ runtime: null,
172
+ signals: [],
173
+ commerce,
174
+ });
175
+ localStorage.setItem("drip_cfg:shop_123:production", JSON.stringify({
176
+ fetchedAt: Date.now(),
177
+ hash,
178
+ experiments,
179
+ commerce,
180
+ }));
181
+ globalThis.fetch = vi.fn(async (input: RequestInfo | URL) => {
182
+ const url = String(input);
183
+ if (url.includes("/config")) {
184
+ return Response.json({
185
+ experiments,
186
+ commerce,
187
+ profile: { platform: "shopify", version: 1 },
188
+ });
189
+ }
190
+ if (url.endsWith("/cart.js")) {
191
+ return Response.json({ token: "cart-runtime?key=runtime-secret" });
192
+ }
193
+ if (url.includes("/v1/assign")) {
194
+ return Response.json(commerceAssignmentResponseFixture);
195
+ }
196
+ return new Response("ok", { status: 200 });
197
+ }) as typeof fetch;
198
+ const { getDiagnostics, init } = await import("./index");
199
+
200
+ init({
201
+ antiFlickerMode: "off",
202
+ endpoint: "https://events.test",
203
+ ingestSignature: "ingest-token",
204
+ lateConfigPolicy: "apply",
205
+ shopId: "shop_123",
206
+ });
207
+
208
+ await vi.waitFor(() => {
209
+ expect(getDiagnostics().configSource).toBe("network");
210
+ expect(document.querySelector(".price")?.textContent).toBe("€1.149,95");
211
+ });
212
+ });
213
+
214
+ it("clears a stale shipping authorization after the Worker confirms the assignment disappeared", async () => {
215
+ sessionStorage.setItem("drip_cart_attr", JSON.stringify({
216
+ assignments: [["exp_shipping", "treatment", 6]],
217
+ requiredAssignments: [["exp_shipping", "treatment", 6]],
218
+ visitorId: "visitor_123",
219
+ sessionId: "session_old",
220
+ shippingAuthorizationRevision: 6,
221
+ priceAuthorizationRevision: null,
222
+ }));
223
+ const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
224
+ const url = String(input);
225
+ if (url.includes("/v1/assign")) {
226
+ return Response.json({ assignments: [] });
227
+ }
228
+ return new Response("ok", { status: 200 });
229
+ });
230
+ globalThis.fetch = fetchMock as typeof fetch;
231
+ const { init } = await import("./index");
232
+
233
+ init({
234
+ antiFlickerMode: "off",
235
+ commerce: {
236
+ shipping: {
237
+ authorization: {
238
+ version: 1,
239
+ revision: 7,
240
+ checksum: "a".repeat(64),
241
+ experimentIds: ["exp_shipping"],
242
+ },
243
+ experiments: [],
244
+ },
245
+ },
246
+ endpoint: "https://events.test",
247
+ experiments: [],
248
+ ingestSignature: "ingest-token",
249
+ runtimeProfile: "full",
250
+ shopId: "shop_123",
251
+ trackingStart: "immediate",
252
+ userId: "visitor_123",
253
+ });
254
+
255
+ await vi.waitFor(() =>
256
+ expect(fetchMock.mock.calls.map(([url]) => String(url))).toContain("/cart/update.js"),
257
+ );
258
+ const authorizationRequest = fetchMock.mock.calls.find(([url]) =>
259
+ String(url).includes("/v1/assign")
260
+ );
261
+ expect(authorizationRequest).toBeDefined();
262
+ expect(JSON.parse(String(authorizationRequest?.[1]?.body))).toMatchObject({
263
+ experiments: [],
264
+ });
265
+ const cartUpdate = fetchMock.mock.calls.find(([url]) => url === "/cart/update.js");
266
+ expect(JSON.parse(String(cartUpdate?.[1]?.body)).attributes).toMatchObject({
267
+ apex_assignments: "[]",
268
+ apex_assignment_authorization: "",
269
+ apex_experiment_id: "",
270
+ apex_variation_id: "",
271
+ apex_assignment_epoch: "",
272
+ });
273
+ });
274
+ });
@@ -0,0 +1,62 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import {
4
+ commercePriceFixture as commerce,
5
+ installCommerceProductPage,
6
+ priceExperimentsFixture as experiments,
7
+ } from "./test-fixtures/commerce";
8
+
9
+ describe("commerce assignment epoch lifecycle", () => {
10
+ const originalFetch = globalThis.fetch;
11
+ let fetchMock: ReturnType<typeof vi.fn>;
12
+
13
+ beforeEach(() => {
14
+ vi.resetModules();
15
+ installCommerceProductPage();
16
+ fetchMock = vi.fn(async (input: RequestInfo | URL) =>
17
+ String(input).endsWith("/cart.js")
18
+ ? Response.json({ token: "cart-epoch?key=epoch-secret" })
19
+ : String(input).includes("/v1/assign")
20
+ ? new Response(JSON.stringify({
21
+ assignments: [{
22
+ experiment_id: "exp_price",
23
+ variation_id: "treatment",
24
+ assignment_epoch: 2,
25
+ }],
26
+ commerce_authorization: "signed",
27
+ }), { status: 200 })
28
+ : new Response("ok", { status: 202 }));
29
+ globalThis.fetch = fetchMock as typeof fetch;
30
+ });
31
+
32
+ afterEach(() => {
33
+ vi.restoreAllMocks();
34
+ globalThis.fetch = originalFetch;
35
+ delete (window as Window & { Shopify?: unknown }).Shopify;
36
+ delete (window as Window & { ShopifyAnalytics?: unknown }).ShopifyAnalytics;
37
+ document.head.innerHTML = "";
38
+ document.body.innerHTML = "";
39
+ });
40
+
41
+ it("keeps the published price when the Worker rejects a stale assignment epoch", async () => {
42
+ const { init } = await import("./index");
43
+
44
+ init({
45
+ antiFlickerMode: "off",
46
+ commerce,
47
+ endpoint: "https://events.test",
48
+ experiments,
49
+ ingestSignature: "ingest-token",
50
+ shopId: "shop_123",
51
+ trackingStart: "immediate",
52
+ });
53
+
54
+ await vi.waitFor(() =>
55
+ expect(fetchMock.mock.calls.some(([url]) => String(url).includes("/v1/assign")))
56
+ .toBe(true));
57
+ await new Promise((resolve) => setTimeout(resolve, 50));
58
+ const cartWrite = fetchMock.mock.calls.find(([url]) => url === "/cart/update.js");
59
+ expect(cartWrite).toBeUndefined();
60
+ expect(document.querySelector(".price")?.textContent).toBe("€1.299,00");
61
+ });
62
+ });