@@ -49,6 +49,7 @@ function fixture(options = {}) {
4949 createElement : tag => {
5050 assert . equal ( tag , 'iframe' ) ;
5151 const frameListeners = new Map ( ) ;
52+ const attributes = new Map ( ) ;
5253 frame = {
5354 contentWindow : { postMessage : ( data , origin ) => {
5455 if ( remainingSendFailures > 0 ) {
@@ -57,7 +58,8 @@ function fixture(options = {}) {
5758 }
5859 sent . push ( { data, origin } ) ;
5960 } } ,
60- setAttribute ( ) { } ,
61+ setAttribute : ( name , value ) => attributes . set ( name , value ) ,
62+ getAttribute : name => attributes . get ( name ) ?? null ,
6163 addEventListener : ( type , listener ) => frameListeners . set ( type , listener ) ,
6264 fireLoad : ( ) => frameListeners . get ( 'load' ) ?. ( ) ,
6365 fireError : ( ) => frameListeners . get ( 'error' ) ?. ( ) ,
@@ -108,20 +110,88 @@ async function nextRequest(run) {
108110 return { promise } ;
109111}
110112
111- test ( 'reads shared consent through one fixed iframe without a parent tracking script' , async ( ) => {
113+ test ( 'reads shared consent through one fixed iframe without a parent tracking script' , async ( t ) => {
112114 const f = fixture ( ) ;
115+ t . after ( ( ) => f . client . close ( ) ) ;
113116 const { promise } = await nextRequest ( ( ) => f . client . read ( ) ) ;
117+ void promise . catch ( ( ) => { } ) ;
114118 assert . equal ( f . frame . src , bridgeUrl ) ;
115119 assert . equal ( f . frame . hidden , true ) ;
116120 assert . equal ( f . frame . referrerPolicy , 'no-referrer' ) ;
117121 assert . equal ( f . sent [ 0 ] . origin , 'https://registrystack.org' ) ;
118122 assert . equal ( f . sent [ 0 ] . data . type , 'read' ) ;
123+ assert . equal ( f . frame . getAttribute ( 'data-tracking-ready' ) , 'load' ) ;
119124 f . reply ( unknown ) ;
120125 assert . deepEqual ( await promise , unknown ) ;
121126 assert . equal ( f . values . size , 0 ) ;
122127 f . client . close ( ) ;
123128} ) ;
124129
130+ test ( 'broker readiness permits a consent read before the iframe load event' , async ( t ) => {
131+ const f = fixture ( { autoLoad : false } ) ;
132+ t . after ( ( ) => f . client . close ( ) ) ;
133+ const { promise } = await nextRequest ( ( ) => f . client . read ( ) ) ;
134+ void promise . catch ( ( ) => { } ) ;
135+ assert . equal ( f . sent . length , 0 ) ;
136+
137+ f . message ( { protocol : envelope . protocol , version : envelope . version , type : 'ready' } ) ;
138+ await new Promise ( resolve => setImmediate ( resolve ) ) ;
139+ assert . equal ( f . sent . length , 1 ) ;
140+ assert . equal ( f . sent [ 0 ] . data . type , 'read' ) ;
141+ assert . equal ( f . frame . getAttribute ( 'data-tracking-ready' ) , 'broker' ) ;
142+ assert . deepEqual ( f . client . state , unknown ) ;
143+ f . reply ( rejected ) ;
144+ assert . deepEqual ( await promise , rejected ) ;
145+
146+ f . frame . fireLoad ( ) ;
147+ await new Promise ( resolve => setImmediate ( resolve ) ) ;
148+ assert . equal ( f . sent . length , 1 ) ;
149+ assert . equal ( f . failures . length , 0 ) ;
150+ } ) ;
151+
152+ test ( 'readiness requires the exact envelope from the current bridge origin and frame' , async ( t ) => {
153+ const f = fixture ( { autoLoad : false } ) ;
154+ t . after ( ( ) => f . client . close ( ) ) ;
155+ const { promise } = await nextRequest ( ( ) => f . client . read ( ) ) ;
156+ void promise . catch ( ( ) => { } ) ;
157+ const ready = { protocol : envelope . protocol , version : envelope . version , type : 'ready' } ;
158+ f . message ( ready , { origin : 'https://attacker.example' } ) ;
159+ f . message ( ready , { source : { } } ) ;
160+ f . message ( { ...ready , state : rejected } ) ;
161+ f . message ( { ...ready , version : 1 } ) ;
162+ await new Promise ( resolve => setImmediate ( resolve ) ) ;
163+ assert . equal ( f . sent . length , 0 ) ;
164+ assert . deepEqual ( f . client . state , unknown ) ;
165+
166+ f . message ( ready ) ;
167+ await new Promise ( resolve => setImmediate ( resolve ) ) ;
168+ assert . equal ( f . sent . length , 1 ) ;
169+ f . reply ( rejected ) ;
170+ assert . deepEqual ( await promise , rejected ) ;
171+ } ) ;
172+
173+ test ( 'readiness from a removed failed frame cannot unlock its replacement' , async ( t ) => {
174+ const f = fixture ( { autoLoad : false } ) ;
175+ t . after ( ( ) => f . client . close ( ) ) ;
176+ const { promise } = await nextRequest ( ( ) => f . client . read ( ) ) ;
177+ void promise . catch ( ( ) => { } ) ;
178+ const oldFrame = f . frame ;
179+ oldFrame . fireError ( ) ;
180+ await new Promise ( resolve => setImmediate ( resolve ) ) ;
181+ assert . equal ( f . frames . length , 2 ) ;
182+ const ready = { protocol : envelope . protocol , version : envelope . version , type : 'ready' } ;
183+ f . message ( ready , { source : oldFrame . contentWindow } ) ;
184+ await new Promise ( resolve => setImmediate ( resolve ) ) ;
185+ assert . equal ( f . sent . length , 0 ) ;
186+
187+ f . message ( ready ) ;
188+ await new Promise ( resolve => setImmediate ( resolve ) ) ;
189+ assert . equal ( f . sent . length , 1 ) ;
190+ f . reply ( rejected ) ;
191+ assert . deepEqual ( await promise , rejected ) ;
192+ assert . equal ( f . failures . length , 0 ) ;
193+ } ) ;
194+
125195test ( 'concurrent startup and resume reads share one request and confirm an unchanged remote state' , async ( t ) => {
126196 const f = fixture ( ) ;
127197 t . after ( ( ) => f . client . close ( ) ) ;
0 commit comments