From 622c1a8da6ee3fbae9ef2bb6c51a8684b830d5ed Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 7 Dec 2022 14:36:32 -0800 Subject: [PATCH] tests: run browserType.connect tests against launchServer and run-server (#19340) --- tests/config/browserTest.ts | 36 +- tests/config/commonFixtures.ts | 4 +- tests/config/remoteServer.ts | 40 +- tests/library/browsertype-connect.spec.ts | 1431 ++++++++++-------- tests/library/port-forwarding-server.spec.ts | 202 --- tests/library/signals.spec.ts | 20 +- utils/testserver/index.ts | 2 +- 7 files changed, 877 insertions(+), 858 deletions(-) delete mode 100644 tests/library/port-forwarding-server.spec.ts diff --git a/tests/config/browserTest.ts b/tests/config/browserTest.ts index 4cead198fa..3b895e8f06 100644 --- a/tests/config/browserTest.ts +++ b/tests/config/browserTest.ts @@ -21,8 +21,7 @@ import * as path from 'path'; import type { BrowserContext, BrowserContextOptions, BrowserType, Page } from 'playwright-core'; import { removeFolders } from '../../packages/playwright-core/lib/utils/fileUtils'; import { baseTest } from './baseTest'; -import type { RemoteServerOptions } from './remoteServer'; -import { RemoteServer } from './remoteServer'; +import { type RemoteServerOptions, type PlaywrightServer, RunServer, RemoteServer } from './remoteServer'; import type { Log } from '../../packages/trace/src/har'; import { parseHar } from '../config/utils'; @@ -36,10 +35,15 @@ export type BrowserTestWorkerFixtures = PageWorkerFixtures & { isElectron: boolean; }; +interface StartRemoteServer { + (kind: 'run-server' | 'launchServer'): Promise; + (kind: 'launchServer', options?: RemoteServerOptions): Promise; +} + type BrowserTestTestFixtures = PageTestFixtures & { createUserDataDir: () => Promise; launchPersistent: (options?: Parameters[1]) => Promise<{ context: BrowserContext, page: Page }>; - startRemoteServer: (options?: RemoteServerOptions) => Promise; + startRemoteServer: StartRemoteServer; contextFactory: (options?: BrowserContextOptions) => Promise; pageWithHar(options?: { outputPath?: string, content?: 'embed' | 'attach' | 'omit', omitContent?: boolean }): Promise<{ context: BrowserContext, page: Page, getLog: () => Promise, getZip: () => Promise> }> }; @@ -118,16 +122,24 @@ const test = baseTest.extend }, startRemoteServer: async ({ childProcess, browserType }, run) => { - let remoteServer: RemoteServer | undefined; - await run(async options => { - if (remoteServer) + let server: PlaywrightServer | undefined; + const fn = async (kind: 'launchServer' | 'run-server', options?: RemoteServerOptions) => { + if (server) throw new Error('can only start one remote server'); - remoteServer = new RemoteServer(); - await remoteServer._start(childProcess, browserType, options); - return remoteServer; - }); - if (remoteServer) { - await remoteServer.close(); + if (kind === 'launchServer') { + const remoteServer = new RemoteServer(); + await remoteServer._start(childProcess, browserType, options); + server = remoteServer; + } else { + const runServer = new RunServer(); + await runServer._start(childProcess); + server = runServer; + } + return server; + }; + await run(fn as any); + if (server) { + await server.close(); // Give any connected browsers a chance to disconnect to avoid // poisoning next test with quasy-alive browsers. await new Promise(f => setTimeout(f, 1000)); diff --git a/tests/config/commonFixtures.ts b/tests/config/commonFixtures.ts index 344401668c..bc7ddb47d4 100644 --- a/tests/config/commonFixtures.ts +++ b/tests/config/commonFixtures.ts @@ -31,7 +31,7 @@ export class TestChildProcess { params: TestChildParams; process: ChildProcess; output = ''; - onOutput?: () => void; + onOutput?: (chunk: string | Buffer) => void; exited: Promise<{ exitCode: number, signal: string | null }>; exitCode: Promise; @@ -59,7 +59,7 @@ export class TestChildProcess { this.output += String(chunk); if (process.env.PWTEST_DEBUG) process.stdout.write(String(chunk)); - this.onOutput?.(); + this.onOutput?.(chunk); for (const cb of this._outputCallbacks) cb(); this._outputCallbacks.clear(); diff --git a/tests/config/remoteServer.ts b/tests/config/remoteServer.ts index fa003d3117..b0681f557c 100644 --- a/tests/config/remoteServer.ts +++ b/tests/config/remoteServer.ts @@ -18,6 +18,42 @@ import path from 'path'; import type { BrowserType, Browser, LaunchOptions } from 'playwright-core'; import type { CommonFixtures, TestChildProcess } from './commonFixtures'; +export interface PlaywrightServer { + wsEndpoint(): string; + close(): Promise; +} + +export class RunServer implements PlaywrightServer { + private _process: TestChildProcess; + _wsEndpoint: string; + + async _start(childProcess: CommonFixtures['childProcess']) { + this._process = childProcess({ + command: ['node', path.join(__dirname, '..', '..', 'packages', 'playwright-core', 'lib', 'cli', 'cli.js'), 'run-server'], + }); + + let wsEndpointCallback; + const wsEndpointPromise = new Promise(f => wsEndpointCallback = f); + this._process.onOutput = data => { + const prefix = 'Listening on '; + const line = data.toString(); + if (line.startsWith(prefix)) + wsEndpointCallback(line.substr(prefix.length)); + }; + + this._wsEndpoint = await wsEndpointPromise; + } + + wsEndpoint() { + return this._wsEndpoint; + } + + async close() { + await this._process.close(); + await this._process.exitCode; + } +} + export type RemoteServerOptions = { stallOnClose?: boolean; disconnectOnSIGHUP?: boolean; @@ -26,7 +62,7 @@ export type RemoteServerOptions = { url?: string; }; -export class RemoteServer { +export class RemoteServer implements PlaywrightServer { private _process: TestChildProcess; _output: Map; _outputCallback: Map void>; @@ -114,6 +150,6 @@ export class RemoteServer { this._browser = undefined; } await this._process.close(); - return await this.childExitCode(); + await this.childExitCode(); } } diff --git a/tests/library/browsertype-connect.spec.ts b/tests/library/browsertype-connect.spec.ts index 379713445d..87713c87a4 100644 --- a/tests/library/browsertype-connect.spec.ts +++ b/tests/library/browsertype-connect.spec.ts @@ -17,648 +17,821 @@ import fs from 'fs'; import os from 'os'; +import http from 'http'; +import type net from 'net'; import * as path from 'path'; import { getUserAgent } from '../../packages/playwright-core/lib/common/userAgent'; import WebSocket from 'ws'; -import { expect, playwrightTest as test } from '../config/browserTest'; +import { expect, playwrightTest } from '../config/browserTest'; import { parseTrace, suppressCertificateWarning } from '../config/utils'; import formidable from 'formidable'; +import type { Browser, ConnectOptions } from 'playwright-core'; + +type ExtraFixtures = { + connect: (wsEndpoint: string, options?: ConnectOptions, redirectPortForTest?: number) => Promise, + dummyServerPort: number, +}; +const test = playwrightTest.extend({ + connect: async ({ browserType }, use) => { + let browser: Browser | undefined; + await use(async (wsEndpoint, options = {}, redirectPortForTest): Promise => { + (options as any).__testHookRedirectPortForwarding = redirectPortForTest; + options.headers = { + 'x-playwright-launch-options': JSON.stringify((browserType as any)._defaultLaunchOptions || {}), + ...options.headers, + }; + browser = await browserType.connect(wsEndpoint, options); + return browser; + }); + await browser?.close(); + }, + + dummyServerPort: async ({}, use) => { + const server = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => { + res.end('from-dummy-server'); + }); + await new Promise(resolve => server.listen(0, resolve)); + await use((server.address() as net.AddressInfo).port); + await new Promise(resolve => server.close(resolve)); + }, +}); test.slow(true, 'All connect tests are slow'); -test('should connect over wss', async ({ browserType, startRemoteServer, httpsServer, mode }) => { - test.skip(mode !== 'default'); // Out of process transport does not allow us to set env vars dynamically. - const remoteServer = await startRemoteServer(); +for (const kind of ['launchServer', 'run-server'] as const) { + test.describe(kind, () => { - const oldValue = process.env['NODE_TLS_REJECT_UNAUTHORIZED']; - // https://stackoverflow.com/a/21961005/552185 - process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0'; - suppressCertificateWarning(); - try { - httpsServer.onceWebSocketConnection((ws, request) => { - const remote = new WebSocket(remoteServer.wsEndpoint(), [], { - perMessageDeflate: false, - maxPayload: 256 * 1024 * 1024, // 256Mb, - }); - const remoteReadyPromise = new Promise((f, r) => { - remote.once('open', f); - remote.once('error', r); - }); - remote.on('close', () => ws.close()); - remote.on('error', error => ws.close()); - remote.on('message', message => ws.send(message)); - ws.on('message', async message => { - await remoteReadyPromise; - remote.send(message); - }); - ws.on('close', () => remote.close()); - ws.on('error', () => remote.close()); + test('should connect over wss', async ({ connect, startRemoteServer, httpsServer, mode }) => { + test.skip(mode !== 'default'); // Out of process transport does not allow us to set env vars dynamically. + const remoteServer = await startRemoteServer(kind); + + const oldValue = process.env['NODE_TLS_REJECT_UNAUTHORIZED']; + // https://stackoverflow.com/a/21961005/552185 + process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0'; + suppressCertificateWarning(); + try { + httpsServer.onceWebSocketConnection((ws, request) => { + const headers = Object.fromEntries(Object.entries(request.headers).filter(entry => entry[0].startsWith('x-playwright'))); + const remote = new WebSocket(remoteServer.wsEndpoint(), [], { + perMessageDeflate: false, + maxPayload: 256 * 1024 * 1024, // 256Mb, + headers, + }); + const remoteReadyPromise = new Promise((f, r) => { + remote.once('open', f); + remote.once('error', r); + }); + remote.on('close', () => ws.close()); + remote.on('error', error => ws.close()); + remote.on('message', message => ws.send(message)); + ws.on('message', async message => { + await remoteReadyPromise; + remote.send(message); + }); + ws.on('close', () => remote.close()); + ws.on('error', () => remote.close()); + }); + const browser = await connect(`wss://localhost:${httpsServer.PORT}/ws`); + expect(browser.version()).toBeTruthy(); + await browser.close(); + } finally { + process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = oldValue; + } }); - const browser = await browserType.connect(`wss://localhost:${httpsServer.PORT}/ws`); - expect(browser.version()).toBeTruthy(); - await browser.close(); - } finally { - process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = oldValue; - } -}); -test('should print HTTP error', async ({ browserType, server, mode }) => { - test.skip(mode !== 'default'); // Out of process transport does not allow us to set env vars dynamically. - const error = await browserType.connect(`ws://localhost:${server.PORT}/ws-401`).catch(e => e); - expect(error.message).toContain('401'); - expect(error.message).toContain('Unauthorized body'); -}); - -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(); - expect(browser2.contexts().length).toBe(1); - const page2 = await browser2.newPage(); - expect(await page2.evaluate(() => 7 * 6)).toBe(42); // original browser should still work - - await browser2.close(); -}); - -test('should timeout in socket while connecting', async ({ browserType, startRemoteServer, server }) => { - const e = await browserType.connect({ - wsEndpoint: `ws://localhost:${server.PORT}/ws-slow`, - timeout: 1000, - }).catch(e => e); - expect(e.message).toContain('browserType.connect: Timeout 1000ms exceeded'); -}); - -test('should timeout in connect while connecting', async ({ browserType, startRemoteServer, server }) => { - const e = await browserType.connect({ - wsEndpoint: `ws://localhost:${server.PORT}/ws`, - timeout: 100, - }).catch(e => e); - expect(e.message).toContain('browserType.connect: Timeout 100ms exceeded'); -}); - -test('should send extra headers with connect request', async ({ browserType, startRemoteServer, server }) => { - const [request] = await Promise.all([ - server.waitForWebSocketConnectionRequest(), - browserType.connect({ - wsEndpoint: `ws://localhost:${server.PORT}/ws`, - headers: { - 'User-Agent': 'Playwright', - 'foo': 'bar', - }, - timeout: 100, - }).catch(() => {}) - ]); - expect(request.headers['user-agent']).toBe('Playwright'); - expect(request.headers['foo']).toBe('bar'); -}); - -test('should send default User-Agent and X-Playwright-Browser headers with connect request', async ({ browserType, browserName, server }) => { - const [request] = await Promise.all([ - server.waitForWebSocketConnectionRequest(), - browserType.connect({ - wsEndpoint: `ws://localhost:${server.PORT}/ws`, - headers: { - 'foo': 'bar', - }, - timeout: 100, - }).catch(() => {}) - ]); - expect(request.headers['user-agent']).toBe(getUserAgent()); - expect(request.headers['x-playwright-browser']).toBe(browserName); - expect(request.headers['foo']).toBe('bar'); -}); - -test('should support slowmo option', async ({ browserType, startRemoteServer }) => { - const remoteServer = await startRemoteServer(); - - const browser1 = await browserType.connect(remoteServer.wsEndpoint(), { slowMo: 200 }); - const start = Date.now(); - await browser1.newContext(); - await browser1.close(); - expect(Date.now() - start).toBeGreaterThan(199); -}); - -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() }); - await browser1.newPage(); - - const browser2 = await browserType.connect({ wsEndpoint: remoteServer.wsEndpoint() }); - await browser2.newPage(); - - 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 throw when calling waitForNavigation 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([ - remoteServer.close(), - new Promise(f => browser.once('disconnected', f)), - ]); - expect(browser.isConnected()).toBe(false); - const error = await page.waitForNavigation().catch(e => e); - expect(error.message).toContain('Navigation failed because page was 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('has been 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('has been closed'); -}); - -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 connecting remotely. 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(); - 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 connecting remotely. 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(); - 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('Target page, context or browser has been closed'); - 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); -}); - -test('should properly disconnect when connection closes from the client side', async ({ browserType, startRemoteServer, server }) => { - server.setRoute('/one-style.css', () => {}); - const remoteServer = await startRemoteServer(); - 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); - const waitForNavigationPromise = page.waitForNavigation().catch(e => e); - - const disconnectedPromise = new Promise(f => browser.once('disconnected', f)); - // This closes the websocket. - (browser as any)._connection.close(); - await disconnectedPromise; - expect(browser.isConnected()).toBe(false); - - const navMessage = (await navigationPromise).message; - expect(navMessage).toContain('Connection closed'); - expect(navMessage).toContain('Closed by'); - expect(navMessage).toContain(__filename); - expect((await waitForNavigationPromise).message).toContain('Navigation failed because page was closed'); - expect((await page.goto(server.EMPTY_PAGE).catch(e => e)).message).toContain('has been closed'); - expect((await page.waitForNavigation().catch(e => e)).message).toContain('Navigation failed because page was closed'); -}); - -test('should properly disconnect when connection closes from the server side', async ({ browserType, startRemoteServer, server, platform }) => { - test.skip(platform === 'win32', 'Cannot send signals'); - - server.setRoute('/one-style.css', () => {}); - const remoteServer = await startRemoteServer({ disconnectOnSIGHUP: true }); - 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); - const waitForNavigationPromise = page.waitForNavigation().catch(e => e); - - const disconnectedPromise = new Promise(f => browser.once('disconnected', f)); - // This closes the websocket server. - process.kill(remoteServer.child().pid, 'SIGHUP'); - await disconnectedPromise; - expect(browser.isConnected()).toBe(false); - - expect((await navigationPromise).message).toContain('has been closed'); - expect((await waitForNavigationPromise).message).toContain('Navigation failed because page was closed'); - expect((await page.goto(server.EMPTY_PAGE).catch(e => e)).message).toContain('has been closed'); - expect((await page.waitForNavigation().catch(e => e)).message).toContain('Navigation failed because page was closed'); -}); - -test('should be able to connect when the wsEndpont is passed as the first argument', async ({ browserType, startRemoteServer }) => { - const remoteServer = await startRemoteServer(); - const browser = await browserType.connect(remoteServer.wsEndpoint()); - const page = await browser.newPage(); - expect(await page.evaluate('1 + 2')).toBe(3); - await browser.close(); -}); - -test('should save har', async ({ browserType, startRemoteServer, server }, testInfo) => { - const remoteServer = await startRemoteServer(); - const browser = await browserType.connect(remoteServer.wsEndpoint()); - const harPath = testInfo.outputPath('test.har'); - const context = await browser.newContext({ - recordHar: { - path: harPath, - } - }); - const page = await context.newPage(); - await page.goto(server.EMPTY_PAGE); - await context.close(); - await browser.close(); - - const log = JSON.parse(fs.readFileSync(harPath).toString())['log']; - expect(log.entries.length).toBe(1); - const entry = log.entries[0]; - expect(entry.pageref).toBe(log.pages[0].id); - expect(entry.request.url).toBe(server.EMPTY_PAGE); -}); - -test('should record trace with sources', async ({ browserType, startRemoteServer, server, trace }, testInfo) => { - test.skip(trace === 'on'); - const remoteServer = await startRemoteServer(); - const browser = await browserType.connect(remoteServer.wsEndpoint()); - const context = await browser.newContext(); - const page = await context.newPage(); - - await context.tracing.start({ sources: true }); - await page.goto(server.EMPTY_PAGE); - await page.setContent(''); - await page.click('"Click"'); - await context.tracing.stop({ path: testInfo.outputPath('trace1.zip') }); - - await context.close(); - await browser.close(); - - const { resources } = await parseTrace(testInfo.outputPath('trace1.zip')); - const sourceNames = Array.from(resources.keys()).filter(k => k.endsWith('.txt')); - expect(sourceNames.length).toBe(1); - const sourceFile = resources.get(sourceNames[0]); - const thisFile = await fs.promises.readFile(__filename); - expect(sourceFile).toEqual(thisFile); -}); - -test('should fulfill with global fetch result', async ({ browserType, startRemoteServer, playwright, server }) => { - const remoteServer = await startRemoteServer(); - const browser = await browserType.connect(remoteServer.wsEndpoint()); - const context = await browser.newContext(); - const page = await context.newPage(); - - await page.route('**/*', async route => { - const request = await playwright.request.newContext(); - const response = await request.get(server.PREFIX + '/simple.json'); - route.fulfill({ response }); - }); - const response = await page.goto(server.EMPTY_PAGE); - expect(response.status()).toBe(200); - expect(await response.json()).toEqual({ 'foo': 'bar' }); -}); - -test('should upload large file', async ({ browserType, startRemoteServer, server, browserName, isMac }, testInfo) => { - test.skip(browserName === 'webkit' && isMac && parseInt(os.release(), 10) < 20, 'WebKit for macOS 10.15 is frozen and does not have corresponding protocol features.'); - test.slow(); - const remoteServer = await startRemoteServer(); - const browser = await browserType.connect(remoteServer.wsEndpoint()); - const context = await browser.newContext(); - const page = await context.newPage(); - - await page.goto(server.PREFIX + '/input/fileupload.html'); - const uploadFile = testInfo.outputPath('200MB.zip'); - const str = 'A'.repeat(4 * 1024); - const stream = fs.createWriteStream(uploadFile); - for (let i = 0; i < 50 * 1024; i++) { - await new Promise((fulfill, reject) => { - stream.write(str, err => { - if (err) - reject(err); - else - fulfill(); - }); + test('should print HTTP error', async ({ connect, server, mode }) => { + test.skip(mode !== 'default'); // Out of process transport does not allow us to set env vars dynamically. + const error = await connect(`ws://localhost:${server.PORT}/ws-401`).catch(e => e); + expect(error.message).toContain('401'); + expect(error.message).toContain('Unauthorized body'); }); - } - await new Promise(f => stream.end(f)); - const input = page.locator('input[type="file"]'); - const events = await input.evaluateHandle(e => { - const events = []; - e.addEventListener('input', () => events.push('input')); - e.addEventListener('change', () => events.push('change')); - return events; - }); - await input.setInputFiles(uploadFile); - expect(await input.evaluate(e => (e as HTMLInputElement).files[0].name)).toBe('200MB.zip'); - expect(await events.evaluate(e => e)).toEqual(['input', 'change']); - const serverFilePromise = new Promise(fulfill => { - server.setRoute('/upload', async (req, res) => { - const form = new formidable.IncomingForm({ uploadDir: testInfo.outputPath() }); - form.parse(req, function(err, fields, f) { - res.end(); - const files = f as Record; - fulfill(files.file1); + + test('should be able to reconnect to a browser', async ({ connect, startRemoteServer, server }) => { + const remoteServer = await startRemoteServer(kind); + { + const browser = await connect(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 connect(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 ({ connect, startRemoteServer }) => { + const remoteServer = await startRemoteServer(kind); + + const browser1 = await connect(remoteServer.wsEndpoint()); + expect(browser1.contexts().length).toBe(0); + await browser1.newContext(); + expect(browser1.contexts().length).toBe(1); + + const browser2 = await connect(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(); + expect(browser2.contexts().length).toBe(1); + const page2 = await browser2.newPage(); + expect(await page2.evaluate(() => 7 * 6)).toBe(42); // original browser should still work + + await browser2.close(); + }); + + test('should timeout in socket while connecting', async ({ connect, server }) => { + const e = await connect(`ws://localhost:${server.PORT}/ws-slow`, { + timeout: 1000, + }).catch(e => e); + expect(e.message).toContain('browserType.connect: Timeout 1000ms exceeded'); + }); + + test('should timeout in connect while connecting', async ({ connect, server }) => { + const e = await connect(`ws://localhost:${server.PORT}/ws`, { + timeout: 100, + }).catch(e => e); + expect(e.message).toContain('browserType.connect: Timeout 100ms exceeded'); + }); + + test('should send extra headers with connect request', async ({ connect, server }) => { + const [request] = await Promise.all([ + server.waitForWebSocketConnectionRequest(), + connect(`ws://localhost:${server.PORT}/ws`, { + headers: { + 'User-Agent': 'Playwright', + 'foo': 'bar', + }, + timeout: 100, + }).catch(() => {}) + ]); + expect(request.headers['user-agent']).toBe('Playwright'); + expect(request.headers['foo']).toBe('bar'); + }); + + test('should send default User-Agent and X-Playwright-Browser headers with connect request', async ({ connect, browserName, server }) => { + const [request] = await Promise.all([ + server.waitForWebSocketConnectionRequest(), + connect(`ws://localhost:${server.PORT}/ws`, { + headers: { + 'foo': 'bar', + }, + timeout: 100, + }).catch(() => {}) + ]); + expect(request.headers['user-agent']).toBe(getUserAgent()); + expect(request.headers['x-playwright-browser']).toBe(browserName); + expect(request.headers['foo']).toBe('bar'); + }); + + test('should support slowmo option', async ({ connect, startRemoteServer }) => { + const remoteServer = await startRemoteServer(kind); + + const browser1 = await connect(remoteServer.wsEndpoint(), { slowMo: 200 }); + const start = Date.now(); + await browser1.newContext(); + await browser1.close(); + expect(Date.now() - start).toBeGreaterThan(199); + }); + + test('disconnected event should be emitted when browser is closed or server is closed', async ({ connect, startRemoteServer }) => { + const remoteServer = await startRemoteServer(kind); + + const browser1 = await connect(remoteServer.wsEndpoint()); + await browser1.newPage(); + + const browser2 = await connect(remoteServer.wsEndpoint()); + await browser2.newPage(); + + 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 ({ connect, startRemoteServer }) => { + const remoteServer = await startRemoteServer(kind); + const browser = await connect(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 ({ connect, startRemoteServer, mode }) => { + test.skip(mode !== 'default'); + + const remoteServer = await startRemoteServer(kind); + const __testHookBeforeCreateBrowser = () => { throw new Error('Dummy'); }; + const error = await connect(remoteServer.wsEndpoint(), { __testHookBeforeCreateBrowser } as any).catch(e => e); + expect(error.message).toContain('Dummy'); + }); + + test('should set the browser connected state', async ({ connect, startRemoteServer }) => { + const remoteServer = await startRemoteServer(kind); + const remote = await connect(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 ({ connect, startRemoteServer }) => { + const remoteServer = await startRemoteServer(kind); + const browser = await connect(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 throw when calling waitForNavigation after disconnect', async ({ connect, startRemoteServer }) => { + const remoteServer = await startRemoteServer(kind); + const browser = await connect(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.waitForNavigation().catch(e => e); + expect(error.message).toContain('Navigation failed because page was closed'); + }); + + test('should reject navigation when browser closes', async ({ connect, startRemoteServer, server }) => { + const remoteServer = await startRemoteServer(kind); + server.setRoute('/one-style.css', () => {}); + const browser = await connect(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('has been closed'); + }); + + test('should reject waitForSelector when browser closes', async ({ connect, startRemoteServer, server }) => { + const remoteServer = await startRemoteServer(kind); + server.setRoute('/empty.html', () => {}); + const browser = await connect(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('has been closed'); + }); + + test('should emit close events on pages and contexts', async ({ connect, startRemoteServer }) => { + const remoteServer = await startRemoteServer(kind); + const browser = await connect(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 ({ connect, startRemoteServer, server }) => { + const remoteServer = await startRemoteServer(kind); + const browser = await connect(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, connect, startRemoteServer }) => { + const remoteServer = await startRemoteServer(kind); + + const mycss1 = 'mycss1-' + kind; + const mycss2 = 'mycss2-' + kind; + const mycss3 = 'mycss3-' + kind; + + 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 connect(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 connect(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 ({ connect, startRemoteServer }) => { + const remoteServer = await startRemoteServer(kind); + const browser = await connect(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 ({ connect, startRemoteServer }) => { + const remoteServer = await startRemoteServer(kind); + const browser = await connect(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 ({ connect, startRemoteServer }) => { + const remoteServer = await startRemoteServer(kind); + const browser = await connect(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 ({ connect, startRemoteServer }, testInfo) => { + const remoteServer = await startRemoteServer(kind); + const browser = await connect(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 connecting remotely. Use saveAs() to save a local copy.'); + }); + + test('should be able to connect 20 times to a single server without warnings', async ({ connect, startRemoteServer }) => { + const remoteServer = await startRemoteServer(kind); + + let warning = null; + const warningHandler = w => warning = w; + process.on('warning', warningHandler); + + const browsers = []; + for (let i = 0; i < 20; i++) + browsers.push(await connect(remoteServer.wsEndpoint())); + await Promise.all([browsers.map(browser => browser.close())]); + + process.off('warning', warningHandler); + expect(warning).toBe(null); + }); + + test('should save download', async ({ server, connect, 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(kind); + const browser = await connect(remoteServer.wsEndpoint()); + const page = await browser.newPage(); + 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 connecting remotely. Use saveAs() to save a local copy.'); + await browser.close(); + }); + + test('should error when saving download after deletion', async ({ server, connect, 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(kind); + const browser = await connect(remoteServer.wsEndpoint()); + const page = await browser.newPage(); + 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('Target page, context or browser has been closed'); + await browser.close(); + }); + + test('should properly disconnect when connection closes from the client side', async ({ connect, startRemoteServer, server }) => { + server.setRoute('/one-style.css', () => {}); + const remoteServer = await startRemoteServer(kind); + const browser = await connect(remoteServer.wsEndpoint()); + const page = await browser.newPage(); + const navigationPromise = page.goto(server.PREFIX + '/one-style.html', { timeout: 60000 }).catch(e => e); + const waitForNavigationPromise = page.waitForNavigation().catch(e => e); + + const disconnectedPromise = new Promise(f => browser.once('disconnected', f)); + // This closes the websocket. + (browser as any)._connection.close(); + await disconnectedPromise; + expect(browser.isConnected()).toBe(false); + + const navMessage = (await navigationPromise).message; + expect(navMessage).toContain('Connection closed'); + expect(navMessage).toContain('Closed by'); + expect(navMessage).toContain(__filename); + expect((await waitForNavigationPromise).message).toContain('Navigation failed because page was closed'); + expect((await page.goto(server.EMPTY_PAGE).catch(e => e)).message).toContain('has been closed'); + expect((await page.waitForNavigation().catch(e => e)).message).toContain('Navigation failed because page was closed'); + }); + + test('should be able to connect when the wsEndpont is passed as an option', async ({ browserType, startRemoteServer }) => { + const remoteServer = await startRemoteServer(kind); + const browser = await browserType.connect({ + wsEndpoint: remoteServer.wsEndpoint(), + headers: { + 'x-playwright-launch-options': JSON.stringify((browserType as any)._defaultLaunchOptions || {}), + }, + }); + const page = await browser.newPage(); + expect(await page.evaluate('1 + 2')).toBe(3); + await browser.close(); + }); + + test('should save har', async ({ connect, startRemoteServer, server }, testInfo) => { + const remoteServer = await startRemoteServer(kind); + const browser = await connect(remoteServer.wsEndpoint()); + const harPath = testInfo.outputPath('test.har'); + const context = await browser.newContext({ + recordHar: { + path: harPath, + } + }); + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + await context.close(); + await browser.close(); + + const log = JSON.parse(fs.readFileSync(harPath).toString())['log']; + expect(log.entries.length).toBe(1); + const entry = log.entries[0]; + expect(entry.pageref).toBe(log.pages[0].id); + expect(entry.request.url).toBe(server.EMPTY_PAGE); + }); + + test('should record trace with sources', async ({ connect, startRemoteServer, server, trace }, testInfo) => { + test.skip(trace === 'on'); + const remoteServer = await startRemoteServer(kind); + const browser = await connect(remoteServer.wsEndpoint()); + const context = await browser.newContext(); + const page = await context.newPage(); + + await context.tracing.start({ sources: true }); + await page.goto(server.EMPTY_PAGE); + await page.setContent(''); + await page.click('"Click"'); + await context.tracing.stop({ path: testInfo.outputPath('trace1.zip') }); + + await context.close(); + await browser.close(); + + const { resources } = await parseTrace(testInfo.outputPath('trace1.zip')); + const sourceNames = Array.from(resources.keys()).filter(k => k.endsWith('.txt')); + expect(sourceNames.length).toBe(1); + const sourceFile = resources.get(sourceNames[0]); + const thisFile = await fs.promises.readFile(__filename); + expect(sourceFile).toEqual(thisFile); + }); + + test('should fulfill with global fetch result', async ({ connect, startRemoteServer, playwright, server }) => { + const remoteServer = await startRemoteServer(kind); + const browser = await connect(remoteServer.wsEndpoint()); + const context = await browser.newContext(); + const page = await context.newPage(); + + await page.route('**/*', async route => { + const request = await playwright.request.newContext(); + const response = await request.get(server.PREFIX + '/simple.json'); + route.fulfill({ response }); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.status()).toBe(200); + expect(await response.json()).toEqual({ 'foo': 'bar' }); + }); + + test('should upload large file', async ({ connect, startRemoteServer, server, browserName, isMac }, testInfo) => { + test.skip(browserName === 'webkit' && isMac && parseInt(os.release(), 10) < 20, 'WebKit for macOS 10.15 is frozen and does not have corresponding protocol features.'); + test.slow(); + const remoteServer = await startRemoteServer(kind); + const browser = await connect(remoteServer.wsEndpoint()); + const context = await browser.newContext(); + const page = await context.newPage(); + + await page.goto(server.PREFIX + '/input/fileupload.html'); + const uploadFile = testInfo.outputPath('200MB.zip'); + const str = 'A'.repeat(4 * 1024); + const stream = fs.createWriteStream(uploadFile); + for (let i = 0; i < 50 * 1024; i++) { + await new Promise((fulfill, reject) => { + stream.write(str, err => { + if (err) + reject(err); + else + fulfill(); + }); + }); + } + await new Promise(f => stream.end(f)); + const input = page.locator('input[type="file"]'); + const events = await input.evaluateHandle(e => { + const events = []; + e.addEventListener('input', () => events.push('input')); + e.addEventListener('change', () => events.push('change')); + return events; + }); + await input.setInputFiles(uploadFile); + expect(await input.evaluate(e => (e as HTMLInputElement).files[0].name)).toBe('200MB.zip'); + expect(await events.evaluate(e => e)).toEqual(['input', 'change']); + const serverFilePromise = new Promise(fulfill => { + server.setRoute('/upload', async (req, res) => { + const form = new formidable.IncomingForm({ uploadDir: testInfo.outputPath() }); + form.parse(req, function(err, fields, f) { + res.end(); + const files = f as Record; + fulfill(files.file1); + }); + }); + }); + const [file1] = await Promise.all([ + serverFilePromise, + page.click('input[type=submit]') + ]); + expect(file1.originalFilename).toBe('200MB.zip'); + expect(file1.size).toBe(200 * 1024 * 1024); + await Promise.all([uploadFile, file1.filepath].map(fs.promises.unlink)); + }); + + test('should connect when launching', async ({ browserType, startRemoteServer }) => { + const remoteServer = await startRemoteServer(kind); + (browserType as any)._defaultConnectOptions = { + wsEndpoint: remoteServer.wsEndpoint() + }; + + const browser = await browserType.launch(); + + await Promise.all([ + new Promise(f => browser.on('disconnected', f)), + remoteServer.close(), + ]); + + (browserType as any)._defaultConnectOptions = undefined; + }); + + test('should connect over http', async ({ connect, startRemoteServer, mode }) => { + test.skip(mode !== 'default'); + const remoteServer = await startRemoteServer(kind); + + const url = new URL(remoteServer.wsEndpoint()); + const browser = await connect(`http://localhost:${url.port}`); + expect(browser.version()).toBeTruthy(); + await browser.close(); + }); + + test.describe('socks proxy', () => { + test.fixme(({ platform, browserName }) => browserName === 'webkit' && platform === 'win32'); + test.skip(({ mode }) => mode !== 'default'); + test.skip(kind === 'launchServer', 'This feature is not yet supported in launchServer'); + + test('should forward non-forwarded requests', async ({ server, startRemoteServer, connect }) => { + let reachedOriginalTarget = false; + server.setRoute('/foo.html', async (req, res) => { + reachedOriginalTarget = true; + res.end('original-target'); + }); + const remoteServer = await startRemoteServer(kind); + const browser = await connect(remoteServer.wsEndpoint(), { + headers: { 'x-playwright-proxy': '*' } + }); + const page = await browser.newPage(); + await page.goto(server.PREFIX + '/foo.html'); + expect(await page.content()).toContain('original-target'); + expect(reachedOriginalTarget).toBe(true); + }); + + test('should proxy localhost requests @smoke', async ({ startRemoteServer, server, browserName, connect, platform, dummyServerPort }, testInfo) => { + test.skip(browserName === 'webkit' && platform === 'darwin', 'no localhost proxying'); + + let reachedOriginalTarget = false; + server.setRoute('/foo.html', async (req, res) => { + reachedOriginalTarget = true; + res.end(''); + }); + const examplePort = 20_000 + testInfo.workerIndex * 3; + const remoteServer = await startRemoteServer(kind); + const browser = await connect(remoteServer.wsEndpoint(), { + headers: { 'x-playwright-proxy': '*' } + }, dummyServerPort); + const page = await browser.newPage(); + await page.goto(`http://127.0.0.1:${examplePort}/foo.html`); + expect(await page.content()).toContain('from-dummy-server'); + expect(reachedOriginalTarget).toBe(false); + }); + + test('should proxy localhost requests from fetch api', async ({ startRemoteServer, server, browserName, connect, channel, platform, dummyServerPort }, workerInfo) => { + test.skip(browserName === 'webkit' && platform === 'darwin', 'no localhost proxying'); + + let reachedOriginalTarget = false; + server.setRoute('/foo.html', async (req, res) => { + reachedOriginalTarget = true; + res.end(''); + }); + const examplePort = 20_000 + workerInfo.workerIndex * 3; + const remoteServer = await startRemoteServer(kind); + const browser = await connect(remoteServer.wsEndpoint(), { + headers: { 'x-playwright-proxy': '*' } + }, dummyServerPort); + const page = await browser.newPage(); + const response = await page.request.get(`http://127.0.0.1:${examplePort}/foo.html`); + expect(response.status()).toBe(200); + expect(await response.text()).toContain('from-dummy-server'); + expect(reachedOriginalTarget).toBe(false); + }); + + test('should proxy local.playwright requests', async ({ connect, server, dummyServerPort, startRemoteServer }, workerInfo) => { + let reachedOriginalTarget = false; + server.setRoute('/foo.html', async (req, res) => { + reachedOriginalTarget = true; + res.end(''); + }); + const examplePort = 20_000 + workerInfo.workerIndex * 3; + const remoteServer = await startRemoteServer(kind); + const browser = await connect(remoteServer.wsEndpoint(), { + headers: { 'x-playwright-proxy': '*' } + }, dummyServerPort); + const page = await browser.newPage(); + await page.goto(`http://local.playwright:${examplePort}/foo.html`); + expect(await page.content()).toContain('from-dummy-server'); + expect(reachedOriginalTarget).toBe(false); + }); + + test('should lead to the error page for forwarded requests when the connection is refused', async ({ connect, startRemoteServer, browserName }, workerInfo) => { + const examplePort = 20_000 + workerInfo.workerIndex * 3; + const remoteServer = await startRemoteServer(kind); + const browser = await connect(remoteServer.wsEndpoint(), { + headers: { 'x-playwright-proxy': '*' } + }); + const page = await browser.newPage(); + const error = await page.goto(`http://127.0.0.1:${examplePort}`).catch(e => e); + if (browserName === 'chromium') + expect(error.message).toContain('net::ERR_SOCKS_CONNECTION_FAILED at http://127.0.0.1:20'); + else if (browserName === 'webkit') + expect(error.message).toBeTruthy(); + else if (browserName === 'firefox') + expect(error.message.includes('NS_ERROR_NET_RESET') || error.message.includes('NS_ERROR_CONNECTION_REFUSED')).toBe(true); + }); + + test('should proxy based on the pattern', async ({ connect, startRemoteServer, server, browserName, platform, dummyServerPort }, workerInfo) => { + test.skip(browserName === 'webkit' && platform === 'darwin', 'no localhost proxying'); + + let reachedOriginalTarget = false; + server.setRoute('/foo.html', async (req, res) => { + reachedOriginalTarget = true; + res.end('from-original-server'); + }); + const examplePort = 20_000 + workerInfo.workerIndex * 3; + const remoteServer = await startRemoteServer(kind); + const browser = await connect(remoteServer.wsEndpoint(), { + headers: { 'x-playwright-proxy': 'localhost' } + }, dummyServerPort); + const page = await browser.newPage(); + + // localhost should be proxied. + await page.goto(`http://localhost:${examplePort}/foo.html`); + expect(await page.content()).toContain('from-dummy-server'); + expect(reachedOriginalTarget).toBe(false); + + // 127.0.0.1 should be served directly. + await page.goto(`http://127.0.0.1:${server.PORT}/foo.html`); + expect(await page.content()).toContain('from-original-server'); + expect(reachedOriginalTarget).toBe(true); + + // Random domain should be served directly and fail. + let failed = false; + await page.goto(`http://does-not-exist-bad-domain.oh-no-should-not-work`).catch(e => { + failed = true; + }); + expect(failed).toBe(true); }); }); }); - const [file1] = await Promise.all([ - serverFilePromise, - page.click('input[type=submit]') - ]); - expect(file1.originalFilename).toBe('200MB.zip'); - expect(file1.size).toBe(200 * 1024 * 1024); - await Promise.all([uploadFile, file1.filepath].map(fs.promises.unlink)); -}); - -test('should connect when launching', async ({ browserType, startRemoteServer, httpsServer, mode }) => { - const remoteServer = await startRemoteServer(); - (browserType as any)._defaultConnectOptions = { - wsEndpoint: remoteServer.wsEndpoint() - }; - - const browser = await browserType.launch(); - - await Promise.all([ - new Promise(f => browser.on('disconnected', f)), - remoteServer.close(), - ]); - - (browserType as any)._defaultConnectOptions = undefined; -}); - -test('should connect over http', async ({ browserType, startRemoteServer, mode }) => { - test.skip(mode !== 'default'); - const remoteServer = await startRemoteServer(); - - const url = new URL(remoteServer.wsEndpoint()); - const browser = await browserType.connect(`http://localhost:${url.port}`); - expect(browser.version()).toBeTruthy(); - await browser.close(); +} + +test.describe('launchServer only', () => { + test('should work with cluster', async ({ connect, startRemoteServer }) => { + const remoteServer = await startRemoteServer('launchServer', { inCluster: true }); + const browser = await connect(remoteServer.wsEndpoint()); + const page = await browser.newPage(); + expect(await page.evaluate('1 + 2')).toBe(3); + }); + + test('should properly disconnect when connection closes from the server side', async ({ connect, startRemoteServer, server, platform }) => { + test.skip(platform === 'win32', 'Cannot send signals'); + + server.setRoute('/one-style.css', () => {}); + const remoteServer = await startRemoteServer('launchServer', { disconnectOnSIGHUP: true }); + const browser = await connect(remoteServer.wsEndpoint()); + const page = await browser.newPage(); + const navigationPromise = page.goto(server.PREFIX + '/one-style.html', { timeout: 60000 }).catch(e => e); + const waitForNavigationPromise = page.waitForNavigation().catch(e => e); + + const disconnectedPromise = new Promise(f => browser.once('disconnected', f)); + // This closes the websocket server. + process.kill(remoteServer.child().pid, 'SIGHUP'); + await disconnectedPromise; + expect(browser.isConnected()).toBe(false); + + expect((await navigationPromise).message).toContain('has been closed'); + expect((await waitForNavigationPromise).message).toContain('Navigation failed because page was closed'); + expect((await page.goto(server.EMPTY_PAGE).catch(e => e)).message).toContain('has been closed'); + expect((await page.waitForNavigation().catch(e => e)).message).toContain('Navigation failed because page was closed'); + }); }); diff --git a/tests/library/port-forwarding-server.spec.ts b/tests/library/port-forwarding-server.spec.ts deleted file mode 100644 index 6950f04344..0000000000 --- a/tests/library/port-forwarding-server.spec.ts +++ /dev/null @@ -1,202 +0,0 @@ -/** - * 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 childProcess from 'child_process'; -import http from 'http'; -import path from 'path'; -import type net from 'net'; - -import { contextTest, expect } from '../config/browserTest'; -import type { Page, Browser } from 'playwright-core'; - -class OutOfProcessPlaywrightServer { - private _driverProcess: childProcess.ChildProcess; - private _receivedPortPromise: Promise; - - constructor(port: number, proxyPort: number) { - this._driverProcess = childProcess.fork(path.join(__dirname, '..', '..', 'packages', 'playwright-core', 'lib', 'cli', 'cli.js'), ['run-server', '--port', port.toString()], { - stdio: 'pipe', - detached: true, - env: { - ...process.env - } - }); - this._driverProcess.unref(); - this._receivedPortPromise = new Promise((resolve, reject) => { - this._driverProcess.stdout.on('data', (data: Buffer) => { - const prefix = 'Listening on '; - const line = data.toString(); - if (line.startsWith(prefix)) - resolve(line.substr(prefix.length)); - }); - this._driverProcess.stderr.on('data', (data: Buffer) => { - console.log(data.toString()); - }); - this._driverProcess.on('exit', () => reject()); - }); - } - async kill() { - const waitForExit = new Promise(resolve => this._driverProcess.on('exit', () => resolve())); - this._driverProcess.kill('SIGKILL'); - await waitForExit; - } - public async wsEndpoint(): Promise { - return await this._receivedPortPromise; - } -} - -const it = contextTest.extend<{ pageFactory: (redirectPortForTest?: number, pattern?: string) => Promise }>({ - pageFactory: async ({ browserType, browserName, channel }, run, testInfo) => { - const playwrightServers: OutOfProcessPlaywrightServer[] = []; - const browsers: Browser[] = []; - await run(async (redirectPortForTest?: number, pattern = '*'): Promise => { - const server = new OutOfProcessPlaywrightServer(0, 3200 + testInfo.workerIndex); - playwrightServers.push(server); - const browser = await browserType.connect({ - wsEndpoint: await server.wsEndpoint() + `?proxy=${pattern}&browser=` + browserName, - headers: { 'x-playwright-launch-options': JSON.stringify({ channel }) }, - __testHookRedirectPortForwarding: redirectPortForTest, - } as any); - browsers.push(browser); - return await browser.newPage(); - }); - for (const playwrightServer of playwrightServers) - await playwrightServer.kill(); - await Promise.all(browsers.map(async browser => { - if (browser.isConnected()) - await new Promise(f => browser.once('disconnected', f)); - })); - }, -}); - -it.fixme(({ platform, browserName }) => browserName === 'webkit' && platform === 'win32'); -it.skip(({ mode }) => mode !== 'default'); - -async function startTestServer() { - const server = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => { - res.end('from-retargeted-server'); - }); - await new Promise(resolve => server.listen(0, resolve)); - return { - testServerPort: (server.address() as net.AddressInfo).port, - stopTestServer: () => server.close() - }; -} - -it('should forward non-forwarded requests', async ({ pageFactory, server }) => { - let reachedOriginalTarget = false; - server.setRoute('/foo.html', async (req, res) => { - reachedOriginalTarget = true; - res.end('original-target'); - }); - const page = await pageFactory(); - await page.goto(server.PREFIX + '/foo.html'); - expect(await page.content()).toContain('original-target'); - expect(reachedOriginalTarget).toBe(true); -}); - -it('should proxy localhost requests @smoke', async ({ pageFactory, server, browserName, platform }, workerInfo) => { - it.skip(browserName === 'webkit' && platform === 'darwin'); - const { testServerPort, stopTestServer } = await startTestServer(); - let reachedOriginalTarget = false; - server.setRoute('/foo.html', async (req, res) => { - reachedOriginalTarget = true; - res.end(''); - }); - const examplePort = 20_000 + workerInfo.workerIndex * 3; - const page = await pageFactory(testServerPort); - await page.goto(`http://127.0.0.1:${examplePort}/foo.html`); - expect(await page.content()).toContain('from-retargeted-server'); - expect(reachedOriginalTarget).toBe(false); - stopTestServer(); -}); - -it('should proxy localhost requests from fetch api', async ({ pageFactory, server, browserName, platform }, workerInfo) => { - it.skip(browserName === 'webkit' && platform === 'darwin'); - - const { testServerPort, stopTestServer } = await startTestServer(); - let reachedOriginalTarget = false; - server.setRoute('/foo.html', async (req, res) => { - reachedOriginalTarget = true; - res.end(''); - }); - const examplePort = 20_000 + workerInfo.workerIndex * 3; - const page = await pageFactory(testServerPort); - const response = await page.request.get(`http://127.0.0.1:${examplePort}/foo.html`); - expect(response.status()).toBe(200); - expect(await response.text()).toContain('from-retargeted-server'); - expect(reachedOriginalTarget).toBe(false); - stopTestServer(); -}); - -it('should proxy local.playwright requests', async ({ pageFactory, server, browserName }, workerInfo) => { - const { testServerPort, stopTestServer } = await startTestServer(); - let reachedOriginalTarget = false; - server.setRoute('/foo.html', async (req, res) => { - reachedOriginalTarget = true; - res.end(''); - }); - const examplePort = 20_000 + workerInfo.workerIndex * 3; - const page = await pageFactory(testServerPort); - await page.goto(`http://local.playwright:${examplePort}/foo.html`); - expect(await page.content()).toContain('from-retargeted-server'); - expect(reachedOriginalTarget).toBe(false); - stopTestServer(); -}); - -it('should lead to the error page for forwarded requests when the connection is refused', async ({ pageFactory, browserName }, workerInfo) => { - const examplePort = 20_000 + workerInfo.workerIndex * 3; - const page = await pageFactory(); - const error = await page.goto(`http://127.0.0.1:${examplePort}`).catch(e => e); - if (browserName === 'chromium') - expect(error.message).toContain('net::ERR_SOCKS_CONNECTION_FAILED at http://127.0.0.1:20'); - else if (browserName === 'webkit') - expect(error.message).toBeTruthy(); - else if (browserName === 'firefox') - expect(error.message.includes('NS_ERROR_NET_RESET') || error.message.includes('NS_ERROR_CONNECTION_REFUSED')).toBe(true); -}); - -it('should proxy based on the pattern', async ({ pageFactory, server, browserName, platform }, workerInfo) => { - it.skip(browserName === 'webkit' && platform === 'darwin'); - - const { testServerPort, stopTestServer } = await startTestServer(); - let reachedOriginalTarget = false; - server.setRoute('/foo.html', async (req, res) => { - reachedOriginalTarget = true; - res.end('from-original-server'); - }); - const examplePort = 20_000 + workerInfo.workerIndex * 3; - const page = await pageFactory(testServerPort, 'localhost'); - - // localhost should be proxied. - await page.goto(`http://localhost:${examplePort}/foo.html`); - expect(await page.content()).toContain('from-retargeted-server'); - expect(reachedOriginalTarget).toBe(false); - - // 127.0.0.1 should be served directly. - await page.goto(`http://127.0.0.1:${server.PORT}/foo.html`); - expect(await page.content()).toContain('from-original-server'); - expect(reachedOriginalTarget).toBe(true); - - // Random domain should be served directly and fail. - let failed = false; - await page.goto(`http://does-not-exist-bad-domain.oh-no-should-not-work`).catch(e => { - failed = true; - }); - expect(failed).toBe(true); - - stopTestServer(); -}); diff --git a/tests/library/signals.spec.ts b/tests/library/signals.spec.ts index 62294915fd..02a1a8b83d 100644 --- a/tests/library/signals.spec.ts +++ b/tests/library/signals.spec.ts @@ -22,7 +22,7 @@ import fs from 'fs'; test.slow(); test('should close the browser when the node process closes', async ({ startRemoteServer, isWindows, server }) => { - const remoteServer = await startRemoteServer({ url: server.EMPTY_PAGE }); + const remoteServer = await startRemoteServer('launchServer', { url: server.EMPTY_PAGE }); try { if (isWindows) execSync(`taskkill /pid ${remoteServer.child().pid} /T /F`, { stdio: 'ignore' }); @@ -43,7 +43,7 @@ test('should close the browser when the node process closes', async ({ startRemo test('should remove temp dir on process.exit', async ({ startRemoteServer, server }, testInfo) => { const file = testInfo.outputPath('exit.file'); - const remoteServer = await startRemoteServer({ url: server.EMPTY_PAGE, exitOnFile: file }); + const remoteServer = await startRemoteServer('launchServer', { url: server.EMPTY_PAGE, exitOnFile: file }); const tempDir = await remoteServer.out('tempDir'); const before = fs.existsSync(tempDir); fs.writeFileSync(file, 'data', 'utf-8'); @@ -59,7 +59,7 @@ test.describe('signals', () => { test('should report browser close signal', async ({ startRemoteServer, server, headless }) => { test.skip(!headless, 'Wrong exit code in headed'); - const remoteServer = await startRemoteServer({ url: server.EMPTY_PAGE }); + const remoteServer = await startRemoteServer('launchServer', { url: server.EMPTY_PAGE }); const pid = await remoteServer.out('pid'); process.kill(-pid, 'SIGTERM'); expect(await remoteServer.out('exitCode')).toBe('null'); @@ -69,7 +69,7 @@ test.describe('signals', () => { }); test('should report browser close signal 2', async ({ startRemoteServer, server }) => { - const remoteServer = await startRemoteServer({ url: server.EMPTY_PAGE }); + const remoteServer = await startRemoteServer('launchServer', { url: server.EMPTY_PAGE }); const pid = await remoteServer.out('pid'); process.kill(-pid, 'SIGKILL'); expect(await remoteServer.out('exitCode')).toBe('null'); @@ -79,7 +79,7 @@ test.describe('signals', () => { }); test('should close the browser on SIGINT', async ({ startRemoteServer, server }) => { - const remoteServer = await startRemoteServer({ url: server.EMPTY_PAGE }); + const remoteServer = await startRemoteServer('launchServer', { url: server.EMPTY_PAGE }); process.kill(remoteServer.child().pid, 'SIGINT'); expect(await remoteServer.out('exitCode')).toBe('0'); expect(await remoteServer.out('signal')).toBe('null'); @@ -87,7 +87,7 @@ test.describe('signals', () => { }); test('should close the browser on SIGTERM', async ({ startRemoteServer, server }) => { - const remoteServer = await startRemoteServer({ url: server.EMPTY_PAGE }); + const remoteServer = await startRemoteServer('launchServer', { url: server.EMPTY_PAGE }); process.kill(remoteServer.child().pid, 'SIGTERM'); expect(await remoteServer.out('exitCode')).toBe('0'); expect(await remoteServer.out('signal')).toBe('null'); @@ -95,7 +95,7 @@ test.describe('signals', () => { }); test('should close the browser on SIGHUP', async ({ startRemoteServer, server }) => { - const remoteServer = await startRemoteServer({ url: server.EMPTY_PAGE }); + const remoteServer = await startRemoteServer('launchServer', { url: server.EMPTY_PAGE }); process.kill(remoteServer.child().pid, 'SIGHUP'); expect(await remoteServer.out('exitCode')).toBe('0'); expect(await remoteServer.out('signal')).toBe('null'); @@ -103,7 +103,7 @@ test.describe('signals', () => { }); test('should kill the browser on double SIGINT and remove temp dir', async ({ startRemoteServer, server }) => { - const remoteServer = await startRemoteServer({ stallOnClose: true, url: server.EMPTY_PAGE }); + const remoteServer = await startRemoteServer('launchServer', { stallOnClose: true, url: server.EMPTY_PAGE }); const tempDir = await remoteServer.out('tempDir'); const before = fs.existsSync(tempDir); process.kill(remoteServer.child().pid, 'SIGINT'); @@ -118,7 +118,7 @@ test.describe('signals', () => { }); test('should kill the browser on SIGINT + SIGTERM', async ({ startRemoteServer, server }) => { - const remoteServer = await startRemoteServer({ stallOnClose: true, url: server.EMPTY_PAGE }); + const remoteServer = await startRemoteServer('launchServer', { stallOnClose: true, url: server.EMPTY_PAGE }); process.kill(remoteServer.child().pid, 'SIGINT'); await remoteServer.out('stalled'); process.kill(remoteServer.child().pid, 'SIGTERM'); @@ -128,7 +128,7 @@ test.describe('signals', () => { }); test('should kill the browser on SIGTERM + SIGINT', async ({ startRemoteServer, server }) => { - const remoteServer = await startRemoteServer({ stallOnClose: true, url: server.EMPTY_PAGE }); + const remoteServer = await startRemoteServer('launchServer', { stallOnClose: true, url: server.EMPTY_PAGE }); process.kill(remoteServer.child().pid, 'SIGTERM'); await remoteServer.out('stalled'); process.kill(remoteServer.child().pid, 'SIGINT'); diff --git a/utils/testserver/index.ts b/utils/testserver/index.ts index 5fac659ffc..6f7a126291 100644 --- a/utils/testserver/index.ts +++ b/utils/testserver/index.ts @@ -281,7 +281,7 @@ export class TestServer { } } - onceWebSocketConnection(handler) { + onceWebSocketConnection(handler: (socket: ws.WebSocket, request: http.IncomingMessage) => void) { this._wsServer.once('connection', handler); }