Skip to content

Commit 7908ae3

Browse files
committed
Refactor Consent/Preferences to remove modal behavior, update unit and e2e tests
1 parent a0a0acc commit 7908ae3

11 files changed

Lines changed: 319 additions & 329 deletions

File tree

src/components/Consent/Banner/client/__tests__/index.spec.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@ vi.mock('@components/scripts/store', () => {
2525
}
2626
})
2727

28-
vi.mock('@components/Consent/Preferences/client', () => ({
29-
showConsentCustomizeModal: vi.fn(),
30-
}))
31-
3228
import * as consentStore from '@components/scripts/store'
3329

3430
const showConsentBannerMock = vi.mocked(consentStore.showConsentBanner)
@@ -154,4 +150,28 @@ describe('ConsentBannerElement', () => {
154150
expect(hideConsentBannerMock).toHaveBeenCalled()
155151
})
156152
})
153+
154+
it('navigates to the consent page when Customize is triggered', async () => {
155+
await renderConsentBanner(({ element, window }) => {
156+
const customizeBtn = window.document.querySelector('.consent-modal__btn-customize') as HTMLButtonElement | null
157+
expect(customizeBtn).not.toBeNull()
158+
159+
const bannerCtor = element.constructor as typeof HTMLElement & {
160+
navigateToUrl: (url: string) => void
161+
}
162+
const navigateSpy = vi.spyOn(bannerCtor, 'navigateToUrl').mockImplementation(() => {})
163+
164+
try {
165+
customizeBtn!.dispatchEvent(new window.MouseEvent('click', { bubbles: true }))
166+
167+
expect(navigateSpy).toHaveBeenCalledTimes(1)
168+
expect(navigateSpy).toHaveBeenCalledWith(expect.stringContaining('/consent'))
169+
170+
const wrapper = window.document.getElementById('consent-modal-id') as HTMLDivElement | null
171+
expect(wrapper?.style.display).toBe('none')
172+
} finally {
173+
navigateSpy.mockRestore()
174+
}
175+
})
176+
})
157177
})

