@owlmeans/consent 0.1.18-rc.8 → 0.1.18

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 (120) hide show
  1. package/README.md +229 -0
  2. package/build/consts.d.ts +84 -23
  3. package/build/consts.d.ts.map +1 -1
  4. package/build/consts.js +110 -29
  5. package/build/consts.js.map +1 -1
  6. package/build/consts.local.d.ts +12 -0
  7. package/build/consts.local.d.ts.map +1 -0
  8. package/build/consts.local.js +12 -0
  9. package/build/consts.local.js.map +1 -0
  10. package/build/geo/types.d.ts +47 -0
  11. package/build/geo/types.d.ts.map +1 -0
  12. package/build/geo/types.js +2 -0
  13. package/build/geo/types.js.map +1 -0
  14. package/build/geo.d.ts +4 -0
  15. package/build/geo.d.ts.map +1 -0
  16. package/build/geo.js +96 -0
  17. package/build/geo.js.map +1 -0
  18. package/build/gtobserver/types.d.ts +75 -0
  19. package/build/gtobserver/types.d.ts.map +1 -0
  20. package/build/gtobserver/types.js +2 -0
  21. package/build/gtobserver/types.js.map +1 -0
  22. package/build/gtobserver.d.ts +7 -0
  23. package/build/gtobserver.d.ts.map +1 -0
  24. package/build/gtobserver.js +142 -0
  25. package/build/gtobserver.js.map +1 -0
  26. package/build/i18n/be.json +9 -4
  27. package/build/i18n/de.json +9 -4
  28. package/build/i18n/en.json +9 -4
  29. package/build/i18n/es.json +9 -4
  30. package/build/i18n/fr.json +9 -4
  31. package/build/i18n/pl.json +9 -4
  32. package/build/i18n/ru.json +9 -4
  33. package/build/i18n/types.d.ts +11 -0
  34. package/build/i18n/types.d.ts.map +1 -0
  35. package/build/i18n/types.js +2 -0
  36. package/build/i18n/types.js.map +1 -0
  37. package/build/i18n/uk.json +9 -4
  38. package/build/i18n.d.ts +4 -14
  39. package/build/i18n.d.ts.map +1 -1
  40. package/build/i18n.js +12 -27
  41. package/build/i18n.js.map +1 -1
  42. package/build/index.d.ts +13 -6
  43. package/build/index.d.ts.map +1 -1
  44. package/build/index.js +5 -4
  45. package/build/index.js.map +1 -1
  46. package/build/linker/types.d.ts +59 -0
  47. package/build/linker/types.d.ts.map +1 -0
  48. package/build/linker/types.js +2 -0
  49. package/build/linker/types.js.map +1 -0
  50. package/build/linker.d.ts +5 -76
  51. package/build/linker.d.ts.map +1 -1
  52. package/build/linker.js +290 -339
  53. package/build/linker.js.map +1 -1
  54. package/build/plugins/types.d.ts +42 -0
  55. package/build/plugins/types.d.ts.map +1 -0
  56. package/build/plugins/types.js +2 -0
  57. package/build/plugins/types.js.map +1 -0
  58. package/build/plugins.d.ts +4 -69
  59. package/build/plugins.d.ts.map +1 -1
  60. package/build/plugins.js +73 -62
  61. package/build/plugins.js.map +1 -1
  62. package/build/storage/types.d.ts +31 -0
  63. package/build/storage/types.d.ts.map +1 -0
  64. package/build/storage/types.js +2 -0
  65. package/build/storage/types.js.map +1 -0
  66. package/build/storage.d.ts +6 -22
  67. package/build/storage.d.ts.map +1 -1
  68. package/build/storage.js +72 -81
  69. package/build/storage.js.map +1 -1
  70. package/build/store.d.ts +2 -2
  71. package/build/store.d.ts.map +1 -1
  72. package/build/store.js +116 -67
  73. package/build/store.js.map +1 -1
  74. package/build/types.d.ts +147 -26
  75. package/build/types.d.ts.map +1 -1
  76. package/build/types.local.d.ts +9 -0
  77. package/build/types.local.d.ts.map +1 -0
  78. package/build/types.local.js +2 -0
  79. package/build/types.local.js.map +1 -0
  80. package/package.json +3 -3
  81. package/src/consts.local.ts +16 -0
  82. package/src/consts.ts +126 -34
  83. package/src/geo/types.ts +49 -0
  84. package/src/geo.ts +122 -0
  85. package/src/gtobserver/types.ts +73 -0
  86. package/src/gtobserver.ts +182 -0
  87. package/src/i18n/be.json +9 -4
  88. package/src/i18n/de.json +9 -4
  89. package/src/i18n/en.json +9 -4
  90. package/src/i18n/es.json +9 -4
  91. package/src/i18n/fr.json +9 -4
  92. package/src/i18n/pl.json +9 -4
  93. package/src/i18n/ru.json +9 -4
  94. package/src/i18n/types.ts +11 -0
  95. package/src/i18n/uk.json +9 -4
  96. package/src/i18n.ts +20 -32
  97. package/src/index.ts +13 -13
  98. package/src/linker/types.ts +59 -0
  99. package/src/linker.ts +301 -364
  100. package/src/plugins/types.ts +42 -0
  101. package/src/plugins.ts +82 -106
  102. package/src/storage/types.ts +31 -0
  103. package/src/storage.ts +75 -77
  104. package/src/store.ts +122 -69
  105. package/src/types.local.ts +9 -0
  106. package/src/types.ts +153 -25
  107. package/tests/consent.spec.ts +26 -28
  108. package/tests/geo.spec.ts +474 -0
  109. package/tests/linker.spec.ts +144 -309
  110. package/tests/tracking.spec.ts +14 -14
  111. package/build/functional.d.ts +0 -35
  112. package/build/functional.d.ts.map +0 -1
  113. package/build/functional.js +0 -67
  114. package/build/functional.js.map +0 -1
  115. package/build/gtm.d.ts +0 -72
  116. package/build/gtm.d.ts.map +0 -1
  117. package/build/gtm.js +0 -182
  118. package/build/gtm.js.map +0 -1
  119. package/src/functional.ts +0 -73
  120. package/src/gtm.ts +0 -220
@@ -1,17 +1,10 @@
1
1
  import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
