Skip to content

Commit 3370bf5

Browse files
committed
Update Consent Banner component tests to new test pattern, refactor to use consent store instead of cookies for managing state
1 parent 91e4b67 commit 3370bf5

9 files changed

Lines changed: 480 additions & 204 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
import ConsentBanner from '@components/Consent/Banner/index.astro'
3+
---
4+
5+
<ConsentBanner />
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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 BannerFixture from '@components/Consent/Banner/client/__tests__/banner.fixture.astro'
6+
import type { ConsentBannerElement } from '@components/Consent/Banner/client'
7+
import type { WebComponentModule } from '@components/scripts/@types/webComponentModule'
8+
import {
9+
executeRender,
10+
loadWebComponentModule,
11+
withJsdomEnvironment,
12+
} from '@test/unit/helpers/litRuntime'
13+
14+
vi.mock('@components/scripts/store', () => {
15+
const showConsentBanner = vi.fn()
16+
const hideConsentBanner = vi.fn()
17+
const initConsentCookies = vi.fn(() => true)
18+
const allowAllConsentCookies = vi.fn()
19+
20+
return {
21+
showConsentBanner,
22+
hideConsentBanner,
23+
initConsentCookies,
24+
allowAllConsentCookies,
25+
}
26+
})
27+
28+
vi.mock('@components/Consent/Preferences/client', () => ({
29+
showConsentCustomizeModal: vi.fn(),
30+
}))
31+
32+
import * as consentStore from '@components/scripts/store'
33+
34+
const showConsentBannerMock = vi.mocked(consentStore.showConsentBanner)
35+
const hideConsentBannerMock = vi.mocked(consentStore.hideConsentBanner)
36+
const initConsentCookiesMock = vi.mocked(consentStore.initConsentCookies)
37+
const allowAllConsentCookiesMock = vi.mocked(consentStore.allowAllConsentCookies)
38+
39+
type ConsentBannerModule = WebComponentModule<ConsentBannerElement>
40+
41+
const CONSENT_READY_TIMEOUT_MS = 2_000
42+
const BANNER_READY_EVENT = 'consent-banner:ready'
43+
44+
const waitForBannerReady = async (element: ConsentBannerElement) => {
45+
if (element.isInitialized) {
46+
return
47+
}
48+
49+
await new Promise<void>((resolve, reject) => {
50+
const timeoutId = setTimeout(() => {
51+
element.removeEventListener(BANNER_READY_EVENT, onReady)
52+
reject(new Error('Consent banner never finished initializing'))
53+
}, CONSENT_READY_TIMEOUT_MS)
54+
55+
function onReady() {
56+
clearTimeout(timeoutId)
57+
resolve()
58+
}
59+
60+
element.addEventListener(BANNER_READY_EVENT, onReady, { once: true })
61+
})
62+
}
63+
64+
type JsdomWindow = Window & typeof globalThis
65+
66+
const renderConsentBanner = async (
67+
assertion: (_context: { element: ConsentBannerElement; window: JsdomWindow }) => Promise<void> | void,
68+
) => {
69+
const container = await AstroContainer.create()
70+
71+
await executeRender<ConsentBannerModule>({
72+
container,
73+
component: BannerFixture,
74+
moduleSpecifier: '@components/Consent/Banner/client/index',
75+
selector: 'consent-banner',
76+
waitForReady: waitForBannerReady,
77+
assert: async ({ element, window }) => {
78+
if (!window) {
79+
throw new Error('JSDOM window is not available for consent banner tests')
80+
}
81+
82+
await assertion({ element, window: window as JsdomWindow })
83+
},
84+
})
85+
}
86+
87+
beforeEach(async () => {
88+
initConsentCookiesMock.mockReturnValue(true)
89+
showConsentBannerMock.mockClear()
90+
hideConsentBannerMock.mockClear()
91+
initConsentCookiesMock.mockClear()
92+
allowAllConsentCookiesMock.mockClear()
93+
94+
await withJsdomEnvironment(async ({ window }) => {
95+
window.sessionStorage.clear()
96+
window.localStorage.clear()
97+
98+
const module = await loadWebComponentModule<ConsentBannerModule>(
99+
'@components/Consent/Banner/client/index',
100+
)
101+
102+
const bannerCtor = module.componentCtor as typeof ConsentBannerElement
103+
// Reset the static visibility flag so each test starts from a clean slate
104+
;(bannerCtor as unknown as { isModalCurrentlyVisible: boolean }).isModalCurrentlyVisible = false
105+
})
106+
})
107+
108+
describe('ConsentBannerElement', () => {
109+
it('shows the modal when consent cookies are uninitialized', async () => {
110+
initConsentCookiesMock.mockReturnValue(true)
111+
112+
await renderConsentBanner(({ window }) => {
113+
const wrapper = window.document.getElementById('consent-modal-id') as HTMLDivElement | null
114+
expect(wrapper).not.toBeNull()
115+
expect(wrapper!.style.display).toBe('block')
116+
expect(showConsentBannerMock).toHaveBeenCalled()
117+
expect(window.sessionStorage.getItem('consent-modal-visible')).toBe('true')
118+
expect(window.sessionStorage.getItem('consent-modal-shown')).toBe('true')
119+
})
120+
})
121+
122+
it('skips rendering the modal when consent cookies already exist', async () => {
123+
initConsentCookiesMock.mockReturnValue(false)
124+
125+
await renderConsentBanner(({ window }) => {
126+
const wrapper = window.document.getElementById('consent-modal-id') as HTMLDivElement | null
127+
expect(wrapper).not.toBeNull()
128+
expect(wrapper!.style.display).toBe('none')
129+
expect(showConsentBannerMock).not.toHaveBeenCalled()
130+
})
131+
})
132+
133+
it('hides the banner when the close button is clicked', async () => {
134+
await renderConsentBanner(({ window }) => {
135+
const closeBtn = window.document.querySelector('.consent-modal__close-btn') as HTMLButtonElement | null
136+
expect(closeBtn).not.toBeNull()
137+
138+
closeBtn!.dispatchEvent(new window.MouseEvent('click', { bubbles: true }))
139+
140+
expect(hideConsentBannerMock).toHaveBeenCalled()
141+
const wrapper = window.document.getElementById('consent-modal-id') as HTMLDivElement | null
142+
expect(wrapper!.style.display).toBe('none')
143+
})
144+
})
145+
146+
it('grants all consent when Allow All is triggered', async () => {
147+
await renderConsentBanner(({ window }) => {
148+
const allowBtn = window.document.querySelector('.consent-modal__btn-allow') as HTMLButtonElement | null
149+
expect(allowBtn).not.toBeNull()
150+
151+
allowBtn!.dispatchEvent(new window.MouseEvent('click', { bubbles: true }))
152+
153+
expect(allowAllConsentCookiesMock).toHaveBeenCalled()
154+
expect(hideConsentBannerMock).toHaveBeenCalled()
155+
})
156+
})
157+
})
Lines changed: 82 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
// @vitest-environment node
2-
import { beforeEach, afterEach, describe, expect, it } from 'vitest'
3-
import { Window } from 'happy-dom'
2+
3+
import { beforeEach, describe, expect, it } from 'vitest'
44
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
5-
import ConsentBanner from '@components/Consent/Banner/index.astro'
5+
import BannerFixture from '@components/Consent/Banner/client/__tests__/banner.fixture.astro'
6+
import type { ConsentBannerElement } from '@components/Consent/Banner/client'
7+
import type { WebComponentModule } from '@components/scripts/@types/webComponentModule'
8+
import {
9+
executeRender,
10+
withJsdomEnvironment,
11+
} from '@test/unit/helpers/litRuntime'
612
import {
713
getConsentWrapper,
814
getConsentCloseBtn,
@@ -11,69 +17,99 @@ import {
1117
} from '@components/Consent/Banner/client/selectors'
1218
import { ClientScriptError } from '@components/scripts/errors'
1319

14-
const attachDom = (html: string): Window => {
15-
const windowInstance = new Window()
16-
windowInstance.document.body.innerHTML = html
20+
type ConsentBannerModule = WebComponentModule<ConsentBannerElement>
1721

18-
globalThis.window = windowInstance as unknown as typeof globalThis.window
19-
globalThis.document = windowInstance.document as unknown as Document
20-
globalThis.Node = windowInstance.Node as unknown as typeof globalThis.Node
22+
const CONSENT_READY_TIMEOUT_MS = 2_000
23+
const BANNER_READY_EVENT = 'consent-banner:ready'
2124

22-
return windowInstance
23-
}
25+
const waitForBannerReady = async (element: ConsentBannerElement) => {
26+
if (element.isInitialized) {
27+
return
28+
}
2429

25-
describe('Consent Banner Selectors', () => {
26-
let container: AstroContainer
27-
let windowInstance: Window
30+
await new Promise<void>((resolve, reject) => {
31+
const timeoutId = setTimeout(() => {
32+
element.removeEventListener(BANNER_READY_EVENT, onReady)
33+
reject(new Error('Consent banner never finished initializing'))
34+
}, CONSENT_READY_TIMEOUT_MS)
2835

29-
beforeEach(async () => {
30-
container = await AstroContainer.create()
31-
const markup = await container.renderToString(ConsentBanner)
32-
windowInstance = attachDom(markup)
36+
function onReady() {
37+
clearTimeout(timeoutId)
38+
resolve()
39+
}
40+
41+
element.addEventListener(BANNER_READY_EVENT, onReady, { once: true })
3342
})
43+
}
44+
45+
const renderConsentBanner = async (
46+
assertion: () => Promise<void> | void,
47+
) => {
48+
const container = await AstroContainer.create()
49+
50+
await executeRender<ConsentBannerModule>({
51+
container,
52+
component: BannerFixture,
53+
moduleSpecifier: '@components/Consent/Banner/client/index',
54+
selector: 'consent-banner',
55+
waitForReady: waitForBannerReady,
56+
assert: async () => assertion(),
57+
})
58+
}
3459

35-
afterEach(() => {
36-
windowInstance.happyDOM?.cancelAsync?.()
37-
delete (globalThis as { document?: Document }).document
38-
delete (globalThis as { window?: typeof globalThis.window }).window
39-
delete (globalThis as { Node?: typeof globalThis.Node }).Node
60+
describe('Consent Banner Selectors', () => {
61+
beforeEach(async () => {
62+
await withJsdomEnvironment(({ window }) => {
63+
window.sessionStorage.clear()
64+
window.localStorage.clear()
65+
})
4066
})
4167

42-
it('returns consent modal wrapper with expected attributes', () => {
43-
const wrapper = getConsentWrapper()
68+
it('returns consent modal wrapper with expected attributes', async () => {
69+
await renderConsentBanner(() => {
70+
const wrapper = getConsentWrapper()
4471

45-
expect(wrapper.id).toBe('consent-modal-id')
46-
expect(wrapper.getAttribute('role')).toBe('dialog')
47-
expect(wrapper.getAttribute('aria-label')).toBe('Cookie consent dialog')
72+
expect(wrapper.id).toBe('consent-modal-id')
73+
expect(wrapper.getAttribute('role')).toBe('dialog')
74+
expect(wrapper.getAttribute('aria-label')).toBe('Cookie consent dialog')
75+
})
4876
})
4977

50-
it('locates the close button', () => {
51-
const closeBtn = getConsentCloseBtn()
78+
it('locates the close button', async () => {
79+
await renderConsentBanner(() => {
80+
const closeBtn = getConsentCloseBtn()
5281

53-
expect(closeBtn).toBeTruthy()
54-
expect(closeBtn.classList.contains('consent-modal__close-btn')).toBe(true)
55-
expect(closeBtn.getAttribute('aria-label')).toMatch(/close cookie consent dialog/i)
82+
expect(closeBtn).toBeTruthy()
83+
expect(closeBtn.classList.contains('consent-modal__close-btn')).toBe(true)
84+
expect(closeBtn.getAttribute('aria-label')).toMatch(/close cookie consent dialog/i)
85+
})
5686
})
5787

58-
it('locates the allow-all button', () => {
59-
const allowBtn = getConsentAllowBtn()
88+
it('locates the allow-all button', async () => {
89+
await renderConsentBanner(() => {
90+
const allowBtn = getConsentAllowBtn()
6091

61-
expect(allowBtn).toBeTruthy()
62-
expect(allowBtn.classList.contains('consent-modal__btn-allow')).toBe(true)
63-
expect(allowBtn.textContent?.trim()).toBe('Allow All')
92+
expect(allowBtn).toBeTruthy()
93+
expect(allowBtn.classList.contains('consent-modal__btn-allow')).toBe(true)
94+
expect(allowBtn.textContent?.trim()).toBe('Allow All')
95+
})
6496
})
6597

66-
it('locates the customize button', () => {
67-
const customizeBtn = getConsentCustomizeBtn()
98+
it('locates the customize button', async () => {
99+
await renderConsentBanner(() => {
100+
const customizeBtn = getConsentCustomizeBtn()
68101

69-
expect(customizeBtn).toBeTruthy()
70-
expect(customizeBtn.classList.contains('consent-modal__btn-customize')).toBe(true)
71-
expect(customizeBtn.textContent?.trim()).toBe('Customize')
102+
expect(customizeBtn).toBeTruthy()
103+
expect(customizeBtn.classList.contains('consent-modal__btn-customize')).toBe(true)
104+
expect(customizeBtn.textContent?.trim()).toBe('Customize')
105+
})
72106
})
73107

74-
it('throws a ClientScriptError when wrapper is missing', () => {
75-
document.getElementById('consent-modal-id')?.remove()
108+
it('throws a ClientScriptError when wrapper is missing', async () => {
109+
await renderConsentBanner(() => {
110+
document.getElementById('consent-modal-id')?.remove()
76111

77-
expect(() => getConsentWrapper()).toThrowError(ClientScriptError)
112+
expect(() => getConsentWrapper()).toThrowError(ClientScriptError)
113+
})
78114
})
79115
})

0 commit comments

Comments
 (0)