1
- import type { CDPSession , Page as PlaywrightPage , Frame } from "playwright" ;
1
+ import type {
2
+ CDPSession ,
3
+ Page as PlaywrightPage ,
4
+ Frame ,
5
+ ElementHandle ,
6
+ } from "playwright" ;
7
+ import { selectors } from "playwright" ;
2
8
import { z } from "zod/v3" ;
3
9
import { Page , defaultExtractSchema } from "../types/page" ;
4
10
import {
@@ -29,6 +35,7 @@ import {
29
35
import { StagehandAPIError } from "@/types/stagehandApiErrors" ;
30
36
import { scriptContent } from "@/lib/dom/build/scriptContent" ;
31
37
import type { Protocol } from "devtools-protocol" ;
38
+ import { StagehandBackdoor } from "@/lib/dom/global" ;
32
39
33
40
async function getCurrentRootFrameId ( session : CDPSession ) : Promise < string > {
34
41
const { frameTree } = ( await session . send (
@@ -37,6 +44,9 @@ async function getCurrentRootFrameId(session: CDPSession): Promise<string> {
37
44
return frameTree . frame . id ;
38
45
}
39
46
47
+ /** ensure we register the custom selector only once per process */
48
+ let stagehandSelectorRegistered = false ;
49
+
40
50
export class StagehandPage {
41
51
private stagehand : Stagehand ;
42
52
private rawPage : PlaywrightPage ;
@@ -188,6 +198,113 @@ ${scriptContent} \
188
198
}
189
199
}
190
200
201
+ /** Register the custom selector engine that pierces open/closed shadow roots. */
202
+ private async ensureStagehandSelectorEngine ( ) : Promise < void > {
203
+ if ( stagehandSelectorRegistered ) return ;
204
+ stagehandSelectorRegistered = true ;
205
+
206
+ await selectors . register ( "stagehand" , ( ) => {
207
+ type Backdoor = {
208
+ getClosedRoot ?: ( host : Element ) => ShadowRoot | undefined ;
209
+ } ;
210
+
211
+ function parseSelector ( input : string ) : { name : string ; value : string } {
212
+ // Accept either: "abc123" → uses DEFAULT_ATTR
213
+ // or explicitly: "data-__stagehand-id=abc123"
214
+ const raw = input . trim ( ) ;
215
+ const eq = raw . indexOf ( "=" ) ;
216
+ if ( eq === - 1 ) {
217
+ return {
218
+ name : "data-__stagehand-id" ,
219
+ value : raw . replace ( / ^ [ " ' ] | [ " ' ] $ / g, "" ) ,
220
+ } ;
221
+ }
222
+ const name = raw . slice ( 0 , eq ) . trim ( ) ;
223
+ const value = raw
224
+ . slice ( eq + 1 )
225
+ . trim ( )
226
+ . replace ( / ^ [ " ' ] | [ " ' ] $ / g, "" ) ;
227
+ return { name, value } ;
228
+ }
229
+
230
+ function pushChildren ( node : Node , stack : Node [ ] ) : void {
231
+ if ( node . nodeType === Node . DOCUMENT_NODE ) {
232
+ const de = ( node as Document ) . documentElement ;
233
+ if ( de ) stack . push ( de ) ;
234
+ return ;
235
+ }
236
+
237
+ if ( node . nodeType === Node . DOCUMENT_FRAGMENT_NODE ) {
238
+ const frag = node as DocumentFragment ;
239
+ const hc = frag . children as HTMLCollection | undefined ;
240
+ if ( hc && hc . length ) {
241
+ for ( let i = hc . length - 1 ; i >= 0 ; i -- )
242
+ stack . push ( hc [ i ] as Element ) ;
243
+ } else {
244
+ const cn = frag . childNodes ;
245
+ for ( let i = cn . length - 1 ; i >= 0 ; i -- ) stack . push ( cn [ i ] ) ;
246
+ }
247
+ return ;
248
+ }
249
+
250
+ if ( node . nodeType === Node . ELEMENT_NODE ) {
251
+ const el = node as Element ;
252
+ for ( let i = el . children . length - 1 ; i >= 0 ; i -- )
253
+ stack . push ( el . children [ i ] ) ;
254
+ }
255
+ }
256
+
257
+ function * traverseAllTrees (
258
+ start : Node ,
259
+ ) : Generator < Element , void , unknown > {
260
+ const backdoor = window . __stagehand__ as Backdoor | undefined ;
261
+ const stack : Node [ ] = [ ] ;
262
+
263
+ if ( start . nodeType === Node . DOCUMENT_NODE ) {
264
+ const de = ( start as Document ) . documentElement ;
265
+ if ( de ) stack . push ( de ) ;
266
+ } else {
267
+ stack . push ( start ) ;
268
+ }
269
+
270
+ while ( stack . length ) {
271
+ const node = stack . pop ( ) ! ;
272
+ if ( node . nodeType === Node . ELEMENT_NODE ) {
273
+ const el = node as Element ;
274
+ yield el ;
275
+
276
+ // open shadow
277
+ const open = el . shadowRoot as ShadowRoot | null ;
278
+ if ( open ) stack . push ( open ) ;
279
+
280
+ // closed shadow via backdoor
281
+ const closed = backdoor ?. getClosedRoot ?.( el ) ;
282
+ if ( closed ) stack . push ( closed ) ;
283
+ }
284
+ pushChildren ( node , stack ) ;
285
+ }
286
+ }
287
+
288
+ return {
289
+ query ( root : Node , selector : string ) : Element | null {
290
+ const { name, value } = parseSelector ( selector ) ;
291
+ for ( const el of traverseAllTrees ( root ) ) {
292
+ if ( el . getAttribute ( name ) === value ) return el ;
293
+ }
294
+ return null ;
295
+ } ,
296
+ queryAll ( root : Node , selector : string ) : Element [ ] {
297
+ const { name, value } = parseSelector ( selector ) ;
298
+ const out : Element [ ] = [ ] ;
299
+ for ( const el of traverseAllTrees ( root ) ) {
300
+ if ( el . getAttribute ( name ) === value ) out . push ( el ) ;
301
+ }
302
+ return out ;
303
+ } ,
304
+ } ;
305
+ } ) ;
306
+ }
307
+
191
308
/**
192
309
* Waits for a captcha to be solved when using Browserbase environment.
193
310
*
@@ -410,6 +527,11 @@ ${scriptContent} \
410
527
this . intContext . registerFrameId ( rootId , this ) ;
411
528
412
529
this . intPage = new Proxy ( page , handler ) as unknown as Page ;
530
+
531
+ // Ensure our backdoor and selector engine are ready up front
532
+ await this . ensureStagehandScript ( ) ;
533
+ await this . ensureStagehandSelectorEngine ( ) ;
534
+
413
535
this . initialized = true ;
414
536
return this ;
415
537
} catch ( err : unknown ) {
@@ -999,4 +1121,23 @@ ${scriptContent} \
999
1121
) : Promise < void > {
1000
1122
await this . sendCDP < void > ( `${ domain } .disable` , { } , target ) ;
1001
1123
}
1124
+
1125
+ async getShadowRootHandle (
1126
+ this : StagehandPage ,
1127
+ host : ElementHandle < Element > ,
1128
+ ) : Promise < ElementHandle < ShadowRoot > | null > {
1129
+ const h = await host . evaluateHandle ( ( el : Element ) : ShadowRoot | null => {
1130
+ // Open root?
1131
+ if ( ( el as HTMLElement ) . shadowRoot )
1132
+ return ( el as HTMLElement ) . shadowRoot ! ;
1133
+ // Closed root kept in our isolated world
1134
+ return (
1135
+ (
1136
+ window as Window & { __stagehand__ ?: StagehandBackdoor }
1137
+ ) . __stagehand__ ?. getClosedRoot ( el ) ?? null
1138
+ ) ;
1139
+ } ) ;
1140
+
1141
+ return h . asElement ( ) as ElementHandle < ShadowRoot > | null ;
1142
+ }
1002
1143
}
0 commit comments