feat(websocket): implement Web Sockets for Chromium & WebKit (#4234)

This commit is contained in:
Pavel Feldman 2020-10-26 22:20:43 -07:00 committed by GitHub
parent 00d6313f6d
commit be8428475d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 7929 additions and 9 deletions

View file

@ -23,6 +23,7 @@
- [class: Response](#class-response) - [class: Response](#class-response)
- [class: Selectors](#class-selectors) - [class: Selectors](#class-selectors)
- [class: Route](#class-route) - [class: Route](#class-route)
- [class: WebSocket](#class-websocket)
- [class: TimeoutError](#class-timeouterror) - [class: TimeoutError](#class-timeouterror)
- [class: Accessibility](#class-accessibility) - [class: Accessibility](#class-accessibility)
- [class: Worker](#class-worker) - [class: Worker](#class-worker)
@ -738,6 +739,7 @@ page.removeListener('request', logRequest);
- [event: 'requestfailed'](#event-requestfailed) - [event: 'requestfailed'](#event-requestfailed)
- [event: 'requestfinished'](#event-requestfinished) - [event: 'requestfinished'](#event-requestfinished)
- [event: 'response'](#event-response) - [event: 'response'](#event-response)
- [event: 'websocket'](#event-websocket)
- [event: 'worker'](#event-worker) - [event: 'worker'](#event-worker)
- [page.$(selector)](#pageselector) - [page.$(selector)](#pageselector)
- [page.$$(selector)](#pageselector-1) - [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`. 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' #### event: 'worker'
- <[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. - returns: <[Request]> A request to be routed.
### class: WebSocket
The [WebSocket] class represents websocket connections in the page.
<!-- GEN:toc -->
- [event: 'close'](#event-close-2)
- [event: 'framereceived'](#event-framereceived)
- [event: 'framesent'](#event-framesent)
- [event: 'socketerror'](#event-socketerror)
- [webSocket.url()](#websocketurl)
<!-- GEN:stop -->
#### 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 ### class: TimeoutError
* extends: [Error] * extends: [Error]
@ -4233,7 +4279,7 @@ for (const worker of page.workers())
``` ```
<!-- GEN:toc --> <!-- GEN:toc -->
- [event: 'close'](#event-close-2) - [event: 'close'](#event-close-3)
- [worker.evaluate(pageFunction[, arg])](#workerevaluatepagefunction-arg) - [worker.evaluate(pageFunction[, arg])](#workerevaluatepagefunction-arg)
- [worker.evaluateHandle(pageFunction[, arg])](#workerevaluatehandlepagefunction-arg) - [worker.evaluateHandle(pageFunction[, arg])](#workerevaluatehandlepagefunction-arg)
- [worker.url()](#workerurl) - [worker.url()](#workerurl)
@ -4269,7 +4315,7 @@ If the function passed to the `worker.evaluateHandle` returns a [Promise], then
### class: BrowserServer ### class: BrowserServer
<!-- GEN:toc --> <!-- GEN:toc -->
- [event: 'close'](#event-close-3) - [event: 'close'](#event-close-4)
- [browserServer.close()](#browserserverclose) - [browserServer.close()](#browserserverclose)
- [browserServer.kill()](#browserserverkill) - [browserServer.kill()](#browserserverkill)
- [browserServer.process()](#browserserverprocess) - [browserServer.process()](#browserserverprocess)

View file

@ -29,7 +29,7 @@ export { TimeoutError } from '../utils/errors';
export { Frame } from './frame'; export { Frame } from './frame';
export { Keyboard, Mouse, Touchscreen } from './input'; export { Keyboard, Mouse, Touchscreen } from './input';
export { JSHandle } from './jsHandle'; export { JSHandle } from './jsHandle';
export { Request, Response, Route } from './network'; export { Request, Response, Route, WebSocket } from './network';
export { Page } from './page'; export { Page } from './page';
export { Selectors } from './selectors'; export { Selectors } from './selectors';
export { Video } from './video'; export { Video } from './video';

View file

@ -21,7 +21,7 @@ import { ChannelOwner } from './channelOwner';
import { ElementHandle } from './elementHandle'; import { ElementHandle } from './elementHandle';
import { Frame } from './frame'; import { Frame } from './frame';
import { JSHandle } from './jsHandle'; import { JSHandle } from './jsHandle';
import { Request, Response, Route } from './network'; import { Request, Response, Route, WebSocket } from './network';
import { Page, BindingCall } from './page'; import { Page, BindingCall } from './page';
import { Worker } from './worker'; import { Worker } from './worker';
import { ConsoleMessage } from './consoleMessage'; import { ConsoleMessage } from './consoleMessage';
@ -226,6 +226,9 @@ export class Connection {
case 'Selectors': case 'Selectors':
result = new SelectorsOwner(parent, type, guid, initializer); result = new SelectorsOwner(parent, type, guid, initializer);
break; break;
case 'WebSocket':
result = new WebSocket(parent, type, guid, initializer);
break;
case 'Worker': case 'Worker':
result = new Worker(parent, type, guid, initializer); result = new Worker(parent, type, guid, initializer);
break; break;

View file

@ -49,9 +49,17 @@ export const Events = {
FrameNavigated: 'framenavigated', FrameNavigated: 'framenavigated',
Load: 'load', Load: 'load',
Popup: 'popup', Popup: 'popup',
WebSocket: 'websocket',
Worker: 'worker', Worker: 'worker',
}, },
WebSocket: {
Close: 'close',
Error: 'socketerror',
FrameReceived: 'framereceived',
FrameSent: 'framesent',
},
Worker: { Worker: {
Close: 'close', Close: 'close',
}, },

View file

@ -23,6 +23,7 @@ import * as fs from 'fs';
import * as mime from 'mime'; import * as mime from 'mime';
import * as util from 'util'; import * as util from 'util';
import { isString, headersObjectToArray, headersArrayToObject } from '../utils/utils'; import { isString, headersObjectToArray, headersArrayToObject } from '../utils/utils';
import { Events } from './events';
export type NetworkCookie = { export type NetworkCookie = {
name: string, name: string,
@ -312,6 +313,30 @@ export class Response extends ChannelOwner<channels.ResponseChannel, channels.Re
} }
} }
export class WebSocket extends ChannelOwner<channels.WebSocketChannel, channels.WebSocketInitializer> {
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) { export function validateHeaders(headers: Headers) {
for (const key of Object.keys(headers)) { for (const key of Object.keys(headers)) {
const value = headers[key]; const value = headers[key];

View file

@ -31,7 +31,7 @@ import { Worker } from './worker';
import { Frame, verifyLoadState, WaitForNavigationOptions } from './frame'; import { Frame, verifyLoadState, WaitForNavigationOptions } from './frame';
import { Keyboard, Mouse, Touchscreen } from './input'; import { Keyboard, Mouse, Touchscreen } from './input';
import { assertMaxArguments, Func1, FuncOn, SmartHandle, serializeArgument, parseResult, JSHandle } from './jsHandle'; 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 { FileChooser } from './fileChooser';
import { Buffer } from 'buffer'; import { Buffer } from 'buffer';
import { ChromiumCoverage } from './chromiumCoverage'; import { ChromiumCoverage } from './chromiumCoverage';
@ -130,6 +130,7 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
this._channel.on('response', ({ response }) => this.emit(Events.Page.Response, Response.from(response))); this._channel.on('response', ({ response }) => 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('route', ({ route, request }) => this._onRoute(Route.from(route), Request.from(request)));
this._channel.on('video', ({ relativePath }) => this.video()!._setRelativePath(relativePath)); 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))); this._channel.on('worker', ({ worker }) => this._onWorker(Worker.from(worker)));
if (this._browserContext._browserName === 'chromium') { if (this._browserContext._browserName === 'chromium') {

View file

@ -14,7 +14,7 @@
* limitations under the License. * 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 * as channels from '../protocol/channels';
import { Dispatcher, DispatcherScope, lookupNullableDispatcher, existingDispatcher } from './dispatcher'; import { Dispatcher, DispatcherScope, lookupNullableDispatcher, existingDispatcher } from './dispatcher';
import { FrameDispatcher } from './frameDispatcher'; import { FrameDispatcher } from './frameDispatcher';
@ -98,3 +98,15 @@ export class RouteDispatcher extends Dispatcher<Route, channels.RouteInitializer
await this._object.abort(params.errorCode || 'failed'); await this._object.abort(params.errorCode || 'failed');
} }
} }
export class WebSocketDispatcher extends Dispatcher<WebSocket, channels.WebSocketInitializer> 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', {}));
}
}

View file

@ -25,7 +25,7 @@ import { ConsoleMessageDispatcher } from './consoleMessageDispatcher';
import { DialogDispatcher } from './dialogDispatcher'; import { DialogDispatcher } from './dialogDispatcher';
import { DownloadDispatcher } from './downloadDispatcher'; import { DownloadDispatcher } from './downloadDispatcher';
import { FrameDispatcher } from './frameDispatcher'; import { FrameDispatcher } from './frameDispatcher';
import { RequestDispatcher, ResponseDispatcher, RouteDispatcher } from './networkDispatchers'; import { RequestDispatcher, ResponseDispatcher, RouteDispatcher, WebSocketDispatcher } from './networkDispatchers';
import { serializeResult, parseArgument, JSHandleDispatcher } from './jsHandleDispatcher'; import { serializeResult, parseArgument, JSHandleDispatcher } from './jsHandleDispatcher';
import { ElementHandleDispatcher, createHandle } from './elementHandlerDispatcher'; import { ElementHandleDispatcher, createHandle } from './elementHandlerDispatcher';
import { FileChooser } from '../server/fileChooser'; import { FileChooser } from '../server/fileChooser';
@ -72,6 +72,7 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> i
})); }));
page.on(Page.Events.Response, response => this._dispatchEvent('response', { response: new ResponseDispatcher(this._scope, response) })); 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.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) })); page.on(Page.Events.Worker, worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this._scope, worker) }));
} }

View file

@ -694,6 +694,7 @@ export interface PageChannel extends Channel {
on(event: 'response', callback: (params: PageResponseEvent) => void): this; on(event: 'response', callback: (params: PageResponseEvent) => void): this;
on(event: 'route', callback: (params: PageRouteEvent) => void): this; on(event: 'route', callback: (params: PageRouteEvent) => void): this;
on(event: 'video', callback: (params: PageVideoEvent) => 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; on(event: 'worker', callback: (params: PageWorkerEvent) => void): this;
setDefaultNavigationTimeoutNoReply(params: PageSetDefaultNavigationTimeoutNoReplyParams, metadata?: Metadata): Promise<PageSetDefaultNavigationTimeoutNoReplyResult>; setDefaultNavigationTimeoutNoReply(params: PageSetDefaultNavigationTimeoutNoReplyParams, metadata?: Metadata): Promise<PageSetDefaultNavigationTimeoutNoReplyResult>;
setDefaultTimeoutNoReply(params: PageSetDefaultTimeoutNoReplyParams, metadata?: Metadata): Promise<PageSetDefaultTimeoutNoReplyResult>; setDefaultTimeoutNoReply(params: PageSetDefaultTimeoutNoReplyParams, metadata?: Metadata): Promise<PageSetDefaultTimeoutNoReplyResult>;
@ -782,6 +783,9 @@ export type PageRouteEvent = {
export type PageVideoEvent = { export type PageVideoEvent = {
relativePath: string, relativePath: string,
}; };
export type PageWebSocketEvent = {
webSocket: WebSocketChannel,
};
export type PageWorkerEvent = { export type PageWorkerEvent = {
worker: WorkerChannel, worker: WorkerChannel,
}; };
@ -2185,6 +2189,31 @@ export type ResponseFinishedResult = {
error?: string, 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 ----------- // ----------- ConsoleMessage -----------
export type ConsoleMessageInitializer = { export type ConsoleMessageInitializer = {
type: string, type: string,

View file

@ -945,6 +945,10 @@ Page:
parameters: parameters:
relativePath: string relativePath: string
webSocket:
parameters:
webSocket: WebSocket
worker: worker:
parameters: parameters:
worker: Worker worker: Worker
@ -1844,6 +1848,32 @@ Response:
error: string? 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: ConsoleMessage:
type: interface type: interface

View file

@ -55,6 +55,13 @@ export class CRNetworkManager {
helper.addEventListener(session, 'Network.responseReceived', this._onResponseReceived.bind(this)), helper.addEventListener(session, 'Network.responseReceived', this._onResponseReceived.bind(this)),
helper.addEventListener(session, 'Network.loadingFinished', this._onLoadingFinished.bind(this)), helper.addEventListener(session, 'Network.loadingFinished', this._onLoadingFinished.bind(this)),
helper.addEventListener(session, 'Network.loadingFailed', this._onLoadingFailed.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)),
]; ];
} }

View file

@ -69,6 +69,7 @@ export class FrameManager {
private _mainFrame: Frame; private _mainFrame: Frame;
readonly _consoleMessageTags = new Map<string, ConsoleTagHandler>(); readonly _consoleMessageTags = new Map<string, ConsoleTagHandler>();
readonly _signalBarriers = new Set<SignalBarrier>(); readonly _signalBarriers = new Set<SignalBarrier>();
private _webSockets = new Map<string, network.WebSocket>();
constructor(page: Page) { constructor(page: Page) {
this._page = page; this._page = page;
@ -165,6 +166,7 @@ export class FrameManager {
frameCommittedNewDocumentNavigation(frameId: string, url: string, name: string, documentId: string, initial: boolean) { frameCommittedNewDocumentNavigation(frameId: string, url: string, name: string, documentId: string, initial: boolean) {
const frame = this._frames.get(frameId)!; const frame = this._frames.get(frameId)!;
this.removeChildFramesRecursively(frame); this.removeChildFramesRecursively(frame);
this.clearWebSockets(frame);
frame._url = url; frame._url = url;
frame._name = name; frame._name = name;
@ -328,6 +330,57 @@ export class FrameManager {
handler(); handler();
return true; 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 { export class Frame extends EventEmitter {

View file

@ -17,6 +17,7 @@
import * as frames from './frames'; import * as frames from './frames';
import * as types from './types'; import * as types from './types';
import { assert } from '../utils/utils'; import { assert } from '../utils/utils';
import { EventEmitter } from 'events';
export function filterCookies(cookies: types.NetworkCookie[], urls: string[]): types.NetworkCookie[] { export function filterCookies(cookies: types.NetworkCookie[], urls: string[]): types.NetworkCookie[] {
const parsedURLs = urls.map(s => new URL(s)); 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 { export interface RouteDelegate {
abort(errorCode: string): Promise<void>; abort(errorCode: string): Promise<void>;
fulfill(response: types.NormalizedFulfillResponse): Promise<void>; fulfill(response: types.NormalizedFulfillResponse): Promise<void>;

View file

@ -113,6 +113,7 @@ export class Page extends EventEmitter {
FrameNavigated: 'framenavigated', FrameNavigated: 'framenavigated',
Load: 'load', Load: 'load',
Popup: 'popup', Popup: 'popup',
WebSocket: 'websocket',
Worker: 'worker', Worker: 'worker',
VideoStarted: 'videostarted', VideoStarted: 'videostarted',
}; };

View file

@ -357,6 +357,13 @@ export class WKPage implements PageDelegate {
helper.addEventListener(this._session, 'Network.responseReceived', e => this._onResponseReceived(e)), 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.loadingFinished', e => this._onLoadingFinished(e)),
helper.addEventListener(this._session, 'Network.loadingFailed', e => this._onLoadingFailed(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)),
]; ];
} }

View file

@ -34,6 +34,16 @@ if (browserName !== 'chromium') {
api.delete('cDPSession.detach'); 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 // Some permissions tests are disabled in webkit. See permissions.jest.js
if (browserName === 'webkit') if (browserName === 'webkit')
api.delete('browserContext.clearPermissions'); api.delete('browserContext.clearPermissions');

104
test/web-socket.spec.ts Normal file
View file

@ -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<ws://localhost:${server.PORT}/ws>: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<outgoing>:received<incoming>: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');
});
});

7544
types/types.d.ts vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -181,7 +181,7 @@ class MDOutline {
const ul = content.querySelector('ul'); const ul = content.querySelector('ul');
for (const element of content.querySelectorAll('h4 + ul > li')) { for (const element of content.querySelectorAll('h4 + ul > li')) {
if (element.matches('li') && element.textContent.trim().startsWith('<')) { if (element.matches('li') && element.textContent.trim().startsWith('<')) {
returnType = parseProperty(element, false); returnType = parseProperty(element, element.textContent.trim().includes('data'));
} else if (element.matches('li') && element.firstChild.matches && element.firstChild.matches('code')) { } else if (element.matches('li') && element.firstChild.matches && element.firstChild.matches('code')) {
const property = parseProperty(element, false); const property = parseProperty(element, false);
property.required = !optionalparams.has(property.name) && !property.name.startsWith('...'); property.required = !optionalparams.has(property.name) && !property.name.startsWith('...');

View file

@ -125,7 +125,9 @@ function classToString(classDesc) {
function argNameForType(type) { function argNameForType(type) {
if (type === 'void') if (type === 'void')
return null; return null;
return type[0].toLowerCase() + type.slice(1); if (type.includes('{'))
return 'data';
return (type[0].toLowerCase() + type.slice(1)).replace(/\|/g, 'Or');
} }
/** /**