diff --git a/test/browsertype-connect-subprocess.spec.ts b/test/browsertype-connect-subprocess.spec.ts new file mode 100644 index 0000000000000..3b6ff7f3d49ad --- /dev/null +++ b/test/browsertype-connect-subprocess.spec.ts @@ -0,0 +1,46 @@ +/** + * Copyright Microsoft Corporation. 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 { options } from './playwright.fixtures'; +import './remoteServer.fixture'; +import utils from './utils'; + +it.skip(options.WIRE).slow()('should connect to server from another process', async({ browserType, remoteServer }) => { + const browser = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); + const page = await browser.newPage(); + expect(await page.evaluate('2 + 3')).toBe(5); + await browser.close(); +}); + +it.skip(options.WIRE).fail(true).slow()('should respect selectors in another process', async({ playwright, browserType, remoteServer }) => { + const mycss = () => ({ + create(root, target) {}, + query(root, selector) { + return root.querySelector(selector); + }, + queryAll(root: HTMLElement, selector: string) { + return Array.from(root.querySelectorAll(selector)); + } + }); + await utils.registerEngine(playwright, 'mycss', mycss); + + const browser = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); + const page = await browser.newPage(); + await page.setContent(`
hello
`); + expect(await page.innerHTML('css=div')).toBe('hello'); + expect(await page.innerHTML('mycss=div')).toBe('hello'); + await browser.close(); +}); diff --git a/test/browsertype-connect.spec.ts b/test/browsertype-connect.spec.ts index 030c4eb02c13c..6423a028be45d 100644 --- a/test/browsertype-connect.spec.ts +++ b/test/browsertype-connect.spec.ts @@ -16,6 +16,7 @@ */ import { options } from './playwright.fixtures'; +import utils from './utils'; it.skip(options.WIRE).slow()('should be able to reconnect to a browser', async({browserType, defaultBrowserOptions, server}) => { const browserServer = await browserType.launchServer(defaultBrowserOptions); @@ -65,3 +66,24 @@ it.skip(options.WIRE)('should throw when used after isConnected returns false', const error = await page.evaluate('1 + 1').catch(e => e) as Error; expect(error.message).toContain('has been closed'); }); + +it.skip(options.WIRE)('should respect selectors', async({playwright, browserType, defaultBrowserOptions}) => { + const mycss = () => ({ + create(root, target) {}, + query(root, selector) { + return root.querySelector(selector); + }, + queryAll(root: HTMLElement, selector: string) { + return Array.from(root.querySelectorAll(selector)); + } + }); + await utils.registerEngine(playwright, 'mycss', mycss); + + const browserServer = await browserType.launchServer(defaultBrowserOptions); + const browser = await browserType.connect({ wsEndpoint: browserServer.wsEndpoint() }); + const page = await browser.newPage(); + await page.setContent(`
hello
`); + expect(await page.innerHTML('css=div')).toBe('hello'); + expect(await page.innerHTML('mycss=div')).toBe('hello'); + await browserServer.close(); +}); diff --git a/test/fixtures.spec.ts b/test/fixtures.spec.ts index 6b0c9d520b928..8c4bb6f7b084a 100644 --- a/test/fixtures.spec.ts +++ b/test/fixtures.spec.ts @@ -15,190 +15,93 @@ * limitations under the License. */ import { options } from './playwright.fixtures'; -import { registerFixture } from '../test-runner'; +import './remoteServer.fixture'; +import { execSync } from 'child_process'; import path from 'path'; -import {spawn, execSync} from 'child_process'; -import { BrowserType, Browser, LaunchOptions } from '..'; -const playwrightPath = path.join(__dirname, '..'); - -class Wrapper { - _output: Map; - _outputCallback: Map; - _browserType: BrowserType; - _child: import("child_process").ChildProcess; - _exitPromise: Promise; - _exitAndDisconnectPromise: Promise; - constructor(browserType: BrowserType, defaultBrowserOptions: LaunchOptions, extraOptions?: { stallOnClose: boolean; }) { - this._output = new Map(); - this._outputCallback = new Map(); - - this._browserType = browserType; - const launchOptions = {...defaultBrowserOptions, - handleSIGINT: true, - handleSIGTERM: true, - handleSIGHUP: true, - executablePath: defaultBrowserOptions.executablePath || browserType.executablePath(), - logger: undefined, - }; - const options = { - playwrightPath, - browserTypeName: browserType.name(), - launchOptions, - ...extraOptions, - }; - this._child = spawn('node', [path.join(__dirname, 'fixtures', 'closeme.js'), JSON.stringify(options)]); - this._child.on('error', (...args) => console.log("ERROR", ...args)); - this._exitPromise = new Promise(resolve => this._child.on('exit', resolve)); - - let outputString = ''; - this._child.stdout.on('data', data => { - outputString += data.toString(); - // Uncomment to debug. - // console.log(data.toString()); - let match; - while (match = outputString.match(/\(([^()]+)=>([^()]+)\)/)) { - const key = match[1]; - const value = match[2]; - this._addOutput(key, value); - outputString = outputString.substring(match.index + match[0].length); - } - }); - } - - _addOutput(key, value) { - this._output.set(key, value); - const cb = this._outputCallback.get(key); - this._outputCallback.delete(key); - if (cb) - cb(); - } - - async out(key) { - if (!this._output.has(key)) - await new Promise(f => this._outputCallback.set(key, f)); - return this._output.get(key); - } - - async connect() { - const wsEndpoint = await this.out('wsEndpoint'); - const browser = await this._browserType.connect({ wsEndpoint }); - this._exitAndDisconnectPromise = Promise.all([ - this._exitPromise, - new Promise(resolve => browser.once('disconnected', resolve)), - ]).then(([exitCode]) => exitCode); - } - - child() { - return this._child; - } - - async childExitCode() { - return await this._exitAndDisconnectPromise; - } -} - -declare global { - interface TestState { - wrapper: Wrapper; - stallingWrapper: Wrapper; - } -} -registerFixture('wrapper', async ({browserType, defaultBrowserOptions}, test) => { - const wrapper = new Wrapper(browserType, defaultBrowserOptions); - await wrapper.connect(); - await test(wrapper); -}); - -registerFixture('stallingWrapper', async ({browserType, defaultBrowserOptions}, test) => { - const wrapper = new Wrapper(browserType, defaultBrowserOptions, { stallOnClose: true }); - await wrapper.connect(); - await test(wrapper); -}); - -it.slow()('should close the browser when the node process closes', async ({wrapper}) => { +it.slow()('should close the browser when the node process closes', async ({remoteServer}) => { if (WIN) - execSync(`taskkill /pid ${wrapper.child().pid} /T /F`); + execSync(`taskkill /pid ${remoteServer.child().pid} /T /F`); else - process.kill(wrapper.child().pid); - expect(await wrapper.childExitCode()).toBe(WIN ? 1 : 0); + process.kill(remoteServer.child().pid); + expect(await remoteServer.childExitCode()).toBe(WIN ? 1 : 0); // We might not get browser exitCode in time when killing the parent node process, // so we don't check it here. }); // Cannot reliably send signals on Windows. -it.skip(WIN || !options.HEADLESS).slow()('should report browser close signal', async ({wrapper}) => { - const pid = await wrapper.out('pid'); +it.skip(WIN || !options.HEADLESS).slow()('should report browser close signal', async ({remoteServer}) => { + const pid = await remoteServer.out('pid'); process.kill(-pid, 'SIGTERM'); - expect(await wrapper.out('exitCode')).toBe('null'); - expect(await wrapper.out('signal')).toBe('SIGTERM'); - process.kill(wrapper.child().pid); - await wrapper.childExitCode(); + expect(await remoteServer.out('exitCode')).toBe('null'); + expect(await remoteServer.out('signal')).toBe('SIGTERM'); + process.kill(remoteServer.child().pid); + await remoteServer.childExitCode(); }); -it.skip(WIN || !options.HEADLESS).slow()('should report browser close signal 2', async ({wrapper}) => { - const pid = await wrapper.out('pid'); +it.skip(WIN || !options.HEADLESS).slow()('should report browser close signal 2', async ({remoteServer}) => { + const pid = await remoteServer.out('pid'); process.kill(-pid, 'SIGKILL'); - expect(await wrapper.out('exitCode')).toBe('null'); - expect(await wrapper.out('signal')).toBe('SIGKILL'); - process.kill(wrapper.child().pid); - await wrapper.childExitCode(); + expect(await remoteServer.out('exitCode')).toBe('null'); + expect(await remoteServer.out('signal')).toBe('SIGKILL'); + process.kill(remoteServer.child().pid); + await remoteServer.childExitCode(); }); -it.skip(WIN || !options.HEADLESS).slow()('should close the browser on SIGINT', async ({wrapper}) => { - process.kill(wrapper.child().pid, 'SIGINT'); - expect(await wrapper.out('exitCode')).toBe('0'); - expect(await wrapper.out('signal')).toBe('null'); - expect(await wrapper.childExitCode()).toBe(130); +it.skip(WIN || !options.HEADLESS).slow()('should close the browser on SIGINT', async ({remoteServer}) => { + process.kill(remoteServer.child().pid, 'SIGINT'); + expect(await remoteServer.out('exitCode')).toBe('0'); + expect(await remoteServer.out('signal')).toBe('null'); + expect(await remoteServer.childExitCode()).toBe(130); }); -it.skip(WIN || !options.HEADLESS).slow()('should close the browser on SIGTERM', async ({wrapper}) => { - process.kill(wrapper.child().pid, 'SIGTERM'); - expect(await wrapper.out('exitCode')).toBe('0'); - expect(await wrapper.out('signal')).toBe('null'); - expect(await wrapper.childExitCode()).toBe(0); +it.skip(WIN || !options.HEADLESS).slow()('should close the browser on SIGTERM', async ({remoteServer}) => { + process.kill(remoteServer.child().pid, 'SIGTERM'); + expect(await remoteServer.out('exitCode')).toBe('0'); + expect(await remoteServer.out('signal')).toBe('null'); + expect(await remoteServer.childExitCode()).toBe(0); }); -it.skip(WIN || !options.HEADLESS).slow()('should close the browser on SIGHUP', async ({wrapper}) => { - process.kill(wrapper.child().pid, 'SIGHUP'); - expect(await wrapper.out('exitCode')).toBe('0'); - expect(await wrapper.out('signal')).toBe('null'); - expect(await wrapper.childExitCode()).toBe(0); +it.skip(WIN || !options.HEADLESS).slow()('should close the browser on SIGHUP', async ({remoteServer}) => { + process.kill(remoteServer.child().pid, 'SIGHUP'); + expect(await remoteServer.out('exitCode')).toBe('0'); + expect(await remoteServer.out('signal')).toBe('null'); + expect(await remoteServer.childExitCode()).toBe(0); }); -it.skip(WIN || !options.HEADLESS).slow()('should kill the browser on double SIGINT', async ({stallingWrapper}) => { - const wrapper = stallingWrapper; - process.kill(wrapper.child().pid, 'SIGINT'); - await wrapper.out('stalled'); - process.kill(wrapper.child().pid, 'SIGINT'); - expect(await wrapper.out('exitCode')).toBe('null'); - expect(await wrapper.out('signal')).toBe('SIGKILL'); - expect(await wrapper.childExitCode()).toBe(130); +it.skip(WIN || !options.HEADLESS).slow()('should kill the browser on double SIGINT', async ({stallingRemoteServer}) => { + const remoteServer = stallingRemoteServer; + process.kill(remoteServer.child().pid, 'SIGINT'); + await remoteServer.out('stalled'); + process.kill(remoteServer.child().pid, 'SIGINT'); + expect(await remoteServer.out('exitCode')).toBe('null'); + expect(await remoteServer.out('signal')).toBe('SIGKILL'); + expect(await remoteServer.childExitCode()).toBe(130); }); -it.skip(WIN || !options.HEADLESS).slow()('should kill the browser on SIGINT + SIGTERM', async ({stallingWrapper}) => { - const wrapper = stallingWrapper; - process.kill(wrapper.child().pid, 'SIGINT'); - await wrapper.out('stalled'); - process.kill(wrapper.child().pid, 'SIGTERM'); - expect(await wrapper.out('exitCode')).toBe('null'); - expect(await wrapper.out('signal')).toBe('SIGKILL'); - expect(await wrapper.childExitCode()).toBe(0); +it.skip(WIN || !options.HEADLESS).slow()('should kill the browser on SIGINT + SIGTERM', async ({stallingRemoteServer}) => { + const remoteServer = stallingRemoteServer; + process.kill(remoteServer.child().pid, 'SIGINT'); + await remoteServer.out('stalled'); + process.kill(remoteServer.child().pid, 'SIGTERM'); + expect(await remoteServer.out('exitCode')).toBe('null'); + expect(await remoteServer.out('signal')).toBe('SIGKILL'); + expect(await remoteServer.childExitCode()).toBe(0); }); -it.skip(WIN || !options.HEADLESS).slow()('should kill the browser on SIGTERM + SIGINT', async ({stallingWrapper}) => { - const wrapper = stallingWrapper; - process.kill(wrapper.child().pid, 'SIGTERM'); - await wrapper.out('stalled'); - process.kill(wrapper.child().pid, 'SIGINT'); - expect(await wrapper.out('exitCode')).toBe('null'); - expect(await wrapper.out('signal')).toBe('SIGKILL'); - expect(await wrapper.childExitCode()).toBe(130); +it.skip(WIN || !options.HEADLESS).slow()('should kill the browser on SIGTERM + SIGINT', async ({stallingRemoteServer}) => { + const remoteServer = stallingRemoteServer; + process.kill(remoteServer.child().pid, 'SIGTERM'); + await remoteServer.out('stalled'); + process.kill(remoteServer.child().pid, 'SIGINT'); + expect(await remoteServer.out('exitCode')).toBe('null'); + expect(await remoteServer.out('signal')).toBe('SIGKILL'); + expect(await remoteServer.childExitCode()).toBe(130); }); it('caller file path', async ({}) => { - const stackTrace = require(path.join(playwrightPath, 'lib', 'utils', 'stackTrace')); + const stackTrace = require(path.join(__dirname, '..', 'lib', 'utils', 'stackTrace')); const callme = require('./fixtures/callback'); const filePath = callme(() => { return stackTrace.getCallerFilePath(path.join(__dirname, 'fixtures') + path.sep); diff --git a/test/remoteServer.fixture.ts b/test/remoteServer.fixture.ts new file mode 100644 index 0000000000000..d4b5868834b08 --- /dev/null +++ b/test/remoteServer.fixture.ts @@ -0,0 +1,131 @@ +/** + * Copyright Microsoft Corporation. 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 { registerFixture } from '../test-runner/lib'; + +import path from 'path'; +import { spawn } from 'child_process'; +import { BrowserType, Browser, LaunchOptions } from '..'; + +declare global { + interface TestState { + remoteServer: RemoteServer; + stallingRemoteServer: RemoteServer; + } +} + +const playwrightPath = path.join(__dirname, '..'); + +class RemoteServer { + _output: Map; + _outputCallback: Map; + _browserType: BrowserType; + _child: import("child_process").ChildProcess; + _exitPromise: Promise; + _exitAndDisconnectPromise: Promise; + _browser: Browser; + _didExit: boolean; + _wsEndpoint: string; + + async _start(browserType: BrowserType, defaultBrowserOptions: LaunchOptions, extraOptions?: { stallOnClose: boolean; }) { + this._output = new Map(); + this._outputCallback = new Map(); + this._didExit = false; + + this._browserType = browserType; + const launchOptions = {...defaultBrowserOptions, + handleSIGINT: true, + handleSIGTERM: true, + handleSIGHUP: true, + executablePath: defaultBrowserOptions.executablePath || browserType.executablePath(), + logger: undefined, + }; + const options = { + playwrightPath, + browserTypeName: browserType.name(), + launchOptions, + ...extraOptions, + }; + this._child = spawn('node', [path.join(__dirname, 'fixtures', 'closeme.js'), JSON.stringify(options)]); + this._child.on('error', (...args) => console.log("ERROR", ...args)); + this._exitPromise = new Promise(resolve => this._child.on('exit', (exitCode, signal) => { + this._didExit = true; + resolve(exitCode); + })); + + let outputString = ''; + this._child.stdout.on('data', data => { + outputString += data.toString(); + // Uncomment to debug. + // console.log(data.toString()); + let match; + while (match = outputString.match(/\(([^()]+)=>([^()]+)\)/)) { + const key = match[1]; + const value = match[2]; + this._addOutput(key, value); + outputString = outputString.substring(match.index + match[0].length); + } + }); + + this._wsEndpoint = await this.out('wsEndpoint'); + } + + _addOutput(key, value) { + this._output.set(key, value); + const cb = this._outputCallback.get(key); + this._outputCallback.delete(key); + if (cb) + cb(); + } + + async out(key) { + if (!this._output.has(key)) + await new Promise(f => this._outputCallback.set(key, f)); + return this._output.get(key); + } + + wsEndpoint() { + return this._wsEndpoint; + } + + child() { + return this._child; + } + + async childExitCode() { + return await this._exitPromise; + } + + async _close() { + if (!this._didExit) + this._child.kill(); + return await this.childExitCode(); + } +} + +registerFixture('remoteServer', async ({browserType, defaultBrowserOptions}, test) => { + const remoteServer = new RemoteServer(); + await remoteServer._start(browserType, defaultBrowserOptions); + await test(remoteServer); + await remoteServer._close(); +}); + +registerFixture('stallingRemoteServer', async ({browserType, defaultBrowserOptions}, test) => { + const remoteServer = new RemoteServer(); + await remoteServer._start(browserType, defaultBrowserOptions, { stallOnClose: true }); + await test(remoteServer); + await remoteServer._close(); +});