Skip to content

Commit b4b0317

Browse files
committed
Refactor scripts/store files to JSDOM environment unit tests, add missing coverages
1 parent d54b03a commit b4b0317

5 files changed

Lines changed: 441 additions & 18 deletions

File tree

src/components/scripts/store/__tests__/consent.spec.ts

Lines changed: 140 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Unit tests for cookie consent state management
44
*/
55
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
6+
import type { ConsentState } from '@components/scripts/store/consent'
67
import {
78
$consent,
89
$hasAnalyticsConsent,
@@ -20,7 +21,9 @@ import {
2021
setConsentCookie,
2122
removeConsentCookies,
2223
getAnalyticsConsentPreference,
24+
initConsentSideEffects,
2325
} from '@components/scripts/store/consent'
26+
import { $isConsentBannerVisible } from '@components/scripts/store/visibility'
2427

2528
// Mock js-cookie
2629
vi.mock('js-cookie', () => ({
@@ -41,6 +44,24 @@ vi.mock('@components/scripts/utils/cookies', () => ({
4144

4245
// Import mocked functions for spying
4346
import { getCookie, removeCookie, setCookie } from '@components/scripts/utils/cookies'
47+
import { deleteDataSubjectId } from '@components/scripts/utils/dataSubjectId'
48+
import { updateConsentContext } from '@components/scripts/sentry/helpers'
49+
50+
vi.mock('@components/scripts/utils/dataSubjectId', () => ({
51+
getOrCreateDataSubjectId: vi.fn(() => 'data-subject-123'),
52+
deleteDataSubjectId: vi.fn(),
53+
}))
54+
55+
vi.mock('@components/scripts/sentry/helpers', () => ({
56+
updateConsentContext: vi.fn(),
57+
}))
58+
59+
afterEach(() => {
60+
vi.restoreAllMocks()
61+
vi.clearAllMocks()
62+
vi.unstubAllGlobals()
63+
document.getElementById('consent-modal-id')?.remove()
64+
})
4465

4566
describe('Cookie Consent Management', () => {
4667
beforeEach(() => {
@@ -59,10 +80,6 @@ describe('Cookie Consent Management', () => {
5980
localStorage.clear()
6081
})
6182

62-
afterEach(() => {
63-
vi.clearAllMocks()
64-
})
65-
6683
describe('Consent State', () => {
6784
it('should initialize with default consent state', () => {
6885
const consent = $consent.get()
@@ -265,3 +282,122 @@ describe('Cookie Consent Management', () => {
265282
})
266283
})
267284
})
285+
286+
describe('Consent side effects', () => {
287+
beforeEach(() => {
288+
vi.clearAllMocks()
289+
})
290+
291+
it('syncs consent modal visibility with banner store updates', () => {
292+
const modal = document.createElement('div')
293+
modal.id = 'consent-modal-id'
294+
document.body.appendChild(modal)
295+
296+
let visibilityListener: ((visible: boolean) => void) | undefined
297+
vi.spyOn($isConsentBannerVisible, 'subscribe').mockImplementation((listener) => {
298+
visibilityListener = listener
299+
return () => {}
300+
})
301+
vi.spyOn($consent, 'subscribe').mockImplementation(() => () => {})
302+
vi.spyOn($hasFunctionalConsent, 'subscribe').mockImplementation(() => () => {})
303+
vi.spyOn($hasAnalyticsConsent, 'subscribe').mockImplementation(() => () => {})
304+
305+
initConsentSideEffects()
306+
307+
expect(visibilityListener).toBeDefined()
308+
309+
visibilityListener?.(true)
310+
expect(modal.style.display).toBe('flex')
311+
312+
visibilityListener?.(false)
313+
expect(modal.style.display).toBe('none')
314+
})
315+
316+
it('logs consent updates via the GDPR API when preferences change', async () => {
317+
const fetchSpy = vi.fn().mockResolvedValue({ ok: true })
318+
vi.stubGlobal('fetch', fetchSpy)
319+
320+
let consentListener:
321+
| ((state: ConsentState, oldState?: ConsentState) => Promise<void> | void)
322+
| undefined
323+
vi.spyOn($consent, 'subscribe').mockImplementation((listener) => {
324+
consentListener = listener
325+
return () => {}
326+
})
327+
vi.spyOn($isConsentBannerVisible, 'subscribe').mockImplementation(() => () => {})
328+
vi.spyOn($hasFunctionalConsent, 'subscribe').mockImplementation(() => () => {})
329+
vi.spyOn($hasAnalyticsConsent, 'subscribe').mockImplementation(() => () => {})
330+
331+
initConsentSideEffects()
332+
333+
const oldState = {
334+
analytics: false,
335+
marketing: false,
336+
functional: false,
337+
DataSubjectId: 'subject-123',
338+
}
339+
const newState = {
340+
analytics: true,
341+
marketing: false,
342+
functional: false,
343+
DataSubjectId: 'subject-123',
344+
}
345+
346+
await consentListener?.(newState, oldState)
347+
348+
expect(fetchSpy).toHaveBeenCalledTimes(1)
349+
const [url, options] = fetchSpy.mock.calls[0]
350+
expect(url).toBe('/api/gdpr/consent')
351+
expect(options?.method).toBe('POST')
352+
const payload = JSON.parse(options?.body as string)
353+
expect(payload).toMatchObject({
354+
DataSubjectId: 'subject-123',
355+
purposes: ['analytics'],
356+
source: 'cookies_modal',
357+
verified: false,
358+
})
359+
360+
await consentListener?.(newState, undefined)
361+
expect(fetchSpy).toHaveBeenCalledTimes(1)
362+
})
363+
364+
it('deletes the data subject id when functional consent is revoked', () => {
365+
let functionalListener: ((hasConsent: boolean) => void) | undefined
366+
vi.spyOn($hasFunctionalConsent, 'subscribe').mockImplementation((listener) => {
367+
functionalListener = listener
368+
return () => {}
369+
})
370+
vi.spyOn($isConsentBannerVisible, 'subscribe').mockImplementation(() => () => {})
371+
vi.spyOn($consent, 'subscribe').mockImplementation(() => () => {})
372+
vi.spyOn($hasAnalyticsConsent, 'subscribe').mockImplementation(() => () => {})
373+
374+
initConsentSideEffects()
375+
376+
expect(functionalListener).toBeDefined()
377+
378+
functionalListener?.(true)
379+
expect(deleteDataSubjectId).not.toHaveBeenCalled()
380+
381+
functionalListener?.(false)
382+
expect(deleteDataSubjectId).toHaveBeenCalledTimes(1)
383+
})
384+
385+
it('updates the Sentry consent context when analytics consent changes', async () => {
386+
let analyticsListener: ((hasConsent: boolean) => void) | undefined
387+
vi.spyOn($hasAnalyticsConsent, 'subscribe').mockImplementation((listener) => {
388+
analyticsListener = listener
389+
return () => {}
390+
})
391+
vi.spyOn($isConsentBannerVisible, 'subscribe').mockImplementation(() => () => {})
392+
vi.spyOn($consent, 'subscribe').mockImplementation(() => () => {})
393+
vi.spyOn($hasFunctionalConsent, 'subscribe').mockImplementation(() => () => {})
394+
395+
initConsentSideEffects()
396+
397+
analyticsListener?.(true)
398+
399+
await vi.waitFor(() => {
400+
expect(updateConsentContext).toHaveBeenCalledWith(true)
401+
})
402+
})
403+
})

src/components/scripts/store/__tests__/mastodonInstances.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// @vitest-environment happy-dom
1+
// @vitest-environment jsdom
22
/**
33
* Unit tests for Mastodon instances state management
44
*/

src/components/scripts/store/__tests__/socialEmbeds.spec.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// @vitest-environment happy-dom
1+
// @vitest-environment jsdom
22
/**
33
* Unit tests for social embeds cache state management
44
*
@@ -11,7 +11,13 @@
1111
* No consent check required - always caches and retrieves.
1212
*/
1313
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
14-
import { cacheEmbed, getCachedEmbed } from '@components/scripts/store/socialEmbeds'
14+
import {
15+
cacheEmbed,
16+
clearEmbedCache,
17+
getCachedEmbed,
18+
getEmbedCacheState,
19+
setEmbedCacheState,
20+
} from '@components/scripts/store/socialEmbeds'
1521

1622
// Mock js-cookie
1723
vi.mock('js-cookie', () => ({
@@ -87,4 +93,26 @@ describe('Embed Cache Management', () => {
8793
const cached = getCachedEmbed('twitter_123')
8894
expect(cached).toEqual(updatedData)
8995
})
96+
97+
it('should clear the entire cache when requested', () => {
98+
cacheEmbed('twitter_123', { html: 'foo' }, 3600000)
99+
cacheEmbed('youtube_456', { html: 'bar' }, 3600000)
100+
101+
clearEmbedCache()
102+
103+
expect(getCachedEmbed('twitter_123')).toBeNull()
104+
expect(getCachedEmbed('youtube_456')).toBeNull()
105+
expect(getEmbedCacheState()).toEqual({})
106+
})
107+
108+
it('should expose cache state getters and setters', () => {
109+
const state = {
110+
entry_a: { data: { html: 'A' }, timestamp: 0, ttl: 1000 },
111+
entry_b: { data: { html: 'B' }, timestamp: 0, ttl: 1000 },
112+
}
113+
114+
setEmbedCacheState(state)
115+
116+
expect(getEmbedCacheState()).toEqual(state)
117+
})
90118
})

0 commit comments

Comments
 (0)