1818import os from 'os' ;
1919import path from 'path' ;
2020
21+ import ws from 'ws' ;
2122import { debugLogger , RecentLogsCollector } from '@utils/debugLogger' ;
2223import { removeFolders } from '@utils/fileUtils' ;
24+ import { httpHappyEyeballsAgent , httpsHappyEyeballsAgent } from '@utils/happyEyeballs' ;
2325import { headersArrayToObject } from '@isomorphic/headers' ;
2426import { Browser } from '../../browser' ;
2527import { helper } from '../../helper' ;
26- import { WebSocketTransport } from '../../transport' ;
28+ import { perMessageDeflate } from '../../transport' ;
2729import { getUserAgent } from '../../userAgent' ;
2830import { BrowserContext } from '../../browserContext' ;
2931import { DialogBridge } from './dialogBridge' ;
@@ -33,9 +35,11 @@ import { WVPage } from './wvPage';
3335import type { BrowserOptions , BrowserProcess } from '../../browser' ;
3436import type { SdkObject } from '../../instrumentation' ;
3537import type { InitScript , Page } from '../../page' ;
38+ import type { ProtocolRequest , ProtocolResponse } from '../../transport' ;
3639import type * as types from '../../types' ;
3740import type * as channels from '@protocol/channels' ;
3841import type { Progress } from '../../progress' ;
42+ import type { ConnectOverCDPTransport } from '../../../../types/types.d.ts' ;
3943
4044type ProxyTab = {
4145 url : string ;
@@ -64,26 +68,82 @@ async function listTabs(proxyBase: string, headers: { [key: string]: string }):
6468 return data . filter ( t => ! ! t . webSocketDebuggerUrl ) ;
6569}
6670
67- export async function connectOverRDP ( progress : Progress , parent : SdkObject , endpointURL : string , options : { slowMo ?: number , headers ?: types . HeadersArray , isLocal ?: boolean , noDefaults ?: boolean , artifactsDir ?: string } ) : Promise < Browser > {
71+ // Local WebSocket-backed transport: defers opening the socket until `open()`
72+ // is called, so listeners on `onmessage` are wired before the remote side
73+ // starts emitting events.
74+ class DeferredWebSocketTransport implements ConnectOverCDPTransport {
75+ private readonly _url : string ;
76+ private readonly _headers : { [ key : string ] : string } ;
77+ private _ws : ws | undefined ;
78+ private _closed = false ;
79+
80+ onmessage ?: ( message : object ) => void ;
81+ onclose ?: ( reason ?: string ) => void ;
82+
83+ constructor ( url : string , headers : { [ key : string ] : string } ) {
84+ this . _url = url ;
85+ this . _headers = headers ;
86+ }
87+
88+ open ( ) : void {
89+ if ( this . _closed )
90+ return ;
91+ const url = this . _url ;
92+ this . _ws = new ws ( url , [ ] , {
93+ maxPayload : 256 * 1024 * 1024 ,
94+ headers : this . _headers ,
95+ followRedirects : true ,
96+ agent : ( / ^ ( h t t p s | w s s ) : \/ \/ / . test ( url ) ) ? httpsHappyEyeballsAgent : httpHappyEyeballsAgent ,
97+ perMessageDeflate,
98+ allowSynchronousEvents : false ,
99+ } ) ;
100+ this . _ws . addEventListener ( 'message' , event => {
101+ const eventData = event . data as string ;
102+ let parsedJson : ProtocolResponse ;
103+ try {
104+ parsedJson = JSON . parse ( eventData ) ;
105+ this . onmessage ?.( parsedJson ) ;
106+ } catch {
107+ this . _ws ?. close ( ) ;
108+ }
109+ } ) ;
110+ this . _ws . addEventListener ( 'close' , event => {
111+ this . onclose ?.( event . reason ) ;
112+ } ) ;
113+ this . _ws . addEventListener ( 'error' , ( ) => { } ) ;
114+ }
115+
116+ send ( message : object ) : void {
117+ this . _ws ?. send ( JSON . stringify ( message as ProtocolRequest ) ) ;
118+ }
119+
120+ close ( ) : void {
121+ this . _closed = true ;
122+ this . _ws ?. close ( ) ;
123+ }
124+ }
125+
126+ export async function connectOverRDP ( progress : Progress , parent : SdkObject , params : channels . BrowserTypeConnectOverCDPParams ) : Promise < Browser > {
68127 let headersMap : { [ key : string ] : string ; } | undefined ;
69- if ( options . headers )
70- headersMap = headersArrayToObject ( options . headers , false ) ;
128+ if ( params . headers )
129+ headersMap = headersArrayToObject ( params . headers , false ) ;
71130 if ( ! headersMap )
72131 headersMap = { 'User-Agent' : getUserAgent ( ) } ;
73132 else if ( ! Object . keys ( headersMap ) . some ( key => key . toLowerCase ( ) === 'user-agent' ) )
74133 headersMap [ 'User-Agent' ] = getUserAgent ( ) ;
75134
76- const proxyBase = deriveProxyBase ( endpointURL ) ;
135+ const transport = params . transport as ConnectOverCDPTransport | undefined ;
136+ const proxyBase = transport ? '' : deriveProxyBase ( params . endpointURL ! ) ;
77137
78- const artifactsDir = options . artifactsDir ?? path . join ( os . tmpdir ( ) , 'playwright-artifacts-' ) ;
138+ const artifactsDir = params . artifactsDir ?? path . join ( os . tmpdir ( ) , 'playwright-artifacts-' ) ;
79139 const doCleanup = async ( ) => {
80140 await removeFolders ( [ artifactsDir ] ) ;
81141 } ;
82142
83143 const browser = await progress . race ( ( async ( ) => {
84144 const dialogBridge = await DialogBridge . start ( ) ;
85- const created = new WVBrowser ( parent , proxyBase , headersMap ! , dialogBridge , {
86- slowMo : options . slowMo ,
145+ const created = new WVBrowser ( parent , proxyBase , headersMap ! , dialogBridge , transport , {
146+ slowMo : params . slowMo ,
87147 name : 'webkit' ,
88148 browserType : 'webkit' ,
89149 browserProcess : { close : async ( ) => { } , kill : async ( ) => { } } as BrowserProcess ,
@@ -104,15 +164,15 @@ export async function connectOverRDP(progress: Progress, parent: SdkObject, endp
104164 return created ;
105165 } ) ( ) ) ;
106166
107- if ( ! options . isLocal )
167+ if ( ! params . isLocal )
108168 browser . _isCollocatedWithServer = false ;
109169 browser . on ( Browser . Events . Disconnected , doCleanup ) ;
110170 return browser ;
111171}
112172
113173type TabEntry = {
114174 pageId : string ;
115- transport : WebSocketTransport ;
175+ transport : ConnectOverCDPTransport ;
116176 connection : WVConnection ;
117177 page : WVPage ;
118178} ;
@@ -122,24 +182,30 @@ export class WVBrowser extends Browser {
122182 readonly _proxyBase : string ;
123183 readonly _headers : { [ key : string ] : string } ;
124184 readonly _dialogBridge : DialogBridge ;
185+ readonly _directPageTransport : ConnectOverCDPTransport | undefined ;
125186 readonly _tabs = new Map < string , TabEntry > ( ) ;
126187 private _didCloseFired = false ;
127188 // Backwards compat — old code still reads `_page` for the "primary" tab.
128189 _page ! : WVPage ;
129190
130- constructor ( parent : SdkObject , proxyBase : string , headers : { [ key : string ] : string } , dialogBridge : DialogBridge , options : BrowserOptions ) {
191+ constructor ( parent : SdkObject , proxyBase : string , headers : { [ key : string ] : string } , dialogBridge : DialogBridge , directPageTransport : ConnectOverCDPTransport | undefined , options : BrowserOptions ) {
131192 super ( parent , options ) ;
132193 this . _proxyBase = proxyBase ;
133194 this . _headers = headers ;
134195 this . _dialogBridge = dialogBridge ;
196+ this . _directPageTransport = directPageTransport ;
135197 this . _context = new WVBrowserContext ( this ) ;
136198 }
137199
138200 async _initialize ( ) : Promise < void > {
139201 await this . _context . initialize ( ) ;
140- await this . _syncTabs ( ) ;
141- if ( ! this . _tabs . size )
142- throw new Error ( `No Mobile Safari tabs found at ${ this . _proxyBase } /json — open Safari first.` ) ;
202+ if ( this . _directPageTransport ) {
203+ await this . _attachTab ( 'rdp-transport' , this . _directPageTransport ) ;
204+ } else {
205+ await this . _syncTabs ( ) ;
206+ if ( ! this . _tabs . size )
207+ throw new Error ( `No Mobile Safari tabs found at ${ this . _proxyBase } /json — open Safari first.` ) ;
208+ }
143209 this . _page = this . _firstTab ( ) . page ;
144210 }
145211
@@ -156,7 +222,7 @@ export class WVBrowser extends Browser {
156222 if ( this . _tabs . has ( pageId ) )
157223 continue ;
158224 try {
159- await this . _attachTab ( pageId , tab ) ;
225+ await this . _attachTab ( pageId , new DeferredWebSocketTransport ( tab . webSocketDebuggerUrl , this . _headers ) ) ;
160226 } catch ( e ) {
161227 debugLogger . log ( 'error' , `webview: failed to attach to tab ${ pageId } : ${ ( e as Error ) . message } ` ) ;
162228 }
@@ -168,15 +234,14 @@ export class WVBrowser extends Browser {
168234 }
169235 }
170236
171- private async _attachTab ( pageId : string , tab : ProxyTab ) : Promise < void > {
172- const transport = await WebSocketTransport . connect ( undefined , tab . webSocketDebuggerUrl , { headers : this . _headers , followRedirects : true } ) ;
237+ private async _attachTab ( pageId : string , transport : ConnectOverCDPTransport ) : Promise < void > {
173238 const connection = new WVConnection ( transport , ( ) => this . _detachTab ( pageId ) , this . options . protocolLogger , this . options . browserLogsCollector ) ;
174- // TODO: handle this as RDP connection parameter.
175- connection . outerSession . sendMayFail ( 'Target.setPauseOnStart' , { pauseOnStart : true } ) ;
176239 const dialogEndpoint = this . _dialogBridge . endpointFor ( pageId ) ;
177240 const page = new WVPage ( this . _context , connection . outerSession , dialogEndpoint ) ;
178241 this . _dialogBridge . registerTab ( pageId , req => page . onBridgeDialog ( req ) ) ;
179242 this . _tabs . set ( pageId , { pageId, transport, connection, page } ) ;
243+ transport . open ?.( ) ;
244+ connection . outerSession . sendMayFail ( 'Target.setPauseOnStart' , { pauseOnStart : true } ) ;
180245 await page . waitForInitialized ( ) ;
181246 }
182247
0 commit comments