@@ -51,6 +51,18 @@ vi.mock('../../packages/script/src/runtime/composables/useScriptEventPage', () =
5151 useScriptEventPage : vi . fn ( ) ,
5252} ) )
5353
54+ /** `gtag()` queues the Arguments object; `dataLayer.push([...])` uses a real Array. */
55+ function gtmConsentCommandParts ( e : any ) : [ string , string , any ?] | null {
56+ if ( e == null || typeof e !== 'object' )
57+ return null
58+ if ( ! Array . isArray ( e ) && ! ( 'length' in e && typeof ( e as any ) . length === 'number' ) )
59+ return null
60+ const row = Array . isArray ( e ) ? e : Array . from ( e as ArrayLike < any > )
61+ if ( row [ 0 ] !== 'consent' )
62+ return null
63+ return [ row [ 0 ] as string , row [ 1 ] as string , row [ 2 ] ]
64+ }
65+
5466describe ( 'consent defaults — clientInit ordering' , ( ) => {
5567 beforeEach ( ( ) => {
5668 delete ( window as any ) . dataLayer
@@ -72,13 +84,14 @@ describe('consent defaults — clientInit ordering', () => {
7284 const dl = ( window as any ) . dataLayer as any [ ]
7385 expect ( Array . isArray ( dl ) ) . toBe ( true )
7486
75- const consentIdx = dl . findIndex ( e => Array . isArray ( e ) && e [ 0 ] === 'consent' && e [ 1 ] === 'default' )
87+ const consentIdx = dl . findIndex ( e => gtmConsentCommandParts ( e ) ?. [ 1 ] === 'default' )
7688 const startIdx = dl . findIndex ( e => e && typeof e === 'object' && ! Array . isArray ( e ) && e . event === 'gtm.js' )
7789
7890 expect ( consentIdx ) . toBeGreaterThanOrEqual ( 0 )
7991 expect ( startIdx ) . toBeGreaterThanOrEqual ( 0 )
8092 expect ( consentIdx ) . toBeLessThan ( startIdx )
81- expect ( dl [ consentIdx ] [ 2 ] ) . toMatchObject ( { analytics_storage : 'denied' , ad_storage : 'denied' } )
93+ const consentRow = gtmConsentCommandParts ( dl [ consentIdx ] )
94+ expect ( consentRow ?. [ 2 ] ) . toMatchObject ( { analytics_storage : 'denied' , ad_storage : 'denied' } )
8295 } )
8396
8497 it ( 'gtm: array form fires multiple ["consent","default",…] entries in input order, all before gtm.js' , async ( ) => {
@@ -98,14 +111,14 @@ describe('consent defaults — clientInit ordering', () => {
98111
99112 const consentEntries = dl
100113 . map ( ( e , i ) => ( { e, i } ) )
101- . filter ( ( { e } ) => Array . isArray ( e ) && e [ 0 ] === 'consent' && e [ 1 ] === 'default' )
114+ . filter ( ( { e } ) => gtmConsentCommandParts ( e ) ?. [ 1 ] === 'default' )
102115 const startIdx = dl . findIndex ( e => e && typeof e === 'object' && ! Array . isArray ( e ) && e . event === 'gtm.js' )
103116
104117 expect ( consentEntries ) . toHaveLength ( 2 )
105118 expect ( startIdx ) . toBeGreaterThanOrEqual ( 0 )
106119 for ( const { i } of consentEntries ) expect ( i ) . toBeLessThan ( startIdx )
107- expect ( consentEntries [ 0 ] . e [ 2 ] ) . toMatchObject ( { analytics_storage : 'denied' , region : [ 'ES' , 'US-AK' ] , wait_for_update : 500 } )
108- expect ( consentEntries [ 1 ] . e [ 2 ] ) . toMatchObject ( { ad_storage : 'denied' } )
120+ expect ( gtmConsentCommandParts ( consentEntries [ 0 ] . e ) ?. [ 2 ] ) . toMatchObject ( { analytics_storage : 'denied' , region : [ 'ES' , 'US-AK' ] , wait_for_update : 500 } )
121+ expect ( gtmConsentCommandParts ( consentEntries [ 1 ] . e ) ?. [ 2 ] ) . toMatchObject ( { ad_storage : 'denied' } )
109122 } )
110123
111124 it ( 'gtm: single-entry array is observationally equivalent to a bare object' , async ( ) => {
@@ -120,6 +133,14 @@ describe('consent defaults — clientInit ordering', () => {
120133 // Slice up to (but not including) gtm.js — the entry carries a Date.now() timestamp
121134 // that differs between calls. Ordering relative to gtm.js is locked by the sibling test.
122135 return dl . slice ( 0 , startIdx )
136+ // `gtag()` pushes the Arguments object onto dataLayer, so each run yields distinct exotic
137+ // objects. Normalize array-like rows to real arrays so `toEqual` compares values only,
138+ // not Arguments identity or matcher quirks across two separate `clientInit` runs.
139+ . map ( e =>
140+ e != null && typeof e === 'object' && ! Array . isArray ( e ) && 'length' in e && typeof ( e as any ) . length === 'number'
141+ ? Array . from ( e as ArrayLike < any > )
142+ : e ,
143+ )
123144 }
124145
125146 const fromObject = runWith ( { ad_storage : 'denied' } )
@@ -135,13 +156,32 @@ describe('consent defaults — clientInit ordering', () => {
135156 result . _opts . clientInit ( )
136157
137158 const dl = ( window as any ) . dataLayer as any [ ]
138- const consentEntries = dl . filter ( e => Array . isArray ( e ) && e [ 0 ] === 'consent' && e [ 1 ] === 'default' )
159+ const consentEntries = dl . filter ( e => gtmConsentCommandParts ( e ) ?. [ 1 ] === 'default' )
139160 const startIdx = dl . findIndex ( e => e && typeof e === 'object' && ! Array . isArray ( e ) && e . event === 'gtm.js' )
140161
141162 expect ( consentEntries ) . toHaveLength ( 0 )
142163 expect ( startIdx ) . toBeGreaterThanOrEqual ( 0 )
143164 } )
144165
166+ it ( 'gtm: defaultConsent via gtag queues Arguments, not a plain Array (gtag.js contract)' , async ( ) => {
167+ const { useScriptGoogleTagManager } = await import ( '../../packages/script/src/runtime/registry/google-tag-manager' )
168+ const result : any = useScriptGoogleTagManager ( {
169+ id : 'GTM-XXXX' ,
170+ defaultConsent : { analytics_storage : 'denied' } ,
171+ } )
172+
173+ result . _opts . clientInit ( )
174+
175+ const dl = ( window as any ) . dataLayer as any [ ]
176+ const entry = dl . find ( e => gtmConsentCommandParts ( e ) ?. [ 1 ] === 'default' )
177+ expect ( entry ) . toBeDefined ( )
178+ expect ( Array . isArray ( entry ) ) . toBe ( false )
179+ const parts = Array . from ( entry as ArrayLike < any > )
180+ expect ( parts [ 0 ] ) . toBe ( 'consent' )
181+ expect ( parts [ 1 ] ) . toBe ( 'default' )
182+ expect ( parts [ 2 ] ) . toMatchObject ( { analytics_storage : 'denied' } )
183+ } )
184+
145185 it ( 'matomo: "required" pushes requireConsent before setSiteId' , async ( ) => {
146186 ; ( window as any ) . _paq = [ ]
147187 const { useScriptMatomoAnalytics } = await import ( '../../packages/script/src/runtime/registry/matomo-analytics' )
@@ -339,14 +379,17 @@ describe('per-script consent object', () => {
339379 expect ( updateArgs ?. [ 2 ] ) . toEqual ( { ad_storage : 'granted' } )
340380 } )
341381
342- it ( 'gtm: consent.update() pushes [" consent"," update" , state] onto dataLayer' , async ( ) => {
382+ it ( 'gtm: consent.update() queues Arguments( consent, update, state) to dataLayer' , async ( ) => {
343383 ; ( window as any ) . dataLayer = [ ]
344384 const { useScriptGoogleTagManager } = await import ( '../../packages/script/src/runtime/registry/google-tag-manager' )
345385 const result : any = useScriptGoogleTagManager ( { id : 'GTM-XXXX' } )
346386 result . _opts . clientInit ( )
347387 result . consent . update ( { analytics_storage : 'granted' } )
348388 const dl = ( window as any ) . dataLayer as any [ ]
349- expect ( dl ) . toContainEqual ( [ 'consent' , 'update' , { analytics_storage : 'granted' } ] )
389+ const entry = dl . find ( e => gtmConsentCommandParts ( e ) ?. [ 1 ] === 'update' )
390+ expect ( entry ) . toBeDefined ( )
391+ expect ( Array . isArray ( entry ) ) . toBe ( false )
392+ expect ( gtmConsentCommandParts ( entry ) ) . toEqual ( [ 'consent' , 'update' , { analytics_storage : 'granted' } ] )
350393 } )
351394
352395 it ( 'meta: consent.grant()/revoke() queue fbq(\'consent\', ...) calls' , async ( ) => {
0 commit comments