@@ -25,6 +25,8 @@ import { Page } from './page';
25
25
import * as platform from './platform' ;
26
26
import { Selectors } from './selectors' ;
27
27
28
+ export type WaitForInteractableOptions = types . TimeoutOptions & { waitForInteractable ?: boolean } ;
29
+
28
30
export class FrameExecutionContext extends js . ExecutionContext {
29
31
readonly frame : frames . Frame ;
30
32
@@ -230,10 +232,15 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
230
232
return point ;
231
233
}
232
234
233
- async _performPointerAction ( action : ( point : types . Point ) => Promise < void > , options ?: input . PointerActionOptions ) : Promise < void > {
235
+ async _performPointerAction ( action : ( point : types . Point ) => Promise < void > , options ?: input . PointerActionOptions & WaitForInteractableOptions ) : Promise < void > {
236
+ const { waitForInteractable = true } = ( options || { } ) ;
237
+ if ( waitForInteractable )
238
+ await this . _waitForStablePosition ( options ) ;
234
239
const relativePoint = options ? options . relativePoint : undefined ;
235
240
await this . _scrollRectIntoViewIfNeeded ( relativePoint ? { x : relativePoint . x , y : relativePoint . y , width : 0 , height : 0 } : undefined ) ;
236
241
const point = relativePoint ? await this . _relativePoint ( relativePoint ) : await this . _clickablePoint ( ) ;
242
+ if ( waitForInteractable )
243
+ await this . _waitForHitTargetAt ( point , options ) ;
237
244
let restoreModifiers : input . Modifier [ ] | undefined ;
238
245
if ( options && options . modifiers )
239
246
restoreModifiers = await this . _page . keyboard . _ensureModifiers ( options . modifiers ) ;
@@ -242,19 +249,19 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
242
249
await this . _page . keyboard . _ensureModifiers ( restoreModifiers ) ;
243
250
}
244
251
245
- hover ( options ?: input . PointerActionOptions ) : Promise < void > {
252
+ hover ( options ?: input . PointerActionOptions & WaitForInteractableOptions ) : Promise < void > {
246
253
return this . _performPointerAction ( point => this . _page . mouse . move ( point . x , point . y ) , options ) ;
247
254
}
248
255
249
- click ( options ?: input . ClickOptions ) : Promise < void > {
256
+ click ( options ?: input . ClickOptions & WaitForInteractableOptions ) : Promise < void > {
250
257
return this . _performPointerAction ( point => this . _page . mouse . click ( point . x , point . y , options ) , options ) ;
251
258
}
252
259
253
- dblclick ( options ?: input . MultiClickOptions ) : Promise < void > {
260
+ dblclick ( options ?: input . MultiClickOptions & WaitForInteractableOptions ) : Promise < void > {
254
261
return this . _performPointerAction ( point => this . _page . mouse . dblclick ( point . x , point . y , options ) , options ) ;
255
262
}
256
263
257
- tripleclick ( options ?: input . MultiClickOptions ) : Promise < void > {
264
+ tripleclick ( options ?: input . MultiClickOptions & WaitForInteractableOptions ) : Promise < void > {
258
265
return this . _performPointerAction ( point => this . _page . mouse . tripleclick ( point . x , point . y , options ) , options ) ;
259
266
}
260
267
@@ -402,19 +409,20 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
402
409
await this . _page . keyboard . type ( text , options ) ;
403
410
}
404
411
405
- async press ( key : string , options : { delay ?: number ; text ?: string ; } | undefined ) {
412
+ async press ( key : string , options ? : { delay ?: number , text ?: string } ) {
406
413
await this . focus ( ) ;
407
414
await this . _page . keyboard . press ( key , options ) ;
408
415
}
409
- async check ( ) {
410
- await this . _setChecked ( true ) ;
416
+
417
+ async check ( options ?: WaitForInteractableOptions ) {
418
+ await this . _setChecked ( true , options ) ;
411
419
}
412
420
413
- async uncheck ( ) {
414
- await this . _setChecked ( false ) ;
421
+ async uncheck ( options ?: WaitForInteractableOptions ) {
422
+ await this . _setChecked ( false , options ) ;
415
423
}
416
424
417
- private async _setChecked ( state : boolean ) {
425
+ private async _setChecked ( state : boolean , options : WaitForInteractableOptions = { } ) {
418
426
const isCheckboxChecked = async ( ) : Promise < boolean > => {
419
427
return this . _evaluateInUtility ( ( node : Node ) => {
420
428
if ( node . nodeType !== Node . ELEMENT_NODE )
@@ -442,7 +450,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
442
450
443
451
if ( await isCheckboxChecked ( ) === state )
444
452
return ;
445
- await this . click ( ) ;
453
+ await this . click ( options ) ;
446
454
if ( await isCheckboxChecked ( ) !== state )
447
455
throw new Error ( 'Unable to click checkbox' ) ;
448
456
}
@@ -497,6 +505,58 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
497
505
return visibleRatio ;
498
506
} ) ;
499
507
}
508
+
509
+ async _waitForStablePosition ( options : types . TimeoutOptions = { } ) : Promise < void > {
510
+ const context = await this . _context . frame . _utilityContext ( ) ;
511
+ const stablePromise = context . evaluate ( ( injected : Injected , node : Node , timeout : number ) => {
512
+ if ( ! node . isConnected )
513
+ throw new Error ( 'Element is not attached to the DOM' ) ;
514
+ const element = node . nodeType === Node . ELEMENT_NODE ? ( node as Element ) : node . parentElement ;
515
+ if ( ! element )
516
+ throw new Error ( 'Element is not attached to the DOM' ) ;
517
+
518
+ let lastRect : types . Rect | undefined ;
519
+ let counter = 0 ;
520
+ return injected . poll ( 'raf' , undefined , timeout , ( ) => {
521
+ // First raf happens in the same animation frame as evaluation, so it does not produce
522
+ // any client rect difference compared to synchronous call. We skip the synchronous call
523
+ // and only force layout during actual rafs as a small optimisation.
524
+ if ( ++ counter === 1 )
525
+ return false ;
526
+ const clientRect = element . getBoundingClientRect ( ) ;
527
+ const rect = { x : clientRect . top , y : clientRect . left , width : clientRect . width , height : clientRect . height } ;
528
+ const isStable = lastRect && rect . x === lastRect . x && rect . y === lastRect . y && rect . width === lastRect . width && rect . height === lastRect . height ;
529
+ lastRect = rect ;
530
+ return isStable ;
531
+ } ) ;
532
+ } , await context . _injected ( ) , this , options . timeout || 0 ) ;
533
+ await helper . waitWithTimeout ( stablePromise , 'element to stop moving' , options . timeout || 0 ) ;
534
+ }
535
+
536
+ async _waitForHitTargetAt ( point : types . Point , options : types . TimeoutOptions = { } ) : Promise < void > {
537
+ const frame = await this . ownerFrame ( ) ;
538
+ if ( frame && frame . parentFrame ( ) ) {
539
+ const element = await frame . frameElement ( ) ;
540
+ const box = await element . boundingBox ( ) ;
541
+ if ( ! box )
542
+ throw new Error ( 'Element is not attached to the DOM' ) ;
543
+ // Translate from viewport coordinates to frame coordinates.
544
+ point = { x : point . x - box . x , y : point . y - box . y } ;
545
+ }
546
+ const context = await this . _context . frame . _utilityContext ( ) ;
547
+ const hitTargetPromise = context . evaluate ( ( injected : Injected , node : Node , timeout : number , point : types . Point ) => {
548
+ const element = node . nodeType === Node . ELEMENT_NODE ? ( node as Element ) : node . parentElement ;
549
+ if ( ! element )
550
+ throw new Error ( 'Element is not attached to the DOM' ) ;
551
+ return injected . poll ( 'raf' , undefined , timeout , ( ) => {
552
+ let hitElement = injected . utils . deepElementFromPoint ( document , point . x , point . y ) ;
553
+ while ( hitElement && hitElement !== element )
554
+ hitElement = injected . utils . parentElementOrShadowHost ( hitElement ) ;
555
+ return hitElement === element ;
556
+ } ) ;
557
+ } , await context . _injected ( ) , this , options . timeout || 0 , point ) ;
558
+ await helper . waitWithTimeout ( hitTargetPromise , 'element to receive mouse events' , options . timeout || 0 ) ;
559
+ }
500
560
}
501
561
502
562
function normalizeSelector ( selector : string ) : string {
@@ -514,51 +574,44 @@ function normalizeSelector(selector: string): string {
514
574
515
575
export type Task = ( context : FrameExecutionContext ) => Promise < js . JSHandle > ;
516
576
517
- export function waitForFunctionTask ( selector : string | undefined , pageFunction : Function | string , options : types . WaitForFunctionOptions , ...args : any [ ] ) {
518
- const { polling = 'raf' } = options ;
577
+ function assertPolling ( polling : types . Polling ) {
519
578
if ( helper . isString ( polling ) )
520
579
assert ( polling === 'raf' || polling === 'mutation' , 'Unknown polling option: ' + polling ) ;
521
580
else if ( helper . isNumber ( polling ) )
522
581
assert ( polling > 0 , 'Cannot poll with non-positive interval: ' + polling ) ;
523
582
else
524
583
throw new Error ( 'Unknown polling options: ' + polling ) ;
584
+ }
585
+
586
+ export function waitForFunctionTask ( selector : string | undefined , pageFunction : Function | string , options : types . WaitForFunctionOptions , ...args : any [ ] ) : Task {
587
+ const { polling = 'raf' } = options ;
588
+ assertPolling ( polling ) ;
525
589
const predicateBody = helper . isString ( pageFunction ) ? 'return (' + pageFunction + ')' : 'return (' + pageFunction + ')(...args)' ;
526
590
if ( selector !== undefined )
527
591
selector = normalizeSelector ( selector ) ;
528
592
529
593
return async ( context : FrameExecutionContext ) => context . evaluateHandle ( ( injected : Injected , selector : string | undefined , predicateBody : string , polling : types . Polling , timeout : number , ...args ) => {
530
594
const innerPredicate = new Function ( '...args' , predicateBody ) ;
531
- if ( polling === 'raf' )
532
- return injected . pollRaf ( selector , predicate , timeout ) ;
533
- if ( polling === 'mutation' )
534
- return injected . pollMutation ( selector , predicate , timeout ) ;
535
- return injected . pollInterval ( selector , polling , predicate , timeout ) ;
536
-
537
- function predicate ( element : Element | undefined ) : any {
595
+ return injected . poll ( polling , selector , timeout , ( element : Element | undefined ) : any => {
538
596
if ( selector === undefined )
539
597
return innerPredicate ( ...args ) ;
540
598
return innerPredicate ( element , ...args ) ;
541
- }
599
+ } ) ;
542
600
} , await context . _injected ( ) , selector , predicateBody , polling , options . timeout || 0 , ...args ) ;
543
601
}
544
602
545
603
export function waitForSelectorTask ( selector : string , visibility : types . Visibility , timeout : number ) : Task {
546
- return async ( context : FrameExecutionContext ) => {
547
- selector = normalizeSelector ( selector ) ;
548
- return context . evaluateHandle ( ( injected : Injected , selector : string , visibility : types . Visibility , timeout : number ) => {
549
- if ( visibility !== 'any' )
550
- return injected . pollRaf ( selector , predicate , timeout ) ;
551
- return injected . pollMutation ( selector , predicate , timeout ) ;
552
-
553
- function predicate ( element : Element | undefined ) : Element | boolean {
554
- if ( ! element )
555
- return visibility === 'hidden' ;
556
- if ( visibility === 'any' )
557
- return element ;
558
- return injected . isVisible ( element ) === ( visibility === 'visible' ) ? element : false ;
559
- }
560
- } , await context . _injected ( ) , selector , visibility , timeout ) ;
561
- } ;
604
+ selector = normalizeSelector ( selector ) ;
605
+ return async ( context : FrameExecutionContext ) => context . evaluateHandle ( ( injected : Injected , selector : string , visibility : types . Visibility , timeout : number ) => {
606
+ const polling = visibility === 'any' ? 'mutation' : 'raf' ;
607
+ return injected . poll ( polling , selector , timeout , ( element : Element | undefined ) : Element | boolean => {
608
+ if ( ! element )
609
+ return visibility === 'hidden' ;
610
+ if ( visibility === 'any' )
611
+ return element ;
612
+ return injected . isVisible ( element ) === ( visibility === 'visible' ) ? element : false ;
613
+ } ) ;
614
+ } , await context . _injected ( ) , selector , visibility , timeout ) ;
562
615
}
563
616
564
617
export const setFileInputFunction = async ( element : HTMLInputElement , payloads : types . FilePayload [ ] ) => {
0 commit comments