Skip to content

Commit d023b3d

Browse files
committed
Update unit test for consent preferences component, add data selector and expand anchor target, add selectors unit test for consent banner and preferences components, fix e2e tests for the preferences component
1 parent 1c84be3 commit d023b3d

9 files changed

Lines changed: 344 additions & 15 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// @vitest-environment node
2+
import { beforeEach, afterEach, describe, expect, it } from 'vitest'
3+
import { Window } from 'happy-dom'
4+
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
5+
import ConsentBanner from '@components/Consent/Banner/index.astro'
6+
import {
7+
getConsentWrapper,
8+
getConsentCloseBtn,
9+
getConsentAllowBtn,
10+
getConsentCustomizeBtn,
11+
} from '@components/Consent/Banner/client/selectors'
12+
import { ClientScriptError } from '@components/scripts/errors'
13+
14+
const attachDom = (html: string): Window => {
15+
const windowInstance = new Window()
16+
windowInstance.document.body.innerHTML = html
17+
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
21+
22+
return windowInstance
23+
}
24+
25+
describe('Consent Banner Selectors', () => {
26+
let container: AstroContainer
27+
let windowInstance: Window
28+
29+
beforeEach(async () => {
30+
container = await AstroContainer.create()
31+
const markup = await container.renderToString(ConsentBanner)
32+
windowInstance = attachDom(markup)
33+
})
34+
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
40+
})
41+
42+
it('returns consent modal wrapper with expected attributes', () => {
43+
const wrapper = getConsentWrapper()
44+
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')
48+
})
49+
50+
it('locates the close button', () => {
51+
const closeBtn = getConsentCloseBtn()
52+
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)
56+
})
57+
58+
it('locates the allow-all button', () => {
59+
const allowBtn = getConsentAllowBtn()
60+
61+
expect(allowBtn).toBeTruthy()
62+
expect(allowBtn.classList.contains('consent-modal__btn-allow')).toBe(true)
63+
expect(allowBtn.textContent?.trim()).toBe('Allow All')
64+
})
65+
66+
it('locates the customize button', () => {
67+
const customizeBtn = getConsentCustomizeBtn()
68+
69+
expect(customizeBtn).toBeTruthy()
70+
expect(customizeBtn.classList.contains('consent-modal__btn-customize')).toBe(true)
71+
expect(customizeBtn.textContent?.trim()).toBe('Customize')
72+
})
73+
74+
it('throws a ClientScriptError when wrapper is missing', () => {
75+
document.getElementById('consent-modal-id')?.remove()
76+
77+
expect(() => getConsentWrapper()).toThrowError(ClientScriptError)
78+
})
79+
})

src/components/Consent/Preferences/__tests__/client.spec.ts renamed to src/components/Consent/Preferences/client/__tests__/index.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ describe('ConsentPreferencesElement', () => {
113113
mockModal = mockDiv('consent-modal-modal-id')
114114
mockCloseBtn = mockButton('consent-modal__close-btn')
115115
mockCloseBtn.classList.add('consent-modal__close-btn')
116+
mockCloseBtn.dataset['testid'] = 'consent-preferences-close'
116117
mockAllowBtn = mockButton('consent-allow-all')
117118
mockSaveBtn = mockButton('consent-save-preferences')
118119

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// @vitest-environment node
2+
import { beforeEach, afterEach, describe, expect, it } from 'vitest'
3+
import { Window } from 'happy-dom'
4+
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
5+
import ConsentPreferences from '@components/Consent/Preferences/index.astro'
6+
import {
7+
getConsentCustomizeModal,
8+
getConsentCustomizeCloseBtn,
9+
getAllowAllBtn,
10+
getSavePreferencesBtn,
11+
} from '@components/Consent/Preferences/client/selectors'
12+
import { ClientScriptError } from '@components/scripts/errors'
13+
14+
const attachDom = (html: string): Window => {
15+
const windowInstance = new Window()
16+
windowInstance.document.body.innerHTML = html
17+
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
21+
22+
return windowInstance
23+
}
24+
25+
describe('Consent Preferences Selectors', () => {
26+
let container: AstroContainer
27+
let windowInstance: Window
28+
29+
beforeEach(async () => {
30+
container = await AstroContainer.create()
31+
const markup = await container.renderToString(ConsentPreferences)
32+
windowInstance = attachDom(markup)
33+
})
34+
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
40+
})
41+
42+
it('returns the consent customize modal wrapper', () => {
43+
const modal = getConsentCustomizeModal()
44+
45+
expect(modal.id).toBe('consent-modal-modal-id')
46+
expect(modal.getAttribute('role')).toBe('dialog')
47+
expect(modal.getAttribute('aria-label')).toBe('customize consent dialog')
48+
})
49+
50+
it('returns the close button with expected attributes', () => {
51+
const closeBtn = getConsentCustomizeCloseBtn()
52+
53+
expect(closeBtn.classList.contains('consent-modal__close-btn')).toBe(true)
54+
expect(closeBtn.dataset['testid']).toBe('consent-preferences-close')
55+
expect(closeBtn.getAttribute('aria-label')).toMatch(/privacy preferences dialog/i)
56+
})
57+
58+
it('returns the allow-all button', () => {
59+
const allowBtn = getAllowAllBtn()
60+
61+
expect(allowBtn.id).toBe('consent-allow-all')
62+
expect(allowBtn.textContent?.trim()).toBe('Allow All')
63+
})
64+
65+
it('returns the save preferences button', () => {
66+
const saveBtn = getSavePreferencesBtn()
67+
68+
expect(saveBtn.id).toBe('consent-save-preferences')
69+
expect(saveBtn.textContent?.trim()).toBe('Save My Preferences')
70+
})
71+
72+
it('throws ClientScriptError when the modal is missing', () => {
73+
document.getElementById('consent-modal-modal-id')?.remove()
74+
75+
expect(() => getConsentCustomizeModal()).toThrowError(ClientScriptError)
76+
})
77+
})

