@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
package/src/signals.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
type SignalValue = string | number | boolean | string[] | Record<string, unknown> | null;
|
|
2
|
+
|
|
3
|
+
type StoredSignal = {
|
|
4
|
+
value: SignalValue;
|
|
5
|
+
updatedAt: number;
|
|
6
|
+
expiresAt?: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type ProductSignalInput = {
|
|
10
|
+
id?: string | number | null;
|
|
11
|
+
handle?: string | null;
|
|
12
|
+
title?: string | null;
|
|
13
|
+
type?: string | null;
|
|
14
|
+
vendor?: string | null;
|
|
15
|
+
tags?: string[] | null;
|
|
16
|
+
price?: number | string | null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const STORE_KEY = "drip_signals_v1";
|
|
20
|
+
const VISITOR_SEEN_KEY = "drip_visitor_seen_v1";
|
|
21
|
+
const DEFAULT_TTL_DAYS = 30;
|
|
22
|
+
const MAX_ARRAY_LENGTH = 24;
|
|
23
|
+
const MAX_VALUE_BYTES = 4096;
|
|
24
|
+
const runtimeSignals: Record<string, SignalValue> = {};
|
|
25
|
+
const signalSources: Record<string, string> = {};
|
|
26
|
+
let signalPersistenceAllowed: () => boolean = () => true;
|
|
27
|
+
|
|
28
|
+
function now(): number {
|
|
29
|
+
return Date.now();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ttlMs(days: number): number {
|
|
33
|
+
return Math.max(1, days) * 86400 * 1000;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function canUseLocalStorage(): boolean {
|
|
37
|
+
if (!canPersistSignals()) return false;
|
|
38
|
+
try {
|
|
39
|
+
if (typeof localStorage === "undefined") return false;
|
|
40
|
+
const key = "__drip_signal_test__";
|
|
41
|
+
localStorage.setItem(key, "1");
|
|
42
|
+
localStorage.removeItem(key);
|
|
43
|
+
return true;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function canPersistSignals(): boolean {
|
|
50
|
+
try {
|
|
51
|
+
return signalPersistenceAllowed() === true;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function configureSignalPersistenceAllowed(predicate?: () => boolean): void {
|
|
58
|
+
signalPersistenceAllowed = predicate ?? (() => true);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readStore(): Record<string, StoredSignal> {
|
|
62
|
+
if (!canUseLocalStorage()) return {};
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(localStorage.getItem(STORE_KEY) || "{}");
|
|
65
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
66
|
+
} catch {
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function writeStore(store: Record<string, StoredSignal>): void {
|
|
72
|
+
if (!canUseLocalStorage()) return;
|
|
73
|
+
try {
|
|
74
|
+
localStorage.setItem(STORE_KEY, JSON.stringify(store));
|
|
75
|
+
} catch {}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeValue(value: unknown): SignalValue | undefined {
|
|
79
|
+
if (value === null) return null;
|
|
80
|
+
if (typeof value === "string") return value.slice(0, 1000);
|
|
81
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : undefined;
|
|
82
|
+
if (typeof value === "boolean") return value;
|
|
83
|
+
if (Array.isArray(value)) {
|
|
84
|
+
return value
|
|
85
|
+
.map((item) => String(item).trim())
|
|
86
|
+
.filter(Boolean)
|
|
87
|
+
.slice(0, MAX_ARRAY_LENGTH);
|
|
88
|
+
}
|
|
89
|
+
if (value && typeof value === "object") {
|
|
90
|
+
try {
|
|
91
|
+
const json = JSON.stringify(value);
|
|
92
|
+
if (json.length > MAX_VALUE_BYTES) return undefined;
|
|
93
|
+
return JSON.parse(json);
|
|
94
|
+
} catch {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function compactStore(store: Record<string, StoredSignal>): Record<string, StoredSignal> {
|
|
102
|
+
const ts = now();
|
|
103
|
+
const next: Record<string, StoredSignal> = {};
|
|
104
|
+
for (const [key, entry] of Object.entries(store)) {
|
|
105
|
+
if (!entry || typeof entry !== "object") continue;
|
|
106
|
+
if (entry.expiresAt && entry.expiresAt <= ts) continue;
|
|
107
|
+
next[key] = entry;
|
|
108
|
+
}
|
|
109
|
+
return next;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function setSignal(key: string, value: unknown, ttlDays = DEFAULT_TTL_DAYS, source = "manual"): void {
|
|
113
|
+
if (!canPersistSignals()) return;
|
|
114
|
+
const normalized = normalizeValue(value);
|
|
115
|
+
if (normalized === undefined) return;
|
|
116
|
+
const store = compactStore(readStore());
|
|
117
|
+
store[key] = {
|
|
118
|
+
value: normalized,
|
|
119
|
+
updatedAt: now(),
|
|
120
|
+
expiresAt: now() + ttlMs(ttlDays),
|
|
121
|
+
};
|
|
122
|
+
writeStore(store);
|
|
123
|
+
signalSources[key] = source;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function getSignals(options: { markVisitorSeen?: boolean } = {}): Record<string, SignalValue> {
|
|
127
|
+
const store = compactStore(readStore());
|
|
128
|
+
if (options.markVisitorSeen !== false) writeStore(store);
|
|
129
|
+
const signals: Record<string, SignalValue> = {};
|
|
130
|
+
for (const [key, entry] of Object.entries(store)) {
|
|
131
|
+
signals[key] = entry.value;
|
|
132
|
+
}
|
|
133
|
+
Object.assign(signals, runtimeSignals);
|
|
134
|
+
|
|
135
|
+
if (options.markVisitorSeen !== false && canPersistSignals()) {
|
|
136
|
+
try {
|
|
137
|
+
const seen = localStorage.getItem(VISITOR_SEEN_KEY) === "1";
|
|
138
|
+
signals["visitor.returning"] = seen;
|
|
139
|
+
localStorage.setItem(VISITOR_SEEN_KEY, "1");
|
|
140
|
+
} catch {}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return signals;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function clearStoredSignals(): void {
|
|
147
|
+
try {
|
|
148
|
+
localStorage.removeItem(STORE_KEY);
|
|
149
|
+
localStorage.removeItem(VISITOR_SEEN_KEY);
|
|
150
|
+
} catch {}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function setRuntimeSignal(key: string, value: unknown, source = "runtime"): void {
|
|
154
|
+
const normalized = normalizeValue(value);
|
|
155
|
+
if (normalized === undefined) return;
|
|
156
|
+
runtimeSignals[key] = normalized;
|
|
157
|
+
signalSources[key] = source;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function clearRuntimeSignal(key: string): void {
|
|
161
|
+
delete runtimeSignals[key];
|
|
162
|
+
delete signalSources[key];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function getSignalDiagnostics(options: { markVisitorSeen?: boolean } = {}): { values: Record<string, SignalValue>; sources: Record<string, string> } {
|
|
166
|
+
const values = getSignals(options);
|
|
167
|
+
return {
|
|
168
|
+
values,
|
|
169
|
+
sources: { ...signalSources },
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function prependUnique(values: string[], next: string): string[] {
|
|
174
|
+
const clean = next.trim();
|
|
175
|
+
if (!clean) return values;
|
|
176
|
+
return [clean, ...values.filter((value) => value !== clean)].slice(0, MAX_ARRAY_LENGTH);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function readStringArray(key: string): string[] {
|
|
180
|
+
const value = getSignals({ markVisitorSeen: false })[key];
|
|
181
|
+
return Array.isArray(value) ? value.map((item) => String(item)) : [];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function rememberProduct(product: ProductSignalInput, ttlDays = DEFAULT_TTL_DAYS): void {
|
|
185
|
+
const id = product.id == null ? "" : String(product.id);
|
|
186
|
+
const handle = product.handle ? String(product.handle) : "";
|
|
187
|
+
const productType = product.type ? String(product.type) : "";
|
|
188
|
+
|
|
189
|
+
if (id) {
|
|
190
|
+
setSignal("recently_viewed.product_ids", prependUnique(readStringArray("recently_viewed.product_ids"), id), ttlDays);
|
|
191
|
+
}
|
|
192
|
+
if (handle) {
|
|
193
|
+
setSignal("recently_viewed.product_handles", prependUnique(readStringArray("recently_viewed.product_handles"), handle), ttlDays);
|
|
194
|
+
}
|
|
195
|
+
if (productType) {
|
|
196
|
+
setSignal(
|
|
197
|
+
"recently_viewed.product_types",
|
|
198
|
+
prependUnique(readStringArray("recently_viewed.product_types"), productType),
|
|
199
|
+
ttlDays,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
if (product.tags?.length) {
|
|
203
|
+
const existing = readStringArray("recently_viewed.product_tags");
|
|
204
|
+
const next = product.tags.reduce((acc, tag) => prependUnique(acc, String(tag)), existing);
|
|
205
|
+
setSignal("recently_viewed.product_tags", next, ttlDays);
|
|
206
|
+
}
|
|
207
|
+
setSignal("recently_viewed.last_product", {
|
|
208
|
+
id: id || undefined,
|
|
209
|
+
handle: handle || undefined,
|
|
210
|
+
title: product.title || undefined,
|
|
211
|
+
type: productType || undefined,
|
|
212
|
+
vendor: product.vendor || undefined,
|
|
213
|
+
price: product.price ?? undefined,
|
|
214
|
+
}, ttlDays);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function setCartSignals(args: { itemCount?: number; productIds?: string[] }, ttlDays = 14): void {
|
|
218
|
+
if (typeof args.itemCount === "number") setSignal("cart.item_count", args.itemCount, ttlDays);
|
|
219
|
+
if (args.productIds) setSignal("cart.product_ids", args.productIds, ttlDays);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function setSearchSignal(query: string, ttlDays = 7): void {
|
|
223
|
+
const trimmed = query.trim();
|
|
224
|
+
if (!trimmed) return;
|
|
225
|
+
setSignal("search.last_query", trimmed, ttlDays);
|
|
226
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { CART_DRAWER_OPEN_SIGNAL, detectCartDrawerOpen } from "./surface-signals";
|
|
3
|
+
import { clearRuntimeSignal, getSignals, setRuntimeSignal } from "./signals";
|
|
4
|
+
|
|
5
|
+
class FakeElement {
|
|
6
|
+
private attrs = new Map<string, string>();
|
|
7
|
+
|
|
8
|
+
constructor(attrs: Record<string, string> = {}) {
|
|
9
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
10
|
+
this.attrs.set(key, value);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getAttribute(name: string): string | null {
|
|
15
|
+
return this.attrs.get(name) ?? null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
hasAttribute(name: string): boolean {
|
|
19
|
+
return this.attrs.has(name);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
getBoundingClientRect() {
|
|
23
|
+
return { width: 320, height: 640 };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function installDom(candidates: FakeElement[], bodyClass = "") {
|
|
28
|
+
const body = new FakeElement({ class: bodyClass });
|
|
29
|
+
vi.stubGlobal("window", {
|
|
30
|
+
getComputedStyle: () => ({
|
|
31
|
+
display: "block",
|
|
32
|
+
opacity: "1",
|
|
33
|
+
pointerEvents: "auto",
|
|
34
|
+
visibility: "visible",
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
vi.stubGlobal("document", {
|
|
38
|
+
body,
|
|
39
|
+
querySelectorAll: () => candidates,
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
querySelectorAll: () => candidates,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("surface signals", () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
vi.stubGlobal("localStorage", {
|
|
49
|
+
getItem: () => null,
|
|
50
|
+
setItem: vi.fn(),
|
|
51
|
+
removeItem: vi.fn(),
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
clearRuntimeSignal(CART_DRAWER_OPEN_SIGNAL);
|
|
57
|
+
vi.unstubAllGlobals();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("detects a cart drawer body open state", () => {
|
|
61
|
+
const root = installDom([], "cart-drawer-open");
|
|
62
|
+
expect(detectCartDrawerOpen(root as unknown as ParentNode)).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("detects an open visible cart drawer element", () => {
|
|
66
|
+
const root = installDom([
|
|
67
|
+
new FakeElement({ class: "cart-drawer is-active", "aria-hidden": "false" }),
|
|
68
|
+
]);
|
|
69
|
+
expect(detectCartDrawerOpen(root as unknown as ParentNode)).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("does not detect closed cart drawer elements", () => {
|
|
73
|
+
const root = installDom([
|
|
74
|
+
new FakeElement({ class: "cart-drawer is-hidden", "aria-hidden": "true" }),
|
|
75
|
+
]);
|
|
76
|
+
expect(detectCartDrawerOpen(root as unknown as ParentNode)).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("exposes runtime surface state through targeting signals", () => {
|
|
80
|
+
setRuntimeSignal(CART_DRAWER_OPEN_SIGNAL, true);
|
|
81
|
+
expect(getSignals()[CART_DRAWER_OPEN_SIGNAL]).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { clearRuntimeSignal, setRuntimeSignal } from "./signals";
|
|
2
|
+
|
|
3
|
+
export const CART_DRAWER_OPEN_SIGNAL = "surface.cart_drawer.open";
|
|
4
|
+
|
|
5
|
+
const BODY_OPEN_PATTERN =
|
|
6
|
+
/\b(cart|mini-cart|side-cart|cart-drawer|drawer)[-_ ]?(open|opened|active|visible|shown)\b|\b(open|active|visible|shown)[-_ ]?(cart|mini-cart|side-cart|cart-drawer|drawer)\b/i;
|
|
7
|
+
|
|
8
|
+
export const CART_DRAWER_SELECTORS = [
|
|
9
|
+
"cart-drawer",
|
|
10
|
+
"mini-cart",
|
|
11
|
+
"side-cart",
|
|
12
|
+
"[data-cart-drawer]",
|
|
13
|
+
"[data-cart-drawer-open]",
|
|
14
|
+
"[data-side-cart]",
|
|
15
|
+
"[data-mini-cart]",
|
|
16
|
+
"[data-state='open'][class*='cart' i]",
|
|
17
|
+
"[data-state='open'][id*='cart' i]",
|
|
18
|
+
"[aria-hidden='false'][class*='cart' i][class*='drawer' i]",
|
|
19
|
+
"[aria-hidden='false'][id*='cart' i][id*='drawer' i]",
|
|
20
|
+
"[class*='cart-drawer' i]",
|
|
21
|
+
"[class*='mini-cart' i]",
|
|
22
|
+
"[class*='side-cart' i]",
|
|
23
|
+
"[id*='cart-drawer' i]",
|
|
24
|
+
"[id*='mini-cart' i]",
|
|
25
|
+
"[id*='side-cart' i]",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export function findCartDrawerContainer(
|
|
29
|
+
root: ParentNode = document,
|
|
30
|
+
): Element | null {
|
|
31
|
+
if (typeof document === "undefined") return null;
|
|
32
|
+
for (const selector of CART_DRAWER_SELECTORS) {
|
|
33
|
+
const element = root.querySelector(selector);
|
|
34
|
+
if (element) return element;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function classNameOf(element: Element): string {
|
|
40
|
+
const value = element.getAttribute("class");
|
|
41
|
+
return typeof value === "string" ? value : "";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hasOpenHint(element: Element): boolean {
|
|
45
|
+
const classes = classNameOf(element);
|
|
46
|
+
const state = element.getAttribute("data-state") ?? "";
|
|
47
|
+
return (
|
|
48
|
+
element.hasAttribute("open") ||
|
|
49
|
+
element.getAttribute("aria-hidden") === "false" ||
|
|
50
|
+
element.getAttribute("aria-expanded") === "true" ||
|
|
51
|
+
/^(open|opened|active|visible|shown)$/i.test(state) ||
|
|
52
|
+
/\b(is-|has-)?(open|opened|active|visible|shown)\b/i.test(classes)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isExplicitlyClosed(element: Element): boolean {
|
|
57
|
+
const classes = classNameOf(element);
|
|
58
|
+
return (
|
|
59
|
+
element.hasAttribute("hidden") ||
|
|
60
|
+
element.getAttribute("aria-hidden") === "true" ||
|
|
61
|
+
element.getAttribute("data-state") === "closed" ||
|
|
62
|
+
/\b(is-)?(closed|closing|hidden)\b/i.test(classes)
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isRendered(element: Element): boolean {
|
|
67
|
+
if (typeof window === "undefined") return false;
|
|
68
|
+
const node = element as HTMLElement;
|
|
69
|
+
const style = window.getComputedStyle ? window.getComputedStyle(node) : null;
|
|
70
|
+
if (
|
|
71
|
+
style &&
|
|
72
|
+
(style.display === "none" ||
|
|
73
|
+
style.visibility === "hidden" ||
|
|
74
|
+
style.opacity === "0" ||
|
|
75
|
+
style.pointerEvents === "none")
|
|
76
|
+
) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
const rect = node.getBoundingClientRect?.();
|
|
80
|
+
return Boolean(rect && rect.width > 0 && rect.height > 0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function detectCartDrawerOpen(root: ParentNode = document): boolean {
|
|
84
|
+
if (typeof document === "undefined") return false;
|
|
85
|
+
const bodyClasses = document.body ? classNameOf(document.body) : "";
|
|
86
|
+
if (BODY_OPEN_PATTERN.test(bodyClasses)) return true;
|
|
87
|
+
|
|
88
|
+
for (const selector of CART_DRAWER_SELECTORS) {
|
|
89
|
+
const candidates = Array.from(root.querySelectorAll(selector));
|
|
90
|
+
for (const element of candidates) {
|
|
91
|
+
if (isExplicitlyClosed(element)) continue;
|
|
92
|
+
if (!hasOpenHint(element)) continue;
|
|
93
|
+
if (isRendered(element)) return true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function installCartDrawerOpenListener(
|
|
101
|
+
onChange: (open: boolean) => void,
|
|
102
|
+
): () => void {
|
|
103
|
+
if (
|
|
104
|
+
typeof document === "undefined" ||
|
|
105
|
+
typeof window === "undefined" ||
|
|
106
|
+
typeof MutationObserver === "undefined" ||
|
|
107
|
+
!document.documentElement
|
|
108
|
+
) {
|
|
109
|
+
return () => {};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let current = detectCartDrawerOpen();
|
|
113
|
+
|
|
114
|
+
let timeoutId: number | null = null;
|
|
115
|
+
const check = () => {
|
|
116
|
+
timeoutId = null;
|
|
117
|
+
const next = detectCartDrawerOpen();
|
|
118
|
+
if (next === current) return;
|
|
119
|
+
current = next;
|
|
120
|
+
onChange(next);
|
|
121
|
+
};
|
|
122
|
+
const schedule = () => {
|
|
123
|
+
if (timeoutId !== null) return;
|
|
124
|
+
timeoutId = window.setTimeout(check, 80);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const observer = new MutationObserver(schedule);
|
|
128
|
+
observer.observe(document.documentElement, {
|
|
129
|
+
attributes: true,
|
|
130
|
+
attributeFilter: [
|
|
131
|
+
"aria-expanded",
|
|
132
|
+
"aria-hidden",
|
|
133
|
+
"class",
|
|
134
|
+
"data-state",
|
|
135
|
+
"hidden",
|
|
136
|
+
"open",
|
|
137
|
+
"style",
|
|
138
|
+
],
|
|
139
|
+
childList: true,
|
|
140
|
+
subtree: true,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
document.addEventListener("click", schedule, true);
|
|
144
|
+
document.addEventListener("keyup", schedule, true);
|
|
145
|
+
|
|
146
|
+
return () => {
|
|
147
|
+
observer.disconnect();
|
|
148
|
+
document.removeEventListener("click", schedule, true);
|
|
149
|
+
document.removeEventListener("keyup", schedule, true);
|
|
150
|
+
if (timeoutId !== null) window.clearTimeout(timeoutId);
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function installSurfaceSignals(onChange: () => void): () => void {
|
|
155
|
+
const current = detectCartDrawerOpen();
|
|
156
|
+
setRuntimeSignal(CART_DRAWER_OPEN_SIGNAL, current);
|
|
157
|
+
const stop = installCartDrawerOpenListener((open) => {
|
|
158
|
+
setRuntimeSignal(CART_DRAWER_OPEN_SIGNAL, open);
|
|
159
|
+
onChange();
|
|
160
|
+
});
|
|
161
|
+
return () => {
|
|
162
|
+
stop();
|
|
163
|
+
clearRuntimeSignal(CART_DRAWER_OPEN_SIGNAL);
|
|
164
|
+
};
|
|
165
|
+
}
|