@drip-apex/sdk 0.1.0 → 0.3.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 +19283 -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 +19267 -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 +16636 -13533
- 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 +28 -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 +2071 -0
- package/src/consent-evaluation-borlabs.test.ts +41 -0
- package/src/consent-evaluation-cookiescript.test.ts +50 -0
- package/src/consent-evaluation.ts +151 -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
|
@@ -0,0 +1,1091 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
declare global {
|
|
5
|
+
// eslint-disable-next-line no-var
|
|
6
|
+
var __dripS4Js: number | undefined;
|
|
7
|
+
// eslint-disable-next-line no-var
|
|
8
|
+
var __dripF2Reset: number | undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("artifact concurrency", () => {
|
|
12
|
+
const originalFetch = globalThis.fetch;
|
|
13
|
+
const originalPushState = history.pushState;
|
|
14
|
+
const originalReplaceState = history.replaceState;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.resetModules();
|
|
18
|
+
delete (globalThis as any).__drip;
|
|
19
|
+
delete (globalThis as any).drip;
|
|
20
|
+
delete (globalThis as any).Apex;
|
|
21
|
+
delete (globalThis as any).__dripPredecided;
|
|
22
|
+
delete (globalThis as any).__dripPerf;
|
|
23
|
+
delete (globalThis as any).__dripS4Js;
|
|
24
|
+
delete (globalThis as any).__dripF2Reset;
|
|
25
|
+
delete (globalThis as any).__dripF1DeactivatingResets;
|
|
26
|
+
delete (globalThis as any).__dripF1PersistentResets;
|
|
27
|
+
delete (globalThis as any).__dripF1PersistentRuns;
|
|
28
|
+
delete (globalThis as any).__dripF11Resets;
|
|
29
|
+
delete (globalThis as any).__dripF11Runs;
|
|
30
|
+
delete (globalThis as any).__dripD4SharedResets;
|
|
31
|
+
delete (globalThis as any).__dripD4SharedRuns;
|
|
32
|
+
delete (globalThis as any).__dripD4SoloResets;
|
|
33
|
+
delete (globalThis as any).__dripD4SoloRuns;
|
|
34
|
+
delete (globalThis as any).__dripD4VariationResets;
|
|
35
|
+
delete (globalThis as any).__dripD4VariationRuns;
|
|
36
|
+
delete (globalThis as any).__dripD4DeactivateSharedResets;
|
|
37
|
+
delete (globalThis as any).__dripD4DeactivateSharedRuns;
|
|
38
|
+
delete (globalThis as any).__dripF14ResetSideEffect;
|
|
39
|
+
delete (globalThis as any).__dripD5PersistentRuns;
|
|
40
|
+
delete (globalThis as any).__dripD5CachedRuns;
|
|
41
|
+
delete (globalThis as any).__dripD5CachedResets;
|
|
42
|
+
delete (globalThis as any).__dripD5NetworkRuns;
|
|
43
|
+
delete (globalThis as any).__dripVanishedDeactivateResets;
|
|
44
|
+
delete (globalThis as any).__dripVanishedResetResets;
|
|
45
|
+
delete (globalThis as any).__dripVanishedResetRuns;
|
|
46
|
+
localStorage.clear();
|
|
47
|
+
sessionStorage.clear();
|
|
48
|
+
document.head.innerHTML = "";
|
|
49
|
+
document.body.innerHTML = "";
|
|
50
|
+
history.pushState = originalPushState;
|
|
51
|
+
history.replaceState = originalReplaceState;
|
|
52
|
+
history.replaceState({}, "", "/products/widget");
|
|
53
|
+
globalThis.fetch = vi.fn(async () =>
|
|
54
|
+
new Response("ok", { status: 202 })) as typeof fetch;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
vi.clearAllTimers();
|
|
59
|
+
vi.useRealTimers();
|
|
60
|
+
vi.restoreAllMocks();
|
|
61
|
+
globalThis.fetch = originalFetch;
|
|
62
|
+
delete (globalThis as any).__drip;
|
|
63
|
+
delete (globalThis as any).drip;
|
|
64
|
+
delete (globalThis as any).Apex;
|
|
65
|
+
delete (globalThis as any).__dripPredecided;
|
|
66
|
+
delete (globalThis as any).__dripS4Js;
|
|
67
|
+
delete (globalThis as any).__dripF2Reset;
|
|
68
|
+
delete (globalThis as any).__dripF1DeactivatingResets;
|
|
69
|
+
delete (globalThis as any).__dripF1PersistentResets;
|
|
70
|
+
delete (globalThis as any).__dripF1PersistentRuns;
|
|
71
|
+
delete (globalThis as any).__dripF11Resets;
|
|
72
|
+
delete (globalThis as any).__dripF11Runs;
|
|
73
|
+
delete (globalThis as any).__dripD4SharedResets;
|
|
74
|
+
delete (globalThis as any).__dripD4SharedRuns;
|
|
75
|
+
delete (globalThis as any).__dripD4SoloResets;
|
|
76
|
+
delete (globalThis as any).__dripD4SoloRuns;
|
|
77
|
+
delete (globalThis as any).__dripD4VariationResets;
|
|
78
|
+
delete (globalThis as any).__dripD4VariationRuns;
|
|
79
|
+
delete (globalThis as any).__dripD4DeactivateSharedResets;
|
|
80
|
+
delete (globalThis as any).__dripD4DeactivateSharedRuns;
|
|
81
|
+
delete (globalThis as any).__dripF14ResetSideEffect;
|
|
82
|
+
delete (globalThis as any).__dripD5PersistentRuns;
|
|
83
|
+
delete (globalThis as any).__dripD5CachedRuns;
|
|
84
|
+
delete (globalThis as any).__dripD5CachedResets;
|
|
85
|
+
delete (globalThis as any).__dripD5NetworkRuns;
|
|
86
|
+
delete (globalThis as any).__dripVanishedDeactivateResets;
|
|
87
|
+
delete (globalThis as any).__dripVanishedResetResets;
|
|
88
|
+
delete (globalThis as any).__dripVanishedResetRuns;
|
|
89
|
+
document.head.innerHTML = "";
|
|
90
|
+
document.body.innerHTML = "";
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// S4
|
|
94
|
+
it("keeps another page's pending external-CSS continuation alive through a page-scoped reapply", async () => {
|
|
95
|
+
document.body.innerHTML = '<h1 class="headline">Original</h1>';
|
|
96
|
+
const { init, reInit } = await import("./index");
|
|
97
|
+
|
|
98
|
+
init({
|
|
99
|
+
antiFlickerMode: "off",
|
|
100
|
+
endpoint: "https://events.test",
|
|
101
|
+
experiments: [
|
|
102
|
+
{
|
|
103
|
+
id: "exp_pending",
|
|
104
|
+
status: "running",
|
|
105
|
+
trafficAllocation: 1,
|
|
106
|
+
targeting: {
|
|
107
|
+
pageRules: [{
|
|
108
|
+
id: "page_pending",
|
|
109
|
+
includeRules: [{ type: "simple", pattern: "/products/*" }],
|
|
110
|
+
}],
|
|
111
|
+
},
|
|
112
|
+
variations: [{
|
|
113
|
+
id: "var_pending",
|
|
114
|
+
weight: 1,
|
|
115
|
+
mutations: [],
|
|
116
|
+
externalCssUrl: "https://cdn.test/pending.css",
|
|
117
|
+
js: "globalThis.__dripS4Js = (globalThis.__dripS4Js ?? 0) + 1;",
|
|
118
|
+
}],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "exp_forced",
|
|
122
|
+
status: "running",
|
|
123
|
+
trafficAllocation: 1,
|
|
124
|
+
targeting: {
|
|
125
|
+
pageRules: [{
|
|
126
|
+
id: "page_forced",
|
|
127
|
+
includeRules: [{ type: "simple", pattern: "/products/*" }],
|
|
128
|
+
trigger: { deactivationMode: "force-reset" },
|
|
129
|
+
}],
|
|
130
|
+
},
|
|
131
|
+
variations: [{
|
|
132
|
+
id: "var_forced",
|
|
133
|
+
weight: 1,
|
|
134
|
+
mutations: [{ selector: ".headline", action: "text", text: "Forced" }],
|
|
135
|
+
}],
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
shopId: "shop_123",
|
|
139
|
+
trackingStart: "immediate",
|
|
140
|
+
userId: "visitor_pending",
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const link = document.getElementById("drip-css-link-exp_pending-var_pending");
|
|
144
|
+
expect(link).not.toBeNull();
|
|
145
|
+
expect(globalThis.__dripS4Js).toBeUndefined();
|
|
146
|
+
|
|
147
|
+
// A force-reset reapply of the sibling page must not invalidate the
|
|
148
|
+
// continuation waiting on this page's external CSS.
|
|
149
|
+
reInit();
|
|
150
|
+
expect(document.querySelector(".headline")?.textContent).toBe("Forced");
|
|
151
|
+
|
|
152
|
+
link!.dispatchEvent(new Event("load"));
|
|
153
|
+
await vi.waitFor(() => expect(globalThis.__dripS4Js).toBe(1));
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// F3
|
|
157
|
+
it("stops the early-reassert watcher from re-applying a deactivated page's mutation", async () => {
|
|
158
|
+
document.body.innerHTML = '<h1 id="hero">Original</h1>';
|
|
159
|
+
(globalThis as any).__dripPredecided = {
|
|
160
|
+
decision: [["exp_early", "var_early"]],
|
|
161
|
+
configRevision: 42,
|
|
162
|
+
visitorId: "visitor_early",
|
|
163
|
+
groups: {},
|
|
164
|
+
};
|
|
165
|
+
const earlyReassert = await import("./early-reassert");
|
|
166
|
+
const installSpy = vi.spyOn(earlyReassert, "installEarlyReassert");
|
|
167
|
+
const { init } = await import("./index");
|
|
168
|
+
|
|
169
|
+
init({
|
|
170
|
+
antiFlickerMode: "off",
|
|
171
|
+
delivery: { revision: 42 },
|
|
172
|
+
endpoint: "https://events.test",
|
|
173
|
+
experiments: [{
|
|
174
|
+
id: "exp_early",
|
|
175
|
+
status: "running",
|
|
176
|
+
trafficAllocation: 1,
|
|
177
|
+
targeting: {
|
|
178
|
+
pageRules: [{
|
|
179
|
+
id: "page_early",
|
|
180
|
+
includeRules: [{ type: "simple", pattern: "/products/*" }],
|
|
181
|
+
trigger: { deactivationMode: "reset" },
|
|
182
|
+
}],
|
|
183
|
+
},
|
|
184
|
+
variations: [{
|
|
185
|
+
id: "var_early",
|
|
186
|
+
weight: 1,
|
|
187
|
+
mutations: [{ selector: "#hero", action: "text", text: "Treatment" }],
|
|
188
|
+
}],
|
|
189
|
+
}],
|
|
190
|
+
shopId: "shop_123",
|
|
191
|
+
spa: { applyTiming: "early_reassert", hydrationTimeoutMs: 3_000 },
|
|
192
|
+
trackingStart: "immediate",
|
|
193
|
+
userId: "visitor_early",
|
|
194
|
+
});
|
|
195
|
+
expect(document.querySelector("#hero")?.textContent).toBe("Treatment");
|
|
196
|
+
const watcher = installSpy.mock.calls[0]?.[0];
|
|
197
|
+
expect(watcher).toBeDefined();
|
|
198
|
+
|
|
199
|
+
history.pushState({}, "", "/collections/all");
|
|
200
|
+
expect(document.querySelector("#hero")?.textContent).toBe("Original");
|
|
201
|
+
|
|
202
|
+
// A framework write on the treated selector still reaches the watcher.
|
|
203
|
+
// Repairing it would resurrect a treatment whose page is gone — it has no
|
|
204
|
+
// snapshot left and would be unrevertable. Fire the repair exactly as the
|
|
205
|
+
// watcher's own observer callback does.
|
|
206
|
+
document.querySelector("#hero")!.textContent = "Framework";
|
|
207
|
+
watcher!.reapply(watcher!.resolveAssignments());
|
|
208
|
+
expect(document.querySelector("#hero")?.textContent).toBe("Framework");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// F1
|
|
212
|
+
it("deactivates one experiment without resetting or reapplying an unrelated persistent page", async () => {
|
|
213
|
+
document.body.innerHTML = [
|
|
214
|
+
'<div id="deactivating-hero">Control A</div>',
|
|
215
|
+
'<h1 id="persistent-hero">Control B</h1>',
|
|
216
|
+
].join("");
|
|
217
|
+
const { init } = await import("./index");
|
|
218
|
+
|
|
219
|
+
init({
|
|
220
|
+
antiFlickerMode: "off",
|
|
221
|
+
endpoint: "https://events.test",
|
|
222
|
+
experiments: [
|
|
223
|
+
{
|
|
224
|
+
id: "exp_deactivating",
|
|
225
|
+
status: "running",
|
|
226
|
+
trafficAllocation: 1,
|
|
227
|
+
targeting: {
|
|
228
|
+
pageRules: [{
|
|
229
|
+
id: "page_deactivating",
|
|
230
|
+
includeRules: [{ type: "simple", pattern: "/products/*" }],
|
|
231
|
+
trigger: { deactivationMode: "reset" },
|
|
232
|
+
}],
|
|
233
|
+
},
|
|
234
|
+
variations: [{
|
|
235
|
+
id: "var_deactivating",
|
|
236
|
+
weight: 1,
|
|
237
|
+
mutations: [{ selector: "#deactivating-hero", action: "text", text: "Treatment A" }],
|
|
238
|
+
resetJs: "globalThis.__dripF1DeactivatingResets = (globalThis.__dripF1DeactivatingResets ?? 0) + 1;",
|
|
239
|
+
}],
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
id: "exp_persistent",
|
|
243
|
+
status: "running",
|
|
244
|
+
trafficAllocation: 1,
|
|
245
|
+
targeting: {
|
|
246
|
+
pageRules: [{
|
|
247
|
+
id: "page_persistent",
|
|
248
|
+
includeRules: [{ type: "simple", pattern: "/products/*" }],
|
|
249
|
+
trigger: { deactivationMode: "persistent" },
|
|
250
|
+
}],
|
|
251
|
+
},
|
|
252
|
+
variations: [{
|
|
253
|
+
id: "var_persistent",
|
|
254
|
+
weight: 1,
|
|
255
|
+
mutations: [{ selector: "#persistent-hero", action: "text", text: "Persistent" }],
|
|
256
|
+
css: "#persistent-hero{color:purple}",
|
|
257
|
+
js: "globalThis.__dripF1PersistentRuns = (globalThis.__dripF1PersistentRuns ?? 0) + 1;",
|
|
258
|
+
resetJs: "globalThis.__dripF1PersistentResets = (globalThis.__dripF1PersistentResets ?? 0) + 1;",
|
|
259
|
+
}],
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
shopId: "shop_123",
|
|
263
|
+
trackingStart: "immediate",
|
|
264
|
+
userId: "visitor_f1",
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
await vi.waitFor(() => expect((globalThis as any).__dripF1PersistentRuns).toBe(1));
|
|
268
|
+
const style = document.getElementById("drip-css-exp_persistent-var_persistent");
|
|
269
|
+
expect(style).not.toBeNull();
|
|
270
|
+
const removeStyle = vi.spyOn(style!, "remove");
|
|
271
|
+
|
|
272
|
+
history.pushState({}, "", "/collections/all");
|
|
273
|
+
await vi.waitFor(() => expect((globalThis as any).__dripF1DeactivatingResets).toBe(1));
|
|
274
|
+
|
|
275
|
+
expect((globalThis as any).__dripF1PersistentResets).toBeUndefined();
|
|
276
|
+
expect((globalThis as any).__dripF1PersistentRuns).toBe(1);
|
|
277
|
+
expect(document.getElementById("drip-css-exp_persistent-var_persistent")).toBe(style);
|
|
278
|
+
expect(removeStyle).not.toHaveBeenCalled();
|
|
279
|
+
expect(document.getElementById("persistent-hero")?.textContent).toBe("Persistent");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// F11
|
|
283
|
+
it("replays a force-reset artifact on every trigger while a sibling page still owns it", async () => {
|
|
284
|
+
document.body.innerHTML = '<h1 id="force-hero">Control</h1>';
|
|
285
|
+
const { init, reInit } = await import("./index");
|
|
286
|
+
|
|
287
|
+
init({
|
|
288
|
+
antiFlickerMode: "off",
|
|
289
|
+
endpoint: "https://events.test",
|
|
290
|
+
experiments: [{
|
|
291
|
+
id: "exp_force_siblings",
|
|
292
|
+
status: "running",
|
|
293
|
+
trafficAllocation: 1,
|
|
294
|
+
targeting: {
|
|
295
|
+
pageRules: [
|
|
296
|
+
{
|
|
297
|
+
id: "page_force",
|
|
298
|
+
includeRules: [{ type: "simple", pattern: "/products/*" }],
|
|
299
|
+
trigger: { deactivationMode: "force-reset" },
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
id: "page_sibling",
|
|
303
|
+
includeRules: [{ type: "simple", pattern: "/products/*" }],
|
|
304
|
+
trigger: { deactivationMode: "persistent" },
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
},
|
|
308
|
+
variations: [{
|
|
309
|
+
id: "var_force_siblings",
|
|
310
|
+
weight: 1,
|
|
311
|
+
mutations: [{ selector: "#force-hero", action: "text", text: "Treatment" }],
|
|
312
|
+
css: "#force-hero{color:green}",
|
|
313
|
+
js: [
|
|
314
|
+
"globalThis.__dripF11Runs = (globalThis.__dripF11Runs ?? 0) + 1;",
|
|
315
|
+
"document.querySelector('#force-hero')?.setAttribute('data-run', String(globalThis.__dripF11Runs));",
|
|
316
|
+
].join("\n"),
|
|
317
|
+
resetJs: [
|
|
318
|
+
"globalThis.__dripF11Resets = (globalThis.__dripF11Resets ?? 0) + 1;",
|
|
319
|
+
"document.querySelector('#force-hero')?.setAttribute('data-run', 'reset');",
|
|
320
|
+
].join("\n"),
|
|
321
|
+
}],
|
|
322
|
+
}],
|
|
323
|
+
shopId: "shop_123",
|
|
324
|
+
trackingStart: "immediate",
|
|
325
|
+
userId: "visitor_f11",
|
|
326
|
+
});
|
|
327
|
+
await vi.waitFor(() => expect((globalThis as any).__dripF11Runs).toBe(1));
|
|
328
|
+
|
|
329
|
+
reInit();
|
|
330
|
+
await vi.waitFor(() => expect((globalThis as any).__dripF11Runs).toBe(2));
|
|
331
|
+
expect((globalThis as any).__dripF11Resets).toBe(1);
|
|
332
|
+
expect(document.getElementById("force-hero")?.getAttribute("data-run")).toBe("2");
|
|
333
|
+
expect(document.getElementById("force-hero")?.textContent).toBe("Treatment");
|
|
334
|
+
|
|
335
|
+
reInit();
|
|
336
|
+
await vi.waitFor(() => expect((globalThis as any).__dripF11Runs).toBe(3));
|
|
337
|
+
expect((globalThis as any).__dripF11Resets).toBe(2);
|
|
338
|
+
expect(document.getElementById("force-hero")?.getAttribute("data-run")).toBe("3");
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// S9: the old fallback stopped after the first shared experiment. The two
|
|
342
|
+
// independent reset counters prove that the closure includes both.
|
|
343
|
+
it("expands a reset closure across every resetting experiment and reapplies its sibling page", async () => {
|
|
344
|
+
document.body.innerHTML = [
|
|
345
|
+
'<h1 id="d4-shared">Shared control</h1>',
|
|
346
|
+
'<h2 id="d4-solo">Solo control</h2>',
|
|
347
|
+
].join("");
|
|
348
|
+
const { getDiagnostics, init } = await import("./index");
|
|
349
|
+
|
|
350
|
+
init({
|
|
351
|
+
antiFlickerMode: "off",
|
|
352
|
+
endpoint: "https://events.test",
|
|
353
|
+
experiments: [
|
|
354
|
+
{
|
|
355
|
+
id: "exp_d4_shared",
|
|
356
|
+
status: "running",
|
|
357
|
+
trafficAllocation: 1,
|
|
358
|
+
resetJs: [
|
|
359
|
+
"globalThis.__dripD4SharedResets = (globalThis.__dripD4SharedResets ?? 0) + 1;",
|
|
360
|
+
"document.querySelector('#d4-shared')?.setAttribute('data-state', 'reset');",
|
|
361
|
+
].join("\n"),
|
|
362
|
+
targeting: {
|
|
363
|
+
pageRules: [
|
|
364
|
+
{
|
|
365
|
+
id: "page_d4_trigger",
|
|
366
|
+
includeRules: [{ type: "simple", pattern: "/products/*" }],
|
|
367
|
+
trigger: { deactivationMode: "reset" },
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
id: "page_d4_survivor",
|
|
371
|
+
includeRules: [{ type: "simple", pattern: "/products/*" }],
|
|
372
|
+
trigger: { deactivationMode: "persistent" },
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
},
|
|
376
|
+
variations: [{
|
|
377
|
+
id: "var_d4_shared",
|
|
378
|
+
weight: 1,
|
|
379
|
+
mutations: [{ selector: "#d4-shared", action: "text", text: "Shared treatment" }],
|
|
380
|
+
js: [
|
|
381
|
+
"globalThis.__dripD4SharedRuns = (globalThis.__dripD4SharedRuns ?? 0) + 1;",
|
|
382
|
+
"document.querySelector('#d4-shared')?.setAttribute('data-run', String(globalThis.__dripD4SharedRuns));",
|
|
383
|
+
].join("\n"),
|
|
384
|
+
}],
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
id: "exp_d4_solo",
|
|
388
|
+
status: "running",
|
|
389
|
+
trafficAllocation: 1,
|
|
390
|
+
resetJs: "globalThis.__dripD4SoloResets = (globalThis.__dripD4SoloResets ?? 0) + 1;",
|
|
391
|
+
targeting: {
|
|
392
|
+
pageRules: [{
|
|
393
|
+
id: "page_d4_trigger",
|
|
394
|
+
includeRules: [{ type: "simple", pattern: "/products/*" }],
|
|
395
|
+
trigger: { deactivationMode: "reset" },
|
|
396
|
+
}],
|
|
397
|
+
},
|
|
398
|
+
variations: [{
|
|
399
|
+
id: "var_d4_solo",
|
|
400
|
+
weight: 1,
|
|
401
|
+
mutations: [{ selector: "#d4-solo", action: "text", text: "Solo treatment" }],
|
|
402
|
+
js: "globalThis.__dripD4SoloRuns = (globalThis.__dripD4SoloRuns ?? 0) + 1;",
|
|
403
|
+
}],
|
|
404
|
+
},
|
|
405
|
+
],
|
|
406
|
+
shopId: "shop_123",
|
|
407
|
+
spa: { activationMode: "manual" },
|
|
408
|
+
trackingStart: "immediate",
|
|
409
|
+
userId: "visitor_d4_s9",
|
|
410
|
+
});
|
|
411
|
+
await vi.waitFor(() => expect((globalThis as any).__dripD4SharedRuns).toBe(1));
|
|
412
|
+
expect((globalThis as any).__dripD4SoloRuns).toBe(1);
|
|
413
|
+
|
|
414
|
+
history.pushState({}, "", "/collections/all");
|
|
415
|
+
await vi.waitFor(() => expect((globalThis as any).__dripD4SharedRuns).toBe(2));
|
|
416
|
+
|
|
417
|
+
expect((globalThis as any).__dripD4SharedResets).toBe(1);
|
|
418
|
+
expect((globalThis as any).__dripD4SoloResets).toBe(1);
|
|
419
|
+
expect((globalThis as any).__dripD4SoloRuns).toBe(1);
|
|
420
|
+
expect(document.getElementById("d4-shared")?.textContent).toBe("Shared treatment");
|
|
421
|
+
expect(document.getElementById("d4-shared")?.getAttribute("data-run")).toBe("2");
|
|
422
|
+
expect(document.getElementById("d4-solo")?.textContent).toBe("Solo control");
|
|
423
|
+
expect(getDiagnostics().pageActivation).toEqual(expect.arrayContaining([
|
|
424
|
+
expect.objectContaining({
|
|
425
|
+
pageId: "page_d4_trigger",
|
|
426
|
+
isolationFallback: "whole_experiment",
|
|
427
|
+
}),
|
|
428
|
+
expect.objectContaining({
|
|
429
|
+
pageId: "page_d4_survivor",
|
|
430
|
+
isolationFallback: "whole_experiment",
|
|
431
|
+
}),
|
|
432
|
+
]));
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// S3/F10: reset JS removes both a JS-owned node and a journal-owned insert.
|
|
436
|
+
// Their presence after navigation proves that D4 invoked D2 replay.
|
|
437
|
+
it("rebuilds JS and structural treatment for a surviving page after variation reset JS", async () => {
|
|
438
|
+
document.body.innerHTML = '<div id="d4-anchor">Anchor</div>';
|
|
439
|
+
const { init } = await import("./index");
|
|
440
|
+
|
|
441
|
+
init({
|
|
442
|
+
antiFlickerMode: "off",
|
|
443
|
+
endpoint: "https://events.test",
|
|
444
|
+
experiments: [{
|
|
445
|
+
id: "exp_d4_variation_reset",
|
|
446
|
+
status: "running",
|
|
447
|
+
trafficAllocation: 1,
|
|
448
|
+
targeting: {
|
|
449
|
+
pageRules: [
|
|
450
|
+
{
|
|
451
|
+
id: "page_d4_variation_reset",
|
|
452
|
+
includeRules: [{ type: "simple", pattern: "/products/*" }],
|
|
453
|
+
trigger: { deactivationMode: "reset" },
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
id: "page_d4_variation_survivor",
|
|
457
|
+
includeRules: [{ type: "simple", pattern: "/products/*" }],
|
|
458
|
+
trigger: { deactivationMode: "persistent" },
|
|
459
|
+
},
|
|
460
|
+
],
|
|
461
|
+
},
|
|
462
|
+
variations: [{
|
|
463
|
+
id: "var_d4_variation_reset",
|
|
464
|
+
weight: 1,
|
|
465
|
+
mutations: [{
|
|
466
|
+
selector: "#d4-anchor",
|
|
467
|
+
action: "insertAfter",
|
|
468
|
+
html: '<p id="d4-structural">Structural treatment</p>',
|
|
469
|
+
}],
|
|
470
|
+
js: [
|
|
471
|
+
"globalThis.__dripD4VariationRuns = (globalThis.__dripD4VariationRuns ?? 0) + 1;",
|
|
472
|
+
"if (!document.querySelector('#d4-js-built')) {",
|
|
473
|
+
" const node = document.createElement('p');",
|
|
474
|
+
" node.id = 'd4-js-built';",
|
|
475
|
+
" node.textContent = 'JS treatment';",
|
|
476
|
+
" document.querySelector('#d4-anchor')?.after(node);",
|
|
477
|
+
"}",
|
|
478
|
+
].join("\n"),
|
|
479
|
+
resetJs: [
|
|
480
|
+
"globalThis.__dripD4VariationResets = (globalThis.__dripD4VariationResets ?? 0) + 1;",
|
|
481
|
+
"document.querySelector('#d4-js-built')?.remove();",
|
|
482
|
+
"document.querySelector('#d4-structural')?.remove();",
|
|
483
|
+
].join("\n"),
|
|
484
|
+
}],
|
|
485
|
+
}],
|
|
486
|
+
shopId: "shop_123",
|
|
487
|
+
spa: { activationMode: "manual" },
|
|
488
|
+
trackingStart: "immediate",
|
|
489
|
+
userId: "visitor_d4_s3",
|
|
490
|
+
});
|
|
491
|
+
await vi.waitFor(() => expect((globalThis as any).__dripD4VariationRuns).toBe(1));
|
|
492
|
+
expect(document.querySelectorAll("#d4-structural")).toHaveLength(1);
|
|
493
|
+
expect(document.querySelectorAll("#d4-js-built")).toHaveLength(1);
|
|
494
|
+
|
|
495
|
+
history.pushState({}, "", "/collections/all");
|
|
496
|
+
await vi.waitFor(() => expect((globalThis as any).__dripD4VariationRuns).toBe(2));
|
|
497
|
+
|
|
498
|
+
expect((globalThis as any).__dripD4VariationResets).toBe(1);
|
|
499
|
+
expect(document.querySelectorAll("#d4-structural")).toHaveLength(1);
|
|
500
|
+
expect(document.querySelector("#d4-structural")?.textContent).toBe("Structural treatment");
|
|
501
|
+
expect(document.querySelectorAll("#d4-js-built")).toHaveLength(1);
|
|
502
|
+
expect(document.querySelector("#d4-js-built")?.textContent).toBe("JS treatment");
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("suppresses reset JS and replays when a deactivate-mode page shares the artifact", async () => {
|
|
506
|
+
document.body.innerHTML = '<h1 id="d4-deactivate-shared">Control</h1>';
|
|
507
|
+
const { init } = await import("./index");
|
|
508
|
+
|
|
509
|
+
init({
|
|
510
|
+
antiFlickerMode: "off",
|
|
511
|
+
endpoint: "https://events.test",
|
|
512
|
+
experiments: [{
|
|
513
|
+
id: "exp_d4_deactivate_shared",
|
|
514
|
+
status: "running",
|
|
515
|
+
trafficAllocation: 1,
|
|
516
|
+
targeting: {
|
|
517
|
+
pageRules: [
|
|
518
|
+
{
|
|
519
|
+
id: "page_d4_reset_exact",
|
|
520
|
+
includeRules: [{ type: "simple", pattern: "/products/widget" }],
|
|
521
|
+
trigger: { deactivationMode: "reset" },
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
id: "page_d4_deactivate_wildcard",
|
|
525
|
+
includeRules: [{ type: "simple", pattern: "/products/*" }],
|
|
526
|
+
trigger: { deactivationMode: "deactivate" },
|
|
527
|
+
},
|
|
528
|
+
],
|
|
529
|
+
},
|
|
530
|
+
variations: [{
|
|
531
|
+
id: "var_d4_deactivate_shared",
|
|
532
|
+
weight: 1,
|
|
533
|
+
mutations: [{
|
|
534
|
+
selector: "#d4-deactivate-shared",
|
|
535
|
+
action: "text",
|
|
536
|
+
text: "Treatment",
|
|
537
|
+
}],
|
|
538
|
+
js: "globalThis.__dripD4DeactivateSharedRuns = (globalThis.__dripD4DeactivateSharedRuns ?? 0) + 1;",
|
|
539
|
+
resetJs: "globalThis.__dripD4DeactivateSharedResets = (globalThis.__dripD4DeactivateSharedResets ?? 0) + 1;",
|
|
540
|
+
}],
|
|
541
|
+
}],
|
|
542
|
+
shopId: "shop_123",
|
|
543
|
+
spa: { activationMode: "manual" },
|
|
544
|
+
trackingStart: "immediate",
|
|
545
|
+
userId: "visitor_d4_deactivate_shared",
|
|
546
|
+
});
|
|
547
|
+
await vi.waitFor(() => expect((globalThis as any).__dripD4DeactivateSharedRuns).toBe(1));
|
|
548
|
+
|
|
549
|
+
history.pushState({}, "", "/products/another");
|
|
550
|
+
await vi.waitFor(() => expect((globalThis as any).__dripD4DeactivateSharedRuns).toBe(2));
|
|
551
|
+
|
|
552
|
+
expect((globalThis as any).__dripD4DeactivateSharedResets).toBeUndefined();
|
|
553
|
+
expect(document.getElementById("d4-deactivate-shared")?.textContent).toBe("Treatment");
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// F14: an audience gate removes the artifact while the deactivate-mode
|
|
557
|
+
// page itself still matches. The observable counter proves no reset JS ran.
|
|
558
|
+
it("does not run reset JS when a non-page gate invalidates a deactivate-mode page", async () => {
|
|
559
|
+
document.body.innerHTML = '<h1 id="d4-f14">Control</h1>';
|
|
560
|
+
const attributes = { eligible: true };
|
|
561
|
+
const { init, reInit } = await import("./index");
|
|
562
|
+
|
|
563
|
+
init({
|
|
564
|
+
antiFlickerMode: "off",
|
|
565
|
+
attributes,
|
|
566
|
+
endpoint: "https://events.test",
|
|
567
|
+
experiments: [{
|
|
568
|
+
id: "exp_d4_f14",
|
|
569
|
+
status: "running",
|
|
570
|
+
trafficAllocation: 1,
|
|
571
|
+
targeting: {
|
|
572
|
+
attributes: [{ attribute: "eligible", operator: "equals", value: true }],
|
|
573
|
+
pageRules: [{
|
|
574
|
+
id: "page_d4_f14_deactivate",
|
|
575
|
+
includeRules: [{ type: "simple", pattern: "/products/*" }],
|
|
576
|
+
trigger: { deactivationMode: "deactivate" },
|
|
577
|
+
}],
|
|
578
|
+
},
|
|
579
|
+
variations: [{
|
|
580
|
+
id: "var_d4_f14",
|
|
581
|
+
weight: 1,
|
|
582
|
+
mutations: [{ selector: "#d4-f14", action: "text", text: "Treatment" }],
|
|
583
|
+
resetJs: "globalThis.__dripF14ResetSideEffect = (globalThis.__dripF14ResetSideEffect ?? 0) + 1;",
|
|
584
|
+
}],
|
|
585
|
+
}],
|
|
586
|
+
shopId: "shop_123",
|
|
587
|
+
spa: { activationMode: "manual" },
|
|
588
|
+
trackingStart: "immediate",
|
|
589
|
+
userId: "visitor_d4_f14",
|
|
590
|
+
});
|
|
591
|
+
expect(document.getElementById("d4-f14")?.textContent).toBe("Treatment");
|
|
592
|
+
|
|
593
|
+
attributes.eligible = false;
|
|
594
|
+
reInit();
|
|
595
|
+
|
|
596
|
+
expect((globalThis as any).__dripF14ResetSideEffect).toBeUndefined();
|
|
597
|
+
expect(document.getElementById("d4-f14")?.textContent).toBe("Control");
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it("changes the artifact id for every rendered content input", async () => {
|
|
601
|
+
const { __test } = await import("./index");
|
|
602
|
+
const base = {
|
|
603
|
+
experimentId: "exp_hash",
|
|
604
|
+
variationId: "var_hash",
|
|
605
|
+
pageActive: true,
|
|
606
|
+
index: 0,
|
|
607
|
+
mutations: [],
|
|
608
|
+
isControl: false,
|
|
609
|
+
hashValue: "visitor",
|
|
610
|
+
};
|
|
611
|
+
const baseline = __test.baseArtifactApplicationKey(base);
|
|
612
|
+
const changed = [
|
|
613
|
+
{ mutations: [{ selector: "h1", action: "text" as const, text: "Changed" }] },
|
|
614
|
+
{ css: "h1{color:red}" },
|
|
615
|
+
{ experimentCss: "body{display:block}" },
|
|
616
|
+
{ js: "globalThis.changed=true" },
|
|
617
|
+
{ experimentJs: "globalThis.experimentChanged=true" },
|
|
618
|
+
{ externalCssUrl: "https://cdn.test/change.css" },
|
|
619
|
+
{ externalJsUrl: "https://cdn.test/change.js" },
|
|
620
|
+
].map((content) => __test.baseArtifactApplicationKey({ ...base, ...content }));
|
|
621
|
+
|
|
622
|
+
expect(new Set(changed).size).toBe(changed.length);
|
|
623
|
+
changed.forEach((artifactId) => expect(artifactId).not.toBe(baseline));
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// F9: the cached config applies on the target URL. The SPA reconciler keeps
|
|
627
|
+
// the persistent page active after navigation, and the network re-init must
|
|
628
|
+
// carry that same state instead of rebuilding it from the current URL.
|
|
629
|
+
it("keeps a persistent off-URL page active through cache-to-network re-init", async () => {
|
|
630
|
+
document.body.innerHTML = [
|
|
631
|
+
'<h1 id="d5-persistent">Control</h1>',
|
|
632
|
+
'<button id="d5-goal">Convert</button>',
|
|
633
|
+
].join("");
|
|
634
|
+
const cachedExperiment = {
|
|
635
|
+
id: "exp_d5_persistent",
|
|
636
|
+
name: "Cached persistent",
|
|
637
|
+
status: "running",
|
|
638
|
+
trafficAllocation: 1,
|
|
639
|
+
targeting: {
|
|
640
|
+
pageRules: [{
|
|
641
|
+
id: "page_d5_persistent",
|
|
642
|
+
includeRules: [{ type: "simple", pattern: "/products/*" }],
|
|
643
|
+
trigger: { deactivationMode: "persistent" },
|
|
644
|
+
}],
|
|
645
|
+
},
|
|
646
|
+
variations: [{
|
|
647
|
+
id: "var_d5_persistent",
|
|
648
|
+
weight: 1,
|
|
649
|
+
mutations: [{ selector: "#d5-persistent", action: "text", text: "Persistent treatment" }],
|
|
650
|
+
css: "#d5-persistent{color:purple}",
|
|
651
|
+
js: "globalThis.__dripD5PersistentRuns = (globalThis.__dripD5PersistentRuns ?? 0) + 1;",
|
|
652
|
+
}],
|
|
653
|
+
goals: [{
|
|
654
|
+
id: "goal_d5_persistent",
|
|
655
|
+
type: "click",
|
|
656
|
+
selector: "#d5-goal",
|
|
657
|
+
pageId: "page_d5_persistent",
|
|
658
|
+
}],
|
|
659
|
+
};
|
|
660
|
+
localStorage.setItem("drip_cfg:shop_123:production", JSON.stringify({
|
|
661
|
+
fetchedAt: Date.now(),
|
|
662
|
+
hash: "d5-persistent-cache",
|
|
663
|
+
experiments: [cachedExperiment],
|
|
664
|
+
}));
|
|
665
|
+
let resolveConfig!: (response: Response) => void;
|
|
666
|
+
const fetchMock = vi.fn((input: RequestInfo | URL) => {
|
|
667
|
+
if (String(input).includes("/config")) {
|
|
668
|
+
return new Promise<Response>((resolve) => {
|
|
669
|
+
resolveConfig = resolve;
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
return Promise.resolve(new Response("ok", { status: 202 }));
|
|
673
|
+
});
|
|
674
|
+
globalThis.fetch = fetchMock as typeof fetch;
|
|
675
|
+
const { __test, flush, getDiagnostics, init, reInit } = await import("./index");
|
|
676
|
+
|
|
677
|
+
init({
|
|
678
|
+
antiFlickerMode: "off",
|
|
679
|
+
endpoint: "https://events.test",
|
|
680
|
+
lateConfigPolicy: "apply",
|
|
681
|
+
shopId: "shop_123",
|
|
682
|
+
spa: { routerMode: "none" },
|
|
683
|
+
trackingStart: "immediate",
|
|
684
|
+
userId: "visitor_d5_persistent",
|
|
685
|
+
});
|
|
686
|
+
await vi.waitFor(() => expect((globalThis as any).__dripD5PersistentRuns).toBe(1));
|
|
687
|
+
const style = document.getElementById("drip-css-exp_d5_persistent-var_d5_persistent");
|
|
688
|
+
expect(document.getElementById("d5-persistent")?.textContent).toBe("Persistent treatment");
|
|
689
|
+
|
|
690
|
+
history.pushState({}, "", "/collections/all");
|
|
691
|
+
reInit();
|
|
692
|
+
expect(getDiagnostics().pageActivation).toContainEqual(expect.objectContaining({
|
|
693
|
+
pageId: "page_d5_persistent",
|
|
694
|
+
active: true,
|
|
695
|
+
appliedArtifactCount: 1,
|
|
696
|
+
}));
|
|
697
|
+
|
|
698
|
+
resolveConfig(Response.json({
|
|
699
|
+
experiments: [{ ...cachedExperiment, name: "Authoritative persistent" }],
|
|
700
|
+
}));
|
|
701
|
+
await vi.waitFor(() => expect(getDiagnostics().configSource).toBe("network"));
|
|
702
|
+
await vi.waitFor(() => expect(getDiagnostics().pageActivation).toContainEqual(
|
|
703
|
+
expect.objectContaining({
|
|
704
|
+
pageId: "page_d5_persistent",
|
|
705
|
+
active: true,
|
|
706
|
+
appliedArtifactCount: 1,
|
|
707
|
+
}),
|
|
708
|
+
));
|
|
709
|
+
expect(document.getElementById("d5-persistent")?.textContent).toBe("Persistent treatment");
|
|
710
|
+
expect(document.getElementById("drip-css-exp_d5_persistent-var_d5_persistent")).toBe(style);
|
|
711
|
+
expect((globalThis as any).__dripD5PersistentRuns).toBe(1);
|
|
712
|
+
expect(__test.artifactInventory()).toEqual([
|
|
713
|
+
expect.objectContaining({ ownerPageIds: ["page_d5_persistent"] }),
|
|
714
|
+
]);
|
|
715
|
+
|
|
716
|
+
document.getElementById("d5-goal")!.click();
|
|
717
|
+
await flush();
|
|
718
|
+
const events = fetchMock.mock.calls
|
|
719
|
+
.filter((call) => String(call[0]).endsWith("/events"))
|
|
720
|
+
.flatMap((call) => JSON.parse(String((call[1] as RequestInit | undefined)?.body)).events);
|
|
721
|
+
expect(events).toEqual(expect.arrayContaining([
|
|
722
|
+
expect.objectContaining({ type: "experiment_pageview", experimentId: "exp_d5_persistent" }),
|
|
723
|
+
expect.objectContaining({
|
|
724
|
+
type: "goal",
|
|
725
|
+
experimentId: "exp_d5_persistent",
|
|
726
|
+
data: expect.objectContaining({ goalId: "goal_d5_persistent" }),
|
|
727
|
+
}),
|
|
728
|
+
]));
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// S8: the authoritative config no longer defines the old page id. The
|
|
732
|
+
// removal must start from the artifact inventory because the new page map
|
|
733
|
+
// cannot name that owner.
|
|
734
|
+
it("tears down inventory owned by a page id removed during network re-init", async () => {
|
|
735
|
+
document.body.innerHTML = '<h1 id="d5-inventory">Control</h1>';
|
|
736
|
+
const cachedExperiment = {
|
|
737
|
+
id: "exp_d5_inventory",
|
|
738
|
+
status: "running",
|
|
739
|
+
trafficAllocation: 1,
|
|
740
|
+
targeting: {
|
|
741
|
+
pageRules: [{
|
|
742
|
+
id: "page_d5_old",
|
|
743
|
+
includeRules: [{ type: "simple", pattern: "/products/*" }],
|
|
744
|
+
trigger: { deactivationMode: "reset" },
|
|
745
|
+
}],
|
|
746
|
+
},
|
|
747
|
+
variations: [{
|
|
748
|
+
id: "var_d5_inventory",
|
|
749
|
+
weight: 1,
|
|
750
|
+
mutations: [{ selector: "#d5-inventory", action: "text", text: "Cached treatment" }],
|
|
751
|
+
css: "#d5-inventory{color:red}",
|
|
752
|
+
externalJsUrl: "https://cdn.test/d5-inventory.js",
|
|
753
|
+
}],
|
|
754
|
+
};
|
|
755
|
+
localStorage.setItem("drip_cfg:shop_123:production", JSON.stringify({
|
|
756
|
+
fetchedAt: Date.now(),
|
|
757
|
+
hash: "d5-inventory-cache",
|
|
758
|
+
experiments: [cachedExperiment],
|
|
759
|
+
}));
|
|
760
|
+
let resolveConfig!: (response: Response) => void;
|
|
761
|
+
globalThis.fetch = vi.fn((input: RequestInfo | URL) => {
|
|
762
|
+
if (String(input).includes("/config")) {
|
|
763
|
+
return new Promise<Response>((resolve) => {
|
|
764
|
+
resolveConfig = resolve;
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
return Promise.resolve(new Response("ok", { status: 202 }));
|
|
768
|
+
}) as typeof fetch;
|
|
769
|
+
const { __test, getDiagnostics, init } = await import("./index");
|
|
770
|
+
|
|
771
|
+
init({
|
|
772
|
+
antiFlickerMode: "off",
|
|
773
|
+
endpoint: "https://events.test",
|
|
774
|
+
lateConfigPolicy: "apply",
|
|
775
|
+
shopId: "shop_123",
|
|
776
|
+
trackingStart: "immediate",
|
|
777
|
+
userId: "visitor_d5_inventory",
|
|
778
|
+
});
|
|
779
|
+
await vi.waitFor(() => expect(
|
|
780
|
+
document.getElementById("drip-external-js-exp_d5_inventory-var_d5_inventory"),
|
|
781
|
+
).not.toBeNull());
|
|
782
|
+
expect(document.getElementById("d5-inventory")?.textContent).toBe("Cached treatment");
|
|
783
|
+
expect(__test.artifactInventory()[0]?.ownerPageIds).toEqual(["page_d5_old"]);
|
|
784
|
+
|
|
785
|
+
resolveConfig(Response.json({
|
|
786
|
+
experiments: [{
|
|
787
|
+
...cachedExperiment,
|
|
788
|
+
targeting: {
|
|
789
|
+
pageRules: [{
|
|
790
|
+
id: "page_d5_renamed",
|
|
791
|
+
includeRules: [{ type: "simple", pattern: "/collections/*" }],
|
|
792
|
+
trigger: { deactivationMode: "reset" },
|
|
793
|
+
}],
|
|
794
|
+
},
|
|
795
|
+
}],
|
|
796
|
+
}));
|
|
797
|
+
await vi.waitFor(() => expect(getDiagnostics().configSource).toBe("network"));
|
|
798
|
+
|
|
799
|
+
expect(document.getElementById("d5-inventory")?.textContent).toBe("Control");
|
|
800
|
+
expect(document.getElementById("drip-css-exp_d5_inventory-var_d5_inventory")).toBeNull();
|
|
801
|
+
expect(document.getElementById("drip-external-js-exp_d5_inventory-var_d5_inventory")).toBeNull();
|
|
802
|
+
expect(getDiagnostics().pageActivation).not.toEqual(expect.arrayContaining([
|
|
803
|
+
expect.objectContaining({ pageId: "page_d5_old" }),
|
|
804
|
+
]));
|
|
805
|
+
expect(getDiagnostics().pageActivation).toContainEqual(expect.objectContaining({
|
|
806
|
+
pageId: "page_d5_renamed",
|
|
807
|
+
active: false,
|
|
808
|
+
appliedArtifactCount: 0,
|
|
809
|
+
}));
|
|
810
|
+
expect(__test.artifactInventory()).toEqual([]);
|
|
811
|
+
|
|
812
|
+
document.getElementById("d5-inventory")!.textContent = "Framework update";
|
|
813
|
+
await Promise.resolve();
|
|
814
|
+
expect(document.getElementById("d5-inventory")?.textContent).toBe("Framework update");
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
it("uses deactivate teardown when a cached page disappears from network config", async () => {
|
|
818
|
+
document.body.innerHTML = '<h1 id="vanished-deactivate">Control</h1>';
|
|
819
|
+
const cachedExperiment = {
|
|
820
|
+
id: "exp_vanished_deactivate",
|
|
821
|
+
status: "running",
|
|
822
|
+
trafficAllocation: 1,
|
|
823
|
+
targeting: {
|
|
824
|
+
pageRules: [{
|
|
825
|
+
id: "page_vanished_deactivate",
|
|
826
|
+
includeRules: [{ type: "simple", pattern: "/products/*" }],
|
|
827
|
+
trigger: { deactivationMode: "deactivate" },
|
|
828
|
+
}],
|
|
829
|
+
},
|
|
830
|
+
variations: [{
|
|
831
|
+
id: "var_vanished_deactivate",
|
|
832
|
+
weight: 1,
|
|
833
|
+
mutations: [{ selector: "#vanished-deactivate", action: "text", text: "Treatment" }],
|
|
834
|
+
resetJs: "globalThis.__dripVanishedDeactivateResets = (globalThis.__dripVanishedDeactivateResets ?? 0) + 1;",
|
|
835
|
+
}],
|
|
836
|
+
};
|
|
837
|
+
localStorage.setItem("drip_cfg:shop_123:production", JSON.stringify({
|
|
838
|
+
fetchedAt: Date.now(),
|
|
839
|
+
hash: "vanished-deactivate-cache",
|
|
840
|
+
experiments: [cachedExperiment],
|
|
841
|
+
}));
|
|
842
|
+
let resolveConfig!: (response: Response) => void;
|
|
843
|
+
globalThis.fetch = vi.fn((input: RequestInfo | URL) => {
|
|
844
|
+
if (String(input).includes("/config")) {
|
|
845
|
+
return new Promise<Response>((resolve) => {
|
|
846
|
+
resolveConfig = resolve;
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
return Promise.resolve(new Response("ok", { status: 202 }));
|
|
850
|
+
}) as typeof fetch;
|
|
851
|
+
const { __test, getDiagnostics, init } = await import("./index");
|
|
852
|
+
|
|
853
|
+
init({
|
|
854
|
+
antiFlickerMode: "off",
|
|
855
|
+
endpoint: "https://events.test",
|
|
856
|
+
lateConfigPolicy: "apply",
|
|
857
|
+
shopId: "shop_123",
|
|
858
|
+
trackingStart: "immediate",
|
|
859
|
+
userId: "visitor_vanished_deactivate",
|
|
860
|
+
});
|
|
861
|
+
await vi.waitFor(() => expect(
|
|
862
|
+
document.getElementById("vanished-deactivate")?.textContent,
|
|
863
|
+
).toBe("Treatment"));
|
|
864
|
+
|
|
865
|
+
resolveConfig(Response.json({ experiments: [] }));
|
|
866
|
+
await vi.waitFor(() => expect(getDiagnostics().configSource).toBe("network"));
|
|
867
|
+
|
|
868
|
+
expect(document.getElementById("vanished-deactivate")?.textContent).toBe("Control");
|
|
869
|
+
expect((globalThis as any).__dripVanishedDeactivateResets).toBeUndefined();
|
|
870
|
+
expect(__test.artifactInventory()).toEqual([]);
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
it("runs reset teardown and replays a shared persistent artifact when one page disappears", async () => {
|
|
874
|
+
document.body.innerHTML = '<h1 id="vanished-reset">Control</h1>';
|
|
875
|
+
const variation = {
|
|
876
|
+
id: "var_vanished_reset",
|
|
877
|
+
weight: 1,
|
|
878
|
+
mutations: [{ selector: "#vanished-reset", action: "text", text: "Treatment" }],
|
|
879
|
+
js: "globalThis.__dripVanishedResetRuns = (globalThis.__dripVanishedResetRuns ?? 0) + 1;",
|
|
880
|
+
resetJs: [
|
|
881
|
+
"globalThis.__dripVanishedResetResets = (globalThis.__dripVanishedResetResets ?? 0) + 1;",
|
|
882
|
+
"document.querySelector('#vanished-reset').textContent = 'Reset';",
|
|
883
|
+
].join("\n"),
|
|
884
|
+
};
|
|
885
|
+
const cachedExperiment = {
|
|
886
|
+
id: "exp_vanished_reset",
|
|
887
|
+
status: "running",
|
|
888
|
+
trafficAllocation: 1,
|
|
889
|
+
targeting: {
|
|
890
|
+
pageRules: [
|
|
891
|
+
{
|
|
892
|
+
id: "page_vanished_reset",
|
|
893
|
+
includeRules: [{ type: "simple", pattern: "/products/*" }],
|
|
894
|
+
trigger: { deactivationMode: "reset" },
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
id: "page_vanished_persistent",
|
|
898
|
+
includeRules: [{ type: "simple", pattern: "/products/*" }],
|
|
899
|
+
trigger: { deactivationMode: "persistent" },
|
|
900
|
+
},
|
|
901
|
+
],
|
|
902
|
+
},
|
|
903
|
+
variations: [variation],
|
|
904
|
+
};
|
|
905
|
+
localStorage.setItem("drip_cfg:shop_123:production", JSON.stringify({
|
|
906
|
+
fetchedAt: Date.now(),
|
|
907
|
+
hash: "vanished-reset-cache",
|
|
908
|
+
experiments: [cachedExperiment],
|
|
909
|
+
}));
|
|
910
|
+
let resolveConfig!: (response: Response) => void;
|
|
911
|
+
globalThis.fetch = vi.fn((input: RequestInfo | URL) => {
|
|
912
|
+
if (String(input).includes("/config")) {
|
|
913
|
+
return new Promise<Response>((resolve) => {
|
|
914
|
+
resolveConfig = resolve;
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
return Promise.resolve(new Response("ok", { status: 202 }));
|
|
918
|
+
}) as typeof fetch;
|
|
919
|
+
const { __test, getDiagnostics, init } = await import("./index");
|
|
920
|
+
|
|
921
|
+
init({
|
|
922
|
+
antiFlickerMode: "off",
|
|
923
|
+
endpoint: "https://events.test",
|
|
924
|
+
lateConfigPolicy: "apply",
|
|
925
|
+
shopId: "shop_123",
|
|
926
|
+
trackingStart: "immediate",
|
|
927
|
+
userId: "visitor_vanished_reset",
|
|
928
|
+
});
|
|
929
|
+
await vi.waitFor(() => expect((globalThis as any).__dripVanishedResetRuns).toBe(1));
|
|
930
|
+
|
|
931
|
+
resolveConfig(Response.json({
|
|
932
|
+
experiments: [{
|
|
933
|
+
...cachedExperiment,
|
|
934
|
+
targeting: {
|
|
935
|
+
pageRules: [{
|
|
936
|
+
id: "page_vanished_persistent",
|
|
937
|
+
includeRules: [{ type: "simple", pattern: "/products/*" }],
|
|
938
|
+
trigger: { deactivationMode: "persistent" },
|
|
939
|
+
}],
|
|
940
|
+
},
|
|
941
|
+
}],
|
|
942
|
+
}));
|
|
943
|
+
await vi.waitFor(() => expect(getDiagnostics().configSource).toBe("network"));
|
|
944
|
+
|
|
945
|
+
expect((globalThis as any).__dripVanishedResetResets).toBe(1);
|
|
946
|
+
expect((globalThis as any).__dripVanishedResetRuns).toBe(2);
|
|
947
|
+
expect(document.getElementById("vanished-reset")?.textContent).toBe("Treatment");
|
|
948
|
+
expect(__test.artifactInventory()).toEqual([
|
|
949
|
+
expect.objectContaining({ ownerPageIds: ["page_vanished_persistent"] }),
|
|
950
|
+
]);
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
// F8: experiment and variation ids are stable, but the content hash changes.
|
|
954
|
+
// The network payload is authoritative for every rendered artifact plane.
|
|
955
|
+
it("replaces cached mutation, CSS, and JS when authoritative content changes", async () => {
|
|
956
|
+
document.body.innerHTML = '<h1 id="d5-authoritative">Control</h1>';
|
|
957
|
+
const cachedExperiment = {
|
|
958
|
+
id: "exp_d5_authoritative",
|
|
959
|
+
status: "running",
|
|
960
|
+
trafficAllocation: 1,
|
|
961
|
+
variations: [{
|
|
962
|
+
id: "var_d5_authoritative",
|
|
963
|
+
weight: 1,
|
|
964
|
+
mutations: [{ selector: "#d5-authoritative", action: "text", text: "Cached content" }],
|
|
965
|
+
css: "#d5-authoritative{color:red}",
|
|
966
|
+
js: [
|
|
967
|
+
"globalThis.__dripD5CachedRuns = (globalThis.__dripD5CachedRuns ?? 0) + 1;",
|
|
968
|
+
"document.querySelector('#d5-authoritative')?.setAttribute('data-code', 'cached');",
|
|
969
|
+
].join("\n"),
|
|
970
|
+
resetJs: [
|
|
971
|
+
"globalThis.__dripD5CachedResets = (globalThis.__dripD5CachedResets ?? 0) + 1;",
|
|
972
|
+
"document.querySelector('#d5-authoritative')?.removeAttribute('data-code');",
|
|
973
|
+
].join("\n"),
|
|
974
|
+
}],
|
|
975
|
+
};
|
|
976
|
+
localStorage.setItem("drip_cfg:shop_123:production", JSON.stringify({
|
|
977
|
+
fetchedAt: Date.now(),
|
|
978
|
+
hash: "d5-authoritative-cache",
|
|
979
|
+
experiments: [cachedExperiment],
|
|
980
|
+
}));
|
|
981
|
+
let resolveConfig!: (response: Response) => void;
|
|
982
|
+
globalThis.fetch = vi.fn((input: RequestInfo | URL) => {
|
|
983
|
+
if (String(input).includes("/config")) {
|
|
984
|
+
return new Promise<Response>((resolve) => {
|
|
985
|
+
resolveConfig = resolve;
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
return Promise.resolve(new Response("ok", { status: 202 }));
|
|
989
|
+
}) as typeof fetch;
|
|
990
|
+
const { __test, getDiagnostics, init } = await import("./index");
|
|
991
|
+
|
|
992
|
+
init({
|
|
993
|
+
antiFlickerMode: "off",
|
|
994
|
+
endpoint: "https://events.test",
|
|
995
|
+
lateConfigPolicy: "apply",
|
|
996
|
+
shopId: "shop_123",
|
|
997
|
+
trackingStart: "immediate",
|
|
998
|
+
userId: "visitor_d5_authoritative",
|
|
999
|
+
});
|
|
1000
|
+
await vi.waitFor(() => expect((globalThis as any).__dripD5CachedRuns).toBe(1));
|
|
1001
|
+
const cachedArtifactId = __test.artifactInventory()[0]!.id;
|
|
1002
|
+
expect(document.getElementById("d5-authoritative")?.textContent).toBe("Cached content");
|
|
1003
|
+
expect(document.getElementById("drip-css-exp_d5_authoritative-var_d5_authoritative")?.textContent)
|
|
1004
|
+
.toBe("#d5-authoritative{color:red}");
|
|
1005
|
+
|
|
1006
|
+
resolveConfig(Response.json({
|
|
1007
|
+
experiments: [{
|
|
1008
|
+
...cachedExperiment,
|
|
1009
|
+
variations: [{
|
|
1010
|
+
id: "var_d5_authoritative",
|
|
1011
|
+
weight: 1,
|
|
1012
|
+
mutations: [{ selector: "#d5-authoritative", action: "text", text: "Network content" }],
|
|
1013
|
+
css: "#d5-authoritative{color:blue}",
|
|
1014
|
+
js: [
|
|
1015
|
+
"globalThis.__dripD5NetworkRuns = (globalThis.__dripD5NetworkRuns ?? 0) + 1;",
|
|
1016
|
+
"document.querySelector('#d5-authoritative')?.setAttribute('data-code', 'network');",
|
|
1017
|
+
].join("\n"),
|
|
1018
|
+
}],
|
|
1019
|
+
}],
|
|
1020
|
+
}));
|
|
1021
|
+
await vi.waitFor(() => expect(getDiagnostics().configSource).toBe("network"));
|
|
1022
|
+
await vi.waitFor(() => expect((globalThis as any).__dripD5NetworkRuns).toBe(1));
|
|
1023
|
+
|
|
1024
|
+
expect((globalThis as any).__dripD5CachedResets).toBe(1);
|
|
1025
|
+
expect(document.getElementById("d5-authoritative")?.textContent).toBe("Network content");
|
|
1026
|
+
expect(document.getElementById("d5-authoritative")?.getAttribute("data-code")).toBe("network");
|
|
1027
|
+
expect(document.getElementById("drip-css-exp_d5_authoritative-var_d5_authoritative")?.textContent)
|
|
1028
|
+
.toBe("#d5-authoritative{color:blue}");
|
|
1029
|
+
expect(__test.artifactInventory()).toHaveLength(1);
|
|
1030
|
+
expect(__test.artifactInventory()[0]!.id).not.toBe(cachedArtifactId);
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
// F2
|
|
1034
|
+
it("ends a reset-JS pushState re-entry with page state, artifacts, and DOM agreeing", async () => {
|
|
1035
|
+
document.body.innerHTML = '<h1 id="hero">Original</h1>';
|
|
1036
|
+
const { getDiagnostics, init } = await import("./index");
|
|
1037
|
+
|
|
1038
|
+
init({
|
|
1039
|
+
antiFlickerMode: "off",
|
|
1040
|
+
endpoint: "https://events.test",
|
|
1041
|
+
experiments: [{
|
|
1042
|
+
id: "exp_reset",
|
|
1043
|
+
status: "running",
|
|
1044
|
+
trafficAllocation: 1,
|
|
1045
|
+
targeting: {
|
|
1046
|
+
pageRules: [{
|
|
1047
|
+
id: "page_reset",
|
|
1048
|
+
includeRules: [{ type: "simple", pattern: "/products/*" }],
|
|
1049
|
+
trigger: { deactivationMode: "reset" },
|
|
1050
|
+
}],
|
|
1051
|
+
},
|
|
1052
|
+
variations: [{
|
|
1053
|
+
id: "var_reset",
|
|
1054
|
+
weight: 1,
|
|
1055
|
+
mutations: [{ selector: "#hero", action: "text", text: "Treatment" }],
|
|
1056
|
+
resetJs: [
|
|
1057
|
+
"globalThis.__dripF2Reset = (globalThis.__dripF2Reset ?? 0) + 1;",
|
|
1058
|
+
"history.pushState({}, '', '/products/widget');",
|
|
1059
|
+
].join("\n"),
|
|
1060
|
+
}],
|
|
1061
|
+
}],
|
|
1062
|
+
shopId: "shop_123",
|
|
1063
|
+
trackingStart: "immediate",
|
|
1064
|
+
userId: "visitor_reset",
|
|
1065
|
+
});
|
|
1066
|
+
expect(document.querySelector("#hero")?.textContent).toBe("Treatment");
|
|
1067
|
+
|
|
1068
|
+
// Leaving the page runs merchant reset JS, which navigates straight back.
|
|
1069
|
+
// history.pushState notifies the router synchronously, so this re-enters
|
|
1070
|
+
// reconciliation from inside the teardown.
|
|
1071
|
+
history.pushState({}, "", "/collections/all");
|
|
1072
|
+
|
|
1073
|
+
const page = getDiagnostics().pageActivation.find(
|
|
1074
|
+
(row) => row.pageId === "page_reset",
|
|
1075
|
+
);
|
|
1076
|
+
expect(page).toBeDefined();
|
|
1077
|
+
expect({
|
|
1078
|
+
active: page!.active,
|
|
1079
|
+
appliedArtifactCount: page!.appliedArtifactCount,
|
|
1080
|
+
hero: document.querySelector("#hero")?.textContent,
|
|
1081
|
+
resetRuns: globalThis.__dripF2Reset,
|
|
1082
|
+
url: location.pathname,
|
|
1083
|
+
}).toEqual({
|
|
1084
|
+
active: true,
|
|
1085
|
+
appliedArtifactCount: 1,
|
|
1086
|
+
hero: "Treatment",
|
|
1087
|
+
resetRuns: 1,
|
|
1088
|
+
url: "/products/widget",
|
|
1089
|
+
});
|
|
1090
|
+
});
|
|
1091
|
+
});
|