src/components/Consent/Preferences/client.ts renamed to src/components/Consent/Preferences/client/index.ts

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,19 @@
66

77
import { LitElement } from 'lit'
88
import { isInputElement } from '@components/scripts/assertions/elements'
9-
import {
10-
getConsentCustomizeModal,
11-
getConsentCustomizeCloseBtn,
12-
getAllowAllBtn,
13-
getSavePreferencesBtn,
14-
} from '@components/Consent/Preferences/selectors'
159
import { addButtonEventListeners } from '@components/scripts/elementListeners'
1610
import {
1711
updateConsent,
1812
allowAllConsent,
1913
createConsentController,
2014
type ConsentState
2115
} from '@components/scripts/store'
16+
import {
17+
getConsentCustomizeModal,
18+
getConsentCustomizeCloseBtn,
19+
getAllowAllBtn,
20+
getSavePreferencesBtn,
21+
} from '@components/Consent/Preferences/client/selectors'
2222

2323
/**
2424
* Consent Preferences web component
@@ -31,6 +31,7 @@ export class ConsentPreferencesElement extends LitElement {
3131
private closeBtn: HTMLButtonElement | null = null
3232
private allowAllBtn: HTMLButtonElement | null = null
3333
private saveBtn: HTMLButtonElement | null = null
34+
private toggleLabels: HTMLLabelElement[] = []
3435

3536
override createRenderRoot() {
3637
return this
@@ -41,6 +42,11 @@ export class ConsentPreferencesElement extends LitElement {
4142
this.initialize()
4243
}
4344

45+
override disconnectedCallback(): void {
46+
this.cleanupToggleLabelListeners()
47+
super.disconnectedCallback()
48+
}
49+
4450
private initialize(): void {
4551
this.findElements()
4652
this.bindEvents()
@@ -98,6 +104,41 @@ export class ConsentPreferencesElement extends LitElement {
98104
if (this.saveBtn) {
99105
addButtonEventListeners(this.saveBtn, () => this.savePreferences())
100106
}
107+
108+
this.bindToggleLabelListeners()
109+
}
110+
111+
private bindToggleLabelListeners(): void {
112+
this.cleanupToggleLabelListeners()
113+
this.toggleLabels = Array.from(
114+
this.querySelectorAll<HTMLLabelElement>('[data-consent-toggle]'),
115+
)
116+
117+
this.toggleLabels.forEach((label) => {
118+
label.addEventListener('click', this.handleToggleLabelClick)
119+
})
120+
}
121+
122+
private cleanupToggleLabelListeners(): void {
123+
this.toggleLabels.forEach((label) => {
124+
label.removeEventListener('click', this.handleToggleLabelClick)
125+
})
126+
this.toggleLabels = []
127+
}
128+
129+
private handleToggleLabelClick = (event: Event): void => {
130+
event.preventDefault()
131+
const label = event.currentTarget as HTMLLabelElement | null
132+
const checkboxId = label?.getAttribute('data-consent-toggle')
133+
134+
if (!checkboxId) {
135+
return
136+
}
137+
138+
const checkbox = document.getElementById(checkboxId)
139+
if (isInputElement(checkbox)) {
140+
checkbox.checked = !checkbox.checked
141+
}
101142
}
102143

103144
private loadPreferences(): ConsentState | null {

src/components/Consent/Preferences/selectors.ts renamed to src/components/Consent/Preferences/client/selectors.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export const SELECTORS = {
88
/** Consent customize modal wrapper */
99
modal: 'consent-modal-modal-id',
1010
/** Close button for modal */
11-
closeBtn: '.consent-modal__close-btn',
11+
closeBtn: '[data-testid="consent-preferences-close"]',
1212
/** Allow all consent button */
1313
allowAllBtn: 'consent-allow-all',
1414
/** Save preferences button */
@@ -35,7 +35,7 @@ export const getConsentCustomizeCloseBtn = (): HTMLButtonElement => {
3535
const closeBtn = document.querySelector(SELECTORS.closeBtn)
3636
if (!isButtonElement(closeBtn)) {
3737
throw new ClientScriptError(
38-
`Consent customize close button with class '${SELECTORS.closeBtn}' not found`
38+
`Consent customize close button with selector '${SELECTORS.closeBtn}' not found`
3939
)
4040
}
4141
return closeBtn

‎src/components/Consent/Preferences/index.astro‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import logoSvg from '@assets/images/site/logo.svg'
2727
<button
2828
type="button"
2929
class="consent-modal__close-btn bg-transparent border-0 text-primary outline-none hover:text-success-offset focus:text-success-offset transition-colors"
30+
data-testid="consent-preferences-close"
3031
aria-label="Close privacy preferences dialog"
3132
>
3233
<Sprite name="close" />
@@ -87,7 +88,7 @@ import logoSvg from '@assets/images/site/logo.svg'
8788
Helps us understand how visitors use our website
8889
</p>
8990
</div>
90-
<label class="relative inline-flex items-center cursor-pointer">
91+
<label class="relative inline-flex items-center cursor-pointer" data-consent-toggle="analytics-cookies">
9192
<span class="sr-only">Toggle Performance and Analytics</span>
9293
<input type="checkbox" id="analytics-cookies" class="sr-only peer" checked />
9394
<div
@@ -116,7 +117,7 @@ import logoSvg from '@assets/images/site/logo.svg'
116117
Remembers your social media preferences
117118
</p>
118119
</div>
119-
<label class="relative inline-flex items-center cursor-pointer">
120+
<label class="relative inline-flex items-center cursor-pointer" data-consent-toggle="functional-cookies">
120121
<span class="sr-only">Toggle Enhanced Features</span>
121122
<input type="checkbox" id="functional-cookies" class="sr-only peer" />
122123
<div
@@ -151,7 +152,7 @@ import logoSvg from '@assets/images/site/logo.svg'
151152
Used to deliver relevant advertisements
152153
</p>
153154
</div>
154-
<label class="relative inline-flex items-center cursor-pointer">
155+
<label class="relative inline-flex items-center cursor-pointer" data-consent-toggle="marketing-cookies">
155156
<span class="sr-only">Toggle Marketing and Advertising</span>
156157
<input type="checkbox" id="marketing-cookies" class="sr-only peer" />
157158
<div
@@ -177,13 +178,13 @@ import logoSvg from '@assets/images/site/logo.svg'
177178
class="sticky bottom-0 bg-bg border-t border-bg-offset px-6 py-4 flex gap-3 justify-end"
178179
>
179180
<button
180-
id="cookie-save-preferences"
181+
id="consent-save-preferences"
181182
class="px-6 py-2 bg-text-muted text-white rounded-lg hover:bg-text transition-colors duration-200 font-medium"
182183
>
183184
Save My Preferences
184185
</button>
185186
<button
186-
id="cookie-allow-all"
187+
id="consent-allow-all"
187188
class="px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover transition-colors duration-200 font-medium"
188189
>
189190
Allow All

‎src/pages/consent/index.astro‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ const path = '/consent/'
2222
<h2 class="text-2xl font-bold text-[var(--color-text)]">Your Cookie Preferences</h2>
2323
<div class="flex gap-3">
2424
<button
25-
id="cookie-allow-all"
25+
id="consent-allow-all"
2626
class="px-4 py-2 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-hover)] transition-colors duration-200 font-medium"
2727
>
2828
Allow All
2929
</button>
3030
<button
31-
id="cookie-save-preferences"
31+
id="consent-save-preferences"
3232
class="px-4 py-2 bg-[var(--color-text-muted)] text-white rounded-lg hover:bg-[var(--color-text)] transition-colors duration-200 font-medium"
3333
>
3434
Save Preferences
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
import BaseLayout from '@layouts/BaseLayout.astro'
3+
import ConsentPreferences from '@components/Consent/Preferences/index.astro'
4+
5+
const pageTitle = 'Consent Preferences Testing Ground'
6+
const pageDescription = 'Isolated render of the consent preferences modal for QA and automation coverage.'
7+
const pagePath = '/testing/consent-preferences'
8+
---
9+
10+
<BaseLayout pageTitle={pageTitle} path={pagePath} description={pageDescription} noindex>
11+
<section class="mx-auto mb-10 max-w-3xl space-y-4 text-lg text-offset">
12+
<p>
13+
This route renders the production consent preferences modal in isolation so automated tests
14+
can interact with its web component without relying on marketing content or feature flags.
15+
</p>
16+
<p>
17+
The markup below is identical to what ships with the consent banner customize experience,
18+
ensuring we exercise the same selectors, accessibility attributes, and button wiring users
19+
rely on when opting into different storage categories.
20+
</p>
21+
</section>
22+
23+
<ConsentPreferences />
24+
</BaseLayout>

0 commit comments

Comments
 (0)