feat(websocket): implement Web Sockets for Chromium & WebKit (#4234)
This commit is contained in:
parent
00d6313f6d
commit
be8428475d
50
docs/api.md
50
docs/api.md
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
|
|
@ -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') {
|
||||||
|
|
|
||||||
|
|
@ -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', {}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
104
test/web-socket.spec.ts
Normal 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
7544
types/types.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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('...');
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue