44
55import type * as puppeteer from 'puppeteer-core' ;
66
7+ import { AsyncScope } from '../../conductor/async-scope.js' ;
78import { installPageErrorHandlers } from '../../conductor/events.js' ;
89
910import { PageWrapper } from './page-wrapper.js' ;
1011
12+ export type Action = ( element : puppeteer . ElementHandle ) => Promise < void > ;
13+
14+ export interface ClickOptions {
15+ root ?: puppeteer . ElementHandle ;
16+ clickOptions ?: puppeteer . ClickOptions ;
17+ maxPixelsFromLeft ?: number ;
18+ }
19+
1120const envThrottleRate = process . env [ 'STRESS' ] ? 3 : 1 ;
1221const envLatePromises = process . env [ 'LATE_PROMISES' ] !== undefined ?
1322 [ 'true' , '' ] . includes ( process . env [ 'LATE_PROMISES' ] . toLowerCase ( ) ) ? 10 : Number ( process . env [ 'LATE_PROMISES' ] ) :
1423 0 ;
1524
25+ type DeducedElementType < ElementType extends Element | null , Selector extends string > =
26+ ElementType extends null ? puppeteer . NodeFor < Selector > : ElementType ;
27+
1628export class DevToolsFronendPage extends PageWrapper {
1729 async setExperimentEnabled ( experiment : string , enabled : boolean ) {
1830 await this . evaluate ( `(async () => {
@@ -78,7 +90,7 @@ export class DevToolsFronendPage extends PageWrapper {
7890 }
7991
8092 async ensureReadyForTesting ( ) {
81- await this . waitForFunction ( `
93+ await this . page . waitForFunction ( `
8294 (async function() {
8395 const Main = await import('./entrypoints/main/main.js');
8496 return Main.MainImpl.MainImpl.instanceForTest !== null;
@@ -91,6 +103,117 @@ export class DevToolsFronendPage extends PageWrapper {
91103 })();
92104 ` ) ;
93105 }
106+
107+ // Get a single element handle. Uses `pierce` handler per default for piercing Shadow DOM.
108+ async $ < ElementType extends Element | null = null , Selector extends string = string > (
109+ selector : Selector , root ?: puppeteer . ElementHandle , handler = 'pierce' ) {
110+ const rootElement = root ? root : this . page ;
111+ const element = await rootElement . $ ( `${ handler } /${ selector } ` ) as
112+ puppeteer . ElementHandle < DeducedElementType < ElementType , Selector > > ;
113+ return element ;
114+ }
115+
116+ async performActionOnSelector ( selector : string , options : { root ?: puppeteer . ElementHandle } , action : Action ) :
117+ Promise < puppeteer . ElementHandle > {
118+ // TODO(crbug.com/1410168): we should refactor waitFor to be compatible with
119+ // Puppeteer's syntax for selectors.
120+ const queryHandlers = new Set ( [
121+ 'pierceShadowText' ,
122+ 'pierce' ,
123+ 'aria' ,
124+ 'xpath' ,
125+ 'text' ,
126+ ] ) ;
127+ let queryHandler = 'pierce' ;
128+ for ( const handler of queryHandlers ) {
129+ const prefix = handler + '/' ;
130+ if ( selector . startsWith ( prefix ) ) {
131+ queryHandler = handler ;
132+ selector = selector . substring ( prefix . length ) ;
133+ break ;
134+ }
135+ }
136+ return await this . waitForFunction ( async ( ) => {
137+ const element = await this . waitFor ( selector , options ?. root , undefined , queryHandler ) ;
138+ try {
139+ await action ( element ) ;
140+ await this . drainFrontendTaskQueue ( ) ;
141+ return element ;
142+ } catch {
143+ return undefined ;
144+ }
145+ } ) ;
146+ }
147+
148+ async waitFor < ElementType extends Element = Element > (
149+ selector : string , root ?: puppeteer . ElementHandle , asyncScope = new AsyncScope ( ) , handler ?: string ) {
150+ return await asyncScope . exec ( ( ) => this . waitForFunction ( async ( ) => {
151+ const element = await this . $ < ElementType , typeof selector > ( selector , root , handler ) ;
152+ return ( element || undefined ) ;
153+ } , asyncScope ) , `Waiting for element matching selector '${ selector } '` ) ;
154+ }
155+
156+ /**
157+ * Schedules a task in the frontend page that ensures that previously
158+ * handled tasks have been handled.
159+ */
160+ async drainFrontendTaskQueue ( ) : Promise < void > {
161+ await this . evaluate ( async ( ) => {
162+ await new Promise ( resolve => setTimeout ( resolve , 0 ) ) ;
163+ } ) ;
164+ }
165+
166+ async waitForFunction < T > ( fn : ( ) => Promise < T | undefined > , asyncScope = new AsyncScope ( ) , description ?: string ) {
167+ const innerFunction = async ( ) => {
168+ while ( true ) {
169+ AsyncScope . abortSignal ?. throwIfAborted ( ) ;
170+ const result = await fn ( ) ;
171+ AsyncScope . abortSignal ?. throwIfAborted ( ) ;
172+ if ( result ) {
173+ return result ;
174+ }
175+ await this . timeout ( 100 ) ;
176+ }
177+ } ;
178+ return await asyncScope . exec ( innerFunction , description ) ;
179+ }
180+
181+ timeout ( duration : number ) {
182+ return new Promise < void > ( resolve => setTimeout ( resolve , duration ) ) ;
183+ }
184+
185+ async click ( selector : string , options ?: ClickOptions ) {
186+ return await this . performActionOnSelector (
187+ selector , { root : options ?. root } , element => element . click ( options ?. clickOptions ) ) ;
188+ }
189+
190+ async hover ( selector : string , options ?: { root ?: puppeteer . ElementHandle } ) {
191+ return await this . performActionOnSelector ( selector , { root : options ?. root } , element => element . hover ( ) ) ;
192+ }
193+
194+ waitForAria < ElementType extends Element = Element > (
195+ selector : string , root ?: puppeteer . ElementHandle , asyncScope = new AsyncScope ( ) ) {
196+ return this . waitFor < ElementType > ( selector , root , asyncScope , 'aria' ) ;
197+ }
198+
199+ async waitForNone ( selector : string , root ?: puppeteer . ElementHandle , asyncScope = new AsyncScope ( ) , handler ?: string ) {
200+ return await asyncScope . exec ( ( ) => this . waitForFunction ( async ( ) => {
201+ const elements = await this . $$ ( selector , root , handler ) ;
202+ if ( elements . length === 0 ) {
203+ return true ;
204+ }
205+ return false ;
206+ } , asyncScope ) , `Waiting for no elements to match selector '${ selector } '` ) ;
207+ }
208+
209+ // Get multiple element handles. Uses `pierce` handler per default for piercing Shadow DOM.
210+ async $$ < ElementType extends Element | null = null , Selector extends string = string > (
211+ selector : Selector , root ?: puppeteer . JSHandle , handler = 'pierce' ) {
212+ const rootElement = root ? root . asElement ( ) || this . page : this . page ;
213+ const elements = await rootElement . $$ ( `${ handler } /${ selector } ` ) as
214+ Array < puppeteer . ElementHandle < DeducedElementType < ElementType , Selector > > > ;
215+ return elements ;
216+ }
94217}
95218
96219export interface DevtoolsSettings {
0 commit comments