2
- import {
3
- CONSENT_ANALYTICS, CONSENT_ESSENTIAL, CONSENT_FUNCTIONAL, CONSENT_KEY, CONSENT_LANGUAGE_EVENT,
4
- CONSENT_LANGUAGE_KEY, CONSENT_MARKETING, CONSENT_PENDING_LANGUAGE, DEFAULT_CONSENT_CATEGORIES,
5
- } from '../src/consts.js'
6
- import { consentBootstrapScript } from '../src/gtm.js'
7
- import {
8
- consentLinker, consentLinkerScript, decodeConsentLink, encodeConsentLink, stripConsentLinkParam, writeConsentLanguage,
9
- CONSENT_LINK_MAX_AGE, CONSENT_LINK_PARAM,
10
- } from '../src/linker.js'
11
- import { adoptConsentLanguage, consentDomains, decorateConsentUrl, registerConsentPlugin } from '../src/plugins.js'
2
+ import { CONSENT_ANALYTICS, CONSENT_ESSENTIAL, CONSENT_KEY, CONSENT_LANGUAGE_KEY, CONSENT_MARKETING, DEFAULT_CONSENT_CATEGORIES, CONSENT_LINK_MAX_AGE, CONSENT_LINK_PARAM } from '../src/consts.js'
3
+ import { consentModeHelper } from '../src/gtobserver.js'
4
+ import { consentLinkHelper } from '../src/linker.js'
5
+ import { consentPluginHelper } from '../src/plugins.js'
12
6
  import { makeConsentStore } from '../src/store.js'
13
- import { consentAllowsScript, functionalGranted, functionalKeysOf, writeFunctionalPreference } from '../src/functional.js'
14
- import { readConsent, writeConsent } from '../src/storage.js'
7
+ import { consentStorageHelper } from '../src/storage.js'
15
8
  import type { ConsentRecord } from '../src/types.js'
16
9
 
