@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.
Files changed (227) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -0
  3. package/dist/_headers +3 -0
  4. package/dist/bundle-report.json +564 -0
  5. package/dist/drip.debug.js +2 -0
  6. package/dist/drip.debug.js.map +1 -0
  7. package/dist/drip.heatmap.js +2 -0
  8. package/dist/drip.heatmap.js.map +1 -0
  9. package/dist/drip.js +2 -0
  10. package/dist/drip.js.map +1 -0
  11. package/dist/drip.live.js +2 -0
  12. package/dist/drip.live.js.map +1 -0
  13. package/dist/editor.js +480 -0
  14. package/dist/editor.js.map +7 -0
  15. package/dist/index.mjs +645 -0
  16. package/dist/index.mjs.map +7 -0
  17. package/dist/metafiles/drip.debug.meta.json +973 -0
  18. package/dist/metafiles/drip.heatmap.meta.json +918 -0
  19. package/dist/metafiles/drip.live.meta.json +918 -0
  20. package/dist/metafiles/drip.meta.json +973 -0
  21. package/dist/metafiles/editor.meta.json +321 -0
  22. package/dist/metafiles/index.meta.json +1039 -0
  23. package/dist/metafiles/index.mjs.meta.json +78 -0
  24. package/dist/react/index.js +16973 -0
  25. package/dist/react/index.js.map +7 -0
  26. package/dist/sri.json +27 -0
  27. package/dist/types/analytics.d.ts +15 -0
  28. package/dist/types/antiflicker.d.ts +14 -0
  29. package/dist/types/assignment-store.d.ts +30 -0
  30. package/dist/types/audit.d.ts +9 -0
  31. package/dist/types/bucket.d.ts +2 -0
  32. package/dist/types/bucketing-cleanup.d.ts +5 -0
  33. package/dist/types/commerce-gift.d.ts +41 -0
  34. package/dist/types/commerce-price.d.ts +22 -0
  35. package/dist/types/commerce-shipping.d.ts +28 -0
  36. package/dist/types/css.d.ts +5 -0
  37. package/dist/types/cwv.d.ts +10 -0
  38. package/dist/types/data-layer-normalize.d.ts +1 -0
  39. package/dist/types/debug.d.ts +23 -0
  40. package/dist/types/define.d.ts +87 -0
  41. package/dist/types/devtools.d.ts +32 -0
  42. package/dist/types/dom-trigger.d.ts +3 -0
  43. package/dist/types/editor/api.d.ts +37 -0
  44. package/dist/types/editor/artifacts.d.ts +15 -0
  45. package/dist/types/editor/command-runtime.d.ts +48 -0
  46. package/dist/types/editor/controller.d.ts +97 -0
  47. package/dist/types/editor/device-preview.d.ts +23 -0
  48. package/dist/types/editor/index.d.ts +9 -0
  49. package/dist/types/editor/operator.d.ts +39 -0
  50. package/dist/types/editor/picker.d.ts +23 -0
  51. package/dist/types/editor/selector.d.ts +2 -0
  52. package/dist/types/editor/types.d.ts +55 -0
  53. package/dist/types/editor/view-kit.d.ts +33 -0
  54. package/dist/types/editor/view.d.ts +5 -0
  55. package/dist/types/editor-bootstrap.d.ts +37 -0
  56. package/dist/types/exclusion-groups.d.ts +59 -0
  57. package/dist/types/exposure-signals.d.ts +7 -0
  58. package/dist/types/flags.d.ts +65 -0
  59. package/dist/types/goals.d.ts +16 -0
  60. package/dist/types/guards.d.ts +39 -0
  61. package/dist/types/handover.d.ts +10 -0
  62. package/dist/types/hash.d.ts +19 -0
  63. package/dist/types/heatmap.d.ts +8 -0
  64. package/dist/types/hideflicker.d.ts +15 -0
  65. package/dist/types/hydration.d.ts +11 -0
  66. package/dist/types/index.d.ts +144 -0
  67. package/dist/types/js.d.ts +6 -0
  68. package/dist/types/module.d.ts +4 -0
  69. package/dist/types/money.d.ts +14 -0
  70. package/dist/types/mutate.d.ts +34 -0
  71. package/dist/types/observer-mute.d.ts +3 -0
  72. package/dist/types/pii-keys.d.ts +1 -0
  73. package/dist/types/prerequisites.d.ts +12 -0
  74. package/dist/types/qa-assistant-state.d.ts +68 -0
  75. package/dist/types/qa-assistant-view.d.ts +6 -0
  76. package/dist/types/qa-assistant.d.ts +9 -0
  77. package/dist/types/qa-toolbar.d.ts +19 -0
  78. package/dist/types/react/index.d.ts +84 -0
  79. package/dist/types/reassert-sentinel.d.ts +6 -0
  80. package/dist/types/router.d.ts +9 -0
  81. package/dist/types/runtime-tools.d.ts +50 -0
  82. package/dist/types/runtime.d.ts +22 -0
  83. package/dist/types/safe-regex.d.ts +6 -0
  84. package/dist/types/sanitize.d.ts +5 -0
  85. package/dist/types/semantic.d.ts +7 -0
  86. package/dist/types/shopify-cart-events.d.ts +7 -0
  87. package/dist/types/shopify-cart.d.ts +62 -0
  88. package/dist/types/signals.d.ts +31 -0
  89. package/dist/types/surface-signals.d.ts +6 -0
  90. package/dist/types/targeting-diagnostics.d.ts +5 -0
  91. package/dist/types/targeting.d.ts +50 -0
  92. package/dist/types/test-fixtures/commerce.d.ts +12 -0
  93. package/dist/types/track.d.ts +79 -0
  94. package/dist/types/types.d.ts +859 -0
  95. package/dist/types/wait.d.ts +7 -0
  96. package/package.json +74 -0
  97. package/src/analytics.ts +146 -0
  98. package/src/antiflicker.test.ts +127 -0
  99. package/src/antiflicker.ts +147 -0
  100. package/src/assignment-store.test.ts +171 -0
  101. package/src/assignment-store.ts +208 -0
  102. package/src/audit.test.ts +17 -0
  103. package/src/audit.ts +362 -0
  104. package/src/bucket.test.ts +65 -0
  105. package/src/bucket.ts +18 -0
  106. package/src/bucketing-cleanup.test.ts +30 -0
  107. package/src/bucketing-cleanup.ts +72 -0
  108. package/src/bucketing-parity.test.ts +41 -0
  109. package/src/commerce-analytics-lifecycle.test.ts +1416 -0
  110. package/src/commerce-config-lifecycle.test.ts +274 -0
  111. package/src/commerce-epoch-lifecycle.test.ts +62 -0
  112. package/src/commerce-gift.test.ts +1293 -0
  113. package/src/commerce-gift.ts +979 -0
  114. package/src/commerce-offers-display.test.ts +154 -0
  115. package/src/commerce-price.test.ts +1145 -0
  116. package/src/commerce-price.ts +941 -0
  117. package/src/commerce-revocation-lifecycle.test.ts +78 -0
  118. package/src/commerce-shipping.test.ts +630 -0
  119. package/src/commerce-shipping.ts +447 -0
  120. package/src/config-fetch.test.ts +325 -0
  121. package/src/css.ts +54 -0
  122. package/src/cwv.test.ts +95 -0
  123. package/src/cwv.ts +129 -0
  124. package/src/data-layer-normalize.ts +21 -0
  125. package/src/debug.test.ts +83 -0
  126. package/src/debug.ts +138 -0
  127. package/src/define.test.ts +327 -0
  128. package/src/define.ts +208 -0
  129. package/src/devtools.ts +582 -0
  130. package/src/dom-trigger.test.ts +70 -0
  131. package/src/dom-trigger.ts +55 -0
  132. package/src/editor/api.test.ts +154 -0
  133. package/src/editor/api.ts +185 -0
  134. package/src/editor/artifacts.test.ts +57 -0
  135. package/src/editor/artifacts.ts +115 -0
  136. package/src/editor/command-runtime.test.ts +225 -0
  137. package/src/editor/command-runtime.ts +572 -0
  138. package/src/editor/controller.test.ts +371 -0
  139. package/src/editor/controller.ts +596 -0
  140. package/src/editor/device-preview.test.ts +60 -0
  141. package/src/editor/device-preview.ts +120 -0
  142. package/src/editor/index.test.ts +161 -0
  143. package/src/editor/index.ts +169 -0
  144. package/src/editor/operator.test.ts +148 -0
  145. package/src/editor/operator.ts +256 -0
  146. package/src/editor/picker.test.ts +58 -0
  147. package/src/editor/picker.ts +152 -0
  148. package/src/editor/selector.test.ts +44 -0
  149. package/src/editor/selector.ts +110 -0
  150. package/src/editor/types.ts +92 -0
  151. package/src/editor/view-kit.ts +162 -0
  152. package/src/editor/view.test.ts +535 -0
  153. package/src/editor/view.ts +1395 -0
  154. package/src/editor-bootstrap.test.ts +116 -0
  155. package/src/editor-bootstrap.ts +213 -0
  156. package/src/event-schema-contract.test.ts +35 -0
  157. package/src/exclusion-groups.test.ts +383 -0
  158. package/src/exclusion-groups.ts +270 -0
  159. package/src/exposure-signals.test.ts +182 -0
  160. package/src/exposure-signals.ts +110 -0
  161. package/src/flags.test.ts +322 -0
  162. package/src/flags.ts +201 -0
  163. package/src/goals.test.ts +42 -0
  164. package/src/goals.ts +200 -0
  165. package/src/guards.test.ts +249 -0
  166. package/src/guards.ts +250 -0
  167. package/src/handover-cache-order.test.ts +32 -0
  168. package/src/handover.test.ts +349 -0
  169. package/src/handover.ts +309 -0
  170. package/src/hash.ts +214 -0
  171. package/src/heatmap.test.ts +305 -0
  172. package/src/heatmap.ts +373 -0
  173. package/src/hideflicker.ts +21 -0
  174. package/src/hydration.test.ts +252 -0
  175. package/src/hydration.ts +203 -0
  176. package/src/index.ts +7513 -0
  177. package/src/init-empty.test.ts +5572 -0
  178. package/src/js.test.ts +26 -0
  179. package/src/js.ts +61 -0
  180. package/src/module.ts +50 -0
  181. package/src/money.test.ts +35 -0
  182. package/src/money.ts +53 -0
  183. package/src/mutate.test.ts +304 -0
  184. package/src/mutate.ts +469 -0
  185. package/src/observer-mute.test.ts +88 -0
  186. package/src/observer-mute.ts +45 -0
  187. package/src/page-trigger.test.ts +84 -0
  188. package/src/personalization.test.ts +68 -0
  189. package/src/pii-keys.ts +17 -0
  190. package/src/prerequisites.test.ts +39 -0
  191. package/src/prerequisites.ts +94 -0
  192. package/src/qa-assistant-state.test.ts +471 -0
  193. package/src/qa-assistant-state.ts +505 -0
  194. package/src/qa-assistant-view.test.ts +185 -0
  195. package/src/qa-assistant-view.ts +768 -0
  196. package/src/qa-assistant.ts +40 -0
  197. package/src/qa-toolbar.ts +444 -0
  198. package/src/react/index.test.tsx +177 -0
  199. package/src/react/index.tsx +252 -0
  200. package/src/reassert-sentinel.test.ts +314 -0
  201. package/src/reassert-sentinel.ts +191 -0
  202. package/src/router.test.ts +78 -0
  203. package/src/router.ts +92 -0
  204. package/src/runtime-tools.test.ts +112 -0
  205. package/src/runtime-tools.ts +403 -0
  206. package/src/runtime.test.ts +217 -0
  207. package/src/runtime.ts +268 -0
  208. package/src/safe-regex.ts +35 -0
  209. package/src/sanitize.ts +34 -0
  210. package/src/semantic.ts +245 -0
  211. package/src/shopify-cart-events.test.ts +76 -0
  212. package/src/shopify-cart-events.ts +115 -0
  213. package/src/shopify-cart.test.ts +2823 -0
  214. package/src/shopify-cart.ts +1256 -0
  215. package/src/signals.ts +226 -0
  216. package/src/surface-signals.test.ts +83 -0
  217. package/src/surface-signals.ts +165 -0
  218. package/src/targeting-diagnostics.ts +560 -0
  219. package/src/targeting.detailed.test.ts +443 -0
  220. package/src/targeting.test.ts +1054 -0
  221. package/src/targeting.ts +1024 -0
  222. package/src/test-fixtures/commerce.ts +65 -0
  223. package/src/track.test.ts +817 -0
  224. package/src/track.ts +519 -0
  225. package/src/types.ts +991 -0
  226. package/src/wait.test.ts +96 -0
  227. package/src/wait.ts +238 -0
