@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.
Files changed (246) hide show
  1. package/README.md +5 -0
  2. package/dist/bundle-report.json +584 -226
  3. package/dist/define.cjs +123 -0
  4. package/dist/define.cjs.map +7 -0
  5. package/dist/define.mjs +102 -0
  6. package/dist/define.mjs.map +7 -0
  7. package/dist/drip.cjs +19283 -0
  8. package/dist/drip.cjs.map +7 -0
  9. package/dist/drip.debug.js +1 -1
  10. package/dist/drip.debug.js.map +1 -1
  11. package/dist/drip.heatmap.js +1 -1
  12. package/dist/drip.heatmap.js.map +1 -1
  13. package/dist/drip.js +1 -1
  14. package/dist/drip.js.map +1 -1
  15. package/dist/drip.live.js +1 -1
  16. package/dist/drip.live.js.map +1 -1
  17. package/dist/drip.mjs +19267 -0
  18. package/dist/drip.mjs.map +7 -0
  19. package/dist/drip.replay-recorder.js +2 -0
  20. package/dist/drip.replay-recorder.js.map +1 -0
  21. package/dist/editor.js +147 -142
  22. package/dist/editor.js.map +4 -4
  23. package/dist/flags.cjs +1788 -0
  24. package/dist/flags.cjs.map +7 -0
  25. package/dist/flags.mjs +1765 -0
  26. package/dist/flags.mjs.map +7 -0
  27. package/dist/index.mjs +12 -0
  28. package/dist/index.mjs.map +2 -2
  29. package/dist/metafiles/define.cjs.meta.json +28 -0
  30. package/dist/metafiles/define.mjs.meta.json +32 -0
  31. package/dist/metafiles/drip.cjs.meta.json +3193 -0
  32. package/dist/metafiles/drip.debug.meta.json +2859 -574
  33. package/dist/metafiles/drip.heatmap.meta.json +2732 -460
  34. package/dist/metafiles/drip.live.meta.json +2732 -460
  35. package/dist/metafiles/drip.meta.json +2859 -574
  36. package/dist/metafiles/drip.mjs.meta.json +3235 -0
  37. package/dist/metafiles/drip.replay-recorder.meta.json +82 -0
  38. package/dist/metafiles/editor.meta.json +67 -27
  39. package/dist/metafiles/flags.cjs.meta.json +220 -0
  40. package/dist/metafiles/flags.mjs.meta.json +225 -0
  41. package/dist/metafiles/index.meta.json +2717 -491
  42. package/dist/metafiles/index.mjs.meta.json +14 -4
  43. package/dist/react/index.js +16636 -13533
  44. package/dist/react/index.js.map +4 -4
  45. package/dist/sri.json +20 -15
  46. package/dist/types/activation-runtime.d.ts +91 -0
  47. package/dist/types/analytics-runtime.d.ts +166 -0
  48. package/dist/types/antiflicker.d.ts +9 -3
  49. package/dist/types/assignment-cookie-sync.d.ts +31 -0
  50. package/dist/types/assignment-runtime.d.ts +257 -0
  51. package/dist/types/assignment-store.d.ts +4 -8
  52. package/dist/types/bucket.d.ts +1 -2
  53. package/dist/types/compact-store.d.ts +1 -0
  54. package/dist/types/config-runtime.d.ts +313 -0
  55. package/dist/types/consent-evaluation.d.ts +28 -0
  56. package/dist/types/css.d.ts +5 -3
  57. package/dist/types/debug-loader.d.ts +2 -0
  58. package/dist/types/decide-assignments.d.ts +31 -0
  59. package/dist/types/delivery-timing.d.ts +8 -0
  60. package/dist/types/early-interaction-capture.d.ts +79 -0
  61. package/dist/types/early-reassert.d.ts +32 -0
  62. package/dist/types/edge-runtime.d.ts +11 -0
  63. package/dist/types/editor/api.d.ts +3 -0
  64. package/dist/types/editor/controller.d.ts +3 -1
  65. package/dist/types/editor/publication-result.d.ts +11 -0
  66. package/dist/types/editor/types.d.ts +2 -1
  67. package/dist/types/editor/view-kit.d.ts +1 -1
  68. package/dist/types/endpoint.d.ts +8 -0
  69. package/dist/types/exclusion-groups.d.ts +4 -7
  70. package/dist/types/flags.d.ts +2 -0
  71. package/dist/types/goal-eligibility.d.ts +4 -0
  72. package/dist/types/goals.d.ts +10 -1
  73. package/dist/types/guards.d.ts +7 -7
  74. package/dist/types/handover.d.ts +12 -1
  75. package/dist/types/hash.d.ts +8 -4
  76. package/dist/types/hydration.d.ts +2 -1
  77. package/dist/types/index.d.ts +24 -128
  78. package/dist/types/init-test-helpers.d.ts +48 -0
  79. package/dist/types/inline-bootstrap.d.ts +26 -0
  80. package/dist/types/js.d.ts +3 -1
  81. package/dist/types/money.d.ts +12 -0
  82. package/dist/types/mutate.d.ts +45 -6
  83. package/dist/types/performance.d.ts +2 -0
  84. package/dist/types/persistence-decoders.d.ts +12 -0
  85. package/dist/types/public-api.d.ts +209 -0
  86. package/dist/types/qa-assistant-state.d.ts +34 -0
  87. package/dist/types/react/index.d.ts +28 -3
  88. package/dist/types/reassert-sentinel.d.ts +6 -0
  89. package/dist/types/redirect.d.ts +48 -0
  90. package/dist/types/replay-recorder-entry.d.ts +1 -0
  91. package/dist/types/replay-signal-state.d.ts +6 -0
  92. package/dist/types/replay-signals.d.ts +13 -0
  93. package/dist/types/replay.d.ts +110 -0
  94. package/dist/types/router.d.ts +12 -2
  95. package/dist/types/runtime-state.d.ts +166 -0
  96. package/dist/types/sanitize.d.ts +1 -0
  97. package/dist/types/shadow.d.ts +3 -0
  98. package/dist/types/shopify-cart.d.ts +25 -1
  99. package/dist/types/storefront-vitals.d.ts +24 -0
  100. package/dist/types/targeting-diagnostics.d.ts +1 -0
  101. package/dist/types/targeting.d.ts +16 -2
  102. package/dist/types/track.d.ts +15 -1
  103. package/dist/types/types.d.ts +115 -9
  104. package/dist/types/wait.d.ts +14 -1
  105. package/dist/types/weekday-schedule.d.ts +3 -0
  106. package/package.json +17 -8
  107. package/src/activation-runtime.ts +2737 -0
  108. package/src/analytics-runtime.ts +2175 -0
  109. package/src/antiflicker.test.ts +216 -44
  110. package/src/antiflicker.ts +103 -28
  111. package/src/artifact-concurrency.test.ts +1091 -0
  112. package/src/assignment-cookie-sync.test.ts +202 -0
  113. package/src/assignment-cookie-sync.ts +189 -0
  114. package/src/assignment-runtime.ts +3204 -0
  115. package/src/assignment-store.test.ts +56 -0
  116. package/src/assignment-store.ts +7 -38
  117. package/src/bucket.ts +3 -18
  118. package/src/commerce-analytics-lifecycle.test.ts +75 -15
  119. package/src/commerce-config-lifecycle.test.ts +81 -1
  120. package/src/commerce-price-preserve.test.ts +133 -0
  121. package/src/commerce-price.test.ts +97 -0
  122. package/src/commerce-price.ts +74 -20
  123. package/src/commerce-shipping.test.ts +53 -0
  124. package/src/commerce-shipping.ts +5 -11
  125. package/src/compact-store.ts +15 -0
  126. package/src/config-fetch.test.ts +480 -9
  127. package/src/config-runtime.ts +2071 -0
  128. package/src/consent-evaluation-borlabs.test.ts +41 -0
  129. package/src/consent-evaluation-cookiescript.test.ts +50 -0
  130. package/src/consent-evaluation.ts +151 -0
  131. package/src/css.test.ts +30 -0
  132. package/src/css.ts +120 -6
  133. package/src/cwv.test.ts +60 -77
  134. package/src/debug-loader.test.ts +214 -0
  135. package/src/debug-loader.ts +41 -0
  136. package/src/decide-assignments.test.ts +196 -0
  137. package/src/decide-assignments.ts +204 -0
  138. package/src/delivery-timing.test.ts +104 -0
  139. package/src/delivery-timing.ts +89 -0
  140. package/src/early-interaction-capture.test.ts +451 -0
  141. package/src/early-interaction-capture.ts +230 -0
  142. package/src/early-reassert.test.ts +207 -0
  143. package/src/early-reassert.ts +152 -0
  144. package/src/edge-adoption.test.ts +435 -0
  145. package/src/edge-runtime.ts +30 -0
  146. package/src/editor/api.test.ts +109 -0
  147. package/src/editor/api.ts +34 -3
  148. package/src/editor/controller.test.ts +297 -14
  149. package/src/editor/controller.ts +69 -5
  150. package/src/editor/index.test.ts +2 -0
  151. package/src/editor/index.ts +1 -0
  152. package/src/editor/publication-result.ts +58 -0
  153. package/src/editor/types.ts +10 -1
  154. package/src/editor/view-kit.ts +2 -1
  155. package/src/editor/view.test.ts +108 -0
  156. package/src/editor/view.ts +49 -7
  157. package/src/endpoint.test.ts +15 -0
  158. package/src/endpoint.ts +10 -0
  159. package/src/exclusion-groups.ts +8 -18
  160. package/src/flags.test.ts +88 -1
  161. package/src/flags.ts +29 -29
  162. package/src/global-runtime-reconciler.test.ts +271 -0
  163. package/src/goal-eligibility.test.ts +29 -0
  164. package/src/goal-eligibility.ts +17 -0
  165. package/src/goals.test.ts +144 -1
  166. package/src/goals.ts +97 -18
  167. package/src/guards.test.ts +99 -1
  168. package/src/guards.ts +34 -15
  169. package/src/handover-cache-order.test.ts +6 -3
  170. package/src/handover.test.ts +154 -8
  171. package/src/handover.ts +122 -12
  172. package/src/hash.ts +29 -54
  173. package/src/holdout.test.ts +26 -0
  174. package/src/hydration.test.ts +73 -2
  175. package/src/hydration.ts +18 -9
  176. package/src/index.ts +130 -7287
  177. package/src/init-analytics.test.ts +1694 -0
  178. package/src/init-anti-flicker.test.ts +1211 -0
  179. package/src/init-cart-attributes.test.ts +2003 -0
  180. package/src/init-config-resolution.test.ts +999 -0
  181. package/src/init-consent.test.ts +971 -0
  182. package/src/init-empty.test.ts +11 -5406
  183. package/src/init-integrations.test.ts +846 -0
  184. package/src/init-page-activation.test.ts +5064 -0
  185. package/src/init-qa-loading.test.ts +740 -0
  186. package/src/init-redirects.test.ts +2156 -0
  187. package/src/init-test-helpers.ts +287 -0
  188. package/src/inline-bootstrap.test.ts +184 -0
  189. package/src/inline-bootstrap.ts +73 -0
  190. package/src/js.test.ts +51 -1
  191. package/src/js.ts +86 -2
  192. package/src/money.test.ts +55 -1
  193. package/src/money.ts +104 -2
  194. package/src/money.web-parity.contract.test.ts +85 -0
  195. package/src/mutate.test.ts +474 -10
  196. package/src/mutate.ts +739 -230
  197. package/src/page-trigger.test.ts +218 -8
  198. package/src/performance.ts +10 -0
  199. package/src/persistence-decoders.ts +18 -0
  200. package/src/public-api.ts +1828 -0
  201. package/src/qa-assistant-state.test.ts +249 -3
  202. package/src/qa-assistant-state.ts +146 -6
  203. package/src/qa-assistant-view.test.ts +220 -2
  204. package/src/qa-assistant-view.ts +102 -5
  205. package/src/react/index.test.tsx +37 -0
  206. package/src/react/index.tsx +19 -3
  207. package/src/reassert-sentinel.test.ts +155 -1
  208. package/src/reassert-sentinel.ts +168 -8
  209. package/src/redirect.test.ts +541 -0
  210. package/src/redirect.ts +572 -0
  211. package/src/replay-recorder-entry.ts +115 -0
  212. package/src/replay-signal-state.ts +100 -0
  213. package/src/replay-signals.test.ts +249 -0
  214. package/src/replay-signals.ts +151 -0
  215. package/src/replay.test.ts +764 -0
  216. package/src/replay.ts +514 -0
  217. package/src/router.test.ts +61 -2
  218. package/src/router.ts +50 -12
  219. package/src/runtime-state.ts +507 -0
  220. package/src/runtime.test.ts +174 -7
  221. package/src/runtime.ts +88 -14
  222. package/src/sanitize.test.ts +34 -0
  223. package/src/sanitize.ts +23 -12
  224. package/src/shadow.test.ts +187 -0
  225. package/src/shadow.ts +99 -0
  226. package/src/shopify-cart.test.ts +733 -23
  227. package/src/shopify-cart.ts +402 -49
  228. package/src/storefront-vitals.test.ts +217 -0
  229. package/src/storefront-vitals.ts +206 -0
  230. package/src/surface-signals.ts +6 -0
  231. package/src/targeting-conformance.test.ts +9 -0
  232. package/src/targeting-diagnostics.ts +38 -8
  233. package/src/targeting-parity-corpus.json +559 -0
  234. package/src/targeting-parity-corpus.test.ts +18 -0
  235. package/src/targeting.detailed.test.ts +105 -0
  236. package/src/targeting.test.ts +211 -0
  237. package/src/targeting.ts +88 -207
  238. package/src/track.test.ts +147 -0
  239. package/src/track.ts +125 -26
  240. package/src/types.ts +120 -13
  241. package/src/wait.test.ts +23 -0
  242. package/src/wait.ts +129 -13
  243. package/src/weekday-delivery.test.ts +56 -0
  244. package/src/weekday-schedule-parity-corpus.json +92 -0
  245. package/src/weekday-schedule.test.ts +69 -0
  246. package/src/weekday-schedule.ts +20 -0
