diff --git a/docs/api.md b/docs/api.md index 39154acbe4995..fc4c2c07b48ff 100644 --- a/docs/api.md +++ b/docs/api.md @@ -23,6 +23,7 @@ - [class: Response](#class-response) - [class: Selectors](#class-selectors) - [class: Route](#class-route) +- [class: WebSocket](#class-websocket) - [class: TimeoutError](#class-timeouterror) - [class: Accessibility](#class-accessibility) - [class: Worker](#class-worker) @@ -738,6 +739,7 @@ page.removeListener('request', logRequest); - [event: 'requestfailed'](#event-requestfailed) - [event: 'requestfinished'](#event-requestfinished) - [event: 'response'](#event-response) +- [event: 'websocket'](#event-websocket) - [event: 'worker'](#event-worker) - [page.$(selector)](#pageselector) - [page.$$(selector)](#pageselector-1) @@ -949,6 +951,11 @@ Emitted when a request finishes successfully after downloading the response body Emitted when [response] status and headers are received for a request. For a successful response, the sequence of events is `request`, `response` and `requestfinished`. +#### event: 'websocket' +- <[WebSocket]> websocket + +Emitted when <[WebSocket]> request is sent. + #### event: 'worker' - <[Worker]> @@ -4133,6 +4140,45 @@ await page.route('**/xhr_endpoint', route => route.fulfill({ path: 'mock_data.js - returns: <[Request]> A request to be routed. +### class: WebSocket + +The [WebSocket] class represents websocket connections in the page. + + +- [event: 'close'](#event-close-2) +- [event: 'framereceived'](#event-framereceived) +- [event: 'framesent'](#event-framesent) +- [event: 'socketerror'](#event-socketerror) +- [webSocket.url()](#websocketurl) + + +#### event: 'close' + +Fired when the websocket closes. + +#### event: 'framereceived' +- <[Object]> web socket frame data + - `payload` <[string]|[Buffer]> frame payload + +Fired when the websocket recieves a frame. + +#### event: 'framesent' +- <[Object]> web socket frame data + - `payload` <[string]|[Buffer]> frame payload + +Fired when the websocket sends a frame. + +#### event: 'socketerror' +- <[String]> the error message + +Fired when the websocket has an error. + +#### webSocket.url() +- returns: <[string]> + +Contains the URL of the WebSocket. + + ### class: TimeoutError * extends: [Error] @@ -4233,7 +4279,7 @@ for (const worker of page.workers()) ``` -- [event: 'close'](#event-close-2) +- [event: 'close'](#event-close-3) - [worker.evaluate(pageFunction[, arg])](#workerevaluatepagefunction-arg) - [worker.evaluateHandle(pageFunction[, arg])](#workerevaluatehandlepagefunction-arg) - [worker.url()](#workerurl) @@ -4269,7 +4315,7 @@ If the function passed to the `worker.evaluateHandle` returns a [Promise], then ### class: BrowserServer -- [event: 'close'](#event-close-3) +- [event: 'close'](#event-close-4) - [browserServer.close()](#browserserverclose) - [browserServer.kill()](#browserserverkill) - [browserServer.process()](#browserserverprocess) diff --git a/src/client/api.ts b/src/client/api.ts index 306dd501171e5..25fe52ef14e8c 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -29,7 +29,7 @@ export { TimeoutError } from '../utils/errors'; export { Frame } from './frame'; export { Keyboard, Mouse, Touchscreen } from './input'; export { JSHandle } from './jsHandle'; -export { Request, Response, Route } from './network'; +export { Request, Response, Route, WebSocket } from './network'; export { Page } from './page'; export { Selectors } from './selectors'; export { Video } from './video'; diff --git a/src/client/connection.ts b/src/client/connection.ts index fe0408b23a6c0..bf0461d640d40 100644 --- a/src/client/connection.ts +++ b/src/client/connection.ts @@ -21,7 +21,7 @@ import { ChannelOwner } from './channelOwner'; import { ElementHandle } from './elementHandle'; import { Frame } from './frame'; import { JSHandle } from './jsHandle'; -import { Request, Response, Route } from './network'; +import { Request, Response, Route, WebSocket } from './network'; import { Page, BindingCall } from './page'; import { Worker } from './worker'; import { ConsoleMessage } from './consoleMessage'; @@ -226,6 +226,9 @@ export class Connection { case 'Selectors': result = new SelectorsOwner(parent, type, guid, initializer); break; + case 'WebSocket': + result = new WebSocket(parent, type, guid, initializer); + break; case 'Worker': result = new Worker(parent, type, guid, initializer); break; diff --git a/src/client/events.ts b/src/client/events.ts index 2c02e19ef820b..f689b19e2c889 100644 --- a/src/client/events.ts +++ b/src/client/events.ts @@ -49,9 +49,17 @@ export const Events = { FrameNavigated: 'framenavigated', Load: 'load', Popup: 'popup', + WebSocket: 'websocket', Worker: 'worker', }, + WebSocket: { + Close: 'close', + Error: 'socketerror', + FrameReceived: 'framereceived', + FrameSent: 'framesent', + }, + Worker: { Close: 'close', }, diff --git a/src/client/network.ts b/src/client/network.ts index aa750344a3173..496fde703b547 100644 --- a/src/client/network.ts +++ b/src/client/network.ts @@ -23,6 +23,7 @@ import * as fs from 'fs'; import * as mime from 'mime'; import * as util from 'util'; import { isString, headersObjectToArray, headersArrayToObject } from '../utils/utils'; +import { Events } from './events'; export type NetworkCookie = { name: string, @@ -312,6 +313,30 @@ export class Response extends ChannelOwner { + static from(webSocket: channels.WebSocketChannel): WebSocket { + return (webSocket as any)._object; + } + + constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.WebSocketInitializer) { + super(parent, type, guid, initializer); + this._channel.on('frameSent', (event: { opcode: number, data: string }) => { + const payload = event.opcode === 2 ? Buffer.from(event.data, 'base64') : event.data; + this.emit(Events.WebSocket.FrameSent, { payload }); + }); + this._channel.on('frameReceived', (event: { opcode: number, data: string }) => { + const payload = event.opcode === 2 ? Buffer.from(event.data, 'base64') : event.data; + this.emit(Events.WebSocket.FrameReceived, { payload }); + }); + this._channel.on('error', ({ error }) => this.emit(Events.WebSocket.Error, error)); + this._channel.on('close', () => this.emit(Events.WebSocket.Close)); + } + + url(): string { + return this._initializer.url; + } +} + export function validateHeaders(headers: Headers) { for (const key of Object.keys(headers)) { const value = headers[key]; diff --git a/src/client/page.ts b/src/client/page.ts index 64f1cca67c189..579c35d5646c6 100644 --- a/src/client/page.ts +++ b/src/client/page.ts @@ -31,7 +31,7 @@ import { Worker } from './worker'; import { Frame, verifyLoadState, WaitForNavigationOptions } from './frame'; import { Keyboard, Mouse, Touchscreen } from './input'; import { assertMaxArguments, Func1, FuncOn, SmartHandle, serializeArgument, parseResult, JSHandle } from './jsHandle'; -import { Request, Response, Route, RouteHandler, validateHeaders } from './network'; +import { Request, Response, Route, RouteHandler, WebSocket, validateHeaders } from './network'; import { FileChooser } from './fileChooser'; import { Buffer } from 'buffer'; import { ChromiumCoverage } from './chromiumCoverage'; @@ -130,6 +130,7 @@ export class Page extends ChannelOwner this.emit(Events.Page.Response, Response.from(response))); this._channel.on('route', ({ route, request }) => this._onRoute(Route.from(route), Request.from(request))); this._channel.on('video', ({ relativePath }) => this.video()!._setRelativePath(relativePath)); + this._channel.on('webSocket', ({ webSocket }) => this.emit(Events.Page.WebSocket, WebSocket.from(webSocket))); this._channel.on('worker', ({ worker }) => this._onWorker(Worker.from(worker))); if (this._browserContext._browserName === 'chromium') { diff --git a/src/dispatchers/networkDispatchers.ts b/src/dispatchers/networkDispatchers.ts index 6c5f3f0168201..f74d693cf994b 100644 --- a/src/dispatchers/networkDispatchers.ts +++ b/src/dispatchers/networkDispatchers.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Request, Response, Route } from '../server/network'; +import { Request, Response, Route, WebSocket } from '../server/network'; import * as channels from '../protocol/channels'; import { Dispatcher, DispatcherScope, lookupNullableDispatcher, existingDispatcher } from './dispatcher'; import { FrameDispatcher } from './frameDispatcher'; @@ -98,3 +98,15 @@ export class RouteDispatcher extends Dispatcher implements channels.WebSocketChannel { + constructor(scope: DispatcherScope, webSocket: WebSocket) { + super(scope, webSocket, 'WebSocket', { + url: webSocket.url(), + }); + webSocket.on(WebSocket.Events.FrameSent, (event: { opcode: number, data: string }) => this._dispatchEvent('frameSent', event)); + webSocket.on(WebSocket.Events.FrameReceived, (event: { opcode: number, data: string }) => this._dispatchEvent('frameReceived', event)); + webSocket.on(WebSocket.Events.Error, (error: string) => this._dispatchEvent('error', { error })); + webSocket.on(WebSocket.Events.Close, () => this._dispatchEvent('close', {})); + } +} diff --git a/src/dispatchers/pageDispatcher.ts b/src/dispatchers/pageDispatcher.ts index 1a777707d40fb..9b402bc298ced 100644 --- a/src/dispatchers/pageDispatcher.ts +++ b/src/dispatchers/pageDispatcher.ts @@ -25,7 +25,7 @@ import { ConsoleMessageDispatcher } from './consoleMessageDispatcher'; import { DialogDispatcher } from './dialogDispatcher'; import { DownloadDispatcher } from './downloadDispatcher'; import { FrameDispatcher } from './frameDispatcher'; -import { RequestDispatcher, ResponseDispatcher, RouteDispatcher } from './networkDispatchers'; +import { RequestDispatcher, ResponseDispatcher, RouteDispatcher, WebSocketDispatcher } from './networkDispatchers'; import { serializeResult, parseArgument, JSHandleDispatcher } from './jsHandleDispatcher'; import { ElementHandleDispatcher, createHandle } from './elementHandlerDispatcher'; import { FileChooser } from '../server/fileChooser'; @@ -72,6 +72,7 @@ export class PageDispatcher extends Dispatcher i })); page.on(Page.Events.Response, response => this._dispatchEvent('response', { response: new ResponseDispatcher(this._scope, response) })); page.on(Page.Events.VideoStarted, (video: Video) => this._dispatchEvent('video', { relativePath: video._relativePath })); + page.on(Page.Events.WebSocket, webSocket => this._dispatchEvent('webSocket', { webSocket: new WebSocketDispatcher(this._scope, webSocket) })); page.on(Page.Events.Worker, worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this._scope, worker) })); } diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index 81499253ec1af..e40c252b33a12 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -694,6 +694,7 @@ export interface PageChannel extends Channel { on(event: 'response', callback: (params: PageResponseEvent) => void): this; on(event: 'route', callback: (params: PageRouteEvent) => void): this; on(event: 'video', callback: (params: PageVideoEvent) => void): this; + on(event: 'webSocket', callback: (params: PageWebSocketEvent) => void): this; on(event: 'worker', callback: (params: PageWorkerEvent) => void): this; setDefaultNavigationTimeoutNoReply(params: PageSetDefaultNavigationTimeoutNoReplyParams, metadata?: Metadata): Promise; setDefaultTimeoutNoReply(params: PageSetDefaultTimeoutNoReplyParams, metadata?: Metadata): Promise; @@ -782,6 +783,9 @@ export type PageRouteEvent = { export type PageVideoEvent = { relativePath: string, }; +export type PageWebSocketEvent = { + webSocket: WebSocketChannel, +}; export type PageWorkerEvent = { worker: WorkerChannel, }; @@ -2185,6 +2189,31 @@ export type ResponseFinishedResult = { error?: string, }; +// ----------- WebSocket ----------- +export type WebSocketInitializer = { + url: string, +}; +export interface WebSocketChannel extends Channel { + on(event: 'open', callback: (params: WebSocketOpenEvent) => void): this; + on(event: 'frameSent', callback: (params: WebSocketFrameSentEvent) => void): this; + on(event: 'frameReceived', callback: (params: WebSocketFrameReceivedEvent) => void): this; + on(event: 'error', callback: (params: WebSocketErrorEvent) => void): this; + on(event: 'close', callback: (params: WebSocketCloseEvent) => void): this; +} +export type WebSocketOpenEvent = {}; +export type WebSocketFrameSentEvent = { + opcode: number, + data: string, +}; +export type WebSocketFrameReceivedEvent = { + opcode: number, + data: string, +}; +export type WebSocketErrorEvent = { + error: string, +}; +export type WebSocketCloseEvent = {}; + // ----------- ConsoleMessage ----------- export type ConsoleMessageInitializer = { type: string, diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index 6c9b7fe517ca0..eb344adb1516b 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -945,6 +945,10 @@ Page: parameters: relativePath: string + webSocket: + parameters: + webSocket: WebSocket + worker: parameters: worker: Worker @@ -1844,6 +1848,32 @@ Response: error: string? +WebSocket: + type: interface + + initializer: + url: string + + events: + open: + + frameSent: + parameters: + opcode: number + data: string + + frameReceived: + parameters: + opcode: number + data: string + + error: + parameters: + error: string + + close: + + ConsoleMessage: type: interface diff --git a/src/server/chromium/crNetworkManager.ts b/src/server/chromium/crNetworkManager.ts index 20abf16aa2ae9..c729aad15a398 100644 --- a/src/server/chromium/crNetworkManager.ts +++ b/src/server/chromium/crNetworkManager.ts @@ -55,6 +55,13 @@ export class CRNetworkManager { helper.addEventListener(session, 'Network.responseReceived', this._onResponseReceived.bind(this)), helper.addEventListener(session, 'Network.loadingFinished', this._onLoadingFinished.bind(this)), helper.addEventListener(session, 'Network.loadingFailed', this._onLoadingFailed.bind(this)), + helper.addEventListener(session, 'Network.webSocketCreated', e => this._page._frameManager.onWebSocketCreated(e.requestId, e.url)), + helper.addEventListener(session, 'Network.webSocketWillSendHandshakeRequest', e => this._page._frameManager.onWebSocketRequest(e.requestId)), + helper.addEventListener(session, 'Network.webSocketHandshakeResponseReceived', e => this._page._frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText)), + helper.addEventListener(session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page._frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData)), + helper.addEventListener(session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page._frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData)), + helper.addEventListener(session, 'Network.webSocketClosed', e => this._page._frameManager.webSocketClosed(e.requestId)), + helper.addEventListener(session, 'Network.webSocketFrameError', e => this._page._frameManager.webSocketError(e.requestId, e.errorMessage)), ]; } diff --git a/src/server/frames.ts b/src/server/frames.ts index f8ecd8b40282c..de54ff172b57b 100644 --- a/src/server/frames.ts +++ b/src/server/frames.ts @@ -69,6 +69,7 @@ export class FrameManager { private _mainFrame: Frame; readonly _consoleMessageTags = new Map(); readonly _signalBarriers = new Set(); + private _webSockets = new Map(); constructor(page: Page) { this._page = page; @@ -165,6 +166,7 @@ export class FrameManager { frameCommittedNewDocumentNavigation(frameId: string, url: string, name: string, documentId: string, initial: boolean) { const frame = this._frames.get(frameId)!; this.removeChildFramesRecursively(frame); + this.clearWebSockets(frame); frame._url = url; frame._name = name; @@ -328,6 +330,57 @@ export class FrameManager { handler(); return true; } + + clearWebSockets(frame: Frame) { + // TODO: attribute sockets to frames. + if (frame.parentFrame()) + return; + this._webSockets.clear(); + } + + onWebSocketCreated(requestId: string, url: string) { + const ws = new network.WebSocket(url); + this._webSockets.set(requestId, ws); + } + + onWebSocketRequest(requestId: string) { + const ws = this._webSockets.get(requestId); + if (ws) + this._page.emit(Page.Events.WebSocket, ws); + } + + onWebSocketResponse(requestId: string, status: number, statusText: string) { + const ws = this._webSockets.get(requestId); + if (status >= 200 && status < 400) + return; + if (ws) + ws.error(`${statusText}: ${status}`); + } + + onWebSocketFrameSent(requestId: string, opcode: number, data: string) { + const ws = this._webSockets.get(requestId); + if (ws) + ws.frameSent(opcode, data); + } + + webSocketFrameReceived(requestId: string, opcode: number, data: string) { + const ws = this._webSockets.get(requestId); + if (ws) + ws.frameReceived(opcode, data); + } + + webSocketClosed(requestId: string) { + const ws = this._webSockets.get(requestId); + if (ws) + ws.closed(); + this._webSockets.delete(requestId); + } + + webSocketError(requestId: string, errorMessage: string): void { + const ws = this._webSockets.get(requestId); + if (ws) + ws.error(errorMessage); + } } export class Frame extends EventEmitter { diff --git a/src/server/network.ts b/src/server/network.ts index 9b3502123c810..c61407489e9e8 100644 --- a/src/server/network.ts +++ b/src/server/network.ts @@ -17,6 +17,7 @@ import * as frames from './frames'; import * as types from './types'; import { assert } from '../utils/utils'; +import { EventEmitter } from 'events'; export function filterCookies(cookies: types.NetworkCookie[], urls: string[]): types.NetworkCookie[] { const parsedURLs = urls.map(s => new URL(s)); @@ -319,6 +320,42 @@ export class Response { } } +export class WebSocket extends EventEmitter { + private _url: string; + + static Events = { + Close: 'close', + Error: 'socketerror', + FrameReceived: 'framereceived', + FrameSent: 'framesent', + }; + + constructor(url: string) { + super(); + this._url = url; + } + + url(): string { + return this._url; + } + + frameSent(opcode: number, data: string) { + this.emit(WebSocket.Events.FrameSent, { opcode, data }); + } + + frameReceived(opcode: number, data: string) { + this.emit(WebSocket.Events.FrameReceived, { opcode, data }); + } + + error(errorMessage: string) { + this.emit(WebSocket.Events.Error, errorMessage); + } + + closed() { + this.emit(WebSocket.Events.Close); + } +} + export interface RouteDelegate { abort(errorCode: string): Promise; fulfill(response: types.NormalizedFulfillResponse): Promise; diff --git a/src/server/page.ts b/src/server/page.ts index 42a95c8fcd3c7..12b9c26bfdd12 100644 --- a/src/server/page.ts +++ b/src/server/page.ts @@ -113,6 +113,7 @@ export class Page extends EventEmitter { FrameNavigated: 'framenavigated', Load: 'load', Popup: 'popup', + WebSocket: 'websocket', Worker: 'worker', VideoStarted: 'videostarted', }; diff --git a/src/server/webkit/wkPage.ts b/src/server/webkit/wkPage.ts index 0c01a13dfabf4..c48d2ad3a5916 100644 --- a/src/server/webkit/wkPage.ts +++ b/src/server/webkit/wkPage.ts @@ -357,6 +357,13 @@ export class WKPage implements PageDelegate { helper.addEventListener(this._session, 'Network.responseReceived', e => this._onResponseReceived(e)), helper.addEventListener(this._session, 'Network.loadingFinished', e => this._onLoadingFinished(e)), helper.addEventListener(this._session, 'Network.loadingFailed', e => this._onLoadingFailed(e)), + helper.addEventListener(this._session, 'Network.webSocketCreated', e => this._page._frameManager.onWebSocketCreated(e.requestId, e.url)), + helper.addEventListener(this._session, 'Network.webSocketWillSendHandshakeRequest', e => this._page._frameManager.onWebSocketRequest(e.requestId)), + helper.addEventListener(this._session, 'Network.webSocketHandshakeResponseReceived', e => this._page._frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText)), + helper.addEventListener(this._session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page._frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData)), + helper.addEventListener(this._session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page._frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData)), + helper.addEventListener(this._session, 'Network.webSocketClosed', e => this._page._frameManager.webSocketClosed(e.requestId)), + helper.addEventListener(this._session, 'Network.webSocketFrameError', e => this._page._frameManager.webSocketError(e.requestId, e.errorMessage)), ]; } diff --git a/test/checkCoverage.js b/test/checkCoverage.js index 4e8f5b22778bc..06a98ba67a8e0 100644 --- a/test/checkCoverage.js +++ b/test/checkCoverage.js @@ -34,6 +34,16 @@ if (browserName !== 'chromium') { api.delete('cDPSession.detach'); } +if (browserName === 'firefox') { + // WebSockets on FF are work in progress. + api.delete('webSocket.url'); + api.delete('webSocket.emit("close")'); + api.delete('webSocket.emit("socketerror")'); + api.delete('webSocket.emit("framereceived")'); + api.delete('webSocket.emit("framesent")'); + api.delete('page.emit("websocket")'); +} + // Some permissions tests are disabled in webkit. See permissions.jest.js if (browserName === 'webkit') api.delete('browserContext.clearPermissions'); diff --git a/test/web-socket.spec.ts b/test/web-socket.spec.ts new file mode 100644 index 0000000000000..56c4249335e76 --- /dev/null +++ b/test/web-socket.spec.ts @@ -0,0 +1,104 @@ +/** + * Copyright 2018 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. + */ + +import { it, describe, expect } from './fixtures'; + +describe('web socket', (test, { browserName }) => { + test.fixme(browserName === 'firefox'); +}, () => { + it('should work', async ({ page, server }) => { + const value = await page.evaluate(port => { + let cb; + const result = new Promise(f => cb = f); + const ws = new WebSocket('ws://localhost:' + port + '/ws'); + ws.addEventListener('message', data => { ws.close(); cb(data.data); }); + return result; + }, server.PORT); + expect(value).toBe('incoming'); + }); + + it('should emit close events', async ({ page, server }) => { + let socketClosed; + const socketClosePromise = new Promise(f => socketClosed = f); + const log = []; + page.on('websocket', ws => { + log.push(`open<${ws.url()}>`); + ws.on('close', () => { log.push('close'); socketClosed(); }); + }); + await page.evaluate(port => { + const ws = new WebSocket('ws://localhost:' + port + '/ws'); + ws.addEventListener('open', () => ws.close()); + }, server.PORT); + await socketClosePromise; + expect(log.join(':')).toBe(`open:close`); + }); + + it('should emit frame events', async ({ page, server }) => { + let socketClosed; + const socketClosePromise = new Promise(f => socketClosed = f); + const log = []; + page.on('websocket', ws => { + log.push('open'); + ws.on('framesent', d => log.push('sent<' + d.payload + '>')); + ws.on('framereceived', d => log.push('received<' + d.payload + '>')); + ws.on('close', () => { log.push('close'); socketClosed(); }); + }); + await page.evaluate(port => { + const ws = new WebSocket('ws://localhost:' + port + '/ws'); + ws.addEventListener('open', () => ws.send('outgoing')); + ws.addEventListener('message', () => { ws.close(); }); + }, server.PORT); + await socketClosePromise; + expect(log.join(':')).toBe('open:sent:received:close'); + }); + + it('should emit binary frame events', async ({ page, server }) => { + let doneCallback; + const donePromise = new Promise(f => doneCallback = f); + const sent = []; + page.on('websocket', ws => { + ws.on('close', doneCallback); + ws.on('framesent', d => sent.push(d.payload)); + }); + await page.evaluate(port => { + const ws = new WebSocket('ws://localhost:' + port + '/ws'); + ws.addEventListener('open', () => { + const binary = new Uint8Array(5); + for (let i = 0; i < 5; ++i) + binary[i] = i; + ws.send('text'); + ws.send(binary); + ws.close(); + }); + }, server.PORT); + await donePromise; + expect(sent[0]).toBe('text'); + for (let i = 0; i < 5; ++i) + expect(sent[1][i]).toBe(i); + }); + + it('should emit error', async ({page, server}) => { + let callback; + const result = new Promise(f => callback = f); + page.on('websocket', ws => ws.on('socketerror', callback)); + page.evaluate(port => { + new WebSocket('ws://localhost:' + port + '/bogus-ws'); + }, server.PORT); + const message = await result; + expect(message).toContain(': 400'); + }); +}); diff --git a/types/types.d.ts b/types/types.d.ts new file mode 100644 index 0000000000000..cdc4b76fd99ba --- /dev/null +++ b/types/types.d.ts @@ -0,0 +1,7544 @@ +// This file is generated by /utils/generate_types/index.js +/** + * 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. + */ +import { Protocol } from './protocol'; +import { ChildProcess } from 'child_process'; +import { EventEmitter } from 'events'; +import { Readable } from 'stream'; + +/** + * Can be converted to JSON + */ +type Serializable = {}; +/** + * Can be converted to JSON, but may also contain JSHandles. + */ +type EvaluationArgument = {}; + +type NoHandles = Arg extends JSHandle ? never : (Arg extends object ? { [Key in keyof Arg]: NoHandles } : Arg); +type Unboxed = + Arg extends ElementHandle ? T : + Arg extends JSHandle ? T : + Arg extends NoHandles ? Arg : + Arg extends [infer A0] ? [Unboxed] : + Arg extends [infer A0, infer A1] ? [Unboxed, Unboxed] : + Arg extends [infer A0, infer A1, infer A2] ? [Unboxed, Unboxed, Unboxed] : + Arg extends [infer A0, infer A1, infer A2, infer A3] ? [Unboxed, Unboxed, Unboxed, Unboxed] : + Arg extends Array ? Array> : + Arg extends object ? { [Key in keyof Arg]: Unboxed } : + Arg; +type PageFunction = string | ((arg: Unboxed) => R | Promise); +type PageFunctionOn = string | ((on: On, arg2: Unboxed) => R | Promise); +type SmartHandle = T extends Node ? ElementHandle : JSHandle; +type ElementHandleForTag = ElementHandle; + +type PageWaitForSelectorOptionsNotHidden = PageWaitForSelectorOptions & { + state: 'visible'|'attached'; +}; +type ElementHandleWaitForSelectorOptionsNotHidden = ElementHandleWaitForSelectorOptions & { + state: 'visible'|'attached'; +}; + +type BindingSource = { context: BrowserContext, page: Page, frame: Frame }; + +/** + * Page provides methods to interact with a single tab in a Browser, or an extension background page in Chromium. One Browser instance might have multiple Page instances. + * This example creates a page, navigates it to a URL, and then saves a screenshot: + * ```js + * const { webkit } = require('playwright'); // Or 'chromium' or 'firefox'. + * + * (async () => { + * const browser = await webkit.launch(); + * const context = await browser.newContext(); + * const page = await context.newPage(); + * await page.goto('https://example.com'); + * await page.screenshot({path: 'screenshot.png'}); + * await browser.close(); + * })(); + * ``` + * The Page class emits various events (described below) which can be handled using any of Node's native `EventEmitter` methods, such as `on`, `once` or `removeListener`. + * This example logs a message for a single page `load` event: + * ```js + * page.once('load', () => console.log('Page loaded!')); + * ``` + * To unsubscribe from events use the `removeListener` method: + * ```js + * function logRequest(interceptedRequest) { + * console.log('A request was made:', interceptedRequest.url()); + * } + * page.on('request', logRequest); + * // Sometime later... + * page.removeListener('request', logRequest); + * ``` + */ +export interface Page { + /** + * If the function passed to the `page.evaluate` returns a Promise, then `page.evaluate` would wait for the promise to resolve and return its value. + * If the function passed to the `page.evaluate` returns a non-Serializable value, then `page.evaluate` resolves to `undefined`. DevTools Protocol also supports transferring some additional values that are not serializable by `JSON`: `-0`, `NaN`, `Infinity`, `-Infinity`, and bigint literals. + * Passing argument to `pageFunction`: + * ```js + * const result = await page.evaluate(([x, y]) => { + * return Promise.resolve(x * y); + * }, [7, 8]); + * console.log(result); // prints "56" + * ``` + * A string can also be passed in instead of a function: + * ```js + * console.log(await page.evaluate('1 + 2')); // prints "3" + * const x = 10; + * console.log(await page.evaluate(`1 + ${x}`)); // prints "11" + * ``` + * ElementHandle instances can be passed as an argument to the `page.evaluate`: + * ```js + * const bodyHandle = await page.$('body'); + * const html = await page.evaluate(([body, suffix]) => body.innerHTML + suffix, [bodyHandle, 'hello']); + * await bodyHandle.dispose(); + * ``` + * Shortcut for page.mainFrame().evaluate(pageFunction[, arg]). + * @param pageFunction Function to be evaluated in the page context + * @param arg Optional argument to pass to `pageFunction` + * @returns Promise which resolves to the return value of `pageFunction` + */ + evaluate(pageFunction: PageFunction, arg: Arg): Promise; + evaluate(pageFunction: PageFunction, arg?: any): Promise; + + /** + * The only difference between `page.evaluate` and `page.evaluateHandle` is that `page.evaluateHandle` returns in-page object (JSHandle). + * If the function passed to the `page.evaluateHandle` returns a Promise, then `page.evaluateHandle` would wait for the promise to resolve and return its value. + * A string can also be passed in instead of a function: + * ```js + * const aHandle = await page.evaluateHandle('document'); // Handle for the 'document' + * ``` + * JSHandle instances can be passed as an argument to the `page.evaluateHandle`: + * ```js + * const aHandle = await page.evaluateHandle(() => document.body); + * const resultHandle = await page.evaluateHandle(body => body.innerHTML, aHandle); + * console.log(await resultHandle.jsonValue()); + * await resultHandle.dispose(); + * ``` + * @param pageFunction Function to be evaluated in the page context + * @param arg Optional argument to pass to `pageFunction` + * @returns Promise which resolves to the return value of `pageFunction` as in-page object (JSHandle) + */ + evaluateHandle(pageFunction: PageFunction, arg: Arg): Promise>; + evaluateHandle(pageFunction: PageFunction, arg?: any): Promise>; + + /** + * The method finds an element matching the specified selector within the page. If no elements match the selector, the return value resolves to `null`. + * Shortcut for page.mainFrame().$(selector). + * @param selector A selector to query page for. See working with selectors for more details. + */ + $(selector: K): Promise | null>; + $(selector: string): Promise | null>; + + /** + * The method finds all elements matching the specified selector within the page. If no elements match the selector, the return value resolves to `[]`. + * Shortcut for page.mainFrame().$$(selector). + * @param selector A selector to query page for. See working with selectors for more details. + */ + $$(selector: K): Promise[]>; + $$(selector: string): Promise[]>; + + /** + * The method finds an element matching the specified selector within the page and passes it as a first argument to `pageFunction`. If no elements match the selector, the method throws an error. + * If `pageFunction` returns a Promise, then `page.$eval` would wait for the promise to resolve and return its value. + * Examples: + * ```js + * const searchValue = await page.$eval('#search', el => el.value); + * const preloadHref = await page.$eval('link[rel=preload]', el => el.href); + * const html = await page.$eval('.main-container', (e, suffix) => e.outerHTML + suffix, 'hello'); + * ``` + * Shortcut for page.mainFrame().$eval(selector, pageFunction). + * @param selector A selector to query page for. See working with selectors for more details. + * @param pageFunction Function to be evaluated in browser context + * @param arg Optional argument to pass to `pageFunction` + * @returns Promise which resolves to the return value of `pageFunction` + */ + $eval(selector: K, pageFunction: PageFunctionOn, arg: Arg): Promise; + $eval(selector: string, pageFunction: PageFunctionOn, arg: Arg): Promise; + $eval(selector: K, pageFunction: PageFunctionOn, arg?: any): Promise; + $eval(selector: string, pageFunction: PageFunctionOn, arg?: any): Promise; + + /** + * The method finds all elements matching the specified selector within the page and passes an array of matched elements as a first argument to `pageFunction`. + * If `pageFunction` returns a Promise, then `page.$$eval` would wait for the promise to resolve and return its value. + * Examples: + * ```js + * const divsCounts = await page.$$eval('div', (divs, min) => divs.length >= min, 10); + * ``` + * @param selector A selector to query page for. See working with selectors for more details. + * @param pageFunction Function to be evaluated in browser context + * @param arg Optional argument to pass to `pageFunction` + * @returns Promise which resolves to the return value of `pageFunction` + */ + $$eval(selector: K, pageFunction: PageFunctionOn, arg: Arg): Promise; + $$eval(selector: string, pageFunction: PageFunctionOn, arg: Arg): Promise; + $$eval(selector: K, pageFunction: PageFunctionOn, arg?: any): Promise; + $$eval(selector: string, pageFunction: PageFunctionOn, arg?: any): Promise; + + /** + * The `waitForFunction` can be used to observe viewport size change: + * ```js + * const { webkit } = require('playwright'); // Or 'chromium' or 'firefox'. + * + * (async () => { + * const browser = await webkit.launch(); + * const page = await browser.newPage(); + * const watchDog = page.waitForFunction('window.innerWidth < 100'); + * await page.setViewportSize({width: 50, height: 50}); + * await watchDog; + * await browser.close(); + * })(); + * ``` + * To pass an argument from Node.js to the predicate of `page.waitForFunction` function: + * ```js + * const selector = '.foo'; + * await page.waitForFunction(selector => !!document.querySelector(selector), selector); + * ``` + * Shortcut for page.mainFrame().waitForFunction(pageFunction[, arg, options]). + * @param pageFunction Function to be evaluated in browser context + * @param arg Optional argument to pass to `pageFunction` + * @param options Optional waiting parameters + * @returns Promise which resolves when the `pageFunction` returns a truthy value. It resolves to a JSHandle of the truthy value. + */ + waitForFunction(pageFunction: PageFunction, arg: Arg, options?: PageWaitForFunctionOptions): Promise>; + waitForFunction(pageFunction: PageFunction, arg?: any, options?: PageWaitForFunctionOptions): Promise>; + + /** + * Wait for the `selector` to satisfy `state` option (either appear/disappear from dom, or become visible/hidden). If at the moment of calling the method `selector` already satisfies the condition, the method will return immediately. If the selector doesn't satisfy the condition for the `timeout` milliseconds, the function will throw. + * This method works across navigations: + * ```js + * const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'. + * + * (async () => { + * const browser = await chromium.launch(); + * const page = await browser.newPage(); + * let currentURL; + * page + * .waitForSelector('img') + * .then(() => console.log('First URL with image: ' + currentURL)); + * for (currentURL of ['https://example.com', 'https://google.com', 'https://bbc.com']) { + * await page.goto(currentURL); + * } + * await browser.close(); + * })(); + * ``` + * Shortcut for page.mainFrame().waitForSelector(selector[, options]). + * @param selector A selector of an element to wait for. See working with selectors for more details. + * @param options + * @returns Promise which resolves when element specified by selector satisfies `state` option. Resolves to `null` if waiting for `hidden` or `detached`. + */ + waitForSelector(selector: K, options?: PageWaitForSelectorOptionsNotHidden): Promise>; + waitForSelector(selector: string, options?: PageWaitForSelectorOptionsNotHidden): Promise>; + waitForSelector(selector: K, options: PageWaitForSelectorOptions): Promise | null>; + waitForSelector(selector: string, options: PageWaitForSelectorOptions): Promise>; + + /** + * The method adds a function called `name` on the `window` object of every frame in this page. + * When called, the function executes `playwrightBinding` in Node.js and returns a Promise which resolves to the return value of `playwrightBinding`. + * If the `playwrightBinding` returns a Promise, it will be awaited. + * The first argument of the `playwrightBinding` function contains information about the caller: + * `{ browserContext: BrowserContext, page: Page, frame: Frame }`. + * See browserContext.exposeBinding(name, playwrightBinding) for the context-wide version. + * + * **NOTE** Functions installed via `page.exposeBinding` survive navigations. + * + * An example of exposing page URL to all frames in a page: + * ```js + * const { webkit } = require('playwright'); // Or 'chromium' or 'firefox'. + * + * (async () => { + * const browser = await webkit.launch({ headless: false }); + * const context = await browser.newContext(); + * const page = await context.newPage(); + * await page.exposeBinding('pageURL', ({ page }) => page.url()); + * await page.setContent(` + * + * + *
+ * `); + * await page.click('button'); + * })(); + * ``` + * An example of passing an element handle: + * ```js + * await page.exposeBinding('clicked', async (source, element) => { + * console.log(await element.textContent()); + * }, { handle: true }); + * await page.setContent(` + * + *
Click me
+ *
Or click me
+ * `); + * ``` + * @param name Name of the function on the window object. + * @param playwrightBinding Callback function that will be called in the Playwright's context. + * @param options + */ + exposeBinding(name: string, playwrightBinding: (source: BindingSource, arg: JSHandle) => any, options: { handle: true }): Promise; + exposeBinding(name: string, playwrightBinding: (source: BindingSource, ...args: any[]) => any, options?: { handle?: boolean }): Promise; + /** + * Emitted when the page closes. + */ + on(event: 'close', listener: () => void): this; + + /** + * Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also emitted if the page throws an error or a warning. + * The arguments passed into `console.log` appear as arguments on the event handler. + * An example of handling `console` event: + * ```js + * page.on('console', msg => { + * for (let i = 0; i < msg.args().length; ++i) + * console.log(`${i}: ${msg.args()[i]}`); + * }); + * page.evaluate(() => console.log('hello', 5, {foo: 'bar'})); + * ``` + */ + on(event: 'console', listener: (consoleMessage: ConsoleMessage) => void): this; + + /** + * Emitted when the page crashes. Browser pages might crash if they try to allocate too much memory. When the page crashes, ongoing and subsequent operations will throw. + * The most common way to deal with crashes is to catch an exception: + * ```js + * try { + * // Crash might happen during a click. + * await page.click('button'); + * // Or while waiting for an event. + * await page.waitForEvent('popup'); + * } catch (e) { + * // When the page crashes, exception message contains 'crash'. + * } + * ``` + * However, when manually listening to events, it might be useful to avoid stalling when the page crashes. In this case, handling `crash` event helps: + * ```js + * await new Promise((resolve, reject) => { + * page.on('requestfinished', async request => { + * if (await someProcessing(request)) + * resolve(request); + * }); + * page.on('crash', error => reject(error)); + * }); + * ``` + */ + on(event: 'crash', listener: () => void): this; + + /** + * Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Playwright can respond to the dialog via Dialog's accept or dismiss methods. + */ + on(event: 'dialog', listener: (dialog: Dialog) => void): this; + + /** + * Emitted when the JavaScript `DOMContentLoaded` event is dispatched. + */ + on(event: 'domcontentloaded', listener: () => void): this; + + /** + * Emitted when attachment download started. User can access basic file operations on downloaded content via the passed Download instance. + * + * **NOTE** Browser context **must** be created with the `acceptDownloads` set to `true` when user needs access to the downloaded content. If `acceptDownloads` is not set or set to `false`, download events are emitted, but the actual download is not performed and user has no access to the downloaded files. + */ + on(event: 'download', listener: (download: Download) => void): this; + + /** + * Emitted when a file chooser is supposed to appear, such as after clicking the ``. Playwright can respond to it via setting the input files using `fileChooser.setFiles` that can be uploaded after that. + * ```js + * page.on('filechooser', async (fileChooser) => { + * await fileChooser.setFiles('/tmp/myfile.pdf'); + * }); + * ``` + */ + on(event: 'filechooser', listener: (fileChooser: FileChooser) => void): this; + + /** + * Emitted when a frame is attached. + */ + on(event: 'frameattached', listener: (frame: Frame) => void): this; + + /** + * Emitted when a frame is detached. + */ + on(event: 'framedetached', listener: (frame: Frame) => void): this; + + /** + * Emitted when a frame is navigated to a new url. + */ + on(event: 'framenavigated', listener: (frame: Frame) => void): this; + + /** + * Emitted when the JavaScript `load` event is dispatched. + */ + on(event: 'load', listener: () => void): this; + + /** + * Emitted when an uncaught exception happens within the page. + */ + on(event: 'pageerror', listener: (error: Error) => void): this; + + /** + * Emitted when the page opens a new tab or window. This event is emitted in addition to the `browserContext.on('page')`, but only for popups relevant to this page. + * The earliest moment that page is available is when it has navigated to the initial url. For example, when opening a popup with `window.open('http://example.com')`, this event will fire when the network request to "http://example.com" is done and its response has started loading in the popup. + * ```js + * const [popup] = await Promise.all([ + * page.waitForEvent('popup'), + * page.evaluate(() => window.open('https://example.com')), + * ]); + * console.log(await popup.evaluate('location.href')); + * ``` + * + * **NOTE** Use `page.waitForLoadState([state[, options]])` to wait until the page gets to a particular state (you should not need it in most cases). + */ + on(event: 'popup', listener: (page: Page) => void): this; + + /** + * Emitted when a page issues a request. The request object is read-only. + * In order to intercept and mutate requests, see `page.route()` or `browserContext.route()`. + */ + on(event: 'request', listener: (request: Request) => void): this; + + /** + * Emitted when a request fails, for example by timing out. + * + * **NOTE** HTTP Error responses, such as 404 or 503, are still successful responses from HTTP standpoint, so request will complete with `'requestfinished'` event and not with `'requestfailed'`. + */ + on(event: 'requestfailed', listener: (request: Request) => void): this; + + /** + * Emitted when a request finishes successfully after downloading the response body. For a successful response, the sequence of events is `request`, `response` and `requestfinished`. + */ + on(event: 'requestfinished', listener: (request: Request) => void): this; + + /** + * Emitted when response status and headers are received for a request. For a successful response, the sequence of events is `request`, `response` and `requestfinished`. + */ + on(event: 'response', listener: (response: Response) => void): this; + + /** + * Emitted when request is sent. + */ + on(event: 'websocket', listener: (webSocket: WebSocket) => void): this; + + /** + * Emitted when a dedicated WebWorker is spawned by the page. + */ + on(event: 'worker', listener: (worker: Worker) => void): this; + + /** + * Emitted when the page closes. + */ + once(event: 'close', listener: () => void): this; + + /** + * Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also emitted if the page throws an error or a warning. + * The arguments passed into `console.log` appear as arguments on the event handler. + * An example of handling `console` event: + * ```js + * page.on('console', msg => { + * for (let i = 0; i < msg.args().length; ++i) + * console.log(`${i}: ${msg.args()[i]}`); + * }); + * page.evaluate(() => console.log('hello', 5, {foo: 'bar'})); + * ``` + */ + once(event: 'console', listener: (consoleMessage: ConsoleMessage) => void): this; + + /** + * Emitted when the page crashes. Browser pages might crash if they try to allocate too much memory. When the page crashes, ongoing and subsequent operations will throw. + * The most common way to deal with crashes is to catch an exception: + * ```js + * try { + * // Crash might happen during a click. + * await page.click('button'); + * // Or while waiting for an event. + * await page.waitForEvent('popup'); + * } catch (e) { + * // When the page crashes, exception message contains 'crash'. + * } + * ``` + * However, when manually listening to events, it might be useful to avoid stalling when the page crashes. In this case, handling `crash` event helps: + * ```js + * await new Promise((resolve, reject) => { + * page.on('requestfinished', async request => { + * if (await someProcessing(request)) + * resolve(request); + * }); + * page.on('crash', error => reject(error)); + * }); + * ``` + */ + once(event: 'crash', listener: () => void): this; + + /** + * Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Playwright can respond to the dialog via Dialog's accept or dismiss methods. + */ + once(event: 'dialog', listener: (dialog: Dialog) => void): this; + + /** + * Emitted when the JavaScript `DOMContentLoaded` event is dispatched. + */ + once(event: 'domcontentloaded', listener: () => void): this; + + /** + * Emitted when attachment download started. User can access basic file operations on downloaded content via the passed Download instance. + * + * **NOTE** Browser context **must** be created with the `acceptDownloads` set to `true` when user needs access to the downloaded content. If `acceptDownloads` is not set or set to `false`, download events are emitted, but the actual download is not performed and user has no access to the downloaded files. + */ + once(event: 'download', listener: (download: Download) => void): this; + + /** + * Emitted when a file chooser is supposed to appear, such as after clicking the ``. Playwright can respond to it via setting the input files using `fileChooser.setFiles` that can be uploaded after that. + * ```js + * page.on('filechooser', async (fileChooser) => { + * await fileChooser.setFiles('/tmp/myfile.pdf'); + * }); + * ``` + */ + once(event: 'filechooser', listener: (fileChooser: FileChooser) => void): this; + + /** + * Emitted when a frame is attached. + */ + once(event: 'frameattached', listener: (frame: Frame) => void): this; + + /** + * Emitted when a frame is detached. + */ + once(event: 'framedetached', listener: (frame: Frame) => void): this; + + /** + * Emitted when a frame is navigated to a new url. + */ + once(event: 'framenavigated', listener: (frame: Frame) => void): this; + + /** + * Emitted when the JavaScript `load` event is dispatched. + */ + once(event: 'load', listener: () => void): this; + + /** + * Emitted when an uncaught exception happens within the page. + */ + once(event: 'pageerror', listener: (error: Error) => void): this; + + /** + * Emitted when the page opens a new tab or window. This event is emitted in addition to the `browserContext.on('page')`, but only for popups relevant to this page. + * The earliest moment that page is available is when it has navigated to the initial url. For example, when opening a popup with `window.open('http://example.com')`, this event will fire when the network request to "http://example.com" is done and its response has started loading in the popup. + * ```js + * const [popup] = await Promise.all([ + * page.waitForEvent('popup'), + * page.evaluate(() => window.open('https://example.com')), + * ]); + * console.log(await popup.evaluate('location.href')); + * ``` + * + * **NOTE** Use `page.waitForLoadState([state[, options]])` to wait until the page gets to a particular state (you should not need it in most cases). + */ + once(event: 'popup', listener: (page: Page) => void): this; + + /** + * Emitted when a page issues a request. The request object is read-only. + * In order to intercept and mutate requests, see `page.route()` or `browserContext.route()`. + */ + once(event: 'request', listener: (request: Request) => void): this; + + /** + * Emitted when a request fails, for example by timing out. + * + * **NOTE** HTTP Error responses, such as 404 or 503, are still successful responses from HTTP standpoint, so request will complete with `'requestfinished'` event and not with `'requestfailed'`. + */ + once(event: 'requestfailed', listener: (request: Request) => void): this; + + /** + * Emitted when a request finishes successfully after downloading the response body. For a successful response, the sequence of events is `request`, `response` and `requestfinished`. + */ + once(event: 'requestfinished', listener: (request: Request) => void): this; + + /** + * Emitted when response status and headers are received for a request. For a successful response, the sequence of events is `request`, `response` and `requestfinished`. + */ + once(event: 'response', listener: (response: Response) => void): this; + + /** + * Emitted when request is sent. + */ + once(event: 'websocket', listener: (webSocket: WebSocket) => void): this; + + /** + * Emitted when a dedicated WebWorker is spawned by the page. + */ + once(event: 'worker', listener: (worker: Worker) => void): this; + + /** + * Emitted when the page closes. + */ + addListener(event: 'close', listener: () => void): this; + + /** + * Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also emitted if the page throws an error or a warning. + * The arguments passed into `console.log` appear as arguments on the event handler. + * An example of handling `console` event: + * ```js + * page.on('console', msg => { + * for (let i = 0; i < msg.args().length; ++i) + * console.log(`${i}: ${msg.args()[i]}`); + * }); + * page.evaluate(() => console.log('hello', 5, {foo: 'bar'})); + * ``` + */ + addListener(event: 'console', listener: (consoleMessage: ConsoleMessage) => void): this; + + /** + * Emitted when the page crashes. Browser pages might crash if they try to allocate too much memory. When the page crashes, ongoing and subsequent operations will throw. + * The most common way to deal with crashes is to catch an exception: + * ```js + * try { + * // Crash might happen during a click. + * await page.click('button'); + * // Or while waiting for an event. + * await page.waitForEvent('popup'); + * } catch (e) { + * // When the page crashes, exception message contains 'crash'. + * } + * ``` + * However, when manually listening to events, it might be useful to avoid stalling when the page crashes. In this case, handling `crash` event helps: + * ```js + * await new Promise((resolve, reject) => { + * page.on('requestfinished', async request => { + * if (await someProcessing(request)) + * resolve(request); + * }); + * page.on('crash', error => reject(error)); + * }); + * ``` + */ + addListener(event: 'crash', listener: () => void): this; + + /** + * Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Playwright can respond to the dialog via Dialog's accept or dismiss methods. + */ + addListener(event: 'dialog', listener: (dialog: Dialog) => void): this; + + /** + * Emitted when the JavaScript `DOMContentLoaded` event is dispatched. + */ + addListener(event: 'domcontentloaded', listener: () => void): this; + + /** + * Emitted when attachment download started. User can access basic file operations on downloaded content via the passed Download instance. + * + * **NOTE** Browser context **must** be created with the `acceptDownloads` set to `true` when user needs access to the downloaded content. If `acceptDownloads` is not set or set to `false`, download events are emitted, but the actual download is not performed and user has no access to the downloaded files. + */ + addListener(event: 'download', listener: (download: Download) => void): this; + + /** + * Emitted when a file chooser is supposed to appear, such as after clicking the ``. Playwright can respond to it via setting the input files using `fileChooser.setFiles` that can be uploaded after that. + * ```js + * page.on('filechooser', async (fileChooser) => { + * await fileChooser.setFiles('/tmp/myfile.pdf'); + * }); + * ``` + */ + addListener(event: 'filechooser', listener: (fileChooser: FileChooser) => void): this; + + /** + * Emitted when a frame is attached. + */ + addListener(event: 'frameattached', listener: (frame: Frame) => void): this; + + /** + * Emitted when a frame is detached. + */ + addListener(event: 'framedetached', listener: (frame: Frame) => void): this; + + /** + * Emitted when a frame is navigated to a new url. + */ + addListener(event: 'framenavigated', listener: (frame: Frame) => void): this; + + /** + * Emitted when the JavaScript `load` event is dispatched. + */ + addListener(event: 'load', listener: () => void): this; + + /** + * Emitted when an uncaught exception happens within the page. + */ + addListener(event: 'pageerror', listener: (error: Error) => void): this; + + /** + * Emitted when the page opens a new tab or window. This event is emitted in addition to the `browserContext.on('page')`, but only for popups relevant to this page. + * The earliest moment that page is available is when it has navigated to the initial url. For example, when opening a popup with `window.open('http://example.com')`, this event will fire when the network request to "http://example.com" is done and its response has started loading in the popup. + * ```js + * const [popup] = await Promise.all([ + * page.waitForEvent('popup'), + * page.evaluate(() => window.open('https://example.com')), + * ]); + * console.log(await popup.evaluate('location.href')); + * ``` + * + * **NOTE** Use `page.waitForLoadState([state[, options]])` to wait until the page gets to a particular state (you should not need it in most cases). + */ + addListener(event: 'popup', listener: (page: Page) => void): this; + + /** + * Emitted when a page issues a request. The request object is read-only. + * In order to intercept and mutate requests, see `page.route()` or `browserContext.route()`. + */ + addListener(event: 'request', listener: (request: Request) => void): this; + + /** + * Emitted when a request fails, for example by timing out. + * + * **NOTE** HTTP Error responses, such as 404 or 503, are still successful responses from HTTP standpoint, so request will complete with `'requestfinished'` event and not with `'requestfailed'`. + */ + addListener(event: 'requestfailed', listener: (request: Request) => void): this; + + /** + * Emitted when a request finishes successfully after downloading the response body. For a successful response, the sequence of events is `request`, `response` and `requestfinished`. + */ + addListener(event: 'requestfinished', listener: (request: Request) => void): this; + + /** + * Emitted when response status and headers are received for a request. For a successful response, the sequence of events is `request`, `response` and `requestfinished`. + */ + addListener(event: 'response', listener: (response: Response) => void): this; + + /** + * Emitted when request is sent. + */ + addListener(event: 'websocket', listener: (webSocket: WebSocket) => void): this; + + /** + * Emitted when a dedicated WebWorker is spawned by the page. + */ + addListener(event: 'worker', listener: (worker: Worker) => void): this; + + /** + * Emitted when the page closes. + */ + removeListener(event: 'close', listener: () => void): this; + + /** + * Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also emitted if the page throws an error or a warning. + * The arguments passed into `console.log` appear as arguments on the event handler. + * An example of handling `console` event: + * ```js + * page.on('console', msg => { + * for (let i = 0; i < msg.args().length; ++i) + * console.log(`${i}: ${msg.args()[i]}`); + * }); + * page.evaluate(() => console.log('hello', 5, {foo: 'bar'})); + * ``` + */ + removeListener(event: 'console', listener: (consoleMessage: ConsoleMessage) => void): this; + + /** + * Emitted when the page crashes. Browser pages might crash if they try to allocate too much memory. When the page crashes, ongoing and subsequent operations will throw. + * The most common way to deal with crashes is to catch an exception: + * ```js + * try { + * // Crash might happen during a click. + * await page.click('button'); + * // Or while waiting for an event. + * await page.waitForEvent('popup'); + * } catch (e) { + * // When the page crashes, exception message contains 'crash'. + * } + * ``` + * However, when manually listening to events, it might be useful to avoid stalling when the page crashes. In this case, handling `crash` event helps: + * ```js + * await new Promise((resolve, reject) => { + * page.on('requestfinished', async request => { + * if (await someProcessing(request)) + * resolve(request); + * }); + * page.on('crash', error => reject(error)); + * }); + * ``` + */ + removeListener(event: 'crash', listener: () => void): this; + + /** + * Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Playwright can respond to the dialog via Dialog's accept or dismiss methods. + */ + removeListener(event: 'dialog', listener: (dialog: Dialog) => void): this; + + /** + * Emitted when the JavaScript `DOMContentLoaded` event is dispatched. + */ + removeListener(event: 'domcontentloaded', listener: () => void): this; + + /** + * Emitted when attachment download started. User can access basic file operations on downloaded content via the passed Download instance. + * + * **NOTE** Browser context **must** be created with the `acceptDownloads` set to `true` when user needs access to the downloaded content. If `acceptDownloads` is not set or set to `false`, download events are emitted, but the actual download is not performed and user has no access to the downloaded files. + */ + removeListener(event: 'download', listener: (download: Download) => void): this; + + /** + * Emitted when a file chooser is supposed to appear, such as after clicking the ``. Playwright can respond to it via setting the input files using `fileChooser.setFiles` that can be uploaded after that. + * ```js + * page.on('filechooser', async (fileChooser) => { + * await fileChooser.setFiles('/tmp/myfile.pdf'); + * }); + * ``` + */ + removeListener(event: 'filechooser', listener: (fileChooser: FileChooser) => void): this; + + /** + * Emitted when a frame is attached. + */ + removeListener(event: 'frameattached', listener: (frame: Frame) => void): this; + + /** + * Emitted when a frame is detached. + */ + removeListener(event: 'framedetached', listener: (frame: Frame) => void): this; + + /** + * Emitted when a frame is navigated to a new url. + */ + removeListener(event: 'framenavigated', listener: (frame: Frame) => void): this; + + /** + * Emitted when the JavaScript `load` event is dispatched. + */ + removeListener(event: 'load', listener: () => void): this; + + /** + * Emitted when an uncaught exception happens within the page. + */ + removeListener(event: 'pageerror', listener: (error: Error) => void): this; + + /** + * Emitted when the page opens a new tab or window. This event is emitted in addition to the `browserContext.on('page')`, but only for popups relevant to this page. + * The earliest moment that page is available is when it has navigated to the initial url. For example, when opening a popup with `window.open('http://example.com')`, this event will fire when the network request to "http://example.com" is done and its response has started loading in the popup. + * ```js + * const [popup] = await Promise.all([ + * page.waitForEvent('popup'), + * page.evaluate(() => window.open('https://example.com')), + * ]); + * console.log(await popup.evaluate('location.href')); + * ``` + * + * **NOTE** Use `page.waitForLoadState([state[, options]])` to wait until the page gets to a particular state (you should not need it in most cases). + */ + removeListener(event: 'popup', listener: (page: Page) => void): this; + + /** + * Emitted when a page issues a request. The request object is read-only. + * In order to intercept and mutate requests, see `page.route()` or `browserContext.route()`. + */ + removeListener(event: 'request', listener: (request: Request) => void): this; + + /** + * Emitted when a request fails, for example by timing out. + * + * **NOTE** HTTP Error responses, such as 404 or 503, are still successful responses from HTTP standpoint, so request will complete with `'requestfinished'` event and not with `'requestfailed'`. + */ + removeListener(event: 'requestfailed', listener: (request: Request) => void): this; + + /** + * Emitted when a request finishes successfully after downloading the response body. For a successful response, the sequence of events is `request`, `response` and `requestfinished`. + */ + removeListener(event: 'requestfinished', listener: (request: Request) => void): this; + + /** + * Emitted when response status and headers are received for a request. For a successful response, the sequence of events is `request`, `response` and `requestfinished`. + */ + removeListener(event: 'response', listener: (response: Response) => void): this; + + /** + * Emitted when request is sent. + */ + removeListener(event: 'websocket', listener: (webSocket: WebSocket) => void): this; + + /** + * Emitted when a dedicated WebWorker is spawned by the page. + */ + removeListener(event: 'worker', listener: (worker: Worker) => void): this; + + /** + * Emitted when the page closes. + */ + off(event: 'close', listener: () => void): this; + + /** + * Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also emitted if the page throws an error or a warning. + * The arguments passed into `console.log` appear as arguments on the event handler. + * An example of handling `console` event: + * ```js + * page.on('console', msg => { + * for (let i = 0; i < msg.args().length; ++i) + * console.log(`${i}: ${msg.args()[i]}`); + * }); + * page.evaluate(() => console.log('hello', 5, {foo: 'bar'})); + * ``` + */ + off(event: 'console', listener: (consoleMessage: ConsoleMessage) => void): this; + + /** + * Emitted when the page crashes. Browser pages might crash if they try to allocate too much memory. When the page crashes, ongoing and subsequent operations will throw. + * The most common way to deal with crashes is to catch an exception: + * ```js + * try { + * // Crash might happen during a click. + * await page.click('button'); + * // Or while waiting for an event. + * await page.waitForEvent('popup'); + * } catch (e) { + * // When the page crashes, exception message contains 'crash'. + * } + * ``` + * However, when manually listening to events, it might be useful to avoid stalling when the page crashes. In this case, handling `crash` event helps: + * ```js + * await new Promise((resolve, reject) => { + * page.on('requestfinished', async request => { + * if (await someProcessing(request)) + * resolve(request); + * }); + * page.on('crash', error => reject(error)); + * }); + * ``` + */ + off(event: 'crash', listener: () => void): this; + + /** + * Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Playwright can respond to the dialog via Dialog's accept or dismiss methods. + */ + off(event: 'dialog', listener: (dialog: Dialog) => void): this; + + /** + * Emitted when the JavaScript `DOMContentLoaded` event is dispatched. + */ + off(event: 'domcontentloaded', listener: () => void): this; + + /** + * Emitted when attachment download started. User can access basic file operations on downloaded content via the passed Download instance. + * + * **NOTE** Browser context **must** be created with the `acceptDownloads` set to `true` when user needs access to the downloaded content. If `acceptDownloads` is not set or set to `false`, download events are emitted, but the actual download is not performed and user has no access to the downloaded files. + */ + off(event: 'download', listener: (download: Download) => void): this; + + /** + * Emitted when a file chooser is supposed to appear, such as after clicking the ``. Playwright can respond to it via setting the input files using `fileChooser.setFiles` that can be uploaded after that. + * ```js + * page.on('filechooser', async (fileChooser) => { + * await fileChooser.setFiles('/tmp/myfile.pdf'); + * }); + * ``` + */ + off(event: 'filechooser', listener: (fileChooser: FileChooser) => void): this; + + /** + * Emitted when a frame is attached. + */ + off(event: 'frameattached', listener: (frame: Frame) => void): this; + + /** + * Emitted when a frame is detached. + */ + off(event: 'framedetached', listener: (frame: Frame) => void): this; + + /** + * Emitted when a frame is navigated to a new url. + */ + off(event: 'framenavigated', listener: (frame: Frame) => void): this; + + /** + * Emitted when the JavaScript `load` event is dispatched. + */ + off(event: 'load', listener: () => void): this; + + /** + * Emitted when an uncaught exception happens within the page. + */ + off(event: 'pageerror', listener: (error: Error) => void): this; + + /** + * Emitted when the page opens a new tab or window. This event is emitted in addition to the `browserContext.on('page')`, but only for popups relevant to this page. + * The earliest moment that page is available is when it has navigated to the initial url. For example, when opening a popup with `window.open('http://example.com')`, this event will fire when the network request to "http://example.com" is done and its response has started loading in the popup. + * ```js + * const [popup] = await Promise.all([ + * page.waitForEvent('popup'), + * page.evaluate(() => window.open('https://example.com')), + * ]); + * console.log(await popup.evaluate('location.href')); + * ``` + * + * **NOTE** Use `page.waitForLoadState([state[, options]])` to wait until the page gets to a particular state (you should not need it in most cases). + */ + off(event: 'popup', listener: (page: Page) => void): this; + + /** + * Emitted when a page issues a request. The request object is read-only. + * In order to intercept and mutate requests, see `page.route()` or `browserContext.route()`. + */ + off(event: 'request', listener: (request: Request) => void): this; + + /** + * Emitted when a request fails, for example by timing out. + * + * **NOTE** HTTP Error responses, such as 404 or 503, are still successful responses from HTTP standpoint, so request will complete with `'requestfinished'` event and not with `'requestfailed'`. + */ + off(event: 'requestfailed', listener: (request: Request) => void): this; + + /** + * Emitted when a request finishes successfully after downloading the response body. For a successful response, the sequence of events is `request`, `response` and `requestfinished`. + */ + off(event: 'requestfinished', listener: (request: Request) => void): this; + + /** + * Emitted when response status and headers are received for a request. For a successful response, the sequence of events is `request`, `response` and `requestfinished`. + */ + off(event: 'response', listener: (response: Response) => void): this; + + /** + * Emitted when request is sent. + */ + off(event: 'websocket', listener: (webSocket: WebSocket) => void): this; + + /** + * Emitted when a dedicated WebWorker is spawned by the page. + */ + off(event: 'worker', listener: (worker: Worker) => void): this; + + accessibility: Accessibility; + + /** + * Adds a script which would be evaluated in one of the following scenarios: + * + * Whenever the page is navigated. + * Whenever the child frame is attached or navigated. In this case, the script is evaluated in the context of the newly attached frame. + * + * The script is evaluated after the document was created but before any of its scripts were run. This is useful to amend the JavaScript environment, e.g. to seed `Math.random`. + * An example of overriding `Math.random` before the page loads: + * ```js + * // preload.js + * Math.random = () => 42; + * + * // In your playwright script, assuming the preload.js file is in same folder + * const preloadFile = fs.readFileSync('./preload.js', 'utf8'); + * await page.addInitScript(preloadFile); + * ``` + * + * **NOTE** The order of evaluation of multiple scripts installed via browserContext.addInitScript(script[, arg]) and page.addInitScript(script[, arg]) is not defined. + * @param script Script to be evaluated in the page. + * @param arg Optional argument to pass to `script` (only supported when passing a function). + */ + addInitScript(script: Function|string|{ + /** + * Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to current working directory. + */ + path?: string; + + /** + * Raw script content. + */ + content?: string; + }, arg?: Serializable): Promise; + + /** + * Adds a ` + * + *
+ * `); + * await page.click('button'); + * })(); + * ``` + * An example of adding a `window.readfile` function to the page: + * ```js + * const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'. + * const fs = require('fs'); + * + * (async () => { + * const browser = await chromium.launch(); + * const page = await browser.newPage(); + * page.on('console', msg => console.log(msg.text())); + * await page.exposeFunction('readfile', async filePath => { + * return new Promise((resolve, reject) => { + * fs.readFile(filePath, 'utf8', (err, text) => { + * if (err) + * reject(err); + * else + * resolve(text); + * }); + * }); + * }); + * await page.evaluate(async () => { + * // use window.readfile to read contents of a file + * const content = await window.readfile('/etc/hosts'); + * console.log(content); + * }); + * await browser.close(); + * })(); + * ``` + * @param name Name of the function on the window object + * @param playwrightFunction Callback function which will be called in Playwright's context. + */ + exposeFunction(name: string, playwrightFunction: Function): Promise; + + /** + * This method waits for an element matching `selector`, waits for actionability checks, focuses the element, fills it and triggers an `input` event after filling. + * If the element matching `selector` is not an ``, `