@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/antiflicker.test.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
|
|
3
3
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
applyAntiFlicker,
|
|
6
|
+
bootedTooLateForAntiFlicker,
|
|
7
|
+
removeAntiFlicker,
|
|
8
|
+
resolveAntiFlickerCss,
|
|
9
|
+
} from "./antiflicker";
|
|
10
|
+
|
|
11
|
+
const style = (ownerDocument: Document = document): HTMLStyleElement | null =>
|
|
12
|
+
ownerDocument.getElementById("drip-antiflicker") as HTMLStyleElement | null;
|
|
5
13
|
|
|
6
14
|
afterEach(() => {
|
|
7
15
|
vi.unstubAllGlobals();
|
|
@@ -15,81 +23,252 @@ afterEach(() => {
|
|
|
15
23
|
});
|
|
16
24
|
|
|
17
25
|
describe("anti-flicker css resolution", () => {
|
|
26
|
+
const stubNavigationTiming = (
|
|
27
|
+
now: number,
|
|
28
|
+
navigation?: Pick<PerformanceNavigationTiming, "startTime" | "domContentLoadedEventEnd">,
|
|
29
|
+
paintEntries: Pick<PerformanceEntry, "name" | "startTime">[] = [],
|
|
30
|
+
) => {
|
|
31
|
+
vi.stubGlobal("performance", {
|
|
32
|
+
now: () => now,
|
|
33
|
+
getEntriesByType: (type: string) => {
|
|
34
|
+
if (type === "paint") return paintEntries;
|
|
35
|
+
return type === "navigation" && navigation ? [navigation] : [];
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
it("skips runtime hiding after a recorded paint even inside the timing thresholds", () => {
|
|
41
|
+
vi.useFakeTimers();
|
|
42
|
+
stubNavigationTiming(
|
|
43
|
+
300,
|
|
44
|
+
{ startTime: 0, domContentLoadedEventEnd: 150 },
|
|
45
|
+
[{ name: "first-paint", startTime: 100 }],
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
expect(applyAntiFlicker(
|
|
49
|
+
"full", 100, undefined, undefined, bootedTooLateForAntiFlicker(),
|
|
50
|
+
)).toBe(false);
|
|
51
|
+
expect(style()).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("skips runtime hiding when the SDK boots after the first-paint window", () => {
|
|
55
|
+
vi.useFakeTimers();
|
|
56
|
+
stubNavigationTiming(500, { startTime: 0, domContentLoadedEventEnd: 100 });
|
|
57
|
+
|
|
58
|
+
expect(applyAntiFlicker(
|
|
59
|
+
"full", 100, undefined, undefined, bootedTooLateForAntiFlicker(),
|
|
60
|
+
)).toBe(false);
|
|
61
|
+
expect(style()).toBeNull();
|
|
62
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("skips runtime hiding more than two seconds after navigation starts", () => {
|
|
66
|
+
vi.useFakeTimers();
|
|
67
|
+
stubNavigationTiming(2_001, { startTime: 0, domContentLoadedEventEnd: 0 });
|
|
68
|
+
|
|
69
|
+
expect(applyAntiFlicker(
|
|
70
|
+
"full", 100, undefined, undefined, bootedTooLateForAntiFlicker(),
|
|
71
|
+
)).toBe(false);
|
|
72
|
+
expect(style()).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("keeps runtime hiding unchanged during an early boot", () => {
|
|
76
|
+
vi.useFakeTimers();
|
|
77
|
+
stubNavigationTiming(200, { startTime: 0, domContentLoadedEventEnd: 0 });
|
|
78
|
+
|
|
79
|
+
expect(applyAntiFlicker(
|
|
80
|
+
"full", 100, undefined, undefined, bootedTooLateForAntiFlicker(),
|
|
81
|
+
)).toBe(true);
|
|
82
|
+
expect(style()?.textContent).toContain("opacity: 0");
|
|
83
|
+
vi.advanceTimersByTime(100);
|
|
84
|
+
expect(style()?.textContent).not.toMatch(/\bopacity\s*:/);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("falls back to navigation thresholds without Paint Timing API support", () => {
|
|
88
|
+
vi.useFakeTimers();
|
|
89
|
+
vi.stubGlobal("performance", {
|
|
90
|
+
now: () => 200,
|
|
91
|
+
getEntriesByType: undefined,
|
|
92
|
+
timing: {
|
|
93
|
+
navigationStart: Date.now() - 200,
|
|
94
|
+
domContentLoadedEventEnd: 0,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(applyAntiFlicker(
|
|
99
|
+
"full", 100, undefined, undefined, bootedTooLateForAntiFlicker(),
|
|
100
|
+
)).toBe(true);
|
|
101
|
+
expect(style()?.textContent).toContain("opacity: 0");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("defaults safely to current hiding behavior without navigation timing entries", () => {
|
|
105
|
+
vi.useFakeTimers();
|
|
106
|
+
stubNavigationTiming(5_000);
|
|
107
|
+
|
|
108
|
+
expect(applyAntiFlicker(
|
|
109
|
+
"full", 100, undefined, undefined, bootedTooLateForAntiFlicker(),
|
|
110
|
+
)).toBe(true);
|
|
111
|
+
expect(style()?.textContent).toContain("opacity: 0");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("leaves the edge pre-paint path unaffected when the runtime guard is not requested", () => {
|
|
115
|
+
vi.useFakeTimers();
|
|
116
|
+
stubNavigationTiming(5_000, { startTime: 0, domContentLoadedEventEnd: 100 });
|
|
117
|
+
|
|
118
|
+
expect(applyAntiFlicker("full", 100)).toBe(true);
|
|
119
|
+
expect(style()?.textContent).toContain("opacity: 0");
|
|
120
|
+
});
|
|
121
|
+
|
|
18
122
|
it("does not fall back to full-page hiding for scoped mode without selectors", () => {
|
|
19
123
|
expect(resolveAntiFlickerCss("scoped")).toBeNull();
|
|
20
124
|
expect(resolveAntiFlickerCss("scoped", [])).toBeNull();
|
|
21
125
|
});
|
|
22
126
|
|
|
23
|
-
it("builds scoped css when selectors are available", () => {
|
|
127
|
+
it("builds class-free scoped css when selectors are available", () => {
|
|
24
128
|
const css = resolveAntiFlickerCss("scoped", [".hero h1", ".cta"]);
|
|
25
|
-
expect(css).
|
|
26
|
-
expect(css).toContain("
|
|
129
|
+
expect(css).toBe(".hero h1,.cta { visibility: hidden !important; }");
|
|
130
|
+
expect(css).not.toContain("drip-af");
|
|
27
131
|
});
|
|
28
132
|
|
|
29
|
-
it("preserves explicit full-page hiding", () => {
|
|
30
|
-
|
|
31
|
-
expect(css).toContain("html.drip-af");
|
|
32
|
-
expect(css).toContain("opacity: 0");
|
|
133
|
+
it("preserves explicit full-page hiding without a root class", () => {
|
|
134
|
+
expect(resolveAntiFlickerCss("full")).toBe("html { opacity: 0 !important; }");
|
|
33
135
|
});
|
|
34
136
|
|
|
35
|
-
it("
|
|
137
|
+
it("uses the style element as the only DOM footprint throughout the lifecycle", () => {
|
|
138
|
+
vi.useFakeTimers();
|
|
139
|
+
const root = document.documentElement;
|
|
140
|
+
root.setAttribute("lang", "en");
|
|
141
|
+
root.setAttribute("data-framework", "remix");
|
|
142
|
+
const initialAttributes = Array.from(root.attributes, ({ name, value }) => [name, value]);
|
|
143
|
+
const initialClassName = root.className;
|
|
144
|
+
|
|
145
|
+
applyAntiFlicker("scoped", 100, [".hero", ".cta"]);
|
|
146
|
+
expect(style()?.textContent).toBe(".hero,.cta { visibility: hidden !important; }");
|
|
147
|
+
expect(root.className).toBe(initialClassName);
|
|
148
|
+
expect(Array.from(root.attributes, ({ name, value }) => [name, value])).toEqual(initialAttributes);
|
|
149
|
+
|
|
150
|
+
vi.advanceTimersByTime(80);
|
|
151
|
+
expect(style()?.textContent).toBe(
|
|
152
|
+
".hero,.cta { visibility: visible !important; transition: visibility 0s, opacity 0.05s ease-in !important; }",
|
|
153
|
+
);
|
|
154
|
+
expect(root.className).toBe(initialClassName);
|
|
155
|
+
expect(Array.from(root.attributes, ({ name, value }) => [name, value])).toEqual(initialAttributes);
|
|
156
|
+
|
|
157
|
+
vi.advanceTimersByTime(100);
|
|
158
|
+
expect(style()).toBeNull();
|
|
159
|
+
expect(root.className).toBe(initialClassName);
|
|
160
|
+
expect(Array.from(root.attributes, ({ name, value }) => [name, value])).toEqual(initialAttributes);
|
|
161
|
+
|
|
162
|
+
root.removeAttribute("lang");
|
|
163
|
+
root.removeAttribute("data-framework");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("keeps full mode hidden at progressive reveal and releases it at hard timeout", () => {
|
|
36
167
|
vi.useFakeTimers();
|
|
37
168
|
applyAntiFlicker("full", 100);
|
|
169
|
+
expect(style()?.textContent).toBe("html { opacity: 0 !important; }");
|
|
38
170
|
|
|
39
|
-
|
|
40
|
-
expect(
|
|
41
|
-
|
|
171
|
+
vi.advanceTimersByTime(80);
|
|
172
|
+
expect(style()?.textContent).toBe(
|
|
173
|
+
"html { opacity: 0 !important; transition: opacity 0.05s ease-in !important; }",
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
vi.advanceTimersByTime(19);
|
|
177
|
+
expect(style()?.textContent).toContain("opacity: 0 !important");
|
|
178
|
+
|
|
179
|
+
vi.advanceTimersByTime(1);
|
|
180
|
+
expect(style()?.textContent).toBe(
|
|
181
|
+
"html { transition: opacity 0.05s ease-in !important; }",
|
|
182
|
+
);
|
|
183
|
+
expect(style()?.textContent).not.toMatch(/\bopacity\s*:/);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("restores natural full-page opacity when removed before hard timeout", () => {
|
|
187
|
+
vi.useFakeTimers();
|
|
188
|
+
applyAntiFlicker("full", 1_000);
|
|
189
|
+
|
|
190
|
+
removeAntiFlicker();
|
|
191
|
+
|
|
192
|
+
expect(style()?.textContent).toBe(
|
|
193
|
+
"html { transition: opacity 0.05s ease-in !important; }",
|
|
194
|
+
);
|
|
195
|
+
expect(style()?.textContent).not.toMatch(/\bopacity\s*:/);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("preserves custom CSS until progressive reveal, then releases it", () => {
|
|
199
|
+
vi.useFakeTimers();
|
|
200
|
+
applyAntiFlicker("custom", 100, undefined, "body { filter: blur(2px); }");
|
|
201
|
+
expect(style()?.textContent).toBe("body { filter: blur(2px); }");
|
|
202
|
+
vi.advanceTimersByTime(80);
|
|
203
|
+
expect(style()?.textContent).toBe("");
|
|
204
|
+
vi.advanceTimersByTime(100);
|
|
205
|
+
expect(style()).toBeNull();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("removes legacy root classes defensively on arm and cleanup", () => {
|
|
209
|
+
vi.useFakeTimers();
|
|
210
|
+
document.documentElement.className = "shop drip-af drip-af-reveal";
|
|
211
|
+
applyAntiFlicker("full", 100);
|
|
212
|
+
expect(document.documentElement.className).toBe("shop");
|
|
213
|
+
document.documentElement.classList.add("drip-af", "drip-af-reveal");
|
|
214
|
+
removeAntiFlicker();
|
|
215
|
+
expect(document.documentElement.className).toBe("shop");
|
|
216
|
+
});
|
|
42
217
|
|
|
218
|
+
it("binds reveal timers to their owner document after the global DOM tears down", () => {
|
|
219
|
+
vi.useFakeTimers();
|
|
220
|
+
applyAntiFlicker("full", 100);
|
|
221
|
+
const ownerDocument = document;
|
|
43
222
|
vi.stubGlobal("document", undefined);
|
|
44
223
|
expect(() => vi.advanceTimersByTime(180)).not.toThrow();
|
|
45
|
-
|
|
46
|
-
expect(ownerDocument.documentElement.classList.contains("drip-af")).toBe(false);
|
|
47
|
-
expect(ownerDocument.documentElement.classList.contains("drip-af-reveal")).toBe(false);
|
|
48
|
-
expect(ownerDocument.getElementById("drip-antiflicker")).toBeNull();
|
|
224
|
+
expect(style(ownerDocument)).toBeNull();
|
|
49
225
|
});
|
|
50
226
|
|
|
51
227
|
it("clears old timers and supports a fresh anti-flicker cycle after cleanup", () => {
|
|
52
228
|
vi.useFakeTimers();
|
|
53
229
|
applyAntiFlicker("full", 1_000);
|
|
54
|
-
|
|
55
230
|
removeAntiFlicker();
|
|
56
231
|
vi.advanceTimersByTime(80);
|
|
57
|
-
expect(
|
|
58
|
-
expect(document.documentElement.classList.contains("drip-af-reveal")).toBe(false);
|
|
232
|
+
expect(style()).toBeNull();
|
|
59
233
|
|
|
60
234
|
applyAntiFlicker("full", 200);
|
|
61
235
|
vi.advanceTimersByTime(160);
|
|
62
|
-
expect(
|
|
236
|
+
expect(style()?.textContent).toContain("opacity: 0");
|
|
63
237
|
vi.advanceTimersByTime(120);
|
|
64
|
-
|
|
65
|
-
expect(document.getElementById("drip-antiflicker")).toBeNull();
|
|
66
|
-
expect(document.documentElement.classList.contains("drip-af")).toBe(false);
|
|
67
|
-
expect(document.documentElement.classList.contains("drip-af-reveal")).toBe(false);
|
|
238
|
+
expect(style()).toBeNull();
|
|
68
239
|
});
|
|
69
240
|
|
|
70
241
|
it("re-arms immediately when re-init starts during reveal cleanup", () => {
|
|
71
242
|
vi.useFakeTimers();
|
|
72
243
|
applyAntiFlicker("full", 1_000);
|
|
73
|
-
const originalStyle =
|
|
244
|
+
const originalStyle = style();
|
|
74
245
|
|
|
75
246
|
removeAntiFlicker();
|
|
76
|
-
expect(
|
|
247
|
+
expect(originalStyle?.textContent).not.toMatch(/\bopacity\s*:/);
|
|
77
248
|
applyAntiFlicker("full", 200);
|
|
78
249
|
|
|
79
|
-
expect(
|
|
80
|
-
expect(
|
|
81
|
-
expect(document.documentElement.classList.contains("drip-af-reveal")).toBe(false);
|
|
82
|
-
|
|
250
|
+
expect(style()).toBe(originalStyle);
|
|
251
|
+
expect(style()?.textContent).toContain("opacity: 0");
|
|
83
252
|
vi.advanceTimersByTime(160);
|
|
84
|
-
expect(
|
|
253
|
+
expect(style()?.textContent).toContain("opacity: 0");
|
|
85
254
|
vi.advanceTimersByTime(120);
|
|
86
|
-
expect(
|
|
255
|
+
expect(style()).toBeNull();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("keeps an existing armed barrier and its timers", () => {
|
|
259
|
+
vi.useFakeTimers();
|
|
260
|
+
applyAntiFlicker("full", 100);
|
|
261
|
+
const originalStyle = style();
|
|
262
|
+
applyAntiFlicker("scoped", 1_000, [".hero"]);
|
|
263
|
+
expect(style()).toBe(originalStyle);
|
|
264
|
+
expect(style()?.textContent).toContain("opacity: 0");
|
|
265
|
+
vi.advanceTimersByTime(80);
|
|
266
|
+
expect(style()?.textContent).toContain("opacity: 0");
|
|
87
267
|
});
|
|
88
268
|
|
|
89
269
|
it("is safe when invoked without a DOM", () => {
|
|
90
270
|
vi.useFakeTimers();
|
|
91
271
|
vi.stubGlobal("document", undefined);
|
|
92
|
-
|
|
93
272
|
expect(() => applyAntiFlicker("full", 100)).not.toThrow();
|
|
94
273
|
expect(() => removeAntiFlicker()).not.toThrow();
|
|
95
274
|
expect(vi.getTimerCount()).toBe(0);
|
|
@@ -100,13 +279,9 @@ describe("anti-flicker css resolution", () => {
|
|
|
100
279
|
applyAntiFlicker("full", 1_000);
|
|
101
280
|
const ownerDocument = document;
|
|
102
281
|
vi.stubGlobal("document", undefined);
|
|
103
|
-
|
|
104
282
|
expect(() => removeAntiFlicker()).not.toThrow();
|
|
105
283
|
vi.advanceTimersByTime(80);
|
|
106
|
-
|
|
107
|
-
expect(ownerDocument.getElementById("drip-antiflicker")).toBeNull();
|
|
108
|
-
expect(ownerDocument.documentElement.classList.contains("drip-af")).toBe(false);
|
|
109
|
-
expect(ownerDocument.documentElement.classList.contains("drip-af-reveal")).toBe(false);
|
|
284
|
+
expect(style(ownerDocument)).toBeNull();
|
|
110
285
|
});
|
|
111
286
|
|
|
112
287
|
it("cleans its owner when the ambient document changes", () => {
|
|
@@ -116,12 +291,9 @@ describe("anti-flicker css resolution", () => {
|
|
|
116
291
|
const replacementDocument = document.implementation.createHTMLDocument("replacement");
|
|
117
292
|
vi.stubGlobal("document", replacementDocument);
|
|
118
293
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
expect(ownerDocument.getElementById("drip-antiflicker")).toBeNull();
|
|
123
|
-
expect(ownerDocument.documentElement.classList.contains("drip-af")).toBe(false);
|
|
124
|
-
expect(ownerDocument.documentElement.classList.contains("drip-af-reveal")).toBe(false);
|
|
294
|
+
applyAntiFlicker("full", 100);
|
|
295
|
+
expect(style(ownerDocument)).toBeNull();
|
|
296
|
+
expect(style(replacementDocument)?.textContent).toContain("opacity: 0");
|
|
125
297
|
expect(replacementDocument.documentElement.className).toBe("");
|
|
126
298
|
});
|
|
127
299
|
});
|
package/src/antiflicker.ts
CHANGED
|
@@ -4,15 +4,22 @@ const STYLE_ID = "drip-antiflicker";
|
|
|
4
4
|
const DEFAULT_TIMEOUT_MS = 1200;
|
|
5
5
|
const PROGRESSIVE_REVEAL_RATIO = 0.8; // Start progressive reveal at 80% of timeout
|
|
6
6
|
const REVEAL_CLEANUP_MS = 80;
|
|
7
|
+
const MAX_NAVIGATION_AGE_MS = 2_000;
|
|
8
|
+
const MAX_POST_DOM_CONTENT_LOADED_MS = 250;
|
|
9
|
+
const PAINT_TIMING_EPSILON_MS = 50;
|
|
7
10
|
|
|
8
|
-
const FULL_PAGE_CSS =
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
+
const FULL_PAGE_CSS = "html { opacity: 0 !important; }";
|
|
12
|
+
const FULL_PAGE_PROGRESSIVE_CSS =
|
|
13
|
+
"html { opacity: 0 !important; transition: opacity 0.05s ease-in !important; }";
|
|
14
|
+
const FULL_PAGE_REVEAL_CSS =
|
|
15
|
+
"html { transition: opacity 0.05s ease-in !important; }";
|
|
11
16
|
|
|
12
17
|
let progressiveRevealTimer: ReturnType<typeof setTimeout> | null = null;
|
|
13
18
|
let hardRevealTimer: ReturnType<typeof setTimeout> | null = null;
|
|
14
19
|
let cleanupTimer: ReturnType<typeof setTimeout> | null = null;
|
|
15
20
|
let antiFlickerOwnerDocument: Document | null = null;
|
|
21
|
+
let antiFlickerProgressiveCss = "";
|
|
22
|
+
let antiFlickerRevealCss = "";
|
|
16
23
|
|
|
17
24
|
function activeDocument(): Document | null {
|
|
18
25
|
if (typeof document === "undefined" || !document.documentElement) return null;
|
|
@@ -37,7 +44,11 @@ function clearTimers(): void {
|
|
|
37
44
|
function cleanupDocumentImmediately(ownerDocument: Document): void {
|
|
38
45
|
ownerDocument.documentElement?.classList.remove("drip-af", "drip-af-reveal");
|
|
39
46
|
ownerDocument.getElementById(STYLE_ID)?.remove();
|
|
40
|
-
if (antiFlickerOwnerDocument === ownerDocument)
|
|
47
|
+
if (antiFlickerOwnerDocument === ownerDocument) {
|
|
48
|
+
antiFlickerOwnerDocument = null;
|
|
49
|
+
antiFlickerProgressiveCss = "";
|
|
50
|
+
antiFlickerRevealCss = "";
|
|
51
|
+
}
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
export function resolveAntiFlickerCss(
|
|
@@ -57,13 +68,37 @@ export function resolveAntiFlickerCss(
|
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
const selectorList = scopedSelectors.join(",");
|
|
71
|
+
return selectorList + " { visibility: hidden !important; }";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return FULL_PAGE_CSS;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolveAntiFlickerProgressiveCss(
|
|
78
|
+
mode: AntiFlickerMode,
|
|
79
|
+
scopedSelectors?: string[],
|
|
80
|
+
): string {
|
|
81
|
+
if (mode === "scoped" && scopedSelectors?.length) {
|
|
60
82
|
return (
|
|
61
|
-
|
|
62
|
-
"
|
|
83
|
+
scopedSelectors.join(",") +
|
|
84
|
+
" { visibility: visible !important; transition: visibility 0s, opacity 0.05s ease-in !important; }"
|
|
63
85
|
);
|
|
64
86
|
}
|
|
65
87
|
|
|
66
|
-
return
|
|
88
|
+
if (mode === "full") return FULL_PAGE_PROGRESSIVE_CSS;
|
|
89
|
+
return "";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function resolveAntiFlickerRevealCss(
|
|
93
|
+
mode: AntiFlickerMode,
|
|
94
|
+
scopedSelectors?: string[],
|
|
95
|
+
): string {
|
|
96
|
+
if (mode === "scoped") {
|
|
97
|
+
return resolveAntiFlickerProgressiveCss(mode, scopedSelectors);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (mode === "full") return FULL_PAGE_REVEAL_CSS;
|
|
101
|
+
return "";
|
|
67
102
|
}
|
|
68
103
|
|
|
69
104
|
/**
|
|
@@ -73,50 +108,49 @@ export function resolveAntiFlickerCss(
|
|
|
73
108
|
* - "off": no-op.
|
|
74
109
|
* Scoped mode is a no-op when no selectors are available.
|
|
75
110
|
*
|
|
76
|
-
* Progressive reveal: at 80% of timeout,
|
|
77
|
-
*
|
|
111
|
+
* Progressive reveal: at 80% of timeout, scoped mode reveals its selectors.
|
|
112
|
+
* Full mode stays hidden and only pre-arms its opacity transition.
|
|
78
113
|
*/
|
|
79
114
|
export function applyAntiFlicker(
|
|
80
115
|
mode: AntiFlickerMode = "scoped",
|
|
81
116
|
timeoutMs: number = DEFAULT_TIMEOUT_MS,
|
|
82
117
|
scopedSelectors?: string[],
|
|
83
118
|
customCss?: string,
|
|
84
|
-
|
|
119
|
+
bootedTooLate = false,
|
|
120
|
+
): boolean {
|
|
121
|
+
if (bootedTooLate) return false;
|
|
85
122
|
const ownerDocument = activeDocument();
|
|
86
|
-
if (!ownerDocument) return;
|
|
123
|
+
if (!ownerDocument) return false;
|
|
124
|
+
const root = ownerDocument.documentElement;
|
|
125
|
+
// Remove state left by older SDK embeds without ever adding root classes.
|
|
126
|
+
root.classList.remove("drip-af", "drip-af-reveal");
|
|
87
127
|
const existingStyle = ownerDocument.getElementById(STYLE_ID);
|
|
88
128
|
// A live barrier owns its current timers. During the short reveal cleanup
|
|
89
129
|
// window, however, network config or re-init may need to arm a fresh barrier
|
|
90
130
|
// immediately against the same document.
|
|
91
|
-
if (existingStyle && cleanupTimer === null) return;
|
|
131
|
+
if (existingStyle && cleanupTimer === null) return true;
|
|
92
132
|
const css = resolveAntiFlickerCss(mode, scopedSelectors, customCss);
|
|
93
|
-
if (!css) return;
|
|
133
|
+
if (!css) return false;
|
|
94
134
|
clearTimers();
|
|
95
135
|
if (antiFlickerOwnerDocument && antiFlickerOwnerDocument !== ownerDocument) {
|
|
96
136
|
cleanupDocumentImmediately(antiFlickerOwnerDocument);
|
|
97
137
|
}
|
|
98
138
|
antiFlickerOwnerDocument = ownerDocument;
|
|
139
|
+
antiFlickerProgressiveCss = resolveAntiFlickerProgressiveCss(mode, scopedSelectors);
|
|
140
|
+
antiFlickerRevealCss = resolveAntiFlickerRevealCss(mode, scopedSelectors);
|
|
99
141
|
|
|
100
|
-
const root = ownerDocument.documentElement;
|
|
101
142
|
const style = existingStyle ?? ownerDocument.createElement("style");
|
|
102
143
|
style.id = STYLE_ID;
|
|
103
144
|
|
|
104
145
|
style.textContent = css;
|
|
105
|
-
root.classList.remove("drip-af-reveal");
|
|
106
|
-
if (mode === "full") {
|
|
107
|
-
root.classList.add("drip-af");
|
|
108
|
-
} else {
|
|
109
|
-
root.classList.remove("drip-af");
|
|
110
|
-
}
|
|
111
146
|
|
|
112
147
|
if (!style.isConnected) (ownerDocument.head || root).appendChild(style);
|
|
113
148
|
|
|
114
|
-
//
|
|
149
|
+
// At 80%, scoped mode reveals while full mode only pre-arms its transition.
|
|
115
150
|
const progressiveMs = Math.round(timeoutMs * PROGRESSIVE_REVEAL_RATIO);
|
|
116
151
|
progressiveRevealTimer = setTimeout(() => {
|
|
117
152
|
progressiveRevealTimer = null;
|
|
118
|
-
|
|
119
|
-
root.classList.add("drip-af-reveal");
|
|
153
|
+
style.textContent = antiFlickerProgressiveCss;
|
|
120
154
|
}, progressiveMs);
|
|
121
155
|
|
|
122
156
|
// Hard timeout: fully remove anti-flicker
|
|
@@ -124,21 +158,62 @@ export function applyAntiFlicker(
|
|
|
124
158
|
hardRevealTimer = null;
|
|
125
159
|
removeAntiFlickerFrom(ownerDocument);
|
|
126
160
|
}, timeoutMs);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Runtime-loaded SDKs must not introduce a hiding barrier after the browser
|
|
166
|
+
* has likely painted. Inline edge bootstrap callers deliberately do not opt
|
|
167
|
+
* into this guard because their barrier is installed synchronously pre-paint.
|
|
168
|
+
*/
|
|
169
|
+
export function bootedTooLateForAntiFlicker(): boolean {
|
|
170
|
+
if (typeof performance === "undefined") return false;
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const now = performance.now();
|
|
174
|
+
const paintEntries = performance.getEntriesByType?.("paint") ?? [];
|
|
175
|
+
if (paintEntries.some((entry) =>
|
|
176
|
+
(entry.name === "first-paint" || entry.name === "first-contentful-paint")
|
|
177
|
+
&& entry.startTime <= now - PAINT_TIMING_EPSILON_MS
|
|
178
|
+
)) return true;
|
|
179
|
+
|
|
180
|
+
const navigation = performance.getEntriesByType?.("navigation")?.[0] as
|
|
181
|
+
| PerformanceNavigationTiming
|
|
182
|
+
| undefined;
|
|
183
|
+
if (navigation) {
|
|
184
|
+
if (now - navigation.startTime > MAX_NAVIGATION_AGE_MS) return true;
|
|
185
|
+
return navigation.domContentLoadedEventEnd > 0
|
|
186
|
+
&& now - navigation.domContentLoadedEventEnd > MAX_POST_DOM_CONTENT_LOADED_MS;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const timing = performance.timing;
|
|
190
|
+
if (!timing?.navigationStart) return false;
|
|
191
|
+
const wallNow = Date.now();
|
|
192
|
+
if (wallNow - timing.navigationStart > MAX_NAVIGATION_AGE_MS) return true;
|
|
193
|
+
return timing.domContentLoadedEventEnd > 0
|
|
194
|
+
&& wallNow - timing.domContentLoadedEventEnd > MAX_POST_DOM_CONTENT_LOADED_MS;
|
|
195
|
+
} catch {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
127
198
|
}
|
|
128
199
|
|
|
129
200
|
function removeAntiFlickerFrom(ownerDocument: Document | null): void {
|
|
130
201
|
clearTimers();
|
|
131
202
|
if (!ownerDocument?.documentElement) return;
|
|
132
203
|
const root = ownerDocument.documentElement;
|
|
133
|
-
root.classList.remove("drip-af");
|
|
134
|
-
root.classList.add("drip-af-reveal");
|
|
204
|
+
root.classList.remove("drip-af", "drip-af-reveal");
|
|
135
205
|
const el = ownerDocument.getElementById(STYLE_ID);
|
|
136
|
-
|
|
206
|
+
if (el) el.textContent = antiFlickerRevealCss;
|
|
207
|
+
// Remove the style after the existing transition cleanup window completes.
|
|
137
208
|
cleanupTimer = setTimeout(() => {
|
|
138
209
|
cleanupTimer = null;
|
|
139
210
|
el?.remove();
|
|
140
|
-
root.classList.remove("drip-af-reveal");
|
|
141
|
-
if (antiFlickerOwnerDocument === ownerDocument)
|
|
211
|
+
root.classList.remove("drip-af", "drip-af-reveal");
|
|
212
|
+
if (antiFlickerOwnerDocument === ownerDocument) {
|
|
213
|
+
antiFlickerOwnerDocument = null;
|
|
214
|
+
antiFlickerProgressiveCss = "";
|
|
215
|
+
antiFlickerRevealCss = "";
|
|
216
|
+
}
|
|
142
217
|
}, REVEAL_CLEANUP_MS);
|
|
143
218
|
}
|
|
144
219
|
|