Skip to content

Commit 17763e1

Browse files
committed
Update Consent Banner component tests to new test pattern, add consent store actions to support client
1 parent 3370bf5 commit 17763e1

6 files changed

Lines changed: 441 additions & 346 deletions

File tree

‎.github/instructions/general.instructions.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ applyTo: "**"
1818
- Do not use run-astro-dev, always use "npm run dev".
1919
- Always create TypeScript files, not JavaScript files.
2020
- Prefer destructured imports over namespace imports when importing specific functions from modules (e.g., `import { resolve } from 'path'` instead of `import * as path from 'path'`).
21+
- Do not access nanostore observables (e.g., `$consent`) directly from components; expose helper/action methods in `@components/scripts/store` and import those instead.
2122

2223
# Code Organization and Directory Structure
2324

Lines changed: 154 additions & 184 deletions
Original file line numberDiff line numberDiff line change
@@ -1,212 +1,182 @@
1-
// @vitest-environment happy-dom
2-
3-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
4-
import { ConsentPreferencesElement } from '@components/Consent/Preferences/client'
5-
import { $consent, updateConsent, allowAllConsent } from '@components/scripts/store'
6-
7-
// Mock dependencies
8-
vi.mock('@components/scripts/store', () => ({
9-
$consent: {
10-
get: vi.fn(),
11-
set: vi.fn(),
12-
},
13-
updateConsent: vi.fn(),
14-
allowAllConsent: vi.fn(),
15-
createConsentController: vi.fn(() => ({
16-
value: {
17-
analytics: false,
18-
functional: true,
19-
marketing: false,
20-
DataSubjectId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', // Valid UUID format
21-
},
22-
})),
23-
}))
24-
25-
// Mock bootstrap initialization
26-
const mockBootstrap = {
27-
init: vi.fn(),
28-
}
29-
30-
vi.mock('@components/scripts/bootstrap', () => ({
31-
AppBootstrap: mockBootstrap,
32-
}))
33-
34-
// Mock toast controller
35-
const mockToastController = {
36-
show: vi.fn(),
37-
}
38-
39-
vi.mock('@components/Toasts/controller', () => ({
40-
ToastController: mockToastController,
41-
}))
42-
43-
// DOM element creation helpers
44-
const mockButton = (id: string): HTMLButtonElement => {
45-
const button = document.createElement('button')
46-
button.id = id
47-
return button
48-
}
49-
50-
const mockDiv = (id: string): HTMLDivElement => {
51-
const div = document.createElement('div')
52-
div.id = id
53-
div.style.display = 'none'
54-
return div
1+
// @vitest-environment node
2+
3+
import { beforeEach, describe, expect, it, vi } from 'vitest'
4+
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
5+
import type { WebComponentModule } from '@components/scripts/@types/webComponentModule'
6+
import ConsentPreferencesComponent from '@components/Consent/Preferences/index.astro'
7+
import type { ConsentPreferencesElement } from '@components/Consent/Preferences/client'
8+
import type { ConsentCategory, ConsentState } from '@components/scripts/store'
9+
import { allowAllConsent, updateConsent } from '@components/scripts/store'
10+
import { executeRender, withJsdomEnvironment } from '@test/unit/helpers/litRuntime'
11+
12+
type ConsentPreferencesModule = WebComponentModule<ConsentPreferencesElement>
13+
type JsdomWindow = Window & typeof globalThis
14+
15+
const CONSENT_READY_TIMEOUT_MS = 2_000
16+
const CONSENT_PREFERENCES_READY_EVENT = 'consent-preferences:ready'
17+
18+
const waitForPreferencesReady = async (element: ConsentPreferencesElement) => {
19+
if (element.dataset['consentPreferencesReady'] === 'true') {
20+
return
21+
}
22+
23+
await new Promise<void>((resolve, reject) => {
24+
const timeoutId = setTimeout(() => {
25+
element.removeEventListener(CONSENT_PREFERENCES_READY_EVENT, onReady)
26+
reject(new Error('Consent preferences component never finished initializing'))
27+
}, CONSENT_READY_TIMEOUT_MS)
28+
29+
function onReady() {
30+
clearTimeout(timeoutId)
31+
resolve()
32+
}
33+
34+
element.addEventListener(CONSENT_PREFERENCES_READY_EVENT, onReady, { once: true })
35+
})
5536
}
5637

57-
const mockCheckbox = (id: string): HTMLInputElement => {
58-
const input = document.createElement('input')
59-
input.type = 'checkbox'
60-
input.id = id
61-
return input
38+
const renderConsentPreferences = async (
39+
assertion: (_context: { element: ConsentPreferencesElement; window: JsdomWindow }) => Promise<void> | void,
40+
) => {
41+
const container = await AstroContainer.create()
42+
43+
await executeRender<ConsentPreferencesModule>({
44+
container,
45+
component: ConsentPreferencesComponent,
46+
moduleSpecifier: '@components/Consent/Preferences/client/index',
47+
selector: 'consent-preferences',
48+
waitForReady: waitForPreferencesReady,
49+
assert: async ({ element, window }) => {
50+
if (!window) {
51+
throw new Error('Consent preferences tests require a window instance')
52+
}
53+
54+
await assertion({ element, window: window as JsdomWindow })
55+
},
56+
})
6257
}
6358

64-
// Mock cookie storage for cleanup
65-
let cookieStorage: any[] = []
59+
const consentMockHelpers = vi.hoisted(() => {
60+
const defaultState: ConsentState = {
61+
analytics: false,
62+
functional: false,
63+
marketing: false,
64+
DataSubjectId: '00000000-0000-0000-0000-000000000000',
65+
}
66+
67+
const state: ConsentState = { ...defaultState }
68+
const listeners = new Set<(_consent: ConsentState) => void>()
69+
70+
return {
71+
state,
72+
listeners,
73+
reset() {
74+
state.analytics = defaultState.analytics
75+
state.functional = defaultState.functional
76+
state.marketing = defaultState.marketing
77+
state.DataSubjectId = defaultState.DataSubjectId
78+
listeners.clear()
79+
},
80+
notify() {
81+
const snapshot: ConsentState = { ...state }
82+
listeners.forEach((listener) => listener(snapshot))
83+
},
84+
}
85+
})
6686

67-
Object.defineProperty(document, 'cookie', {
68-
get: () => cookieStorage.join('; '),
69-
set: (value) => {
70-
cookieStorage.push(value)
71-
},
72-
configurable: true,
87+
vi.mock('@components/scripts/store', () => {
88+
const { state, listeners, notify } = consentMockHelpers
89+
90+
return {
91+
__esModule: true,
92+
getConsentSnapshot: vi.fn(() => ({ ...state })),
93+
subscribeToConsentState: vi.fn((listener: (_consent: ConsentState) => void) => {
94+
listeners.add(listener)
95+
return () => listeners.delete(listener)
96+
}),
97+
updateConsent: vi.fn((category: ConsentCategory, value: boolean) => {
98+
state[category] = value
99+
notify()
100+
}),
101+
allowAllConsent: vi.fn(() => {
102+
state.analytics = true
103+
state.functional = true
104+
state.marketing = true
105+
notify()
106+
}),
107+
}
73108
})
74109

75110
describe('ConsentPreferencesElement', () => {
76-
beforeEach(() => {
77-
// Initialize state management
78-
mockBootstrap.init()
79-
80-
// Reset mocks
111+
beforeEach(async () => {
112+
consentMockHelpers.reset()
81113
vi.clearAllMocks()
82114

83-
// Reset cookies
84-
cookieStorage = []
115+
await withJsdomEnvironment(({ window }) => {
116+
window.sessionStorage.clear()
117+
window.localStorage.clear()
118+
})
119+
})
85120

86-
// Clear document body
87-
document.body.innerHTML = ''
121+
it('shows the modal when showModal is invoked', async () => {
122+
await renderConsentPreferences(({ element, window }) => {
123+
const wrapper = window.document.getElementById('consent-modal-modal-id') as HTMLDivElement | null
124+
expect(wrapper).not.toBeNull()
88125

89-
// Mock default consent state
90-
vi.mocked($consent.get).mockReturnValue({
91-
analytics: false,
92-
functional: false,
93-
marketing: false,
94-
DataSubjectId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', // Valid UUID format
126+
element.showModal()
127+
expect(wrapper!.style.display).toBe('flex')
95128
})
96129
})
97130

98-
describe('Web Component Registration', () => {
99-
it('should be registered as custom element', () => {
100-
expect(customElements.get('consent-preferences')).toBeDefined()
131+
it('hides the modal when the close button is clicked', async () => {
132+
await renderConsentPreferences(({ element, window }) => {
133+
element.showModal()
134+
const wrapper = window.document.getElementById('consent-modal-modal-id') as HTMLDivElement | null
135+
expect(wrapper).not.toBeNull()
136+
const closeBtn = window.document.querySelector('.consent-modal__close-btn') as HTMLButtonElement | null
137+
expect(closeBtn).not.toBeNull()
138+
139+
closeBtn!.dispatchEvent(new window.MouseEvent('click', { bubbles: true }))
140+
141+
expect(wrapper!.style.display).toBe('none')
101142
})
102143
})
103144

104-
describe('Component Interaction', () => {
105-
let element: ConsentPreferencesElement
106-
let mockModal: HTMLDivElement
107-
let mockCloseBtn: HTMLButtonElement
108-
let mockAllowBtn: HTMLButtonElement
109-
let mockSaveBtn: HTMLButtonElement
110-
111-
beforeEach(() => {
112-
// Create mock DOM structure
113-
mockModal = mockDiv('consent-modal-modal-id')
114-
mockCloseBtn = mockButton('consent-modal__close-btn')
115-
mockCloseBtn.classList.add('consent-modal__close-btn')
116-
mockCloseBtn.dataset['testid'] = 'consent-preferences-close'
117-
mockAllowBtn = mockButton('consent-allow-all')
118-
mockSaveBtn = mockButton('consent-save-preferences')
119-
120-
// Add elements to the document
121-
document.body.appendChild(mockModal)
122-
document.body.appendChild(mockCloseBtn)
123-
document.body.appendChild(mockAllowBtn)
124-
document.body.appendChild(mockSaveBtn)
125-
126-
// Create web component instance
127-
element = new ConsentPreferencesElement()
128-
document.body.appendChild(element)
129-
})
145+
it('grants all consent when Allow All is clicked', async () => {
146+
await renderConsentPreferences(({ window }) => {
147+
const allowBtn = window.document.getElementById('consent-allow-all') as HTMLButtonElement | null
148+
expect(allowBtn).not.toBeNull()
130149

131-
afterEach(() => {
132-
// Cleanup
133-
if (mockModal.parentNode) document.body.removeChild(mockModal)
134-
if (mockCloseBtn.parentNode) document.body.removeChild(mockCloseBtn)
135-
if (mockAllowBtn.parentNode) document.body.removeChild(mockAllowBtn)
136-
if (mockSaveBtn.parentNode) document.body.removeChild(mockSaveBtn)
137-
if (element.parentNode) document.body.removeChild(element)
150+
allowBtn!.dispatchEvent(new window.MouseEvent('click', { bubbles: true }))
151+
152+
expect(allowAllConsent).toHaveBeenCalled()
153+
expect((window.document.getElementById('analytics-cookies') as HTMLInputElement).checked).toBe(true)
154+
expect((window.document.getElementById('functional-cookies') as HTMLInputElement).checked).toBe(true)
155+
expect((window.document.getElementById('marketing-cookies') as HTMLInputElement).checked).toBe(true)
138156
})
157+
})
139158

140-
describe('Modal Operations', () => {
141-
it('should show modal when showModal is called', () => {
142-
element.showModal()
143-
expect(element['modal']?.style.display).toBe('flex')
144-
})
159+
it('saves consent preferences when Save button is clicked', async () => {
160+
await renderConsentPreferences(({ window }) => {
161+
const analyticsCheckbox = window.document.getElementById('analytics-cookies') as HTMLInputElement | null
162+
const functionalCheckbox = window.document.getElementById('functional-cookies') as HTMLInputElement | null
163+
const marketingCheckbox = window.document.getElementById('marketing-cookies') as HTMLInputElement | null
164+
expect(analyticsCheckbox).not.toBeNull()
165+
expect(functionalCheckbox).not.toBeNull()
166+
expect(marketingCheckbox).not.toBeNull()
145167

146-
it('should hide modal when close button is clicked', () => {
147-
// First show the modal
148-
element.showModal()
149-
expect(element['modal']?.style.display).toBe('flex')
168+
analyticsCheckbox!.checked = true
169+
functionalCheckbox!.checked = false
170+
marketingCheckbox!.checked = true
150171

151-
// Click close button
152-
mockCloseBtn.click()
172+
const saveBtn = window.document.getElementById('consent-save-preferences') as HTMLButtonElement | null
173+
expect(saveBtn).not.toBeNull()
153174

154-
// Modal should be hidden
155-
expect(element['modal']?.style.display).toBe('none')
156-
})
157-
})
175+
saveBtn!.dispatchEvent(new window.MouseEvent('click', { bubbles: true }))
158176

159-
describe('Consent Management', () => {
160-
it('should handle allow all button click', () => {
161-
const mockDataSubjectId = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' // Valid UUID format
162-
vi.mocked($consent.get).mockReturnValue({
163-
analytics: false,
164-
functional: false,
165-
marketing: false,
166-
DataSubjectId: mockDataSubjectId,
167-
})
168-
169-
// Click allow all button
170-
mockAllowBtn.click()
171-
172-
// Verify: allowAllConsent action called
173-
expect(allowAllConsent).toHaveBeenCalled()
174-
})
175-
176-
it('should handle save preferences button click', () => {
177-
// Set up checkbox elements that the component will find
178-
const analyticsCheckbox = mockCheckbox('analytics-cookies')
179-
analyticsCheckbox.checked = true
180-
const functionalCheckbox = mockCheckbox('functional-cookies')
181-
functionalCheckbox.checked = false
182-
const marketingCheckbox = mockCheckbox('marketing-cookies')
183-
marketingCheckbox.checked = true
184-
185-
document.body.appendChild(analyticsCheckbox)
186-
document.body.appendChild(functionalCheckbox)
187-
document.body.appendChild(marketingCheckbox)
188-
189-
const mockDataSubjectId = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' // Valid UUID format
190-
vi.mocked($consent.get).mockReturnValue({
191-
analytics: false,
192-
functional: false,
193-
marketing: false,
194-
DataSubjectId: mockDataSubjectId,
195-
})
196-
197-
// Click save preferences button
198-
mockSaveBtn.click()
199-
200-
// Verify: updateConsent called with checkbox states
201-
expect(updateConsent).toHaveBeenCalledWith('analytics', true)
202-
expect(updateConsent).toHaveBeenCalledWith('functional', false)
203-
expect(updateConsent).toHaveBeenCalledWith('marketing', true)
204-
205-
// Cleanup
206-
document.body.removeChild(analyticsCheckbox)
207-
document.body.removeChild(functionalCheckbox)
208-
document.body.removeChild(marketingCheckbox)
209-
})
177+
expect(updateConsent).toHaveBeenCalledWith('analytics', true)
178+
expect(updateConsent).toHaveBeenCalledWith('functional', false)
179+
expect(updateConsent).toHaveBeenCalledWith('marketing', true)
210180
})
211181
})
212182
})

0 commit comments

Comments
 (0)