17
10
  /** A minimal browser: storage, a settable referrer, and a `location`/`history` pair that actually
@@ -74,41 +67,40 @@ beforeEach(() => {
74
67
  env.goto('https://platform.test/dispatcher')
75
68
  delete (globalThis as any).dataLayer
76
69
  delete (globalThis as any).cookieConsentSetup
77
- delete (globalThis as any)[CONSENT_PENDING_LANGUAGE]
78
70
  })
79
71
 
80
72
  describe('encodeConsentLink / decodeConsentLink', () => {
81
73
  test('round-trips a record through the optional categories only', () => {
82
74
  const record: ConsentRecord = { [CONSENT_ESSENTIAL]: true, [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: false }
83
- const encoded = encodeConsentLink(record)
84
- const decoded = decodeConsentLink(encoded)
75
+ const encoded = consentLinkHelper.encodeConsentLink(record)
76
+ const decoded = consentLinkHelper.decodeConsentLink(encoded)
85
77
 
86
78
  expect(decoded?.v).toBe(2)
87
- expect(decoded?.c).toEqual({ [CONSENT_FUNCTIONAL]: 0, [CONSENT_ANALYTICS]: 1, [CONSENT_MARKETING]: 0 })
79
+ expect(decoded?.c).toEqual({ [CONSENT_ANALYTICS]: 1, [CONSENT_MARKETING]: 0 })
88
80
  expect(decoded?.t).toBeGreaterThan(0)
89
81
  })
90
82
 
91
83
  test('rejects garbage rather than throwing', () => {
92
- expect(decodeConsentLink('not-base64url-json')).toBeNull()
93
- expect(decodeConsentLink('')).toBeNull()
84
+ expect(consentLinkHelper.decodeConsentLink('not-base64url-json')).toBeNull()
85
+ expect(consentLinkHelper.decodeConsentLink('')).toBeNull()
94
86
  })
95
87
 
96
88
  test('rejects a payload of the wrong version', () => {
97
89
  const bad = Buffer.from(JSON.stringify({ v: 1, c: {}, t: nowSeconds() })).toString('base64url')
98
90
 
99
- expect(decodeConsentLink(bad)).toBeNull()
91
+ expect(consentLinkHelper.decodeConsentLink(bad)).toBeNull()
100
92
  })
101
93
  })
102
94
 
103
95
  describe('consentLinker().decorate', () => {
104
- const plugin = consentLinker()
96
+ const plugin = consentLinkHelper.consentLinker()
105
97
  const record: ConsentRecord = { [CONSENT_ESSENTIAL]: true, [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: false }
106
98
 
107
99
  test('decorates a listed, foreign host', () => {
108
100
  const decorated = plugin.decorate!(new URL('https://site.test/legal/cookies'), record, linkerOpts)
109
101
 
110
102
  expect(decorated).not.toBeNull()
111
- const payload = decodeConsentLink(decorated!.searchParams.get(CONSENT_LINK_PARAM)!)
103
+ const payload = consentLinkHelper.decodeConsentLink(decorated!.searchParams.get(CONSENT_LINK_PARAM)!)
112
104
  expect(payload?.c[CONSENT_ANALYTICS]).toBe(1)
113
105
  })
114
106
 
@@ -134,8 +126,8 @@ describe('consentLinker().decorate', () => {
134
126
  })
135
127
 
136
128
  describe('consentLinker().adopt — every trust rule', () => {
137
- const plugin = consentLinker()
138
- const valid = () => encodeConsentLink({ [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: false })
129
+ const plugin = consentLinkHelper.consentLinker()
130
+ const valid = () => consentLinkHelper.encodeConsentLink({ [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: false })
139
131
 
140
132
  test('adopts a fresh, referred, fully-covered decision', () => {
141
133
  env.goto(`https://platform.test/dispatcher?owlcc=${valid()}`, 'https://site.test/')
@@ -201,21 +193,21 @@ describe('consentLinker().adopt — every trust rule', () => {
201
193
  describe('stripConsentLinkParam', () => {
202
194
  test('removes only the parameter, keeping the rest and the hash', () => {
203
195
  env.goto('https://platform.test/dispatcher?owlcc=abc&next=%2Fhome&ref=site#panel')
204
- stripConsentLinkParam(linkerOpts)
196
+ consentLinkHelper.stripConsentLinkParam(linkerOpts)
205
197
 
206
198
  expect(env.href()).toBe('https://platform.test/dispatcher?next=%2Fhome&ref=site#panel')
207
199
  })
208
200
 
209
201
  test('is a no-op with no parameter present', () => {
210
202
  env.goto('https://platform.test/dispatcher?next=%2Fhome')
211
- stripConsentLinkParam(linkerOpts)
203
+ consentLinkHelper.stripConsentLinkParam(linkerOpts)
212
204
 
213
205
  expect(env.href()).toBe('https://platform.test/dispatcher?next=%2Fhome')
214
206
  })
215
207
 
216
208
  test('is a no-op with no linker configured', () => {
217
209
  env.goto('https://platform.test/dispatcher?owlcc=abc')
218
- stripConsentLinkParam({})
210
+ consentLinkHelper.stripConsentLinkParam({})
219
211
 
220
212
  expect(env.href()).toBe('https://platform.test/dispatcher?owlcc=abc')
221
213
  })
@@ -223,8 +215,8 @@ describe('stripConsentLinkParam', () => {
223
215
 
224
216
  describe('consentDomains / decorateConsentUrl (registry level)', () => {
225
217
  test('discloses the current host plus every plugin-named domain, deduplicated', () => {
226
- registerConsentPlugin(consentLinker())
227
- const domains = consentDomains(linkerOpts)
218
+ consentPluginHelper.registerConsentPlugin(consentLinkHelper.consentLinker())
219
+ const domains = consentPluginHelper.consentDomains(linkerOpts)
228
220
 
229
221
  expect(domains).toContain('platform.test')
230
222
  expect(domains).toContain('site.test')
@@ -232,18 +224,18 @@ describe('consentDomains / decorateConsentUrl (registry level)', () => {
232
224
  })
233
225
 
234
226
  test('decorateConsentUrl applies the registered plugin to a plain URL', () => {
235
- registerConsentPlugin(consentLinker())
227
+ consentPluginHelper.registerConsentPlugin(consentLinkHelper.consentLinker())
236
228
  const record: ConsentRecord = { [CONSENT_ESSENTIAL]: true, [CONSENT_ANALYTICS]: false, [CONSENT_MARKETING]: false }
237
- const decorated = decorateConsentUrl('https://site.test/legal/cookies', record, linkerOpts)
229
+ const decorated = consentPluginHelper.decorateConsentUrl('https://site.test/legal/cookies', record, linkerOpts)
238
230
 
239
231
  expect(decorated.searchParams.has(CONSENT_LINK_PARAM)).toBe(true)
240
232
  })
241
233
 
242
234
  test('registering the same alias twice replaces rather than duplicates', () => {
243
- registerConsentPlugin(consentLinker())
244
- registerConsentPlugin(consentLinker())
235
+ consentPluginHelper.registerConsentPlugin(consentLinkHelper.consentLinker())
236
+ consentPluginHelper.registerConsentPlugin(consentLinkHelper.consentLinker())
245
237
  const record: ConsentRecord = { [CONSENT_ESSENTIAL]: true }
246
- const decorated = decorateConsentUrl('https://site.test/', record, linkerOpts)
238
+ const decorated = consentPluginHelper.decorateConsentUrl('https://site.test/', record, linkerOpts)
247
239
 
248
240
  // A duplicate `decorate` implementation would still be idempotent (same value both times), so
249
241
  // what this actually guards is that the registry never grows unbounded per alias.
@@ -254,7 +246,7 @@ describe('consentDomains / decorateConsentUrl (registry level)', () => {
254
246
  describe('the store, with a linker configured', () => {
255
247
  test('adopts on init when nothing is stored yet, and applies it', () => {
256
248
  const record = { [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: false }
257
- env.goto(`https://platform.test/dispatcher?owlcc=${encodeConsentLink(record)}`, 'https://site.test/')
249
+ env.goto(`https://platform.test/dispatcher?owlcc=${consentLinkHelper.encodeConsentLink(record)}`, 'https://site.test/')
258
250
 
259
251
  const store = makeConsentStore()
260
252
  store.init({ silent: true, linker: { domains: DOMAINS } })
@@ -263,7 +255,7 @@ describe('the store, with a linker configured', () => {
263
255
  expect(store.get().record?.[CONSENT_ANALYTICS]).toBe(true)
264
256
  expect(store.granted(CONSENT_ESSENTIAL)).toBe(true)
265
257
  // Written through, not just held in memory — a second page load must see it too.
266
- expect(readConsent()?.[CONSENT_ANALYTICS]).toBe(true)
258
+ expect(consentStorageHelper.readConsent()?.[CONSENT_ANALYTICS]).toBe(true)
267
259
  })
268
260
 
269
261
  test('strips the parameter from the URL either way', () => {
@@ -276,9 +268,9 @@ describe('the store, with a linker configured', () => {
276
268
  })
277
269
 
278
270
  test('never overwrites a decision this document already made', () => {
279
- writeConsent({ [CONSENT_ANALYTICS]: false, [CONSENT_MARKETING]: false })
271
+ consentStorageHelper.writeConsent({ [CONSENT_ANALYTICS]: false, [CONSENT_MARKETING]: false })
280
272
  const incoming = { [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: true }
281
- env.goto(`https://platform.test/dispatcher?owlcc=${encodeConsentLink(incoming)}`, 'https://site.test/')
273
+ env.goto(`https://platform.test/dispatcher?owlcc=${consentLinkHelper.encodeConsentLink(incoming)}`, 'https://site.test/')
282
274
 
283
275
  const store = makeConsentStore()
284
276
  store.init({ silent: true, linker: { domains: DOMAINS } })
@@ -299,7 +291,7 @@ describe('the store, with a linker configured', () => {
299
291
 
300
292
  describe('consentBootstrapScript with a linker: order and shape', () => {
301
293
  test('the linker fragment sits after the default push and before the storage read', () => {
302
- const script = consentBootstrapScript({ linker: { domains: DOMAINS } })
294
+ const script = consentModeHelper.consentBootstrapScript({ linker: { domains: DOMAINS } })
303
295
  const defaultAt = script.indexOf("'consent','default'")
304
296
  const linkerAt = script.indexOf('URLSearchParams')
305
297
  const storageAt = script.indexOf('w.localStorage.getItem')
@@ -310,11 +302,11 @@ describe('consentBootstrapScript with a linker: order and shape', () => {
310
302
  })
311
303
 
312
304
  test('emits nothing extra with no linker configured', () => {
313
- expect(consentBootstrapScript()).not.toContain('URLSearchParams')
305
+ expect(consentModeHelper.consentBootstrapScript()).not.toContain('URLSearchParams')
314
306
  })
315
307
 
316
308
  test('escapes "<" inside embedded strings', () => {
317
- const script = consentLinkerScript({ linker: { domains: ['<script>evil.test'] } })
309
+ const script = consentLinkHelper.consentLinkerScript({ linker: { domains: ['<script>evil.test'] } })
318
310
 
319
311
  expect(script).not.toContain('<script>evil.test')
320
312
  expect(script).toContain('\\u003cscript>evil.test')
@@ -324,31 +316,31 @@ describe('consentBootstrapScript with a linker: order and shape', () => {
324
316
  describe('consentLinkerScript, executed', () => {
325
317
  test('adopts a valid link and strips the parameter, via new Function', () => {
326
318
  const record = { [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: false }
327
- env.goto(`https://platform.test/dispatcher?owlcc=${encodeConsentLink(record)}`, 'https://site.test/')
319
+ env.goto(`https://platform.test/dispatcher?owlcc=${consentLinkHelper.encodeConsentLink(record)}`, 'https://site.test/')
328
320
 
329
- const script = consentLinkerScript({ linker: { domains: DOMAINS } })
321
+ const script = consentLinkHelper.consentLinkerScript({ linker: { domains: DOMAINS } })
330
322
  // eslint-disable-next-line no-new-func
331
323
  new Function(script)()
332
324
 
333
325
  expect(env.href()).toBe('https://platform.test/dispatcher')
334
- expect(readConsent()?.[CONSENT_ANALYTICS]).toBe(true)
326
+ expect(consentStorageHelper.readConsent()?.[CONSENT_ANALYTICS]).toBe(true)
335
327
  })
336
328
 
337
329
  test('never overwrites an existing record, and still strips', () => {
338
- writeConsent({ [CONSENT_ANALYTICS]: false, [CONSENT_MARKETING]: false })
330
+ consentStorageHelper.writeConsent({ [CONSENT_ANALYTICS]: false, [CONSENT_MARKETING]: false })
339
331
  const incoming = { [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: true }
340
- env.goto(`https://platform.test/dispatcher?owlcc=${encodeConsentLink(incoming)}`, 'https://site.test/')
332
+ env.goto(`https://platform.test/dispatcher?owlcc=${consentLinkHelper.encodeConsentLink(incoming)}`, 'https://site.test/')
341
333
 
342
- const script = consentLinkerScript({ linker: { domains: DOMAINS } })
334
+ const script = consentLinkHelper.consentLinkerScript({ linker: { domains: DOMAINS } })
343
335
  // eslint-disable-next-line no-new-func
344
336
  new Function(script)()
345
337
 
346
338
  expect(env.href()).toBe('https://platform.test/dispatcher')
347
- expect(readConsent()?.[CONSENT_ANALYTICS]).toBe(false)
339
+ expect(consentStorageHelper.readConsent()?.[CONSENT_ANALYTICS]).toBe(false)
348
340
  })
349
341
 
350
342
  test('is an empty string with no linker configured', () => {
351
- expect(consentLinkerScript({})).toBe('')
343
+ expect(consentLinkHelper.consentLinkerScript({})).toBe('')
352
344
  })
353
345
  })
354
346
 
@@ -359,12 +351,11 @@ describe('every category the built-in bundle carries', () => {
359
351
  expect(DEFAULT_CONSENT_CATEGORIES.filter(c => c.required === true).map(c => c.key)).toEqual([CONSENT_ESSENTIAL])
360
352
  })
361
353
 
362
- test('functional is an optional category that drives no Consent Mode signal', () => {
363
- const functional = DEFAULT_CONSENT_CATEGORIES.find(c => c.key === CONSENT_FUNCTIONAL)
364
-
365
- expect(functional).toBeDefined()
366
- expect(functional?.required).not.toBe(true)
367
- expect(functional?.signals ?? []).toEqual([])
354
+ test('the optional categories are analytics and marketing — nothing asks about the language', () => {
355
+ // The interface language is strictly necessary storage: no category governs it, so the default
356
+ // set has no "functional" row that would offer a choice which changes nothing.
357
+ expect(DEFAULT_CONSENT_CATEGORIES.filter(c => c.required !== true).map(c => c.key))
358
+ .toEqual([CONSENT_ANALYTICS, CONSENT_MARKETING])
368
359
  })
369
360
  })
370
361
 
@@ -373,29 +364,29 @@ describe('language: the link parameter', () => {
373
364
 
374
365
  test('carries the page language as `l`, lower-cased, when the sender turned it on', () => {
375
366
  env.lang('PL')
376
- const decoded = decodeConsentLink(encodeConsentLink({ [CONSENT_ANALYTICS]: true }, withLanguage))
367
+ const decoded = consentLinkHelper.decodeConsentLink(consentLinkHelper.encodeConsentLink({ [CONSENT_ANALYTICS]: true }, withLanguage))
377
368
 
378
369
  expect(decoded?.l).toBe('pl')
379
- expect(decoded?.c).toEqual({ [CONSENT_FUNCTIONAL]: 0, [CONSENT_ANALYTICS]: 1, [CONSENT_MARKETING]: 0 })
370
+ expect(decoded?.c).toEqual({ [CONSENT_ANALYTICS]: 1, [CONSENT_MARKETING]: 0 })
380
371
  })
381
372
 
382
373
  test('carries no `l` when the sender did not turn it on, whatever the page says', () => {
383
374
  env.lang('pl')
384
375
 
385
- expect(decodeConsentLink(encodeConsentLink({ [CONSENT_ANALYTICS]: true }, linkerOpts))?.l).toBeUndefined()
376
+ expect(consentLinkHelper.decodeConsentLink(consentLinkHelper.encodeConsentLink({ [CONSENT_ANALYTICS]: true }, linkerOpts))?.l).toBeUndefined()
386
377
  })
387
378
 
388
379
  test('carries no `l` for a page with no usable language', () => {
389
380
  for (const lang of ['', 'not a language!', '12']) {
390
381
  env.lang(lang)
391
382
 
392
- expect(decodeConsentLink(encodeConsentLink(null, withLanguage))?.l).toBeUndefined()
383
+ expect(consentLinkHelper.decodeConsentLink(consentLinkHelper.encodeConsentLink(null, withLanguage))?.l).toBeUndefined()
393
384
  }
394
385
  })
395
386
 
396
387
  test('encodes no decision at all as an empty `c`', () => {
397
388
  env.lang('de')
398
- const decoded = decodeConsentLink(encodeConsentLink(null, withLanguage))
389
+ const decoded = consentLinkHelper.decodeConsentLink(consentLinkHelper.encodeConsentLink(null, withLanguage))
399
390
 
400
391
  expect(decoded?.c).toEqual({})
401
392
  expect(decoded?.l).toBe('de')
@@ -405,7 +396,7 @@ describe('language: the link parameter', () => {
405
396
  const raw = Buffer.from(JSON.stringify({
406
397
  v: 2, c: { [CONSENT_ANALYTICS]: 1 }, t: nowSeconds(), l: '../../etc',
407
398
  })).toString('base64url')
408
- const decoded = decodeConsentLink(raw)
399
+ const decoded = consentLinkHelper.decodeConsentLink(raw)
409
400
 
410
401
  expect(decoded).not.toBeNull()
411
402
  expect(decoded?.l).toBeUndefined()
@@ -414,7 +405,7 @@ describe('language: the link parameter', () => {
414
405
  })
415
406
 
416
407
  describe('language: consentLinker().decorate', () => {
417
- const plugin = consentLinker()
408
+ const plugin = consentLinkHelper.consentLinker()
418
409
  const withLanguage = { linker: { domains: DOMAINS, language: {} } }
419
410
  const record: ConsentRecord = { [CONSENT_ESSENTIAL]: true, [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: false }
420
411
 
@@ -423,14 +414,14 @@ describe('language: consentLinker().decorate', () => {
423
414
  const decorated = plugin.decorate!(new URL('https://site.test/start'), null, withLanguage)
424
415
 
425
416
  expect(decorated).not.toBeNull()
426
- const payload = decodeConsentLink(decorated!.searchParams.get(CONSENT_LINK_PARAM)!)
417
+ const payload = consentLinkHelper.decodeConsentLink(decorated!.searchParams.get(CONSENT_LINK_PARAM)!)
427
418
  expect(payload?.l).toBe('uk')
428
419
  expect(payload?.c).toEqual({})
429
420
  })
430
421
 
431
422
  test('carries the decision and the language together', () => {
432
423
  env.lang('pl')
433
- const payload = decodeConsentLink(
424
+ const payload = consentLinkHelper.decodeConsentLink(
434
425
  plugin.decorate!(new URL('https://site.test/start'), record, withLanguage)!.searchParams.get(CONSENT_LINK_PARAM)!
435
426
  )
436
427
 
@@ -458,12 +449,12 @@ describe('language: consentLinker().decorate', () => {
458
449
  })
459
450
 
460
451
  describe('language: consentLinker().adoptLanguage', () => {
461
- const plugin = consentLinker()
452
+ const plugin = consentLinkHelper.consentLinker()
462
453
  const receiving = { linker: { domains: DOMAINS, language: { supported: ['en', 'pl', 'de', 'fr'] } } }
463
454
  const carried = (lang: string, record: ConsentRecord | null = null): string => {
464
455
  env.lang(lang)
465
456
 
466
- return encodeConsentLink(record, { linker: { domains: DOMAINS, language: {} } })
457
+ return consentLinkHelper.encodeConsentLink(record, { linker: { domains: DOMAINS, language: {} } })
467
458
  }
468
459
  const arrive = (owlcc: string, referrer = 'https://site.test/') => {
469
460
  env.goto(`https://platform.test/dispatcher?owlcc=${owlcc}`, referrer)
@@ -529,115 +520,64 @@ describe('language: consentLinker().adoptLanguage', () => {
529
520
  })
530
521
 
531
522
  test('registry: adoptConsentLanguage asks the registered plugin', () => {
532
- registerConsentPlugin(consentLinker())
523
+ consentPluginHelper.registerConsentPlugin(consentLinkHelper.consentLinker())
533
524
  arrive(carried('pl'))
534
525
 
535
- expect(adoptConsentLanguage(receiving)).toBe('pl')
536
- expect(adoptConsentLanguage(linkerOpts)).toBeNull()
526
+ expect(consentPluginHelper.adoptConsentLanguage(receiving)).toBe('pl')
527
+ expect(consentPluginHelper.adoptConsentLanguage(linkerOpts)).toBeNull()
537
528
  })
538
529
  })
539
530
 
540
- describe('language: only while functional storage is granted', () => {
531
+ describe('language: strictly necessary, stored whatever the cookie decision is', () => {
541
532
  const receiving = { silent: true, linker: { domains: DOMAINS, language: { supported: ['en', 'pl', 'de'] } } }
542
533
  const arrive = (lang: string, record: ConsentRecord | null = null, referrer = 'https://site.test/') => {
543
534
  env.lang(lang)
544
- const owlcc = encodeConsentLink(record, { linker: { domains: DOMAINS, language: {} } })
535
+ const owlcc = consentLinkHelper.encodeConsentLink(record, { linker: { domains: DOMAINS, language: {} } })
545
536
  env.goto(`https://platform.test/dispatcher?owlcc=${owlcc}`, referrer)
546
537
  env.lang('en')
547
538
  }
548
- /** A decision that allows remembering a preference, and one that refuses everything optional. */
549
- const granted: ConsentRecord = { [CONSENT_FUNCTIONAL]: true, [CONSENT_ANALYTICS]: false, [CONSENT_MARKETING]: false }
550
- const refused: ConsentRecord = { [CONSENT_FUNCTIONAL]: false, [CONSENT_ANALYTICS]: false, [CONSENT_MARKETING]: false }
551
- const legacy: ConsentRecord = { [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: false }
552
-
553
- test('writeConsentLanguage writes only on a functional grant, and says so', () => {
554
- expect(writeConsentLanguage('pl', receiving)).toBe(false)
555
-
556
- writeConsent(refused)
557
- expect(writeConsentLanguage('pl', receiving)).toBe(false)
558
-
559
- writeConsent(legacy)
560
- expect(writeConsentLanguage('pl', receiving)).toBe(false)
561
- expect(env.store.has(CONSENT_LANGUAGE_KEY)).toBe(false)
562
-
563
- writeConsent(granted)
564
- expect(writeConsentLanguage('pl', receiving)).toBe(true)
565
- expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('pl')
566
- })
567
-
568
- test('a grant that survives only in the cookie counts; a corrupt record does not', () => {
569
- env.store.set(CONSENT_KEY, '{not json')
570
- expect(writeConsentLanguage('pl', receiving)).toBe(false)
539
+ const accepted: ConsentRecord = { [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: true }
540
+ const refused: ConsentRecord = { [CONSENT_ANALYTICS]: false, [CONSENT_MARKETING]: false }
541
+ /** Saved before the `functional` category was retired: an extra key nobody reads any more. */
542
+ const withRetiredKey: ConsentRecord = { functional: false, [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: false }
571
543
 
572
- env.reset()
573
- ;(globalThis as any).document.cookie = `${CONSENT_KEY}=${JSON.stringify({ v: 2, essential: true, ...granted })}`
574
- expect(writeConsentLanguage('pl', receiving)).toBe(true)
575
- expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('pl')
544
+ test('writeConsentLanguage writes with no decision, a refusal, an acceptance or an old record', () => {
545
+ for (const record of [null, refused, accepted, withRetiredKey]) {
546
+ env.reset()
547
+ if (record != null) {
548
+ consentStorageHelper.writeConsent(record)
549
+ }
550
+ expect(consentLinkHelper.writeConsentLanguage('pl', receiving)).toBe(true)
551
+ expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('pl')
552
+ }
576
553
  })
577
554
 
578
- test('functionalGranted, writeFunctionalPreference and functionalKeysOf agree with the record', () => {
579
- expect(functionalGranted()).toBe(false)
580
- expect(writeFunctionalPreference('theme-choice', 'dark')).toBe(false)
581
-
582
- writeConsent(granted)
583
- expect(functionalGranted()).toBe(true)
584
- expect(writeFunctionalPreference('theme-choice', 'dark')).toBe(true)
585
- expect(env.store.get('theme-choice')).toBe('dark')
586
-
587
- expect(functionalKeysOf()).toEqual([CONSENT_LANGUAGE_KEY])
588
- expect(functionalKeysOf({ functionalKeys: ['a', 'b'], linker: { domains: DOMAINS, language: { storageKey: 'a' } } }))
589
- .toEqual(['a', 'b'])
555
+ test('storage that refuses the write is not an error', () => {
556
+ const storage = (globalThis as any).localStorage
557
+ const setItem = storage.setItem
558
+ storage.setItem = () => { throw new Error('blocked') }
559
+ try {
560
+ expect(consentLinkHelper.writeConsentLanguage('pl', receiving)).toBe(false)
561
+ } finally {
562
+ // The mock storage is shared by every test in the file.
563
+ storage.setItem = setItem
564
+ }
590
565
  })
591
566
 
592
567
  describe('the store', () => {
593
- test('with no decision the language is NOT stored — it is held in memory, the dialog asks, the parameter is stripped', () => {
568
+ test('with no decision the language is stored at once, the dialog asks, the parameter is stripped', () => {
594
569
  arrive('pl')
595
570
 
596
571
  const store = makeConsentStore()
597
572
  store.init(receiving)
598
573
 
599
- expect(env.store.has(CONSENT_LANGUAGE_KEY)).toBe(false)
600
- expect(store.pendingLanguage()).toBe('pl')
574
+ expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('pl')
601
575
  expect(store.get().open).toBe(true)
602
576
  expect(env.href()).toBe('https://platform.test/dispatcher')
603
577
  })
604
578
 
605
- test('answering the dialog AFTER arrival stores the waiting language on a grant, and tells the app', () => {
606
- arrive('pl')
607
- const store = makeConsentStore()
608
- store.init(receiving)
609
- const heard: string[] = []
610
- // Process-wide globals: put them back, whatever the assertions do (bun shares them across files).
611
- const saved = { dispatch: (globalThis as any).dispatchEvent, custom: (globalThis as any).CustomEvent }
612
- ;(globalThis as any).dispatchEvent = (e: any) => { if (e.type === CONSENT_LANGUAGE_EVENT) heard.push(e.detail.language) }
613
- ;(globalThis as any).CustomEvent = class { constructor(public type: string, public init: any) {} get detail() { return this.init.detail } }
614
- try {
615
- store.save(granted)
616
- } finally {
617
- (globalThis as any).dispatchEvent = saved.dispatch
618
- ;(globalThis as any).CustomEvent = saved.custom
619
- }
620
-
621
- expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('pl')
622
- expect(store.pendingLanguage()).toBeNull()
623
- expect(heard).toEqual(['pl'])
624
- })
625
-
626
- test('answering with a refusal stores nothing, and the language keeps waiting for a later grant', () => {
627
- arrive('pl')
628
- const store = makeConsentStore()
629
- store.init(receiving)
630
-
631
- store.save(refused)
632
- expect(env.store.has(CONSENT_LANGUAGE_KEY)).toBe(false)
633
- expect(store.pendingLanguage()).toBe('pl')
634
-
635
- store.save(granted)
636
- expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('pl')
637
- })
638
-
639
- test('a decision carried by the link that grants functional stores decision and language together', () => {
640
- arrive('de', { [CONSENT_FUNCTIONAL]: true, [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: false })
579
+ test('a decision carried by the link is stored together with the language', () => {
580
+ arrive('de', { [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: false })
641
581
 
642
582
  const store = makeConsentStore()
643
583
  store.init(receiving)
@@ -647,80 +587,62 @@ describe('language: only while functional storage is granted', () => {
647
587
  expect(store.get().record?.[CONSENT_ANALYTICS]).toBe(true)
648
588
  })
649
589
 
650
- test('a carried "reject all" stores the decision but NOT the language', () => {
590
+ test('a carried "reject all" stores the decision AND the language', () => {
651
591
  arrive('de', refused)
652
592
 
653
593
  const store = makeConsentStore()
654
594
  store.init(receiving)
655
595
 
656
596
  expect(store.get().open).toBe(false)
657
- expect(store.get().record?.[CONSENT_FUNCTIONAL]).toBe(false)
658
- expect(env.store.has(CONSENT_LANGUAGE_KEY)).toBe(false)
659
- expect(store.pendingLanguage()).toBe('de')
597
+ expect(store.get().record?.[CONSENT_ANALYTICS]).toBe(false)
598
+ expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('de')
660
599
  })
661
600
 
662
- test('a functional grant stored here is enough for a link that carries only the language', () => {
663
- writeConsent(granted)
664
- arrive('pl')
601
+ test('a stored decision of any kind lets the link\'s language through, and still wins over its decision', () => {
602
+ for (const stored of [refused, accepted, withRetiredKey]) {
603
+ env.reset()
604
+ consentStorageHelper.writeConsent(stored)
605
+ arrive('pl', { [CONSENT_ANALYTICS]: !stored[CONSENT_ANALYTICS], [CONSENT_MARKETING]: true })
665
606
 
666
- makeConsentStore().init(receiving)
607
+ const store = makeConsentStore()
608
+ store.init(receiving)
667
609
 
668
- expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('pl')
610
+ expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('pl')
611
+ expect(store.get().record?.[CONSENT_ANALYTICS]).toBe(stored[CONSENT_ANALYTICS])
612
+ }
669
613
  })
670
614
 
671
- test('the carried language outranks one chosen here earlier, while a stored decision still wins', () => {
672
- writeConsent(granted)
615
+ test('the carried language outranks one chosen here earlier', () => {
673
616
  env.store.set(CONSENT_LANGUAGE_KEY, 'de')
674
- arrive('pl', { [CONSENT_FUNCTIONAL]: false, [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: true })
617
+ arrive('pl')
675
618
 
676
- const store = makeConsentStore()
677
- store.init(receiving)
619
+ makeConsentStore().init(receiving)
678
620
 
679
621
  expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('pl')
680
- expect(store.get().record?.[CONSENT_ANALYTICS]).toBe(false)
681
622
  })
682
623
 
683
- test('no functional grant removes a language stored earlier — on init, and on a refusal', () => {
684
- env.store.set(CONSENT_LANGUAGE_KEY, 'de')
624
+ test('answering the dialog never touches the language, in either direction', () => {
625
+ arrive('pl')
685
626
  const store = makeConsentStore()
686
627
  store.init(receiving)
687
- expect(env.store.has(CONSENT_LANGUAGE_KEY)).toBe(false)
688
628
 
689
- store.save(granted)
690
- env.store.set(CONSENT_LANGUAGE_KEY, 'de')
691
629
  store.save(refused)
692
- expect(env.store.has(CONSENT_LANGUAGE_KEY)).toBe(false)
693
- })
694
-
695
- test('a grant keeps what is stored, and every functional key is purged when it is withdrawn', () => {
696
- writeConsent(granted)
697
- env.store.set(CONSENT_LANGUAGE_KEY, 'de')
698
- env.store.set('locale_chosen', '1')
699
- const opts = { ...receiving, functionalKeys: [CONSENT_LANGUAGE_KEY, 'locale_chosen'] }
700
- const store = makeConsentStore()
701
- store.init(opts)
702
- expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('de')
703
- expect(env.store.get('locale_chosen')).toBe('1')
630
+ expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('pl')
704
631
 
705
- store.save(refused)
706
- expect(env.store.has(CONSENT_LANGUAGE_KEY)).toBe(false)
707
- expect(env.store.has('locale_chosen')).toBe(false)
632
+ store.save(accepted)
633
+ expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('pl')
708
634
  })
709
635
 
710
- test('a record saved before the category existed is not a grant', () => {
711
- writeConsent(legacy)
636
+ test('a language stored earlier survives every decision', () => {
712
637
  env.store.set(CONSENT_LANGUAGE_KEY, 'de')
713
- arrive('pl')
714
-
715
638
  const store = makeConsentStore()
716
639
  store.init(receiving)
640
+ store.save(refused)
717
641
 
718
- expect(env.store.has(CONSENT_LANGUAGE_KEY)).toBe(false)
719
- expect(store.pendingLanguage()).toBe('pl')
642
+ expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('de')
720
643
  })
721
644
 
722
645
  test('honours a custom storage key', () => {
723
- writeConsent(granted)
724
646
  arrive('pl')
725
647
 
726
648
  makeConsentStore().init({ ...receiving, linker: { ...receiving.linker, language: { supported: ['pl'], storageKey: 'app-lng' } } })
@@ -729,110 +651,72 @@ describe('language: only while functional storage is granted', () => {
729
651
  expect(env.store.has(CONSENT_LANGUAGE_KEY)).toBe(false)
730
652
  })
731
653
 
732
- test('writes nothing for an unsupported language or an untrusted referrer, grant or not', () => {
733
- writeConsent(granted)
654
+ test('writes nothing for an unsupported language or an untrusted referrer', () => {
734
655
  arrive('ja')
735
656
  makeConsentStore().init(receiving)
736
657
  expect(env.store.has(CONSENT_LANGUAGE_KEY)).toBe(false)
737
658
 
738
659
  arrive('pl', null, 'https://evil.test/')
739
- const store = makeConsentStore()
740
- store.init(receiving)
660
+ makeConsentStore().init(receiving)
741
661
  expect(env.store.has(CONSENT_LANGUAGE_KEY)).toBe(false)
742
- expect(store.pendingLanguage()).toBeNull()
743
- })
744
-
745
- test('picks up what the inline fragment left on window, and clears it', () => {
746
- ;(globalThis as any)[CONSENT_PENDING_LANGUAGE] = 'pl'
747
- env.goto('https://platform.test/dispatcher')
748
-
749
- const store = makeConsentStore()
750
- store.init(receiving)
751
-
752
- expect(store.pendingLanguage()).toBe('pl')
753
- expect((globalThis as any)[CONSENT_PENDING_LANGUAGE]).toBeUndefined()
754
-
755
- store.save(granted)
756
- expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('pl')
757
662
  })
758
663
  })
759
664
 
760
665
  describe('the inline fragment', () => {
761
- const run = (opts: Parameters<typeof consentLinkerScript>[0] = receiving) => {
666
+ const run = (opts: Parameters<typeof consentLinkHelper.consentLinkerScript>[0] = receiving) => {
762
667
  // eslint-disable-next-line no-new-func
763
- new Function(consentLinkerScript(opts))()
668
+ new Function(consentLinkHelper.consentLinkerScript(opts))()
764
669
  }
765
670
  const all = { linker: { domains: DOMAINS, language: { supported: ['en', 'pl', 'de', 'fr'] } } }
766
671
 
767
- test('with no decision anywhere it writes NO language, leaves it on window, adopts nothing, and still strips', () => {
672
+ test('with no decision anywhere it stores the language, adopts nothing, and still strips', () => {
768
673
  arrive('pl')
769
674
  run()
770
675
 
771
- expect(env.store.has(CONSENT_LANGUAGE_KEY)).toBe(false)
772
- expect((globalThis as any)[CONSENT_PENDING_LANGUAGE]).toBe('pl')
773
- expect(readConsent()).toBeNull()
676
+ expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('pl')
677
+ expect(consentStorageHelper.readConsent()).toBeNull()
774
678
  expect(env.href()).toBe('https://platform.test/dispatcher')
775
679
  })
776
680
 
777
- test('a link that grants functional writes decision and language together', () => {
778
- arrive('fr', { [CONSENT_FUNCTIONAL]: true, [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: false })
681
+ test('a link that carries a decision writes decision and language together', () => {
682
+ arrive('fr', { [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: false })
779
683
  run(all)
780
684
 
781
685
  expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('fr')
782
- expect(readConsent()?.[CONSENT_ANALYTICS]).toBe(true)
783
- expect((globalThis as any)[CONSENT_PENDING_LANGUAGE]).toBeUndefined()
686
+ expect(consentStorageHelper.readConsent()?.[CONSENT_ANALYTICS]).toBe(true)
784
687
  })
785
688
 
786
- test('a link that carries "reject all" adopts the decision but keeps the language in memory only', () => {
689
+ test('a link that carries "reject all" adopts the decision and stores the language', () => {
787
690
  arrive('fr', refused)
788
691
  run(all)
789
692
 
790
- expect(readConsent()?.[CONSENT_FUNCTIONAL]).toBe(false)
791
- expect(env.store.has(CONSENT_LANGUAGE_KEY)).toBe(false)
792
- expect((globalThis as any)[CONSENT_PENDING_LANGUAGE]).toBe('fr')
793
- })
794
-
795
- test('a functional grant stored here is enough, and the link\'s own decision never overwrites it', () => {
796
- writeConsent({ ...granted, [CONSENT_ANALYTICS]: false })
797
- arrive('de', { [CONSENT_FUNCTIONAL]: true, [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: true })
798
- run()
799
-
800
- expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('de')
801
- expect(readConsent()?.[CONSENT_ANALYTICS]).toBe(false)
802
- })
803
-
804
- test('a functional grant stored here is enough for a link that carries only the language', () => {
805
- writeConsent(granted)
806
- arrive('pl')
807
- run()
808
-
809
- expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('pl')
693
+ expect(consentStorageHelper.readConsent()?.[CONSENT_ANALYTICS]).toBe(false)
694
+ expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('fr')
810
695
  })
811
696
 
812
- test('a stored refusal or a legacy record is not a grant: nothing written, language kept in memory', () => {
813
- for (const record of [refused, legacy]) {
697
+ test('a stored record of any kind lets the language through, and the link\'s decision never overwrites it', () => {
698
+ for (const stored of [refused, accepted, withRetiredKey]) {
814
699
  env.reset()
815
- delete (globalThis as any)[CONSENT_PENDING_LANGUAGE]
816
- writeConsent(record)
817
- arrive('pl')
700
+ consentStorageHelper.writeConsent(stored)
701
+ arrive('de', { [CONSENT_ANALYTICS]: !stored[CONSENT_ANALYTICS], [CONSENT_MARKETING]: true })
818
702
  run()
819
703
 
820
- expect(env.store.has(CONSENT_LANGUAGE_KEY)).toBe(false)
821
- expect((globalThis as any)[CONSENT_PENDING_LANGUAGE]).toBe('pl')
704
+ expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('de')
705
+ expect(consentStorageHelper.readConsent()?.[CONSENT_ANALYTICS]).toBe(stored[CONSENT_ANALYTICS])
822
706
  }
823
707
  })
824
708
 
825
- test('an unparseable stored record is no grant: nothing is written, and nothing is adopted over it', () => {
709
+ test('an unparseable stored record still lets the language through, and nothing is adopted over it', () => {
826
710
  env.store.set(CONSENT_KEY, '{not json')
827
- arrive('pl', { [CONSENT_FUNCTIONAL]: true, [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: true })
711
+ arrive('pl', { [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: true })
828
712
  run()
829
713
 
830
- expect(env.store.has(CONSENT_LANGUAGE_KEY)).toBe(false)
714
+ expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('pl')
831
715
  expect(env.store.get(CONSENT_KEY)).toBe('{not json')
832
716
  })
833
717
 
834
718
  test('matches by base tag and by the receiver\'s spelling, like the TypeScript path', () => {
835
- const grant = { [CONSENT_FUNCTIONAL]: true, [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: false }
719
+ const grant = { [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: false }
836
720
  arrive('de-CH', grant)
837
721
  run()
838
722
  expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('de')
@@ -843,97 +727,48 @@ describe('language: only while functional storage is granted', () => {
843
727
  expect(env.store.get(CONSENT_LANGUAGE_KEY)).toBe('PL')
844
728
  })
845
729
 
846
- test('agrees with the TypeScript path on what it refuses, grant or not', () => {
847
- writeConsent(granted)
730
+ test('agrees with the TypeScript path on what it refuses', () => {
848
731
  arrive('ja')
849
732
  run()
850
733
  expect(env.store.has(CONSENT_LANGUAGE_KEY)).toBe(false)
851
734
 
852
- delete (globalThis as any)[CONSENT_PENDING_LANGUAGE]
853
735
  arrive('pl', null, 'https://evil.test/')
854
736
  run()
855
737
  expect(env.store.has(CONSENT_LANGUAGE_KEY)).toBe(false)
856
- expect((globalThis as any)[CONSENT_PENDING_LANGUAGE]).toBeUndefined()
857
738
 
858
739
  arrive('pl', null, '')
859
740
  run()
860
741
  expect(env.store.has(CONSENT_LANGUAGE_KEY)).toBe(false)
861
- expect((globalThis as any)[CONSENT_PENDING_LANGUAGE]).toBeUndefined()
862
742
  })
863
743
 
864
744
  test('honours a custom storage key', () => {
865
- arrive('pl', { [CONSENT_FUNCTIONAL]: true, [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: false })
745
+ arrive('pl', { [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: false })
866
746
  run({ linker: { domains: DOMAINS, language: { supported: ['pl'], storageKey: 'app-lng' } } })
867
747
 
868
748
  expect(env.store.get('app-lng')).toBe('pl')
869
749
  })
870
750
 
871
751
  test('a page that only sends carries no language code at all', () => {
872
- const script = consentLinkerScript({ linker: { domains: DOMAINS, language: {} } })
752
+ const script = consentLinkHelper.consentLinkerScript({ linker: { domains: DOMAINS, language: {} } })
873
753
 
874
754
  expect(script).not.toContain(CONSENT_LANGUAGE_KEY)
875
- expect(script).not.toContain(CONSENT_PENDING_LANGUAGE)
876
- expect(consentLinkerScript(linkerOpts)).toBe(script)
755
+ expect(consentLinkHelper.consentLinkerScript(linkerOpts)).toBe(script)
877
756
  })
878
757
 
879
- test('the bootstrap embeds it, after the decision is settled and before its own storage read', () => {
880
- const script = consentBootstrapScript(receiving)
758
+ test('the bootstrap embeds it, after the defaults and before its own storage read', () => {
759
+ const script = consentModeHelper.consentBootstrapScript(receiving)
881
760
 
882
761
  expect(script.indexOf(CONSENT_LANGUAGE_KEY)).toBeGreaterThan(script.indexOf("'consent','default'"))
883
- // The fragment reads storage itself (to know what is granted) before it writes the language;
884
- // the bootstrap's own read is the LAST one, after the whole fragment.
762
+ // The fragment reads storage itself (to know whether a decision exists) before it writes the
763
+ // language; the bootstrap's own read is the LAST one, after the whole fragment.
885
764
  expect(script.indexOf(CONSENT_LANGUAGE_KEY)).toBeLessThan(script.lastIndexOf('w.localStorage.getItem'))
886
765
  })
887
766
 
888
767
  test('escapes "<" in a configured language too', () => {
889
- const script = consentLinkerScript({ linker: { domains: DOMAINS, language: { supported: ['<b>'], storageKey: '<k>' } } })
768
+ const script = consentLinkHelper.consentLinkerScript({ linker: { domains: DOMAINS, language: { supported: ['<b>'], storageKey: '<k>' } } })
890
769
 
891
770
  expect(script).not.toContain('<b>')
892
771
  expect(script).not.toContain('<k>')
893
772
  })
894
773
  })
895
774
  })
896
-
897
- describe('consentAllowsScript, executed', () => {
898
- const run = (opts?: Parameters<typeof consentAllowsScript>[0]) => {
899
- // eslint-disable-next-line no-new-func
900
- new Function(consentAllowsScript(opts))()
901
-
902
- return (globalThis as any).owlConsentAllows as (category: string) => boolean
903
- }
904
- afterEach(() => { delete (globalThis as any).owlConsentAllows })
905
-
906
- test('answers from the stored record, category by category', () => {
907
- writeConsent({ [CONSENT_FUNCTIONAL]: true, [CONSENT_ANALYTICS]: false, [CONSENT_MARKETING]: false })
908
- const allows = run()
909
-
910
- expect(allows(CONSENT_FUNCTIONAL)).toBe(true)
911
- expect(allows(CONSENT_ANALYTICS)).toBe(false)
912
- expect(allows('unknown')).toBe(false)
913
- })
914
-
915
- test('no record, a legacy record and a corrupt one are all a no', () => {
916
- const allows = run()
917
- expect(allows(CONSENT_FUNCTIONAL)).toBe(false)
918
-
919
- writeConsent({ [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: true })
920
- expect(allows(CONSENT_FUNCTIONAL)).toBe(false)
921
-
922
- env.store.set(CONSENT_KEY, '{not json')
923
- expect(allows(CONSENT_FUNCTIONAL)).toBe(false)
924
- })
925
-
926
- test('reads the cookie when localStorage has nothing, and honours a custom key', () => {
927
- ;(globalThis as any).document.cookie = `${CONSENT_KEY}=${JSON.stringify({ v: 2, functional: true })}`
928
- expect(run()(CONSENT_FUNCTIONAL)).toBe(true)
929
-
930
- env.reset()
931
- env.store.set('my_consent', JSON.stringify({ v: 2, functional: true }))
932
- expect(run({ storageKey: 'my_consent' })(CONSENT_FUNCTIONAL)).toBe(true)
933
- expect(run()(CONSENT_FUNCTIONAL)).toBe(false)
934
- })
935
-
936
- test('escapes "<" in the key', () => {
937
- expect(consentAllowsScript({ storageKey: '<k>' })).not.toContain('<k>')
938
- })
939
- })