@@ -370,14 +370,86 @@ describeControlUiE2e("Control UI custom-widget host mocked Gateway E2E", () => {
370370 // Post a spoofed data message from the PARENT window (event.source !== the
371371 // iframe's contentWindow). The bridge must ignore it: #value stays the
372372 // legitimate value and never shows the injected marker.
373- await page . evaluate ( ( ) => {
374- window . postMessage (
375- { v : 1 , type : "workspace:data" , requestId : "x" , bindingId : "value" , data : "SPOOFED" } ,
376- "*" ,
377- ) ;
373+ await frame . locator ( "#value" ) . evaluate ( ( node ) => {
374+ type SpoofObservationWindow = Window & {
375+ openclawSawSpoofed ?: boolean ;
376+ openclawSpoofObserver ?: MutationObserver ;
377+ } ;
378+ const targetWindow = window as SpoofObservationWindow ;
379+ const valueNode = node as HTMLElement ;
380+ const recordSpoof = ( ) => {
381+ if ( valueNode . textContent ?. includes ( "SPOOFED" ) ) {
382+ targetWindow . openclawSawSpoofed = true ;
383+ }
384+ } ;
385+ targetWindow . openclawSawSpoofed = false ;
386+ targetWindow . openclawSpoofObserver ?. disconnect ( ) ;
387+ targetWindow . openclawSpoofObserver = new MutationObserver ( recordSpoof ) ;
388+ targetWindow . openclawSpoofObserver . observe ( valueNode , {
389+ characterData : true ,
390+ childList : true ,
391+ subtree : true ,
392+ } ) ;
393+ recordSpoof ( ) ;
378394 } ) ;
379- await page . waitForTimeout ( 300 ) ;
380- expect ( await frame . locator ( "#value" ) . textContent ( ) ) . not . toContain ( "SPOOFED" ) ;
395+ await page . evaluate ( async ( ) => {
396+ await new Promise < void > ( ( resolve ) => {
397+ const onMessage = ( event : MessageEvent ) => {
398+ const data = event . data as { requestId ?: string } | undefined ;
399+ if ( event . source !== window || data ?. requestId !== "x" ) {
400+ return ;
401+ }
402+ window . removeEventListener ( "message" , onMessage ) ;
403+ resolve ( ) ;
404+ } ;
405+ window . addEventListener ( "message" , onMessage ) ;
406+ window . postMessage (
407+ { v : 1 , type : "workspace:data" , requestId : "x" , bindingId : "value" , data : "SPOOFED" } ,
408+ "*" ,
409+ ) ;
410+ } ) ;
411+ } ) ;
412+ await frame . locator ( "#value" ) . evaluate ( async ( ) => {
413+ type WidgetBridge = {
414+ postMessage : ( message : unknown ) => void ;
415+ addEventListener : ( type : "message" , listener : ( event : MessageEvent ) => void ) => void ;
416+ removeEventListener : ( type : "message" , listener : ( event : MessageEvent ) => void ) => void ;
417+ } ;
418+ const bridge = ( window as Window & { openclawWorkspaceBridge ?: WidgetBridge } )
419+ . openclawWorkspaceBridge ;
420+ if ( ! bridge ) {
421+ throw new Error ( "workspace bridge unavailable" ) ;
422+ }
423+ await new Promise < void > ( ( resolve ) => {
424+ const onMessage = ( event : MessageEvent ) => {
425+ const data = event . data as { requestId ?: string } | undefined ;
426+ if ( data ?. requestId !== "after-spoof" ) {
427+ return ;
428+ }
429+ bridge . removeEventListener ( "message" , onMessage ) ;
430+ resolve ( ) ;
431+ } ;
432+ bridge . addEventListener ( "message" , onMessage ) ;
433+ const barrierRequest = {
434+ v : 1 ,
435+ type : "workspace:getData" ,
436+ requestId : "after-spoof" ,
437+ bindingId : "value" ,
438+ } ;
439+ // oxlint-disable-next-line unicorn/require-post-message-target-origin -- This is a MessagePort-style widget bridge, not Window.postMessage.
440+ bridge . postMessage ( barrierRequest ) ;
441+ } ) ;
442+ } ) ;
443+ const sawSpoofed = await frame . locator ( "#value" ) . evaluate ( ( node ) => {
444+ type SpoofObservationWindow = Window & {
445+ openclawSawSpoofed ?: boolean ;
446+ openclawSpoofObserver ?: MutationObserver ;
447+ } ;
448+ const targetWindow = window as SpoofObservationWindow ;
449+ targetWindow . openclawSpoofObserver ?. disconnect ( ) ;
450+ return targetWindow . openclawSawSpoofed === true || node . textContent ?. includes ( "SPOOFED" ) ;
451+ } ) ;
452+ expect ( sawSpoofed ) . toBe ( false ) ;
381453 } finally {
382454 await page . context ( ) . close ( ) ;
383455 }
0 commit comments