From a4eb4c0baf982d3b01c040e8a0060abc49f6b5a5 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 1 Apr 2021 19:13:08 -0700 Subject: [PATCH] test: migrate remoteServer tests to new folio (#6042) --- test/browsertype-connect.spec.ts | 276 -------------- test/browsertype-launch-server.spec.ts | 8 +- test/download.spec.ts | 38 +- test/fixtures.spec.ts | 145 -------- tests/browsertype-connect.spec.ts | 347 ++++++++++++++++++ tests/config/browserEnv.ts | 15 + tests/config/default.config.ts | 3 +- tests/config/playwrightTest.ts | 3 + .../config/remote-server-impl.js | 0 .../config/remoteServer.ts | 56 +-- tests/config/serverEnv.ts | 4 +- tests/signals.spec.ts | 123 +++++++ 12 files changed, 514 insertions(+), 504 deletions(-) delete mode 100644 test/browsertype-connect.spec.ts delete mode 100644 test/fixtures.spec.ts create mode 100644 tests/browsertype-connect.spec.ts rename test/fixtures/closeme.js => tests/config/remote-server-impl.js (100%) rename test/remoteServer.fixture.ts => tests/config/remoteServer.ts (68%) create mode 100644 tests/signals.spec.ts diff --git a/test/browsertype-connect.spec.ts b/test/browsertype-connect.spec.ts deleted file mode 100644 index 4bf48a9339..0000000000 --- a/test/browsertype-connect.spec.ts +++ /dev/null @@ -1,276 +0,0 @@ -/** - * Copyright 2017 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 { folio } from './remoteServer.fixture'; -import fs from 'fs'; -const { it, expect, describe } = folio; - -describe('connect', (suite, { mode }) => { - suite.skip(mode !== 'default'); - suite.slow(); -}, () => { - it('should be able to reconnect to a browser', async ({browserType, remoteServer, server}) => { - { - const browser = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); - const browserContext = await browser.newContext(); - expect(browserContext.pages().length).toBe(0); - const page = await browserContext.newPage(); - expect(await page.evaluate('11 * 11')).toBe(121); - await page.goto(server.EMPTY_PAGE); - await browser.close(); - } - { - const browser = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); - const browserContext = await browser.newContext(); - const page = await browserContext.newPage(); - await page.goto(server.EMPTY_PAGE); - await browser.close(); - } - }); - - it('should be able to connect two browsers at the same time', async ({browserType, remoteServer}) => { - const browser1 = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); - expect(browser1.contexts().length).toBe(0); - await browser1.newContext(); - expect(browser1.contexts().length).toBe(1); - - const browser2 = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); - expect(browser2.contexts().length).toBe(0); - await browser2.newContext(); - expect(browser2.contexts().length).toBe(1); - expect(browser1.contexts().length).toBe(1); - - await browser1.close(); - const page2 = await browser2.newPage(); - expect(await page2.evaluate(() => 7 * 6)).toBe(42); // original browser should still work - - await browser2.close(); - }); - - it('disconnected event should be emitted when browser is closed or server is closed', async ({browserType, remoteServer}) => { - const browser1 = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); - const browser2 = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); - - let disconnected1 = 0; - let disconnected2 = 0; - browser1.on('disconnected', () => ++disconnected1); - browser2.on('disconnected', () => ++disconnected2); - - await Promise.all([ - new Promise(f => browser1.on('disconnected', f)), - browser1.close(), - ]); - expect(disconnected1).toBe(1); - expect(disconnected2).toBe(0); - - await Promise.all([ - new Promise(f => browser2.on('disconnected', f)), - remoteServer.close(), - ]); - expect(disconnected1).toBe(1); - expect(disconnected2).toBe(1); - }); - - it('disconnected event should have browser as argument', async ({browserType, remoteServer}) => { - const browser = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); - const [disconnected] = await Promise.all([ - new Promise(f => browser.on('disconnected', f)), - browser.close(), - ]); - expect(disconnected).toBe(browser); - }); - - it('should handle exceptions during connect', async ({browserType, remoteServer}) => { - const __testHookBeforeCreateBrowser = () => { throw new Error('Dummy'); }; - const error = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint(), __testHookBeforeCreateBrowser } as any).catch(e => e); - expect(error.message).toContain('Dummy'); - }); - - it('should set the browser connected state', async ({browserType, remoteServer}) => { - const remote = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); - expect(remote.isConnected()).toBe(true); - await remote.close(); - expect(remote.isConnected()).toBe(false); - }); - - it('should throw when used after isConnected returns false', async ({browserType, remoteServer}) => { - const remote = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); - const page = await remote.newPage(); - await Promise.all([ - remoteServer.close(), - new Promise(f => remote.once('disconnected', f)), - ]); - expect(remote.isConnected()).toBe(false); - const error = await page.evaluate('1 + 1').catch(e => e) as Error; - expect(error.message).toContain('has been closed'); - }); - - it('should reject navigation when browser closes', async ({browserType, remoteServer, server}) => { - server.setRoute('/one-style.css', () => {}); - const remote = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); - const page = await remote.newPage(); - const navigationPromise = page.goto(server.PREFIX + '/one-style.html', {timeout: 60000}).catch(e => e); - await server.waitForRequest('/one-style.css'); - await remote.close(); - const error = await navigationPromise; - expect(error.message).toContain('Navigation failed because page was closed!'); - }); - - it('should reject waitForSelector when browser closes', async ({browserType, remoteServer, server}) => { - server.setRoute('/empty.html', () => {}); - const remote = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); - const page = await remote.newPage(); - const watchdog = page.waitForSelector('div', { state: 'attached', timeout: 60000 }).catch(e => e); - - // Make sure the previous waitForSelector has time to make it to the browser before we disconnect. - await page.waitForSelector('body', { state: 'attached' }); - - await remote.close(); - const error = await watchdog; - expect(error.message).toContain('Protocol error'); - }); - - it('should emit close events on pages and contexts', async ({browserType, remoteServer}) => { - const remote = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); - const context = await remote.newContext(); - const page = await context.newPage(); - let pageClosed = false; - page.on('close', () => pageClosed = true); - await Promise.all([ - new Promise(f => context.on('close', f)), - remoteServer.close() - ]); - expect(pageClosed).toBeTruthy(); - }); - - it('should terminate network waiters', async ({browserType, remoteServer, server}) => { - const remote = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); - const newPage = await remote.newPage(); - const results = await Promise.all([ - newPage.waitForRequest(server.EMPTY_PAGE).catch(e => e), - newPage.waitForResponse(server.EMPTY_PAGE).catch(e => e), - remoteServer.close(), - ]); - for (let i = 0; i < 2; i++) { - const message = results[i].message; - expect(message).toContain('Page closed'); - expect(message).not.toContain('Timeout'); - } - }); - - it('should respect selectors', async ({ playwright, browserType, remoteServer }) => { - const mycss = () => ({ - query(root, selector) { - return root.querySelector(selector); - }, - queryAll(root: HTMLElement, selector: string) { - return Array.from(root.querySelectorAll(selector)); - } - }); - // Register one engine before connecting. - await playwright.selectors.register('mycss1', mycss); - - const browser1 = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); - const context1 = await browser1.newContext(); - - // Register another engine after creating context. - await playwright.selectors.register('mycss2', mycss); - - const page1 = await context1.newPage(); - await page1.setContent(`
hello
`); - expect(await page1.innerHTML('css=div')).toBe('hello'); - expect(await page1.innerHTML('mycss1=div')).toBe('hello'); - expect(await page1.innerHTML('mycss2=div')).toBe('hello'); - - const browser2 = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); - - // Register third engine after second connect. - await playwright.selectors.register('mycss3', mycss); - - const page2 = await browser2.newPage(); - await page2.setContent(`
hello
`); - expect(await page2.innerHTML('css=div')).toBe('hello'); - expect(await page2.innerHTML('mycss1=div')).toBe('hello'); - expect(await page2.innerHTML('mycss2=div')).toBe('hello'); - expect(await page2.innerHTML('mycss3=div')).toBe('hello'); - - await browser1.close(); - }); - - it('should not throw on close after disconnect', async ({browserType, remoteServer, server}) => { - const remote = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); - await remote.newPage(); - await Promise.all([ - new Promise(f => remote.on('disconnected', f)), - remoteServer.close() - ]); - await remote.close(); - }); - - it('should not throw on context.close after disconnect', async ({browserType, remoteServer, server}) => { - const remote = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); - const context = await remote.newContext(); - await context.newPage(); - await Promise.all([ - new Promise(f => remote.on('disconnected', f)), - remoteServer.close() - ]); - await context.close(); - }); - - it('should not throw on page.close after disconnect', async ({browserType, remoteServer, server}) => { - const remote = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); - const page = await remote.newPage(); - await Promise.all([ - new Promise(f => remote.on('disconnected', f)), - remoteServer.close() - ]); - await page.close(); - }); - - it('should saveAs videos from remote browser', async ({browserType, remoteServer, testInfo}) => { - const remote = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); - const videosPath = testInfo.outputPath(); - const context = await remote.newContext({ - recordVideo: { dir: videosPath, size: { width: 320, height: 240 } }, - }); - const page = await context.newPage(); - await page.evaluate(() => document.body.style.backgroundColor = 'red'); - await new Promise(r => setTimeout(r, 1000)); - await context.close(); - - const savedAsPath = testInfo.outputPath('my-video.webm'); - await page.video().saveAs(savedAsPath); - expect(fs.existsSync(savedAsPath)).toBeTruthy(); - const error = await page.video().path().catch(e => e); - expect(error.message).toContain('Path is not available when using browserType.connect(). Use saveAs() to save a local copy.'); - }); - - it('should be able to connect 20 times to a single server without warnings', async ({browserType, remoteServer, server}) => { - let warning = null; - const warningHandler = w => warning = w; - process.on('warning', warningHandler); - - const browsers = []; - for (let i = 0; i < 20; i++) - browsers.push(await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() })); - await Promise.all([browsers.map(browser => browser.close())]); - - process.off('warning', warningHandler); - expect(warning).toBe(null); - }); -}); diff --git a/test/browsertype-launch-server.spec.ts b/test/browsertype-launch-server.spec.ts index a441a6f505..9574d2b671 100644 --- a/test/browsertype-launch-server.spec.ts +++ b/test/browsertype-launch-server.spec.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { folio } from './remoteServer.fixture'; +import { folio } from './fixtures'; const { it, expect, describe } = folio; describe('launch server', (suite, { mode }) => { @@ -90,10 +90,4 @@ describe('launch server', (suite, { mode }) => { expect(logs.some(log => log.startsWith('protocol:verbose:SEND ►'))).toBe(true); expect(logs.some(log => log.startsWith('protocol:verbose:◀ RECV'))).toBe(true); }); - - it('should work with cluster', async ({browserType, clusterRemoteServer}) => { - const browser = await browserType.connect({ wsEndpoint: clusterRemoteServer.wsEndpoint() }); - const page = await browser.newPage(); - expect(await page.evaluate('1 + 2')).toBe(3); - }); }); diff --git a/test/download.spec.ts b/test/download.spec.ts index 295ccf6fa7..4e01c70f94 100644 --- a/test/download.spec.ts +++ b/test/download.spec.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { folio } from './remoteServer.fixture'; +import { folio } from './fixtures'; const { it, expect, beforeEach, describe } = folio; import fs from 'fs'; @@ -174,25 +174,6 @@ describe('download event', () => { await page.close(); }); - 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 }); - await page.setContent(`download`); - const [ download ] = await Promise.all([ - page.waitForEvent('download'), - page.click('a') - ]); - const nestedPath = testInfo.outputPath(path.join('these', 'are', 'directories', 'download.txt')); - await download.saveAs(nestedPath); - expect(fs.existsSync(nestedPath)).toBeTruthy(); - expect(fs.readFileSync(nestedPath).toString()).toBe('Hello world'); - const error = await download.path().catch(e => e); - expect(error.message).toContain('Path is not available when using browserType.connect(). Use saveAs() to save a local copy.'); - await browser.close(); - }); - it('should error when saving with downloads disabled', async ({testInfo, browser, server}) => { const page = await browser.newPage({ acceptDownloads: false }); await page.setContent(`download`); @@ -220,23 +201,6 @@ describe('download event', () => { await page.close(); }); - 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 }); - await page.setContent(`download`); - const [ download ] = await Promise.all([ - page.waitForEvent('download'), - page.click('a') - ]); - const userPath = testInfo.outputPath('download.txt'); - await download.delete(); - const { message } = await download.saveAs(userPath).catch(e => e); - expect(message).toContain('File already deleted. Save before deleting.'); - await browser.close(); - }); - it('should report non-navigation downloads', async ({browser, server}) => { // Mac WebKit embedder does not download in this case, although Safari does. server.setRoute('/download', (req, res) => { diff --git a/test/fixtures.spec.ts b/test/fixtures.spec.ts deleted file mode 100644 index cf99813696..0000000000 --- a/test/fixtures.spec.ts +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Copyright 2019 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 { folio } from './remoteServer.fixture'; -import { execSync } from 'child_process'; -import type { Browser } from '../index'; - -const { it, describe, expect, beforeEach, afterEach } = folio; - -it('should close the browser when the node process closes', test => { - test.slow(); -}, async ({browserType, remoteServer, isWindows, server}) => { - const browser = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); - const page = await browser.newPage(); - await page.goto(server.EMPTY_PAGE); - if (isWindows) - execSync(`taskkill /pid ${remoteServer.child().pid} /T /F`); - else - process.kill(remoteServer.child().pid); - const exitCode = await remoteServer.childExitCode(); - await browser.close(); - // We might not get browser exitCode in time when killing the parent node process, - // so we don't check it here. - expect(exitCode).toBe(isWindows ? 1 : 0); -}); - -describe('signals', (suite, { platform, headful }) => { - suite.skip(platform === 'win32' || headful); - suite.slow(); -}, () => { - let browser: Browser; - - beforeEach(async ({ browserType, server, remoteServer }) => { - browser = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); - const page = await browser.newPage(); - await page.goto(server.EMPTY_PAGE); - }); - - afterEach(async () => { - await browser.close(); - }); - - it('should report browser close signal', async ({remoteServer}) => { - const pid = await remoteServer.out('pid'); - process.kill(-pid, 'SIGTERM'); - expect(await remoteServer.out('exitCode')).toBe('null'); - expect(await remoteServer.out('signal')).toBe('SIGTERM'); - process.kill(remoteServer.child().pid); - await remoteServer.childExitCode(); - }); - - it('should report browser close signal 2', async ({remoteServer}) => { - const pid = await remoteServer.out('pid'); - process.kill(-pid, 'SIGKILL'); - expect(await remoteServer.out('exitCode')).toBe('null'); - expect(await remoteServer.out('signal')).toBe('SIGKILL'); - process.kill(remoteServer.child().pid); - await remoteServer.childExitCode(); - }); - - it('should close the browser on SIGINT', (test, { browserChannel }) => { - test.fixme(!!browserChannel, 'Uncomment on roll'); - }, 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('should close the browser on SIGTERM', (test, { browserChannel }) => { - test.fixme(!!browserChannel, 'Uncomment on roll'); - }, 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('should close the browser on SIGHUP', (test, { browserChannel }) => { - test.fixme(!!browserChannel, 'Uncomment on roll'); - }, 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); - }); -}); - -describe('stalling signals', (suite, { platform, headful }) => { - suite.skip(platform === 'win32' || headful); - suite.slow(); -}, () => { - let browser: Browser; - - beforeEach(async ({ browserType, server, stallingRemoteServer }) => { - browser = await browserType.connect({ wsEndpoint: stallingRemoteServer.wsEndpoint() }); - const page = await browser.newPage(); - await page.goto(server.EMPTY_PAGE); - }); - - afterEach(async () => { - await browser.close(); - }); - - it('should kill the browser on double SIGINT', async ({stallingRemoteServer}) => { - process.kill(stallingRemoteServer.child().pid, 'SIGINT'); - await stallingRemoteServer.out('stalled'); - process.kill(stallingRemoteServer.child().pid, 'SIGINT'); - expect(await stallingRemoteServer.out('exitCode')).toBe('null'); - expect(await stallingRemoteServer.out('signal')).toBe('SIGKILL'); - expect(await stallingRemoteServer.childExitCode()).toBe(130); - }); - - it('should kill the browser on SIGINT + SIGTERM', async ({stallingRemoteServer}) => { - process.kill(stallingRemoteServer.child().pid, 'SIGINT'); - await stallingRemoteServer.out('stalled'); - process.kill(stallingRemoteServer.child().pid, 'SIGTERM'); - expect(await stallingRemoteServer.out('exitCode')).toBe('null'); - expect(await stallingRemoteServer.out('signal')).toBe('SIGKILL'); - expect(await stallingRemoteServer.childExitCode()).toBe(0); - }); - - it('should kill the browser on SIGTERM + SIGINT', async ({stallingRemoteServer}) => { - process.kill(stallingRemoteServer.child().pid, 'SIGTERM'); - await stallingRemoteServer.out('stalled'); - process.kill(stallingRemoteServer.child().pid, 'SIGINT'); - expect(await stallingRemoteServer.out('exitCode')).toBe('null'); - expect(await stallingRemoteServer.out('signal')).toBe('SIGKILL'); - expect(await stallingRemoteServer.childExitCode()).toBe(130); - }); -}); diff --git a/tests/browsertype-connect.spec.ts b/tests/browsertype-connect.spec.ts new file mode 100644 index 0000000000..c9fa81d7ce --- /dev/null +++ b/tests/browsertype-connect.spec.ts @@ -0,0 +1,347 @@ +/** + * Copyright 2017 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 { slowTest as test, expect } from './config/playwrightTest'; +import fs from 'fs'; +import * as path from 'path'; + +test('should be able to reconnect to a browser', async ({browserType, startRemoteServer, server}) => { + const remoteServer = await startRemoteServer(); + { + const browser = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); + const browserContext = await browser.newContext(); + expect(browserContext.pages().length).toBe(0); + const page = await browserContext.newPage(); + expect(await page.evaluate('11 * 11')).toBe(121); + await page.goto(server.EMPTY_PAGE); + await browser.close(); + } + { + const browser = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); + const browserContext = await browser.newContext(); + const page = await browserContext.newPage(); + await page.goto(server.EMPTY_PAGE); + await browser.close(); + } +}); + +test('should be able to connect two browsers at the same time', async ({browserType, startRemoteServer}) => { + const remoteServer = await startRemoteServer(); + + const browser1 = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); + expect(browser1.contexts().length).toBe(0); + await browser1.newContext(); + expect(browser1.contexts().length).toBe(1); + + const browser2 = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); + expect(browser2.contexts().length).toBe(0); + await browser2.newContext(); + expect(browser2.contexts().length).toBe(1); + expect(browser1.contexts().length).toBe(1); + + await browser1.close(); + const page2 = await browser2.newPage(); + expect(await page2.evaluate(() => 7 * 6)).toBe(42); // original browser should still work + + await browser2.close(); +}); + +test('disconnected event should be emitted when browser is closed or server is closed', async ({browserType, startRemoteServer}) => { + const remoteServer = await startRemoteServer(); + + const browser1 = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); + const browser2 = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); + + let disconnected1 = 0; + let disconnected2 = 0; + browser1.on('disconnected', () => ++disconnected1); + browser2.on('disconnected', () => ++disconnected2); + + await Promise.all([ + new Promise(f => browser1.on('disconnected', f)), + browser1.close(), + ]); + expect(disconnected1).toBe(1); + expect(disconnected2).toBe(0); + + await Promise.all([ + new Promise(f => browser2.on('disconnected', f)), + remoteServer.close(), + ]); + expect(disconnected1).toBe(1); + expect(disconnected2).toBe(1); +}); + +test('disconnected event should have browser as argument', async ({browserType, startRemoteServer}) => { + const remoteServer = await startRemoteServer(); + const browser = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); + const [disconnected] = await Promise.all([ + new Promise(f => browser.on('disconnected', f)), + browser.close(), + ]); + expect(disconnected).toBe(browser); +}); + +test('should handle exceptions during connect', async ({browserType, startRemoteServer, mode}) => { + test.skip(mode !== 'default'); + + const remoteServer = await startRemoteServer(); + const __testHookBeforeCreateBrowser = () => { throw new Error('Dummy'); }; + const error = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint(), __testHookBeforeCreateBrowser } as any).catch(e => e); + expect(error.message).toContain('Dummy'); +}); + +test('should set the browser connected state', async ({browserType, startRemoteServer}) => { + const remoteServer = await startRemoteServer(); + const remote = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); + expect(remote.isConnected()).toBe(true); + await remote.close(); + expect(remote.isConnected()).toBe(false); +}); + +test('should throw when used after isConnected returns false', async ({browserType, startRemoteServer}) => { + const remoteServer = await startRemoteServer(); + const browser = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); + const page = await browser.newPage(); + await Promise.all([ + remoteServer.close(), + new Promise(f => browser.once('disconnected', f)), + ]); + expect(browser.isConnected()).toBe(false); + const error = await page.evaluate('1 + 1').catch(e => e) as Error; + expect(error.message).toContain('has been closed'); +}); + +test('should reject navigation when browser closes', async ({browserType, startRemoteServer, server}) => { + const remoteServer = await startRemoteServer(); + server.setRoute('/one-style.css', () => {}); + const browser = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); + const page = await browser.newPage(); + const navigationPromise = page.goto(server.PREFIX + '/one-style.html', {timeout: 60000}).catch(e => e); + await server.waitForRequest('/one-style.css'); + await browser.close(); + const error = await navigationPromise; + expect(error.message).toContain('Navigation failed because page was closed!'); +}); + +test('should reject waitForSelector when browser closes', async ({browserType, startRemoteServer, server}) => { + const remoteServer = await startRemoteServer(); + server.setRoute('/empty.html', () => {}); + const browser = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); + const page = await browser.newPage(); + const watchdog = page.waitForSelector('div', { state: 'attached', timeout: 60000 }).catch(e => e); + + // Make sure the previous waitForSelector has time to make it to the browser before we disconnect. + await page.waitForSelector('body', { state: 'attached' }); + + await browser.close(); + const error = await watchdog; + expect(error.message).toContain('Protocol error'); +}); + +test('should emit close events on pages and contexts', async ({browserType, startRemoteServer}) => { + const remoteServer = await startRemoteServer(); + const browser = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); + const context = await browser.newContext(); + const page = await context.newPage(); + let pageClosed = false; + page.on('close', () => pageClosed = true); + await Promise.all([ + new Promise(f => context.on('close', f)), + remoteServer.close() + ]); + expect(pageClosed).toBeTruthy(); +}); + +test('should terminate network waiters', async ({browserType, startRemoteServer, server}) => { + const remoteServer = await startRemoteServer(); + const browser = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); + const newPage = await browser.newPage(); + const results = await Promise.all([ + newPage.waitForRequest(server.EMPTY_PAGE).catch(e => e), + newPage.waitForResponse(server.EMPTY_PAGE).catch(e => e), + remoteServer.close(), + ]); + for (let i = 0; i < 2; i++) { + const message = results[i].message; + expect(message).toContain('Page closed'); + expect(message).not.toContain('Timeout'); + } +}); + +test('should respect selectors', async ({ playwright, browserType, startRemoteServer }) => { + const remoteServer = await startRemoteServer(); + + const mycss = () => ({ + query(root, selector) { + return root.querySelector(selector); + }, + queryAll(root: HTMLElement, selector: string) { + return Array.from(root.querySelectorAll(selector)); + } + }); + // Register one engine before connecting. + await playwright.selectors.register('mycss1', mycss); + + const browser1 = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); + const context1 = await browser1.newContext(); + + // Register another engine after creating context. + await playwright.selectors.register('mycss2', mycss); + + const page1 = await context1.newPage(); + await page1.setContent(`
hello
`); + expect(await page1.innerHTML('css=div')).toBe('hello'); + expect(await page1.innerHTML('mycss1=div')).toBe('hello'); + expect(await page1.innerHTML('mycss2=div')).toBe('hello'); + + const browser2 = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); + + // Register third engine after second connect. + await playwright.selectors.register('mycss3', mycss); + + const page2 = await browser2.newPage(); + await page2.setContent(`
hello
`); + expect(await page2.innerHTML('css=div')).toBe('hello'); + expect(await page2.innerHTML('mycss1=div')).toBe('hello'); + expect(await page2.innerHTML('mycss2=div')).toBe('hello'); + expect(await page2.innerHTML('mycss3=div')).toBe('hello'); + + await browser1.close(); +}); + +test('should not throw on close after disconnect', async ({browserType, startRemoteServer}) => { + const remoteServer = await startRemoteServer(); + const browser = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); + await browser.newPage(); + await Promise.all([ + new Promise(f => browser.on('disconnected', f)), + remoteServer.close() + ]); + await browser.close(); +}); + +test('should not throw on context.close after disconnect', async ({browserType, startRemoteServer}) => { + const remoteServer = await startRemoteServer(); + const browser = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); + const context = await browser.newContext(); + await context.newPage(); + await Promise.all([ + new Promise(f => browser.on('disconnected', f)), + remoteServer.close() + ]); + await context.close(); +}); + +test('should not throw on page.close after disconnect', async ({browserType, startRemoteServer}) => { + const remoteServer = await startRemoteServer(); + const browser = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); + const page = await browser.newPage(); + await Promise.all([ + new Promise(f => browser.on('disconnected', f)), + remoteServer.close() + ]); + await page.close(); +}); + +test('should saveAs videos from remote browser', async ({browserType, startRemoteServer}, testInfo) => { + const remoteServer = await startRemoteServer(); + const browser = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); + const videosPath = testInfo.outputPath(); + const context = await browser.newContext({ + recordVideo: { dir: videosPath, size: { width: 320, height: 240 } }, + }); + const page = await context.newPage(); + await page.evaluate(() => document.body.style.backgroundColor = 'red'); + await new Promise(r => setTimeout(r, 1000)); + await context.close(); + + const savedAsPath = testInfo.outputPath('my-video.webm'); + await page.video().saveAs(savedAsPath); + expect(fs.existsSync(savedAsPath)).toBeTruthy(); + const error = await page.video().path().catch(e => e); + expect(error.message).toContain('Path is not available when using browserType.connect(). Use saveAs() to save a local copy.'); +}); + +test('should be able to connect 20 times to a single server without warnings', async ({browserType, startRemoteServer}) => { + const remoteServer = await startRemoteServer(); + + let warning = null; + const warningHandler = w => warning = w; + process.on('warning', warningHandler); + + const browsers = []; + for (let i = 0; i < 20; i++) + browsers.push(await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() })); + await Promise.all([browsers.map(browser => browser.close())]); + + process.off('warning', warningHandler); + expect(warning).toBe(null); +}); + +test('should save download', async ({server, browserType, startRemoteServer}, testInfo) => { + server.setRoute('/download', (req, res) => { + res.setHeader('Content-Type', 'application/octet-stream'); + res.setHeader('Content-Disposition', 'attachment'); + res.end(`Hello world`); + }); + + const remoteServer = await startRemoteServer(); + const browser = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); + const page = await browser.newPage({ acceptDownloads: true }); + await page.setContent(`download`); + const [ download ] = await Promise.all([ + page.waitForEvent('download'), + page.click('a') + ]); + const nestedPath = testInfo.outputPath(path.join('these', 'are', 'directories', 'download.txt')); + await download.saveAs(nestedPath); + expect(fs.existsSync(nestedPath)).toBeTruthy(); + expect(fs.readFileSync(nestedPath).toString()).toBe('Hello world'); + const error = await download.path().catch(e => e); + expect(error.message).toContain('Path is not available when using browserType.connect(). Use saveAs() to save a local copy.'); + await browser.close(); +}); + +test('should error when saving download after deletion', async ({server, browserType, startRemoteServer}, testInfo) => { + server.setRoute('/download', (req, res) => { + res.setHeader('Content-Type', 'application/octet-stream'); + res.setHeader('Content-Disposition', 'attachment'); + res.end(`Hello world`); + }); + + const remoteServer = await startRemoteServer(); + const browser = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); + const page = await browser.newPage({ acceptDownloads: true }); + await page.setContent(`download`); + const [ download ] = await Promise.all([ + page.waitForEvent('download'), + page.click('a') + ]); + const userPath = testInfo.outputPath('download.txt'); + await download.delete(); + const { message } = await download.saveAs(userPath).catch(e => e); + expect(message).toContain('File already deleted. Save before deleting.'); + await browser.close(); +}); + +test('should work with cluster', async ({browserType, startRemoteServer}) => { + const remoteServer = await startRemoteServer({ inCluster: true }); + const browser = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); + const page = await browser.newPage(); + expect(await page.evaluate('1 + 2')).toBe(3); +}); diff --git a/tests/config/browserEnv.ts b/tests/config/browserEnv.ts index c02f583af1..6138afbc1a 100644 --- a/tests/config/browserEnv.ts +++ b/tests/config/browserEnv.ts @@ -27,6 +27,7 @@ import * as util from 'util'; import * as childProcess from 'child_process'; import { PlaywrightTestArgs } from './playwrightTest'; import { BrowserTestArgs } from './browserTest'; +import { RemoteServer, RemoteServerOptions } from './remoteServer'; const mkdtempAsync = util.promisify(fs.mkdtemp); @@ -107,6 +108,7 @@ export class PlaywrightEnv implements Env { private _coverage: ReturnType | undefined; private _userDataDirs: string[] = []; private _persistentContext: BrowserContext | undefined; + private _remoteServer: RemoteServer | undefined; constructor(browserName: BrowserName, options: LaunchOptions & TestOptions) { this._browserName = browserName; @@ -150,6 +152,14 @@ export class PlaywrightEnv implements Env { return { context: this._persistentContext, page }; } + private async _startRemoteServer(options?: RemoteServerOptions): Promise { + if (this._remoteServer) + throw new Error('can only start one remote server'); + this._remoteServer = new RemoteServer(); + await this._remoteServer._start(this._browserType, this._browserOptions, options); + return this._remoteServer; + } + async beforeEach(testInfo: TestInfo) { // Different screenshots per browser. testInfo.snapshotPathSegment = this._browserName; @@ -172,6 +182,7 @@ export class PlaywrightEnv implements Env { createUserDataDir: this._createUserDataDir.bind(this), launchPersistent: this._launchPersistent.bind(this), toImpl: (this._playwright as any)._toImpl, + startRemoteServer: this._startRemoteServer.bind(this), }; } @@ -182,6 +193,10 @@ export class PlaywrightEnv implements Env { await this._persistentContext.close(); this._persistentContext = undefined; } + if (this._remoteServer) { + await this._remoteServer.close(); + this._remoteServer = undefined; + } } async afterAll(workerInfo: WorkerInfo) { diff --git a/tests/config/default.config.ts b/tests/config/default.config.ts index 9982d23cbf..d16d236f3c 100644 --- a/tests/config/default.config.ts +++ b/tests/config/default.config.ts @@ -16,7 +16,7 @@ import { setConfig, Config } from '../folio/out'; import * as path from 'path'; -import { test as playwrightTest } from './playwrightTest'; +import { test as playwrightTest, slowTest as playwrightSlowTest } from './playwrightTest'; import { test as browserTest } from './browserTest'; import { test as pageTest } from './pageTest'; import { PlaywrightEnv, BrowserEnv, PageEnv, BrowserName } from './browserEnv'; @@ -57,6 +57,7 @@ for (const browserName of browsers) { video: !!process.env.PWVIDEO, }; playwrightTest.runWith(browserName, serverEnv, new PlaywrightEnv(browserName, options), {}); + playwrightSlowTest.runWith(browserName, serverEnv, new PlaywrightEnv(browserName, options), { timeout: config.timeout * 3 }); browserTest.runWith(browserName, serverEnv, new BrowserEnv(browserName, options), {}); pageTest.runWith(browserName, serverEnv, new PageEnv(browserName, options), {}); } diff --git a/tests/config/playwrightTest.ts b/tests/config/playwrightTest.ts index 5d17103ae9..d3942e255a 100644 --- a/tests/config/playwrightTest.ts +++ b/tests/config/playwrightTest.ts @@ -18,6 +18,7 @@ import { newTestType } from '../folio/out'; import type { Browser, BrowserType, LaunchOptions, BrowserContext, Page } from '../../index'; import { CommonTestArgs } from './pageTest'; import type { ServerTestArgs } from './serverTest'; +import { RemoteServer, RemoteServerOptions } from './remoteServer'; export { expect } from 'folio'; export type PlaywrightTestArgs = CommonTestArgs & { @@ -28,6 +29,8 @@ export type PlaywrightTestArgs = CommonTestArgs & { headful: boolean; createUserDataDir: () => Promise; launchPersistent: (options?: Parameters['launchPersistentContext']>[1]) => Promise<{ context: BrowserContext, page: Page }>; + startRemoteServer: (options?: RemoteServerOptions) => Promise; }; export const test = newTestType(); +export const slowTest = newTestType(); diff --git a/test/fixtures/closeme.js b/tests/config/remote-server-impl.js similarity index 100% rename from test/fixtures/closeme.js rename to tests/config/remote-server-impl.js diff --git a/test/remoteServer.fixture.ts b/tests/config/remoteServer.ts similarity index 68% rename from test/remoteServer.fixture.ts rename to tests/config/remoteServer.ts index 67ba1b8a50..5b84cb3285 100644 --- a/test/remoteServer.fixture.ts +++ b/tests/config/remoteServer.ts @@ -14,43 +14,17 @@ * limitations under the License. */ -import { folio as base } from './fixtures'; - import path from 'path'; import { spawn } from 'child_process'; -import type { BrowserType, Browser, LaunchOptions } from '..'; +import type { BrowserType, Browser, LaunchOptions } from '../../index'; -type ServerFixtures = { - remoteServer: RemoteServer; - stallingRemoteServer: RemoteServer; - clusterRemoteServer: RemoteServer; +const playwrightPath = path.join(__dirname, '..', '..'); + +export type RemoteServerOptions = { + stallOnClose?: boolean; + inCluster?: boolean; + url?: string; }; -const fixtures = base.extend(); - -fixtures.remoteServer.init(async ({ browserType, browserOptions }, run) => { - const remoteServer = new RemoteServer(); - await remoteServer._start(browserType, browserOptions); - await run(remoteServer); - await remoteServer.close(); -}); - -fixtures.stallingRemoteServer.init(async ({ browserType, browserOptions }, run) => { - const remoteServer = new RemoteServer(); - await remoteServer._start(browserType, browserOptions, { stallOnClose: true }); - await run(remoteServer); - await remoteServer.close(); -}); - -fixtures.clusterRemoteServer.init(async ({ browserType, browserOptions }, run) => { - const remoteServer = new RemoteServer(); - await remoteServer._start(browserType, browserOptions, { inCluster: true }); - await run(remoteServer); - await remoteServer.close(); -}); - -export const folio = fixtures.build(); - -const playwrightPath = path.join(__dirname, '..'); export class RemoteServer { _output: Map; @@ -63,7 +37,7 @@ export class RemoteServer { _didExit: boolean; _wsEndpoint: string; - async _start(browserType: BrowserType, browserOptions: LaunchOptions, extraOptions?: { stallOnClose?: boolean; inCluster?: boolean }) { + async _start(browserType: BrowserType, browserOptions: LaunchOptions, remoteServerOptions: RemoteServerOptions = {}) { this._output = new Map(); this._outputCallback = new Map(); this._didExit = false; @@ -81,9 +55,9 @@ export class RemoteServer { playwrightPath, browserTypeName: browserType.name(), launchOptions, - ...extraOptions, + ...remoteServerOptions, }; - this._child = spawn('node', [path.join(__dirname, 'fixtures', 'closeme.js'), JSON.stringify(options)], { env: process.env }); + this._child = spawn('node', [path.join(__dirname, 'remote-server-impl.js'), JSON.stringify(options)], { env: process.env }); this._child.on('error', (...args) => console.log('ERROR', ...args)); this._exitPromise = new Promise(resolve => this._child.on('exit', (exitCode, signal) => { this._didExit = true; @@ -108,6 +82,12 @@ export class RemoteServer { }); this._wsEndpoint = await this.out('wsEndpoint'); + + if (remoteServerOptions.url) { + this._browser = await this._browserType.connect({ wsEndpoint: this._wsEndpoint }); + const page = await this._browser.newPage(); + await page.goto(remoteServerOptions.url); + } } _addOutput(key, value) { @@ -137,6 +117,10 @@ export class RemoteServer { } async close() { + if (this._browser) { + await this._browser.close(); + this._browser = undefined; + } if (!this._didExit) this._child.kill(); return await this.childExitCode(); diff --git a/tests/config/serverEnv.ts b/tests/config/serverEnv.ts index 1f6a1b5f3d..e3fa2ad944 100644 --- a/tests/config/serverEnv.ts +++ b/tests/config/serverEnv.ts @@ -25,8 +25,8 @@ export class ServerEnv { private _socksServer: any; async beforeAll(workerInfo: WorkerInfo) { - const assetsPath = path.join(__dirname, '..', 'assets'); - const cachedPath = path.join(__dirname, '..', 'assets', 'cached'); + const assetsPath = path.join(__dirname, '..', '..', 'test', 'assets'); + const cachedPath = path.join(__dirname, '..', '..', 'test', 'assets', 'cached'); const port = 8907 + workerInfo.workerIndex * 2; this._server = await TestServer.create(assetsPath, port); diff --git a/tests/signals.spec.ts b/tests/signals.spec.ts new file mode 100644 index 0000000000..ecd64d1124 --- /dev/null +++ b/tests/signals.spec.ts @@ -0,0 +1,123 @@ +/** + * Copyright 2019 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 { slowTest as test, expect } from './config/playwrightTest'; +import { execSync } from 'child_process'; + +test('should close the browser when the node process closes', async ({startRemoteServer, isWindows, server}) => { + const remoteServer = await startRemoteServer({ url: server.EMPTY_PAGE }); + if (isWindows) + execSync(`taskkill /pid ${remoteServer.child().pid} /T /F`); + else + process.kill(remoteServer.child().pid); + // We might not get browser exitCode in time when killing the parent node process, + // so we don't check it here. + expect(await remoteServer.childExitCode()).toBe(isWindows ? 1 : 0); +}); + +test('should report browser close signal', async ({startRemoteServer, server, platform, headful}) => { + test.skip(platform === 'win32' || headful); + + const remoteServer = await startRemoteServer({ url: server.EMPTY_PAGE }); + const pid = await remoteServer.out('pid'); + process.kill(-pid, 'SIGTERM'); + expect(await remoteServer.out('exitCode')).toBe('null'); + expect(await remoteServer.out('signal')).toBe('SIGTERM'); + process.kill(remoteServer.child().pid); + await remoteServer.childExitCode(); +}); + +test('should report browser close signal 2', async ({startRemoteServer, server, platform, headful}) => { + test.skip(platform === 'win32' || headful); + + const remoteServer = await startRemoteServer({ url: server.EMPTY_PAGE }); + const pid = await remoteServer.out('pid'); + process.kill(-pid, 'SIGKILL'); + expect(await remoteServer.out('exitCode')).toBe('null'); + expect(await remoteServer.out('signal')).toBe('SIGKILL'); + process.kill(remoteServer.child().pid); + await remoteServer.childExitCode(); +}); + +test('should close the browser on SIGINT', async ({startRemoteServer, server, browserChannel, platform, headful}) => { + test.skip(platform === 'win32' || headful); + test.fixme(!!browserChannel, 'Uncomment on roll'); + + const remoteServer = await startRemoteServer({ url: server.EMPTY_PAGE }); + 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); +}); + +test('should close the browser on SIGTERM', async ({startRemoteServer, server, browserChannel, platform, headful}) => { + test.skip(platform === 'win32' || headful); + test.fixme(!!browserChannel, 'Uncomment on roll'); + + const remoteServer = await startRemoteServer({ url: server.EMPTY_PAGE }); + 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); +}); + +test('should close the browser on SIGHUP', async ({startRemoteServer, server, browserChannel, platform, headful}) => { + test.skip(platform === 'win32' || headful); + test.fixme(!!browserChannel, 'Uncomment on roll'); + + const remoteServer = await startRemoteServer({ url: server.EMPTY_PAGE }); + 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); +}); + +test('should kill the browser on double SIGINT', async ({startRemoteServer, server, platform, headful}) => { + test.skip(platform === 'win32' || headful); + + const remoteServer = await startRemoteServer({ stallOnClose: true, url: server.EMPTY_PAGE }); + 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); +}); + +test('should kill the browser on SIGINT + SIGTERM', async ({startRemoteServer, server, platform, headful}) => { + test.skip(platform === 'win32' || headful); + + const remoteServer = await startRemoteServer({ stallOnClose: true, url: server.EMPTY_PAGE }); + 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); +}); + +test('should kill the browser on SIGTERM + SIGINT', async ({startRemoteServer, server, platform, headful}) => { + test.skip(platform === 'win32' || headful); + + const remoteServer = await startRemoteServer({ stallOnClose: true, url: server.EMPTY_PAGE }); + 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); +});