@@ -0,0 +1,443 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { build } from "esbuild";
3
+ import { evaluateTargetingGates, evaluateTargetingGatesDetailed } from "./targeting";
4
+ import type { Targeting } from "./types";
5
+
6
+ describe("evaluateTargetingGatesDetailed", () => {
7
+ it("uses aggregate targeting gates with empty conditions for live builds", async () => {
8
+ const liveBuild = await build({
9
+ entryPoints: ["src/targeting.ts"],
10
+ bundle: true,
11
+ format: "esm",
12
+ platform: "node",
13
+ write: false,
14
+ define: {
15
+ __DRIP_QA_BUILD__: "false",
16
+ },
17
+ });
18
+ const liveModuleUrl = `data:text/javascript;base64,${Buffer.from(liveBuild.outputFiles[0].text).toString("base64")}`;
19
+ const liveTargeting = await import(/* @vite-ignore */ liveModuleUrl) as typeof import("./targeting");
20
+
21
+ const cases: Array<{
22
+ name: string;
23
+ targeting?: Targeting;
24
+ ctx: { url: string; attributes: Record<string, unknown> };
25
+ }> = [
26
+ {
27
+ name: "matched url, attributes, and segments",
28
+ targeting: {
29
+ url: [{ type: "simple", pattern: "/products/*" }],
30
+ attributes: [{ attribute: "plan", operator: "equals", value: "pro" }],
31
+ segmentRules: [
32
+ {
33
+ id: "segment_1",
34
+ rules: {
35
+ version: 1,
36
+ operator: "and",
37
+ conditions: [{ signal: "cart.item_count", operator: "greater_than", value: 0 }],
38
+ },
39
+ },
40
+ ],
41
+ },
42
+ ctx: {
43
+ url: "https://shop.test/products/shirt",
44
+ attributes: { plan: "pro", "cart.item_count": 3 },
45
+ },
46
+ },
47
+ {
48
+ name: "page mismatch skips audience",
49
+ targeting: {
50
+ url: [{ type: "simple", pattern: "/products/*" }],
51
+ attributes: [{ attribute: "plan", operator: "equals", value: "pro" }],
52
+ },
53
+ ctx: {
54
+ url: "https://shop.test/pages/about",
55
+ attributes: { plan: "pro" },
56
+ },
57
+ },
58
+ {
59
+ name: "named query condition keeps aggregate precedence over js",
60
+ targeting: {
61
+ segmentRules: [
62
+ {
63
+ id: "query_segment",
64
+ rules: {
65
+ version: 1,
66
+ operator: "and",
67
+ conditions: [{
68
+ signal: "url.query",
69
+ operator: "equals",
70
+ value: "utm=spring",
71
+ js: { code: "return false;" },
72
+ }],
73
+ },
74
+ },
75
+ ],
76
+ },
77
+ ctx: {
78
+ url: "https://shop.test/?utm=spring",
79
+ attributes: {},
80
+ },
81
+ },
82
+ {
83
+ name: "no targeting",
84
+ targeting: undefined,
85
+ ctx: {
86
+ url: "https://shop.test/",
87
+ attributes: {},
88
+ },
89
+ },
90
+ ];
91
+
92
+ for (const testCase of cases) {
93
+ const aggregate = evaluateTargetingGates(testCase.targeting, testCase.ctx);
94
+ const liveFallback = liveTargeting.evaluateTargetingGatesDetailed(
95
+ testCase.targeting,
96
+ testCase.ctx,
97
+ );
98
+ expect(liveFallback, testCase.name).toEqual({ ...aggregate, conditions: [] });
99
+
100
+ const qaDetailed = evaluateTargetingGatesDetailed(testCase.targeting, testCase.ctx);
101
+ expect(qaDetailed.matched, `${testCase.name} (qa matched)`).toBe(aggregate.matched);
102
+ expect(qaDetailed.gates, `${testCase.name} (qa gates)`).toEqual(aggregate.gates);
103
+ }
104
+ });
105
+
106
+ it("reports URL rule match and mismatch conditions", () => {
107
+ const targeting: Targeting = {
108
+ url: [{ type: "simple", pattern: "/products/*" }],
109
+ };
110
+
111
+ const matched = evaluateTargetingGatesDetailed(targeting, {
112
+ url: "https://shop.test/products/shirt",
113
+ attributes: {},
114
+ });
115
+ expect(matched.matched).toBe(true);
116
+ expect(matched.conditions).toEqual([
117
+ expect.objectContaining({
118
+ id: "url:0",
119
+ status: "passed",
120
+ sourceType: "url",
121
+ source: "/products/*",
122
+ expected: "/products/*",
123
+ actual: "https://shop.test/products/shirt",
124
+ }),
125
+ ]);
126
+
127
+ const mismatched = evaluateTargetingGatesDetailed(targeting, {
128
+ url: "https://shop.test/pages/about",
129
+ attributes: {},
130
+ });
131
+ expect(mismatched.matched).toBe(false);
132
+ expect(mismatched.conditions[0]).toMatchObject({
133
+ id: "url:0",
134
+ status: "failed",
135
+ reason: "url_mismatch",
136
+ sourceType: "url",
137
+ });
138
+ expect(evaluateTargetingGates(targeting, {
139
+ url: "https://shop.test/pages/about",
140
+ attributes: {},
141
+ })).toMatchInlineSnapshot(`
142
+ {
143
+ "gates": [
144
+ {
145
+ "detail": {
146
+ "pageRules": 0,
147
+ "rules": 1,
148
+ "url": "https://shop.test/pages/about",
149
+ },
150
+ "id": "page",
151
+ "label": "Page check",
152
+ "reason": "url_mismatch",
153
+ "status": "failed",
154
+ },
155
+ {
156
+ "detail": {
157
+ "skipped": true,
158
+ },
159
+ "id": "audience",
160
+ "label": "Audience check",
161
+ "reason": "page_mismatch",
162
+ "status": "skipped",
163
+ },
164
+ ],
165
+ "matched": false,
166
+ }
167
+ `);
168
+ });
169
+
170
+ it("reports page-rule JS true, false, and error without throwing", () => {
171
+ const baseRule = {
172
+ id: "page_1",
173
+ includeRules: [{ type: "simple" as const, pattern: "/products/*", include: true }],
174
+ };
175
+
176
+ const jsTrue = evaluateTargetingGatesDetailed({
177
+ pageRules: [{ ...baseRule, advancedCondition: { code: "return true;" } }],
178
+ }, {
179
+ url: "https://shop.test/products/shirt",
180
+ attributes: {},
181
+ });
182
+ expect(jsTrue.conditions[0]).toMatchObject({
183
+ id: "page:page_1",
184
+ status: "passed",
185
+ sourceType: "js",
186
+ source: "return true;",
187
+ expected: JSON.stringify([{ include: true, matchType: "simple", pattern: "/products/*" }]),
188
+ actual: true,
189
+ });
190
+
191
+ const jsFalse = evaluateTargetingGatesDetailed({
192
+ pageRules: [{ ...baseRule, advancedCondition: { code: "false" } }],
193
+ }, {
194
+ url: "https://shop.test/products/shirt",
195
+ attributes: {},
196
+ });
197
+ expect(jsFalse.matched).toBe(false);
198
+ expect(jsFalse.conditions[0]).toMatchObject({
199
+ status: "failed",
200
+ reason: "js_false",
201
+ sourceType: "js",
202
+ source: "false",
203
+ actual: false,
204
+ });
205
+
206
+ const jsError = evaluateTargetingGatesDetailed({
207
+ pageRules: [{ ...baseRule, advancedCondition: { code: "throw new Error('bad trigger')" } }],
208
+ }, {
209
+ url: "https://shop.test/products/shirt",
210
+ attributes: {},
211
+ });
212
+ expect(jsError.matched).toBe(false);
213
+ expect(jsError.conditions[0]).toMatchObject({
214
+ status: "failed",
215
+ sourceType: "js",
216
+ source: "throw new Error('bad trigger')",
217
+ actual: false,
218
+ });
219
+ expect(jsError.conditions[0]?.reason).toContain("js_error");
220
+ expect(jsError.conditions[0]?.reason).toContain("bad trigger");
221
+ });
222
+
223
+ it("includes page URL patterns and the current URL in page-rule diagnostics", () => {
224
+ const diagnostics = evaluateTargetingGatesDetailed({
225
+ pageRules: [{
226
+ id: "products",
227
+ name: "Product detail pages",
228
+ includeRules: [{ type: "simple", pattern: "/products/*" }],
229
+ excludeRules: [{ type: "regex", pattern: "/products/sample$" }],
230
+ }],
231
+ }, { url: "https://shop.test/products/shirt", attributes: {} });
232
+
233
+ expect(diagnostics.conditions[0]).toMatchObject({
234
+ sourceType: "page_rule",
235
+ // Patterns live in source (rendered as Rules:); expected stays unset
236
+ // for plain page rules so the panel never duplicates them.
237
+ expected: undefined,
238
+ actual: "https://shop.test/products/shirt — passed",
239
+ });
240
+ expect(diagnostics.conditions[0]?.source).toContain('"include":true');
241
+ expect(diagnostics.conditions[0]?.source).toContain('"pattern":"/products/*"');
242
+ expect(diagnostics.conditions[0]?.source).toContain('"include":false');
243
+ });
244
+
245
+ it("reports page trigger JS as the raw JS source", () => {
246
+ const diagnostics = evaluateTargetingGatesDetailed({
247
+ pageRules: [
248
+ {
249
+ id: "page_1",
250
+ includeRules: [{ type: "simple", pattern: "/products/*", include: true }],
251
+ trigger: {
252
+ mode: "manual-callback",
253
+ signal: "page.trigger.pdp_overlay",
254
+ triggerJs: "activate();",
255
+ },
256
+ },
257
+ ],
258
+ }, {
259
+ url: "https://shop.test/products/shirt",
260
+ attributes: {},
261
+ });
262
+
263
+ expect(diagnostics.matched).toBe(false);
264
+ expect(diagnostics.conditions[0]).toMatchObject({
265
+ id: "page:page_1",
266
+ status: "failed",
267
+ reason: "manual_trigger_mismatch",
268
+ sourceType: "js",
269
+ source: "activate();",
270
+ });
271
+ });
272
+
273
+ it("reports attribute operator mismatches with expected and actual values", () => {
274
+ const diagnostics = evaluateTargetingGatesDetailed({
275
+ attributes: [{ attribute: "plan", operator: "equals", value: "pro" }],
276
+ }, {
277
+ url: "https://shop.test/",
278
+ attributes: { plan: "free" },
279
+ });
280
+
281
+ expect(diagnostics.matched).toBe(false);
282
+ expect(diagnostics.conditions).toContainEqual(expect.objectContaining({
283
+ id: "attribute:plan:0",
284
+ status: "failed",
285
+ reason: "attribute_mismatch",
286
+ sourceType: "attribute",
287
+ signal: "plan",
288
+ operator: "equals",
289
+ expected: "pro",
290
+ actual: "free",
291
+ }));
292
+ });
293
+
294
+ it("reports segment JS conditions with verbatim source", () => {
295
+ const source = "return attributes.plan === 'pro';";
296
+ const diagnostics = evaluateTargetingGatesDetailed({
297
+ segmentRules: [
298
+ {
299
+ id: "segment_1",
300
+ name: "Pro plan",
301
+ rules: {
302
+ version: 1,
303
+ operator: "and",
304
+ conditions: [
305
+ {
306
+ signal: "plan.js",
307
+ operator: "equals",
308
+ value: true,
309
+ js: { code: source },
310
+ },
311
+ ],
312
+ },
313
+ },
314
+ ],
315
+ }, {
316
+ url: "https://shop.test/",
317
+ attributes: { plan: "pro" },
318
+ });
319
+
320
+ expect(diagnostics.matched).toBe(true);
321
+ expect(diagnostics.conditions).toContainEqual(expect.objectContaining({
322
+ id: "segment:segment_1:0",
323
+ status: "passed",
324
+ sourceType: "js",
325
+ source,
326
+ signal: "plan.js",
327
+ operator: "equals",
328
+ expected: true,
329
+ actual: true,
330
+ }));
331
+ });
332
+
333
+ it("marks audience conditions skipped after a page failure", () => {
334
+ const targeting: Targeting = {
335
+ url: [{ type: "simple", pattern: "/products/*" }],
336
+ attributes: [{ attribute: "plan", operator: "equals", value: "pro" }],
337
+ segmentRules: [
338
+ {
339
+ id: "segment_1",
340
+ rules: {
341
+ version: 1,
342
+ operator: "and",
343
+ conditions: [{ signal: "cart.item_count", operator: "greater_than", value: 0 }],
344
+ },
345
+ },
346
+ ],
347
+ };
348
+
349
+ const diagnostics = evaluateTargetingGatesDetailed(targeting, {
350
+ url: "https://shop.test/pages/about",
351
+ attributes: { plan: "pro", "cart.item_count": 3 },
352
+ });
353
+
354
+ expect(diagnostics.matched).toBe(false);
355
+ expect(diagnostics.conditions).toEqual([
356
+ expect.objectContaining({ id: "url:0", status: "failed", reason: "url_mismatch" }),
357
+ expect.objectContaining({ id: "attribute:plan:0", status: "skipped", reason: "page_mismatch" }),
358
+ expect.objectContaining({ id: "segment:segment_1:0", status: "skipped", reason: "page_mismatch" }),
359
+ ]);
360
+ });
361
+
362
+ it("marks grouped segment conditions skipped after a page failure", () => {
363
+ const diagnostics = evaluateTargetingGatesDetailed({
364
+ url: [{ type: "simple", pattern: "/products/*" }],
365
+ segmentRules: [
366
+ {
367
+ id: "segment_grouped",
368
+ rules: {
369
+ version: 1,
370
+ operator: "and",
371
+ conditions: [],
372
+ groups: [
373
+ [{ signal: "plan", operator: "equals", value: "pro" }],
374
+ [{ signal: "cart.item_count", operator: "greater_than", value: 0 }],
375
+ ],
376
+ },
377
+ },
378
+ ],
379
+ }, {
380
+ url: "https://shop.test/pages/about",
381
+ attributes: { plan: "pro", "cart.item_count": 3 },
382
+ });
383
+
384
+ expect(diagnostics.conditions).toEqual([
385
+ expect.objectContaining({ id: "url:0", status: "failed", reason: "url_mismatch" }),
386
+ expect.objectContaining({ id: "segment:segment_grouped:0", status: "skipped", reason: "page_mismatch" }),
387
+ expect.objectContaining({ id: "segment:segment_grouped:1", status: "skipped", reason: "page_mismatch" }),
388
+ ]);
389
+ });
390
+
391
+ it("reports all-pass targeting and preserves the aggregate gate output", () => {
392
+ const targeting: Targeting = {
393
+ url: [{ type: "simple", pattern: "/products/*" }],
394
+ attributes: [{ attribute: "plan", operator: "equals", value: "pro" }],
395
+ segmentRules: [
396
+ {
397
+ id: "segment_1",
398
+ rules: {
399
+ version: 1,
400
+ operator: "and",
401
+ conditions: [{ signal: "cart.item_count", operator: "greater_than", value: 0 }],
402
+ },
403
+ },
404
+ ],
405
+ };
406
+ const ctx = {
407
+ url: "https://shop.test/products/shirt",
408
+ attributes: { plan: "pro", "cart.item_count": 3 },
409
+ };
410
+
411
+ const diagnostics = evaluateTargetingGatesDetailed(targeting, ctx);
412
+ expect(diagnostics.matched).toBe(true);
413
+ expect(diagnostics.conditions.map((condition) => condition.status)).toEqual(["passed", "passed", "passed"]);
414
+ expect(evaluateTargetingGates(targeting, ctx)).toMatchInlineSnapshot(`
415
+ {
416
+ "gates": [
417
+ {
418
+ "detail": {
419
+ "pageRules": 0,
420
+ "rules": 1,
421
+ "url": "https://shop.test/products/shirt",
422
+ },
423
+ "id": "page",
424
+ "label": "Page check",
425
+ "reason": "url_matched",
426
+ "status": "passed",
427
+ },
428
+ {
429
+ "detail": {
430
+ "attributeRules": 1,
431
+ "segmentRules": 1,
432
+ },
433
+ "id": "audience",
434
+ "label": "Audience check",
435
+ "reason": "audience_matched",
436
+ "status": "passed",
437
+ },
438
+ ],
439
+ "matched": true,
440
+ }
441
+ `);
442
+ });
443
+ });