@owlmeans/consent 0.1.18-rc.9 → 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.
- package/README.md +229 -0
- package/build/consts.d.ts +78 -3
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +104 -0
- package/build/consts.js.map +1 -1
- package/build/consts.local.d.ts +12 -0
- package/build/consts.local.d.ts.map +1 -0
- package/build/consts.local.js +12 -0
- package/build/consts.local.js.map +1 -0
- package/build/geo/types.d.ts +47 -0
- package/build/geo/types.d.ts.map +1 -0
- package/build/geo/types.js +2 -0
- package/build/geo/types.js.map +1 -0
- package/build/geo.d.ts +4 -0
- package/build/geo.d.ts.map +1 -0
- package/build/geo.js +96 -0
- package/build/geo.js.map +1 -0
- package/build/gtobserver/types.d.ts +75 -0
- package/build/gtobserver/types.d.ts.map +1 -0
- package/build/gtobserver/types.js +2 -0
- package/build/gtobserver/types.js.map +1 -0
- package/build/gtobserver.d.ts +7 -0
- package/build/gtobserver.d.ts.map +1 -0
- package/build/gtobserver.js +142 -0
- package/build/gtobserver.js.map +1 -0
- package/build/i18n/be.json +8 -1
- package/build/i18n/de.json +8 -1
- package/build/i18n/en.json +8 -1
- package/build/i18n/es.json +8 -1
- package/build/i18n/fr.json +8 -1
- package/build/i18n/pl.json +8 -1
- package/build/i18n/ru.json +8 -1
- package/build/i18n/types.d.ts +11 -0
- package/build/i18n/types.d.ts.map +1 -0
- package/build/i18n/types.js +2 -0
- package/build/i18n/types.js.map +1 -0
- package/build/i18n/uk.json +8 -1
- package/build/i18n.d.ts +4 -14
- package/build/i18n.d.ts.map +1 -1
- package/build/i18n.js +12 -27
- package/build/i18n.js.map +1 -1
- package/build/index.d.ts +13 -5
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5 -3
- package/build/index.js.map +1 -1
- package/build/linker/types.d.ts +59 -0
- package/build/linker/types.d.ts.map +1 -0
- package/build/linker/types.js +2 -0
- package/build/linker/types.js.map +1 -0
- package/build/linker.d.ts +5 -74
- package/build/linker.d.ts.map +1 -1
- package/build/linker.js +290 -324
- package/build/linker.js.map +1 -1
- package/build/plugins/types.d.ts +42 -0
- package/build/plugins/types.d.ts.map +1 -0
- package/build/plugins/types.js +2 -0
- package/build/plugins/types.js.map +1 -0
- package/build/plugins.d.ts +4 -68
- package/build/plugins.d.ts.map +1 -1
- package/build/plugins.js +73 -61
- package/build/plugins.js.map +1 -1
- package/build/storage/types.d.ts +31 -0
- package/build/storage/types.d.ts.map +1 -0
- package/build/storage/types.js +2 -0
- package/build/storage/types.js.map +1 -0
- package/build/storage.d.ts +6 -22
- package/build/storage.d.ts.map +1 -1
- package/build/storage.js +72 -81
- package/build/storage.js.map +1 -1
- package/build/store.d.ts +2 -2
- package/build/store.d.ts.map +1 -1
- package/build/store.js +115 -31
- package/build/store.js.map +1 -1
- package/build/types.d.ts +141 -5
- package/build/types.d.ts.map +1 -1
- package/build/types.local.d.ts +9 -0
- package/build/types.local.d.ts.map +1 -0
- package/build/types.local.js +2 -0
- package/build/types.local.js.map +1 -0
- package/package.json +3 -3
- package/src/consts.local.ts +16 -0
- package/src/consts.ts +120 -3
- package/src/geo/types.ts +49 -0
- package/src/geo.ts +122 -0
- package/src/gtobserver/types.ts +73 -0
- package/src/gtobserver.ts +182 -0
- package/src/i18n/be.json +8 -1
- package/src/i18n/de.json +8 -1
- package/src/i18n/en.json +8 -1
- package/src/i18n/es.json +8 -1
- package/src/i18n/fr.json +8 -1
- package/src/i18n/pl.json +8 -1
- package/src/i18n/ru.json +8 -1
- package/src/i18n/types.ts +11 -0
- package/src/i18n/uk.json +8 -1
- package/src/i18n.ts +20 -32
- package/src/index.ts +13 -12
- package/src/linker/types.ts +59 -0
- package/src/linker.ts +301 -349
- package/src/plugins/types.ts +42 -0
- package/src/plugins.ts +82 -105
- package/src/storage/types.ts +31 -0
- package/src/storage.ts +75 -77
- package/src/store.ts +125 -31
- package/src/types.local.ts +9 -0
- package/src/types.ts +148 -5
- package/tests/consent.spec.ts +26 -28
- package/tests/geo.spec.ts +474 -0
- package/tests/linker.spec.ts +68 -74
- package/tests/tracking.spec.ts +14 -14
- package/build/gtm.d.ts +0 -72
- package/build/gtm.d.ts.map +0 -1
- package/build/gtm.js +0 -182
- package/build/gtm.js.map +0 -1
- package/src/gtm.ts +0 -220
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
CONSENT_ANALYTICS, CONSENT_AUTO_MAX_AGE, CONSENT_ESSENTIAL, CONSENT_KEY, CONSENT_MARKETING,
|
|
4
|
+
CONSENT_REQUIRED_COUNTRIES, CONSENT_TRACE_PATH,
|
|
5
|
+
} from '../src/consts.js'
|
|
6
|
+
import { consentGeoHelper } from '../src/geo.js'
|
|
7
|
+
import { consentModeHelper } from '../src/gtobserver.js'
|
|
8
|
+
import { consentLinkHelper } from '../src/linker.js'
|
|
9
|
+
import { consentPluginHelper } from '../src/plugins.js'
|
|
10
|
+
import { makeConsentStore } from '../src/store.js'
|
|
11
|
+
import { consentStorageHelper } from '../src/storage.js'
|
|
12
|
+
import type { ConsentGeoPlugin, ConsentOptions, ConsentRecord } from '../src/types.js'
|
|
13
|
+
|
|
14
|
+
/** Storage, a cookie jar, `<html>` attributes, a `location`/`history` pair and a fake trace endpoint. */
|
|
15
|
+
const browser = () => {
|
|
16
|
+
const store = new Map<string, string>()
|
|
17
|
+
const attributes = new Map<string, string>()
|
|
18
|
+
let cookie = ''
|
|
19
|
+
let current = new URL('https://site.test/')
|
|
20
|
+
let trace: { status: number, body: string } | 'throw' | 'hang' = { status: 200, body: '' }
|
|
21
|
+
const calls: { path: string, init?: RequestInit }[] = []
|
|
22
|
+
;(globalThis as any).localStorage = {
|
|
23
|
+
getItem: (k: string) => store.get(k) ?? null,
|
|
24
|
+
setItem: (k: string, v: string) => { store.set(k, v) },
|
|
25
|
+
removeItem: (k: string) => { store.delete(k) },
|
|
26
|
+
}
|
|
27
|
+
;(globalThis as any).document = {
|
|
28
|
+
referrer: '',
|
|
29
|
+
documentElement: {
|
|
30
|
+
lang: '',
|
|
31
|
+
setAttribute: (name: string, value: string) => { attributes.set(name, value) },
|
|
32
|
+
},
|
|
33
|
+
get cookie() { return cookie },
|
|
34
|
+
set cookie(value: string) {
|
|
35
|
+
const [pair] = value.split(';')
|
|
36
|
+
const [name] = pair.split('=')
|
|
37
|
+
const rest = cookie === '' ? [] : cookie.split('; ').filter(entry => !entry.startsWith(`${name}=`))
|
|
38
|
+
// An expired write is a removal, the way a browser treats it.
|
|
39
|
+
cookie = [...rest, ...(value.includes('1970') ? [] : [pair])].join('; ')
|
|
40
|
+
},
|
|
41
|
+
addEventListener: () => {},
|
|
42
|
+
removeEventListener: () => {},
|
|
43
|
+
}
|
|
44
|
+
;(globalThis as any).location = new Proxy({}, {
|
|
45
|
+
get: (_t, prop) => (current as unknown as Record<string, unknown>)[prop as string],
|
|
46
|
+
})
|
|
47
|
+
;(globalThis as any).history = {
|
|
48
|
+
state: null,
|
|
49
|
+
replaceState: (_state: unknown, _title: string, next: string) => { current = new URL(next, current.origin) },
|
|
50
|
+
}
|
|
51
|
+
;(globalThis as any).window = globalThis
|
|
52
|
+
;(globalThis as any).fetch = async (path: string, init?: RequestInit): Promise<Response> => {
|
|
53
|
+
calls.push({ path, init })
|
|
54
|
+
if (trace === 'throw') {
|
|
55
|
+
throw new TypeError('network down')
|
|
56
|
+
}
|
|
57
|
+
if (trace === 'hang') {
|
|
58
|
+
return await new Promise<Response>(() => {})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return new Response(trace.body, { status: trace.status })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
store, attributes, calls,
|
|
66
|
+
trace: (answer: typeof trace) => { trace = answer },
|
|
67
|
+
located: (country: string) => {
|
|
68
|
+
trace = { status: 200, body: `fl=1f1\nh=site.test\nip=203.0.113.1\nts=1\nvisit_scheme=https\ncolo=WAW\nloc=${country}\ntls=TLSv1.3\n` }
|
|
69
|
+
},
|
|
70
|
+
goto: (href: string, referrer = '') => {
|
|
71
|
+
current = new URL(href)
|
|
72
|
+
;(globalThis as any).document.referrer = referrer
|
|
73
|
+
},
|
|
74
|
+
reset: () => {
|
|
75
|
+
store.clear(); attributes.clear(); cookie = ''; calls.length = 0
|
|
76
|
+
current = new URL('https://site.test/'); (globalThis as any).document.referrer = ''
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const env = browser()
|
|
82
|
+
const cloudflare: ConsentOptions = { silent: true, geo: { cloudflare: true } }
|
|
83
|
+
const nowSeconds = (): number => Math.floor(Date.now() / 1000)
|
|
84
|
+
const gpc = (value: boolean | undefined): void => {
|
|
85
|
+
Object.defineProperty(globalThis.navigator, 'globalPrivacyControl', { value, configurable: true })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
env.reset()
|
|
90
|
+
env.located('US')
|
|
91
|
+
delete (globalThis as any).dataLayer
|
|
92
|
+
delete (globalThis as any).cookieConsentSetup
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
afterEach(() => {
|
|
96
|
+
for (const alias of ['cloudflare', 'app-geo', 'second-geo', 'linker']) {
|
|
97
|
+
consentPluginHelper.unregisterConsentPlugin(alias)
|
|
98
|
+
}
|
|
99
|
+
gpc(undefined)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('the country lists', () => {
|
|
103
|
+
test('the EU, the UK and Switzerland ask; the United States does not', () => {
|
|
104
|
+
for (const country of ['PL', 'DE', 'FR', 'IE', 'GR', 'RE', 'NO', 'GB', 'CH', 'BR']) {
|
|
105
|
+
expect({ country, ask: CONSENT_REQUIRED_COUNTRIES.includes(country) }).toEqual({ country, ask: true })
|
|
106
|
+
}
|
|
107
|
+
for (const country of ['US', 'JP', 'AU', 'SG', 'IN', 'MX']) {
|
|
108
|
+
expect({ country, ask: CONSENT_REQUIRED_COUNTRIES.includes(country) }).toEqual({ country, ask: false })
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('every entry is a distinct upper-case alpha-2 code', () => {
|
|
113
|
+
expect(CONSENT_REQUIRED_COUNTRIES.every(code => /^[A-Z]{2}$/.test(code))).toBe(true)
|
|
114
|
+
expect(new Set(CONSENT_REQUIRED_COUNTRIES).size).toBe(CONSENT_REQUIRED_COUNTRIES.length)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('consentGeoHelper', () => {
|
|
119
|
+
test('parseTrace reads key=value lines, CRLF included, and nothing out of HTML', () => {
|
|
120
|
+
expect(consentGeoHelper.parseTrace('fl=1\r\ncolo=WAW\r\nloc=PL\r\n')).toEqual({ fl: '1', colo: 'WAW', loc: 'PL' })
|
|
121
|
+
expect(consentGeoHelper.parseTrace('<!doctype html><html lang="en"><body>app</body></html>')).toEqual({})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('requiresConsent asks inside the list and for anything that names no country', () => {
|
|
125
|
+
expect(consentGeoHelper.requiresConsent('PL')).toBe(true)
|
|
126
|
+
expect(consentGeoHelper.requiresConsent('de')).toBe(true)
|
|
127
|
+
expect(consentGeoHelper.requiresConsent('US')).toBe(false)
|
|
128
|
+
for (const unknown of ['XX', 'T1', 'EU', '', 'USA', null]) {
|
|
129
|
+
expect({ unknown, ask: consentGeoHelper.requiresConsent(unknown) }).toEqual({ unknown, ask: true })
|
|
130
|
+
}
|
|
131
|
+
expect(consentGeoHelper.requiresConsent('US', { geo: { countries: ['US'] } })).toBe(true)
|
|
132
|
+
expect(consentGeoHelper.requiresConsent('PL', { geo: { countries: ['US'] } })).toBe(false)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('the Cloudflare locator reads the same-origin trace, without cookies and uncached', async () => {
|
|
136
|
+
env.located('PL')
|
|
137
|
+
|
|
138
|
+
expect(await consentGeoHelper.cloudflareLocator().locate(cloudflare)).toEqual({ country: 'PL' })
|
|
139
|
+
expect(env.calls[0].path).toBe(CONSENT_TRACE_PATH)
|
|
140
|
+
expect(env.calls[0].init).toMatchObject({ credentials: 'omit', cache: 'no-store' })
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('the Cloudflare locator honours a path override', async () => {
|
|
144
|
+
await consentGeoHelper.cloudflareLocator().locate({ geo: { cloudflare: { path: '/edge/trace' } } })
|
|
145
|
+
|
|
146
|
+
expect(env.calls[0].path).toBe('/edge/trace')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('the Cloudflare locator refuses a non-2xx answer, an HTML fallback page and being switched off', async () => {
|
|
150
|
+
env.trace({ status: 404, body: 'not found' })
|
|
151
|
+
await expect(consentGeoHelper.cloudflareLocator().locate(cloudflare)).rejects.toThrow()
|
|
152
|
+
|
|
153
|
+
env.trace({ status: 200, body: '<!doctype html><html><body>index</body></html>' })
|
|
154
|
+
await expect(consentGeoHelper.cloudflareLocator().locate(cloudflare)).rejects.toThrow()
|
|
155
|
+
|
|
156
|
+
await expect(consentGeoHelper.cloudflareLocator().locate({ geo: {} })).rejects.toThrow()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('locators run highest priority first, and a throw or an unusable code hands over to the next', async () => {
|
|
160
|
+
const order: string[] = []
|
|
161
|
+
consentPluginHelper.registerConsentPlugin({
|
|
162
|
+
alias: 'app-geo', priority: 10,
|
|
163
|
+
locate: async () => { order.push('app'); throw new Error('no idea') },
|
|
164
|
+
} satisfies ConsentGeoPlugin)
|
|
165
|
+
consentPluginHelper.registerConsentPlugin({
|
|
166
|
+
alias: 'second-geo', priority: 5,
|
|
167
|
+
locate: async () => { order.push('second'); return { country: 'XX' } },
|
|
168
|
+
} satisfies ConsentGeoPlugin)
|
|
169
|
+
consentPluginHelper.registerConsentPlugin(consentGeoHelper.cloudflareLocator())
|
|
170
|
+
env.located('fr')
|
|
171
|
+
|
|
172
|
+
expect(await consentPluginHelper.locateConsent(cloudflare)).toEqual({ country: 'FR' })
|
|
173
|
+
expect(order).toEqual(['app', 'second'])
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test('with no locator able to answer, locating rejects and deciding asks', async () => {
|
|
177
|
+
await expect(consentPluginHelper.locateConsent(cloudflare)).rejects.toThrow()
|
|
178
|
+
expect(await consentGeoHelper.decide(cloudflare)).toBe('ask')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('decide: outside the list is automatic, inside asks, a lookup past the timeout asks', async () => {
|
|
182
|
+
consentPluginHelper.registerConsentPlugin(consentGeoHelper.cloudflareLocator())
|
|
183
|
+
expect(await consentGeoHelper.decide(cloudflare)).toBe('auto')
|
|
184
|
+
|
|
185
|
+
env.located('PL')
|
|
186
|
+
expect(await consentGeoHelper.decide(cloudflare)).toBe('ask')
|
|
187
|
+
|
|
188
|
+
env.trace('hang')
|
|
189
|
+
expect(await consentGeoHelper.decide({ geo: { cloudflare: true, timeout: 20 } })).toBe('ask')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
test('the automatic decision grants everything, or only the required under Global Privacy Control', () => {
|
|
193
|
+
const all = consentGeoHelper.automaticRecord()
|
|
194
|
+
expect(all).toMatchObject({ [CONSENT_ESSENTIAL]: true, [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: true })
|
|
195
|
+
expect(Math.abs((all.auto ?? 0) - nowSeconds())).toBeLessThanOrEqual(1)
|
|
196
|
+
|
|
197
|
+
gpc(true)
|
|
198
|
+
expect(consentGeoHelper.automaticRecord())
|
|
199
|
+
.toMatchObject({ [CONSENT_ESSENTIAL]: true, [CONSENT_ANALYTICS]: false, [CONSENT_MARKETING]: false })
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
test('autoState: fresh, stale, from the future, explicit', () => {
|
|
203
|
+
expect(consentGeoHelper.autoState({ auto: nowSeconds() - 10 })).toBe('fresh')
|
|
204
|
+
expect(consentGeoHelper.autoState({ auto: nowSeconds() - CONSENT_AUTO_MAX_AGE - 5 })).toBe('stale')
|
|
205
|
+
expect(consentGeoHelper.autoState({ auto: nowSeconds() + 3600 })).toBe('stale')
|
|
206
|
+
expect(consentGeoHelper.autoState({ analytics: true })).toBeNull()
|
|
207
|
+
expect(consentGeoHelper.autoState(null)).toBeNull()
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
test('the gate is on only with geo set and something able to answer', () => {
|
|
211
|
+
expect(consentGeoHelper.enabled({})).toBe(false)
|
|
212
|
+
expect(consentGeoHelper.enabled({ geo: {} })).toBe(false)
|
|
213
|
+
expect(consentGeoHelper.enabled(cloudflare)).toBe(true)
|
|
214
|
+
consentPluginHelper.registerConsentPlugin({ alias: 'app-geo', locate: async () => ({ country: 'US' }) })
|
|
215
|
+
expect(consentGeoHelper.enabled({ geo: {} })).toBe(true)
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
describe('the store, with the geo gate', () => {
|
|
220
|
+
test('without geo it asks at once and never fetches — unchanged', () => {
|
|
221
|
+
const store = makeConsentStore()
|
|
222
|
+
store.init({ silent: true })
|
|
223
|
+
|
|
224
|
+
expect(store.get()).toMatchObject({ open: true, reason: 'initial', locating: null })
|
|
225
|
+
expect(env.calls.length).toBe(0)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
test('outside the list: locating first, then an automatic decision, stored and applied', async () => {
|
|
229
|
+
const store = makeConsentStore()
|
|
230
|
+
const seen: (string | null)[] = []
|
|
231
|
+
store.subscribe(state => seen.push(state.locating))
|
|
232
|
+
store.init(cloudflare)
|
|
233
|
+
|
|
234
|
+
expect(store.get()).toMatchObject({ open: false, locating: 'first', record: null })
|
|
235
|
+
expect(env.attributes.get('data-consent')).toBe('locating')
|
|
236
|
+
|
|
237
|
+
const settled = await store.settled()
|
|
238
|
+
|
|
239
|
+
expect(settled).toMatchObject({ open: false, locating: null })
|
|
240
|
+
expect(settled.record).toMatchObject({ [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: true })
|
|
241
|
+
expect(typeof consentStorageHelper.readConsent()?.auto).toBe('number')
|
|
242
|
+
expect(store.granted(CONSENT_ANALYTICS)).toBe(true)
|
|
243
|
+
expect(env.attributes.get('data-consent')).toBe('decided')
|
|
244
|
+
expect(seen).toEqual(['first', null])
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test('inside the list it asks, and nothing is stored', async () => {
|
|
248
|
+
env.located('PL')
|
|
249
|
+
const store = makeConsentStore()
|
|
250
|
+
store.init(cloudflare)
|
|
251
|
+
await store.settled()
|
|
252
|
+
|
|
253
|
+
expect(store.get()).toMatchObject({ open: true, reason: 'initial', record: null })
|
|
254
|
+
expect(consentStorageHelper.readConsent()).toBeNull()
|
|
255
|
+
expect(env.attributes.get('data-consent')).toBe('open')
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
test('a failed lookup asks', async () => {
|
|
259
|
+
env.trace('throw')
|
|
260
|
+
const store = makeConsentStore()
|
|
261
|
+
store.init(cloudflare)
|
|
262
|
+
await store.settled()
|
|
263
|
+
|
|
264
|
+
expect(store.get()).toMatchObject({ open: true, reason: 'initial' })
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
test('Global Privacy Control: the automatic decision is mandatory-only', async () => {
|
|
268
|
+
gpc(true)
|
|
269
|
+
const store = makeConsentStore()
|
|
270
|
+
store.init(cloudflare)
|
|
271
|
+
await store.settled()
|
|
272
|
+
|
|
273
|
+
expect(store.get().record).toMatchObject({ [CONSENT_ESSENTIAL]: true, [CONSENT_ANALYTICS]: false })
|
|
274
|
+
expect(store.granted(CONSENT_ANALYTICS)).toBe(false)
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
test('a decision saved while locating wins over the location', async () => {
|
|
278
|
+
const store = makeConsentStore()
|
|
279
|
+
store.init(cloudflare)
|
|
280
|
+
store.save({ [CONSENT_ESSENTIAL]: true, [CONSENT_ANALYTICS]: false, [CONSENT_MARKETING]: false })
|
|
281
|
+
await store.settled()
|
|
282
|
+
|
|
283
|
+
expect(store.get().record?.[CONSENT_ANALYTICS]).toBe(false)
|
|
284
|
+
expect(consentStorageHelper.readConsent()?.auto).toBeUndefined()
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
test('the window opened for signing in while locating is never decided over', async () => {
|
|
288
|
+
const store = makeConsentStore()
|
|
289
|
+
store.init(cloudflare)
|
|
290
|
+
store.open('login')
|
|
291
|
+
await store.settled()
|
|
292
|
+
|
|
293
|
+
expect(store.get()).toMatchObject({ open: true, reason: 'login', record: null, locating: null })
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
test('a second init while locating starts no second lookup', async () => {
|
|
297
|
+
const store = makeConsentStore()
|
|
298
|
+
store.init(cloudflare)
|
|
299
|
+
store.init(cloudflare)
|
|
300
|
+
store.init({ silent: true })
|
|
301
|
+
await store.settled()
|
|
302
|
+
|
|
303
|
+
expect(env.calls.length).toBe(1)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
test('an init that brings geo late takes back the ask an earlier init raised', async () => {
|
|
307
|
+
const store = makeConsentStore()
|
|
308
|
+
store.init({ silent: true })
|
|
309
|
+
expect(store.get().open).toBe(true)
|
|
310
|
+
|
|
311
|
+
store.init(cloudflare)
|
|
312
|
+
expect(store.get()).toMatchObject({ open: false, locating: 'first' })
|
|
313
|
+
await store.settled()
|
|
314
|
+
|
|
315
|
+
expect(store.get().record?.auto).toBeNumber()
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
test('a fresh automatic decision is applied as it is, with no lookup', () => {
|
|
319
|
+
consentStorageHelper.writeConsent({ ...consentGeoHelper.automaticRecord(), auto: nowSeconds() - 60 })
|
|
320
|
+
const store = makeConsentStore()
|
|
321
|
+
store.init(cloudflare)
|
|
322
|
+
|
|
323
|
+
expect(store.get()).toMatchObject({ open: false, locating: null })
|
|
324
|
+
expect(store.granted(CONSENT_ANALYTICS)).toBe(true)
|
|
325
|
+
expect(env.calls.length).toBe(0)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
test('an explicit decision is never located', () => {
|
|
329
|
+
consentStorageHelper.writeConsent({ [CONSENT_ESSENTIAL]: true, [CONSENT_ANALYTICS]: false })
|
|
330
|
+
const store = makeConsentStore()
|
|
331
|
+
store.init(cloudflare)
|
|
332
|
+
|
|
333
|
+
expect(store.get()).toMatchObject({ open: false, locating: null })
|
|
334
|
+
expect(env.calls.length).toBe(0)
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
test('a stale automatic decision is held back and re-checked: still outside, it is renewed', async () => {
|
|
338
|
+
const stale = nowSeconds() - CONSENT_AUTO_MAX_AGE - 60
|
|
339
|
+
consentStorageHelper.writeConsent({ ...consentGeoHelper.automaticRecord(), auto: stale })
|
|
340
|
+
const store = makeConsentStore()
|
|
341
|
+
store.init(cloudflare)
|
|
342
|
+
|
|
343
|
+
expect(store.get()).toMatchObject({ record: null, open: false, locating: 'recheck' })
|
|
344
|
+
expect(store.granted(CONSENT_ANALYTICS)).toBe(false)
|
|
345
|
+
await store.settled()
|
|
346
|
+
|
|
347
|
+
expect(store.get().record?.auto).toBeGreaterThan(stale)
|
|
348
|
+
expect(store.granted(CONSENT_ANALYTICS)).toBe(true)
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
test('a stale automatic decision met inside the list is dropped, and the visitor is asked', async () => {
|
|
352
|
+
env.located('DE')
|
|
353
|
+
consentStorageHelper.writeConsent({ ...consentGeoHelper.automaticRecord(), auto: nowSeconds() - CONSENT_AUTO_MAX_AGE - 60 })
|
|
354
|
+
const store = makeConsentStore()
|
|
355
|
+
store.init(cloudflare)
|
|
356
|
+
await store.settled()
|
|
357
|
+
|
|
358
|
+
expect(store.get()).toMatchObject({ open: true, reason: 'initial', record: null })
|
|
359
|
+
expect(consentStorageHelper.readConsent()).toBeNull()
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
test('an explicit save drops the automatic marker', () => {
|
|
363
|
+
const store = makeConsentStore()
|
|
364
|
+
store.init({ silent: true })
|
|
365
|
+
store.save({ ...consentGeoHelper.automaticRecord(), [CONSENT_MARKETING]: false })
|
|
366
|
+
|
|
367
|
+
expect(store.get().record?.auto).toBeUndefined()
|
|
368
|
+
expect(consentStorageHelper.readConsent()?.auto).toBeUndefined()
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
test('an application locator decides without Cloudflare', async () => {
|
|
372
|
+
consentPluginHelper.registerConsentPlugin({ alias: 'app-geo', locate: async () => ({ country: 'JP' }) })
|
|
373
|
+
const store = makeConsentStore()
|
|
374
|
+
store.init({ silent: true, geo: {} })
|
|
375
|
+
await store.settled()
|
|
376
|
+
|
|
377
|
+
expect(store.get().record?.auto).toBeNumber()
|
|
378
|
+
expect(env.calls.length).toBe(0)
|
|
379
|
+
})
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
describe('the head scripts and an automatic decision', () => {
|
|
383
|
+
const run = (script: string) => {
|
|
384
|
+
// eslint-disable-next-line no-new-func
|
|
385
|
+
new Function(script)()
|
|
386
|
+
}
|
|
387
|
+
const updates = (): Record<string, string>[] => ((globalThis as any).dataLayer ?? [])
|
|
388
|
+
.filter((entry: IArguments) => entry[0] === 'consent' && entry[1] === 'update')
|
|
389
|
+
.map((entry: IArguments) => entry[2])
|
|
390
|
+
|
|
391
|
+
test('the bootstrap applies a fresh automatic decision and skips a stale one', () => {
|
|
392
|
+
consentStorageHelper.writeConsent({ ...consentGeoHelper.automaticRecord(), auto: nowSeconds() })
|
|
393
|
+
run(consentModeHelper.consentBootstrapScript())
|
|
394
|
+
expect(updates()[0]?.analytics_storage).toBe('granted')
|
|
395
|
+
|
|
396
|
+
delete (globalThis as any).dataLayer
|
|
397
|
+
delete (globalThis as any).cookieConsentSetup
|
|
398
|
+
consentStorageHelper.writeConsent({ ...consentGeoHelper.automaticRecord(), auto: nowSeconds() - CONSENT_AUTO_MAX_AGE - 1 })
|
|
399
|
+
run(consentModeHelper.consentBootstrapScript())
|
|
400
|
+
expect(updates()).toEqual([])
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
test('the gate loads for a fresh automatic grant and waits on a stale one', () => {
|
|
404
|
+
;(globalThis as any).__loaded = 0
|
|
405
|
+
const listeners: (() => void)[] = []
|
|
406
|
+
const add = (globalThis as any).addEventListener
|
|
407
|
+
;(globalThis as any).addEventListener = (_type: string, cb: () => void) => { listeners.push(cb) }
|
|
408
|
+
try {
|
|
409
|
+
consentStorageHelper.writeConsent({ ...consentGeoHelper.automaticRecord(), auto: nowSeconds() })
|
|
410
|
+
run(consentModeHelper.consentGateScript('window.__loaded++'))
|
|
411
|
+
expect((globalThis as any).__loaded).toBe(1)
|
|
412
|
+
|
|
413
|
+
consentStorageHelper.writeConsent({ ...consentGeoHelper.automaticRecord(), auto: nowSeconds() - CONSENT_AUTO_MAX_AGE - 1 })
|
|
414
|
+
run(consentModeHelper.consentGateScript('window.__loaded++'))
|
|
415
|
+
expect((globalThis as any).__loaded).toBe(1)
|
|
416
|
+
expect(listeners.length).toBe(1)
|
|
417
|
+
} finally {
|
|
418
|
+
;(globalThis as any).addEventListener = add
|
|
419
|
+
delete (globalThis as any).__loaded
|
|
420
|
+
}
|
|
421
|
+
})
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
describe('the linker and an automatic decision', () => {
|
|
425
|
+
const DOMAINS = ['site.test', 'platform.test']
|
|
426
|
+
|
|
427
|
+
test('an automatic decision never travels as the visitor\'s own', () => {
|
|
428
|
+
const record: ConsentRecord = { ...consentGeoHelper.automaticRecord() }
|
|
429
|
+
const decoded = consentLinkHelper.decodeConsentLink(consentLinkHelper.encodeConsentLink(record))
|
|
430
|
+
|
|
431
|
+
expect(decoded?.c).toEqual({})
|
|
432
|
+
expect(consentLinkHelper.consentLinker().decorate?.(new URL('https://platform.test/'), record, { linker: { domains: DOMAINS } })).toBeNull()
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
test('the store adopts an explicit decision carried in over an automatic one', () => {
|
|
436
|
+
consentStorageHelper.writeConsent({ ...consentGeoHelper.automaticRecord(), auto: nowSeconds() })
|
|
437
|
+
const incoming = { [CONSENT_ANALYTICS]: false, [CONSENT_MARKETING]: false }
|
|
438
|
+
env.goto(`https://site.test/?owlcc=${consentLinkHelper.encodeConsentLink(incoming)}`, 'https://platform.test/')
|
|
439
|
+
const store = makeConsentStore()
|
|
440
|
+
store.init({ silent: true, linker: { domains: DOMAINS }, geo: { cloudflare: true } })
|
|
441
|
+
|
|
442
|
+
expect(store.get().record).toMatchObject({ [CONSENT_ANALYTICS]: false, [CONSENT_ESSENTIAL]: true })
|
|
443
|
+
expect(consentStorageHelper.readConsent()?.auto).toBeUndefined()
|
|
444
|
+
expect(env.calls.length).toBe(0)
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
test('the inline fragment adopts over an automatic decision, never over an explicit one', () => {
|
|
448
|
+
const incoming = { [CONSENT_ANALYTICS]: false, [CONSENT_MARKETING]: false }
|
|
449
|
+
const script = consentLinkHelper.consentLinkerScript({ linker: { domains: DOMAINS } })
|
|
450
|
+
|
|
451
|
+
consentStorageHelper.writeConsent({ ...consentGeoHelper.automaticRecord(), auto: nowSeconds() })
|
|
452
|
+
env.goto(`https://site.test/?owlcc=${consentLinkHelper.encodeConsentLink(incoming)}`, 'https://platform.test/')
|
|
453
|
+
// eslint-disable-next-line no-new-func
|
|
454
|
+
new Function(script)()
|
|
455
|
+
expect(JSON.parse(env.store.get(CONSENT_KEY) ?? '{}')).toMatchObject({ [CONSENT_ANALYTICS]: false })
|
|
456
|
+
expect(JSON.parse(env.store.get(CONSENT_KEY) ?? '{}').auto).toBeUndefined()
|
|
457
|
+
|
|
458
|
+
consentStorageHelper.writeConsent({ [CONSENT_ESSENTIAL]: true, [CONSENT_ANALYTICS]: true, [CONSENT_MARKETING]: true })
|
|
459
|
+
env.goto(`https://site.test/?owlcc=${consentLinkHelper.encodeConsentLink(incoming)}`, 'https://platform.test/')
|
|
460
|
+
// eslint-disable-next-line no-new-func
|
|
461
|
+
new Function(script)()
|
|
462
|
+
expect(JSON.parse(env.store.get(CONSENT_KEY) ?? '{}')).toMatchObject({ [CONSENT_ANALYTICS]: true })
|
|
463
|
+
})
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
describe('storage', () => {
|
|
467
|
+
test('clearing removes the cookie written under a cookie domain', () => {
|
|
468
|
+
consentStorageHelper.writeConsent({ [CONSENT_ANALYTICS]: true }, { cookieDomain: 'site.test' })
|
|
469
|
+
env.store.clear()
|
|
470
|
+
consentStorageHelper.clearConsent({ cookieDomain: 'site.test' })
|
|
471
|
+
|
|
472
|
+
expect(consentStorageHelper.readConsent()).toBeNull()
|
|
473
|
+
})
|
|
474
|
+
})
|