@gem-sdk/clarity-js 0.8.94

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 (127) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +1 -0
  3. package/build/clarity.extended.js +1 -0
  4. package/build/clarity.insight.js +1 -0
  5. package/build/clarity.js +7148 -0
  6. package/build/clarity.livechat.js +1 -0
  7. package/build/clarity.min.js +1 -0
  8. package/build/clarity.module.js +7144 -0
  9. package/build/clarity.performance.js +1 -0
  10. package/build/dynamic/clarity.crisp.js +1 -0
  11. package/build/dynamic/clarity.tidio.js +1 -0
  12. package/package.json +49 -0
  13. package/rollup.config.ts +161 -0
  14. package/src/clarity.ts +72 -0
  15. package/src/core/api.ts +8 -0
  16. package/src/core/config.ts +35 -0
  17. package/src/core/copy.ts +3 -0
  18. package/src/core/dynamic.ts +68 -0
  19. package/src/core/event.ts +53 -0
  20. package/src/core/hash.ts +19 -0
  21. package/src/core/history.ts +82 -0
  22. package/src/core/index.ts +81 -0
  23. package/src/core/measure.ts +19 -0
  24. package/src/core/report.ts +28 -0
  25. package/src/core/scrub.ts +201 -0
  26. package/src/core/task.ts +181 -0
  27. package/src/core/throttle.ts +46 -0
  28. package/src/core/time.ts +26 -0
  29. package/src/core/timeout.ts +10 -0
  30. package/src/core/version.ts +2 -0
  31. package/src/custom/README.md +44 -0
  32. package/src/custom/dialog.ts +69 -0
  33. package/src/custom/index.ts +6 -0
  34. package/src/data/baseline.ts +162 -0
  35. package/src/data/compress.ts +31 -0
  36. package/src/data/consent.ts +84 -0
  37. package/src/data/cookie.ts +90 -0
  38. package/src/data/custom.ts +23 -0
  39. package/src/data/dimension.ts +53 -0
  40. package/src/data/encode.ts +155 -0
  41. package/src/data/envelope.ts +53 -0
  42. package/src/data/extract.ts +217 -0
  43. package/src/data/gemx-upload.ts +51 -0
  44. package/src/data/gemx.ts +35 -0
  45. package/src/data/index.ts +57 -0
  46. package/src/data/limit.ts +44 -0
  47. package/src/data/metadata.ts +340 -0
  48. package/src/data/metric.ts +51 -0
  49. package/src/data/ping.ts +36 -0
  50. package/src/data/signal.ts +30 -0
  51. package/src/data/summary.ts +34 -0
  52. package/src/data/token.ts +39 -0
  53. package/src/data/upgrade.ts +44 -0
  54. package/src/data/upload.ts +375 -0
  55. package/src/data/util.ts +18 -0
  56. package/src/data/variable.ts +84 -0
  57. package/src/diagnostic/encode.ts +40 -0
  58. package/src/diagnostic/fraud.ts +37 -0
  59. package/src/diagnostic/index.ts +13 -0
  60. package/src/diagnostic/internal.ts +28 -0
  61. package/src/diagnostic/script.ts +35 -0
  62. package/src/dynamic/agent/blank.ts +2 -0
  63. package/src/dynamic/agent/crisp.ts +40 -0
  64. package/src/dynamic/agent/encode.ts +25 -0
  65. package/src/dynamic/agent/index.ts +8 -0
  66. package/src/dynamic/agent/livechat.ts +58 -0
  67. package/src/dynamic/agent/tidio.ts +44 -0
  68. package/src/global.ts +6 -0
  69. package/src/index.ts +10 -0
  70. package/src/insight/blank.ts +14 -0
  71. package/src/insight/encode.ts +64 -0
  72. package/src/insight/snapshot.ts +115 -0
  73. package/src/interaction/change.ts +38 -0
  74. package/src/interaction/click.ts +256 -0
  75. package/src/interaction/clipboard.ts +32 -0
  76. package/src/interaction/encode.ts +217 -0
  77. package/src/interaction/focus.ts +25 -0
  78. package/src/interaction/index.ts +66 -0
  79. package/src/interaction/input.ts +66 -0
  80. package/src/interaction/pageshow.ts +35 -0
  81. package/src/interaction/pointer.ts +137 -0
  82. package/src/interaction/resize.ts +50 -0
  83. package/src/interaction/scroll.ts +134 -0
  84. package/src/interaction/selection.ts +66 -0
  85. package/src/interaction/submit.ts +30 -0
  86. package/src/interaction/timeline.ts +69 -0
  87. package/src/interaction/unload.ts +32 -0
  88. package/src/interaction/visibility.ts +28 -0
  89. package/src/layout/animation.ts +133 -0
  90. package/src/layout/constants.ts +13 -0
  91. package/src/layout/custom.ts +48 -0
  92. package/src/layout/discover.ts +31 -0
  93. package/src/layout/document.ts +46 -0
  94. package/src/layout/dom.ts +444 -0
  95. package/src/layout/encode.ts +157 -0
  96. package/src/layout/gemx-snapshot.ts +84 -0
  97. package/src/layout/index.ts +47 -0
  98. package/src/layout/mutation.ts +424 -0
  99. package/src/layout/node.ts +302 -0
  100. package/src/layout/offset.ts +19 -0
  101. package/src/layout/region.ts +151 -0
  102. package/src/layout/schema.ts +63 -0
  103. package/src/layout/selector.ts +113 -0
  104. package/src/layout/style.ts +160 -0
  105. package/src/layout/target.ts +34 -0
  106. package/src/layout/traverse.ts +28 -0
  107. package/src/performance/blank.ts +10 -0
  108. package/src/performance/encode.ts +31 -0
  109. package/src/performance/index.ts +12 -0
  110. package/src/performance/interaction.ts +125 -0
  111. package/src/performance/navigation.ts +31 -0
  112. package/src/performance/observer.ts +112 -0
  113. package/src/queue.ts +33 -0
  114. package/test/consentv2.test.ts +1309 -0
  115. package/test/hash.test.ts +68 -0
  116. package/test/time.test.ts +42 -0
  117. package/tsconfig.json +12 -0
  118. package/tslint.json +33 -0
  119. package/types/agent.d.ts +39 -0
  120. package/types/core.d.ts +156 -0
  121. package/types/data.d.ts +611 -0
  122. package/types/diagnostic.d.ts +24 -0
  123. package/types/global.d.ts +43 -0
  124. package/types/index.d.ts +46 -0
  125. package/types/interaction.d.ts +187 -0
  126. package/types/layout.d.ts +271 -0
  127. package/types/performance.d.ts +34 -0
