chore: add websocket connection mode (#4510)

This commit is contained in:
Pavel Feldman 2020-11-20 15:19:39 -08:00 committed by GitHub
parent 14a96ca21f
commit e72d9a4185
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 246 additions and 62 deletions

View File

@ -165,13 +165,13 @@ jobs:
name: headful-${{ matrix.browser }}-linux-test-results
path: test-results
wire_linux:
name: "Wire Linux"
transport_linux:
name: "Transport"
strategy:
fail-fast: false
matrix:
browser: [chromium, firefox, webkit]
runs-on: ubuntu-18.04
mode: [driver, service]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
@ -188,15 +188,15 @@ jobs:
# Enable core dumps in the subshell.
- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- bash -c "ulimit -c unlimited && npx folio test/ --workers=1 --forbid-only --global-timeout=5400000 --retries=3 --reporter=dot,json"
env:
BROWSER: ${{ matrix.browser }}
PWWIRE: true
BROWSER: "chromium"
PWMODE: "${{ matrix.mode }}"
FOLIO_JSON_OUTPUT_NAME: "test-results/report.json"
- run: ./utils/upload_flakiness_dashboard.sh ./test-results/report.json
if: always() && github.ref == 'refs/heads/master'
- uses: actions/upload-artifact@v1
if: ${{ always() }}
with:
name: wire-${{ matrix.browser }}-linux-test-results
name: mode-${{ matrix.mode }}-linux-test-results
path: test-results
video_linux:

View File

@ -0,0 +1,55 @@
/**
* 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 * as WebSocket from 'ws';
import { Connection } from '../client/connection';
import { Playwright } from '../client/playwright';
export class PlaywrightClient {
private _playwright: Playwright;
private _ws: WebSocket;
private _closePromise: Promise<void>;
static async connect(wsEndpoint: string): Promise<PlaywrightClient> {
const connection = new Connection();
const ws = new WebSocket(wsEndpoint);
connection.onmessage = message => ws.send(JSON.stringify(message));
ws.on('message', message => connection.dispatch(JSON.parse(message.toString())));
const errorPromise = new Promise((_, reject) => ws.on('error', error => reject(error)));
const closePromise = new Promise((_, reject) => ws.on('close', () => reject(new Error('Connection closed'))));
const playwright = await Promise.race([
connection.waitForObjectWithKnownName('Playwright'),
errorPromise,
closePromise
]);
return new PlaywrightClient(playwright as Playwright, ws);
}
constructor(playwright: Playwright, ws: WebSocket) {
this._playwright = playwright;
this._ws = ws;
this._closePromise = new Promise(f => ws.on('close', f));
}
playwright(): Playwright {
return this._playwright;
}
async close() {
this._ws.close();
await this._closePromise;
}
}

View File

@ -0,0 +1,84 @@
/**
* 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 * as debug from 'debug';
import * as http from 'http';
import * as WebSocket from 'ws';
import { installDebugController } from '../debug/debugController';
import { DispatcherConnection } from '../dispatchers/dispatcher';
import { PlaywrightDispatcher } from '../dispatchers/playwrightDispatcher';
import { Electron } from '../server/electron/electron';
import { Playwright } from '../server/playwright';
import { gracefullyCloseAll } from '../server/processLauncher';
import { installTracer } from '../trace/tracer';
import { installHarTracer } from '../trace/harTracer';
const debugLog = debug('pw:server');
installDebugController();
installTracer();
installHarTracer();
export class PlaywrightServer {
private _server: http.Server | undefined;
private _client: WebSocket | undefined;
listen(port: number) {
this._server = http.createServer((request, response) => {
response.end('Running');
});
this._server.on('error', error => debugLog(error));
this._server.listen(port);
debugLog('Listening on ' + port);
const wsServer = new WebSocket.Server({ server: this._server, path: '/ws' });
wsServer.on('connection', async ws => {
if (this._client) {
ws.close();
return;
}
this._client = ws;
debugLog('Incoming connection');
const dispatcherConnection = new DispatcherConnection();
ws.on('message', message => dispatcherConnection.dispatch(JSON.parse(message.toString())));
ws.on('close', () => {
debugLog('Client closed');
this._onDisconnect();
});
ws.on('error', error => {
debugLog('Client error ' + error);
this._onDisconnect();
});
dispatcherConnection.onmessage = message => ws.send(JSON.stringify(message));
const playwright = new Playwright(__dirname, require('../../browsers.json')['browsers']);
(playwright as any).electron = new Electron();
new PlaywrightDispatcher(dispatcherConnection.rootDispatcher(), playwright);
});
}
async close() {
if (!this._server)
return;
debugLog('Closing server');
await new Promise(f => this._server!.close(f));
await gracefullyCloseAll();
}
private async _onDisconnect() {
await gracefullyCloseAll();
this._client = undefined;
}
}

21
src/service.ts Normal file
View File

@ -0,0 +1,21 @@
/**
* 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 { PlaywrightServer } from './remote/playwrightServer';
const server = new PlaywrightServer();
server.listen(+process.argv[2]);
console.log('Listening on ' + process.argv[2]); // eslint-disable-line no-console

View File

@ -201,8 +201,8 @@ it('should work with goto following click', async ({page, server}) => {
await page.goto(server.EMPTY_PAGE);
});
it('should report navigation in the log when clicking anchor', (test, { wire }) => {
test.skip(wire);
it('should report navigation in the log when clicking anchor', (test, { mode }) => {
test.skip(mode !== 'default');
}, async ({page, server}) => {
await page.setContent(`<a href="${server.PREFIX + '/frames/one-frame.html'}">click me</a>`);
const __testHookAfterPointerAction = () => new Promise(f => setTimeout(f, 6000));

View File

@ -19,8 +19,8 @@ import { folio } from './remoteServer.fixture';
import * as fs from 'fs';
const { it, expect, describe } = folio;
describe('connect', (suite, { wire }) => {
suite.skip(wire);
describe('connect', (suite, { mode }) => {
suite.skip(mode !== 'default');
suite.slow();
}, () => {
it('should be able to reconnect to a browser', async ({browserType, remoteServer, server}) => {

View File

@ -17,8 +17,8 @@
import { it, expect, describe } from './fixtures';
describe('lauch server', (suite, { wire }) => {
suite.skip(wire);
describe('lauch server', (suite, { mode }) => {
suite.skip(mode !== 'default');
}, () => {
it('should work', async ({browserType, browserOptions}) => {
const browserServer = await browserType.launchServer(browserOptions);

View File

@ -73,8 +73,8 @@ it('should reject if executable path is invalid', async ({browserType, browserOp
expect(waitError.message).toContain('Failed to launch');
});
it('should handle timeout', (test, { wire }) => {
test.skip(wire);
it('should handle timeout', (test, { mode }) => {
test.skip(mode !== 'default');
}, async ({browserType, browserOptions}) => {
const options = { ...browserOptions, timeout: 5000, __testHookBeforeCreateBrowser: () => new Promise(f => setTimeout(f, 6000)) };
const error = await browserType.launch(options).catch(e => e);
@ -83,8 +83,8 @@ it('should handle timeout', (test, { wire }) => {
expect(error.message).toContain(`<launched> pid=`);
});
it('should handle exception', (test, { wire }) => {
test.skip(wire);
it('should handle exception', (test, { mode }) => {
test.skip(mode !== 'default');
}, async ({browserType, browserOptions}) => {
const e = new Error('Dummy');
const options = { ...browserOptions, __testHookBeforeCreateBrowser: () => { throw e; }, timeout: 9000 };
@ -92,8 +92,8 @@ it('should handle exception', (test, { wire }) => {
expect(error.message).toContain('Dummy');
});
it('should report launch log', (test, { wire }) => {
test.skip(wire);
it('should report launch log', (test, { mode }) => {
test.skip(mode !== 'default');
}, async ({browserType, browserOptions}) => {
const e = new Error('Dummy');
const options = { ...browserOptions, __testHookBeforeCreateBrowser: () => { throw e; }, timeout: 9000 };

View File

@ -18,8 +18,8 @@ import { it, expect } from '../fixtures';
import path from 'path';
import type { ChromiumBrowser, ChromiumBrowserContext } from '../..';
it('should throw with remote-debugging-pipe argument', (test, { browserName, wire }) => {
test.skip(wire || browserName !== 'chromium');
it('should throw with remote-debugging-pipe argument', (test, { browserName, mode }) => {
test.skip(mode !== 'default' || browserName !== 'chromium');
}, async ({browserType, browserOptions}) => {
const options = Object.assign({}, browserOptions);
options.args = ['--remote-debugging-pipe'].concat(options.args || []);
@ -27,8 +27,8 @@ it('should throw with remote-debugging-pipe argument', (test, { browserName, wir
expect(error.message).toContain('Playwright manages remote debugging connection itself');
});
it('should not throw with remote-debugging-port argument', (test, { browserName, wire }) => {
test.skip(wire || browserName !== 'chromium');
it('should not throw with remote-debugging-port argument', (test, { browserName, mode }) => {
test.skip(mode !== 'default' || browserName !== 'chromium');
}, async ({browserType, browserOptions}) => {
const options = Object.assign({}, browserOptions);
options.args = ['--remote-debugging-port=0'].concat(options.args || []);
@ -36,8 +36,8 @@ it('should not throw with remote-debugging-port argument', (test, { browserName,
await browser.close();
});
it('should open devtools when "devtools: true" option is given', (test, { wire, browserName, platform}) => {
test.skip(browserName !== 'chromium' || wire || platform === 'win32');
it('should open devtools when "devtools: true" option is given', (test, { mode, browserName, platform}) => {
test.skip(browserName !== 'chromium' || mode !== 'default' || platform === 'win32');
}, async ({browserType, browserOptions}) => {
let devtoolsCallback;
const devtoolsPromise = new Promise(f => devtoolsCallback = f);

View File

@ -17,8 +17,8 @@
import { it, expect } from './fixtures';
it('should avoid side effects after timeout', (test, { wire }) => {
test.skip(wire);
it('should avoid side effects after timeout', (test, { mode }) => {
test.skip(mode !== 'default');
}, async ({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');
const error = await page.click('button', { timeout: 2000, __testHookBeforePointerAction: () => new Promise(f => setTimeout(f, 2500))} as any).catch(e => e);

View File

@ -17,8 +17,8 @@
import { it, expect } from './fixtures';
it('should fail when element jumps during hit testing', (test, { wire }) => {
test.skip(wire);
it('should fail when element jumps during hit testing', (test, { mode }) => {
test.skip(mode !== 'default');
}, async ({page}) => {
await page.setContent('<button>Click me</button>');
let clicked = false;

View File

@ -154,8 +154,8 @@ it('should throw if page argument is passed', (test, { browserName }) => {
expect(error.message).toContain('can not specify page');
});
it('should have passed URL when launching with ignoreDefaultArgs: true', (test, { wire }) => {
test.skip(wire);
it('should have passed URL when launching with ignoreDefaultArgs: true', (test, { mode }) => {
test.skip(mode !== 'default');
}, async ({browserType, browserOptions, server, createUserDataDir, toImpl}) => {
const userDataDir = await createUserDataDir();
const args = toImpl(browserType)._defaultArgs(browserOptions, 'persistent', userDataDir, 0).filter(a => a !== 'about:blank');
@ -173,16 +173,16 @@ it('should have passed URL when launching with ignoreDefaultArgs: true', (test,
await browserContext.close();
});
it('should handle timeout', (test, { wire }) => {
test.skip(wire);
it('should handle timeout', (test, { mode }) => {
test.skip(mode !== 'default');
}, async ({browserType, browserOptions, createUserDataDir}) => {
const options = { ...browserOptions, timeout: 5000, __testHookBeforeCreateBrowser: () => new Promise(f => setTimeout(f, 6000)) };
const error = await browserType.launchPersistentContext(await createUserDataDir(), options).catch(e => e);
expect(error.message).toContain(`browserType.launchPersistentContext: Timeout 5000ms exceeded.`);
});
it('should handle exception', (test, { wire }) => {
test.skip(wire);
it('should handle exception', (test, { mode }) => {
test.skip(mode !== 'default');
}, async ({browserType, browserOptions, createUserDataDir}) => {
const e = new Error('Dummy');
const options = { ...browserOptions, __testHookBeforeCreateBrowser: () => { throw e; } };

View File

@ -145,8 +145,8 @@ describe('download event', () => {
await page.close();
});
it('should save when connected remotely', (test, { wire }) => {
test.skip(wire);
it('should save when connected remotely', (test, { mode }) => {
test.skip(mode !== 'default');
}, async ({testInfo, server, browserType, remoteServer}) => {
const browser = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() });
const page = await browser.newPage({ acceptDownloads: true });
@ -191,8 +191,8 @@ describe('download event', () => {
await page.close();
});
it('should error when saving after deletion when connected remotely', (test, { wire }) => {
test.skip(wire);
it('should error when saving after deletion when connected remotely', (test, { mode }) => {
test.skip(mode !== 'default');
}, async ({testInfo, server, browserType, remoteServer}) => {
const browser = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() });
const page = await browser.newPage({ acceptDownloads: true });

View File

@ -289,8 +289,8 @@ describe('element screenshot', (suite, parameters) => {
await context.close();
});
it('should restore viewport after page screenshot and exception', (test, { wire }) => {
test.skip(wire);
it('should restore viewport after page screenshot and exception', (test, { mode }) => {
test.skip(mode !== 'default');
}, async ({ browser, server }) => {
const context = await browser.newContext({ viewport: { width: 350, height: 360 } });
const page = await context.newPage();
@ -302,8 +302,8 @@ describe('element screenshot', (suite, parameters) => {
await context.close();
});
it('should restore viewport after page screenshot and timeout', (test, { wire }) => {
test.skip(wire);
it('should restore viewport after page screenshot and timeout', (test, { mode }) => {
test.skip(mode !== 'default');
}, async ({ browser, server }) => {
const context = await browser.newContext({ viewport: { width: 350, height: 360 } });
const page = await context.newPage();
@ -348,8 +348,8 @@ describe('element screenshot', (suite, parameters) => {
await context.close();
});
it('should restore viewport after element screenshot and exception', (test, { wire }) => {
test.skip(wire);
it('should restore viewport after element screenshot and exception', (test, { mode }) => {
test.skip(mode !== 'default');
}, async ({browser}) => {
const context = await browser.newContext({ viewport: { width: 350, height: 360 } });
const page = await context.newPage();

View File

@ -26,6 +26,7 @@ import { Transport } from '../lib/protocol/transport';
import { installCoverageHooks } from './coverage';
import { folio as httpFolio } from './http.fixtures';
import { folio as playwrightFolio } from './playwright.fixtures';
import { PlaywrightClient } from '../lib/remote/playwrightClient';
export { expect, config } from 'folio';
const removeFolderAsync = util.promisify(require('rimraf'));
@ -40,8 +41,8 @@ const getExecutablePath = browserName => {
return process.env.WKPATH;
};
type WireParameters = {
wire: boolean;
type ModeParameters = {
mode: 'default' | 'driver' | 'service';
};
type WorkerFixtures = {
toImpl: (rpcObject: any) => any;
@ -51,9 +52,9 @@ type TestFixtures = {
launchPersistent: (options?: Parameters<BrowserType<Browser>['launchPersistentContext']>[1]) => Promise<{ context: BrowserContext, page: Page }>;
};
const fixtures = playwrightFolio.union(httpFolio).extend<TestFixtures, WorkerFixtures, WireParameters>();
const fixtures = playwrightFolio.union(httpFolio).extend<TestFixtures, WorkerFixtures, ModeParameters>();
fixtures.wire.initParameter('Wire testing mode', !!process.env.PWWIRE);
fixtures.mode.initParameter('Testing mode', process.env.PWMODE as any || 'default');
fixtures.createUserDataDir.init(async ({ }, run) => {
const dirs: string[] = [];
@ -99,10 +100,10 @@ fixtures.browserOptions.override(async ({ browserName, headful, slowMo }, run) =
});
});
fixtures.playwright.override(async ({ browserName, testWorkerIndex, platform, wire }, run) => {
fixtures.playwright.override(async ({ browserName, testWorkerIndex, platform, mode }, run) => {
assert(platform); // Depend on platform to generate all tests.
const { coverage, uninstall } = installCoverageHooks(browserName);
if (wire) {
if (mode === 'driver') {
require('../lib/utils/utils').setUnderTest();
const connection = new Connection();
const spawnedProcess = childProcess.fork(path.join(__dirname, '..', 'lib', 'driver.js'), ['serve'], {
@ -124,6 +125,29 @@ fixtures.playwright.override(async ({ browserName, testWorkerIndex, platform, wi
spawnedProcess.stdout.destroy();
spawnedProcess.stderr.destroy();
await teardownCoverage();
} else if (mode === 'service') {
require('../lib/utils/utils').setUnderTest();
const port = 9407 + testWorkerIndex * 2;
const spawnedProcess = childProcess.fork(path.join(__dirname, '..', 'lib', 'service.js'), [String(port)], {
stdio: 'pipe'
});
await new Promise(f => {
spawnedProcess.stdout.on('data', data => {
if (data.toString().includes('Listening on'))
f();
});
});
spawnedProcess.unref();
const onExit = (exitCode, signal) => {
throw new Error(`Server closed with exitCode=${exitCode} signal=${signal}`);
};
spawnedProcess.on('exit', onExit);
const client = await PlaywrightClient.connect(`ws://localhost:${port}/ws`);
await run(client.playwright());
spawnedProcess.removeListener('exit', onExit);
await client.close();
spawnedProcess.kill();
await teardownCoverage();
} else {
const playwright = require('../index');
await run(playwright);

View File

@ -44,8 +44,8 @@ function expectContexts(pageImpl, count, isChromium) {
expect(pageImpl._delegate._contextIdToContext.size).toBe(count);
}
it('should dispose context on navigation', (test, { wire }) => {
test.skip(wire);
it('should dispose context on navigation', (test, { mode }) => {
test.skip(mode !== 'default');
}, async ({ page, server, toImpl, isChromium }) => {
await page.goto(server.PREFIX + '/frames/one-frame.html');
expect(page.frames().length).toBe(2);
@ -54,8 +54,8 @@ it('should dispose context on navigation', (test, { wire }) => {
expectContexts(toImpl(page), 2, isChromium);
});
it('should dispose context on cross-origin navigation', (test, { wire }) => {
test.skip(wire);
it('should dispose context on cross-origin navigation', (test, { mode }) => {
test.skip(mode !== 'default');
}, async ({ page, server, toImpl, isChromium }) => {
await page.goto(server.PREFIX + '/frames/one-frame.html');
expect(page.frames().length).toBe(2);

View File

@ -435,8 +435,8 @@ it('should not throw an error when evaluation does a synchronous navigation and
expect(result).toBe(undefined);
});
it('should transfer 100Mb of data from page to node.js', (test, { wire }) => {
test.skip(wire);
it('should transfer 100Mb of data from page to node.js', (test, { mode }) => {
test.skip(mode !== 'default');
}, async ({ page }) => {
// This is too slow with wire.
const a = await page.evaluate(() => Array(100 * 1024 * 1024 + 1).join('a'));

View File

@ -27,8 +27,8 @@ function crash(page, toImpl, browserName) {
toImpl(page)._delegate._session.send('Page.crash', {}).catch(e => {});
}
describe('', (suite, { browserName, platform, wire }) => {
suite.skip(wire && browserName !== 'chromium');
describe('', (suite, { browserName, platform, mode }) => {
suite.skip(mode !== 'default' && browserName !== 'chromium');
suite.flaky(browserName === 'firefox' && platform === 'win32');
const isBigSur = platform === 'darwin' && parseInt(os.release(), 10) >= 20;
suite.fixme(isBigSur && browserName === 'webkit', 'Timing out after roll');

View File

@ -97,7 +97,7 @@ it('should amend method on main request', async ({page, server}) => {
expect((await request).method).toBe('POST');
});
describe('', (suite, { browserName, platform, wire }) => {
describe('', (suite, { browserName, platform }) => {
const isBigSur = platform === 'darwin' && parseInt(os.release(), 10) >= 20;
suite.flaky(isBigSur && browserName === 'webkit', 'Flaky after roll');
}, () => {

View File

@ -44,8 +44,8 @@ async function checkPageSlowMo(toImpl, page, task) {
`);
await checkSlowMo(toImpl, page, task);
}
describe('slowMo', (suite, { wire }) => {
suite.skip(wire);
describe('slowMo', (suite, { mode }) => {
suite.skip(mode !== 'default');
}, () => {
it('Page SlowMo $$eval', async ({page, toImpl}) => {
await checkPageSlowMo(toImpl, page, () => page.$$eval('button', () => void 0));