@drip-apex/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +46 -0
- package/dist/_headers +3 -0
- package/dist/bundle-report.json +564 -0
- package/dist/drip.debug.js +2 -0
- package/dist/drip.debug.js.map +1 -0
- package/dist/drip.heatmap.js +2 -0
- package/dist/drip.heatmap.js.map +1 -0
- package/dist/drip.js +2 -0
- package/dist/drip.js.map +1 -0
- package/dist/drip.live.js +2 -0
- package/dist/drip.live.js.map +1 -0
- package/dist/editor.js +480 -0
- package/dist/editor.js.map +7 -0
- package/dist/index.mjs +645 -0
- package/dist/index.mjs.map +7 -0
- package/dist/metafiles/drip.debug.meta.json +973 -0
- package/dist/metafiles/drip.heatmap.meta.json +918 -0
- package/dist/metafiles/drip.live.meta.json +918 -0
- package/dist/metafiles/drip.meta.json +973 -0
- package/dist/metafiles/editor.meta.json +321 -0
- package/dist/metafiles/index.meta.json +1039 -0
- package/dist/metafiles/index.mjs.meta.json +78 -0
- package/dist/react/index.js +16973 -0
- package/dist/react/index.js.map +7 -0
- package/dist/sri.json +27 -0
- package/dist/types/analytics.d.ts +15 -0
- package/dist/types/antiflicker.d.ts +14 -0
- package/dist/types/assignment-store.d.ts +30 -0
- package/dist/types/audit.d.ts +9 -0
- package/dist/types/bucket.d.ts +2 -0
- package/dist/types/bucketing-cleanup.d.ts +5 -0
- package/dist/types/commerce-gift.d.ts +41 -0
- package/dist/types/commerce-price.d.ts +22 -0
- package/dist/types/commerce-shipping.d.ts +28 -0
- package/dist/types/css.d.ts +5 -0
- package/dist/types/cwv.d.ts +10 -0
- package/dist/types/data-layer-normalize.d.ts +1 -0
- package/dist/types/debug.d.ts +23 -0
- package/dist/types/define.d.ts +87 -0
- package/dist/types/devtools.d.ts +32 -0
- package/dist/types/dom-trigger.d.ts +3 -0
- package/dist/types/editor/api.d.ts +37 -0
- package/dist/types/editor/artifacts.d.ts +15 -0
- package/dist/types/editor/command-runtime.d.ts +48 -0
- package/dist/types/editor/controller.d.ts +97 -0
- package/dist/types/editor/device-preview.d.ts +23 -0
- package/dist/types/editor/index.d.ts +9 -0
- package/dist/types/editor/operator.d.ts +39 -0
- package/dist/types/editor/picker.d.ts +23 -0
- package/dist/types/editor/selector.d.ts +2 -0
- package/dist/types/editor/types.d.ts +55 -0
- package/dist/types/editor/view-kit.d.ts +33 -0
- package/dist/types/editor/view.d.ts +5 -0
- package/dist/types/editor-bootstrap.d.ts +37 -0
- package/dist/types/exclusion-groups.d.ts +59 -0
- package/dist/types/exposure-signals.d.ts +7 -0
- package/dist/types/flags.d.ts +65 -0
- package/dist/types/goals.d.ts +16 -0
- package/dist/types/guards.d.ts +39 -0
- package/dist/types/handover.d.ts +10 -0
- package/dist/types/hash.d.ts +19 -0
- package/dist/types/heatmap.d.ts +8 -0
- package/dist/types/hideflicker.d.ts +15 -0
- package/dist/types/hydration.d.ts +11 -0
- package/dist/types/index.d.ts +144 -0
- package/dist/types/js.d.ts +6 -0
- package/dist/types/module.d.ts +4 -0
- package/dist/types/money.d.ts +14 -0
- package/dist/types/mutate.d.ts +34 -0
- package/dist/types/observer-mute.d.ts +3 -0
- package/dist/types/pii-keys.d.ts +1 -0
- package/dist/types/prerequisites.d.ts +12 -0
- package/dist/types/qa-assistant-state.d.ts +68 -0
- package/dist/types/qa-assistant-view.d.ts +6 -0
- package/dist/types/qa-assistant.d.ts +9 -0
- package/dist/types/qa-toolbar.d.ts +19 -0
- package/dist/types/react/index.d.ts +84 -0
- package/dist/types/reassert-sentinel.d.ts +6 -0
- package/dist/types/router.d.ts +9 -0
- package/dist/types/runtime-tools.d.ts +50 -0
- package/dist/types/runtime.d.ts +22 -0
- package/dist/types/safe-regex.d.ts +6 -0
- package/dist/types/sanitize.d.ts +5 -0
- package/dist/types/semantic.d.ts +7 -0
- package/dist/types/shopify-cart-events.d.ts +7 -0
- package/dist/types/shopify-cart.d.ts +62 -0
- package/dist/types/signals.d.ts +31 -0
- package/dist/types/surface-signals.d.ts +6 -0
- package/dist/types/targeting-diagnostics.d.ts +5 -0
- package/dist/types/targeting.d.ts +50 -0
- package/dist/types/test-fixtures/commerce.d.ts +12 -0
- package/dist/types/track.d.ts +79 -0
- package/dist/types/types.d.ts +859 -0
- package/dist/types/wait.d.ts +7 -0
- package/package.json +74 -0
- package/src/analytics.ts +146 -0
- package/src/antiflicker.test.ts +127 -0
- package/src/antiflicker.ts +147 -0
- package/src/assignment-store.test.ts +171 -0
- package/src/assignment-store.ts +208 -0
- package/src/audit.test.ts +17 -0
- package/src/audit.ts +362 -0
- package/src/bucket.test.ts +65 -0
- package/src/bucket.ts +18 -0
- package/src/bucketing-cleanup.test.ts +30 -0
- package/src/bucketing-cleanup.ts +72 -0
- package/src/bucketing-parity.test.ts +41 -0
- package/src/commerce-analytics-lifecycle.test.ts +1416 -0
- package/src/commerce-config-lifecycle.test.ts +274 -0
- package/src/commerce-epoch-lifecycle.test.ts +62 -0
- package/src/commerce-gift.test.ts +1293 -0
- package/src/commerce-gift.ts +979 -0
- package/src/commerce-offers-display.test.ts +154 -0
- package/src/commerce-price.test.ts +1145 -0
- package/src/commerce-price.ts +941 -0
- package/src/commerce-revocation-lifecycle.test.ts +78 -0
- package/src/commerce-shipping.test.ts +630 -0
- package/src/commerce-shipping.ts +447 -0
- package/src/config-fetch.test.ts +325 -0
- package/src/css.ts +54 -0
- package/src/cwv.test.ts +95 -0
- package/src/cwv.ts +129 -0
- package/src/data-layer-normalize.ts +21 -0
- package/src/debug.test.ts +83 -0
- package/src/debug.ts +138 -0
- package/src/define.test.ts +327 -0
- package/src/define.ts +208 -0
- package/src/devtools.ts +582 -0
- package/src/dom-trigger.test.ts +70 -0
- package/src/dom-trigger.ts +55 -0
- package/src/editor/api.test.ts +154 -0
- package/src/editor/api.ts +185 -0
- package/src/editor/artifacts.test.ts +57 -0
- package/src/editor/artifacts.ts +115 -0
- package/src/editor/command-runtime.test.ts +225 -0
- package/src/editor/command-runtime.ts +572 -0
- package/src/editor/controller.test.ts +371 -0
- package/src/editor/controller.ts +596 -0
- package/src/editor/device-preview.test.ts +60 -0
- package/src/editor/device-preview.ts +120 -0
- package/src/editor/index.test.ts +161 -0
- package/src/editor/index.ts +169 -0
- package/src/editor/operator.test.ts +148 -0
- package/src/editor/operator.ts +256 -0
- package/src/editor/picker.test.ts +58 -0
- package/src/editor/picker.ts +152 -0
- package/src/editor/selector.test.ts +44 -0
- package/src/editor/selector.ts +110 -0
- package/src/editor/types.ts +92 -0
- package/src/editor/view-kit.ts +162 -0
- package/src/editor/view.test.ts +535 -0
- package/src/editor/view.ts +1395 -0
- package/src/editor-bootstrap.test.ts +116 -0
- package/src/editor-bootstrap.ts +213 -0
- package/src/event-schema-contract.test.ts +35 -0
- package/src/exclusion-groups.test.ts +383 -0
- package/src/exclusion-groups.ts +270 -0
- package/src/exposure-signals.test.ts +182 -0
- package/src/exposure-signals.ts +110 -0
- package/src/flags.test.ts +322 -0
- package/src/flags.ts +201 -0
- package/src/goals.test.ts +42 -0
- package/src/goals.ts +200 -0
- package/src/guards.test.ts +249 -0
- package/src/guards.ts +250 -0
- package/src/handover-cache-order.test.ts +32 -0
- package/src/handover.test.ts +349 -0
- package/src/handover.ts +309 -0
- package/src/hash.ts +214 -0
- package/src/heatmap.test.ts +305 -0
- package/src/heatmap.ts +373 -0
- package/src/hideflicker.ts +21 -0
- package/src/hydration.test.ts +252 -0
- package/src/hydration.ts +203 -0
- package/src/index.ts +7513 -0
- package/src/init-empty.test.ts +5572 -0
- package/src/js.test.ts +26 -0
- package/src/js.ts +61 -0
- package/src/module.ts +50 -0
- package/src/money.test.ts +35 -0
- package/src/money.ts +53 -0
- package/src/mutate.test.ts +304 -0
- package/src/mutate.ts +469 -0
- package/src/observer-mute.test.ts +88 -0
- package/src/observer-mute.ts +45 -0
- package/src/page-trigger.test.ts +84 -0
- package/src/personalization.test.ts +68 -0
- package/src/pii-keys.ts +17 -0
- package/src/prerequisites.test.ts +39 -0
- package/src/prerequisites.ts +94 -0
- package/src/qa-assistant-state.test.ts +471 -0
- package/src/qa-assistant-state.ts +505 -0
- package/src/qa-assistant-view.test.ts +185 -0
- package/src/qa-assistant-view.ts +768 -0
- package/src/qa-assistant.ts +40 -0
- package/src/qa-toolbar.ts +444 -0
- package/src/react/index.test.tsx +177 -0
- package/src/react/index.tsx +252 -0
- package/src/reassert-sentinel.test.ts +314 -0
- package/src/reassert-sentinel.ts +191 -0
- package/src/router.test.ts +78 -0
- package/src/router.ts +92 -0
- package/src/runtime-tools.test.ts +112 -0
- package/src/runtime-tools.ts +403 -0
- package/src/runtime.test.ts +217 -0
- package/src/runtime.ts +268 -0
- package/src/safe-regex.ts +35 -0
- package/src/sanitize.ts +34 -0
- package/src/semantic.ts +245 -0
- package/src/shopify-cart-events.test.ts +76 -0
- package/src/shopify-cart-events.ts +115 -0
- package/src/shopify-cart.test.ts +2823 -0
- package/src/shopify-cart.ts +1256 -0
- package/src/signals.ts +226 -0
- package/src/surface-signals.test.ts +83 -0
- package/src/surface-signals.ts +165 -0
- package/src/targeting-diagnostics.ts +560 -0
- package/src/targeting.detailed.test.ts +443 -0
- package/src/targeting.test.ts +1054 -0
- package/src/targeting.ts +1024 -0
- package/src/test-fixtures/commerce.ts +65 -0
- package/src/track.test.ts +817 -0
- package/src/track.ts +519 -0
- package/src/types.ts +991 -0
- package/src/wait.test.ts +96 -0
- package/src/wait.ts +238 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { EditorApi, EditorApiError } from "./api";
|
|
4
|
+
import { EditorController } from "./controller";
|
|
5
|
+
import type { DevicePreview } from "./device-preview";
|
|
6
|
+
import type { ActiveEditorSession, EditorArtifacts } from "./types";
|
|
7
|
+
|
|
8
|
+
const session: ActiveEditorSession = {
|
|
9
|
+
sid: "session-1",
|
|
10
|
+
bearer: "bearer",
|
|
11
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
12
|
+
shopId: "shop-1",
|
|
13
|
+
experimentId: "experiment-1",
|
|
14
|
+
initialVariationId: "variation-1",
|
|
15
|
+
targetUrl: "https://shop.example/",
|
|
16
|
+
targetOrigin: "https://shop.example",
|
|
17
|
+
variations: [{ id: "variation-1", name: "Treatment" }],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function fakeApi(artifacts: EditorArtifacts = { mutations: [], customCss: "", customJs: "" }) {
|
|
21
|
+
return {
|
|
22
|
+
loadArtifacts: vi.fn(async () => ({
|
|
23
|
+
variation: { id: "variation-1", name: "Treatment", artifactHash: "hash-1", reviewStatus: "approved" },
|
|
24
|
+
artifacts,
|
|
25
|
+
})),
|
|
26
|
+
saveArtifacts: vi.fn(async (_variationId: string, next: EditorArtifacts) => ({
|
|
27
|
+
artifactHash: "hash-2",
|
|
28
|
+
reviewStatus: "approved",
|
|
29
|
+
warningIssues: [],
|
|
30
|
+
artifacts: next,
|
|
31
|
+
})),
|
|
32
|
+
revoke: vi.fn(async () => undefined),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("EditorController", () => {
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
vi.useFakeTimers();
|
|
39
|
+
document.head.innerHTML = "";
|
|
40
|
+
document.body.innerHTML = `<h1 id="title">Shopper copy</h1>`;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
vi.useRealTimers();
|
|
45
|
+
vi.restoreAllMocks();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("applies and toggles a scoped visual draft", async () => {
|
|
49
|
+
const api = fakeApi({
|
|
50
|
+
mutations: [{ selector: "#title", action: "setText", text: "Draft copy" }],
|
|
51
|
+
customCss: "#title{color:red}",
|
|
52
|
+
customJs: "",
|
|
53
|
+
});
|
|
54
|
+
const controller = new EditorController(session, "https://app.example", api as unknown as EditorApi);
|
|
55
|
+
await controller.initialize();
|
|
56
|
+
expect(document.querySelector("h1")?.textContent).toBe("Draft copy");
|
|
57
|
+
expect(document.getElementById("apex-editor-draft-css")).not.toBeNull();
|
|
58
|
+
|
|
59
|
+
controller.toggleArtifacts(false);
|
|
60
|
+
expect(document.querySelector("h1")?.textContent).toBe("Shopper copy");
|
|
61
|
+
expect(document.getElementById("apex-editor-draft-css")).toBeNull();
|
|
62
|
+
controller.destroy();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("upserts edits, supports history, and debounces a full save", async () => {
|
|
66
|
+
const api = fakeApi();
|
|
67
|
+
const controller = new EditorController(session, "https://app.example", api as unknown as EditorApi);
|
|
68
|
+
await controller.initialize();
|
|
69
|
+
const title = document.querySelector("h1")!;
|
|
70
|
+
controller.selectElement(title, "#title");
|
|
71
|
+
controller.setText("Treatment copy");
|
|
72
|
+
expect(title.textContent).toBe("Treatment copy");
|
|
73
|
+
controller.undo();
|
|
74
|
+
expect(title.textContent).toBe("Shopper copy");
|
|
75
|
+
controller.redo();
|
|
76
|
+
expect(title.textContent).toBe("Treatment copy");
|
|
77
|
+
|
|
78
|
+
await vi.advanceTimersByTimeAsync(700);
|
|
79
|
+
expect(api.saveArtifacts).toHaveBeenCalledWith(
|
|
80
|
+
"variation-1",
|
|
81
|
+
expect.objectContaining({
|
|
82
|
+
mutations: [{ selector: "#title", action: "setText", text: "Treatment copy" }],
|
|
83
|
+
}),
|
|
84
|
+
"hash-1",
|
|
85
|
+
);
|
|
86
|
+
expect(controller.getState().saveState).toBe("saved");
|
|
87
|
+
controller.destroy();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("enters a terminal conflict state and does not auto-overwrite", async () => {
|
|
91
|
+
const api = fakeApi();
|
|
92
|
+
api.saveArtifacts.mockRejectedValueOnce(new EditorApiError("Conflict", 409));
|
|
93
|
+
const controller = new EditorController(session, "https://app.example", api as unknown as EditorApi);
|
|
94
|
+
await controller.initialize();
|
|
95
|
+
controller.setCustomCss("h1{color:blue}");
|
|
96
|
+
await vi.advanceTimersByTimeAsync(700);
|
|
97
|
+
expect(controller.getState().saveState).toBe("conflict");
|
|
98
|
+
controller.setCustomCss("h1{color:green}");
|
|
99
|
+
await vi.advanceTimersByTimeAsync(700);
|
|
100
|
+
expect(api.saveArtifacts).toHaveBeenCalledTimes(1);
|
|
101
|
+
controller.destroy();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("serializes autosaves and sends the latest draft with the newest hash", async () => {
|
|
105
|
+
let resolveFirst!: (value: {
|
|
106
|
+
artifactHash: string;
|
|
107
|
+
reviewStatus: string;
|
|
108
|
+
warningIssues: never[];
|
|
109
|
+
artifacts: EditorArtifacts;
|
|
110
|
+
}) => void;
|
|
111
|
+
const firstSave = new Promise<{
|
|
112
|
+
artifactHash: string;
|
|
113
|
+
reviewStatus: string;
|
|
114
|
+
warningIssues: never[];
|
|
115
|
+
artifacts: EditorArtifacts;
|
|
116
|
+
}>((resolve) => { resolveFirst = resolve; });
|
|
117
|
+
const api = fakeApi();
|
|
118
|
+
api.saveArtifacts.mockImplementationOnce(() => firstSave);
|
|
119
|
+
const controller = new EditorController(session, "https://app.example", api as unknown as EditorApi);
|
|
120
|
+
await controller.initialize();
|
|
121
|
+
|
|
122
|
+
controller.setCustomCss("h1{color:blue}");
|
|
123
|
+
await vi.advanceTimersByTimeAsync(700);
|
|
124
|
+
expect(api.saveArtifacts).toHaveBeenCalledTimes(1);
|
|
125
|
+
|
|
126
|
+
controller.setCustomCss("h1{color:green}");
|
|
127
|
+
await vi.advanceTimersByTimeAsync(700);
|
|
128
|
+
expect(api.saveArtifacts).toHaveBeenCalledTimes(1);
|
|
129
|
+
|
|
130
|
+
resolveFirst({
|
|
131
|
+
artifactHash: "hash-2",
|
|
132
|
+
reviewStatus: "approved",
|
|
133
|
+
warningIssues: [],
|
|
134
|
+
artifacts: { mutations: [], customCss: "h1{color:blue}", customJs: "" },
|
|
135
|
+
});
|
|
136
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
137
|
+
|
|
138
|
+
expect(api.saveArtifacts).toHaveBeenCalledTimes(2);
|
|
139
|
+
expect(api.saveArtifacts).toHaveBeenNthCalledWith(
|
|
140
|
+
2,
|
|
141
|
+
"variation-1",
|
|
142
|
+
expect.objectContaining({ customCss: "h1{color:green}" }),
|
|
143
|
+
"hash-2",
|
|
144
|
+
);
|
|
145
|
+
expect(controller.getState().artifacts.customCss).toBe("h1{color:green}");
|
|
146
|
+
controller.destroy();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("ignores a stale variation response after a faster switch", async () => {
|
|
150
|
+
let resolveFirst!: (value: Awaited<ReturnType<EditorApi["loadArtifacts"]>>) => void;
|
|
151
|
+
const first = new Promise<Awaited<ReturnType<EditorApi["loadArtifacts"]>>>((resolve) => {
|
|
152
|
+
resolveFirst = resolve;
|
|
153
|
+
});
|
|
154
|
+
const api = fakeApi();
|
|
155
|
+
api.loadArtifacts
|
|
156
|
+
.mockImplementationOnce(() => first)
|
|
157
|
+
.mockResolvedValueOnce({
|
|
158
|
+
variation: { id: "variation-2", name: "Second", artifactHash: "hash-2", reviewStatus: "approved" },
|
|
159
|
+
artifacts: { mutations: [], customCss: "body{color:blue}", customJs: "" },
|
|
160
|
+
});
|
|
161
|
+
const controller = new EditorController(session, "https://app.example", api as unknown as EditorApi);
|
|
162
|
+
|
|
163
|
+
const pendingFirst = controller.selectVariation("variation-1");
|
|
164
|
+
await controller.selectVariation("variation-2");
|
|
165
|
+
resolveFirst({
|
|
166
|
+
variation: { id: "variation-1", name: "First", artifactHash: "hash-1", reviewStatus: "approved" },
|
|
167
|
+
artifacts: { mutations: [], customCss: "body{color:red}", customJs: "" },
|
|
168
|
+
});
|
|
169
|
+
await pendingFirst;
|
|
170
|
+
|
|
171
|
+
expect(controller.getState().variation?.id).toBe("variation-2");
|
|
172
|
+
expect(controller.getState().artifacts.customCss).toBe("body{color:blue}");
|
|
173
|
+
controller.destroy();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("signals the exact app origin before revoking and reloading", async () => {
|
|
177
|
+
const api = fakeApi();
|
|
178
|
+
const reload = vi.fn();
|
|
179
|
+
const postMessage = vi.fn();
|
|
180
|
+
Object.defineProperty(window, "opener", { configurable: true, value: { postMessage } });
|
|
181
|
+
const controller = new EditorController(
|
|
182
|
+
session,
|
|
183
|
+
"https://app.example",
|
|
184
|
+
api as unknown as EditorApi,
|
|
185
|
+
reload,
|
|
186
|
+
);
|
|
187
|
+
await controller.initialize();
|
|
188
|
+
|
|
189
|
+
await controller.exit();
|
|
190
|
+
|
|
191
|
+
expect(postMessage).toHaveBeenCalledWith({
|
|
192
|
+
type: "apex:editor-exited",
|
|
193
|
+
sid: "session-1",
|
|
194
|
+
experimentId: "experiment-1",
|
|
195
|
+
variationId: "variation-1",
|
|
196
|
+
}, "https://app.example");
|
|
197
|
+
expect(api.revoke).toHaveBeenCalledOnce();
|
|
198
|
+
expect(reload).toHaveBeenCalledOnce();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("keeps the device preview synchronized with the visual toggle", async () => {
|
|
202
|
+
const api = fakeApi();
|
|
203
|
+
const controller = new EditorController(session, "https://app.example", api as unknown as EditorApi);
|
|
204
|
+
await controller.initialize();
|
|
205
|
+
const preview = { update: vi.fn(), open: vi.fn(), close: vi.fn() } as unknown as DevicePreview;
|
|
206
|
+
controller.attachDevicePreview(preview);
|
|
207
|
+
|
|
208
|
+
controller.toggleArtifacts(false);
|
|
209
|
+
controller.openDevicePreview();
|
|
210
|
+
|
|
211
|
+
expect(preview.update).toHaveBeenCalledWith(controller.getState().artifacts, false);
|
|
212
|
+
expect(preview.open).toHaveBeenCalledWith({ mutations: [], customCss: "", customJs: "" });
|
|
213
|
+
controller.destroy();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("applies command artifacts through the scoped controller and reports misses", async () => {
|
|
217
|
+
const api = fakeApi();
|
|
218
|
+
const controller = new EditorController(session, "https://app.example", api as unknown as EditorApi);
|
|
219
|
+
await controller.initialize();
|
|
220
|
+
|
|
221
|
+
const audit = controller.applyCommandArtifacts({
|
|
222
|
+
mutations: [
|
|
223
|
+
{ selector: "#title", action: "setText", text: "Command copy" },
|
|
224
|
+
{ selector: ".missing", action: "setStyle", styles: { color: "red" } },
|
|
225
|
+
],
|
|
226
|
+
customCss: "#title{font-weight:bold}",
|
|
227
|
+
customJs: "fetch('/cart.js')",
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
expect(document.getElementById("title")?.textContent).toBe("Command copy");
|
|
231
|
+
expect(audit).toMatchObject({ ok: false, networkRequired: true, customJsDeferred: true });
|
|
232
|
+
expect(audit.mutationAudit[0]).toMatchObject({ beforeCount: 1, effectVerified: true });
|
|
233
|
+
expect(audit.mutationAudit[1]).toMatchObject({ beforeCount: 0, effectVerified: false });
|
|
234
|
+
controller.destroy();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("verifies HTML mutations against the sanitized browser-normalized result", async () => {
|
|
238
|
+
const api = fakeApi();
|
|
239
|
+
const controller = new EditorController(session, "https://app.example", api as unknown as EditorApi);
|
|
240
|
+
await controller.initialize();
|
|
241
|
+
const verify = (controller as unknown as {
|
|
242
|
+
verifyMutationEffect: (
|
|
243
|
+
mutation: { selector: string; action: "html"; html: string },
|
|
244
|
+
beforeCount: number,
|
|
245
|
+
afterCount: number,
|
|
246
|
+
) => boolean;
|
|
247
|
+
}).verifyMutationEffect.bind(controller);
|
|
248
|
+
const title = document.getElementById("title")!;
|
|
249
|
+
const mutation = {
|
|
250
|
+
selector: "#title",
|
|
251
|
+
action: "html" as const,
|
|
252
|
+
html: `<strong onclick="steal()">Expected</strong><script>steal()</script>`,
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
title.innerHTML = `<em>Different non-empty HTML</em>`;
|
|
256
|
+
expect(verify(mutation, 1, 1)).toBe(false);
|
|
257
|
+
title.innerHTML = `<strong>Expected</strong>`;
|
|
258
|
+
expect(verify(mutation, 1, 1)).toBe(true);
|
|
259
|
+
controller.destroy();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("bounds session revocation before reloading on exit", async () => {
|
|
263
|
+
const api = fakeApi();
|
|
264
|
+
api.revoke.mockImplementationOnce(() => new Promise<void>(() => {}));
|
|
265
|
+
const reload = vi.fn();
|
|
266
|
+
const controller = new EditorController(
|
|
267
|
+
session,
|
|
268
|
+
"https://app.example",
|
|
269
|
+
api as unknown as EditorApi,
|
|
270
|
+
reload,
|
|
271
|
+
);
|
|
272
|
+
await controller.initialize();
|
|
273
|
+
|
|
274
|
+
const exiting = controller.exit();
|
|
275
|
+
await vi.advanceTimersByTimeAsync(1_499);
|
|
276
|
+
expect(reload).not.toHaveBeenCalled();
|
|
277
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
278
|
+
await exiting;
|
|
279
|
+
expect(reload).toHaveBeenCalledOnce();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("removes a selector's mutations and restores artifact snapshots", async () => {
|
|
283
|
+
const api = fakeApi({
|
|
284
|
+
mutations: [{ selector: "#title", action: "setText", text: "Draft copy" }],
|
|
285
|
+
customCss: "",
|
|
286
|
+
customJs: "",
|
|
287
|
+
});
|
|
288
|
+
const controller = new EditorController(session, "https://app.example", api as unknown as EditorApi);
|
|
289
|
+
await controller.initialize();
|
|
290
|
+
const snapshot = controller.getState().artifacts;
|
|
291
|
+
|
|
292
|
+
controller.removeSelector("#title");
|
|
293
|
+
expect(controller.getState().artifacts.mutations).toHaveLength(0);
|
|
294
|
+
expect(document.querySelector("h1")?.textContent).toBe("Shopper copy");
|
|
295
|
+
expect(controller.getState().canUndo).toBe(true);
|
|
296
|
+
|
|
297
|
+
controller.restoreArtifacts(snapshot);
|
|
298
|
+
expect(controller.getState().artifacts.mutations).toHaveLength(1);
|
|
299
|
+
expect(document.querySelector("h1")?.textContent).toBe("Draft copy");
|
|
300
|
+
controller.destroy();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("removes only the selected mutation when a selector has sibling changes", async () => {
|
|
304
|
+
const api = fakeApi({
|
|
305
|
+
mutations: [
|
|
306
|
+
{ selector: "#title", action: "setText", text: "Draft copy" },
|
|
307
|
+
{ selector: "#title", action: "attribute", attributes: { title: "Tooltip" } },
|
|
308
|
+
],
|
|
309
|
+
customCss: "",
|
|
310
|
+
customJs: "",
|
|
311
|
+
});
|
|
312
|
+
const controller = new EditorController(session, "https://app.example", api as unknown as EditorApi);
|
|
313
|
+
await controller.initialize();
|
|
314
|
+
|
|
315
|
+
controller.removeMutation(0);
|
|
316
|
+
|
|
317
|
+
expect(controller.getState().artifacts.mutations).toEqual([
|
|
318
|
+
expect.objectContaining({ action: "attribute", attributes: { title: "Tooltip" } }),
|
|
319
|
+
]);
|
|
320
|
+
controller.destroy();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("resolves a conflict by keeping mine (rebase hash, resave) or loading theirs", async () => {
|
|
324
|
+
const api = fakeApi();
|
|
325
|
+
api.saveArtifacts.mockRejectedValueOnce(new EditorApiError("Conflict", 409));
|
|
326
|
+
const controller = new EditorController(session, "https://app.example", api as unknown as EditorApi);
|
|
327
|
+
await controller.initialize();
|
|
328
|
+
controller.setCustomCss("h1{color:blue}");
|
|
329
|
+
await vi.advanceTimersByTimeAsync(700);
|
|
330
|
+
expect(controller.getState().saveState).toBe("conflict");
|
|
331
|
+
|
|
332
|
+
const keepMine = controller.resolveConflict("mine");
|
|
333
|
+
await vi.advanceTimersByTimeAsync(700);
|
|
334
|
+
await keepMine;
|
|
335
|
+
expect(api.loadArtifacts).toHaveBeenCalledTimes(2);
|
|
336
|
+
expect(api.saveArtifacts).toHaveBeenLastCalledWith(
|
|
337
|
+
"variation-1",
|
|
338
|
+
expect.objectContaining({ customCss: "h1{color:blue}" }),
|
|
339
|
+
"hash-1",
|
|
340
|
+
);
|
|
341
|
+
expect(controller.getState().saveState).toBe("saved");
|
|
342
|
+
|
|
343
|
+
api.saveArtifacts.mockRejectedValueOnce(new EditorApiError("Conflict", 409));
|
|
344
|
+
controller.setCustomCss("h1{color:green}");
|
|
345
|
+
await vi.advanceTimersByTimeAsync(700);
|
|
346
|
+
expect(controller.getState().saveState).toBe("conflict");
|
|
347
|
+
await controller.resolveConflict("theirs");
|
|
348
|
+
expect(controller.getState().artifacts.customCss).toBe("");
|
|
349
|
+
expect(controller.getState().saveState).toBe("saved");
|
|
350
|
+
controller.destroy();
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("extends the session through the api and updates the local expiry", async () => {
|
|
354
|
+
const api = fakeApi();
|
|
355
|
+
const later = new Date(Date.now() + 3_600_000).toISOString();
|
|
356
|
+
const extendable = {
|
|
357
|
+
...api,
|
|
358
|
+
extendSession: vi.fn(async () => later),
|
|
359
|
+
};
|
|
360
|
+
const localSession = { ...session };
|
|
361
|
+
const controller = new EditorController(
|
|
362
|
+
localSession,
|
|
363
|
+
"https://app.example",
|
|
364
|
+
extendable as unknown as EditorApi,
|
|
365
|
+
);
|
|
366
|
+
await controller.initialize();
|
|
367
|
+
await expect(controller.extendSession()).resolves.toBe(later);
|
|
368
|
+
expect(localSession.expiresAt).toBe(later);
|
|
369
|
+
controller.destroy();
|
|
370
|
+
});
|
|
371
|
+
});
|