@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
package/src/mutate.test.ts
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
-
import {
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
addAppliedMutationOwners,
|
|
5
|
+
applyMutations,
|
|
6
|
+
applyTrackedMutation,
|
|
7
|
+
createMutationApplicationContext,
|
|
8
|
+
getMutationKey,
|
|
9
|
+
isInsertAction,
|
|
10
|
+
isInsertDuplicate,
|
|
11
|
+
replayArtifact,
|
|
12
|
+
resetAppliedMutations,
|
|
13
|
+
resetAppliedMutationsForOwner,
|
|
14
|
+
__test,
|
|
15
|
+
} from "./mutate";
|
|
4
16
|
import { setJsPolicy } from "./js";
|
|
5
17
|
|
|
6
|
-
const { normalizeCssPropertyName } = __test;
|
|
18
|
+
const { baselineAttributeNames, normalizeCssPropertyName } = __test;
|
|
7
19
|
|
|
8
20
|
describe("normalizeCssPropertyName", () => {
|
|
9
21
|
it("converts camelCase to kebab-case (backgroundColor)", () => {
|
|
@@ -148,7 +160,426 @@ describe("mutation rollback", () => {
|
|
|
148
160
|
setJsPolicy("hybrid");
|
|
149
161
|
});
|
|
150
162
|
|
|
151
|
-
it("
|
|
163
|
+
it("restores only the tracked attribute and preserves host DOM changes", () => {
|
|
164
|
+
const element = document.createElement("div");
|
|
165
|
+
element.setAttribute("data-state", "control");
|
|
166
|
+
|
|
167
|
+
applyTrackedMutation({
|
|
168
|
+
selector: "div",
|
|
169
|
+
action: "attribute",
|
|
170
|
+
attributes: { "data-state": "treatment" },
|
|
171
|
+
}, element);
|
|
172
|
+
|
|
173
|
+
const hostChild = document.createElement("button");
|
|
174
|
+
hostChild.textContent = "Host child";
|
|
175
|
+
element.appendChild(hostChild);
|
|
176
|
+
element.setAttribute("data-host", "live");
|
|
177
|
+
|
|
178
|
+
resetAppliedMutations();
|
|
179
|
+
|
|
180
|
+
expect(element.getAttribute("data-state")).toBe("control");
|
|
181
|
+
expect(element.getAttribute("data-host")).toBe("live");
|
|
182
|
+
expect(element.firstElementChild).toBe(hostChild);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("preserves body content and host listeners when reverting an attribute", () => {
|
|
186
|
+
document.body.innerHTML = '<button type="button">Host action</button>';
|
|
187
|
+
const button = document.body.querySelector("button")!;
|
|
188
|
+
const onClick = vi.fn();
|
|
189
|
+
button.addEventListener("click", onClick);
|
|
190
|
+
|
|
191
|
+
applyTrackedMutation({
|
|
192
|
+
selector: "body",
|
|
193
|
+
action: "attribute",
|
|
194
|
+
attributes: { "data-treatment": "active" },
|
|
195
|
+
}, document.body);
|
|
196
|
+
document.body.append(" current host content");
|
|
197
|
+
const currentHostHtml = document.body.innerHTML;
|
|
198
|
+
|
|
199
|
+
resetAppliedMutations();
|
|
200
|
+
|
|
201
|
+
expect(document.body.innerHTML).toBe(currentHostHtml);
|
|
202
|
+
button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
203
|
+
expect(onClick).toHaveBeenCalledOnce();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("restores pre-mutation innerHTML for an html action", () => {
|
|
207
|
+
const element = document.createElement("div");
|
|
208
|
+
element.innerHTML = "<strong>Control</strong>";
|
|
209
|
+
|
|
210
|
+
applyTrackedMutation({
|
|
211
|
+
selector: "div",
|
|
212
|
+
action: "html",
|
|
213
|
+
html: "<em>Treatment</em>",
|
|
214
|
+
}, element);
|
|
215
|
+
resetAppliedMutations();
|
|
216
|
+
|
|
217
|
+
expect(element.innerHTML).toBe("<strong>Control</strong>");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("restores text without clobbering host attributes", () => {
|
|
221
|
+
const element = document.createElement("div");
|
|
222
|
+
element.textContent = "Control";
|
|
223
|
+
|
|
224
|
+
applyTrackedMutation({
|
|
225
|
+
selector: "div",
|
|
226
|
+
action: "text",
|
|
227
|
+
text: "Treatment",
|
|
228
|
+
}, element);
|
|
229
|
+
element.setAttribute("data-host", "live");
|
|
230
|
+
resetAppliedMutations();
|
|
231
|
+
|
|
232
|
+
expect(element.textContent).toBe("Control");
|
|
233
|
+
expect(element.getAttribute("data-host")).toBe("live");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("restores pristine markup after text then html mutations", () => {
|
|
237
|
+
const element = document.createElement("div");
|
|
238
|
+
element.innerHTML = "<span>Control</span>";
|
|
239
|
+
|
|
240
|
+
applyTrackedMutation({ selector: "div", action: "text", text: "Treatment" }, element);
|
|
241
|
+
applyTrackedMutation({ selector: "div", action: "html", html: "<em>H</em>" }, element);
|
|
242
|
+
resetAppliedMutations();
|
|
243
|
+
|
|
244
|
+
expect(element.innerHTML).toBe("<span>Control</span>");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("restores child markup after a text mutation", () => {
|
|
248
|
+
const element = document.createElement("div");
|
|
249
|
+
element.innerHTML = "<strong>Control</strong>";
|
|
250
|
+
|
|
251
|
+
applyTrackedMutation({ selector: "div", action: "text", text: "Treatment" }, element);
|
|
252
|
+
resetAppliedMutations();
|
|
253
|
+
|
|
254
|
+
expect(element.innerHTML).toBe("<strong>Control</strong>");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("captures style properties from the original tracked style attribute", () => {
|
|
258
|
+
const element = document.createElement("div");
|
|
259
|
+
element.setAttribute("style", "color: blue");
|
|
260
|
+
|
|
261
|
+
applyTrackedMutation({
|
|
262
|
+
selector: "div",
|
|
263
|
+
action: "attribute",
|
|
264
|
+
attribute: { name: "style", value: "color: red" },
|
|
265
|
+
}, element);
|
|
266
|
+
applyTrackedMutation({
|
|
267
|
+
selector: "div",
|
|
268
|
+
action: "style",
|
|
269
|
+
styles: { color: "green" },
|
|
270
|
+
}, element);
|
|
271
|
+
resetAppliedMutations();
|
|
272
|
+
|
|
273
|
+
expect(element.style.color).toBe("blue");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("does not restore stale content during a later attribute-only teardown", () => {
|
|
277
|
+
const element = document.createElement("div");
|
|
278
|
+
element.textContent = "Control";
|
|
279
|
+
|
|
280
|
+
applyTrackedMutation(
|
|
281
|
+
{ selector: "div", action: "text", text: "Content treatment" },
|
|
282
|
+
element,
|
|
283
|
+
undefined,
|
|
284
|
+
["content-owner"],
|
|
285
|
+
);
|
|
286
|
+
resetAppliedMutationsForOwner("content-owner");
|
|
287
|
+
element.textContent = "Host update";
|
|
288
|
+
|
|
289
|
+
applyTrackedMutation(
|
|
290
|
+
{
|
|
291
|
+
selector: "div",
|
|
292
|
+
action: "attribute",
|
|
293
|
+
attributes: { "data-state": "attribute-treatment" },
|
|
294
|
+
},
|
|
295
|
+
element,
|
|
296
|
+
undefined,
|
|
297
|
+
["attribute-owner"],
|
|
298
|
+
);
|
|
299
|
+
resetAppliedMutationsForOwner("attribute-owner");
|
|
300
|
+
|
|
301
|
+
expect(element.textContent).toBe("Host update");
|
|
302
|
+
expect(element.getAttribute("data-state")).toBeNull();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("shares a pristine baseline between style properties and the style attribute", () => {
|
|
306
|
+
const element = document.createElement("div");
|
|
307
|
+
element.setAttribute("style", "color: blue");
|
|
308
|
+
|
|
309
|
+
applyTrackedMutation({
|
|
310
|
+
selector: "div",
|
|
311
|
+
action: "style",
|
|
312
|
+
styles: { color: "green" },
|
|
313
|
+
}, element);
|
|
314
|
+
applyTrackedMutation({
|
|
315
|
+
selector: "div",
|
|
316
|
+
action: "attribute",
|
|
317
|
+
attribute: { name: "style", value: "color: red" },
|
|
318
|
+
}, element);
|
|
319
|
+
resetAppliedMutations();
|
|
320
|
+
|
|
321
|
+
expect(element.getAttribute("style")).toBe("color: blue");
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("falls back to textContent when innerHTML is not a string", () => {
|
|
325
|
+
const element = {
|
|
326
|
+
innerHTML: undefined,
|
|
327
|
+
textContent: "Control copy",
|
|
328
|
+
} as unknown as Element;
|
|
329
|
+
|
|
330
|
+
applyTrackedMutation(
|
|
331
|
+
{ selector: ".hero", action: "text", text: "Treatment copy" },
|
|
332
|
+
element,
|
|
333
|
+
);
|
|
334
|
+
resetAppliedMutations();
|
|
335
|
+
|
|
336
|
+
expect((element as unknown as { innerHTML?: string }).innerHTML).toBeUndefined();
|
|
337
|
+
expect(element.textContent).toBe("Control copy");
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("replays surviving html while restoring a removed attribute owner", () => {
|
|
341
|
+
const element = document.createElement("div");
|
|
342
|
+
element.setAttribute("data-state", "control");
|
|
343
|
+
element.innerHTML = "<span>Control</span>";
|
|
344
|
+
|
|
345
|
+
applyTrackedMutation({
|
|
346
|
+
selector: "div",
|
|
347
|
+
action: "attribute",
|
|
348
|
+
attributes: { "data-state": "attribute-treatment" },
|
|
349
|
+
}, element, undefined, ["attribute-owner"]);
|
|
350
|
+
applyTrackedMutation({
|
|
351
|
+
selector: "div",
|
|
352
|
+
action: "html",
|
|
353
|
+
html: "<strong>HTML treatment</strong>",
|
|
354
|
+
}, element, undefined, ["html-owner"]);
|
|
355
|
+
|
|
356
|
+
resetAppliedMutationsForOwner("attribute-owner");
|
|
357
|
+
|
|
358
|
+
expect(element.getAttribute("data-state")).toBe("control");
|
|
359
|
+
expect(element.innerHTML).toBe("<strong>HTML treatment</strong>");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("does not adopt and rewrite a framework-owned replacement during route teardown", () => {
|
|
363
|
+
const originalRoute = document.createElement("main");
|
|
364
|
+
originalRoute.className = "route-shell";
|
|
365
|
+
originalRoute.innerHTML = '<button type="button">Original route</button>';
|
|
366
|
+
document.body.appendChild(originalRoute);
|
|
367
|
+
|
|
368
|
+
applyTrackedMutation(
|
|
369
|
+
{
|
|
370
|
+
selector: ".route-shell",
|
|
371
|
+
action: "html",
|
|
372
|
+
html: '<p data-apex-treatment="true">Treatment</p>',
|
|
373
|
+
},
|
|
374
|
+
originalRoute,
|
|
375
|
+
undefined,
|
|
376
|
+
["page-home"],
|
|
377
|
+
"experiment-a:variation-a",
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
const frameworkRoute = document.createElement("main");
|
|
381
|
+
frameworkRoute.className = "route-shell";
|
|
382
|
+
const frameworkButton = document.createElement("button");
|
|
383
|
+
frameworkButton.type = "button";
|
|
384
|
+
frameworkButton.textContent = "Return home";
|
|
385
|
+
const onFrameworkClick = vi.fn();
|
|
386
|
+
frameworkButton.addEventListener("click", onFrameworkClick);
|
|
387
|
+
frameworkRoute.appendChild(frameworkButton);
|
|
388
|
+
originalRoute.replaceWith(frameworkRoute);
|
|
389
|
+
|
|
390
|
+
resetAppliedMutationsForOwner("page-home", "experiment-a:variation-a", {
|
|
391
|
+
preserveReplacementTargets: true,
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
expect(document.querySelector(".route-shell")).toBe(frameworkRoute);
|
|
395
|
+
expect(frameworkRoute.firstElementChild).toBe(frameworkButton);
|
|
396
|
+
frameworkButton.click();
|
|
397
|
+
expect(onFrameworkClick).toHaveBeenCalledOnce();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("restores a still-connected target during replacement-preserving teardown", () => {
|
|
401
|
+
const route = document.createElement("main");
|
|
402
|
+
route.className = "connected-route-shell";
|
|
403
|
+
route.innerHTML = '<button type="button">Original route</button>';
|
|
404
|
+
document.body.appendChild(route);
|
|
405
|
+
|
|
406
|
+
applyTrackedMutation(
|
|
407
|
+
{
|
|
408
|
+
selector: ".connected-route-shell",
|
|
409
|
+
action: "html",
|
|
410
|
+
html: '<p data-apex-treatment="true">Treatment</p>',
|
|
411
|
+
},
|
|
412
|
+
route,
|
|
413
|
+
undefined,
|
|
414
|
+
["page-connected"],
|
|
415
|
+
"experiment-connected:variation-a",
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
expect(route.innerHTML).toBe(
|
|
419
|
+
'<p data-apex-treatment="true">Treatment</p>',
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
resetAppliedMutationsForOwner(
|
|
423
|
+
"page-connected",
|
|
424
|
+
"experiment-connected:variation-a",
|
|
425
|
+
{ preserveReplacementTargets: true },
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
expect(route.isConnected).toBe(true);
|
|
429
|
+
expect(route.innerHTML).toBe(
|
|
430
|
+
'<button type="button">Original route</button>',
|
|
431
|
+
);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("keeps a mutation until its final Page owner tears down", () => {
|
|
435
|
+
const element = { textContent: "Control" } as unknown as Element;
|
|
436
|
+
applyTrackedMutation(
|
|
437
|
+
{ selector: ".hero", action: "text", text: "Treatment" },
|
|
438
|
+
element,
|
|
439
|
+
undefined,
|
|
440
|
+
["page-a"],
|
|
441
|
+
);
|
|
442
|
+
addAppliedMutationOwners(new Set(["page-a"]), ["page-b"]);
|
|
443
|
+
|
|
444
|
+
resetAppliedMutationsForOwner("page-a");
|
|
445
|
+
expect((element as unknown as { textContent: string }).textContent).toBe(
|
|
446
|
+
"Treatment",
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
resetAppliedMutationsForOwner("page-b");
|
|
450
|
+
expect((element as unknown as { textContent: string }).textContent).toBe(
|
|
451
|
+
"Control",
|
|
452
|
+
);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it("adopts owners only for the requested artifact", () => {
|
|
456
|
+
const first = document.createElement("div");
|
|
457
|
+
const second = document.createElement("div");
|
|
458
|
+
first.textContent = "First control";
|
|
459
|
+
second.textContent = "Second control";
|
|
460
|
+
applyTrackedMutation(
|
|
461
|
+
{ selector: ".first", action: "text", text: "First treatment" },
|
|
462
|
+
first,
|
|
463
|
+
undefined,
|
|
464
|
+
["page-a"],
|
|
465
|
+
"artifact-a",
|
|
466
|
+
);
|
|
467
|
+
applyTrackedMutation(
|
|
468
|
+
{ selector: ".second", action: "text", text: "Second treatment" },
|
|
469
|
+
second,
|
|
470
|
+
undefined,
|
|
471
|
+
["page-a"],
|
|
472
|
+
"artifact-b",
|
|
473
|
+
);
|
|
474
|
+
addAppliedMutationOwners(new Set(["page-a"]), ["page-b"], "artifact-a");
|
|
475
|
+
|
|
476
|
+
resetAppliedMutationsForOwner("page-a");
|
|
477
|
+
expect(first.textContent).toBe("First treatment");
|
|
478
|
+
expect(second.textContent).toBe("Second control");
|
|
479
|
+
|
|
480
|
+
resetAppliedMutationsForOwner("page-b");
|
|
481
|
+
expect(first.textContent).toBe("First control");
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("replays a surviving stacked attribute write after the earlier owner tears down", () => {
|
|
485
|
+
const element = document.createElement("div");
|
|
486
|
+
element.setAttribute("data-state", "control");
|
|
487
|
+
|
|
488
|
+
applyTrackedMutation({
|
|
489
|
+
selector: "div",
|
|
490
|
+
action: "attribute",
|
|
491
|
+
attributes: { "data-state": "owner-a" },
|
|
492
|
+
}, element, undefined, ["page-a"]);
|
|
493
|
+
applyTrackedMutation({
|
|
494
|
+
selector: "div",
|
|
495
|
+
action: "attribute",
|
|
496
|
+
attributes: { "data-state": "owner-b" },
|
|
497
|
+
}, element, undefined, ["page-b"]);
|
|
498
|
+
|
|
499
|
+
resetAppliedMutationsForOwner("page-a");
|
|
500
|
+
expect(element.getAttribute("data-state")).toBe("owner-b");
|
|
501
|
+
|
|
502
|
+
resetAppliedMutationsForOwner("page-b");
|
|
503
|
+
expect(element.getAttribute("data-state")).toBe("control");
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("shares an HTML attribute baseline across case-variant stacked owners", () => {
|
|
507
|
+
const element = document.createElement("div");
|
|
508
|
+
element.setAttribute("data-state", "control");
|
|
509
|
+
|
|
510
|
+
applyTrackedMutation({
|
|
511
|
+
selector: "div",
|
|
512
|
+
action: "attribute",
|
|
513
|
+
attributes: { "DATA-STATE": "owner-a" },
|
|
514
|
+
}, element, undefined, ["page-a"]);
|
|
515
|
+
applyTrackedMutation({
|
|
516
|
+
selector: "div",
|
|
517
|
+
action: "attribute",
|
|
518
|
+
attributes: { "data-state": "owner-b" },
|
|
519
|
+
}, element, undefined, ["page-b"]);
|
|
520
|
+
|
|
521
|
+
expect(baselineAttributeNames(element)).toEqual(["data-state"]);
|
|
522
|
+
resetAppliedMutationsForOwner("page-a");
|
|
523
|
+
expect(element.getAttribute("data-state")).toBe("owner-b");
|
|
524
|
+
expect(baselineAttributeNames(element)).toEqual(["data-state"]);
|
|
525
|
+
|
|
526
|
+
resetAppliedMutationsForOwner("page-b");
|
|
527
|
+
expect(element.getAttribute("data-state")).toBe("control");
|
|
528
|
+
expect(baselineAttributeNames(element)).toEqual([]);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it("preserves mixed-case SVG attribute names in the baseline", () => {
|
|
532
|
+
const element = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
533
|
+
element.setAttribute("viewBox", "0 0 10 10");
|
|
534
|
+
|
|
535
|
+
applyTrackedMutation({
|
|
536
|
+
selector: "svg",
|
|
537
|
+
action: "attribute",
|
|
538
|
+
attributes: { viewBox: "0 0 20 20" },
|
|
539
|
+
}, element, undefined, ["page-svg"]);
|
|
540
|
+
|
|
541
|
+
expect(baselineAttributeNames(element)).toEqual(["viewBox"]);
|
|
542
|
+
resetAppliedMutationsForOwner("page-svg");
|
|
543
|
+
expect(element.getAttribute("viewBox")).toBe("0 0 10 10");
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it("adopts a duplicate insertion so the nodes survive until the final owner tears down", () => {
|
|
547
|
+
document.body.innerHTML = '<div id="anchor">Anchor</div>';
|
|
548
|
+
const mutation = {
|
|
549
|
+
selector: "#anchor",
|
|
550
|
+
action: "insertAfter" as const,
|
|
551
|
+
html: '<span class="shared-insert">Shared</span>',
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
applyMutations([mutation], { ownerKeys: ["page-a"] });
|
|
555
|
+
applyMutations([mutation], { ownerKeys: ["page-b"] });
|
|
556
|
+
expect(document.querySelectorAll(".shared-insert")).toHaveLength(1);
|
|
557
|
+
|
|
558
|
+
resetAppliedMutationsForOwner("page-a");
|
|
559
|
+
expect(document.querySelectorAll(".shared-insert")).toHaveLength(1);
|
|
560
|
+
|
|
561
|
+
resetAppliedMutationsForOwner("page-b");
|
|
562
|
+
expect(document.querySelectorAll(".shared-insert")).toHaveLength(0);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it("rebuilds a treatment after a foreign DOM replacement", () => {
|
|
566
|
+
document.body.innerHTML = '<div id="hero">Control</div>';
|
|
567
|
+
applyMutations([{
|
|
568
|
+
selector: "#hero",
|
|
569
|
+
action: "text",
|
|
570
|
+
text: "Treatment",
|
|
571
|
+
}], {
|
|
572
|
+
artifactId: "artifact-a",
|
|
573
|
+
ownerKeys: ["page-a"],
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
document.getElementById("hero")!.outerHTML = '<div id="hero">Framework</div>';
|
|
577
|
+
replayArtifact("artifact-a");
|
|
578
|
+
|
|
579
|
+
expect(document.getElementById("hero")?.textContent).toBe("Treatment");
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
it("blocks dangerous attributes under every custom JavaScript policy", () => {
|
|
152
583
|
const element = document.createElement("a");
|
|
153
584
|
setJsPolicy("strict");
|
|
154
585
|
applyTrackedMutation({ selector: "a", action: "attribute", attributes: {
|
|
@@ -162,8 +593,21 @@ describe("mutation rollback", () => {
|
|
|
162
593
|
applyTrackedMutation({ selector: "a", action: "attribute", attributes: {
|
|
163
594
|
onclick: "alert(1)", href: "javascript:alert(1)",
|
|
164
595
|
} }, element);
|
|
165
|
-
expect(element.getAttribute("onclick")).
|
|
166
|
-
expect(element.getAttribute("href")).
|
|
596
|
+
expect(element.getAttribute("onclick")).toBeNull();
|
|
597
|
+
expect(element.getAttribute("href")).toBeNull();
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it("allows an attribute mutation to remove an existing inline handler", () => {
|
|
601
|
+
const element = document.createElement("button");
|
|
602
|
+
element.setAttribute("onclick", "alert(1)");
|
|
603
|
+
|
|
604
|
+
applyTrackedMutation({
|
|
605
|
+
selector: "button",
|
|
606
|
+
action: "attribute",
|
|
607
|
+
attributes: { onclick: null },
|
|
608
|
+
}, element);
|
|
609
|
+
|
|
610
|
+
expect(element.hasAttribute("onclick")).toBe(false);
|
|
167
611
|
});
|
|
168
612
|
|
|
169
613
|
it.each([
|
|
@@ -171,7 +615,7 @@ describe("mutation rollback", () => {
|
|
|
171
615
|
["data:text/html,<script>alert(1)</script>", false],
|
|
172
616
|
["data:image/svg+xml,<svg onload='alert(1)'></svg>", false],
|
|
173
617
|
[" \tDATA: IMAGE/PNG ;BASE64,iVBORw0KGgo=", true],
|
|
174
|
-
])("applies
|
|
618
|
+
])("applies data URL allowlisting to %s", (url, allowed) => {
|
|
175
619
|
const element = document.createElement("iframe");
|
|
176
620
|
setJsPolicy("strict");
|
|
177
621
|
|
|
@@ -185,20 +629,21 @@ describe("mutation rollback", () => {
|
|
|
185
629
|
});
|
|
186
630
|
|
|
187
631
|
it.each(["html", "insertBefore", "insertAfter"] as const)(
|
|
188
|
-
"strips
|
|
632
|
+
"strips unsafe attributes from %s mutations under the default policy",
|
|
189
633
|
(action) => {
|
|
190
634
|
document.body.innerHTML = '<div id="target"></div>';
|
|
191
635
|
const target = document.querySelector("#target")!;
|
|
192
|
-
setJsPolicy("
|
|
636
|
+
setJsPolicy("hybrid");
|
|
193
637
|
applyTrackedMutation({
|
|
194
638
|
selector: "#target",
|
|
195
639
|
action,
|
|
196
|
-
html: '<iframe srcdoc="<script>alert(1)</script>"></iframe><a onclick="alert(1)" href="
|
|
640
|
+
html: '<iframe srcdoc="<script>alert(1)</script>"></iframe><a onclick="alert(1)" href="data:text/html,alert(1)">Safe copy</a><img src="data:image/png;base64,iVBORw0KGgo=">',
|
|
197
641
|
}, target);
|
|
198
642
|
const scope = action === "html" ? target : document.body;
|
|
199
643
|
expect(scope.querySelector("iframe")?.hasAttribute("srcdoc")).toBe(false);
|
|
200
644
|
expect(scope.querySelector("a")?.hasAttribute("onclick")).toBe(false);
|
|
201
645
|
expect(scope.querySelector("a")?.hasAttribute("href")).toBe(false);
|
|
646
|
+
expect(scope.querySelector("img")?.getAttribute("src")).toBe("data:image/png;base64,iVBORw0KGgo=");
|
|
202
647
|
expect(scope.querySelector("a")?.textContent).toBe("Safe copy");
|
|
203
648
|
},
|
|
204
649
|
);
|
|
@@ -291,6 +736,25 @@ describe("mutation rollback", () => {
|
|
|
291
736
|
);
|
|
292
737
|
});
|
|
293
738
|
|
|
739
|
+
it("rolls back a partial multi-mutation application in reverse order and is idempotent", () => {
|
|
740
|
+
document.body.innerHTML = `<h1 id="title">Original</h1>`;
|
|
741
|
+
const context = createMutationApplicationContext();
|
|
742
|
+
const missed: string[] = [];
|
|
743
|
+
|
|
744
|
+
context.apply([
|
|
745
|
+
{ selector: "#title", action: "text", text: "First" },
|
|
746
|
+
{ selector: "#title", action: "text", text: "Second" },
|
|
747
|
+
{ selector: "#missing", action: "text", text: "Never applied" },
|
|
748
|
+
], { onMiss: (mutation) => missed.push(mutation.selector) });
|
|
749
|
+
|
|
750
|
+
expect(document.getElementById("title")?.textContent).toBe("Second");
|
|
751
|
+
expect(missed).toEqual(["#missing"]);
|
|
752
|
+
context.rollback();
|
|
753
|
+
expect(document.getElementById("title")?.textContent).toBe("Original");
|
|
754
|
+
context.rollback();
|
|
755
|
+
expect(document.getElementById("title")?.textContent).toBe("Original");
|
|
756
|
+
});
|
|
757
|
+
|
|
294
758
|
it("does not snapshot or remove a pre-existing sibling for an empty insertion", () => {
|
|
295
759
|
document.body.innerHTML = `<div id="anchor">Anchor</div><p id="existing">Existing</p>`;
|
|
296
760
|
const context = createMutationApplicationContext();
|