@@ -0,0 +1,1309 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { readFileSync } from "fs";
3
+ import { join } from "path";
4
+
5
+ /**
6
+ * Consent API tests - Tests the production consent functionality by loading
7
+ * the built clarity.min.js in a browser context. This approach tests the actual
8
+ * artifact that ships to users rather than individual source modules.
9
+ */
10
+
11
+ // Type definitions for test results
12
+ interface ConsentState {
13
+ source: number;
14
+ ad_Storage: string;
15
+ analytics_Storage: string;
16
+ }
17
+
18
+ /**
19
+ * Standard result type for all consent tests.
20
+ * Every test should capture and verify this complete state for consistent analysis.
21
+ */
22
+ interface ConsentTestResult {
23
+ consent: ConsentState;
24
+ hasClskCookie: boolean;
25
+ hasClckCookie: boolean;
26
+ clskCookieValue: string;
27
+ clckCookieValue: string;
28
+ cookies: string;
29
+ }
30
+
31
+ const Constant = {
32
+ Granted: "granted",
33
+ Denied: "denied",
34
+ CookieKey: "_clck",
35
+ SessionKey: "_clsk",
36
+ } as const;
37
+
38
+ const ConsentSource = {
39
+ Implicit: 0,
40
+ API: 1,
41
+ GCM: 2,
42
+ TCF: 3,
43
+ APIv1: 4,
44
+ APIv2: 5,
45
+ Cookie: 6,
46
+ Default: 7,
47
+ } as const;
48
+
49
+ // Maximum time to wait from when consent() is called to when its callback resolves
50
+ const CONSENT_CALLBACK_TIMEOUT = 2000;
51
+ // Delay from when cookie mock is set up to when Clarity can reliably read it
52
+ const COOKIE_SETUP_DELAY = 500;
53
+
54
+ // Use the minified browser build which properly exposes window.clarity
55
+ const clarityJsPath = join(__dirname, "../build/clarity.min.js");
56
+
57
+ /**
58
+ * Sets up a cookie mock for data: URLs which don't support cookies natively.
59
+ * Handles both cookie setting and deletion (via max-age or empty values).
60
+ * @param initialCookieValue - Optional initial cookie value (e.g., "marketing_id=abc123")
61
+ */
62
+ function setupCookieMock(initialCookieValue?: string): void {
63
+ let cookieStore = initialCookieValue || "";
64
+ Object.defineProperty(document, "cookie", {
65
+ get: () => cookieStore,
66
+ set: (value: string) => {
67
+ if (value.includes("max-age=-") || value.includes("=;") || value.includes("=^;")) {
68
+ const cookieName = value.split("=")[0];
69
+ const cookies = cookieStore.split("; ").filter(c => !c.startsWith(cookieName + "="));
70
+ cookieStore = cookies.join("; ");
71
+ } else {
72
+ const cookieName = value.split("=")[0];
73
+ const cookies = cookieStore.split("; ").filter(c => !c.startsWith(cookieName + "="));
74
+ cookies.push(value.split(";")[0]);
75
+ cookieStore = cookies.filter(c => c).join("; ");
76
+ }
77
+ },
78
+ configurable: true
79
+ });
80
+ }
81
+
82
+
83
+
84
+ test.describe("consentv2 - Production API", () => {
85
+ test.beforeEach(async ({ page }) => {
86
+ await page.goto("data:text/html,<!DOCTYPE html><html><head></head><body></body></html>");
87
+
88
+ // Expose timeout constants to the page context
89
+ await page.evaluate(({ timeout, delay }) => {
90
+ (window as any).CONSENT_CALLBACK_TIMEOUT = timeout;
91
+ (window as any).COOKIE_SETUP_DELAY = delay;
92
+ }, { timeout: CONSENT_CALLBACK_TIMEOUT, delay: COOKIE_SETUP_DELAY });
93
+
94
+ const clarityJs = readFileSync(clarityJsPath, "utf-8");
95
+ await page.evaluate((code) => {
96
+ eval(code);
97
+ }, clarityJs);
98
+ });
99
+
100
+ // ========================
101
+ // track=false tests
102
+ // ========================
103
+
104
+ test("implicit denied: track=false results in denied consent", async ({ page }) => {
105
+ await page.evaluate(setupCookieMock as () => void);
106
+
107
+ // Verify initial state (before Clarity starts) - no cookies should exist
108
+ const initialState = await page.evaluate(({ sessionKey, cookieKey }) => {
109
+ const cookies = document.cookie;
110
+ return {
111
+ cookies,
112
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
113
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
114
+ clskCookieValue: "",
115
+ clckCookieValue: ""
116
+ };
117
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
118
+
119
+ expect(initialState.cookies).toBe("");
120
+ expect(initialState.hasClskCookie).toBe(false);
121
+ expect(initialState.hasClckCookie).toBe(false);
122
+
123
+ // Start clarity with track=false (cookies disabled, no network calls)
124
+ const result = await page.evaluate(({ sessionKey, cookieKey }) => {
125
+ return new Promise((resolve) => {
126
+ (window as any).clarity("start", {
127
+ projectId: "test",
128
+ track: false,
129
+ upload: false
130
+ });
131
+
132
+ // Register metadata callback AFTER starting clarity
133
+ // Signature: clarity('metadata', callback, wait, recall, consentInfo)
134
+ // The callback receives (data, upgrade, consent)
135
+ // wait=false (don't wait for data), recall=true (resend on changes), consentInfo=true (include consent)
136
+ (window as any).clarity("metadata", (_data: any, _upgrade: any, consent: any) => {
137
+ if (consent) {
138
+ const cookies = document.cookie;
139
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
140
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
141
+ resolve({
142
+ consent,
143
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
144
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
145
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
146
+ clckCookieValue: clckMatch ? clckMatch[1] : "",
147
+ clckConsentCrumb: clckMatch ? (clckMatch[1].split("^")[3] || "") : "",
148
+ cookies
149
+ });
150
+ }
151
+ }, false, true, true);
152
+
153
+ // Timeout fallback
154
+ setTimeout(() => {
155
+ const cookies = document.cookie;
156
+ resolve({
157
+ consent: null,
158
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
159
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
160
+ clskCookieValue: "",
161
+ clckCookieValue: "",
162
+ cookies
163
+ });
164
+ }, (window as any).CONSENT_CALLBACK_TIMEOUT);
165
+ });
166
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
167
+
168
+ expect(result).not.toBeNull();
169
+ const consentResult = result as ConsentTestResult;
170
+ expect(consentResult.consent).not.toBeNull();
171
+ expect(consentResult.consent.source).toBe(ConsentSource.Implicit);
172
+ expect(consentResult.consent.ad_Storage).toBe(Constant.Denied);
173
+ expect(consentResult.consent.analytics_Storage).toBe(Constant.Denied);
174
+ // Verify cookies are not set when track=false
175
+ expect(consentResult.hasClskCookie).toBe(false);
176
+ expect(consentResult.hasClckCookie).toBe(false);
177
+ });
178
+
179
+ test("cookie consent: track=false with consent cookie results in granted", async ({ page }) => {
180
+ // This test uses a pre-set cookie (simulating a returning user with consent)
181
+ // so we manually mock the cookie rather than using setupCookieMock
182
+ const result = await page.evaluate(({ sessionKey, cookieKey }) => {
183
+ // Mock document.cookie to simulate a consent cookie
184
+ // Format: _clck cookie with consent flag set to 1 (granted)
185
+ // Cookie format: userId^version^expiry^consent^dob
186
+ const userId = "testuser123";
187
+ const version = "2";
188
+ const expiry = Math.ceil((Date.now() + 31536e6) / 864e5).toString(36);
189
+ const consentFlag = "1"; // 1 = granted
190
+ const dob = "0";
191
+ const presetCookieValue = `${userId}^${version}^${expiry}^${consentFlag}^${dob}`;
192
+
193
+ Object.defineProperty(document, "cookie", {
194
+ writable: true,
195
+ value: `${cookieKey}=${presetCookieValue}`
196
+ });
197
+
198
+ // Capture initial state with the pre-set cookie
199
+ const initialCookies = document.cookie;
200
+ const initialState = {
201
+ cookies: initialCookies,
202
+ hasClskCookie: initialCookies.includes(`${sessionKey}=`),
203
+ hasClckCookie: initialCookies.includes(`${cookieKey}=`),
204
+ clckCookieValue: presetCookieValue
205
+ };
206
+
207
+ return new Promise((resolve) => {
208
+ // Start clarity with track=false but with consent cookie present
209
+ (window as any).clarity("start", {
210
+ projectId: "test",
211
+ track: false,
212
+ upload: false
213
+ });
214
+
215
+ // Register metadata callback
216
+ (window as any).clarity("metadata", (_data: any, _upgrade: any, consent: any) => {
217
+ if (consent) {
218
+ const cookies = document.cookie;
219
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
220
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
221
+ resolve({
222
+ initialState,
223
+ consent,
224
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
225
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
226
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
227
+ clckCookieValue: clckMatch ? clckMatch[1] : "",
228
+ clckConsentCrumb: clckMatch ? (clckMatch[1].split("^")[3] || "") : "",
229
+ cookies
230
+ });
231
+ }
232
+ }, false, true, true);
233
+
234
+ // Timeout fallback
235
+ setTimeout(() => {
236
+ const cookies = document.cookie;
237
+ resolve({
238
+ initialState,
239
+ consent: null,
240
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
241
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
242
+ clskCookieValue: "",
243
+ clckCookieValue: "",
244
+ cookies
245
+ });
246
+ }, (window as any).CONSENT_CALLBACK_TIMEOUT);
247
+ });
248
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
249
+
250
+ expect(result).not.toBeNull();
251
+ const consentResult = result as ConsentTestResult & { initialState: { cookies: string; hasClskCookie: boolean; hasClckCookie: boolean; clckCookieValue: string } };
252
+
253
+ // Verify initial state had the pre-set consent cookie
254
+ expect(consentResult.initialState.hasClckCookie).toBe(true);
255
+ expect(consentResult.initialState.clckCookieValue).toContain("testuser123");
256
+
257
+ // Verify consent was read from cookie
258
+ expect(consentResult.consent).not.toBeNull();
259
+ expect(consentResult.consent.source).toBe(ConsentSource.Cookie);
260
+ expect(consentResult.consent.ad_Storage).toBe(Constant.Granted);
261
+ expect(consentResult.consent.analytics_Storage).toBe(Constant.Granted);
262
+ // Cookie should still exist
263
+ expect(consentResult.hasClckCookie).toBe(true);
264
+ });
265
+
266
+ test("consentv2 explicit denial: track=false → denied/denied remains without cookies", async ({ page }) => {
267
+ await page.evaluate(setupCookieMock as () => void);
268
+
269
+ // Verify initial state - no cookies before Clarity starts
270
+ const initialState = await page.evaluate(({ sessionKey, cookieKey }) => {
271
+ const cookies = document.cookie;
272
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
273
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
274
+ return {
275
+ cookies,
276
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
277
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
278
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
279
+ clckCookieValue: clckMatch ? clckMatch[1] : ""
280
+ };
281
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
282
+
283
+ expect(initialState.cookies).toBe("");
284
+ expect(initialState.hasClskCookie).toBe(false);
285
+ expect(initialState.hasClckCookie).toBe(false);
286
+
287
+ // Start with track=false (implicit denied), then explicitly deny via consentv2
288
+ const result = await page.evaluate(({ sessionKey, cookieKey }) => {
289
+ return new Promise((resolve) => {
290
+ (window as any).clarity("start", {
291
+ projectId: "test",
292
+ track: false,
293
+ upload: false
294
+ });
295
+
296
+ setTimeout(() => {
297
+ (window as any).clarity("consentv2", {
298
+ ad_Storage: "denied",
299
+ analytics_Storage: "denied"
300
+ });
301
+
302
+ (window as any).clarity("metadata", (_data: any, _upgrade: any, consent: any) => {
303
+ if (consent) {
304
+ const cookies = document.cookie;
305
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
306
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
307
+ resolve({
308
+ consent,
309
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
310
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
311
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
312
+ clckCookieValue: clckMatch ? clckMatch[1] : "",
313
+ clckConsentCrumb: clckMatch ? (clckMatch[1].split("^")[3] || "") : "",
314
+ cookies
315
+ });
316
+ }
317
+ }, false, false, true);
318
+ }, (window as any).COOKIE_SETUP_DELAY);
319
+
320
+ setTimeout(() => resolve(null), (window as any).CONSENT_CALLBACK_TIMEOUT);
321
+ });
322
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
323
+
324
+ expect(result).not.toBeNull();
325
+ const consentResult = result as ConsentTestResult;
326
+ expect(consentResult.consent.source).toBe(ConsentSource.APIv2);
327
+ expect(consentResult.consent.ad_Storage).toBe(Constant.Denied);
328
+ expect(consentResult.consent.analytics_Storage).toBe(Constant.Denied);
329
+ // Verify cookies are not set when analytics denied
330
+ expect(consentResult.hasClskCookie).toBe(false);
331
+ expect(consentResult.hasClckCookie).toBe(false);
332
+ expect(consentResult.clskCookieValue).toBe("");
333
+ expect(consentResult.clckCookieValue).toBe("");
334
+ });
335
+
336
+ test("consentv2 mixed consent: track=false → denied analytics, granted ads no cookies", async ({ page }) => {
337
+ await page.evaluate(setupCookieMock as () => void);
338
+
339
+ // Verify initial state - no cookies before Clarity starts
340
+ const initialState = await page.evaluate(({ sessionKey, cookieKey }) => {
341
+ const cookies = document.cookie;
342
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
343
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
344
+ return {
345
+ cookies,
346
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
347
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
348
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
349
+ clckCookieValue: clckMatch ? clckMatch[1] : ""
350
+ };
351
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
352
+
353
+ expect(initialState.cookies).toBe("");
354
+ expect(initialState.hasClskCookie).toBe(false);
355
+ expect(initialState.hasClckCookie).toBe(false);
356
+
357
+ const result = await page.evaluate(({ sessionKey, cookieKey }) => {
358
+ return new Promise((resolve) => {
359
+ (window as any).clarity("start", {
360
+ projectId: "test",
361
+ track: false,
362
+ upload: false
363
+ });
364
+
365
+ (window as any).clarity("consentv2", {
366
+ ad_Storage: "granted",
367
+ analytics_Storage: "denied"
368
+ });
369
+
370
+ (window as any).clarity("metadata", (_data: any, _upgrade: any, consent: any) => {
371
+ if (consent) {
372
+ const cookies = document.cookie;
373
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
374
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
375
+ resolve({
376
+ consent,
377
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
378
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
379
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
380
+ clckCookieValue: clckMatch ? clckMatch[1] : "",
381
+ clckConsentCrumb: clckMatch ? (clckMatch[1].split("^")[3] || "") : "",
382
+ cookies
383
+ });
384
+ }
385
+ }, false, false, true);
386
+
387
+ setTimeout(() => resolve(null), (window as any).CONSENT_CALLBACK_TIMEOUT);
388
+ });
389
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
390
+
391
+ expect(result).not.toBeNull();
392
+ const consentResult = result as ConsentTestResult;
393
+ expect(consentResult.consent.source).toBe(ConsentSource.APIv2);
394
+ expect(consentResult.consent.ad_Storage).toBe(Constant.Granted);
395
+ expect(consentResult.consent.analytics_Storage).toBe(Constant.Denied);
396
+ // Verify cookies are deleted when analytics is denied (regardless of ads)
397
+ expect(consentResult.hasClskCookie).toBe(false);
398
+ expect(consentResult.hasClckCookie).toBe(false);
399
+ expect(consentResult.clskCookieValue).toBe("");
400
+ expect(consentResult.clckCookieValue).toBe("");
401
+ });
402
+
403
+ test("consentv2 mixed consent: track=false → granted analytics, denied ads sets cookies", async ({ page }) => {
404
+ await page.evaluate(setupCookieMock as () => void);
405
+
406
+ // Verify initial state - no cookies before Clarity starts
407
+ const initialState = await page.evaluate(({ sessionKey, cookieKey }) => {
408
+ const cookies = document.cookie;
409
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
410
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
411
+ return {
412
+ cookies,
413
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
414
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
415
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
416
+ clckCookieValue: clckMatch ? clckMatch[1] : ""
417
+ };
418
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
419
+
420
+ expect(initialState.cookies).toBe("");
421
+ expect(initialState.hasClskCookie).toBe(false);
422
+ expect(initialState.hasClckCookie).toBe(false);
423
+
424
+ // Start with track=false, then grant analytics but deny ads
425
+ const result = await page.evaluate(({ sessionKey, cookieKey }) => {
426
+ return new Promise((resolve) => {
427
+ (window as any).clarity("start", {
428
+ projectId: "test",
429
+ track: false,
430
+ upload: false
431
+ });
432
+
433
+ setTimeout(() => {
434
+ (window as any).clarity("consentv2", {
435
+ ad_Storage: "denied",
436
+ analytics_Storage: "granted"
437
+ });
438
+
439
+ (window as any).clarity("metadata", (_data: any, _upgrade: any, consent: any) => {
440
+ if (consent) {
441
+ const cookies = document.cookie;
442
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
443
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
444
+ resolve({
445
+ consent,
446
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
447
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
448
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
449
+ clckCookieValue: clckMatch ? clckMatch[1] : "",
450
+ clckConsentCrumb: clckMatch ? (clckMatch[1].split("^")[3] || "") : "",
451
+ cookies
452
+ });
453
+ }
454
+ }, false, false, true);
455
+ }, (window as any).COOKIE_SETUP_DELAY);
456
+
457
+ setTimeout(() => resolve(null), (window as any).CONSENT_CALLBACK_TIMEOUT);
458
+ });
459
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
460
+
461
+ expect(result).not.toBeNull();
462
+ const consentResult = result as ConsentTestResult;
463
+ expect(consentResult.consent.source).toBe(ConsentSource.APIv2);
464
+ expect(consentResult.consent.ad_Storage).toBe(Constant.Denied);
465
+ expect(consentResult.consent.analytics_Storage).toBe(Constant.Granted);
466
+ // Verify _clck cookie is set when analytics granted (even if ads denied)
467
+ expect(consentResult.hasClckCookie).toBe(true);
468
+ expect(consentResult.clckCookieValue).not.toBe("");
469
+ });
470
+
471
+ test("consentv2 grants consent: track=false → granted/granted sets cookies", async ({ page }) => {
472
+ await page.evaluate(setupCookieMock as () => void);
473
+
474
+ // Verify initial state - no cookies before Clarity starts
475
+ const initialState = await page.evaluate(({ sessionKey, cookieKey }) => {
476
+ const cookies = document.cookie;
477
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
478
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
479
+ return {
480
+ cookies,
481
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
482
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
483
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
484
+ clckCookieValue: clckMatch ? clckMatch[1] : ""
485
+ };
486
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
487
+
488
+ expect(initialState.cookies).toBe("");
489
+ expect(initialState.hasClskCookie).toBe(false);
490
+ expect(initialState.hasClckCookie).toBe(false);
491
+
492
+ // Start with track=false (implicit denied) and verify initial consent state
493
+ const initialConsent = await page.evaluate(({ sessionKey, cookieKey }) => {
494
+ return new Promise((resolve) => {
495
+ (window as any).clarity("start", {
496
+ projectId: "test",
497
+ track: false,
498
+ upload: false
499
+ });
500
+
501
+ (window as any).clarity("metadata", (_data: any, _upgrade: any, consent: any) => {
502
+ if (consent) {
503
+ const cookies = document.cookie;
504
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
505
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
506
+ resolve({
507
+ consent,
508
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
509
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
510
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
511
+ clckCookieValue: clckMatch ? clckMatch[1] : "",
512
+ clckConsentCrumb: clckMatch ? (clckMatch[1].split("^")[3] || "") : "",
513
+ cookies
514
+ });
515
+ }
516
+ }, false, false, true);
517
+
518
+ setTimeout(() => resolve(null), (window as any).CONSENT_CALLBACK_TIMEOUT);
519
+ });
520
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
521
+
522
+ expect(initialConsent).not.toBeNull();
523
+ const initialResult = initialConsent as ConsentTestResult;
524
+ expect(initialResult.consent.source).toBe(ConsentSource.Implicit);
525
+ expect(initialResult.consent.ad_Storage).toBe(Constant.Denied);
526
+ expect(initialResult.consent.analytics_Storage).toBe(Constant.Denied);
527
+ // Cookies should not be set with track=false
528
+ expect(initialResult.hasClskCookie).toBe(false);
529
+ expect(initialResult.hasClckCookie).toBe(false);
530
+
531
+ // Grant consent via consentv2 API
532
+ const updatedConsent = await page.evaluate(({ sessionKey, cookieKey }) => {
533
+ return new Promise((resolve) => {
534
+ (window as any).clarity("consentv2", {
535
+ ad_Storage: "granted",
536
+ analytics_Storage: "granted"
537
+ });
538
+
539
+ (window as any).clarity("metadata", (_data: any, _upgrade: any, consent: any) => {
540
+ if (consent) {
541
+ const cookies = document.cookie;
542
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
543
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
544
+ resolve({
545
+ consent,
546
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
547
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
548
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
549
+ clckCookieValue: clckMatch ? clckMatch[1] : "",
550
+ clckConsentCrumb: clckMatch ? (clckMatch[1].split("^")[3] || "") : "",
551
+ cookies
552
+ });
553
+ }
554
+ }, false, false, true);
555
+
556
+ setTimeout(() => resolve(null), (window as any).CONSENT_CALLBACK_TIMEOUT);
557
+ });
558
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
559
+
560
+ expect(updatedConsent).not.toBeNull();
561
+ const updatedResult = updatedConsent as ConsentTestResult;
562
+ expect(updatedResult.consent.source).toBe(ConsentSource.APIv2);
563
+ expect(updatedResult.consent.ad_Storage).toBe(Constant.Granted);
564
+ expect(updatedResult.consent.analytics_Storage).toBe(Constant.Granted);
565
+ // Verify cookies are set when consent is granted
566
+ expect(updatedResult.hasClskCookie).toBe(true);
567
+ expect(updatedResult.hasClckCookie).toBe(true);
568
+ expect(updatedResult.clskCookieValue).not.toBe("");
569
+ expect(updatedResult.clckCookieValue).not.toBe("");
570
+ });
571
+
572
+ // ========================
573
+ // track=true tests
574
+ // ========================
575
+
576
+ test("implicit granted: track=true results in granted consent", async ({ page }) => {
577
+ await page.evaluate(setupCookieMock as () => void);
578
+
579
+ // Verify initial state - no cookies before Clarity starts
580
+ const initialState = await page.evaluate(({ sessionKey, cookieKey }) => {
581
+ const cookies = document.cookie;
582
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
583
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
584
+ return {
585
+ cookies,
586
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
587
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
588
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
589
+ clckCookieValue: clckMatch ? clckMatch[1] : ""
590
+ };
591
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
592
+
593
+ expect(initialState.cookies).toBe("");
594
+ expect(initialState.hasClskCookie).toBe(false);
595
+ expect(initialState.hasClckCookie).toBe(false);
596
+
597
+ // Start clarity with track=true (implicit granted consent)
598
+ const result = await page.evaluate(({ sessionKey, cookieKey }) => {
599
+ return new Promise((resolve) => {
600
+ (window as any).clarity("start", {
601
+ projectId: "test",
602
+ track: true,
603
+ upload: false
604
+ });
605
+
606
+ // Register metadata callback
607
+ // wait=false (don't wait for data), recall=true (resend on changes), consentInfo=true (include consent)
608
+ (window as any).clarity("metadata", (_data: any, _upgrade: any, consent: any) => {
609
+ if (consent) {
610
+ const cookies = document.cookie;
611
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
612
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
613
+ resolve({
614
+ consent,
615
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
616
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
617
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
618
+ clckCookieValue: clckMatch ? clckMatch[1] : "",
619
+ clckConsentCrumb: clckMatch ? (clckMatch[1].split("^")[3] || "") : "",
620
+ cookies
621
+ });
622
+ }
623
+ }, false, true, true);
624
+
625
+ // Timeout fallback
626
+ setTimeout(() => {
627
+ const cookies = document.cookie;
628
+ resolve({
629
+ consent: null,
630
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
631
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
632
+ clskCookieValue: "",
633
+ clckCookieValue: "",
634
+ cookies
635
+ });
636
+ }, (window as any).CONSENT_CALLBACK_TIMEOUT);
637
+ });
638
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
639
+
640
+ expect(result).not.toBeNull();
641
+ const consentResult = result as ConsentTestResult;
642
+ expect(consentResult.consent).not.toBeNull();
643
+ expect(consentResult.consent.source).toBe(ConsentSource.Implicit);
644
+ expect(consentResult.consent.ad_Storage).toBe(Constant.Granted);
645
+ expect(consentResult.consent.analytics_Storage).toBe(Constant.Granted);
646
+ // With track=true, _clck cookie should be set
647
+ expect(consentResult.hasClckCookie).toBe(true);
648
+ expect(consentResult.clckCookieValue).not.toBe("");
649
+ });
650
+
651
+ test("consentv2 revokes consent: track=true → denied/denied deletes cookies", async ({ page }) => {
652
+ await page.evaluate(setupCookieMock as () => void);
653
+
654
+ // Verify initial state - no cookies before Clarity starts
655
+ const initialState = await page.evaluate(({ sessionKey, cookieKey }) => {
656
+ const cookies = document.cookie;
657
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
658
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
659
+ return {
660
+ cookies,
661
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
662
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
663
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
664
+ clckCookieValue: clckMatch ? clckMatch[1] : ""
665
+ };
666
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
667
+
668
+ expect(initialState.cookies).toBe("");
669
+ expect(initialState.hasClskCookie).toBe(false);
670
+ expect(initialState.hasClckCookie).toBe(false);
671
+
672
+ // Step 1: Start with track=true and verify initial granted state with cookies
673
+ const initialConsent = await page.evaluate(({ sessionKey, cookieKey }) => {
674
+ return new Promise((resolve) => {
675
+ (window as any).clarity("start", {
676
+ projectId: "test",
677
+ track: true,
678
+ upload: false
679
+ });
680
+
681
+ // Get initial consent state
682
+ (window as any).clarity("metadata", (_data: any, _upgrade: any, consent: any) => {
683
+ if (consent) {
684
+ const cookies = document.cookie;
685
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
686
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
687
+ resolve({
688
+ consent,
689
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
690
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
691
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
692
+ clckCookieValue: clckMatch ? clckMatch[1] : "",
693
+ clckConsentCrumb: clckMatch ? (clckMatch[1].split("^")[3] || "") : "",
694
+ cookies
695
+ });
696
+ }
697
+ }, false, false, true);
698
+
699
+ setTimeout(() => resolve(null), (window as any).CONSENT_CALLBACK_TIMEOUT);
700
+ });
701
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
702
+
703
+ // Verify initial state: implicit granted with cookies
704
+ expect(initialConsent).not.toBeNull();
705
+ const initialResult = initialConsent as ConsentTestResult;
706
+ expect(initialResult.consent.source).toBe(ConsentSource.Implicit);
707
+ expect(initialResult.consent.ad_Storage).toBe(Constant.Granted);
708
+ expect(initialResult.consent.analytics_Storage).toBe(Constant.Granted);
709
+ expect(initialResult.hasClckCookie).toBe(true);
710
+
711
+ // Step 2: Call consentv2 API to deny consent and verify cookies are deleted
712
+ const updatedConsent = await page.evaluate(({ sessionKey, cookieKey }) => {
713
+ return new Promise((resolve) => {
714
+ (window as any).clarity("consentv2", {
715
+ ad_Storage: "denied",
716
+ analytics_Storage: "denied"
717
+ });
718
+
719
+ // Get updated consent state and cookie status
720
+ (window as any).clarity("metadata", (_data: any, _upgrade: any, consent: any) => {
721
+ if (consent) {
722
+ const cookies = document.cookie;
723
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
724
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
725
+ resolve({
726
+ consent,
727
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
728
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
729
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
730
+ clckCookieValue: clckMatch ? clckMatch[1] : "",
731
+ clckConsentCrumb: clckMatch ? (clckMatch[1].split("^")[3] || "") : "",
732
+ cookies
733
+ });
734
+ }
735
+ }, false, false, true);
736
+
737
+ setTimeout(() => resolve(null), (window as any).CONSENT_CALLBACK_TIMEOUT);
738
+ });
739
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
740
+
741
+ // Verify updated state: explicit denied with cookies deleted
742
+ expect(updatedConsent).not.toBeNull();
743
+ const updatedResult = updatedConsent as ConsentTestResult;
744
+ expect(updatedResult.consent.source).toBe(ConsentSource.APIv2);
745
+ expect(updatedResult.consent.ad_Storage).toBe(Constant.Denied);
746
+ expect(updatedResult.consent.analytics_Storage).toBe(Constant.Denied);
747
+
748
+ // Verify both cookies are deleted after consent is denied
749
+ expect(updatedResult.hasClskCookie).toBe(false);
750
+ expect(updatedResult.hasClckCookie).toBe(false);
751
+ });
752
+
753
+ test("consentv2 mixed consent: track=true → granted analytics, denied ads keeps cookies", async ({ page }) => {
754
+ await page.evaluate(setupCookieMock as () => void);
755
+
756
+ // Verify initial state - no cookies before Clarity starts
757
+ const initialState = await page.evaluate(({ sessionKey, cookieKey }) => {
758
+ const cookies = document.cookie;
759
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
760
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
761
+ return {
762
+ cookies,
763
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
764
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
765
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
766
+ clckCookieValue: clckMatch ? clckMatch[1] : ""
767
+ };
768
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
769
+
770
+ expect(initialState.cookies).toBe("");
771
+ expect(initialState.hasClskCookie).toBe(false);
772
+ expect(initialState.hasClckCookie).toBe(false);
773
+
774
+ const result = await page.evaluate(({ sessionKey, cookieKey }) => {
775
+ return new Promise((resolve) => {
776
+ (window as any).clarity("start", {
777
+ projectId: "test",
778
+ track: true,
779
+ upload: false
780
+ });
781
+
782
+ (window as any).clarity("consentv2", {
783
+ ad_Storage: "denied",
784
+ analytics_Storage: "granted"
785
+ });
786
+
787
+ (window as any).clarity("metadata", (_data: any, _upgrade: any, consent: any) => {
788
+ if (consent) {
789
+ const cookies = document.cookie;
790
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
791
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
792
+ resolve({
793
+ consent,
794
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
795
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
796
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
797
+ clckCookieValue: clckMatch ? clckMatch[1] : "",
798
+ clckConsentCrumb: clckMatch ? (clckMatch[1].split("^")[3] || "") : "",
799
+ cookies
800
+ });
801
+ }
802
+ }, false, false, true);
803
+
804
+ setTimeout(() => resolve(null), (window as any).CONSENT_CALLBACK_TIMEOUT);
805
+ });
806
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
807
+
808
+ expect(result).not.toBeNull();
809
+ const consentResult = result as ConsentTestResult;
810
+ expect(consentResult.consent.source).toBe(ConsentSource.APIv2);
811
+ expect(consentResult.consent.ad_Storage).toBe(Constant.Denied);
812
+ expect(consentResult.consent.analytics_Storage).toBe(Constant.Granted);
813
+ // Verify cookies remain when analytics is granted (even if ads is denied)
814
+ expect(consentResult.hasClskCookie).toBe(true);
815
+ expect(consentResult.hasClckCookie).toBe(true);
816
+ expect(consentResult.clskCookieValue).not.toBe("");
817
+ expect(consentResult.clckCookieValue).not.toBe("");
818
+ });
819
+
820
+ test("consentv2 mixed consent: track=true → denied analytics, granted ads deletes cookies", async ({ page }) => {
821
+ await page.evaluate(setupCookieMock as () => void);
822
+
823
+ // Verify initial state - no cookies before Clarity starts
824
+ const initialState = await page.evaluate(({ sessionKey, cookieKey }) => {
825
+ const cookies = document.cookie;
826
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
827
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
828
+ return {
829
+ cookies,
830
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
831
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
832
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
833
+ clckCookieValue: clckMatch ? clckMatch[1] : ""
834
+ };
835
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
836
+
837
+ expect(initialState.cookies).toBe("");
838
+ expect(initialState.hasClskCookie).toBe(false);
839
+ expect(initialState.hasClckCookie).toBe(false);
840
+
841
+ // Start with track=true, then deny analytics but grant ads
842
+ const result = await page.evaluate(({ sessionKey, cookieKey }) => {
843
+ return new Promise((resolve) => {
844
+ (window as any).clarity("start", {
845
+ projectId: "test",
846
+ track: true,
847
+ upload: false
848
+ });
849
+
850
+ setTimeout(() => {
851
+ (window as any).clarity("consentv2", {
852
+ ad_Storage: "granted",
853
+ analytics_Storage: "denied"
854
+ });
855
+
856
+ (window as any).clarity("metadata", (_data: any, _upgrade: any, consent: any) => {
857
+ if (consent) {
858
+ const cookies = document.cookie;
859
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
860
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
861
+ resolve({
862
+ consent,
863
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
864
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
865
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
866
+ clckCookieValue: clckMatch ? clckMatch[1] : "",
867
+ clckConsentCrumb: clckMatch ? (clckMatch[1].split("^")[3] || "") : "",
868
+ cookies
869
+ });
870
+ }
871
+ }, false, false, true);
872
+ }, (window as any).COOKIE_SETUP_DELAY);
873
+
874
+ setTimeout(() => resolve(null), (window as any).CONSENT_CALLBACK_TIMEOUT);
875
+ });
876
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
877
+
878
+ expect(result).not.toBeNull();
879
+ const consentResult = result as ConsentTestResult;
880
+ expect(consentResult.consent.source).toBe(ConsentSource.APIv2);
881
+ expect(consentResult.consent.ad_Storage).toBe(Constant.Granted);
882
+ expect(consentResult.consent.analytics_Storage).toBe(Constant.Denied);
883
+ // Verify cookies are deleted when analytics denied (even if ads granted)
884
+ expect(consentResult.hasClskCookie).toBe(false);
885
+ expect(consentResult.hasClckCookie).toBe(false);
886
+ expect(consentResult.clskCookieValue).toBe("");
887
+ expect(consentResult.clckCookieValue).toBe("");
888
+ });
889
+
890
+ test("consentv2 maintains consent: track=true → granted/granted keeps cookies", async ({ page }) => {
891
+ await page.evaluate(setupCookieMock as () => void);
892
+
893
+ // Verify initial state - no cookies before Clarity starts
894
+ const initialState = await page.evaluate(({ sessionKey, cookieKey }) => {
895
+ const cookies = document.cookie;
896
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
897
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
898
+ return {
899
+ cookies,
900
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
901
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
902
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
903
+ clckCookieValue: clckMatch ? clckMatch[1] : ""
904
+ };
905
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
906
+
907
+ expect(initialState.cookies).toBe("");
908
+ expect(initialState.hasClskCookie).toBe(false);
909
+ expect(initialState.hasClckCookie).toBe(false);
910
+
911
+ // Start with track=true (implicit granted)
912
+ const initialConsent = await page.evaluate(({ sessionKey, cookieKey }) => {
913
+ return new Promise((resolve) => {
914
+ (window as any).clarity("start", {
915
+ projectId: "test",
916
+ track: true,
917
+ upload: false
918
+ });
919
+
920
+ // Wait a bit for cookies to be set, then check
921
+ setTimeout(() => {
922
+ (window as any).clarity("metadata", (_data: any, _upgrade: any, consent: any) => {
923
+ if (consent) {
924
+ const cookies = document.cookie;
925
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
926
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
927
+ resolve({
928
+ consent,
929
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
930
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
931
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
932
+ clckCookieValue: clckMatch ? clckMatch[1] : "",
933
+ clckConsentCrumb: clckMatch ? (clckMatch[1].split("^")[3] || "") : "",
934
+ cookies
935
+ });
936
+ }
937
+ }, false, false, true);
938
+ }, (window as any).COOKIE_SETUP_DELAY);
939
+
940
+ setTimeout(() => resolve(null), (window as any).CONSENT_CALLBACK_TIMEOUT);
941
+ });
942
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
943
+
944
+ // Verify initial state: implicit granted with cookies
945
+ expect(initialConsent).not.toBeNull();
946
+ const initialResult = initialConsent as ConsentTestResult;
947
+ expect(initialResult.consent.source).toBe(ConsentSource.Implicit);
948
+ expect(initialResult.consent.ad_Storage).toBe(Constant.Granted);
949
+ expect(initialResult.consent.analytics_Storage).toBe(Constant.Granted);
950
+ // Note: _clsk cookie is only written during upload operations (metadata.save()), not during initial setup
951
+ // With upload: false in test config, only _clck is written by track() function during initialization
952
+ expect(initialResult.hasClckCookie).toBe(true);
953
+ expect(initialResult.clckCookieValue).not.toBe("");
954
+
955
+ // Explicitly grant via consentv2 API (should maintain granted state)
956
+ const result = await page.evaluate(({ sessionKey, cookieKey }) => {
957
+ return new Promise((resolve) => {
958
+ (window as any).clarity("consentv2", {
959
+ ad_Storage: "granted",
960
+ analytics_Storage: "granted"
961
+ });
962
+
963
+ (window as any).clarity("metadata", (_data: any, _upgrade: any, consent: any) => {
964
+ if (consent) {
965
+ const cookies = document.cookie;
966
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
967
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
968
+ resolve({
969
+ consent,
970
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
971
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
972
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
973
+ clckCookieValue: clckMatch ? clckMatch[1] : "",
974
+ clckConsentCrumb: clckMatch ? (clckMatch[1].split("^")[3] || "") : "",
975
+ cookies
976
+ });
977
+ }
978
+ }, false, false, true);
979
+
980
+ setTimeout(() => resolve(null), (window as any).CONSENT_CALLBACK_TIMEOUT);
981
+ });
982
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
983
+
984
+ expect(result).not.toBeNull();
985
+ const consentResult = result as ConsentTestResult;
986
+ expect(consentResult.consent.source).toBe(ConsentSource.APIv2);
987
+ expect(consentResult.consent.ad_Storage).toBe(Constant.Granted);
988
+ expect(consentResult.consent.analytics_Storage).toBe(Constant.Granted);
989
+ // Verify _clck cookie remains set (user ID/consent preference cookie)
990
+ // Note: _clsk is only written during upload operations, so we only check _clck in tests with upload: false
991
+ expect(consentResult.hasClckCookie).toBe(true);
992
+ expect(consentResult.clckCookieValue).not.toBe("");
993
+ });
994
+
995
+ // ========================
996
+ // V1 Consent API tests
997
+ // The V1 API uses clarity("consent", boolean) where true=granted, false=denied
998
+ // It sets both ad_Storage and analytics_Storage to the same value
999
+ // ========================
1000
+
1001
+ test("consent v1: track=false → consent(true) grants consent and sets cookies", async ({ page }) => {
1002
+ await page.evaluate(setupCookieMock as () => void);
1003
+
1004
+ // Verify initial state - no cookies before Clarity starts
1005
+ const initialState = await page.evaluate(({ sessionKey, cookieKey }) => {
1006
+ const cookies = document.cookie;
1007
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
1008
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
1009
+ return {
1010
+ cookies,
1011
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
1012
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
1013
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
1014
+ clckCookieValue: clckMatch ? clckMatch[1] : ""
1015
+ };
1016
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
1017
+
1018
+ expect(initialState.cookies).toBe("");
1019
+ expect(initialState.hasClskCookie).toBe(false);
1020
+ expect(initialState.hasClckCookie).toBe(false);
1021
+
1022
+ // Start with track=false (implicit denied), then grant consent via V1 API
1023
+ const result = await page.evaluate(({ sessionKey, cookieKey }) => {
1024
+ return new Promise((resolve) => {
1025
+ (window as any).clarity("start", {
1026
+ projectId: "test",
1027
+ track: false,
1028
+ upload: false
1029
+ });
1030
+
1031
+ setTimeout(() => {
1032
+ // V1 API: consent(true) grants both ad_Storage and analytics_Storage
1033
+ (window as any).clarity("consent", true);
1034
+
1035
+ (window as any).clarity("metadata", (_data: any, _upgrade: any, consent: any) => {
1036
+ if (consent) {
1037
+ const cookies = document.cookie;
1038
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
1039
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
1040
+ resolve({
1041
+ consent,
1042
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
1043
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
1044
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
1045
+ clckCookieValue: clckMatch ? clckMatch[1] : "",
1046
+ clckConsentCrumb: clckMatch ? (clckMatch[1].split("^")[3] || "") : "",
1047
+ cookies
1048
+ });
1049
+ }
1050
+ }, false, false, true);
1051
+ }, (window as any).COOKIE_SETUP_DELAY);
1052
+
1053
+ setTimeout(() => resolve(null), (window as any).CONSENT_CALLBACK_TIMEOUT);
1054
+ });
1055
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
1056
+
1057
+ expect(result).not.toBeNull();
1058
+ const consentResult = result as ConsentTestResult;
1059
+ expect(consentResult.consent.source).toBe(ConsentSource.APIv1);
1060
+ expect(consentResult.consent.ad_Storage).toBe(Constant.Granted);
1061
+ expect(consentResult.consent.analytics_Storage).toBe(Constant.Granted);
1062
+ // Verify cookies are set when consent is granted via V1 API
1063
+ expect(consentResult.hasClckCookie).toBe(true);
1064
+ expect(consentResult.clckCookieValue).not.toBe("");
1065
+ });
1066
+
1067
+ test("consent v1: track=true → consent(false) revokes consent and deletes cookies", async ({ page }) => {
1068
+ await page.evaluate(setupCookieMock as () => void);
1069
+
1070
+ // Verify initial state - no cookies before Clarity starts
1071
+ const initialState = await page.evaluate(({ sessionKey, cookieKey }) => {
1072
+ const cookies = document.cookie;
1073
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
1074
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
1075
+ return {
1076
+ cookies,
1077
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
1078
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
1079
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
1080
+ clckCookieValue: clckMatch ? clckMatch[1] : ""
1081
+ };
1082
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
1083
+
1084
+ expect(initialState.cookies).toBe("");
1085
+ expect(initialState.hasClskCookie).toBe(false);
1086
+ expect(initialState.hasClckCookie).toBe(false);
1087
+
1088
+ // Start with track=true (implicit granted) and verify initial state
1089
+ const initialConsent = await page.evaluate(({ sessionKey, cookieKey }) => {
1090
+ return new Promise((resolve) => {
1091
+ (window as any).clarity("start", {
1092
+ projectId: "test",
1093
+ track: true,
1094
+ upload: false
1095
+ });
1096
+
1097
+ (window as any).clarity("metadata", (_data: any, _upgrade: any, consent: any) => {
1098
+ if (consent) {
1099
+ const cookies = document.cookie;
1100
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
1101
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
1102
+ resolve({
1103
+ consent,
1104
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
1105
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
1106
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
1107
+ clckCookieValue: clckMatch ? clckMatch[1] : "",
1108
+ clckConsentCrumb: clckMatch ? (clckMatch[1].split("^")[3] || "") : "",
1109
+ cookies
1110
+ });
1111
+ }
1112
+ }, false, false, true);
1113
+
1114
+ setTimeout(() => resolve(null), (window as any).CONSENT_CALLBACK_TIMEOUT);
1115
+ });
1116
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
1117
+
1118
+ // Verify initial state: implicit granted with cookies
1119
+ expect(initialConsent).not.toBeNull();
1120
+ const initialResult = initialConsent as ConsentTestResult;
1121
+ expect(initialResult.consent.source).toBe(ConsentSource.Implicit);
1122
+ expect(initialResult.consent.ad_Storage).toBe(Constant.Granted);
1123
+ expect(initialResult.consent.analytics_Storage).toBe(Constant.Granted);
1124
+ expect(initialResult.hasClckCookie).toBe(true);
1125
+
1126
+ // Revoke consent via V1 API
1127
+ const result = await page.evaluate(({ sessionKey, cookieKey }) => {
1128
+ return new Promise((resolve) => {
1129
+ // V1 API: consent(false) denies both ad_Storage and analytics_Storage
1130
+ (window as any).clarity("consent", false);
1131
+
1132
+ (window as any).clarity("metadata", (_data: any, _upgrade: any, consent: any) => {
1133
+ if (consent) {
1134
+ const cookies = document.cookie;
1135
+ const clskMatch = cookies.match(new RegExp(`${sessionKey}=([^;]+)`));
1136
+ const clckMatch = cookies.match(new RegExp(`${cookieKey}=([^;]+)`));
1137
+ resolve({
1138
+ consent,
1139
+ hasClskCookie: cookies.includes(`${sessionKey}=`),
1140
+ hasClckCookie: cookies.includes(`${cookieKey}=`),
1141
+ clskCookieValue: clskMatch ? clskMatch[1] : "",
1142
+ clckCookieValue: clckMatch ? clckMatch[1] : "",
1143
+ clckConsentCrumb: clckMatch ? (clckMatch[1].split("^")[3] || "") : "",
1144
+ cookies
1145
+ });
1146
+ }
1147
+ }, false, false, true);
1148
+
1149
+ setTimeout(() => resolve(null), (window as any).CONSENT_CALLBACK_TIMEOUT);
1150
+ });
1151
+ }, { sessionKey: Constant.SessionKey, cookieKey: Constant.CookieKey });
1152
+
1153
+ expect(result).not.toBeNull();
1154
+ const consentResult = result as ConsentTestResult;
1155
+ expect(consentResult.consent.source).toBe(ConsentSource.APIv1);
1156
+ expect(consentResult.consent.ad_Storage).toBe(Constant.Denied);
1157
+ expect(consentResult.consent.analytics_Storage).toBe(Constant.Denied);
1158
+ // Verify cookies are deleted when consent is revoked via V1 API
1159
+ expect(consentResult.hasClskCookie).toBe(false);
1160
+ expect(consentResult.hasClckCookie).toBe(false);
1161
+ expect(consentResult.clskCookieValue).toBe("");
1162
+ expect(consentResult.clckCookieValue).toBe("");
1163
+ });
1164
+
1165
+ // ========================
1166
+ // Config cookies tests
1167
+ // ========================
1168
+
1169
+ interface ConfigCookieTestOptions {
1170
+ track: boolean;
1171
+ grantConsentAfterStart?: boolean;
1172
+ denyConsentAfterStart?: boolean;
1173
+ }
1174
+
1175
+ interface ConfigCookieTestResult {
1176
+ containsCookieValue: boolean;
1177
+ beforeChangeContainsCookie?: boolean;
1178
+ afterChangeContainsCookie?: boolean;
1179
+ }
1180
+
1181
+ /**
1182
+ * Helper to run config cookie tests with consistent setup.
1183
+ * Sets up cookie mock with marketing_id=abc123 and returns whether the cookie value appears in payloads.
1184
+ */
1185
+ async function runConfigCookieTest(page: any, options: ConfigCookieTestOptions): Promise<ConfigCookieTestResult> {
1186
+ // Set up cookie mock with initial marketing cookie before evaluating test logic
1187
+ await page.evaluate(setupCookieMock as (initialValue: string) => void, "marketing_id=abc123");
1188
+
1189
+ const result = await page.evaluate((opts: ConfigCookieTestOptions) => {
1190
+
1191
+ return new Promise((resolve) => {
1192
+ const payloadsBefore: string[] = [];
1193
+ const payloadsAfter: string[] = [];
1194
+ let phase = "before";
1195
+
1196
+ (window as any).clarity("start", {
1197
+ projectId: "test",
1198
+ track: opts.track,
1199
+ cookies: ["marketing_id"],
1200
+ upload: (payload: string) => {
1201
+ if (phase === "before") {
1202
+ payloadsBefore.push(payload);
1203
+ } else {
1204
+ payloadsAfter.push(payload);
1205
+ }
1206
+ }
1207
+ });
1208
+
1209
+ const hasConsentChange = opts.grantConsentAfterStart || opts.denyConsentAfterStart;
1210
+ // For denial tests, use longer delay to ensure initial upload completes before denial
1211
+ const consentChangeDelay = opts.denyConsentAfterStart
1212
+ ? (window as any).CONSENT_CALLBACK_TIMEOUT / 2
1213
+ : (window as any).COOKIE_SETUP_DELAY;
1214
+
1215
+ if (hasConsentChange) {
1216
+ setTimeout(() => {
1217
+ phase = "after";
1218
+ const adStorage = opts.grantConsentAfterStart ? "granted" : "denied";
1219
+ const analyticsStorage = opts.grantConsentAfterStart ? "granted" : "denied";
1220
+ (window as any).clarity("consentv2", { ad_Storage: adStorage, analytics_Storage: analyticsStorage });
1221
+ }, consentChangeDelay);
1222
+ }
1223
+
1224
+ // For denial tests, wait longer to allow for restart
1225
+ const totalTimeout = opts.denyConsentAfterStart
1226
+ ? consentChangeDelay + (window as any).CONSENT_CALLBACK_TIMEOUT
1227
+ : (window as any).CONSENT_CALLBACK_TIMEOUT;
1228
+
1229
+ setTimeout(() => {
1230
+ const allPayloads = [...payloadsBefore, ...payloadsAfter].join("");
1231
+ resolve({
1232
+ containsCookieValue: allPayloads.includes("abc123"),
1233
+ beforeChangeContainsCookie: payloadsBefore.join("").includes("abc123"),
1234
+ afterChangeContainsCookie: payloadsAfter.join("").includes("abc123")
1235
+ });
1236
+ }, totalTimeout);
1237
+ });
1238
+ }, options);
1239
+
1240
+ return result as ConfigCookieTestResult;
1241
+ }
1242
+
1243
+ test("config cookies: track=true logs config cookies as variables", async ({ page }) => {
1244
+ const result = await runConfigCookieTest(page, { track: true });
1245
+ expect(result.containsCookieValue).toBe(true);
1246
+ });
1247
+
1248
+ test("config cookies: track=false does not log config cookies", async ({ page }) => {
1249
+ const result = await runConfigCookieTest(page, { track: false });
1250
+ expect(result.containsCookieValue).toBe(false);
1251
+ });
1252
+
1253
+ test("config cookies: track=false then consentv2 grants consent logs config cookies", async ({ page }) => {
1254
+ const result = await runConfigCookieTest(page, { track: false, grantConsentAfterStart: true });
1255
+ expect(result.containsCookieValue).toBe(true);
1256
+ });
1257
+
1258
+ test("config cookies: track=true then consentv2 denies consent does not log cookies after restart", async ({ page }) => {
1259
+ const result = await runConfigCookieTest(page, { track: true, denyConsentAfterStart: true });
1260
+ // Before denial, cookies should be logged (track=true, implicit granted)
1261
+ expect(result.beforeChangeContainsCookie).toBe(true);
1262
+ // After restart with denied consent, cookies should NOT be logged
1263
+ expect(result.afterChangeContainsCookie).toBe(false);
1264
+ });
1265
+
1266
+ test("GCM error handling: handles misconfigured GTM gracefully", async ({ page }) => {
1267
+ await page.evaluate(setupCookieMock as () => void);
1268
+
1269
+ const result = await page.evaluate(({ clarityJs }) => {
1270
+ return new Promise<{ errorWasThrown: boolean }>((resolve) => {
1271
+ let errorWasThrown = false;
1272
+
1273
+ // Mock GTM with misconfigured consent state
1274
+ (window as any).google_tag_data = {
1275
+ ics: {
1276
+ addListener: function(_events: string[], callback: () => void) {
1277
+ setTimeout(() => callback(), 100);
1278
+ },
1279
+ // getConsentState throws error due to uninitialized internal state
1280
+ getConsentState: function() {
1281
+ errorWasThrown = true;
1282
+ throw new TypeError("Cannot read properties of undefined (reading 'usedContainerScopedDefaults')");
1283
+ }
1284
+ }
1285
+ };
1286
+
1287
+ // Load and execute Clarity
1288
+ eval(clarityJs);
1289
+
1290
+ // Start Clarity with track=true
1291
+ (window as any).clarity("start", {
1292
+ projectId: "test",
1293
+ track: true,
1294
+ upload: false
1295
+ });
1296
+
1297
+ // Wait for callback to be triggered and error to be thrown
1298
+ setTimeout(() => {
1299
+ resolve({
1300
+ errorWasThrown
1301
+ });
1302
+ }, 500);
1303
+ });
1304
+ }, { clarityJs: readFileSync(clarityJsPath, "utf-8") });
1305
+
1306
+ // Verify error was thrown and handled gracefully (test success proves it was caught)
1307
+ expect(result.errorWasThrown).toBe(true);
1308
+ });
1309
+ });