@@ -1392,73 +1392,54 @@ export class Frame extends SdkObject {
1392
1392
}
1393
1393
1394
1394
private async _expectImpl ( metadata : CallMetadata , selector : string , options : FrameExpectParams ) : Promise < { matches : boolean , received ?: any , log ?: string [ ] , timedOut ?: boolean } > {
1395
- let timeout = this . _page . _timeoutSettings . timeout ( options ) ;
1396
- const start = timeout > 0 ? monotonicTime ( ) : 0 ;
1397
1395
const lastIntermediateResult : { received ?: any , isSet : boolean } = { isSet : false } ;
1398
- const resultOneShot = await this . _expectInternal ( metadata , selector , options , true , timeout , lastIntermediateResult ) ;
1399
- if ( resultOneShot . matches !== options . isNot )
1400
- return resultOneShot ;
1401
- if ( timeout > 0 ) {
1402
- const elapsed = monotonicTime ( ) - start ;
1403
- timeout -= elapsed ;
1404
- }
1405
- if ( timeout < 0 )
1406
- return { matches : options . isNot , log : metadata . log , timedOut : true , received : lastIntermediateResult . received } ;
1407
- return await this . _expectInternal ( metadata , selector , options , false , timeout , lastIntermediateResult ) ;
1408
- }
1396
+ try {
1397
+ let timeout = this . _page . _timeoutSettings . timeout ( options ) ;
1398
+ const start = timeout > 0 ? monotonicTime ( ) : 0 ;
1409
1399
1410
- private async _expectInternal ( metadata : CallMetadata , selector : string , options : FrameExpectParams , oneShot : boolean , timeout : number , lastIntermediateResult : { received ?: any , isSet : boolean } ) : Promise < { matches : boolean , received ?: any , log ?: string [ ] , timedOut ?: boolean } > {
1411
- const controller = new ProgressController ( metadata , this ) ;
1412
- return controller . run ( async progress => {
1413
- if ( oneShot ) {
1400
+ // Step 1: perform locator handlers checkpoint with a specified timeout.
1401
+ await ( new ProgressController ( metadata , this ) ) . run ( async progress => {
1414
1402
progress . log ( `${ metadata . apiName } ${ timeout ? ` with timeout ${ timeout } ms` : '' } ` ) ;
1415
1403
progress . log ( `waiting for ${ this . _asLocator ( selector ) } ` ) ;
1416
- }
1417
- return await this . retryWithProgressAndTimeouts ( progress , [ 100 , 250 , 500 , 1000 ] , async continuePolling => {
1418
1404
await this . _page . performLocatorHandlersCheckpoint ( progress ) ;
1405
+ } , timeout ) ;
1419
1406
1420
- const selectorInFrame = await this . selectors . resolveFrameForSelector ( selector , { strict : true } ) ;
1421
- progress . throwIfAborted ( ) ;
1422
-
1423
- const { frame, info } = selectorInFrame || { frame : this , info : undefined } ;
1424
- const world = options . expression === 'to.have.property' ? 'main' : ( info ?. world ?? 'utility' ) ;
1425
- const context = await frame . _context ( world ) ;
1426
- const injected = await context . injectedScript ( ) ;
1427
- progress . throwIfAborted ( ) ;
1428
-
1429
- const { log, matches, received, missingReceived } = await injected . evaluate ( async ( injected , { info, options, callId } ) => {
1430
- const elements = info ? injected . querySelectorAll ( info . parsed , document ) : [ ] ;
1431
- const isArray = options . expression === 'to.have.count' || options . expression . endsWith ( '.array' ) ;
1432
- let log = '' ;
1433
- if ( isArray )
1434
- log = ` locator resolved to ${ elements . length } element${ elements . length === 1 ? '' : 's' } ` ;
1435
- else if ( elements . length > 1 )
1436
- throw injected . strictModeViolationError ( info ! . parsed , elements ) ;
1437
- else if ( elements . length )
1438
- log = ` locator resolved to ${ injected . previewNode ( elements [ 0 ] ) } ` ;
1439
- if ( callId )
1440
- injected . markTargetElements ( new Set ( elements ) , callId ) ;
1441
- return { log, ...await injected . expect ( elements [ 0 ] , options , elements ) } ;
1442
- } , { info, options, callId : metadata . id } ) ;
1443
-
1444
- if ( log )
1445
- progress . log ( log ) ;
1446
- // Note: missingReceived avoids `unexpected value "undefined"` when element was not found.
1447
- if ( matches === options . isNot ) {
1448
- lastIntermediateResult . received = missingReceived ? '<element(s) not found>' : received ;
1449
- lastIntermediateResult . isSet = true ;
1450
- if ( ! missingReceived && ! Array . isArray ( received ) )
1451
- progress . log ( ` unexpected value "${ renderUnexpectedValue ( options . expression , received ) } "` ) ;
1452
- }
1453
- if ( ! oneShot && matches === options . isNot ) {
1454
- // Keep waiting in these cases:
1455
- // expect(locator).conditionThatDoesNotMatch
1456
- // expect(locator).not.conditionThatDoesMatch
1457
- return continuePolling ;
1458
- }
1459
- return { matches, received } ;
1460
- } ) ;
1461
- } , oneShot ? 0 : timeout ) . catch ( e => {
1407
+ // Step 2: perform one-shot expect check without a timeout.
1408
+ // Supports the case of `expect(locator).toBeVisible({ timeout: 1 })`
1409
+ // that should succeed when the locator is already visible.
1410
+ try {
1411
+ const resultOneShot = await ( new ProgressController ( metadata , this ) ) . run ( async progress => {
1412
+ return await this . _expectInternal ( progress , selector , options , lastIntermediateResult ) ;
1413
+ } ) ;
1414
+ if ( resultOneShot . matches !== options . isNot )
1415
+ return resultOneShot ;
1416
+ } catch ( e ) {
1417
+ if ( js . isJavaScriptErrorInEvaluate ( e ) || isInvalidSelectorError ( e ) )
1418
+ throw e ;
1419
+ // Ignore any other errors from one-shot, we'll handle them during retries.
1420
+ }
1421
+ if ( timeout > 0 ) {
1422
+ const elapsed = monotonicTime ( ) - start ;
1423
+ timeout -= elapsed ;
1424
+ }
1425
+ if ( timeout < 0 )
1426
+ return { matches : options . isNot , log : metadata . log , timedOut : true , received : lastIntermediateResult . received } ;
1427
+
1428
+ // Step 3: auto-retry expect with increasing timeouts. Bounded by the total remaining time.
1429
+ return await ( new ProgressController ( metadata , this ) ) . run ( async progress => {
1430
+ return await this . retryWithProgressAndTimeouts ( progress , [ 100 , 250 , 500 , 1000 ] , async continuePolling => {
1431
+ await this . _page . performLocatorHandlersCheckpoint ( progress ) ;
1432
+ const { matches, received } = await this . _expectInternal ( progress , selector , options , lastIntermediateResult ) ;
1433
+ if ( matches === options . isNot ) {
1434
+ // Keep waiting in these cases:
1435
+ // expect(locator).conditionThatDoesNotMatch
1436
+ // expect(locator).not.conditionThatDoesMatch
1437
+ return continuePolling ;
1438
+ }
1439
+ return { matches, received } ;
1440
+ } ) ;
1441
+ } , timeout ) ;
1442
+ } catch ( e ) {
1462
1443
// Q: Why not throw upon isSessionClosedError(e) as in other places?
1463
1444
// A: We want user to receive a friendly message containing the last intermediate result.
1464
1445
if ( js . isJavaScriptErrorInEvaluate ( e ) || isInvalidSelectorError ( e ) )
@@ -1469,7 +1450,44 @@ export class Frame extends SdkObject {
1469
1450
if ( e instanceof TimeoutError )
1470
1451
result . timedOut = true ;
1471
1452
return result ;
1472
- } ) ;
1453
+ }
1454
+ }
1455
+
1456
+ private async _expectInternal ( progress : Progress , selector : string , options : FrameExpectParams , lastIntermediateResult : { received ?: any , isSet : boolean } ) {
1457
+ const selectorInFrame = await this . selectors . resolveFrameForSelector ( selector , { strict : true } ) ;
1458
+ progress . throwIfAborted ( ) ;
1459
+
1460
+ const { frame, info } = selectorInFrame || { frame : this , info : undefined } ;
1461
+ const world = options . expression === 'to.have.property' ? 'main' : ( info ?. world ?? 'utility' ) ;
1462
+ const context = await frame . _context ( world ) ;
1463
+ const injected = await context . injectedScript ( ) ;
1464
+ progress . throwIfAborted ( ) ;
1465
+
1466
+ const { log, matches, received, missingReceived } = await injected . evaluate ( async ( injected , { info, options, callId } ) => {
1467
+ const elements = info ? injected . querySelectorAll ( info . parsed , document ) : [ ] ;
1468
+ const isArray = options . expression === 'to.have.count' || options . expression . endsWith ( '.array' ) ;
1469
+ let log = '' ;
1470
+ if ( isArray )
1471
+ log = ` locator resolved to ${ elements . length } element${ elements . length === 1 ? '' : 's' } ` ;
1472
+ else if ( elements . length > 1 )
1473
+ throw injected . strictModeViolationError ( info ! . parsed , elements ) ;
1474
+ else if ( elements . length )
1475
+ log = ` locator resolved to ${ injected . previewNode ( elements [ 0 ] ) } ` ;
1476
+ if ( callId )
1477
+ injected . markTargetElements ( new Set ( elements ) , callId ) ;
1478
+ return { log, ...await injected . expect ( elements [ 0 ] , options , elements ) } ;
1479
+ } , { info, options, callId : progress . metadata . id } ) ;
1480
+
1481
+ if ( log )
1482
+ progress . log ( log ) ;
1483
+ // Note: missingReceived avoids `unexpected value "undefined"` when element was not found.
1484
+ if ( matches === options . isNot ) {
1485
+ lastIntermediateResult . received = missingReceived ? '<element(s) not found>' : received ;
1486
+ lastIntermediateResult . isSet = true ;
1487
+ if ( ! missingReceived && ! Array . isArray ( received ) )
1488
+ progress . log ( ` unexpected value "${ renderUnexpectedValue ( options . expression , received ) } "` ) ;
1489
+ }
1490
+ return { matches, received } ;
1473
1491
}
1474
1492
1475
1493
async _waitForFunctionExpression < R > ( metadata : CallMetadata , expression : string , isFunction : boolean | undefined , arg : any , options : types . WaitForFunctionOptions , world : types . World = 'main' ) : Promise < js . SmartHandle < R > > {
0 commit comments