@@ -453,6 +453,64 @@ export class BrowserSession {
453453 }
454454 }
455455
456+ /**
457+ * Force links and window.open to navigate in the same tab.
458+ * This makes clicks on anchors with target="_blank" stay in the current page
459+ * and also intercepts window.open so SPA/open-in-new-tab patterns don't spawn popups.
460+ */
461+ private async forceLinksToSameTab ( page : Page ) : Promise < void > {
462+ try {
463+ await page . evaluate ( ( ) => {
464+ try {
465+ // Ensure we only install once per document
466+ if ( ( window as any ) . __ROO_FORCE_SAME_TAB__ ) return
467+ ; ( window as any ) . __ROO_FORCE_SAME_TAB__ = true
468+
469+ // Override window.open to navigate current tab instead of creating a new one
470+ const originalOpen = window . open
471+ window . open = function ( url : string | URL , target ?: string , features ?: string ) {
472+ try {
473+ const href = typeof url === "string" ? url : String ( url )
474+ location . href = href
475+ } catch {
476+ // fall back to original if something unexpected occurs
477+ try {
478+ return originalOpen . apply ( window , [ url as any , "_self" , features ] ) as any
479+ } catch { }
480+ }
481+ return null as any
482+ } as any
483+
484+ // Rewrite anchors that explicitly open new tabs
485+ document . querySelectorAll ( 'a[target="_blank"]' ) . forEach ( ( a ) => {
486+ a . setAttribute ( "target" , "_self" )
487+ } )
488+
489+ // Defensive capture: if an element still tries to open in a new tab, force same-tab
490+ document . addEventListener (
491+ "click" ,
492+ ( ev ) => {
493+ const el = ( ev . target as HTMLElement | null ) ?. closest ?.(
494+ 'a[target="_blank"]' ,
495+ ) as HTMLAnchorElement | null
496+ if ( el && el . href ) {
497+ ev . preventDefault ( )
498+ try {
499+ location . href = el . href
500+ } catch { }
501+ }
502+ } ,
503+ { capture : true , passive : false } ,
504+ )
505+ } catch {
506+ // no-op; forcing same-tab is best-effort
507+ }
508+ } )
509+ } catch {
510+ // If evaluate fails (e.g., cross-origin/state), continue without breaking the action
511+ }
512+ }
513+
456514 /**
457515 * Handles mouse interaction with network activity monitoring
458516 */
@@ -463,6 +521,9 @@ export class BrowserSession {
463521 ) : Promise < void > {
464522 const [ x , y ] = coordinate . split ( "," ) . map ( Number )
465523
524+ // Force any new-tab behavior (target="_blank", window.open) to stay in the same tab
525+ await this . forceLinksToSameTab ( page )
526+
466527 // Set up network request monitoring
467528 let hasNetworkActivity = false
468529 const requestListener = ( ) => {
@@ -506,6 +567,27 @@ export class BrowserSession {
506567 } )
507568 }
508569
570+ async press ( key : string ) : Promise < BrowserActionResult > {
571+ return this . doAction ( async ( page ) => {
572+ // Allow common aliases
573+ const mapping : Record < string , string > = {
574+ esc : "Escape" ,
575+ return : "Enter" ,
576+ escape : "Escape" ,
577+ enter : "Enter" ,
578+ tab : "Tab" ,
579+ space : "Space" ,
580+ arrowup : "ArrowUp" ,
581+ arrowdown : "ArrowDown" ,
582+ arrowleft : "ArrowLeft" ,
583+ arrowright : "ArrowRight" ,
584+ }
585+ const normalized = key . trim ( )
586+ const mapped = mapping [ normalized . toLowerCase ( ) ] ?? key
587+ await page . keyboard . press ( mapped )
588+ } )
589+ }
590+
509591 /**
510592 * Scrolls the page by the specified amount
511593 */
0 commit comments