chore(agnostification): agnostify web socket connections (#6520)

This PR updates the socket transport code to swap between a Node web
socket transport or a web one based on the `isNode` environment. It also
adds unit tests to the browser tests that show we can connect in a
browser.
This commit is contained in:
Jack Franklin 2020-10-19 10:32:41 +01:00 committed by GitHub
parent c2c2bb7e55
commit f63a123ece
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 151 additions and 49 deletions

View File

@ -19,9 +19,9 @@ import { Browser } from './Browser.js';
import { assert } from './assert.js'; import { assert } from './assert.js';
import { debugError } from '../common/helper.js'; import { debugError } from '../common/helper.js';
import { Connection } from './Connection.js'; import { Connection } from './Connection.js';
import { WebSocketTransport } from './WebSocketTransport.js';
import { getFetch } from './fetch.js'; import { getFetch } from './fetch.js';
import { Viewport } from './PuppeteerViewport.js'; import { Viewport } from './PuppeteerViewport.js';
import { isNode } from '../environment.js';
/** /**
* Generic browser options that can be passed when launching any browser. * Generic browser options that can be passed when launching any browser.
@ -33,6 +33,13 @@ export interface BrowserOptions {
slowMo?: number; slowMo?: number;
} }
const getWebSocketTransportClass = async () => {
return isNode
? (await import('../node/NodeWebSocketTransport.js')).NodeWebSocketTransport
: (await import('./BrowserWebSocketTransport.js'))
.BrowserWebSocketTransport;
};
/** /**
* Users should never call this directly; it's called when calling * Users should never call this directly; it's called when calling
* `puppeteer.connect`. * `puppeteer.connect`.
@ -44,7 +51,7 @@ export const connectToBrowser = async (
browserURL?: string; browserURL?: string;
transport?: ConnectionTransport; transport?: ConnectionTransport;
} }
) => { ): Promise<Browser> => {
const { const {
browserWSEndpoint, browserWSEndpoint,
browserURL, browserURL,
@ -64,13 +71,17 @@ export const connectToBrowser = async (
if (transport) { if (transport) {
connection = new Connection('', transport, slowMo); connection = new Connection('', transport, slowMo);
} else if (browserWSEndpoint) { } else if (browserWSEndpoint) {
const connectionTransport = await WebSocketTransport.create( const WebSocketClass = await getWebSocketTransportClass();
const connectionTransport: ConnectionTransport = await WebSocketClass.create(
browserWSEndpoint browserWSEndpoint
); );
connection = new Connection(browserWSEndpoint, connectionTransport, slowMo); connection = new Connection(browserWSEndpoint, connectionTransport, slowMo);
} else if (browserURL) { } else if (browserURL) {
const connectionURL = await getWSEndpoint(browserURL); const connectionURL = await getWSEndpoint(browserURL);
const connectionTransport = await WebSocketTransport.create(connectionURL); const WebSocketClass = await getWebSocketTransportClass();
const connectionTransport: ConnectionTransport = await WebSocketClass.create(
connectionURL
);
connection = new Connection(connectionURL, connectionTransport, slowMo); connection = new Connection(connectionURL, connectionTransport, slowMo);
} }

View File

@ -0,0 +1,55 @@
/**
* Copyright 2020 Google Inc. All rights reserved.
*
* 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 { ConnectionTransport } from './ConnectionTransport.js';
export class BrowserWebSocketTransport implements ConnectionTransport {
static create(url: string): Promise<BrowserWebSocketTransport> {
return new Promise((resolve, reject) => {
const ws = new WebSocket(url);
ws.addEventListener('open', () =>
resolve(new BrowserWebSocketTransport(ws))
);
ws.addEventListener('error', reject);
});
}
private _ws: WebSocket;
onmessage?: (message: string) => void;
onclose?: () => void;
constructor(ws: WebSocket) {
this._ws = ws;
this._ws.addEventListener('message', (event) => {
if (this.onmessage) this.onmessage.call(null, event.data);
});
this._ws.addEventListener('close', () => {
if (this.onclose) this.onclose.call(null);
});
// Silently ignore all errors - we don't know what to do with them.
this._ws.addEventListener('error', () => {});
this.onmessage = null;
this.onclose = null;
}
send(message: string): void {
this._ws.send(message);
}
close(): void {
this._ws.close();
}
}

View File

@ -22,7 +22,7 @@ import { assert } from '../common/assert.js';
import { helper, debugError } from '../common/helper.js'; import { helper, debugError } from '../common/helper.js';
import { LaunchOptions } from './LaunchOptions.js'; import { LaunchOptions } from './LaunchOptions.js';
import { Connection } from '../common/Connection.js'; import { Connection } from '../common/Connection.js';
import { WebSocketTransport } from '../common/WebSocketTransport.js'; import { NodeWebSocketTransport as WebSocketTransport } from '../node/NodeWebSocketTransport.js';
import { PipeTransport } from './PipeTransport.js'; import { PipeTransport } from './PipeTransport.js';
import * as readline from 'readline'; import * as readline from 'readline';
import { TimeoutError } from '../common/Errors.js'; import { TimeoutError } from '../common/Errors.js';

View File

@ -13,23 +13,25 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { ConnectionTransport } from './ConnectionTransport.js'; import { ConnectionTransport } from '../common/ConnectionTransport.js';
import NodeWebSocket from 'ws'; import NodeWebSocket from 'ws';
export class WebSocketTransport implements ConnectionTransport { export class NodeWebSocketTransport implements ConnectionTransport {
static create(url: string): Promise<WebSocketTransport> { static create(url: string): Promise<NodeWebSocketTransport> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const ws = new NodeWebSocket(url, [], { const ws = new NodeWebSocket(url, [], {
perMessageDeflate: false, perMessageDeflate: false,
maxPayload: 256 * 1024 * 1024, // 256Mb maxPayload: 256 * 1024 * 1024, // 256Mb
}); });
ws.addEventListener('open', () => resolve(new WebSocketTransport(ws))); ws.addEventListener('open', () =>
resolve(new NodeWebSocketTransport(ws))
);
ws.addEventListener('error', reject); ws.addEventListener('error', reject);
}); });
} }
_ws: NodeWebSocket; private _ws: NodeWebSocket;
onmessage?: (message: string) => void; onmessage?: (message: string) => void;
onclose?: () => void; onclose?: () => void;
@ -47,7 +49,7 @@ export class WebSocketTransport implements ConnectionTransport {
this.onclose = null; this.onclose = null;
} }
send(message): void { send(message: string): void {
this._ws.send(message); this._ws.send(message);
} }

View File

@ -14,48 +14,42 @@
* limitations under the License. * limitations under the License.
*/ */
import { Connection } from '../lib/esm/puppeteer/common/Connection.js'; import { Connection } from '../lib/esm/puppeteer/common/Connection.js';
import { BrowserWebSocketTransport } from '../lib/esm/puppeteer/common/BrowserWebSocketTransport.js';
import puppeteer from '../lib/esm/puppeteer/web.js';
import expect from '../node_modules/expect/build-es5/index.js'; import expect from '../node_modules/expect/build-es5/index.js';
import { getWebSocketEndpoint } from './helper.js';
/** describe('creating a Connection', () => {
* A fake transport that echoes the message it got back and pretends to have got a result. it('can create a real connection to the backend and send messages', async () => {
* const wsUrl = getWebSocketEndpoint();
* In actual pptr code we expect that `result` is returned from the message const transport = await BrowserWebSocketTransport.create(wsUrl);
* being sent with some data, so we fake that in the `send` method.
*
* We don't define `onmessage` here because Puppeteer's Connection class will
* define an `onmessage` for us.
*/
class EchoTransport {
send(message) {
const object = JSON.parse(message);
const fakeMessageResult = {
result: `fake-test-result-${object.method}`,
};
this.onmessage(
JSON.stringify({
...object,
...fakeMessageResult,
})
);
}
close() {} const connection = new Connection(wsUrl, transport);
} const result = await connection.send('Browser.getVersion');
/* We can't expect exact results as the version of Chrome/CDP might change
describe('Connection', () => { * and we don't want flakey tests, so let's assert the structure, which is
it('can be created in the browser and send/receive messages', async () => { * enough to confirm the result was recieved successfully.
let receivedOutput = '';
const connection = new Connection('fake-url', new EchoTransport());
/**
* Puppeteer increments a counter from 0 for each
* message it sends So we have to register a callback for the object with
* the ID of `1` as the message we send will be the first.
*/ */
connection._callbacks.set(1, { expect(result).toEqual({
resolve: (data) => (receivedOutput = data), protocolVersion: expect.any(String),
jsVersion: expect.any(String),
revision: expect.any(String),
userAgent: expect.any(String),
product: expect.any(String),
}); });
connection.send('Browser.getVersion'); });
expect(receivedOutput).toEqual('fake-test-result-Browser.getVersion'); });
describe('puppeteer.connect', () => {
it('can connect over websocket and make requests to the backend', async () => {
const wsUrl = getWebSocketEndpoint();
const browser = await puppeteer.connect({
browserWSEndpoint: wsUrl,
});
const version = await browser.version();
const versionLooksCorrect = /.+Chrome\/\d{2}/.test(version);
expect(version).toEqual(expect.any(String));
expect(versionLooksCorrect).toEqual(true);
}); });
}); });

27
test-browser/helper.js Normal file
View File

@ -0,0 +1,27 @@
/**
* Copyright 2020 Google Inc. All rights reserved.
*
* 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.
*/
/**
* Returns the web socket endpoint for the backend of the browser the tests run
* in. Used to create connections to that browser in Puppeteer for unit tests.
*
* It's available on window.__ENV__ because setup code in
* web-test-runner.config.js puts it there. If you're changing this code (or
* that code), make sure the other is updated accordingly.
*/
export function getWebSocketEndpoint() {
return window.__ENV__.wsEndpoint;
}

View File

@ -13,9 +13,22 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
const { chromeLauncher } = require('@web/test-runner-chrome');
module.exports = { module.exports = {
files: ['test-browser/**/*.spec.js'], files: ['test-browser/**/*.spec.js'],
browsers: [
chromeLauncher({
async createPage({ browser }) {
const page = await browser.newPage();
page.evaluateOnNewDocument((wsEndpoint) => {
window.__ENV__ = { wsEndpoint };
}, browser.wsEndpoint());
return page;
},
}),
],
plugins: [ plugins: [
{ {
// turn expect UMD into an es module // turn expect UMD into an es module