src/components/Consent/Banner/client/index.ts

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import {
1515
initConsentCookies,
1616
showConsentBanner,
1717
} from '@components/scripts/store'
18-
import { showConsentCustomizeModal } from '@components/Consent/Preferences/client'
1918
import {
2019
getConsentAllowBtn,
2120
getConsentCloseBtn,
@@ -41,6 +40,13 @@ export class ConsentBannerElement extends HTMLElement {
4140
private afterSwapHandler: (() => void) | null = null
4241
public isInitialized = false
4342
private static isModalCurrentlyVisible = false
43+
public static navigateToUrl(url: string): void {
44+
if (typeof window === 'undefined') {
45+
return
46+
}
47+
48+
window.location.assign(url)
49+
}
4450

4551
connectedCallback(): void {
4652
if (this.isInitialized || typeof document === 'undefined') {
@@ -207,12 +213,23 @@ export class ConsentBannerElement extends HTMLElement {
207213
addScriptBreadcrumb(context)
208214

209215
try {
210-
showConsentCustomizeModal()
216+
this.navigateToConsentPage()
211217
} catch (error) {
212218
handleScriptError(error, context)
213219
}
214220
}
215221

222+
private navigateToConsentPage(): void {
223+
if (typeof window === 'undefined') {
224+
return
225+
}
226+
227+
this.handleDismissModal()
228+
229+
const targetUrl = new URL('/consent', window.location.origin)
230+
ConsentBannerElement.navigateToUrl(targetUrl.toString())
231+
}
232+
216233
private bindEvents(): void {
217234
const context = { scriptName: COMPONENT_SCRIPT_NAME, operation: 'bindEvents' }
218235
addScriptBreadcrumb(context)
@@ -236,6 +253,13 @@ export class ConsentBannerElement extends HTMLElement {
236253
const shouldBeVisible = wasVisible ||
237254
ConsentBannerElement.isModalCurrentlyVisible ||
238255
sessionStorage.getItem('consent-modal-visible') === 'true'
256+
const cookiesInitialized = initConsentCookies()
257+
258+
if (this.isConsentRoute()) {
259+
console.log('🍪 Suppressing consent modal on consent settings page')
260+
this.suppressModalForConsentPage()
261+
return
262+
}
239263

240264
if (shouldBeVisible) {
241265
console.log('🍪 Restoring modal from previous navigation')
@@ -246,7 +270,7 @@ export class ConsentBannerElement extends HTMLElement {
246270
return
247271
}
248272

249-
if (!initConsentCookies()) {
273+
if (!cookiesInitialized) {
250274
console.log('🍪 User already consented, skipping modal')
251275
return
252276
}
@@ -263,6 +287,24 @@ export class ConsentBannerElement extends HTMLElement {
263287
}
264288
}
265289

290+
private isConsentRoute(): boolean {
291+
if (typeof window === 'undefined') {
292+
return false
293+
}
294+
295+
const pathname = window.location.pathname
296+
return pathname === '/consent' || pathname.startsWith('/consent/')
297+
}
298+
299+
private suppressModalForConsentPage(): void {
300+
this.removeFocusTrap()
301+
this.wrapper.style.display = 'none'
302+
hideConsentBanner()
303+
ConsentBannerElement.isModalCurrentlyVisible = false
304+
sessionStorage.removeItem('consent-modal-visible')
305+
sessionStorage.removeItem('consent-modal-shown')
306+
}
307+
266308
private setupFocusTrap(): void {
267309
const context = { scriptName: COMPONENT_SCRIPT_NAME, operation: 'setupFocusTrap' }
268310
addScriptBreadcrumb(context)

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

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { WebComponentModule } from '@components/scripts/@types/webComponent
66
import ConsentPreferencesComponent from '@components/Consent/Preferences/index.astro'
77
import type { ConsentPreferencesElement } from '@components/Consent/Preferences/client'
88
import type { ConsentCategory, ConsentState } from '@components/scripts/store'
9-
import { allowAllConsent, updateConsent } from '@components/scripts/store'
9+
import { allowAllConsent, updateConsent, revokeAllConsent } from '@components/scripts/store'
1010
import { executeRender, withJsdomEnvironment } from '@test/unit/helpers/litRuntime'
1111

1212
type ConsentPreferencesModule = WebComponentModule<ConsentPreferencesElement>
@@ -104,6 +104,12 @@ vi.mock('@components/scripts/store', () => {
104104
state.marketing = true
105105
notify()
106106
}),
107+
revokeAllConsent: vi.fn(() => {
108+
state.analytics = false
109+
state.functional = false
110+
state.marketing = false
111+
notify()
112+
}),
107113
}
108114
})
109115

@@ -118,30 +124,6 @@ describe('ConsentPreferencesElement', () => {
118124
})
119125
})
120126

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()
125-
126-
element.showModal()
127-
expect(wrapper!.style.display).toBe('flex')
128-
})
129-
})
130-
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')
142-
})
143-
})
144-
145127
it('grants all consent when Allow All is clicked', async () => {
146128
await renderConsentPreferences(({ window }) => {
147129
const allowBtn = window.document.getElementById('consent-allow-all') as HTMLButtonElement | null
@@ -156,6 +138,20 @@ describe('ConsentPreferencesElement', () => {
156138
})
157139
})
158140

141+
it('revokes all consent when Decline All is clicked', async () => {
142+
await renderConsentPreferences(({ window }) => {
143+
const denyBtn = window.document.getElementById('consent-deny-all') as HTMLButtonElement | null
144+
expect(denyBtn).not.toBeNull()
145+
146+
denyBtn!.dispatchEvent(new window.MouseEvent('click', { bubbles: true }))
147+
148+
expect(revokeAllConsent).toHaveBeenCalled()
149+
expect((window.document.getElementById('analytics-cookies') as HTMLInputElement).checked).toBe(false)
150+
expect((window.document.getElementById('functional-cookies') as HTMLInputElement).checked).toBe(false)
151+
expect((window.document.getElementById('marketing-cookies') as HTMLInputElement).checked).toBe(false)
152+
})
153+
})
154+
159155
it('saves consent preferences when Save button is clicked', async () => {
160156
await renderConsentPreferences(({ window }) => {
161157
const analyticsCheckbox = window.document.getElementById('analytics-cookies') as HTMLInputElement | null

src/components/Consent/Preferences/client/__tests__/selectors.spec.ts

Lines changed: 6 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,7 @@ import { experimental_AstroContainer as AstroContainer } from 'astro/container'
55
import type { WebComponentModule } from '@components/scripts/@types/webComponentModule'
66
import ConsentPreferencesComponent from '@components/Consent/Preferences/index.astro'
77
import type { ConsentPreferencesElement } from '@components/Consent/Preferences/client'
8-
import {
9-
getConsentCustomizeModal,
10-
getConsentCustomizeCloseBtn,
11-
getAllowAllBtn,
12-
getSavePreferencesBtn,
13-
} from '@components/Consent/Preferences/client/selectors'
14-
import { ClientScriptError } from '@components/scripts/errors'
8+
import { getAllowAllBtn, getSavePreferencesBtn, getDenyAllBtn } from '@components/Consent/Preferences/client/selectors'
159
import {
1610
executeRender,
1711
withJsdomEnvironment,
@@ -72,26 +66,6 @@ beforeEach(async () => {
7266
})
7367

7468
describe('Consent Preferences Selectors', () => {
75-
it('returns the consent customize modal wrapper', async () => {
76-
await renderConsentPreferences(() => {
77-
const modal = getConsentCustomizeModal()
78-
79-
expect(modal.id).toBe('consent-modal-modal-id')
80-
expect(modal.getAttribute('role')).toBe('dialog')
81-
expect(modal.getAttribute('aria-label')).toBe('customize consent dialog')
82-
})
83-
})
84-
85-
it('returns the close button with expected attributes', async () => {
86-
await renderConsentPreferences(() => {
87-
const closeBtn = getConsentCustomizeCloseBtn()
88-
89-
expect(closeBtn.classList.contains('consent-modal__close-btn')).toBe(true)
90-
expect(closeBtn.dataset['testid']).toBe('consent-preferences-close')
91-
expect(closeBtn.getAttribute('aria-label')).toMatch(/privacy preferences dialog/i)
92-
})
93-
})
94-
9569
it('returns the allow-all button', async () => {
9670
await renderConsentPreferences(() => {
9771
const allowBtn = getAllowAllBtn()
@@ -110,11 +84,12 @@ describe('Consent Preferences Selectors', () => {
11084
})
11185
})
11286

113-
it('throws ClientScriptError when the modal is missing', async () => {
114-
await renderConsentPreferences(({ window }) => {
115-
window.document.getElementById('consent-modal-modal-id')?.remove()
87+
it('returns the deny-all button', async () => {
88+
await renderConsentPreferences(() => {
89+
const denyBtn = getDenyAllBtn()
11690

117-
expect(() => getConsentCustomizeModal()).toThrowError(ClientScriptError)
91+
expect(denyBtn.id).toBe('consent-deny-all')
92+
expect(denyBtn.textContent?.trim()).toBe('Decline All')
11893
})
11994
})
12095
})

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

Lines changed: 18 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* Consent Preferences Management
3-
* Handles consent preference functionality and modal interactions
3+
* Handles consent preference functionality for the inline component
44
* Uses centralized state management from scripts/store
55
*/
66

@@ -12,14 +12,10 @@ import {
1212
subscribeToConsentState,
1313
updateConsent,
1414
allowAllConsent,
15+
revokeAllConsent,
1516
type ConsentState,
1617
} from '@components/scripts/store'
17-
import {
18-
getConsentCustomizeModal,
19-
getConsentCustomizeCloseBtn,
20-
getAllowAllBtn,
21-
getSavePreferencesBtn,
22-
} from '@components/Consent/Preferences/client/selectors'
18+
import { getAllowAllBtn, getSavePreferencesBtn, getDenyAllBtn } from '@components/Consent/Preferences/client/selectors'
2319
import { addScriptBreadcrumb } from '@components/scripts/errors'
2420
import { handleScriptError } from '@components/scripts/errors/handler'
2521
import { defineCustomElement } from '@components/scripts/utils'
@@ -29,9 +25,8 @@ const COMPONENT_SCRIPT_NAME = 'ConsentPreferencesElement'
2925
export const CONSENT_PREFERENCES_READY_EVENT = 'consent-preferences:ready'
3026

3127
export class ConsentPreferencesElement extends HTMLElement {
32-
private modal!: HTMLDivElement
33-
private closeBtn!: HTMLButtonElement
3428
private allowAllBtn!: HTMLButtonElement
29+
private denyAllBtn!: HTMLButtonElement
3530
private saveBtn!: HTMLButtonElement
3631
private domReadyHandler: (() => void) | null = null
3732
private beforePreparationHandler: (() => void) | null = null
@@ -92,7 +87,6 @@ export class ConsentPreferencesElement extends HTMLElement {
9287
addScriptBreadcrumb(context)
9388

9489
try {
95-
delete this.dataset['consentPreferencesReady']
9690
this.findElements()
9791
this.bindEvents()
9892
this.syncConsentState(getConsentSnapshot())
@@ -111,9 +105,8 @@ export class ConsentPreferencesElement extends HTMLElement {
111105
}
112106

113107
private findElements(): void {
114-
this.modal = getConsentCustomizeModal()
115-
this.closeBtn = getConsentCustomizeCloseBtn()
116108
this.allowAllBtn = getAllowAllBtn()
109+
this.denyAllBtn = getDenyAllBtn()
117110
this.saveBtn = getSavePreferencesBtn()
118111
}
119112

@@ -175,23 +168,9 @@ export class ConsentPreferencesElement extends HTMLElement {
175168
// Reserved for future cleanup work before View Transition swaps
176169
}
177170

178-
/** Show the consent customize modal */
179-
public showModal(): void {
180-
if (this.modal) {
181-
this.modal.style.display = 'flex'
182-
}
183-
}
184-
185-
/** Hide the consent customize modal */
186-
private hideModal = (): void => {
187-
if (this.modal) {
188-
this.modal.style.display = 'none'
189-
}
190-
}
191-
192171
private bindEvents(): void {
193-
addButtonEventListeners(this.closeBtn, this.hideModal)
194172
addButtonEventListeners(this.allowAllBtn, () => this.allowAll())
173+
addButtonEventListeners(this.denyAllBtn, () => this.denyAll())
195174
addButtonEventListeners(this.saveBtn, () => this.savePreferences())
196175
this.bindToggleLabelListeners()
197176
}
@@ -269,6 +248,14 @@ export class ConsentPreferencesElement extends HTMLElement {
269248
this.showNotification('All consent enabled!')
270249
}
271250

251+
private denyAll(): void {
252+
revokeAllConsent()
253+
const updatedConsent = getConsentSnapshot()
254+
this.syncConsentState(updatedConsent)
255+
this.applyPreferences(updatedConsent)
256+
this.showNotification('All optional consent disabled!')
257+
}
258+
272259
private getCurrentPreferences(): Partial<ConsentState> {
273260
const analyticsCheckbox = document.getElementById('analytics-cookies')
274261
const functionalCheckbox = document.getElementById('functional-cookies')
@@ -355,13 +342,10 @@ export class ConsentPreferencesElement extends HTMLElement {
355342
}
356343
}
357344

358-
/**
359-
* Helper function to show the consent customize modal
360-
* Used by the ConsentBanner component
361-
*/
362-
export const showConsentCustomizeModal = (): void => {
363-
const modal = getConsentCustomizeModal()
364-
modal.style.display = 'flex'
345+
declare global {
346+
interface HTMLElementTagNameMap {
347+
'consent-preferences': ConsentPreferencesElement
348+
}
365349
}
366350

367351
export const registerConsentPreferencesWebComponent = (tagName: string = COMPONENT_TAG_NAME) => {

0 commit comments

Comments
 (0)