2019-11-18 18:18:28 -08:00
/ * *
* Copyright 2017 Google Inc . All rights reserved .
* Modifications copyright ( c ) Microsoft Corporation .
*
* Licensed under the Apache License , Version 2.0 ( the "License" ) ;
* you may not use this file except in compliance with the License .
* You may obtain a copy of the License at
*
* http : //www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing , software
* distributed under the License is distributed on an "AS IS" BASIS ,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
* See the License for the specific language governing permissions and
* limitations under the License .
* /
2025-02-07 13:54:01 -08:00
import * as accessibility from './accessibility' ;
import { BrowserContext } from './browserContext' ;
import { ConsoleMessage } from './console' ;
import { TargetClosedError , TimeoutError } from './errors' ;
import { FileChooser } from './fileChooser' ;
2019-12-09 13:08:21 -08:00
import * as frames from './frames' ;
2025-02-07 13:54:01 -08:00
import { helper } from './helper' ;
2019-12-09 13:08:21 -08:00
import * as input from './input' ;
2025-02-07 13:54:01 -08:00
import { SdkObject } from './instrumentation' ;
import { parseEvaluationResultValue , source } from './isomorphic/utilityScriptSerializers' ;
2019-12-09 13:08:21 -08:00
import * as js from './javascript' ;
2025-02-07 13:54:01 -08:00
import { ProgressController } from './progress' ;
2022-04-19 08:43:18 -06:00
import { Screenshotter , validateScreenshotOptions } from './screenshotter' ;
2022-04-07 13:36:13 -08:00
import { TimeoutSettings } from '../common/timeoutSettings' ;
2025-02-12 14:43:52 -08:00
import { LongStandingScope , assert , compressCallLog , trimStringWithEllipsis } from '../utils' ;
import { createGuid } from './utils/crypto' ;
2025-02-07 13:54:01 -08:00
import { asLocator } from '../utils' ;
2025-02-11 15:40:41 -08:00
import { getComparator } from './utils/comparators' ;
2025-02-07 13:54:01 -08:00
import { debugLogger } from '../utils/debugLogger' ;
2023-03-06 18:49:14 -08:00
import { isInvalidSelectorError } from '../utils/isomorphic/selectorParser' ;
2025-02-12 09:34:01 -08:00
import { ManualPromise } from '../utils/isomorphic/manualPromise' ;
2025-02-07 13:54:01 -08:00
import type { Artifact } from './artifact' ;
import type * as dom from './dom' ;
import type { CallMetadata } from './instrumentation' ;
2022-05-09 14:07:04 -08:00
import type { SerializedValue } from './isomorphic/utilityScriptSerializers' ;
2025-02-07 13:54:01 -08:00
import type * as network from './network' ;
import type { Progress } from './progress' ;
import type { ScreenshotOptions } from './screenshotter' ;
import type * as types from './types' ;
import type { TimeoutOptions } from '../common/types' ;
2025-02-11 15:40:41 -08:00
import type { ImageComparatorOptions } from './utils/comparators' ;
2025-02-07 13:54:01 -08:00
import type * as channels from '@protocol/channels' ;
2019-12-09 13:08:21 -08:00
export interface PageDelegate {
readonly rawMouse : input.RawMouse ;
readonly rawKeyboard : input.RawKeyboard ;
2020-10-19 10:07:33 -07:00
readonly rawTouchscreen : input.RawTouchscreen ;
2019-12-11 12:36:42 -08:00
2019-12-17 11:28:09 -08:00
reload ( ) : Promise < void > ;
goBack ( ) : Promise < boolean > ;
goForward ( ) : Promise < boolean > ;
2024-09-26 05:08:33 -07:00
requestGC ( ) : Promise < void > ;
2024-06-27 09:29:20 -07:00
addInitScript ( initScript : InitScript ) : Promise < void > ;
2024-08-07 06:20:12 -07:00
removeNonInternalInitScripts ( ) : Promise < void > ;
2019-12-09 13:08:21 -08:00
closePage ( runBeforeUnload : boolean ) : Promise < void > ;
2019-12-16 22:02:33 -08:00
navigateFrame ( frame : frames.Frame , url : string , referrer : string | undefined ) : Promise < frames.GotoResult > ;
2019-12-11 12:36:42 -08:00
2020-02-26 12:42:20 -08:00
updateExtraHTTPHeaders ( ) : Promise < void > ;
2022-08-03 16:14:28 -07:00
updateEmulatedViewportSize ( preserveWindowBoundaries? : boolean ) : Promise < void > ;
2020-04-06 19:49:33 -07:00
updateEmulateMedia ( ) : Promise < void > ;
2020-03-09 21:02:54 -07:00
updateRequestInterception ( ) : Promise < void > ;
2022-07-11 12:10:08 -08:00
updateFileChooserInterception ( ) : Promise < void > ;
2020-07-21 09:36:54 -07:00
bringToFront ( ) : Promise < void > ;
2019-12-11 12:36:42 -08:00
setBackgroundColor ( color ? : { r : number ; g : number ; b : number ; a : number ; } ) : Promise < void > ;
2022-04-01 12:28:40 -07:00
takeScreenshot ( progress : Progress , format : string , documentRect : types.Rect | undefined , viewportRect : types.Rect | undefined , quality : number | undefined , fitsViewport : boolean , scale : 'css' | 'device' ) : Promise < Buffer > ;
2019-12-12 17:51:05 -08:00
isElementHandle ( remoteObject : any ) : boolean ;
2019-12-12 21:11:52 -08:00
adoptElementHandle < T extends Node > ( handle : dom.ElementHandle < T > , to : dom.FrameExecutionContext ) : Promise < dom.ElementHandle < T > > ;
2019-12-19 15:19:22 -08:00
getContentFrame ( handle : dom.ElementHandle ) : Promise < frames.Frame | null > ; // Only called for frame owner elements.
2020-01-27 11:43:43 -08:00
getOwnerFrame ( handle : dom.ElementHandle ) : Promise < string | null > ; // Returns frameId.
2024-09-04 11:36:52 -07:00
getContentQuads ( handle : dom.ElementHandle ) : Promise < types.Quad [ ] | null | 'error:notconnected' > ;
2022-03-24 07:46:37 -07:00
setInputFiles ( handle : dom.ElementHandle < HTMLInputElement > , files : types.FilePayload [ ] ) : Promise < void > ;
2024-02-29 14:44:45 -08:00
setInputFilePaths ( handle : dom.ElementHandle < HTMLInputElement > , files : string [ ] ) : Promise < void > ;
2019-12-12 17:51:05 -08:00
getBoundingBox ( handle : dom.ElementHandle ) : Promise < types.Rect | null > ;
2020-02-05 17:20:23 -08:00
getFrameElement ( frame : frames.Frame ) : Promise < dom.ElementHandle > ;
2020-06-24 15:12:17 -07:00
scrollRectIntoViewIfNeeded ( handle : dom.ElementHandle , rect? : types.Rect ) : Promise < 'error:notvisible' | 'error:notconnected' | 'done' > ;
2021-05-11 13:21:01 -07:00
setScreencastOptions ( options : { width : number , height : number , quality : number } | null ) : Promise < void > ;
2020-01-03 11:15:43 -08:00
2020-01-14 16:54:50 -08:00
getAccessibilityTree ( needle? : dom.ElementHandle ) : Promise < { tree : accessibility.AXNode , needle : accessibility.AXNode | null } > ;
2022-06-14 22:02:15 -07:00
pdf ? : ( options : channels.PagePdfParams ) = > Promise < Buffer > ;
2020-02-13 17:39:14 -08:00
coverage ? : ( ) = > any ;
2020-03-07 08:19:31 -08:00
2020-06-25 16:57:21 -07:00
// Work around WebKit's raf issues on Windows.
rafCountForStablePosition ( ) : number ;
2020-03-07 08:19:31 -08:00
// Work around Chrome's non-associated input and protocol.
inputActionEpilogue ( ) : Promise < void > ;
2020-04-17 08:51:54 -07:00
// Work around for asynchronously dispatched CSP errors in Firefox.
2024-03-13 12:22:40 +01:00
readonly cspErrorsAsynchronousForInlineScripts? : boolean ;
2023-05-12 13:21:49 -07:00
// Work around for mouse position in Firefox.
resetForReuse ( ) : Promise < void > ;
2023-12-11 18:20:24 -08:00
// WebKit hack.
shouldToggleStyleSheetToSyncAnimations ( ) : boolean ;
2019-12-09 13:08:21 -08:00
}
2022-07-07 15:28:20 -08:00
type EmulatedSize = { screen : types.Size , viewport : types.Size } ;
type EmulatedMedia = {
2022-10-31 09:09:52 -07:00
media : types.MediaType ;
colorScheme : types.ColorScheme ;
reducedMotion : types.ReducedMotion ;
forcedColors : types.ForcedColors ;
2025-02-04 11:15:51 +01:00
contrast : types.Contrast ;
2019-12-09 13:08:21 -08:00
} ;
2024-02-05 19:07:30 -08:00
type ExpectScreenshotOptions = ImageComparatorOptions & ScreenshotOptions & {
2022-02-28 13:25:59 -07:00
timeout? : number ,
expected? : Buffer ,
isNot? : boolean ,
locator ? : {
frame : frames.Frame ,
selector : string ,
} ,
} ;
2021-02-09 09:00:00 -08:00
export class Page extends SdkObject {
2020-08-21 16:26:33 -07:00
static Events = {
Close : 'close' ,
Crash : 'crash' ,
Download : 'download' ,
FileChooser : 'filechooser' ,
FrameAttached : 'frameattached' ,
FrameDetached : 'framedetached' ,
2021-01-13 14:25:42 -08:00
InternalFrameNavigatedToNewDocument : 'internalframenavigatedtonewdocument' ,
2024-01-19 12:35:00 -08:00
LocatorHandlerTriggered : 'locatorhandlertriggered' ,
2021-04-08 05:32:12 +08:00
ScreencastFrame : 'screencastframe' ,
2021-03-31 10:38:05 -07:00
Video : 'video' ,
2020-10-26 22:20:43 -07:00
WebSocket : 'websocket' ,
2020-08-21 16:26:33 -07:00
Worker : 'worker' ,
} ;
2020-06-29 16:26:32 -07:00
private _closedState : 'open' | 'closing' | 'closed' = 'open' ;
2021-08-29 11:21:06 -07:00
private _closedPromise = new ManualPromise < void > ( ) ;
2024-12-14 20:15:58 +00:00
private _initialized : Page | Error | undefined ;
private _initializedPromise = new ManualPromise < Page | Error > ( ) ;
2023-05-04 15:11:46 -07:00
private _eventsToEmitAfterInitialized : { event : string | symbol , args : any [ ] } [ ] = [ ] ;
2023-09-27 14:09:56 -07:00
private _crashed = false ;
2023-08-28 17:44:48 -07:00
readonly openScope = new LongStandingScope ( ) ;
2020-08-19 10:31:59 -07:00
readonly _browserContext : BrowserContext ;
2019-12-06 13:36:47 -08:00
readonly keyboard : input.Keyboard ;
readonly mouse : input.Mouse ;
2020-10-19 10:07:33 -07:00
readonly touchscreen : input.Touchscreen ;
2019-12-09 13:08:21 -08:00
readonly _timeoutSettings : TimeoutSettings ;
readonly _delegate : PageDelegate ;
2022-07-07 15:28:20 -08:00
_emulatedSize : EmulatedSize | undefined ;
private _extraHTTPHeaders : types.HeadersArray | undefined ;
private _emulatedMedia : Partial < EmulatedMedia > = { } ;
private _interceptFileChooser = false ;
2020-12-02 13:43:16 -08:00
private readonly _pageBindings = new Map < string , PageBinding > ( ) ;
2024-08-07 06:20:12 -07:00
initScripts : InitScript [ ] = [ ] ;
2019-12-09 13:08:21 -08:00
readonly _screenshotter : Screenshotter ;
2019-12-16 15:56:11 -08:00
readonly _frameManager : frames.FrameManager ;
2020-01-03 11:15:43 -08:00
readonly accessibility : accessibility.Accessibility ;
2020-01-07 12:59:01 -08:00
private _workers = new Map < string , Worker > ( ) ;
2022-06-14 22:02:15 -07:00
readonly pdf : ( ( options : channels.PagePdfParams ) = > Promise < Buffer > ) | undefined ;
2020-02-13 17:39:14 -08:00
readonly coverage : any ;
2022-07-01 12:49:43 -07:00
_clientRequestInterceptor : network.RouteHandler | undefined ;
_serverRequestInterceptor : network.RouteHandler | undefined ;
2020-02-11 12:06:58 -08:00
_ownedContext : BrowserContext | undefined ;
2021-03-31 10:38:05 -07:00
_video : Artifact | null = null ;
2021-04-02 11:15:07 -07:00
_opener : Page | undefined ;
2022-03-17 17:27:33 -08:00
private _isServerSideOnly = false ;
2024-04-25 14:00:02 -07:00
private _locatorHandlers = new Map < number , { selector : string , noWaitAfter ? : boolean , resolved ? : ManualPromise < void > } > ( ) ;
2024-01-19 12:35:00 -08:00
private _lastLocatorHandlerUid = 0 ;
private _locatorHandlerRunningCounter = 0 ;
2019-11-18 18:18:28 -08:00
2022-10-21 14:30:14 -07:00
// Aiming at 25 fps by default - each frame is 40ms, but we give some slack with 35ms.
// When throttling for tracing, 200ms between frames, except for 10 frames around the action.
private _frameThrottler = new FrameThrottler ( 10 , 35 , 200 ) ;
2023-10-16 20:32:13 -07:00
_closeReason : string | undefined ;
2022-10-21 14:30:14 -07:00
2020-08-19 10:31:59 -07:00
constructor ( delegate : PageDelegate , browserContext : BrowserContext ) {
2021-04-20 23:03:56 -07:00
super ( browserContext , 'page' ) ;
2021-02-09 09:00:00 -08:00
this . attribution . page = this ;
2019-12-09 13:08:21 -08:00
this . _delegate = delegate ;
2019-12-05 14:11:48 -08:00
this . _browserContext = browserContext ;
2020-01-03 11:15:43 -08:00
this . accessibility = new accessibility . Accessibility ( delegate . getAccessibilityTree . bind ( delegate ) ) ;
2024-04-29 08:15:12 -07:00
this . keyboard = new input . Keyboard ( delegate . rawKeyboard ) ;
2020-08-18 19:13:40 -07:00
this . mouse = new input . Mouse ( delegate . rawMouse , this ) ;
2020-10-19 10:07:33 -07:00
this . touchscreen = new input . Touchscreen ( delegate . rawTouchscreen , this ) ;
2020-02-13 14:18:18 -08:00
this . _timeoutSettings = new TimeoutSettings ( browserContext . _timeoutSettings ) ;
2019-12-11 12:36:42 -08:00
this . _screenshotter = new Screenshotter ( this ) ;
2019-12-16 15:56:11 -08:00
this . _frameManager = new frames . FrameManager ( this ) ;
2020-01-07 13:57:37 -08:00
if ( delegate . pdf )
this . pdf = delegate . pdf . bind ( delegate ) ;
2020-02-13 17:39:14 -08:00
this . coverage = delegate . coverage ? delegate . coverage ( ) : null ;
2019-12-05 14:11:48 -08:00
}
2024-12-14 20:15:58 +00:00
async reportAsNew ( opener : Page | undefined , error : Error | undefined = undefined , contextEvent : string = BrowserContext . Events . Page ) {
if ( opener ) {
const openerPageOrError = await opener . waitForInitializedOrError ( ) ;
if ( openerPageOrError instanceof Page && ! openerPageOrError . isClosed ( ) )
this . _opener = openerPageOrError ;
}
this . _markInitialized ( error , contextEvent ) ;
2021-04-02 11:15:07 -07:00
}
2024-12-14 20:15:58 +00:00
private _markInitialized ( error : Error | undefined = undefined , contextEvent : string = BrowserContext . Events . Page ) {
2021-03-30 17:35:42 -07:00
if ( error ) {
2020-11-12 12:41:23 -08:00
// Initialization error could have happened because of
// context/browser closure. Just ignore the page.
if ( this . _browserContext . isClosingOrClosed ( ) )
return ;
2024-12-11 23:07:03 -08:00
this . _frameManager . createDummyMainFrameIfNeeded ( ) ;
2020-11-12 12:41:23 -08:00
}
2024-12-14 20:15:58 +00:00
this . _initialized = error || this ;
2022-06-22 17:23:51 +02:00
this . emitOnContext ( contextEvent , this ) ;
2023-05-04 15:11:46 -07:00
for ( const { event , args } of this . _eventsToEmitAfterInitialized )
this . _browserContext . emit ( event , . . . args ) ;
this . _eventsToEmitAfterInitialized = [ ] ;
// It may happen that page initialization finishes after Close event has already been sent,
2021-04-02 11:15:07 -07:00
// in that case we fire another Close event to ensure that each reported Page will have
// corresponding Close event after it is reported on the context.
if ( this . isClosed ( ) )
this . emit ( Page . Events . Close ) ;
2023-05-04 15:11:46 -07:00
else
this . instrumentation . onPageOpen ( this ) ;
2024-12-14 20:15:58 +00:00
// Note: it is important to resolve _initializedPromise at the end,
// so that anyone who awaits waitForInitializedOrError got a ready and reported page.
this . _initializedPromise . resolve ( this . _initialized ) ;
2020-11-12 12:41:23 -08:00
}
2024-12-14 20:15:58 +00:00
initializedOrUndefined ( ) : Page | undefined {
2021-05-13 10:29:14 -07:00
return this . _initialized ? this : undefined ;
}
2024-12-14 20:15:58 +00:00
waitForInitializedOrError ( ) : Promise < Page | Error > {
return this . _initializedPromise ;
}
2022-03-17 17:27:33 -08:00
emitOnContext ( event : string | symbol , . . . args : any [ ] ) {
if ( this . _isServerSideOnly )
return ;
this . _browserContext . emit ( event , . . . args ) ;
}
2023-05-04 15:11:46 -07:00
emitOnContextOnceInitialized ( event : string | symbol , . . . args : any [ ] ) {
if ( this . _isServerSideOnly )
return ;
// Some events, like console messages, may come before page is ready.
// In this case, postpone the event until page is initialized,
// and dispatch it to the client later, either on the live Page,
// or on the "errored" Page.
if ( this . _initialized )
this . _browserContext . emit ( event , . . . args ) ;
else
this . _eventsToEmitAfterInitialized . push ( { event , args } ) ;
}
2022-07-11 12:10:08 -08:00
async resetForReuse ( metadata : CallMetadata ) {
this . setDefaultNavigationTimeout ( undefined ) ;
this . setDefaultTimeout ( undefined ) ;
2024-01-19 12:35:00 -08:00
this . _locatorHandlers . clear ( ) ;
2022-07-11 12:10:08 -08:00
2022-07-12 13:30:24 -08:00
await this . _removeExposedBindings ( ) ;
await this . _removeInitScripts ( ) ;
await this . setClientRequestInterceptor ( undefined ) ;
2022-07-11 12:10:08 -08:00
await this . _setServerRequestInterceptor ( undefined ) ;
await this . setFileChooserIntercepted ( false ) ;
2022-08-04 16:39:18 -07:00
// Re-navigate once init scripts are gone.
2022-07-11 12:10:08 -08:00
await this . mainFrame ( ) . goto ( metadata , 'about:blank' ) ;
this . _emulatedSize = undefined ;
this . _emulatedMedia = { } ;
this . _extraHTTPHeaders = undefined ;
this . _interceptFileChooser = false ;
2022-08-03 16:14:28 -07:00
await Promise . all ( [
2023-02-08 18:53:07 -08:00
this . _delegate . updateEmulatedViewportSize ( ) ,
2022-08-03 16:14:28 -07:00
this . _delegate . updateEmulateMedia ( ) ,
this . _delegate . updateFileChooserInterception ( ) ,
] ) ;
2023-05-12 13:21:49 -07:00
await this . _delegate . resetForReuse ( ) ;
2022-07-11 12:10:08 -08:00
}
2019-12-05 14:11:48 -08:00
_didClose() {
2020-07-10 16:38:01 -07:00
this . _frameManager . dispose ( ) ;
2022-10-21 14:30:14 -07:00
this . _frameThrottler . dispose ( ) ;
2020-06-29 16:26:32 -07:00
assert ( this . _closedState !== 'closed' , 'Page closed twice' ) ;
this . _closedState = 'closed' ;
2020-08-21 16:26:33 -07:00
this . emit ( Page . Events . Close ) ;
2021-08-29 11:21:06 -07:00
this . _closedPromise . resolve ( ) ;
2022-10-24 19:19:58 -04:00
this . instrumentation . onPageClose ( this ) ;
2023-10-17 15:35:41 -07:00
this . openScope . close ( new TargetClosedError ( ) ) ;
2019-11-18 18:18:28 -08:00
}
2020-01-03 11:10:10 -08:00
_didCrash() {
2020-07-10 16:38:01 -07:00
this . _frameManager . dispose ( ) ;
2022-10-21 14:30:14 -07:00
this . _frameThrottler . dispose ( ) ;
2020-08-21 16:26:33 -07:00
this . emit ( Page . Events . Crash ) ;
2023-09-27 14:09:56 -07:00
this . _crashed = true ;
2022-10-24 19:19:58 -04:00
this . instrumentation . onPageClose ( this ) ;
2023-10-17 15:35:41 -07:00
this . openScope . close ( new Error ( 'Page crashed' ) ) ;
2019-11-18 18:18:28 -08:00
}
2019-12-06 13:36:47 -08:00
async _onFileChooserOpened ( handle : dom.ElementHandle ) {
2021-02-05 11:30:44 -08:00
let multiple ;
try {
multiple = await handle . evaluate ( element = > ! ! ( element as HTMLInputElement ) . multiple ) ;
} catch ( e ) {
// Frame/context may be gone during async processing. Do not throw.
return ;
}
2020-08-21 16:26:33 -07:00
if ( ! this . listenerCount ( Page . Events . FileChooser ) ) {
2020-03-04 17:57:35 -08:00
handle . dispose ( ) ;
2019-11-18 18:18:28 -08:00
return ;
2019-12-06 13:36:47 -08:00
}
2020-04-16 10:25:28 -07:00
const fileChooser = new FileChooser ( this , handle , multiple ) ;
2020-08-21 16:26:33 -07:00
this . emit ( Page . Events . FileChooser , fileChooser ) ;
2019-11-18 18:18:28 -08:00
}
2020-02-10 10:41:45 -08:00
context ( ) : BrowserContext {
2019-12-05 14:11:48 -08:00
return this . _browserContext ;
2019-11-18 18:18:28 -08:00
}
2021-04-02 11:15:07 -07:00
opener ( ) : Page | undefined {
return this . _opener ;
2020-01-31 18:38:45 -08:00
}
2019-11-27 16:03:51 -08:00
mainFrame ( ) : frames . Frame {
2019-12-16 15:56:11 -08:00
return this . _frameManager . mainFrame ( ) ;
2019-11-18 18:18:28 -08:00
}
2019-11-27 16:03:51 -08:00
frames ( ) : frames . Frame [ ] {
2019-12-16 15:56:11 -08:00
return this . _frameManager . frames ( ) ;
2019-11-18 18:18:28 -08:00
}
2021-10-28 07:31:30 -08:00
setDefaultNavigationTimeout ( timeout : number | undefined ) {
2019-11-18 18:18:28 -08:00
this . _timeoutSettings . setDefaultNavigationTimeout ( timeout ) ;
}
2021-10-28 07:31:30 -08:00
setDefaultTimeout ( timeout : number | undefined ) {
2019-11-18 18:18:28 -08:00
this . _timeoutSettings . setDefaultTimeout ( timeout ) ;
}
2021-09-08 14:27:05 -07:00
async exposeBinding ( name : string , needsHandle : boolean , playwrightBinding : frames.FunctionWithSource ) {
if ( this . _pageBindings . has ( name ) )
2020-03-03 16:46:06 -08:00
throw new Error ( ` Function " ${ name } " has been already registered ` ) ;
2021-09-08 14:27:05 -07:00
if ( this . _browserContext . _pageBindings . has ( name ) )
2020-03-03 16:46:06 -08:00
throw new Error ( ` Function " ${ name } " has been already registered in the browser context ` ) ;
2021-09-08 14:27:05 -07:00
const binding = new PageBinding ( name , playwrightBinding , needsHandle ) ;
this . _pageBindings . set ( name , binding ) ;
2024-08-07 06:20:12 -07:00
await this . _delegate . addInitScript ( binding . initScript ) ;
await Promise . all ( this . frames ( ) . map ( frame = > frame . evaluateExpression ( binding . initScript . source ) . catch ( e = > { } ) ) ) ;
2019-11-18 18:18:28 -08:00
}
2022-07-12 13:30:24 -08:00
async _removeExposedBindings() {
2024-08-07 06:20:12 -07:00
for ( const [ key , binding ] of this . _pageBindings ) {
if ( ! binding . internal )
2022-05-09 06:44:20 -08:00
this . _pageBindings . delete ( key ) ;
}
2022-04-04 11:39:43 -08:00
}
2020-08-18 15:38:29 -07:00
setExtraHTTPHeaders ( headers : types.HeadersArray ) {
2022-07-07 15:28:20 -08:00
this . _extraHTTPHeaders = headers ;
2020-02-26 12:42:20 -08:00
return this . _delegate . updateExtraHTTPHeaders ( ) ;
2019-11-18 18:18:28 -08:00
}
2022-07-07 15:28:20 -08:00
extraHTTPHeaders ( ) : types . HeadersArray | undefined {
return this . _extraHTTPHeaders ;
}
2020-05-18 14:28:06 -07:00
async _onBindingCalled ( payload : string , context : dom.FrameExecutionContext ) {
2023-09-27 14:09:56 -07:00
if ( this . _closedState === 'closed' )
2020-07-14 13:34:49 -07:00
return ;
2020-03-03 16:46:06 -08:00
await PageBinding . dispatch ( this , payload , context ) ;
2019-11-18 18:18:28 -08:00
}
2020-06-25 18:01:18 -07:00
_addConsoleMessage ( type : string , args : js.JSHandle [ ] , location : types.ConsoleMessageLocation , text? : string ) {
2021-04-20 23:03:56 -07:00
const message = new ConsoleMessage ( this , type , text , args , location ) ;
2020-01-27 16:51:52 -08:00
const intercepted = this . _frameManager . interceptConsoleMessage ( message ) ;
2023-05-04 15:11:46 -07:00
if ( intercepted ) {
2019-11-18 18:18:28 -08:00
args . forEach ( arg = > arg . dispose ( ) ) ;
2023-05-04 15:11:46 -07:00
return ;
}
this . emitOnContextOnceInitialized ( BrowserContext . Events . Console , message ) ;
2019-11-18 18:18:28 -08:00
}
2021-02-09 14:44:48 -08:00
async reload ( metadata : CallMetadata , options : types.NavigateOptions ) : Promise < network.Response | null > {
2021-02-25 10:00:54 -08:00
const controller = new ProgressController ( metadata , this ) ;
2022-06-17 21:17:30 -07:00
return controller . run ( progress = > this . mainFrame ( ) . raceNavigationAction ( progress , options , async ( ) = > {
2021-01-22 15:58:53 -08:00
// Note: waitForNavigation may fail before we get response to reload(),
// so we should await it immediately.
const [ response ] = await Promise . all ( [
2022-08-12 13:48:47 -07:00
// Reload must be a new document, and should not be confused with a stray pushState.
this . mainFrame ( ) . _waitForNavigation ( progress , true /* requiresNewDocument */ , options ) ,
2021-01-22 15:58:53 -08:00
this . _delegate . reload ( ) ,
] ) ;
return response ;
2021-02-25 10:00:54 -08:00
} ) , this . _timeoutSettings . navigationTimeout ( options ) ) ;
2019-11-18 18:18:28 -08:00
}
2021-02-09 14:44:48 -08:00
async goBack ( metadata : CallMetadata , options : types.NavigateOptions ) : Promise < network.Response | null > {
2021-02-25 10:00:54 -08:00
const controller = new ProgressController ( metadata , this ) ;
2022-06-17 21:17:30 -07:00
return controller . run ( progress = > this . mainFrame ( ) . raceNavigationAction ( progress , options , async ( ) = > {
2021-01-22 15:58:53 -08:00
// Note: waitForNavigation may fail before we get response to goBack,
// so we should catch it immediately.
let error : Error | undefined ;
2022-08-12 13:48:47 -07:00
const waitPromise = this . mainFrame ( ) . _waitForNavigation ( progress , false /* requiresNewDocument */ , options ) . catch ( e = > {
2021-01-22 15:58:53 -08:00
error = e ;
return null ;
} ) ;
2020-09-14 16:43:17 -07:00
const result = await this . _delegate . goBack ( ) ;
2021-01-22 15:58:53 -08:00
if ( ! result )
2020-09-14 16:43:17 -07:00
return null ;
2021-01-22 15:58:53 -08:00
const response = await waitPromise ;
if ( error )
throw error ;
return response ;
2021-02-25 10:00:54 -08:00
} ) , this . _timeoutSettings . navigationTimeout ( options ) ) ;
2019-11-18 18:18:28 -08:00
}
2021-02-09 14:44:48 -08:00
async goForward ( metadata : CallMetadata , options : types.NavigateOptions ) : Promise < network.Response | null > {
2021-02-25 10:00:54 -08:00
const controller = new ProgressController ( metadata , this ) ;
2022-06-17 21:17:30 -07:00
return controller . run ( progress = > this . mainFrame ( ) . raceNavigationAction ( progress , options , async ( ) = > {
2021-01-22 15:58:53 -08:00
// Note: waitForNavigation may fail before we get response to goForward,
// so we should catch it immediately.
let error : Error | undefined ;
2022-08-12 13:48:47 -07:00
const waitPromise = this . mainFrame ( ) . _waitForNavigation ( progress , false /* requiresNewDocument */ , options ) . catch ( e = > {
2021-01-22 15:58:53 -08:00
error = e ;
return null ;
} ) ;
2020-09-14 16:43:17 -07:00
const result = await this . _delegate . goForward ( ) ;
2021-01-22 15:58:53 -08:00
if ( ! result )
2020-09-14 16:43:17 -07:00
return null ;
2021-01-22 15:58:53 -08:00
const response = await waitPromise ;
if ( error )
throw error ;
return response ;
2021-02-25 10:00:54 -08:00
} ) , this . _timeoutSettings . navigationTimeout ( options ) ) ;
2019-11-18 18:18:28 -08:00
}
2024-09-26 05:08:33 -07:00
requestGC ( ) : Promise < void > {
return this . _delegate . requestGC ( ) ;
2024-09-13 14:09:36 -07:00
}
2024-04-25 14:00:02 -07:00
registerLocatorHandler ( selector : string , noWaitAfter : boolean | undefined ) {
2024-01-19 12:35:00 -08:00
const uid = ++ this . _lastLocatorHandlerUid ;
2024-04-25 14:00:02 -07:00
this . _locatorHandlers . set ( uid , { selector , noWaitAfter } ) ;
2024-01-19 12:35:00 -08:00
return uid ;
}
2024-04-24 15:19:12 -07:00
resolveLocatorHandler ( uid : number , remove : boolean | undefined ) {
2024-01-19 12:35:00 -08:00
const handler = this . _locatorHandlers . get ( uid ) ;
2024-04-24 15:19:12 -07:00
if ( remove )
this . _locatorHandlers . delete ( uid ) ;
2024-01-19 12:35:00 -08:00
if ( handler ) {
handler . resolved ? . resolve ( ) ;
handler . resolved = undefined ;
}
}
2024-04-24 15:19:12 -07:00
unregisterLocatorHandler ( uid : number ) {
this . _locatorHandlers . delete ( uid ) ;
}
2024-10-04 07:25:18 -07:00
async performActionPreChecks ( progress : Progress ) {
await this . _performWaitForNavigationCheck ( progress ) ;
progress . throwIfAborted ( ) ;
await this . _performLocatorHandlersCheckpoint ( progress ) ;
progress . throwIfAborted ( ) ;
// Wait once again, just in case a locator handler caused a navigation.
await this . _performWaitForNavigationCheck ( progress ) ;
}
private async _performWaitForNavigationCheck ( progress : Progress ) {
if ( process . env . PLAYWRIGHT_SKIP_NAVIGATION_CHECK )
return ;
const mainFrame = this . _frameManager . mainFrame ( ) ;
if ( ! mainFrame || ! mainFrame . pendingDocument ( ) )
return ;
const url = mainFrame . pendingDocument ( ) ? . request ? . url ( ) ;
const toUrl = url ? ` " ${ trimStringWithEllipsis ( url , 200 ) } " ` : '' ;
progress . log ( ` waiting for ${ toUrl } navigation to finish... ` ) ;
await helper . waitForEvent ( progress , mainFrame , frames . Frame . Events . InternalNavigation , ( e : frames.NavigationEvent ) = > {
if ( ! e . isPublic )
return false ;
if ( ! e . error )
progress . log ( ` navigated to " ${ trimStringWithEllipsis ( mainFrame . url ( ) , 200 ) } " ` ) ;
return true ;
} ) . promise ;
}
private async _performLocatorHandlersCheckpoint ( progress : Progress ) {
2024-01-19 12:35:00 -08:00
// Do not run locator handlers from inside locator handler callbacks to avoid deadlocks.
if ( this . _locatorHandlerRunningCounter )
return ;
for ( const [ uid , handler ] of this . _locatorHandlers ) {
if ( ! handler . resolved ) {
if ( await this . mainFrame ( ) . isVisibleInternal ( handler . selector , { strict : true } ) ) {
handler . resolved = new ManualPromise ( ) ;
this . emit ( Page . Events . LocatorHandlerTriggered , uid ) ;
}
}
if ( handler . resolved ) {
++ this . _locatorHandlerRunningCounter ;
2024-02-08 07:39:05 -08:00
progress . log ( ` found ${ asLocator ( this . attribution . playwright . options . sdkLanguage , handler . selector ) } , intercepting action to run the handler ` ) ;
2024-04-24 15:19:12 -07:00
const promise = handler . resolved . then ( async ( ) = > {
progress . throwIfAborted ( ) ;
2024-04-25 14:00:02 -07:00
if ( ! handler . noWaitAfter ) {
progress . log ( ` locator handler has finished, waiting for ${ asLocator ( this . attribution . playwright . options . sdkLanguage , handler . selector ) } to be hidden ` ) ;
2024-09-18 09:34:06 -07:00
await this . mainFrame ( ) . waitForSelectorInternal ( progress , handler . selector , false , { state : 'hidden' } ) ;
2024-04-25 14:00:02 -07:00
} else {
progress . log ( ` locator handler has finished ` ) ;
}
2024-04-24 15:19:12 -07:00
} ) ;
await this . openScope . race ( promise ) . finally ( ( ) = > -- this . _locatorHandlerRunningCounter ) ;
2024-01-19 12:35:00 -08:00
// Avoid side-effects after long-running operation.
progress . throwIfAborted ( ) ;
2024-02-08 07:39:05 -08:00
progress . log ( ` interception handler has finished, continuing ` ) ;
2024-01-19 12:35:00 -08:00
}
}
}
2022-07-07 15:28:20 -08:00
async emulateMedia ( options : Partial < EmulatedMedia > ) {
2020-01-05 14:39:16 -08:00
if ( options . media !== undefined )
2022-07-07 15:28:20 -08:00
this . _emulatedMedia . media = options . media ;
2019-12-09 13:08:21 -08:00
if ( options . colorScheme !== undefined )
2022-07-07 15:28:20 -08:00
this . _emulatedMedia . colorScheme = options . colorScheme ;
2021-05-22 01:56:09 +02:00
if ( options . reducedMotion !== undefined )
2022-07-07 15:28:20 -08:00
this . _emulatedMedia . reducedMotion = options . reducedMotion ;
2021-09-03 21:48:06 +02:00
if ( options . forcedColors !== undefined )
2022-07-07 15:28:20 -08:00
this . _emulatedMedia . forcedColors = options . forcedColors ;
2025-02-04 11:15:51 +01:00
if ( options . contrast !== undefined )
this . _emulatedMedia . contrast = options . contrast ;
2022-07-12 13:30:24 -08:00
2020-04-06 19:49:33 -07:00
await this . _delegate . updateEmulateMedia ( ) ;
2019-11-18 18:18:28 -08:00
}
2022-07-07 15:28:20 -08:00
emulatedMedia ( ) : EmulatedMedia {
const contextOptions = this . _browserContext . _options ;
return {
2022-10-31 09:09:52 -07:00
media : this._emulatedMedia.media || 'no-override' ,
2022-07-07 15:28:20 -08:00
colorScheme : this._emulatedMedia.colorScheme !== undefined ? this . _emulatedMedia.colorScheme : contextOptions.colorScheme ? ? 'light' ,
reducedMotion : this._emulatedMedia.reducedMotion !== undefined ? this . _emulatedMedia.reducedMotion : contextOptions.reducedMotion ? ? 'no-preference' ,
forcedColors : this._emulatedMedia.forcedColors !== undefined ? this . _emulatedMedia.forcedColors : contextOptions.forcedColors ? ? 'none' ,
2025-02-04 11:15:51 +01:00
contrast : this._emulatedMedia.contrast !== undefined ? this . _emulatedMedia.contrast : contextOptions.contrast ? ? 'no-preference' ,
2022-07-07 15:28:20 -08:00
} ;
}
2020-02-06 19:02:55 -08:00
async setViewportSize ( viewportSize : types.Size ) {
2022-07-07 15:28:20 -08:00
this . _emulatedSize = { viewport : { . . . viewportSize } , screen : { . . . viewportSize } } ;
await this . _delegate . updateEmulatedViewportSize ( ) ;
2019-11-18 18:18:28 -08:00
}
2020-02-06 19:02:55 -08:00
viewportSize ( ) : types . Size | null {
2022-07-07 15:28:20 -08:00
return this . emulatedSize ( ) ? . viewport || null ;
}
emulatedSize ( ) : EmulatedSize | null {
if ( this . _emulatedSize )
return this . _emulatedSize ;
const contextOptions = this . _browserContext . _options ;
return contextOptions . viewport ? { viewport : contextOptions.viewport , screen : contextOptions.screen || contextOptions . viewport } : null ;
2019-11-18 18:18:28 -08:00
}
2020-07-21 09:36:54 -07:00
async bringToFront ( ) : Promise < void > {
await this . _delegate . bringToFront ( ) ;
}
2024-12-30 18:00:10 +00:00
async addInitScript ( source : string , name? : string ) {
const initScript = new InitScript ( source , false /* internal */ , name ) ;
2024-06-27 09:29:20 -07:00
this . initScripts . push ( initScript ) ;
await this . _delegate . addInitScript ( initScript ) ;
2019-11-18 18:18:28 -08:00
}
2022-07-12 13:30:24 -08:00
async _removeInitScripts() {
2024-08-07 06:20:12 -07:00
this . initScripts = this . initScripts . filter ( script = > script . internal ) ;
await this . _delegate . removeNonInternalInitScripts ( ) ;
2022-04-04 11:39:43 -08:00
}
2022-07-01 12:49:43 -07:00
needsRequestInterception ( ) : boolean {
2020-11-13 14:24:53 -08:00
return ! ! this . _clientRequestInterceptor || ! ! this . _serverRequestInterceptor || ! ! this . _browserContext . _requestInterceptor ;
2020-03-09 21:02:54 -07:00
}
2022-04-02 18:02:27 -08:00
async setClientRequestInterceptor ( handler : network.RouteHandler | undefined ) : Promise < void > {
2020-11-13 14:24:53 -08:00
this . _clientRequestInterceptor = handler ;
await this . _delegate . updateRequestInterception ( ) ;
}
async _setServerRequestInterceptor ( handler : network.RouteHandler | undefined ) : Promise < void > {
this . _serverRequestInterceptor = handler ;
2020-04-15 19:55:22 -07:00
await this . _delegate . updateRequestInterception ( ) ;
}
2022-02-28 13:25:59 -07:00
async expectScreenshot ( metadata : CallMetadata , options : ExpectScreenshotOptions = { } ) : Promise < { actual? : Buffer , previous? : Buffer , diff? : Buffer , errorMessage? : string , log? : string [ ] } > {
const locator = options . locator ;
const rafrafScreenshot = locator ? async ( progress : Progress , timeout : number ) = > {
2024-02-05 19:07:30 -08:00
return await locator . frame . rafrafTimeoutScreenshotElementWithProgress ( progress , locator . selector , timeout , options || { } ) ;
2022-02-28 13:25:59 -07:00
} : async ( progress : Progress , timeout : number ) = > {
2024-10-04 07:25:18 -07:00
await this . performActionPreChecks ( progress ) ;
2022-02-28 13:25:59 -07:00
await this . mainFrame ( ) . rafrafTimeout ( timeout ) ;
2024-02-05 19:07:30 -08:00
return await this . _screenshotter . screenshotPage ( progress , options || { } ) ;
2022-02-28 13:25:59 -07:00
} ;
2022-03-21 17:42:21 -06:00
const comparator = getComparator ( 'image/png' ) ;
2022-02-28 13:25:59 -07:00
const controller = new ProgressController ( metadata , this ) ;
2022-03-21 16:10:33 -06:00
if ( ! options . expected && options . isNot )
2022-02-28 13:25:59 -07:00
return { errorMessage : '"not" matcher requires expected result' } ;
2022-04-19 08:43:18 -06:00
try {
2024-02-05 19:07:30 -08:00
const format = validateScreenshotOptions ( options || { } ) ;
2022-04-19 08:43:18 -06:00
if ( format !== 'png' )
throw new Error ( 'Only PNG screenshots are supported' ) ;
} catch ( error ) {
return { errorMessage : error.message } ;
}
2022-02-28 13:25:59 -07:00
let intermediateResult : {
actual? : Buffer ,
previous? : Buffer ,
2022-03-21 16:10:33 -06:00
errorMessage : string ,
2022-02-28 13:25:59 -07:00
diff? : Buffer ,
} | undefined = undefined ;
2022-03-21 16:10:33 -06:00
const areEqualScreenshots = ( actual : Buffer | undefined , expected : Buffer | undefined , previous : Buffer | undefined ) = > {
2024-02-05 19:07:30 -08:00
const comparatorResult = actual && expected ? comparator ( actual , expected , options ) : undefined ;
2022-03-21 16:10:33 -06:00
if ( comparatorResult !== undefined && ! ! comparatorResult === ! ! options . isNot )
return true ;
if ( comparatorResult )
intermediateResult = { errorMessage : comparatorResult.errorMessage , diff : comparatorResult.diff , actual , previous } ;
return false ;
} ;
2022-03-11 23:40:28 -07:00
const callTimeout = this . _timeoutSettings . timeout ( options ) ;
2022-02-28 13:25:59 -07:00
return controller . run ( async progress = > {
let actual : Buffer | undefined ;
let previous : Buffer | undefined ;
2022-03-04 19:17:57 -07:00
const pollIntervals = [ 0 , 100 , 250 , 500 ] ;
2022-03-11 23:40:28 -07:00
progress . log ( ` ${ metadata . apiName } ${ callTimeout ? ` with timeout ${ callTimeout } ms ` : '' } ` ) ;
2022-03-21 16:10:33 -06:00
if ( options . expected )
progress . log ( ` verifying given screenshot expectation ` ) ;
2022-03-11 23:40:28 -07:00
else
2022-03-21 16:10:33 -06:00
progress . log ( ` generating new stable screenshot expectation ` ) ;
let isFirstIteration = true ;
2022-02-28 13:25:59 -07:00
while ( true ) {
progress . throwIfAborted ( ) ;
if ( this . isClosed ( ) )
throw new Error ( 'The page has closed' ) ;
2022-03-08 10:30:14 -07:00
const screenshotTimeout = pollIntervals . shift ( ) ? ? 1000 ;
2022-03-11 23:40:28 -07:00
if ( screenshotTimeout )
progress . log ( ` waiting ${ screenshotTimeout } ms before taking screenshot ` ) ;
2022-03-21 16:10:33 -06:00
previous = actual ;
actual = await rafrafScreenshot ( progress , screenshotTimeout ) . catch ( e = > {
progress . log ( ` failed to take screenshot - ` + e . message ) ;
return undefined ;
} ) ;
if ( ! actual )
continue ;
// Compare against expectation for the first iteration.
const expectation = options . expected && isFirstIteration ? options.expected : previous ;
if ( areEqualScreenshots ( actual , expectation , previous ) )
2022-02-28 13:25:59 -07:00
break ;
2022-03-21 16:10:33 -06:00
if ( intermediateResult )
progress . log ( intermediateResult . errorMessage ) ;
isFirstIteration = false ;
}
if ( ! isFirstIteration )
progress . log ( ` captured a stable screenshot ` ) ;
if ( ! options . expected )
return { actual } ;
if ( isFirstIteration ) {
progress . log ( ` screenshot matched expectation ` ) ;
return { } ;
2022-02-28 13:25:59 -07:00
}
2024-10-10 16:49:17 -07:00
if ( areEqualScreenshots ( actual , options . expected , undefined ) ) {
2022-03-21 16:10:33 -06:00
progress . log ( ` screenshot matched expectation ` ) ;
return { } ;
}
throw new Error ( intermediateResult ! . errorMessage ) ;
2022-03-11 23:40:28 -07:00
} , callTimeout ) . catch ( e = > {
2022-02-28 13:25:59 -07:00
// Q: Why not throw upon isSessionClosedError(e) as in other places?
// A: We want user to receive a friendly diff between actual and expected/previous.
if ( js . isJavaScriptErrorInEvaluate ( e ) || isInvalidSelectorError ( e ) )
throw e ;
2024-10-10 16:49:17 -07:00
let errorMessage = e . message ;
if ( e instanceof TimeoutError && intermediateResult ? . previous )
2024-10-25 12:36:39 -07:00
errorMessage = ` Failed to take two consecutive stable screenshots. ` ;
2022-02-28 13:25:59 -07:00
return {
2024-11-01 13:38:01 -07:00
log : compressCallLog ( e . message ? [ . . . metadata . log , e . message ] : metadata . log ) ,
2022-02-28 13:25:59 -07:00
. . . intermediateResult ,
2024-10-10 16:49:17 -07:00
errorMessage ,
2024-10-25 12:36:39 -07:00
timedOut : ( e instanceof TimeoutError ) ,
2022-02-28 13:25:59 -07:00
} ;
} ) ;
}
async screenshot ( metadata : CallMetadata , options : ScreenshotOptions & TimeoutOptions = { } ) : Promise < Buffer > {
2021-02-09 14:44:48 -08:00
const controller = new ProgressController ( metadata , this ) ;
return controller . run (
2020-06-25 16:57:21 -07:00
progress = > this . _screenshotter . screenshotPage ( progress , options ) ,
2020-08-14 18:25:32 -07:00
this . _timeoutSettings . timeout ( options ) ) ;
2019-11-18 18:18:28 -08:00
}
2023-10-16 20:32:13 -07:00
async close ( metadata : CallMetadata , options : { runBeforeUnload? : boolean , reason? : string } = { } ) {
2020-06-29 16:26:32 -07:00
if ( this . _closedState === 'closed' )
2019-12-18 16:23:05 -08:00
return ;
2023-10-16 20:32:13 -07:00
if ( options . reason )
this . _closeReason = options . reason ;
const runBeforeUnload = ! ! options . runBeforeUnload ;
2020-06-29 16:26:32 -07:00
if ( this . _closedState !== 'closing' ) {
2020-07-01 16:05:56 -07:00
this . _closedState = 'closing' ;
2020-11-16 10:26:34 -08:00
// This might throw if the browser context containing the page closes
// while we are trying to close the page.
await this . _delegate . closePage ( runBeforeUnload ) . catch ( e = > debugLogger . log ( 'error' , e ) ) ;
2020-06-29 16:26:32 -07:00
}
2019-12-09 13:08:21 -08:00
if ( ! runBeforeUnload )
2019-12-05 14:11:48 -08:00
await this . _closedPromise ;
2020-02-11 12:06:58 -08:00
if ( this . _ownedContext )
2023-10-16 20:32:13 -07:00
await this . _ownedContext . close ( options ) ;
2019-11-18 18:18:28 -08:00
}
isClosed ( ) : boolean {
2020-06-29 16:26:32 -07:00
return this . _closedState === 'closed' ;
2019-11-18 18:18:28 -08:00
}
2023-09-27 14:09:56 -07:00
hasCrashed() {
return this . _crashed ;
}
2022-12-19 15:54:53 -08:00
isClosedOrClosingOrCrashed() {
2023-09-27 14:09:56 -07:00
return this . _closedState !== 'open' || this . _crashed ;
2022-12-19 15:54:53 -08:00
}
2020-01-07 12:59:01 -08:00
_addWorker ( workerId : string , worker : Worker ) {
this . _workers . set ( workerId , worker ) ;
2020-08-21 16:26:33 -07:00
this . emit ( Page . Events . Worker , worker ) ;
2020-01-07 12:59:01 -08:00
}
_removeWorker ( workerId : string ) {
const worker = this . _workers . get ( workerId ) ;
if ( ! worker )
return ;
2021-11-03 10:44:50 -07:00
worker . didClose ( ) ;
2020-01-07 12:59:01 -08:00
this . _workers . delete ( workerId ) ;
}
_clearWorkers() {
2020-01-23 17:52:06 -08:00
for ( const [ workerId , worker ] of this . _workers ) {
2021-11-03 10:44:50 -07:00
worker . didClose ( ) ;
2020-01-23 17:52:06 -08:00
this . _workers . delete ( workerId ) ;
}
2020-01-07 12:59:01 -08:00
}
2020-01-30 17:43:06 -08:00
2022-04-02 18:02:27 -08:00
async setFileChooserIntercepted ( enabled : boolean ) : Promise < void > {
2022-07-07 15:28:20 -08:00
this . _interceptFileChooser = enabled ;
2022-07-11 12:10:08 -08:00
await this . _delegate . updateFileChooserInterception ( ) ;
2020-01-30 17:43:06 -08:00
}
2020-10-19 14:35:18 -07:00
2022-07-07 15:28:20 -08:00
fileChooserIntercepted() {
return this . _interceptFileChooser ;
}
2021-01-13 14:25:42 -08:00
frameNavigatedToNewDocument ( frame : frames.Frame ) {
this . emit ( Page . Events . InternalFrameNavigatedToNewDocument , frame ) ;
2024-03-12 19:20:35 -07:00
const origin = frame . origin ( ) ;
if ( origin )
this . _browserContext . addVisitedOrigin ( origin ) ;
2020-11-13 14:24:53 -08:00
}
2020-12-02 13:43:16 -08:00
2024-08-07 06:20:12 -07:00
allInitScripts() {
const bindings = [ . . . this . _browserContext . _pageBindings . values ( ) , . . . this . _pageBindings . values ( ) ] ;
return [ . . . bindings . map ( binding = > binding . initScript ) , . . . this . _browserContext . initScripts , . . . this . initScripts ] ;
2020-12-02 13:43:16 -08:00
}
2021-09-08 14:27:05 -07:00
getBinding ( name : string ) {
return this . _pageBindings . get ( name ) || this . _browserContext . _pageBindings . get ( name ) ;
2020-12-02 13:43:16 -08:00
}
2021-04-08 05:32:12 +08:00
2021-05-11 13:21:01 -07:00
setScreencastOptions ( options : { width : number , height : number , quality : number } | null ) {
this . _delegate . setScreencastOptions ( options ) . catch ( e = > debugLogger . log ( 'error' , e ) ) ;
2022-10-21 14:30:14 -07:00
this . _frameThrottler . setThrottlingEnabled ( ! ! options ) ;
2021-10-29 17:20:17 -08:00
}
throttleScreencastFrameAck ( ack : ( ) = > void ) {
// Don't ack immediately, tracing has smart throttling logic that is implemented here.
this . _frameThrottler . ack ( ack ) ;
}
2023-10-04 22:56:42 -04:00
temporarilyDisableTracingScreencastThrottling() {
2021-10-29 17:20:17 -08:00
this . _frameThrottler . recharge ( ) ;
2021-04-08 05:32:12 +08:00
}
2021-07-01 15:26:55 -07:00
2024-07-12 02:26:16 -07:00
async safeNonStallingEvaluateInAllFrames ( expression : string , world : types.World , options : { throwOnJSErrors? : boolean } = { } ) {
await Promise . all ( this . frames ( ) . map ( async frame = > {
try {
await frame . nonStallingEvaluateInExistingContext ( expression , world ) ;
} catch ( e ) {
if ( options . throwOnJSErrors && js . isJavaScriptErrorInEvaluate ( e ) )
throw e ;
}
} ) ) ;
}
2022-01-12 07:37:48 -08:00
async hideHighlight() {
await Promise . all ( this . frames ( ) . map ( frame = > frame . hideHighlight ( ) . catch ( ( ) = > { } ) ) ) ;
}
2022-03-17 17:27:33 -08:00
markAsServerSideOnly() {
this . _isServerSideOnly = true ;
}
2020-01-07 12:59:01 -08:00
}
2021-02-09 09:00:00 -08:00
export class Worker extends SdkObject {
2020-08-21 16:26:33 -07:00
static Events = {
Close : 'close' ,
} ;
2020-01-07 12:59:01 -08:00
private _url : string ;
private _executionContextPromise : Promise < js.ExecutionContext > ;
2021-02-03 13:49:25 -08:00
private _executionContextCallback : ( value : js.ExecutionContext ) = > void ;
2020-01-13 13:33:25 -08:00
_existingExecutionContext : js.ExecutionContext | null = null ;
2023-08-28 17:44:48 -07:00
readonly openScope = new LongStandingScope ( ) ;
2020-01-07 12:59:01 -08:00
2021-02-09 09:00:00 -08:00
constructor ( parent : SdkObject , url : string ) {
2021-04-20 23:03:56 -07:00
super ( parent , 'worker' ) ;
2020-01-07 12:59:01 -08:00
this . _url = url ;
2020-01-13 13:33:25 -08:00
this . _executionContextCallback = ( ) = > { } ;
2020-01-07 12:59:01 -08:00
this . _executionContextPromise = new Promise ( x = > this . _executionContextCallback = x ) ;
}
2020-01-13 09:14:28 -08:00
2020-01-07 12:59:01 -08:00
_createExecutionContext ( delegate : js.ExecutionContextDelegate ) {
2023-03-31 18:18:45 -07:00
this . _existingExecutionContext = new js . ExecutionContext ( this , delegate , 'worker' ) ;
2020-01-07 12:59:01 -08:00
this . _executionContextCallback ( this . _existingExecutionContext ) ;
}
url ( ) : string {
return this . _url ;
}
2021-11-03 10:44:50 -07:00
didClose() {
if ( this . _existingExecutionContext )
2023-07-23 21:00:07 -07:00
this . _existingExecutionContext . contextDestroyed ( 'Worker was closed' ) ;
2021-11-03 10:44:50 -07:00
this . emit ( Worker . Events . Close , this ) ;
2023-10-17 15:35:41 -07:00
this . openScope . close ( new Error ( 'Worker closed' ) ) ;
2021-11-03 10:44:50 -07:00
}
2021-03-18 03:03:21 +08:00
async evaluateExpression ( expression : string , isFunction : boolean | undefined , arg : any ) : Promise < any > {
2022-11-29 16:57:11 -08:00
return js . evaluateExpression ( await this . _executionContextPromise , expression , { returnByValue : true , isFunction } , arg ) ;
2020-06-30 10:55:11 -07:00
}
2021-03-18 03:03:21 +08:00
async evaluateExpressionHandle ( expression : string , isFunction : boolean | undefined , arg : any ) : Promise < any > {
2022-11-29 16:57:11 -08:00
return js . evaluateExpression ( await this . _executionContextPromise , expression , { returnByValue : false , isFunction } , arg ) ;
2020-06-30 10:55:11 -07:00
}
2019-11-18 18:18:28 -08:00
}
2020-03-03 16:46:06 -08:00
2022-05-09 14:07:04 -08:00
type BindingPayload = {
name : string ;
seq : number ;
serializedArgs? : SerializedValue [ ] ,
} ;
2020-03-03 16:46:06 -08:00
export class PageBinding {
2024-08-07 06:20:12 -07:00
static kPlaywrightBinding = '__playwright__binding__' ;
2020-03-03 16:46:06 -08:00
readonly name : string ;
2020-05-18 14:28:06 -07:00
readonly playwrightFunction : frames.FunctionWithSource ;
2024-08-07 06:20:12 -07:00
readonly initScript : InitScript ;
2020-10-01 22:47:31 -07:00
readonly needsHandle : boolean ;
2024-08-07 06:20:12 -07:00
readonly internal : boolean ;
2020-03-03 16:46:06 -08:00
2021-09-08 14:27:05 -07:00
constructor ( name : string , playwrightFunction : frames.FunctionWithSource , needsHandle : boolean ) {
2020-03-03 16:46:06 -08:00
this . name = name ;
this . playwrightFunction = playwrightFunction ;
2024-08-07 06:20:12 -07:00
this . initScript = new InitScript ( ` ( ${ addPageBinding . toString ( ) } )( ${ JSON . stringify ( PageBinding . kPlaywrightBinding ) } , ${ JSON . stringify ( name ) } , ${ needsHandle } , ( ${ source } )()) ` , true /* internal */ ) ;
2020-10-01 22:47:31 -07:00
this . needsHandle = needsHandle ;
2024-08-07 06:20:12 -07:00
this . internal = name . startsWith ( '__pw' ) ;
2020-03-03 16:46:06 -08:00
}
2020-05-18 14:28:06 -07:00
static async dispatch ( page : Page , payload : string , context : dom.FrameExecutionContext ) {
2022-05-09 14:07:04 -08:00
const { name , seq , serializedArgs } = JSON . parse ( payload ) as BindingPayload ;
2020-03-03 16:46:06 -08:00
try {
2020-12-02 13:43:16 -08:00
assert ( context . world ) ;
2024-08-07 06:20:12 -07:00
const binding = page . getBinding ( name ) ;
if ( ! binding )
throw new Error ( ` Function " ${ name } " is not exposed ` ) ;
2020-10-01 22:47:31 -07:00
let result : any ;
2020-12-02 13:43:16 -08:00
if ( binding . needsHandle ) {
2021-03-18 01:47:07 +08:00
const handle = await context . evaluateHandle ( takeHandle , { name , seq } ) . catch ( e = > null ) ;
2020-12-02 13:43:16 -08:00
result = await binding . playwrightFunction ( { frame : context.frame , page , context : page._browserContext } , handle ) ;
2020-10-01 22:47:31 -07:00
} else {
2024-09-10 12:14:24 +01:00
if ( ! Array . isArray ( serializedArgs ) )
throw new Error ( ` serializedArgs is not an array. This can happen when Array.prototype.toJSON is defined incorrectly ` ) ;
2022-05-09 17:51:53 -08:00
const args = serializedArgs ! . map ( a = > parseEvaluationResultValue ( a ) ) ;
2020-12-02 13:43:16 -08:00
result = await binding . playwrightFunction ( { frame : context.frame , page , context : page._browserContext } , . . . args ) ;
2020-10-01 22:47:31 -07:00
}
2021-03-18 01:47:07 +08:00
context . evaluate ( deliverResult , { name , seq , result } ) . catch ( e = > debugLogger . log ( 'error' , e ) ) ;
2020-03-03 16:46:06 -08:00
} catch ( error ) {
2024-08-09 03:46:52 -07:00
context . evaluate ( deliverResult , { name , seq , error } ) . catch ( e = > debugLogger . log ( 'error' , e ) ) ;
2020-03-03 16:46:06 -08:00
}
2020-10-01 22:47:31 -07:00
function takeHandle ( arg : { name : string , seq : number } ) {
2021-05-12 22:19:27 +00:00
const handle = ( globalThis as any ) [ arg . name ] [ 'handles' ] . get ( arg . seq ) ;
( globalThis as any ) [ arg . name ] [ 'handles' ] . delete ( arg . seq ) ;
2020-10-01 22:47:31 -07:00
return handle ;
}
2024-08-09 03:46:52 -07:00
function deliverResult ( arg : { name : string , seq : number , result? : any , error? : any } ) {
const callbacks = ( globalThis as any ) [ arg . name ] [ 'callbacks' ] ;
if ( 'error' in arg )
callbacks . get ( arg . seq ) . reject ( arg . error ) ;
else
callbacks . get ( arg . seq ) . resolve ( arg . result ) ;
callbacks . delete ( arg . seq ) ;
2020-03-03 16:46:06 -08:00
}
}
}
2024-08-07 06:20:12 -07:00
function addPageBinding ( playwrightBinding : string , bindingName : string , needsHandle : boolean , utilityScriptSerializers : ReturnType < typeof source > ) {
const binding = ( globalThis as any ) [ playwrightBinding ] ;
2021-05-12 22:19:27 +00:00
( globalThis as any ) [ bindingName ] = ( . . . args : any [ ] ) = > {
const me = ( globalThis as any ) [ bindingName ] ;
2020-10-01 22:47:31 -07:00
if ( needsHandle && args . slice ( 1 ) . some ( arg = > arg !== undefined ) )
throw new Error ( ` exposeBindingHandle supports a single argument, ${ args . length } received ` ) ;
2020-03-03 16:46:06 -08:00
let callbacks = me [ 'callbacks' ] ;
if ( ! callbacks ) {
callbacks = new Map ( ) ;
me [ 'callbacks' ] = callbacks ;
}
2022-05-09 14:07:04 -08:00
const seq : number = ( me [ 'lastSeq' ] || 0 ) + 1 ;
2020-03-03 16:46:06 -08:00
me [ 'lastSeq' ] = seq ;
2020-10-01 22:47:31 -07:00
let handles = me [ 'handles' ] ;
if ( ! handles ) {
handles = new Map ( ) ;
me [ 'handles' ] = handles ;
}
2021-09-27 18:58:08 +02:00
const promise = new Promise ( ( resolve , reject ) = > callbacks . set ( seq , { resolve , reject } ) ) ;
2022-05-09 14:07:04 -08:00
let payload : BindingPayload ;
2020-10-01 22:47:31 -07:00
if ( needsHandle ) {
handles . set ( seq , args [ 0 ] ) ;
2022-05-09 14:07:04 -08:00
payload = { name : bindingName , seq } ;
2020-10-01 22:47:31 -07:00
} else {
2023-04-21 19:52:13 +02:00
const serializedArgs = [ ] ;
for ( let i = 0 ; i < args . length ; i ++ ) {
serializedArgs [ i ] = utilityScriptSerializers . serializeAsCallArgument ( args [ i ] , v = > {
return { fallThrough : v } ;
} ) ;
}
2022-05-09 14:07:04 -08:00
payload = { name : bindingName , seq , serializedArgs } ;
2020-10-01 22:47:31 -07:00
}
2022-05-09 14:07:04 -08:00
binding ( JSON . stringify ( payload ) ) ;
2020-03-03 16:46:06 -08:00
return promise ;
} ;
2021-05-12 22:19:27 +00:00
( globalThis as any ) [ bindingName ] . __installed = true ;
2020-03-03 16:46:06 -08:00
}
2021-10-29 17:20:17 -08:00
2024-06-27 09:29:20 -07:00
export class InitScript {
readonly source : string ;
2024-08-07 06:20:12 -07:00
readonly internal : boolean ;
2024-12-30 18:00:10 +00:00
readonly name? : string ;
2024-06-27 09:29:20 -07:00
2024-12-30 18:00:10 +00:00
constructor ( source : string , internal? : boolean , name? : string ) {
2024-06-27 09:29:20 -07:00
const guid = createGuid ( ) ;
this . source = ` (() => {
globalThis . __pwInitScripts = globalThis . __pwInitScripts || { } ;
const hasInitScript = globalThis . __pwInitScripts [ $ { JSON . stringify ( guid ) } ] ;
if ( hasInitScript )
return ;
globalThis . __pwInitScripts [ $ { JSON . stringify ( guid ) } ] = true ;
$ { source }
} ) ( ) ; ` ;
2024-08-07 06:20:12 -07:00
this . internal = ! ! internal ;
2024-12-30 18:00:10 +00:00
this . name = name ;
2024-06-27 09:29:20 -07:00
}
}
2021-10-29 17:20:17 -08:00
class FrameThrottler {
private _acks : ( ( ) = > void ) [ ] = [ ] ;
2022-10-21 14:30:14 -07:00
private _defaultInterval : number ;
private _throttlingInterval : number ;
2021-10-29 17:20:17 -08:00
private _nonThrottledFrames : number ;
private _budget : number ;
2022-10-21 14:30:14 -07:00
private _throttlingEnabled = false ;
private _timeoutId : NodeJS.Timeout | undefined ;
2021-10-29 17:20:17 -08:00
2022-10-21 14:30:14 -07:00
constructor ( nonThrottledFrames : number , defaultInterval : number , throttlingInterval : number ) {
2021-10-29 17:20:17 -08:00
this . _nonThrottledFrames = nonThrottledFrames ;
this . _budget = nonThrottledFrames ;
2022-10-21 14:30:14 -07:00
this . _defaultInterval = defaultInterval ;
this . _throttlingInterval = throttlingInterval ;
this . _tick ( ) ;
2021-10-29 17:20:17 -08:00
}
2022-10-21 14:30:14 -07:00
dispose() {
if ( this . _timeoutId ) {
clearTimeout ( this . _timeoutId ) ;
this . _timeoutId = undefined ;
2021-10-29 17:20:17 -08:00
}
}
2022-10-21 14:30:14 -07:00
setThrottlingEnabled ( enabled : boolean ) {
this . _throttlingEnabled = enabled ;
}
2021-10-29 17:20:17 -08:00
recharge() {
// Send all acks, reset budget.
for ( const ack of this . _acks )
ack ( ) ;
this . _acks = [ ] ;
this . _budget = this . _nonThrottledFrames ;
2022-10-21 14:30:14 -07:00
if ( this . _timeoutId ) {
clearTimeout ( this . _timeoutId ) ;
this . _tick ( ) ;
}
2021-10-29 17:20:17 -08:00
}
ack ( ack : ( ) = > void ) {
2022-10-21 14:30:14 -07:00
if ( ! this . _timeoutId ) {
// Already disposed.
2021-10-29 17:20:17 -08:00
ack ( ) ;
return ;
}
this . _acks . push ( ack ) ;
}
private _tick() {
2022-10-21 14:30:14 -07:00
const ack = this . _acks . shift ( ) ;
if ( ack ) {
-- this . _budget ;
ack ( ) ;
}
if ( this . _throttlingEnabled && this . _budget <= 0 ) {
// Non-throttled frame budget is exceeded. Next ack will be throttled.
this . _timeoutId = setTimeout ( ( ) = > this . _tick ( ) , this . _throttlingInterval ) ;
} else {
// Either not throttling, or still under budget. Next ack will be after the default timeout.
this . _timeoutId = setTimeout ( ( ) = > this . _tick ( ) , this . _defaultInterval ) ;
}
2021-10-29 17:20:17 -08:00
}
}