2019-11-19 03:18:28 +01: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 .
* /
2022-04-06 23:57:14 +02:00
import type * as dom from './dom' ;
2019-12-09 22:08:21 +01:00
import * as frames from './frames' ;
import * as input from './input' ;
import * as js from './javascript' ;
2024-03-13 03:20:35 +01:00
import type * as network from './network' ;
2022-09-21 03:41:51 +02:00
import type * as channels from '@protocol/channels' ;
2022-04-06 23:57:14 +02:00
import type { ScreenshotOptions } from './screenshotter' ;
2022-04-19 16:43:18 +02:00
import { Screenshotter , validateScreenshotOptions } from './screenshotter' ;
2022-04-07 23:36:13 +02:00
import { TimeoutSettings } from '../common/timeoutSettings' ;
2022-04-06 23:57:14 +02:00
import type * as types from './types' ;
2021-03-31 19:38:05 +02:00
import { BrowserContext } from './browserContext' ;
2020-06-26 03:01:18 +02:00
import { ConsoleMessage } from './console' ;
2020-01-03 20:15:43 +01:00
import * as accessibility from './accessibility' ;
2020-04-16 19:25:28 +02:00
import { FileChooser } from './fileChooser' ;
2022-04-06 23:57:14 +02:00
import type { Progress } from './progress' ;
import { ProgressController } from './progress' ;
2023-07-24 06:00:07 +02:00
import { LongStandingScope , assert , isError } from '../utils' ;
2022-04-08 05:18:22 +02:00
import { ManualPromise } from '../utils/manualPromise' ;
2024-01-30 23:36:51 +01:00
import { debugLogger } from '../utils/debugLogger' ;
2022-04-06 23:57:14 +02:00
import type { ImageComparatorOptions } from '../utils/comparators' ;
import { getComparator } from '../utils/comparators' ;
import type { CallMetadata } from './instrumentation' ;
import { SdkObject } from './instrumentation' ;
import type { Artifact } from './artifact' ;
import type { TimeoutOptions } from '../common/types' ;
2023-03-07 03:49:14 +01:00
import { isInvalidSelectorError } from '../utils/isomorphic/selectorParser' ;
2022-05-10 00:07:04 +02:00
import { parseEvaluationResultValue , source } from './isomorphic/utilityScriptSerializers' ;
import type { SerializedValue } from './isomorphic/utilityScriptSerializers' ;
2023-10-18 06:34:02 +02:00
import { TargetClosedError } from './errors' ;
2024-02-08 16:39:05 +01:00
import { asLocator } from '../utils/isomorphic/locatorGenerators' ;
2019-12-09 22:08:21 +01:00
export interface PageDelegate {
readonly rawMouse : input.RawMouse ;
readonly rawKeyboard : input.RawKeyboard ;
2020-10-19 19:07:33 +02:00
readonly rawTouchscreen : input.RawTouchscreen ;
2019-12-11 21:36:42 +01:00
2019-12-17 20:28:09 +01:00
reload ( ) : Promise < void > ;
goBack ( ) : Promise < boolean > ;
goForward ( ) : Promise < boolean > ;
2020-03-04 01:46:06 +01:00
exposeBinding ( binding : PageBinding ) : Promise < void > ;
2022-04-04 21:39:43 +02:00
removeExposedBindings ( ) : Promise < void > ;
2022-04-03 04:02:27 +02:00
addInitScript ( source : string ) : Promise < void > ;
2022-04-04 21:39:43 +02:00
removeInitScripts ( ) : Promise < void > ;
2019-12-09 22:08:21 +01:00
closePage ( runBeforeUnload : boolean ) : Promise < void > ;
2022-03-18 02:27:33 +01:00
potentiallyUninitializedPage ( ) : Page ;
2020-11-12 21:41:23 +01:00
pageOrError ( ) : Promise < Page | Error > ;
2019-12-09 22:08:21 +01:00
2019-12-17 07:02:33 +01:00
navigateFrame ( frame : frames.Frame , url : string , referrer : string | undefined ) : Promise < frames.GotoResult > ;
2019-12-11 21:36:42 +01:00
2020-02-26 21:42:20 +01:00
updateExtraHTTPHeaders ( ) : Promise < void > ;
2022-08-04 01:14:28 +02:00
updateEmulatedViewportSize ( preserveWindowBoundaries? : boolean ) : Promise < void > ;
2020-04-07 04:49:33 +02:00
updateEmulateMedia ( ) : Promise < void > ;
2020-03-10 05:02:54 +01:00
updateRequestInterception ( ) : Promise < void > ;
2022-07-11 22:10:08 +02:00
updateFileChooserInterception ( ) : Promise < void > ;
2020-07-21 18:36:54 +02:00
bringToFront ( ) : Promise < void > ;
2019-12-11 21:36:42 +01:00
setBackgroundColor ( color ? : { r : number ; g : number ; b : number ; a : number ; } ) : Promise < void > ;
2022-04-01 21:28:40 +02: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-13 02:51:05 +01:00
isElementHandle ( remoteObject : any ) : boolean ;
2019-12-13 06:11:52 +01:00
adoptElementHandle < T extends Node > ( handle : dom.ElementHandle < T > , to : dom.FrameExecutionContext ) : Promise < dom.ElementHandle < T > > ;
2019-12-20 00:19:22 +01:00
getContentFrame ( handle : dom.ElementHandle ) : Promise < frames.Frame | null > ; // Only called for frame owner elements.
2020-01-27 20:43:43 +01:00
getOwnerFrame ( handle : dom.ElementHandle ) : Promise < string | null > ; // Returns frameId.
2019-12-13 02:51:05 +01:00
getContentQuads ( handle : dom.ElementHandle ) : Promise < types.Quad [ ] | null > ;
2022-03-24 15:46:37 +01:00
setInputFiles ( handle : dom.ElementHandle < HTMLInputElement > , files : types.FilePayload [ ] ) : Promise < void > ;
2024-02-29 23:44:45 +01:00
setInputFilePaths ( handle : dom.ElementHandle < HTMLInputElement > , files : string [ ] ) : Promise < void > ;
2019-12-13 02:51:05 +01:00
getBoundingBox ( handle : dom.ElementHandle ) : Promise < types.Rect | null > ;
2020-02-06 02:20:23 +01:00
getFrameElement ( frame : frames.Frame ) : Promise < dom.ElementHandle > ;
2020-06-25 00:12:17 +02:00
scrollRectIntoViewIfNeeded ( handle : dom.ElementHandle , rect? : types.Rect ) : Promise < 'error:notvisible' | 'error:notconnected' | 'done' > ;
2021-05-11 22:21:01 +02:00
setScreencastOptions ( options : { width : number , height : number , quality : number } | null ) : Promise < void > ;
2020-01-03 20:15:43 +01:00
2020-01-15 01:54:50 +01:00
getAccessibilityTree ( needle? : dom.ElementHandle ) : Promise < { tree : accessibility.AXNode , needle : accessibility.AXNode | null } > ;
2022-06-15 07:02:15 +02:00
pdf ? : ( options : channels.PagePdfParams ) = > Promise < Buffer > ;
2020-02-14 02:39:14 +01:00
coverage ? : ( ) = > any ;
2020-03-07 17:19:31 +01:00
2020-06-26 01:57:21 +02:00
// Work around WebKit's raf issues on Windows.
rafCountForStablePosition ( ) : number ;
2020-03-07 17:19:31 +01:00
// Work around Chrome's non-associated input and protocol.
inputActionEpilogue ( ) : Promise < void > ;
2020-04-17 17:51:54 +02:00
// Work around for asynchronously dispatched CSP errors in Firefox.
2024-03-13 12:22:40 +01:00
readonly cspErrorsAsynchronousForInlineScripts? : boolean ;
2023-05-12 22:21:49 +02:00
// Work around for mouse position in Firefox.
resetForReuse ( ) : Promise < void > ;
2023-12-12 03:20:24 +01:00
// WebKit hack.
shouldToggleStyleSheetToSyncAnimations ( ) : boolean ;
2019-12-09 22:08:21 +01:00
}
2022-07-08 01:28:20 +02:00
type EmulatedSize = { screen : types.Size , viewport : types.Size } ;
type EmulatedMedia = {
2022-10-31 17:09:52 +01:00
media : types.MediaType ;
colorScheme : types.ColorScheme ;
reducedMotion : types.ReducedMotion ;
forcedColors : types.ForcedColors ;
2019-12-09 22:08:21 +01:00
} ;
2024-02-06 04:07:30 +01:00
type ExpectScreenshotOptions = ImageComparatorOptions & ScreenshotOptions & {
2022-02-28 21:25:59 +01:00
timeout? : number ,
expected? : Buffer ,
isNot? : boolean ,
locator ? : {
frame : frames.Frame ,
selector : string ,
} ,
} ;
2021-02-09 18:00:00 +01:00
export class Page extends SdkObject {
2020-08-22 01:26:33 +02:00
static Events = {
Close : 'close' ,
Crash : 'crash' ,
Download : 'download' ,
FileChooser : 'filechooser' ,
FrameAttached : 'frameattached' ,
FrameDetached : 'framedetached' ,
2021-01-13 23:25:42 +01:00
InternalFrameNavigatedToNewDocument : 'internalframenavigatedtonewdocument' ,
2024-01-19 21:35:00 +01:00
LocatorHandlerTriggered : 'locatorhandlertriggered' ,
2021-04-07 23:32:12 +02:00
ScreencastFrame : 'screencastframe' ,
2021-03-31 19:38:05 +02:00
Video : 'video' ,
2020-10-27 06:20:43 +01:00
WebSocket : 'websocket' ,
2020-08-22 01:26:33 +02:00
Worker : 'worker' ,
} ;
2020-06-30 01:26:32 +02:00
private _closedState : 'open' | 'closing' | 'closed' = 'open' ;
2021-08-29 20:21:06 +02:00
private _closedPromise = new ManualPromise < void > ( ) ;
2021-05-13 19:29:14 +02:00
private _initialized = false ;
2023-05-05 00:11:46 +02:00
private _eventsToEmitAfterInitialized : { event : string | symbol , args : any [ ] } [ ] = [ ] ;
2023-09-27 23:09:56 +02:00
private _crashed = false ;
2023-08-29 02:44:48 +02:00
readonly openScope = new LongStandingScope ( ) ;
2020-08-19 19:31:59 +02:00
readonly _browserContext : BrowserContext ;
2019-12-06 22:36:47 +01:00
readonly keyboard : input.Keyboard ;
readonly mouse : input.Mouse ;
2020-10-19 19:07:33 +02:00
readonly touchscreen : input.Touchscreen ;
2019-12-09 22:08:21 +01:00
readonly _timeoutSettings : TimeoutSettings ;
readonly _delegate : PageDelegate ;
2022-07-08 01:28:20 +02:00
_emulatedSize : EmulatedSize | undefined ;
private _extraHTTPHeaders : types.HeadersArray | undefined ;
private _emulatedMedia : Partial < EmulatedMedia > = { } ;
private _interceptFileChooser = false ;
2020-12-02 22:43:16 +01:00
private readonly _pageBindings = new Map < string , PageBinding > ( ) ;
2022-04-03 04:02:27 +02:00
readonly initScripts : string [ ] = [ ] ;
2019-12-09 22:08:21 +01:00
readonly _screenshotter : Screenshotter ;
2019-12-17 00:56:11 +01:00
readonly _frameManager : frames.FrameManager ;
2020-01-03 20:15:43 +01:00
readonly accessibility : accessibility.Accessibility ;
2020-01-07 21:59:01 +01:00
private _workers = new Map < string , Worker > ( ) ;
2022-06-15 07:02:15 +02:00
readonly pdf : ( ( options : channels.PagePdfParams ) = > Promise < Buffer > ) | undefined ;
2020-02-14 02:39:14 +01:00
readonly coverage : any ;
2022-07-01 21:49:43 +02:00
_clientRequestInterceptor : network.RouteHandler | undefined ;
_serverRequestInterceptor : network.RouteHandler | undefined ;
2020-02-11 21:06:58 +01:00
_ownedContext : BrowserContext | undefined ;
2021-03-18 16:14:57 +01:00
_pageIsError : Error | undefined ;
2021-03-31 19:38:05 +02:00
_video : Artifact | null = null ;
2021-04-02 20:15:07 +02:00
_opener : Page | undefined ;
2022-03-18 02:27:33 +01:00
private _isServerSideOnly = false ;
2024-04-25 23:00:02 +02:00
private _locatorHandlers = new Map < number , { selector : string , noWaitAfter ? : boolean , resolved ? : ManualPromise < void > } > ( ) ;
2024-01-19 21:35:00 +01:00
private _lastLocatorHandlerUid = 0 ;
private _locatorHandlerRunningCounter = 0 ;
2019-11-19 03:18:28 +01:00
2022-10-21 23:30:14 +02: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-17 05:32:13 +02:00
_closeReason : string | undefined ;
2022-10-21 23:30:14 +02:00
2020-08-19 19:31:59 +02:00
constructor ( delegate : PageDelegate , browserContext : BrowserContext ) {
2021-04-21 08:03:56 +02:00
super ( browserContext , 'page' ) ;
2021-02-09 18:00:00 +01:00
this . attribution . page = this ;
2019-12-09 22:08:21 +01:00
this . _delegate = delegate ;
2019-12-05 23:11:48 +01:00
this . _browserContext = browserContext ;
2020-01-03 20:15:43 +01:00
this . accessibility = new accessibility . Accessibility ( delegate . getAccessibilityTree . bind ( delegate ) ) ;
2024-04-29 17:15:12 +02:00
this . keyboard = new input . Keyboard ( delegate . rawKeyboard ) ;
2020-08-19 04:13:40 +02:00
this . mouse = new input . Mouse ( delegate . rawMouse , this ) ;
2020-10-19 19:07:33 +02:00
this . touchscreen = new input . Touchscreen ( delegate . rawTouchscreen , this ) ;
2020-02-13 23:18:18 +01:00
this . _timeoutSettings = new TimeoutSettings ( browserContext . _timeoutSettings ) ;
2019-12-11 21:36:42 +01:00
this . _screenshotter = new Screenshotter ( this ) ;
2019-12-17 00:56:11 +01:00
this . _frameManager = new frames . FrameManager ( this ) ;
2020-01-07 22:57:37 +01:00
if ( delegate . pdf )
this . pdf = delegate . pdf . bind ( delegate ) ;
2020-02-14 02:39:14 +01:00
this . coverage = delegate . coverage ? delegate . coverage ( ) : null ;
2019-12-05 23:11:48 +01:00
}
2021-04-02 20:15:07 +02:00
async initOpener ( opener : PageDelegate | null ) {
if ( ! opener )
return ;
const openerPage = await opener . pageOrError ( ) ;
if ( openerPage instanceof Page && ! openerPage . isClosed ( ) )
this . _opener = openerPage ;
}
2022-06-22 17:23:51 +02:00
reportAsNew ( error : Error | undefined = undefined , contextEvent : string = BrowserContext . Events . Page ) {
2021-03-31 02:35:42 +02:00
if ( error ) {
2020-11-12 21:41:23 +01:00
// Initialization error could have happened because of
// context/browser closure. Just ignore the page.
if ( this . _browserContext . isClosingOrClosed ( ) )
return ;
2021-03-31 02:35:42 +02:00
this . _setIsError ( error ) ;
2020-11-12 21:41:23 +01:00
}
2021-05-13 19:29:14 +02:00
this . _initialized = true ;
2022-06-22 17:23:51 +02:00
this . emitOnContext ( contextEvent , this ) ;
2023-05-05 00:11:46 +02: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 20:15:07 +02: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-05 00:11:46 +02:00
else
this . instrumentation . onPageOpen ( this ) ;
2020-11-12 21:41:23 +01:00
}
2021-05-13 19:29:14 +02:00
initializedOrUndefined() {
return this . _initialized ? this : undefined ;
}
2022-03-18 02:27:33 +01:00
emitOnContext ( event : string | symbol , . . . args : any [ ] ) {
if ( this . _isServerSideOnly )
return ;
this . _browserContext . emit ( event , . . . args ) ;
}
2023-05-05 00:11:46 +02: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 22:10:08 +02:00
async resetForReuse ( metadata : CallMetadata ) {
this . setDefaultNavigationTimeout ( undefined ) ;
this . setDefaultTimeout ( undefined ) ;
2024-01-19 21:35:00 +01:00
this . _locatorHandlers . clear ( ) ;
2022-07-11 22:10:08 +02:00
2022-07-12 23:30:24 +02:00
await this . _removeExposedBindings ( ) ;
await this . _removeInitScripts ( ) ;
await this . setClientRequestInterceptor ( undefined ) ;
2022-07-11 22:10:08 +02:00
await this . _setServerRequestInterceptor ( undefined ) ;
await this . setFileChooserIntercepted ( false ) ;
2022-08-05 01:39:18 +02:00
// Re-navigate once init scripts are gone.
2022-07-11 22:10:08 +02:00
await this . mainFrame ( ) . goto ( metadata , 'about:blank' ) ;
this . _emulatedSize = undefined ;
this . _emulatedMedia = { } ;
this . _extraHTTPHeaders = undefined ;
this . _interceptFileChooser = false ;
2022-08-04 01:14:28 +02:00
await Promise . all ( [
2023-02-09 03:53:07 +01:00
this . _delegate . updateEmulatedViewportSize ( ) ,
2022-08-04 01:14:28 +02:00
this . _delegate . updateEmulateMedia ( ) ,
this . _delegate . updateFileChooserInterception ( ) ,
] ) ;
2023-05-12 22:21:49 +02:00
await this . _delegate . resetForReuse ( ) ;
2022-07-11 22:10:08 +02:00
}
2019-12-05 23:11:48 +01:00
_didClose() {
2020-07-11 01:38:01 +02:00
this . _frameManager . dispose ( ) ;
2022-10-21 23:30:14 +02:00
this . _frameThrottler . dispose ( ) ;
2020-06-30 01:26:32 +02:00
assert ( this . _closedState !== 'closed' , 'Page closed twice' ) ;
this . _closedState = 'closed' ;
2020-08-22 01:26:33 +02:00
this . emit ( Page . Events . Close ) ;
2021-08-29 20:21:06 +02:00
this . _closedPromise . resolve ( ) ;
2022-10-25 01:19:58 +02:00
this . instrumentation . onPageClose ( this ) ;
2023-10-18 00:35:41 +02:00
this . openScope . close ( new TargetClosedError ( ) ) ;
2019-11-19 03:18:28 +01:00
}
2020-01-03 20:10:10 +01:00
_didCrash() {
2020-07-11 01:38:01 +02:00
this . _frameManager . dispose ( ) ;
2022-10-21 23:30:14 +02:00
this . _frameThrottler . dispose ( ) ;
2020-08-22 01:26:33 +02:00
this . emit ( Page . Events . Crash ) ;
2023-09-27 23:09:56 +02:00
this . _crashed = true ;
2022-10-25 01:19:58 +02:00
this . instrumentation . onPageClose ( this ) ;
2023-10-18 00:35:41 +02:00
this . openScope . close ( new Error ( 'Page crashed' ) ) ;
2019-11-19 03:18:28 +01:00
}
2019-12-06 22:36:47 +01:00
async _onFileChooserOpened ( handle : dom.ElementHandle ) {
2021-02-05 20:30:44 +01: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-22 01:26:33 +02:00
if ( ! this . listenerCount ( Page . Events . FileChooser ) ) {
2020-03-05 02:57:35 +01:00
handle . dispose ( ) ;
2019-11-19 03:18:28 +01:00
return ;
2019-12-06 22:36:47 +01:00
}
2020-04-16 19:25:28 +02:00
const fileChooser = new FileChooser ( this , handle , multiple ) ;
2020-08-22 01:26:33 +02:00
this . emit ( Page . Events . FileChooser , fileChooser ) ;
2019-11-19 03:18:28 +01:00
}
2020-02-10 19:41:45 +01:00
context ( ) : BrowserContext {
2019-12-05 23:11:48 +01:00
return this . _browserContext ;
2019-11-19 03:18:28 +01:00
}
2021-04-02 20:15:07 +02:00
opener ( ) : Page | undefined {
return this . _opener ;
2020-02-01 03:38:45 +01:00
}
2019-11-28 01:03:51 +01:00
mainFrame ( ) : frames . Frame {
2019-12-17 00:56:11 +01:00
return this . _frameManager . mainFrame ( ) ;
2019-11-19 03:18:28 +01:00
}
2019-11-28 01:03:51 +01:00
frames ( ) : frames . Frame [ ] {
2019-12-17 00:56:11 +01:00
return this . _frameManager . frames ( ) ;
2019-11-19 03:18:28 +01:00
}
2021-10-28 17:31:30 +02:00
setDefaultNavigationTimeout ( timeout : number | undefined ) {
2019-11-19 03:18:28 +01:00
this . _timeoutSettings . setDefaultNavigationTimeout ( timeout ) ;
}
2021-10-28 17:31:30 +02:00
setDefaultTimeout ( timeout : number | undefined ) {
2019-11-19 03:18:28 +01:00
this . _timeoutSettings . setDefaultTimeout ( timeout ) ;
}
2021-09-08 23:27:05 +02:00
async exposeBinding ( name : string , needsHandle : boolean , playwrightBinding : frames.FunctionWithSource ) {
if ( this . _pageBindings . has ( name ) )
2020-03-04 01:46:06 +01:00
throw new Error ( ` Function " ${ name } " has been already registered ` ) ;
2021-09-08 23:27:05 +02:00
if ( this . _browserContext . _pageBindings . has ( name ) )
2020-03-04 01:46:06 +01:00
throw new Error ( ` Function " ${ name } " has been already registered in the browser context ` ) ;
2021-09-08 23:27:05 +02:00
const binding = new PageBinding ( name , playwrightBinding , needsHandle ) ;
this . _pageBindings . set ( name , binding ) ;
2020-03-04 01:46:06 +01:00
await this . _delegate . exposeBinding ( binding ) ;
2019-11-19 03:18:28 +01:00
}
2022-07-12 23:30:24 +02:00
async _removeExposedBindings() {
2022-05-09 16:44:20 +02:00
for ( const key of this . _pageBindings . keys ( ) ) {
if ( ! key . startsWith ( '__pw' ) )
this . _pageBindings . delete ( key ) ;
}
2022-04-04 21:39:43 +02:00
await this . _delegate . removeExposedBindings ( ) ;
}
2020-08-19 00:38:29 +02:00
setExtraHTTPHeaders ( headers : types.HeadersArray ) {
2022-07-08 01:28:20 +02:00
this . _extraHTTPHeaders = headers ;
2020-02-26 21:42:20 +01:00
return this . _delegate . updateExtraHTTPHeaders ( ) ;
2019-11-19 03:18:28 +01:00
}
2022-07-08 01:28:20 +02:00
extraHTTPHeaders ( ) : types . HeadersArray | undefined {
return this . _extraHTTPHeaders ;
}
2020-05-18 23:28:06 +02:00
async _onBindingCalled ( payload : string , context : dom.FrameExecutionContext ) {
2023-09-27 23:09:56 +02:00
if ( this . _closedState === 'closed' )
2020-07-14 22:34:49 +02:00
return ;
2020-03-04 01:46:06 +01:00
await PageBinding . dispatch ( this , payload , context ) ;
2019-11-19 03:18:28 +01:00
}
2020-06-26 03:01:18 +02:00
_addConsoleMessage ( type : string , args : js.JSHandle [ ] , location : types.ConsoleMessageLocation , text? : string ) {
2021-04-21 08:03:56 +02:00
const message = new ConsoleMessage ( this , type , text , args , location ) ;
2020-01-28 01:51:52 +01:00
const intercepted = this . _frameManager . interceptConsoleMessage ( message ) ;
2023-05-05 00:11:46 +02:00
if ( intercepted ) {
2019-11-19 03:18:28 +01:00
args . forEach ( arg = > arg . dispose ( ) ) ;
2023-05-05 00:11:46 +02:00
return ;
}
this . emitOnContextOnceInitialized ( BrowserContext . Events . Console , message ) ;
2019-11-19 03:18:28 +01:00
}
2021-02-09 23:44:48 +01:00
async reload ( metadata : CallMetadata , options : types.NavigateOptions ) : Promise < network.Response | null > {
2021-02-25 19:00:54 +01:00
const controller = new ProgressController ( metadata , this ) ;
2022-06-18 06:17:30 +02:00
return controller . run ( progress = > this . mainFrame ( ) . raceNavigationAction ( progress , options , async ( ) = > {
2021-01-23 00:58:53 +01: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 22:48:47 +02: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-23 00:58:53 +01:00
this . _delegate . reload ( ) ,
] ) ;
return response ;
2021-02-25 19:00:54 +01:00
} ) , this . _timeoutSettings . navigationTimeout ( options ) ) ;
2019-11-19 03:18:28 +01:00
}
2021-02-09 23:44:48 +01:00
async goBack ( metadata : CallMetadata , options : types.NavigateOptions ) : Promise < network.Response | null > {
2021-02-25 19:00:54 +01:00
const controller = new ProgressController ( metadata , this ) ;
2022-06-18 06:17:30 +02:00
return controller . run ( progress = > this . mainFrame ( ) . raceNavigationAction ( progress , options , async ( ) = > {
2021-01-23 00:58:53 +01:00
// Note: waitForNavigation may fail before we get response to goBack,
// so we should catch it immediately.
let error : Error | undefined ;
2022-08-12 22:48:47 +02:00
const waitPromise = this . mainFrame ( ) . _waitForNavigation ( progress , false /* requiresNewDocument */ , options ) . catch ( e = > {
2021-01-23 00:58:53 +01:00
error = e ;
return null ;
} ) ;
2020-09-15 01:43:17 +02:00
const result = await this . _delegate . goBack ( ) ;
2021-01-23 00:58:53 +01:00
if ( ! result )
2020-09-15 01:43:17 +02:00
return null ;
2021-01-23 00:58:53 +01:00
const response = await waitPromise ;
if ( error )
throw error ;
return response ;
2021-02-25 19:00:54 +01:00
} ) , this . _timeoutSettings . navigationTimeout ( options ) ) ;
2019-11-19 03:18:28 +01:00
}
2021-02-09 23:44:48 +01:00
async goForward ( metadata : CallMetadata , options : types.NavigateOptions ) : Promise < network.Response | null > {
2021-02-25 19:00:54 +01:00
const controller = new ProgressController ( metadata , this ) ;
2022-06-18 06:17:30 +02:00
return controller . run ( progress = > this . mainFrame ( ) . raceNavigationAction ( progress , options , async ( ) = > {
2021-01-23 00:58:53 +01:00
// Note: waitForNavigation may fail before we get response to goForward,
// so we should catch it immediately.
let error : Error | undefined ;
2022-08-12 22:48:47 +02:00
const waitPromise = this . mainFrame ( ) . _waitForNavigation ( progress , false /* requiresNewDocument */ , options ) . catch ( e = > {
2021-01-23 00:58:53 +01:00
error = e ;
return null ;
} ) ;
2020-09-15 01:43:17 +02:00
const result = await this . _delegate . goForward ( ) ;
2021-01-23 00:58:53 +01:00
if ( ! result )
2020-09-15 01:43:17 +02:00
return null ;
2021-01-23 00:58:53 +01:00
const response = await waitPromise ;
if ( error )
throw error ;
return response ;
2021-02-25 19:00:54 +01:00
} ) , this . _timeoutSettings . navigationTimeout ( options ) ) ;
2019-11-19 03:18:28 +01:00
}
2024-04-25 23:00:02 +02:00
registerLocatorHandler ( selector : string , noWaitAfter : boolean | undefined ) {
2024-01-19 21:35:00 +01:00
const uid = ++ this . _lastLocatorHandlerUid ;
2024-04-25 23:00:02 +02:00
this . _locatorHandlers . set ( uid , { selector , noWaitAfter } ) ;
2024-01-19 21:35:00 +01:00
return uid ;
}
2024-04-25 00:19:12 +02:00
resolveLocatorHandler ( uid : number , remove : boolean | undefined ) {
2024-01-19 21:35:00 +01:00
const handler = this . _locatorHandlers . get ( uid ) ;
2024-04-25 00:19:12 +02:00
if ( remove )
this . _locatorHandlers . delete ( uid ) ;
2024-01-19 21:35:00 +01:00
if ( handler ) {
handler . resolved ? . resolve ( ) ;
handler . resolved = undefined ;
}
}
2024-04-25 00:19:12 +02:00
unregisterLocatorHandler ( uid : number ) {
this . _locatorHandlers . delete ( uid ) ;
}
2024-01-19 21:35:00 +01:00
async performLocatorHandlersCheckpoint ( progress : Progress ) {
// 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 16:39:05 +01:00
progress . log ( ` found ${ asLocator ( this . attribution . playwright . options . sdkLanguage , handler . selector ) } , intercepting action to run the handler ` ) ;
2024-04-25 00:19:12 +02:00
const promise = handler . resolved . then ( async ( ) = > {
progress . throwIfAborted ( ) ;
2024-04-25 23:00:02 +02:00
if ( ! handler . noWaitAfter ) {
progress . log ( ` locator handler has finished, waiting for ${ asLocator ( this . attribution . playwright . options . sdkLanguage , handler . selector ) } to be hidden ` ) ;
2024-04-25 00:19:12 +02:00
await this . mainFrame ( ) . waitForSelectorInternal ( progress , handler . selector , { state : 'hidden' } ) ;
2024-04-25 23:00:02 +02:00
} else {
progress . log ( ` locator handler has finished ` ) ;
}
2024-04-25 00:19:12 +02:00
} ) ;
await this . openScope . race ( promise ) . finally ( ( ) = > -- this . _locatorHandlerRunningCounter ) ;
2024-01-19 21:35:00 +01:00
// Avoid side-effects after long-running operation.
progress . throwIfAborted ( ) ;
2024-02-08 16:39:05 +01:00
progress . log ( ` interception handler has finished, continuing ` ) ;
2024-01-19 21:35:00 +01:00
}
}
}
2022-07-08 01:28:20 +02:00
async emulateMedia ( options : Partial < EmulatedMedia > ) {
2020-01-05 23:39:16 +01:00
if ( options . media !== undefined )
2022-07-08 01:28:20 +02:00
this . _emulatedMedia . media = options . media ;
2019-12-09 22:08:21 +01:00
if ( options . colorScheme !== undefined )
2022-07-08 01:28:20 +02:00
this . _emulatedMedia . colorScheme = options . colorScheme ;
2021-05-22 01:56:09 +02:00
if ( options . reducedMotion !== undefined )
2022-07-08 01:28:20 +02:00
this . _emulatedMedia . reducedMotion = options . reducedMotion ;
2021-09-03 21:48:06 +02:00
if ( options . forcedColors !== undefined )
2022-07-08 01:28:20 +02:00
this . _emulatedMedia . forcedColors = options . forcedColors ;
2022-07-12 23:30:24 +02:00
2020-04-07 04:49:33 +02:00
await this . _delegate . updateEmulateMedia ( ) ;
2019-11-19 03:18:28 +01:00
}
2022-07-08 01:28:20 +02:00
emulatedMedia ( ) : EmulatedMedia {
const contextOptions = this . _browserContext . _options ;
return {
2022-10-31 17:09:52 +01:00
media : this._emulatedMedia.media || 'no-override' ,
2022-07-08 01:28:20 +02: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' ,
} ;
}
2020-02-07 04:02:55 +01:00
async setViewportSize ( viewportSize : types.Size ) {
2022-07-08 01:28:20 +02:00
this . _emulatedSize = { viewport : { . . . viewportSize } , screen : { . . . viewportSize } } ;
await this . _delegate . updateEmulatedViewportSize ( ) ;
2019-11-19 03:18:28 +01:00
}
2020-02-07 04:02:55 +01:00
viewportSize ( ) : types . Size | null {
2022-07-08 01:28:20 +02: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-19 03:18:28 +01:00
}
2020-07-21 18:36:54 +02:00
async bringToFront ( ) : Promise < void > {
await this . _delegate . bringToFront ( ) ;
}
2022-04-03 04:02:27 +02:00
async addInitScript ( source : string ) {
this . initScripts . push ( source ) ;
await this . _delegate . addInitScript ( source ) ;
2019-11-19 03:18:28 +01:00
}
2022-07-12 23:30:24 +02:00
async _removeInitScripts() {
2022-04-04 21:39:43 +02:00
this . initScripts . splice ( 0 , this . initScripts . length ) ;
await this . _delegate . removeInitScripts ( ) ;
}
2022-07-01 21:49:43 +02:00
needsRequestInterception ( ) : boolean {
2020-11-13 23:24:53 +01:00
return ! ! this . _clientRequestInterceptor || ! ! this . _serverRequestInterceptor || ! ! this . _browserContext . _requestInterceptor ;
2020-03-10 05:02:54 +01:00
}
2022-04-03 04:02:27 +02:00
async setClientRequestInterceptor ( handler : network.RouteHandler | undefined ) : Promise < void > {
2020-11-13 23:24:53 +01:00
this . _clientRequestInterceptor = handler ;
await this . _delegate . updateRequestInterception ( ) ;
}
async _setServerRequestInterceptor ( handler : network.RouteHandler | undefined ) : Promise < void > {
this . _serverRequestInterceptor = handler ;
2020-04-16 04:55:22 +02:00
await this . _delegate . updateRequestInterception ( ) ;
}
2022-02-28 21:25:59 +01: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-06 04:07:30 +01:00
return await locator . frame . rafrafTimeoutScreenshotElementWithProgress ( progress , locator . selector , timeout , options || { } ) ;
2022-02-28 21:25:59 +01:00
} : async ( progress : Progress , timeout : number ) = > {
2024-01-19 21:35:00 +01:00
await this . performLocatorHandlersCheckpoint ( progress ) ;
2022-02-28 21:25:59 +01:00
await this . mainFrame ( ) . rafrafTimeout ( timeout ) ;
2024-02-06 04:07:30 +01:00
return await this . _screenshotter . screenshotPage ( progress , options || { } ) ;
2022-02-28 21:25:59 +01:00
} ;
2022-03-22 00:42:21 +01:00
const comparator = getComparator ( 'image/png' ) ;
2022-02-28 21:25:59 +01:00
const controller = new ProgressController ( metadata , this ) ;
2022-03-21 23:10:33 +01:00
if ( ! options . expected && options . isNot )
2022-02-28 21:25:59 +01:00
return { errorMessage : '"not" matcher requires expected result' } ;
2022-04-19 16:43:18 +02:00
try {
2024-02-06 04:07:30 +01:00
const format = validateScreenshotOptions ( options || { } ) ;
2022-04-19 16:43:18 +02:00
if ( format !== 'png' )
throw new Error ( 'Only PNG screenshots are supported' ) ;
} catch ( error ) {
return { errorMessage : error.message } ;
}
2022-02-28 21:25:59 +01:00
let intermediateResult : {
actual? : Buffer ,
previous? : Buffer ,
2022-03-21 23:10:33 +01:00
errorMessage : string ,
2022-02-28 21:25:59 +01:00
diff? : Buffer ,
} | undefined = undefined ;
2022-03-21 23:10:33 +01:00
const areEqualScreenshots = ( actual : Buffer | undefined , expected : Buffer | undefined , previous : Buffer | undefined ) = > {
2024-02-06 04:07:30 +01:00
const comparatorResult = actual && expected ? comparator ( actual , expected , options ) : undefined ;
2022-03-21 23:10:33 +01:00
if ( comparatorResult !== undefined && ! ! comparatorResult === ! ! options . isNot )
return true ;
if ( comparatorResult )
intermediateResult = { errorMessage : comparatorResult.errorMessage , diff : comparatorResult.diff , actual , previous } ;
return false ;
} ;
2022-03-12 07:40:28 +01:00
const callTimeout = this . _timeoutSettings . timeout ( options ) ;
2022-02-28 21:25:59 +01:00
return controller . run ( async progress = > {
let actual : Buffer | undefined ;
let previous : Buffer | undefined ;
2022-03-05 03:17:57 +01:00
const pollIntervals = [ 0 , 100 , 250 , 500 ] ;
2022-03-12 07:40:28 +01:00
progress . log ( ` ${ metadata . apiName } ${ callTimeout ? ` with timeout ${ callTimeout } ms ` : '' } ` ) ;
2022-03-21 23:10:33 +01:00
if ( options . expected )
progress . log ( ` verifying given screenshot expectation ` ) ;
2022-03-12 07:40:28 +01:00
else
2022-03-21 23:10:33 +01:00
progress . log ( ` generating new stable screenshot expectation ` ) ;
let isFirstIteration = true ;
2022-02-28 21:25:59 +01:00
while ( true ) {
progress . throwIfAborted ( ) ;
if ( this . isClosed ( ) )
throw new Error ( 'The page has closed' ) ;
2022-03-08 18:30:14 +01:00
const screenshotTimeout = pollIntervals . shift ( ) ? ? 1000 ;
2022-03-12 07:40:28 +01:00
if ( screenshotTimeout )
progress . log ( ` waiting ${ screenshotTimeout } ms before taking screenshot ` ) ;
2022-03-21 23:10:33 +01: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 21:25:59 +01:00
break ;
2022-03-21 23:10:33 +01: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 21:25:59 +01:00
}
2022-03-21 23:10:33 +01:00
if ( areEqualScreenshots ( actual , options . expected , previous ) ) {
progress . log ( ` screenshot matched expectation ` ) ;
return { } ;
}
throw new Error ( intermediateResult ! . errorMessage ) ;
2022-03-12 07:40:28 +01:00
} , callTimeout ) . catch ( e = > {
2022-02-28 21:25:59 +01: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 ;
return {
2022-03-12 07:40:28 +01:00
log : e.message ? [ . . . metadata . log , e . message ] : metadata . log ,
2022-02-28 21:25:59 +01:00
. . . intermediateResult ,
2022-04-19 16:43:18 +02:00
errorMessage : e.message ,
2022-02-28 21:25:59 +01:00
} ;
} ) ;
}
async screenshot ( metadata : CallMetadata , options : ScreenshotOptions & TimeoutOptions = { } ) : Promise < Buffer > {
2021-02-09 23:44:48 +01:00
const controller = new ProgressController ( metadata , this ) ;
return controller . run (
2020-06-26 01:57:21 +02:00
progress = > this . _screenshotter . screenshotPage ( progress , options ) ,
2020-08-15 03:25:32 +02:00
this . _timeoutSettings . timeout ( options ) ) ;
2019-11-19 03:18:28 +01:00
}
2023-10-17 05:32:13 +02:00
async close ( metadata : CallMetadata , options : { runBeforeUnload? : boolean , reason? : string } = { } ) {
2020-06-30 01:26:32 +02:00
if ( this . _closedState === 'closed' )
2019-12-19 01:23:05 +01:00
return ;
2023-10-17 05:32:13 +02:00
if ( options . reason )
this . _closeReason = options . reason ;
const runBeforeUnload = ! ! options . runBeforeUnload ;
2020-06-30 01:26:32 +02:00
if ( this . _closedState !== 'closing' ) {
2020-07-02 01:05:56 +02:00
this . _closedState = 'closing' ;
2020-11-16 19:26:34 +01: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-30 01:26:32 +02:00
}
2019-12-09 22:08:21 +01:00
if ( ! runBeforeUnload )
2019-12-05 23:11:48 +01:00
await this . _closedPromise ;
2020-02-11 21:06:58 +01:00
if ( this . _ownedContext )
2023-10-17 05:32:13 +02:00
await this . _ownedContext . close ( options ) ;
2019-11-19 03:18:28 +01:00
}
2021-03-18 16:14:57 +01:00
private _setIsError ( error : Error ) {
this . _pageIsError = error ;
2022-05-04 21:52:50 +02:00
this . _frameManager . createDummyMainFrameIfNeeded ( ) ;
2020-07-10 22:15:39 +02:00
}
2019-11-19 03:18:28 +01:00
isClosed ( ) : boolean {
2020-06-30 01:26:32 +02:00
return this . _closedState === 'closed' ;
2019-11-19 03:18:28 +01:00
}
2023-09-27 23:09:56 +02:00
hasCrashed() {
return this . _crashed ;
}
2022-12-20 00:54:53 +01:00
isClosedOrClosingOrCrashed() {
2023-09-27 23:09:56 +02:00
return this . _closedState !== 'open' || this . _crashed ;
2022-12-20 00:54:53 +01:00
}
2020-01-07 21:59:01 +01:00
_addWorker ( workerId : string , worker : Worker ) {
this . _workers . set ( workerId , worker ) ;
2020-08-22 01:26:33 +02:00
this . emit ( Page . Events . Worker , worker ) ;
2020-01-07 21:59:01 +01:00
}
_removeWorker ( workerId : string ) {
const worker = this . _workers . get ( workerId ) ;
if ( ! worker )
return ;
2021-11-03 18:44:50 +01:00
worker . didClose ( ) ;
2020-01-07 21:59:01 +01:00
this . _workers . delete ( workerId ) ;
}
_clearWorkers() {
2020-01-24 02:52:06 +01:00
for ( const [ workerId , worker ] of this . _workers ) {
2021-11-03 18:44:50 +01:00
worker . didClose ( ) ;
2020-01-24 02:52:06 +01:00
this . _workers . delete ( workerId ) ;
}
2020-01-07 21:59:01 +01:00
}
2020-01-31 02:43:06 +01:00
2022-04-03 04:02:27 +02:00
async setFileChooserIntercepted ( enabled : boolean ) : Promise < void > {
2022-07-08 01:28:20 +02:00
this . _interceptFileChooser = enabled ;
2022-07-11 22:10:08 +02:00
await this . _delegate . updateFileChooserInterception ( ) ;
2020-01-31 02:43:06 +01:00
}
2020-10-19 23:35:18 +02:00
2022-07-08 01:28:20 +02:00
fileChooserIntercepted() {
return this . _interceptFileChooser ;
}
2021-01-13 23:25:42 +01:00
frameNavigatedToNewDocument ( frame : frames.Frame ) {
this . emit ( Page . Events . InternalFrameNavigatedToNewDocument , frame ) ;
2024-03-13 03:20:35 +01:00
const origin = frame . origin ( ) ;
if ( origin )
this . _browserContext . addVisitedOrigin ( origin ) ;
2020-11-13 23:24:53 +01:00
}
2020-12-02 22:43:16 +01:00
allBindings() {
return [ . . . this . _browserContext . _pageBindings . values ( ) , . . . this . _pageBindings . values ( ) ] ;
}
2021-09-08 23:27:05 +02:00
getBinding ( name : string ) {
return this . _pageBindings . get ( name ) || this . _browserContext . _pageBindings . get ( name ) ;
2020-12-02 22:43:16 +01:00
}
2021-04-07 23:32:12 +02:00
2021-05-11 22:21:01 +02:00
setScreencastOptions ( options : { width : number , height : number , quality : number } | null ) {
this . _delegate . setScreencastOptions ( options ) . catch ( e = > debugLogger . log ( 'error' , e ) ) ;
2022-10-21 23:30:14 +02:00
this . _frameThrottler . setThrottlingEnabled ( ! ! options ) ;
2021-10-30 03:20:17 +02:00
}
throttleScreencastFrameAck ( ack : ( ) = > void ) {
// Don't ack immediately, tracing has smart throttling logic that is implemented here.
this . _frameThrottler . ack ( ack ) ;
}
2023-10-05 04:56:42 +02:00
temporarilyDisableTracingScreencastThrottling() {
2021-10-30 03:20:17 +02:00
this . _frameThrottler . recharge ( ) ;
2021-04-07 23:32:12 +02:00
}
2021-07-02 00:26:55 +02:00
2022-01-12 16:37:48 +01:00
async hideHighlight() {
await Promise . all ( this . frames ( ) . map ( frame = > frame . hideHighlight ( ) . catch ( ( ) = > { } ) ) ) ;
}
2022-03-18 02:27:33 +01:00
markAsServerSideOnly() {
this . _isServerSideOnly = true ;
}
2020-01-07 21:59:01 +01:00
}
2021-02-09 18:00:00 +01:00
export class Worker extends SdkObject {
2020-08-22 01:26:33 +02:00
static Events = {
Close : 'close' ,
} ;
2020-01-07 21:59:01 +01:00
private _url : string ;
private _executionContextPromise : Promise < js.ExecutionContext > ;
2021-02-03 22:49:25 +01:00
private _executionContextCallback : ( value : js.ExecutionContext ) = > void ;
2020-01-13 22:33:25 +01:00
_existingExecutionContext : js.ExecutionContext | null = null ;
2023-08-29 02:44:48 +02:00
readonly openScope = new LongStandingScope ( ) ;
2020-01-07 21:59:01 +01:00
2021-02-09 18:00:00 +01:00
constructor ( parent : SdkObject , url : string ) {
2021-04-21 08:03:56 +02:00
super ( parent , 'worker' ) ;
2020-01-07 21:59:01 +01:00
this . _url = url ;
2020-01-13 22:33:25 +01:00
this . _executionContextCallback = ( ) = > { } ;
2020-01-07 21:59:01 +01:00
this . _executionContextPromise = new Promise ( x = > this . _executionContextCallback = x ) ;
}
2020-01-13 18:14:28 +01:00
2020-01-07 21:59:01 +01:00
_createExecutionContext ( delegate : js.ExecutionContextDelegate ) {
2023-04-01 03:18:45 +02:00
this . _existingExecutionContext = new js . ExecutionContext ( this , delegate , 'worker' ) ;
2020-01-07 21:59:01 +01:00
this . _executionContextCallback ( this . _existingExecutionContext ) ;
}
url ( ) : string {
return this . _url ;
}
2021-11-03 18:44:50 +01:00
didClose() {
if ( this . _existingExecutionContext )
2023-07-24 06:00:07 +02:00
this . _existingExecutionContext . contextDestroyed ( 'Worker was closed' ) ;
2021-11-03 18:44:50 +01:00
this . emit ( Worker . Events . Close , this ) ;
2023-10-18 00:35:41 +02:00
this . openScope . close ( new Error ( 'Worker closed' ) ) ;
2021-11-03 18:44:50 +01:00
}
2021-03-17 20:03:21 +01:00
async evaluateExpression ( expression : string , isFunction : boolean | undefined , arg : any ) : Promise < any > {
2022-11-30 01:57:11 +01:00
return js . evaluateExpression ( await this . _executionContextPromise , expression , { returnByValue : true , isFunction } , arg ) ;
2020-06-30 19:55:11 +02:00
}
2021-03-17 20:03:21 +01:00
async evaluateExpressionHandle ( expression : string , isFunction : boolean | undefined , arg : any ) : Promise < any > {
2022-11-30 01:57:11 +01:00
return js . evaluateExpression ( await this . _executionContextPromise , expression , { returnByValue : false , isFunction } , arg ) ;
2020-06-30 19:55:11 +02:00
}
2019-11-19 03:18:28 +01:00
}
2020-03-04 01:46:06 +01:00
2022-05-10 00:07:04 +02:00
type BindingPayload = {
name : string ;
seq : number ;
serializedArgs? : SerializedValue [ ] ,
} ;
2020-03-04 01:46:06 +01:00
export class PageBinding {
readonly name : string ;
2020-05-18 23:28:06 +02:00
readonly playwrightFunction : frames.FunctionWithSource ;
2020-03-04 01:46:06 +01:00
readonly source : string ;
2020-10-02 07:47:31 +02:00
readonly needsHandle : boolean ;
2020-03-04 01:46:06 +01:00
2021-09-08 23:27:05 +02:00
constructor ( name : string , playwrightFunction : frames.FunctionWithSource , needsHandle : boolean ) {
2020-03-04 01:46:06 +01:00
this . name = name ;
this . playwrightFunction = playwrightFunction ;
2022-05-10 03:51:53 +02:00
this . source = ` ( ${ addPageBinding . toString ( ) } )( ${ JSON . stringify ( name ) } , ${ needsHandle } , ( ${ source } )()) ` ;
2020-10-02 07:47:31 +02:00
this . needsHandle = needsHandle ;
2020-03-04 01:46:06 +01:00
}
2020-05-18 23:28:06 +02:00
static async dispatch ( page : Page , payload : string , context : dom.FrameExecutionContext ) {
2022-05-10 00:07:04 +02:00
const { name , seq , serializedArgs } = JSON . parse ( payload ) as BindingPayload ;
2020-03-04 01:46:06 +01:00
try {
2020-12-02 22:43:16 +01:00
assert ( context . world ) ;
2021-09-08 23:27:05 +02:00
const binding = page . getBinding ( name ) ! ;
2020-10-02 07:47:31 +02:00
let result : any ;
2020-12-02 22:43:16 +01:00
if ( binding . needsHandle ) {
2021-03-17 18:47:07 +01:00
const handle = await context . evaluateHandle ( takeHandle , { name , seq } ) . catch ( e = > null ) ;
2020-12-02 22:43:16 +01:00
result = await binding . playwrightFunction ( { frame : context.frame , page , context : page._browserContext } , handle ) ;
2020-10-02 07:47:31 +02:00
} else {
2022-05-10 03:51:53 +02:00
const args = serializedArgs ! . map ( a = > parseEvaluationResultValue ( a ) ) ;
2020-12-02 22:43:16 +01:00
result = await binding . playwrightFunction ( { frame : context.frame , page , context : page._browserContext } , . . . args ) ;
2020-10-02 07:47:31 +02:00
}
2021-03-17 18:47:07 +01:00
context . evaluate ( deliverResult , { name , seq , result } ) . catch ( e = > debugLogger . log ( 'error' , e ) ) ;
2020-03-04 01:46:06 +01:00
} catch ( error ) {
2020-08-22 16:07:13 +02:00
if ( isError ( error ) )
2021-03-17 18:47:07 +01:00
context . evaluate ( deliverError , { name , seq , message : error.message , stack : error.stack } ) . catch ( e = > debugLogger . log ( 'error' , e ) ) ;
2020-03-04 01:46:06 +01:00
else
2021-03-17 18:47:07 +01:00
context . evaluate ( deliverErrorValue , { name , seq , error } ) . catch ( e = > debugLogger . log ( 'error' , e ) ) ;
2020-03-04 01:46:06 +01:00
}
2020-10-02 07:47:31 +02:00
function takeHandle ( arg : { name : string , seq : number } ) {
2021-05-13 00:19:27 +02:00
const handle = ( globalThis as any ) [ arg . name ] [ 'handles' ] . get ( arg . seq ) ;
( globalThis as any ) [ arg . name ] [ 'handles' ] . delete ( arg . seq ) ;
2020-10-02 07:47:31 +02:00
return handle ;
}
2020-07-16 05:05:11 +02:00
function deliverResult ( arg : { name : string , seq : number , result : any } ) {
2021-05-13 00:19:27 +02:00
( globalThis as any ) [ arg . name ] [ 'callbacks' ] . get ( arg . seq ) . resolve ( arg . result ) ;
( globalThis as any ) [ arg . name ] [ 'callbacks' ] . delete ( arg . seq ) ;
2020-03-04 01:46:06 +01:00
}
2020-07-16 05:05:11 +02:00
function deliverError ( arg : { name : string , seq : number , message : string , stack : string | undefined } ) {
const error = new Error ( arg . message ) ;
error . stack = arg . stack ;
2021-05-13 00:19:27 +02:00
( globalThis as any ) [ arg . name ] [ 'callbacks' ] . get ( arg . seq ) . reject ( error ) ;
( globalThis as any ) [ arg . name ] [ 'callbacks' ] . delete ( arg . seq ) ;
2020-03-04 01:46:06 +01:00
}
2020-07-16 05:05:11 +02:00
function deliverErrorValue ( arg : { name : string , seq : number , error : any } ) {
2021-05-13 00:19:27 +02:00
( globalThis as any ) [ arg . name ] [ 'callbacks' ] . get ( arg . seq ) . reject ( arg . error ) ;
( globalThis as any ) [ arg . name ] [ 'callbacks' ] . delete ( arg . seq ) ;
2020-03-04 01:46:06 +01:00
}
}
}
2022-05-10 00:07:04 +02:00
function addPageBinding ( bindingName : string , needsHandle : boolean , utilityScriptSerializers : ReturnType < typeof source > ) {
2021-05-13 00:19:27 +02:00
const binding = ( globalThis as any ) [ bindingName ] ;
2020-03-11 00:19:01 +01:00
if ( binding . __installed )
return ;
2021-05-13 00:19:27 +02:00
( globalThis as any ) [ bindingName ] = ( . . . args : any [ ] ) = > {
const me = ( globalThis as any ) [ bindingName ] ;
2020-10-02 07:47:31 +02:00
if ( needsHandle && args . slice ( 1 ) . some ( arg = > arg !== undefined ) )
throw new Error ( ` exposeBindingHandle supports a single argument, ${ args . length } received ` ) ;
2020-03-04 01:46:06 +01:00
let callbacks = me [ 'callbacks' ] ;
if ( ! callbacks ) {
callbacks = new Map ( ) ;
me [ 'callbacks' ] = callbacks ;
}
2022-05-10 00:07:04 +02:00
const seq : number = ( me [ 'lastSeq' ] || 0 ) + 1 ;
2020-03-04 01:46:06 +01:00
me [ 'lastSeq' ] = seq ;
2020-10-02 07:47:31 +02: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-10 00:07:04 +02:00
let payload : BindingPayload ;
2020-10-02 07:47:31 +02:00
if ( needsHandle ) {
handles . set ( seq , args [ 0 ] ) ;
2022-05-10 00:07:04 +02:00
payload = { name : bindingName , seq } ;
2020-10-02 07:47:31 +02: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-10 00:07:04 +02:00
payload = { name : bindingName , seq , serializedArgs } ;
2020-10-02 07:47:31 +02:00
}
2022-05-10 00:07:04 +02:00
binding ( JSON . stringify ( payload ) ) ;
2020-03-04 01:46:06 +01:00
return promise ;
} ;
2021-05-13 00:19:27 +02:00
( globalThis as any ) [ bindingName ] . __installed = true ;
2020-03-04 01:46:06 +01:00
}
2021-10-30 03:20:17 +02:00
class FrameThrottler {
private _acks : ( ( ) = > void ) [ ] = [ ] ;
2022-10-21 23:30:14 +02:00
private _defaultInterval : number ;
private _throttlingInterval : number ;
2021-10-30 03:20:17 +02:00
private _nonThrottledFrames : number ;
private _budget : number ;
2022-10-21 23:30:14 +02:00
private _throttlingEnabled = false ;
private _timeoutId : NodeJS.Timeout | undefined ;
2021-10-30 03:20:17 +02:00
2022-10-21 23:30:14 +02:00
constructor ( nonThrottledFrames : number , defaultInterval : number , throttlingInterval : number ) {
2021-10-30 03:20:17 +02:00
this . _nonThrottledFrames = nonThrottledFrames ;
this . _budget = nonThrottledFrames ;
2022-10-21 23:30:14 +02:00
this . _defaultInterval = defaultInterval ;
this . _throttlingInterval = throttlingInterval ;
this . _tick ( ) ;
2021-10-30 03:20:17 +02:00
}
2022-10-21 23:30:14 +02:00
dispose() {
if ( this . _timeoutId ) {
clearTimeout ( this . _timeoutId ) ;
this . _timeoutId = undefined ;
2021-10-30 03:20:17 +02:00
}
}
2022-10-21 23:30:14 +02:00
setThrottlingEnabled ( enabled : boolean ) {
this . _throttlingEnabled = enabled ;
}
2021-10-30 03:20:17 +02:00
recharge() {
// Send all acks, reset budget.
for ( const ack of this . _acks )
ack ( ) ;
this . _acks = [ ] ;
this . _budget = this . _nonThrottledFrames ;
2022-10-21 23:30:14 +02:00
if ( this . _timeoutId ) {
clearTimeout ( this . _timeoutId ) ;
this . _tick ( ) ;
}
2021-10-30 03:20:17 +02:00
}
ack ( ack : ( ) = > void ) {
2022-10-21 23:30:14 +02:00
if ( ! this . _timeoutId ) {
// Already disposed.
2021-10-30 03:20:17 +02:00
ack ( ) ;
return ;
}
this . _acks . push ( ack ) ;
}
private _tick() {
2022-10-21 23:30:14 +02: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-30 03:20:17 +02:00
}
}