@@ -6,31 +6,72 @@ import { act } from 'react'
66import { createRoot , type Root } from 'react-dom/client'
77import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest'
88
9- const { mockUseConsentManager, mockSaveConsents } = vi . hoisted ( ( ) => ( {
10- mockUseConsentManager : vi . fn ( ) ,
11- mockSaveConsents : vi . fn ( ) ,
12- } ) )
9+ const { mockUseConsentManager, mockSaveConsents, mockToastError, mockRevert, lastProps } =
10+ vi . hoisted ( ( ) => ( {
11+ mockUseConsentManager : vi . fn ( ) ,
12+ mockSaveConsents : vi . fn ( ) ,
13+ mockToastError : vi . fn ( ) ,
14+ mockRevert : vi . fn ( ) ,
15+ lastProps : vi . fn ( ) ,
16+ } ) )
1317
14- vi . mock ( '@sim/emcn' , ( ) => ( { toast : { success : vi . fn ( ) , error : vi . fn ( ) } } ) )
18+ vi . mock ( '@sim/emcn' , ( ) => ( { toast : { success : vi . fn ( ) , error : mockToastError } } ) )
1519vi . mock ( '@c15t/nextjs/headless' , ( ) => ( { useConsentManager : mockUseConsentManager } ) )
1620vi . mock ( '@/app/_shell/consent/consent-store-provider' , ( ) => ( {
1721 ConsentStoreProvider : ( { children } : { children : ReactNode } ) => children ,
1822} ) )
1923vi . mock ( '@/app/_shell/consent/consent-preferences' , ( ) => ( {
2024 CONSENT_LINK_CLASS : 'link' ,
21- ConsentPreferences : ( { onChange } : { onChange ?: ( ) => void } ) => (
22- < button type = 'button' data-testid = 'toggle' onClick = { onChange } />
23- ) ,
25+ ConsentPreferences : ( props : {
26+ onChange ?: ( change : { name : string ; revert : ( ) => void } ) => void
27+ disabled ?: boolean
28+ } ) => {
29+ lastProps ( props )
30+ return (
31+ < button
32+ type = 'button'
33+ data-testid = 'toggle'
34+ disabled = { props . disabled }
35+ onClick = { ( ) => props . onChange ?.( { name : 'measurement' , revert : mockRevert } ) }
36+ />
37+ )
38+ } ,
2439} ) )
2540
2641import { CookiePreferences } from '@/app/workspace/[workspaceId]/settings/components/general/components/cookie-preferences'
2742
2843let root : Root | null = null
2944
45+ /** The props the switch list was last rendered with. */
46+ function props ( ) {
47+ return lastProps . mock . calls . at ( - 1 ) ?. [ 0 ] as { disabled ?: boolean }
48+ }
49+
50+ function render ( ) {
51+ ; ( globalThis as { IS_REACT_ACT_ENVIRONMENT ?: boolean } ) . IS_REACT_ACT_ENVIRONMENT = true
52+ const container = document . createElement ( 'div' )
53+ document . body . appendChild ( container )
54+ root = createRoot ( container )
55+ act ( ( ) => root ?. render ( < CookiePreferences /> ) )
56+ return container
57+ }
58+
59+ /** Resolves the pending save on demand, so the in-flight state is observable. */
60+ function deferredSave ( ) {
61+ let resolve ! : ( ) => void
62+ let reject ! : ( error : Error ) => void
63+ mockSaveConsents . mockReturnValue (
64+ new Promise < void > ( ( res , rej ) => {
65+ resolve = res
66+ reject = rej
67+ } )
68+ )
69+ return { resolve, reject }
70+ }
71+
3072beforeEach ( ( ) => {
31- mockUseConsentManager . mockReturnValue ( {
32- saveConsents : mockSaveConsents . mockResolvedValue ( undefined ) ,
33- } )
73+ mockUseConsentManager . mockReturnValue ( { saveConsents : mockSaveConsents } )
74+ mockSaveConsents . mockResolvedValue ( undefined )
3475} )
3576
3677afterEach ( ( ) => {
@@ -40,19 +81,45 @@ afterEach(() => {
4081} )
4182
4283describe ( 'CookiePreferences' , ( ) => {
43- it ( 'commits on every toggle, matching the telemetry switch beside it' , ( ) => {
44- ; ( globalThis as { IS_REACT_ACT_ENVIRONMENT ?: boolean } ) . IS_REACT_ACT_ENVIRONMENT = true
45- const container = document . createElement ( 'div' )
46- document . body . appendChild ( container )
47- root = createRoot ( container )
48- act ( ( ) => root ?. render ( < CookiePreferences /> ) )
84+ it ( 'commits on every toggle, matching the telemetry switch beside it' , async ( ) => {
85+ const container = render ( )
4986
5087 expect ( mockSaveConsents ) . not . toHaveBeenCalled ( )
51- act ( ( ) => container . querySelector < HTMLButtonElement > ( '[data-testid="toggle"]' ) ?. click ( ) )
88+ await act ( async ( ) => {
89+ container . querySelector < HTMLButtonElement > ( '[data-testid="toggle"]' ) ?. click ( )
90+ } )
5291
5392 // `saveConsents('custom')` reads `selectedConsents` from the store at call
5493 // time and the switch's `setSelectedConsent` write is synchronous, so the
5594 // value this toggle staged is the one committed.
5695 expect ( mockSaveConsents ) . toHaveBeenCalledWith ( 'custom' , { uiSource : 'settings' } )
5796 } )
97+
98+ it ( 'locks the switches while a commit is in flight, so two toggles cannot race' , async ( ) => {
99+ const pending = deferredSave ( )
100+ const container = render ( )
101+
102+ act ( ( ) => {
103+ container . querySelector < HTMLButtonElement > ( '[data-testid="toggle"]' ) ?. click ( )
104+ } )
105+ expect ( props ( ) . disabled ) . toBe ( true )
106+
107+ await act ( async ( ) => {
108+ pending . resolve ( )
109+ } )
110+ expect ( props ( ) . disabled ) . toBe ( false )
111+ } )
112+
113+ it ( 'puts the switch back when the commit fails' , async ( ) => {
114+ mockSaveConsents . mockRejectedValue ( new Error ( 'network down' ) )
115+ const container = render ( )
116+
117+ await act ( async ( ) => {
118+ container . querySelector < HTMLButtonElement > ( '[data-testid="toggle"]' ) ?. click ( )
119+ } )
120+
121+ expect ( mockRevert ) . toHaveBeenCalledTimes ( 1 )
122+ expect ( mockToastError ) . toHaveBeenCalled ( )
123+ expect ( props ( ) . disabled ) . toBe ( false )
124+ } )
58125} )
0 commit comments