@@ -1,7 +1,15 @@
1
1
  // @vitest-environment jsdom
2
2
 
3
3
  import { afterEach, describe, expect, it, vi } from "vitest";
4
- import { applyAntiFlicker, removeAntiFlicker, resolveAntiFlickerCss } from "./antiflicker";
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).toContain(".hero h1,.cta");
26
- expect(css).toContain("visibility: hidden");
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
- const css = resolveAntiFlickerCss("full");
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("binds reveal timers to their owner document after the global DOM tears down", () => {
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
- const ownerDocument = document;
40
- expect(ownerDocument.documentElement.classList.contains("drip-af")).toBe(true);
41
- expect(ownerDocument.getElementById("drip-antiflicker")).not.toBeNull();
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(document.getElementById("drip-antiflicker")).toBeNull();
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(document.documentElement.classList.contains("drip-af-reveal")).toBe(true);
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 = document.getElementById("drip-antiflicker");
244
+ const originalStyle = style();
74
245
 
75
246
  removeAntiFlicker();
76
- expect(document.documentElement.classList.contains("drip-af-reveal")).toBe(true);
247
+ expect(originalStyle?.textContent).not.toMatch(/\bopacity\s*:/);
77
248
  applyAntiFlicker("full", 200);
78
249
 
79
- expect(document.getElementById("drip-antiflicker")).toBe(originalStyle);
80
- expect(document.documentElement.classList.contains("drip-af")).toBe(true);
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(document.documentElement.classList.contains("drip-af-reveal")).toBe(true);
253
+ expect(style()?.textContent).toContain("opacity: 0");
85
254
  vi.advanceTimersByTime(120);
86
- expect(document.getElementById("drip-antiflicker")).toBeNull();
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
- removeAntiFlicker();
120
- vi.advanceTimersByTime(80);
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
  });
@@ -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
- "html.drip-af { opacity: 0 !important; }" +
10
- " html.drip-af-reveal { transition: opacity 0.05s ease-in !important; }";
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) antiFlickerOwnerDocument = null;
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
- selectorList + " { visibility: hidden !important; }" +
62
- " .drip-af-reveal " + selectorList + " { visibility: visible !important; transition: visibility 0s, opacity 0.05s ease-in !important; }"
83
+ scopedSelectors.join(",") +
84
+ " { visibility: visible !important; transition: visibility 0s, opacity 0.05s ease-in !important; }"
63
85
  );
64
86
  }
65
87
 
66
- return FULL_PAGE_CSS;
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, begins a CSS transition to fade
77
- * content in gradually rather than a sudden flash at timeout expiry.
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
- ): void {
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
- // Progressive reveal: at 80% of timeout, begin transition before hard timeout
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
- // Add reveal class to start transition (content fades in over 50ms)
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
- // Remove style (if present) and class after transition completes
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) antiFlickerOwnerDocument = null;
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