33 * Unit tests for cookie consent state management
44 */
55import { describe , it , expect , beforeEach , vi , afterEach } from 'vitest'
6+ import type { ConsentState } from '@components/scripts/store/consent'
67import {
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
2629vi . mock ( 'js-cookie' , ( ) => ( {
@@ -41,6 +44,24 @@ vi.mock('@components/scripts/utils/cookies', () => ({
4144
4245// Import mocked functions for spying
4346import { 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
4566describe ( '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+ } )
0 commit comments