From 496db8925481ee5187f5f87d1007c26a0cbfbe31 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 18 Jun 2026 12:17:26 +0100 Subject: [PATCH] feat(client): build @playwright/client as a browser bundle @playwright/client was never built. Build it as an ESM bundle and make the client code isomorphic so it runs in the browser: - Remove the runtime Platform abstraction; client code uses node builtins directly, stubbed for the browser at build time (esbuild aliases plus a `process` inject), with `colors/safe` polyfilled to no-op. - Reuse the existing @utils debug/zones helpers instead of forking them. - Add an end-to-end test that loads the bundle in a page and drives the same browser back via browser.bind(). --- packages/isomorphic/platform.ts | 132 -------------- packages/isomorphic/stackTrace.ts | 30 ++++ packages/playwright-client/package.json | 10 +- packages/playwright-client/src/index.ts | 3 +- .../src/nodeStubs/async_hooks.ts | 27 +++ .../playwright-client/src/nodeStubs/colors.ts | 21 +++ .../{index.mjs => src/nodeStubs/events.ts} | 6 +- .../playwright-client/src/nodeStubs/fs.ts | 25 +++ .../{index.js => src/nodeStubs/inspector.ts} | 6 +- .../playwright-client/src/nodeStubs/path.ts | 29 +++ .../src/nodeStubs/processShim.ts | 22 +++ .../playwright-client/src/nodeStubs/stream.ts | 24 +++ .../playwright-client/src/nodeStubs/util.ts | 21 +++ packages/playwright-client/src/webPlatform.ts | 48 ----- packages/playwright-core/src/DEPS.list | 1 + packages/playwright-core/src/client/DEPS.list | 4 + .../playwright-core/src/client/android.ts | 21 +-- .../playwright-core/src/client/artifact.ts | 6 +- .../playwright-core/src/client/browser.ts | 8 +- .../src/client/browserContext.ts | 38 ++-- .../playwright-core/src/client/browserType.ts | 14 +- .../src/client/channelOwner.ts | 30 ++-- .../src/client/clientHelper.ts | 8 +- .../src/client/clientStackTrace.ts | 14 +- .../playwright-core/src/client/connect.ts | 2 +- .../playwright-core/src/client/connection.ts | 36 ++-- .../src/client/consoleMessage.ts | 9 +- .../playwright-core/src/client/electron.ts | 10 +- .../src/client/elementHandle.ts | 33 ++-- .../src/client/eventEmitter.ts | 13 +- packages/playwright-core/src/client/fetch.ts | 30 ++-- .../playwright-core/src/client/fileUtils.ts | 7 +- packages/playwright-core/src/client/frame.ts | 16 +- .../playwright-core/src/client/harRouter.ts | 6 +- .../playwright-core/src/client/locator.ts | 6 +- .../playwright-core/src/client/network.ts | 11 +- packages/playwright-core/src/client/page.ts | 25 +-- .../playwright-core/src/client/playwright.ts | 2 +- .../playwright-core/src/client/selectors.ts | 8 +- packages/playwright-core/src/client/stream.ts | 25 ++- .../src/client/timeoutSettings.ts | 13 +- packages/playwright-core/src/client/video.ts | 5 +- packages/playwright-core/src/client/waiter.ts | 7 +- packages/playwright-core/src/client/worker.ts | 4 +- .../src/client/writableStream.ts | 25 ++- packages/playwright-core/src/inprocess.ts | 6 +- packages/playwright-core/src/outofprocess.ts | 6 +- packages/playwright/src/index.ts | 2 +- packages/utils/index.ts | 1 - packages/utils/nodePlatform.ts | 170 ------------------ tests/library/events/utils.ts | 6 +- tests/library/playwright-client.spec.ts | 55 ++++++ utils/build/build.js | 34 ++++ 53 files changed, 571 insertions(+), 550 deletions(-) delete mode 100644 packages/isomorphic/platform.ts create mode 100644 packages/playwright-client/src/nodeStubs/async_hooks.ts create mode 100644 packages/playwright-client/src/nodeStubs/colors.ts rename packages/playwright-client/{index.mjs => src/nodeStubs/events.ts} (85%) create mode 100644 packages/playwright-client/src/nodeStubs/fs.ts rename packages/playwright-client/{index.js => src/nodeStubs/inspector.ts} (86%) create mode 100644 packages/playwright-client/src/nodeStubs/path.ts create mode 100644 packages/playwright-client/src/nodeStubs/processShim.ts create mode 100644 packages/playwright-client/src/nodeStubs/stream.ts create mode 100644 packages/playwright-client/src/nodeStubs/util.ts delete mode 100644 packages/playwright-client/src/webPlatform.ts delete mode 100644 packages/utils/nodePlatform.ts create mode 100644 tests/library/playwright-client.spec.ts diff --git a/packages/isomorphic/platform.ts b/packages/isomorphic/platform.ts deleted file mode 100644 index 497b50cff75fd..0000000000000 --- a/packages/isomorphic/platform.ts +++ /dev/null @@ -1,132 +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 { webColors } from './colors'; - -import type * as fs from 'fs'; -import type * as path from 'path'; -import type { Readable, Writable } from 'stream'; -import type { Colors } from '@isomorphic/colors'; - -export type Zone = { - push(data: unknown): Zone; - pop(): Zone; - run(func: () => R): R; - data(): T | undefined; -}; - -export type StreamChannel = { - read(params: { size?: number }): Promise<{ binary: Buffer }>; - close(params?: {}): Promise; -}; - -export type WritableStreamChannel = { - write(params: { binary: Buffer }): Promise; - close(params?: {}): Promise; -}; - -const noopZone: Zone = { - push: () => noopZone, - pop: () => noopZone, - run: func => func(), - data: () => undefined, -}; - -export type Platform = { - name: 'node' | 'web' | 'empty'; - - boxedStackPrefixes: () => string[]; - calculateSha1: (text: string) => Promise; - colors: Colors; - coreDir?: string; - createGuid: () => string; - defaultMaxListeners: () => number; - env: Record; - fs: () => typeof fs; - inspectCustom: symbol | undefined; - isDebugMode: () => boolean; - isJSDebuggerAttached: () => boolean; - isLogEnabled: (name: 'api' | 'channel') => boolean; - isUnderTest: () => boolean, - log: (name: 'api' | 'channel', message: string | Error | object) => void; - path: () => typeof path; - pathSeparator: string; - showInternalStackFrames: () => boolean, - streamFile: (path: string, writable: Writable) => Promise, - streamReadable: (channel: StreamChannel) => Readable, - streamWritable: (channel: WritableStreamChannel) => Writable, - zones: { empty: Zone, current: () => Zone; }; -}; - -export const emptyPlatform: Platform = { - name: 'empty', - - boxedStackPrefixes: () => [], - - calculateSha1: async () => { - throw new Error('Not implemented'); - }, - - colors: webColors, - - createGuid: () => { - throw new Error('Not implemented'); - }, - - defaultMaxListeners: () => 10, - - env: {}, - - fs: () => { - throw new Error('Not implemented'); - }, - - inspectCustom: undefined, - - isDebugMode: () => false, - - isJSDebuggerAttached: () => false, - - isLogEnabled(name: 'api' | 'channel') { - return false; - }, - - isUnderTest: () => false, - - log(name: 'api' | 'channel', message: string | Error | object) { }, - - path: () => { - throw new Error('Function not implemented.'); - }, - - pathSeparator: '/', - - showInternalStackFrames: () => false, - - streamFile(path: string, writable: Writable): Promise { - throw new Error('Streams are not available'); - }, - - streamReadable: (channel: StreamChannel) => { - throw new Error('Streams are not available'); - }, - - streamWritable: (channel: WritableStreamChannel) => { - throw new Error('Streams are not available'); - }, - - zones: { empty: noopZone, current: () => noopZone }, -}; diff --git a/packages/isomorphic/stackTrace.ts b/packages/isomorphic/stackTrace.ts index dca83848e9414..f6c906e0613bf 100644 --- a/packages/isomorphic/stackTrace.ts +++ b/packages/isomorphic/stackTrace.ts @@ -199,3 +199,33 @@ function fileURLToPath(fileUrl: string, pathSeparator: string): string { return path.replace(/\//g, pathSeparator); } + +let _coreDir: string | undefined; +let _boxedStackPrefixes: string[] = []; +let _showInternalStackFrames = false; + +export function setCoreDir(dir: string | undefined) { + _coreDir = dir; +} + +export function coreDir(): string | undefined { + return _coreDir; +} + +export function setBoxedStackPrefixes(prefixes: string[]) { + _boxedStackPrefixes = prefixes; +} + +export function boxedStackPrefixes(): string[] { + if (_showInternalStackFrames) + return []; + return _coreDir ? [_coreDir, ..._boxedStackPrefixes] : _boxedStackPrefixes.slice(); +} + +export function setShowInternalStackFrames(value: boolean) { + _showInternalStackFrames = value; +} + +export function showInternalStackFrames(): boolean { + return _showInternalStackFrames; +} diff --git a/packages/playwright-client/package.json b/packages/playwright-client/package.json index 33597674cf228..7f273569b41ab 100644 --- a/packages/playwright-client/package.json +++ b/packages/playwright-client/package.json @@ -18,17 +18,11 @@ "exports": { ".": { "types": "./index.d.ts", - "import": "./index.mjs", - "require": "./index.js", - "default": "./index.js" + "import": "./lib/index.mjs", + "default": "./lib/index.mjs" }, "./package.json": "./package.json" }, - "scripts": { - "esbuild": "node build.js", - "build": "npm run esbuild", - "watch": "npm run esbuild -- --watch" - }, "dependencies": { "playwright-core": "1.62.0-next" } diff --git a/packages/playwright-client/src/index.ts b/packages/playwright-client/src/index.ts index cacbe8dbdb8d9..97c66cade21d2 100644 --- a/packages/playwright-client/src/index.ts +++ b/packages/playwright-client/src/index.ts @@ -15,7 +15,6 @@ */ import { Connection } from '../../playwright-core/src/client/connection'; -import { webPlatform } from './webPlatform'; import type { Browser } from '../../playwright-core/src/client/browser'; @@ -30,7 +29,7 @@ export async function connect(wsEndpoint: string, browserName: string, options: ws.addEventListener('error', r); }); - const connection = new Connection(webPlatform); + const connection = new Connection(); connection.onmessage = message => ws.send(JSON.stringify(message)); ws.addEventListener('message', message => connection.dispatch(JSON.parse(message.data))); ws.addEventListener('close', () => connection.close()); diff --git a/packages/playwright-client/src/nodeStubs/async_hooks.ts b/packages/playwright-client/src/nodeStubs/async_hooks.ts new file mode 100644 index 0000000000000..afeb52a21158d --- /dev/null +++ b/packages/playwright-client/src/nodeStubs/async_hooks.ts @@ -0,0 +1,27 @@ +/** + * 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. + */ + +export class AsyncLocalStorage { + run(_store: T, callback: () => R): R { + return callback(); + } + + getStore(): T | undefined { + return undefined; + } +} + +export default { AsyncLocalStorage }; diff --git a/packages/playwright-client/src/nodeStubs/colors.ts b/packages/playwright-client/src/nodeStubs/colors.ts new file mode 100644 index 0000000000000..997103271286a --- /dev/null +++ b/packages/playwright-client/src/nodeStubs/colors.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Browser polyfill for `colors/safe`. + +import { noColors } from '@isomorphic/colors'; + +export default noColors; diff --git a/packages/playwright-client/index.mjs b/packages/playwright-client/src/nodeStubs/events.ts similarity index 85% rename from packages/playwright-client/index.mjs rename to packages/playwright-client/src/nodeStubs/events.ts index 0332e501571ce..5e6788f38fbca 100644 --- a/packages/playwright-client/index.mjs +++ b/packages/playwright-client/src/nodeStubs/events.ts @@ -14,4 +14,8 @@ * limitations under the License. */ -export { connect } from './index.js'; +export class EventEmitter { + static defaultMaxListeners = 10; +} + +export default { EventEmitter }; diff --git a/packages/playwright-client/src/nodeStubs/fs.ts b/packages/playwright-client/src/nodeStubs/fs.ts new file mode 100644 index 0000000000000..21846f043f748 --- /dev/null +++ b/packages/playwright-client/src/nodeStubs/fs.ts @@ -0,0 +1,25 @@ +/** + * 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. + */ + +function notAvailable(): never { + throw new Error('fs is not available in the browser'); +} + +export const promises: any = new Proxy({}, { get: () => notAvailable }); +export const createReadStream: any = notAvailable; +export const createWriteStream: any = notAvailable; + +export default { promises, createReadStream, createWriteStream }; diff --git a/packages/playwright-client/index.js b/packages/playwright-client/src/nodeStubs/inspector.ts similarity index 86% rename from packages/playwright-client/index.js rename to packages/playwright-client/src/nodeStubs/inspector.ts index 227eee0fde3b0..2294315ce3039 100644 --- a/packages/playwright-client/index.js +++ b/packages/playwright-client/src/nodeStubs/inspector.ts @@ -14,4 +14,8 @@ * limitations under the License. */ -module.exports = require('./lib/index'); +export function url(): string | undefined { + return undefined; +} + +export default { url }; diff --git a/packages/playwright-client/src/nodeStubs/path.ts b/packages/playwright-client/src/nodeStubs/path.ts new file mode 100644 index 0000000000000..821003dacde57 --- /dev/null +++ b/packages/playwright-client/src/nodeStubs/path.ts @@ -0,0 +1,29 @@ +/** + * 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. + */ + +function notAvailable(): never { + throw new Error('path is not available in the browser'); +} + +export const sep = '/'; +export const dirname: any = notAvailable; +export const basename: any = notAvailable; +export const resolve: any = notAvailable; +export const join: any = notAvailable; +export const relative: any = notAvailable; +export const isAbsolute: any = notAvailable; + +export default { sep, dirname, basename, resolve, join, relative, isAbsolute }; diff --git a/packages/playwright-client/src/nodeStubs/processShim.ts b/packages/playwright-client/src/nodeStubs/processShim.ts new file mode 100644 index 0000000000000..38b99a18e2a6d --- /dev/null +++ b/packages/playwright-client/src/nodeStubs/processShim.ts @@ -0,0 +1,22 @@ +/** + * 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. + */ + +// Injected (esbuild `inject`) as the `process` global in the browser build. +export const process = { + env: {} as Record, + platform: 'browser', + argv: [] as string[], +}; diff --git a/packages/playwright-client/src/nodeStubs/stream.ts b/packages/playwright-client/src/nodeStubs/stream.ts new file mode 100644 index 0000000000000..094f7f8d3c5e5 --- /dev/null +++ b/packages/playwright-client/src/nodeStubs/stream.ts @@ -0,0 +1,24 @@ +/** + * 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. + */ + +export class Readable {} +export class Writable {} + +export const promises = { + pipeline: (): Promise => Promise.reject(new Error('stream is not available in the browser')), +}; + +export default { Readable, Writable, promises }; diff --git a/packages/playwright-client/src/nodeStubs/util.ts b/packages/playwright-client/src/nodeStubs/util.ts new file mode 100644 index 0000000000000..8ffa13dd41d81 --- /dev/null +++ b/packages/playwright-client/src/nodeStubs/util.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const inspect: any = Object.assign((value: unknown) => String(value), { + custom: Symbol.for('nodejs.util.inspect.custom'), +}); + +export default { inspect }; diff --git a/packages/playwright-client/src/webPlatform.ts b/packages/playwright-client/src/webPlatform.ts deleted file mode 100644 index 5cbcd6e85449b..0000000000000 --- a/packages/playwright-client/src/webPlatform.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* eslint-disable no-console */ -/** - * 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 { emptyPlatform } from '@isomorphic/platform'; - -import type { Platform } from '@isomorphic/platform'; - -export const webPlatform: Platform = { - ...emptyPlatform, - - name: 'web', - - boxedStackPrefixes: () => [], - - calculateSha1: async (text: string) => { - const bytes = new TextEncoder().encode(text); - const hashBuffer = await window.crypto.subtle.digest('SHA-1', bytes); - return Array.from(new Uint8Array(hashBuffer), b => b.toString(16).padStart(2, '0')).join(''); - }, - - createGuid: () => { - return Array.from(window.crypto.getRandomValues(new Uint8Array(16)), b => b.toString(16).padStart(2, '0')).join(''); - }, - - isLogEnabled(name: 'api' | 'channel') { - return true; - }, - - log(name: 'api' | 'channel', message: string | Error | object) { - console.debug(name, message); - }, - - showInternalStackFrames: () => true, -}; diff --git a/packages/playwright-core/src/DEPS.list b/packages/playwright-core/src/DEPS.list index d19a641885b93..a780ae6ddf06f 100644 --- a/packages/playwright-core/src/DEPS.list +++ b/packages/playwright-core/src/DEPS.list @@ -16,6 +16,7 @@ server/ [inprocess.ts] ** @utils/** +@isomorphic/** [outofprocess.ts] client/ diff --git a/packages/playwright-core/src/client/DEPS.list b/packages/playwright-core/src/client/DEPS.list index fc43e5b9b576c..636aa007ccea1 100644 --- a/packages/playwright-core/src/client/DEPS.list +++ b/packages/playwright-core/src/client/DEPS.list @@ -1,3 +1,7 @@ [*] ../protocol/ @isomorphic/** +@utils/debug.ts +@utils/debugLogger.ts +@utils/zones.ts +node_modules/colors/safe diff --git a/packages/playwright-core/src/client/android.ts b/packages/playwright-core/src/client/android.ts index 8286a92e1cc0f..3fa887bb5a568 100644 --- a/packages/playwright-core/src/client/android.ts +++ b/packages/playwright-core/src/client/android.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import fs from 'fs'; + import { isRegExp, isString } from '@isomorphic/rtti'; import { monotonicTime } from '@isomorphic/time'; import { raceAgainstDeadline } from '@isomorphic/timeoutRunner'; @@ -30,7 +32,6 @@ import type { Page } from './page'; import type * as types from './types'; import type * as api from '../../types/types'; import type { AndroidServerLauncherImpl } from '../androidServerImpl'; -import type { Platform } from '@isomorphic/platform'; import type * as channels from './channels'; import type { Playwright } from './playwright'; @@ -48,7 +49,7 @@ export class Android extends ChannelOwner implements ap constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.AndroidInitializer) { super(parent, type, guid, initializer); - this._timeoutSettings = new TimeoutSettings(this._platform); + this._timeoutSettings = new TimeoutSettings(); } setDefaultTimeout(timeout: number) { @@ -115,7 +116,7 @@ export class AndroidDevice extends ChannelOwner i super(parent, type, guid, initializer); this._android = parent as Android; this.input = new AndroidInput(this); - this._timeoutSettings = new TimeoutSettings(this._platform, (parent as Android)._timeoutSettings); + this._timeoutSettings = new TimeoutSettings((parent as Android)._timeoutSettings); this._channel.on('webViewAdded', ({ webView }) => this._onWebViewAdded(webView)); this._channel.on('webViewRemoved', ({ socketName }) => this._onWebViewRemoved(socketName)); this._channel.on('close', () => this._didClose()); @@ -216,7 +217,7 @@ export class AndroidDevice extends ChannelOwner i async screenshot(options: { path?: string } = {}): Promise { const { binary } = await this._channel.screenshot(); if (options.path) - await this._platform.fs().promises.writeFile(options.path, binary); + await fs.promises.writeFile(options.path, binary); return binary; } @@ -251,15 +252,15 @@ export class AndroidDevice extends ChannelOwner i } async installApk(file: string | Buffer, options?: { args: string[] }): Promise { - await this._channel.installApk({ file: await loadFile(this._platform, file), args: options && options.args }); + await this._channel.installApk({ file: await loadFile(file), args: options && options.args }); } async push(file: string | Buffer, path: string, options?: { mode: number }): Promise { - await this._channel.push({ file: await loadFile(this._platform, file), path, mode: options ? options.mode : undefined }); + await this._channel.push({ file: await loadFile(file), path, mode: options ? options.mode : undefined }); } async launchBrowser(options: types.BrowserContextOptions & { pkg?: string } = {}): Promise { - const contextOptions = await prepareBrowserContextParams(this._platform, options); + const contextOptions = await prepareBrowserContextParams(options); const result = await this._channel.launchBrowser(contextOptions); const context = BrowserContext.from(result.context); const selectors = this._android._playwright.selectors; @@ -310,9 +311,9 @@ export class AndroidSocket extends ChannelOwner i } } -async function loadFile(platform: Platform, file: string | Buffer): Promise { +async function loadFile(file: string | Buffer): Promise { if (isString(file)) - return await platform.fs().promises.readFile(file); + return await fs.promises.readFile(file); return file; } @@ -400,7 +401,7 @@ export class AndroidWebView extends EventEmitter implements api.AndroidWebView { private _pagePromise: Promise | undefined; constructor(device: AndroidDevice, data: channels.AndroidWebView) { - super(device._platform); + super(); this._device = device; this._data = data; } diff --git a/packages/playwright-core/src/client/artifact.ts b/packages/playwright-core/src/client/artifact.ts index 794d327037555..f8e8ae1a9ea0c 100644 --- a/packages/playwright-core/src/client/artifact.ts +++ b/packages/playwright-core/src/client/artifact.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import fs from 'fs'; + import { ChannelOwner } from './channelOwner'; import { Stream } from './stream'; import { mkdirIfNeeded } from './fileUtils'; @@ -40,9 +42,9 @@ export class Artifact extends ChannelOwner { const result = await this._channel.saveAsStream(); const stream = Stream.from(result.stream); - await mkdirIfNeeded(this._platform, path); + await mkdirIfNeeded(path); await new Promise((resolve, reject) => { - stream.stream().pipe(this._platform.fs().createWriteStream(path)) + stream.stream().pipe(fs.createWriteStream(path)) .on('finish' as any, resolve) .on('error' as any, reject); }); diff --git a/packages/playwright-core/src/client/browser.ts b/packages/playwright-core/src/client/browser.ts index d11adf305ede8..5f1dfc220374e 100644 --- a/packages/playwright-core/src/client/browser.ts +++ b/packages/playwright-core/src/client/browser.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import fs from 'fs'; + import { Artifact } from './artifact'; import { BrowserContext, prepareBrowserContextParams } from './browserContext'; import { CDPSession } from './cdpSession'; @@ -79,7 +81,7 @@ export class Browser extends ChannelOwner implements ap async _innerNewContext(userOptions: BrowserContextOptions = {}, forReuse: boolean): Promise { const options = this._browserType._playwright.selectors._withSelectorOptions(userOptions); await this._instrumentation.runBeforeCreateBrowserContext(options); - const contextOptions = await prepareBrowserContextParams(this._platform, options); + const contextOptions = await prepareBrowserContextParams(options); const response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions); const context = BrowserContext.from(response.context); if (forReuse) @@ -166,8 +168,8 @@ export class Browser extends ChannelOwner implements ap const buffer = await artifact.readIntoBuffer(); await artifact.delete(); if (this._path) { - await mkdirIfNeeded(this._platform, this._path); - await this._platform.fs().promises.writeFile(this._path, buffer); + await mkdirIfNeeded(this._path); + await fs.promises.writeFile(this._path, buffer); this._path = undefined; } return buffer; diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index e450deaeb8d25..375a6489664fc 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -15,6 +15,9 @@ * limitations under the License. */ +import fs from 'fs'; +import path from 'path'; + import { headersObjectToArray } from '@isomorphic/headers'; import { urlMatchesEqual } from '@isomorphic/urlMatch'; import { isRegExp, isString } from '@isomorphic/rtti'; @@ -47,7 +50,6 @@ import type { BrowserContextOptions, Headers, SetStorageState, StorageState, Tim import type * as structs from '../../types/structs'; import type * as api from '../../types/types'; import type { URLMatch } from '@isomorphic/urlMatch'; -import type { Platform } from '@isomorphic/platform'; import type * as channels from './channels'; import type * as actions from '@recorder/actions'; @@ -94,7 +96,7 @@ export class BrowserContext extends ChannelOwner constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.BrowserContextInitializer) { super(parent, type, guid, initializer); this._options = initializer.options; - this._timeoutSettings = new TimeoutSettings(this._platform); + this._timeoutSettings = new TimeoutSettings(); this.debugger = Debugger.from(initializer.debugger); this.tracing = Tracing.from(initializer.tracing); this.request = APIRequestContext.from(initializer.requestContext); @@ -116,7 +118,7 @@ export class BrowserContext extends ChannelOwner this._channel.on('console', event => { const worker = Worker.fromNullable(event.worker); const page = Page.fromNullable(event.page); - const consoleMessage = new ConsoleMessage(this._platform, event, page, worker); + const consoleMessage = new ConsoleMessage(event, page, worker); worker?.emit(Events.Worker.Console, consoleMessage); page?.emit(Events.Page.Console, consoleMessage); if (worker && this._serviceWorkers.has(worker)) { @@ -357,7 +359,7 @@ export class BrowserContext extends ChannelOwner } async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) { - const source = await evaluationScript(this._platform, script, arg); + const source = await evaluationScript(script, arg); return DisposableObject.from((await this._channel.addInitScript({ source })).disposable); } @@ -375,7 +377,7 @@ export class BrowserContext extends ChannelOwner } async route(url: URLMatch, handler: network.RouteHandlerCallback, options: { times?: number } = {}): Promise { - this._routes.unshift(new network.RouteHandler(this._platform, this._options.baseURL, url, handler, options.times)); + this._routes.unshift(new network.RouteHandler(this._options.baseURL, url, handler, options.times)); await this._updateInterceptionPatterns({ title: 'Route requests' }); return new DisposableStub(() => this.unroute(url, handler)); } @@ -462,14 +464,14 @@ export class BrowserContext extends ChannelOwner async storageState(options: { path?: string, indexedDB?: boolean } = {}): Promise { const state = await this._channel.storageState({ indexedDB: options.indexedDB }); if (options.path) { - await mkdirIfNeeded(this._platform, options.path); - await this._platform.fs().promises.writeFile(options.path, JSON.stringify(state, undefined, 2), 'utf8'); + await mkdirIfNeeded(options.path); + await fs.promises.writeFile(options.path, JSON.stringify(state, undefined, 2), 'utf8'); } return state; } async setStorageState(storageState: string | SetStorageState): Promise { - const state = await prepareStorageState(this._platform, storageState); + const state = await prepareStorageState(storageState); await this._channel.setStorageState({ storageState: state }); } @@ -532,18 +534,18 @@ export class BrowserContext extends ChannelOwner } -async function prepareStorageState(platform: Platform, storageState: string | SetStorageState): Promise> { +async function prepareStorageState(storageState: string | SetStorageState): Promise> { if (typeof storageState !== 'string') return storageState as any; try { - return JSON.parse(await platform.fs().promises.readFile(storageState, 'utf8')); + return JSON.parse(await fs.promises.readFile(storageState, 'utf8')); } catch (e) { rewriteErrorMessage(e, `Error reading storage state from ${storageState}:\n` + e.message); throw e; } } -export async function prepareBrowserContextParams(platform: Platform, options: BrowserContextOptions): Promise { +export async function prepareBrowserContextParams(options: BrowserContextOptions): Promise { if (options.extraHTTPHeaders) network.validateHeaders(options.extraHTTPHeaders); const contextParams: channels.BrowserNewContextParams = { @@ -551,17 +553,17 @@ export async function prepareBrowserContextParams(platform: Platform, options: B viewport: options.viewport === null ? undefined : options.viewport, noDefaultViewport: options.viewport === null, extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined, - storageState: options.storageState ? await prepareStorageState(platform, options.storageState) : undefined, + storageState: options.storageState ? await prepareStorageState(options.storageState) : undefined, serviceWorkers: options.serviceWorkers, colorScheme: options.colorScheme === null ? 'no-override' : options.colorScheme, reducedMotion: options.reducedMotion === null ? 'no-override' : options.reducedMotion, forcedColors: options.forcedColors === null ? 'no-override' : options.forcedColors, contrast: options.contrast === null ? 'no-override' : options.contrast, acceptDownloads: toAcceptDownloadsProtocol(options.acceptDownloads), - clientCertificates: await toClientCertificatesProtocol(platform, options.clientCertificates), + clientCertificates: await toClientCertificatesProtocol(options.clientCertificates), }; if (contextParams.recordVideo && contextParams.recordVideo.dir) - contextParams.recordVideo.dir = platform.path().resolve(contextParams.recordVideo.dir); + contextParams.recordVideo.dir = path.resolve(contextParams.recordVideo.dir); return contextParams; } @@ -573,15 +575,15 @@ function toAcceptDownloadsProtocol(acceptDownloads?: boolean) { return 'deny'; } -export async function toClientCertificatesProtocol(platform: Platform, certs?: BrowserContextOptions['clientCertificates']): Promise { +export async function toClientCertificatesProtocol(certs?: BrowserContextOptions['clientCertificates']): Promise { if (!certs) return undefined; - const bufferizeContent = async (value?: Buffer, path?: string): Promise => { + const bufferizeContent = async (value?: Buffer, filePath?: string): Promise => { if (value) return value; - if (path) - return await platform.fs().promises.readFile(path); + if (filePath) + return await fs.promises.readFile(filePath); }; return await Promise.all(certs.map(async cert => ({ diff --git a/packages/playwright-core/src/client/browserType.ts b/packages/playwright-core/src/client/browserType.ts index a37aca97c6c66..e31fa27bf7381 100644 --- a/packages/playwright-core/src/client/browserType.ts +++ b/packages/playwright-core/src/client/browserType.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import path from 'path'; + import { assert } from '@isomorphic/assert'; import { headersObjectToArray } from '@isomorphic/headers'; import { Browser } from './browser'; @@ -72,7 +74,7 @@ export class BrowserType extends ChannelOwner imple ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined, ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs), env: options.env ? envObjectToArray(options.env) : undefined, - timeout: new TimeoutSettings(this._platform).launchTimeout(options), + timeout: new TimeoutSettings().launchTimeout(options), }; return await this._wrapApiCall(async () => { const browser = Browser.from((await this._channel.launch(launchOptions)).browser); @@ -97,15 +99,15 @@ export class BrowserType extends ChannelOwner imple await this._instrumentation.runBeforeCreateBrowserContext(options); const logger = options.logger || this._playwright._defaultLaunchOptions?.logger; - const contextParams = await prepareBrowserContextParams(this._platform, options); + const contextParams = await prepareBrowserContextParams(options); const persistentParams: channels.BrowserTypeLaunchPersistentContextParams = { ...contextParams, ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined, ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs), env: options.env ? envObjectToArray(options.env) : undefined, channel: options.channel, - userDataDir: (this._platform.path().isAbsolute(userDataDir) || !userDataDir) ? userDataDir : this._platform.path().resolve(userDataDir), - timeout: new TimeoutSettings(this._platform).launchTimeout(options), + userDataDir: (path.isAbsolute(userDataDir) || !userDataDir) ? userDataDir : path.resolve(userDataDir), + timeout: new TimeoutSettings().launchTimeout(options), }; const context = await this._wrapApiCall(async () => { const result = await this._channel.launchPersistentContext(persistentParams); @@ -166,7 +168,7 @@ export class BrowserType extends ChannelOwner imple transport: transport as any, headers: params.headers ? headersObjectToArray(params.headers) : undefined, slowMo: params.slowMo, - timeout: new TimeoutSettings(this._platform).timeout(params), + timeout: new TimeoutSettings().timeout(params), isLocal: params.isLocal, noDefaults: params.noDefaults, artifactsDir: params.artifactsDir, @@ -187,7 +189,7 @@ export class BrowserType extends ChannelOwner imple throw new Error('Connecting to workers is only supported in Chromium.'); const result = await this._channel.connectToWorker({ endpoint, - timeout: new TimeoutSettings(this._platform).timeout(options), + timeout: new TimeoutSettings().timeout(options), }); return Worker.from(result.worker); } diff --git a/packages/playwright-core/src/client/channelOwner.ts b/packages/playwright-core/src/client/channelOwner.ts index 781b2e86a94b0..422b1cfacbdb3 100644 --- a/packages/playwright-core/src/client/channelOwner.ts +++ b/packages/playwright-core/src/client/channelOwner.ts @@ -15,7 +15,10 @@ */ import { getMetainfo } from '@isomorphic/protocolMetainfo'; -import { stringifyStackFrames } from '@isomorphic/stackTrace'; +import { showInternalStackFrames, stringifyStackFrames } from '@isomorphic/stackTrace'; +import { isUnderTest } from '@utils/debug'; +import { debugLogger } from '@utils/debugLogger'; +import { currentZone } from '@utils/zones'; import { EventEmitter } from './eventEmitter'; import { ValidationError, maybeFindValidator } from '../protocol/validator'; import { captureLibraryStackTrace } from './clientStackTrace'; @@ -24,7 +27,6 @@ import type { ClientInstrumentation } from './clientInstrumentation'; import type { Connection } from './connection'; import type { Logger } from './types'; import type { ValidatorContext } from '../protocol/validator'; -import type { Platform } from '@isomorphic/platform'; import type * as channels from './channels'; type Listener = (...args: any[]) => void; @@ -45,7 +47,7 @@ export abstract class ChannelOwner) { const connection = parent instanceof ChannelOwner ? parent._connection : parent; - super(connection._platform); + super(); this.setMaxListeners(0); this._connection = connection; this._type = type; @@ -59,7 +61,7 @@ export abstract class ChannelOwner this._platform.isUnderTest(), + isUnderTest, }; } @@ -156,7 +158,7 @@ export abstract class ChannelOwner ${apiZone.apiName} started`); + logApiCall(this._logger, `=> ${apiZone.apiName} started`); return await this._connection.sendMessageToServer(this, prop, validatedParams, apiZone); } // Since this api call is either internal, or has already been reported/traced once, @@ -175,22 +177,22 @@ export abstract class ChannelOwner(func: (apiZone: ApiZone) => Promise, options?: { internal?: boolean, title?: string }): Promise { const logger = this._logger; - const existingApiZone = this._platform.zones.current().data(); + const existingApiZone = currentZone().data('apiZone'); if (existingApiZone) return await func(existingApiZone); - const stackTrace = captureLibraryStackTrace(this._platform); + const stackTrace = captureLibraryStackTrace(); const apiZone: ApiZone = { title: options?.title, apiName: stackTrace.apiName, frames: stackTrace.frames, internal: options?.internal ?? false, reported: false, userData: undefined, stepId: undefined }; try { - const result = await this._platform.zones.current().push(apiZone).run(async () => await func(apiZone)); + const result = await currentZone().with('apiZone', apiZone).run(async () => await func(apiZone)); if (!options?.internal) { - logApiCall(this._platform, logger, `<= ${apiZone.apiName} succeeded`); + logApiCall(logger, `<= ${apiZone.apiName} succeeded`); this._instrumentation.onApiCallEnd(apiZone); } return result; } catch (e) { - const innerError = ((this._platform.showInternalStackFrames() || this._platform.isUnderTest()) && e.stack) ? '\n\n' + e.stack : ''; + const innerError = ((showInternalStackFrames() || isUnderTest()) && e.stack) ? '\n\n' + e.stack : ''; if (apiZone.apiName && !apiZone.apiName.includes('')) e.message = apiZone.apiName + ': ' + e.message; const stackFrames = '\n' + stringifyStackFrames(stackTrace.frames).join('\n') + innerError; @@ -200,7 +202,7 @@ export abstract class ChannelOwner { +export async function evaluationScript(fun: Function | string | { path?: string, content?: string }, arg?: any, addSourceUrl: boolean = true): Promise { if (typeof fun === 'function') { const source = fun.toString(); const argString = Object.is(arg, undefined) ? 'undefined' : JSON.stringify(arg); @@ -41,7 +41,7 @@ export async function evaluationScript(platform: Platform, fun: Function | strin if (fun.content !== undefined) return fun.content; if (fun.path !== undefined) { - let source = await platform.fs().promises.readFile(fun.path, 'utf8'); + let source = await fs.promises.readFile(fun.path, 'utf8'); if (addSourceUrl) source = addSourceUrlToScript(source, fun.path); return source; diff --git a/packages/playwright-core/src/client/clientStackTrace.ts b/packages/playwright-core/src/client/clientStackTrace.ts index 6c4456c293a26..3f2e8fcd73219 100644 --- a/packages/playwright-core/src/client/clientStackTrace.ts +++ b/packages/playwright-core/src/client/clientStackTrace.ts @@ -14,13 +14,15 @@ * limitations under the License. */ -import { captureRawStack, parseStackFrame } from '@isomorphic/stackTrace'; +import path from 'path'; + +import { boxedStackPrefixes, captureRawStack, coreDir, parseStackFrame, showInternalStackFrames } from '@isomorphic/stackTrace'; -import type { Platform } from '@isomorphic/platform'; import type { StackFrame } from '@isomorphic/stackTrace'; -export function captureLibraryStackTrace(platform: Platform): { frames: StackFrame[], apiName: string } { +export function captureLibraryStackTrace(): { frames: StackFrame[], apiName: string } { const stack = captureRawStack(); + const playwrightCoreDir = coreDir(); type ParsedFrame = { frame: StackFrame; @@ -28,10 +30,10 @@ export function captureLibraryStackTrace(platform: Platform): { frames: StackFra isPlaywrightLibrary: boolean; }; let parsedFrames = stack.map(line => { - const frame = parseStackFrame(line, platform.pathSeparator, platform.showInternalStackFrames()); + const frame = parseStackFrame(line, path.sep, showInternalStackFrames()); if (!frame || !frame.file) return null; - const isPlaywrightLibrary = !!platform.coreDir && frame.file.startsWith(platform.coreDir); + const isPlaywrightLibrary = !!playwrightCoreDir && frame.file.startsWith(playwrightCoreDir); const parsed: ParsedFrame = { frame, frameText: line, @@ -63,7 +65,7 @@ export function captureLibraryStackTrace(platform: Platform): { frames: StackFra } // This is for the inspector so that it did not include the test runner stack frames. - const filterPrefixes = platform.boxedStackPrefixes(); + const filterPrefixes = boxedStackPrefixes(); parsedFrames = parsedFrames.filter(f => { if (filterPrefixes.some(prefix => f.frame.file.startsWith(prefix))) return false; diff --git a/packages/playwright-core/src/client/connect.ts b/packages/playwright-core/src/client/connect.ts index 86836d4d3b9e4..52003a4ef0aa9 100644 --- a/packages/playwright-core/src/client/connect.ts +++ b/packages/playwright-core/src/client/connect.ts @@ -78,7 +78,7 @@ export async function connectToEndpoint(parentConnection: Connection, params: ch const localUtils = parentConnection.localUtils(); const transport = localUtils ? new JsonPipeTransport(localUtils) : new WebSocketTransport(); const connectHeaders = await transport.connect(params); - const connection = new Connection(parentConnection._platform, localUtils, parentConnection._instrumentation, connectHeaders); + const connection = new Connection(localUtils, parentConnection._instrumentation, connectHeaders); connection.markAsRemote(); connection.on('close', () => transport.close()); diff --git a/packages/playwright-core/src/client/connection.ts b/packages/playwright-core/src/client/connection.ts index 41b6485e648d7..83a3f24b01c73 100644 --- a/packages/playwright-core/src/client/connection.ts +++ b/packages/playwright-core/src/client/connection.ts @@ -14,7 +14,11 @@ * limitations under the License. */ +import colors from 'colors/safe'; import { rewriteErrorMessage } from '@isomorphic/stackTrace'; +import { isUnderTest } from '@utils/debug'; +import { debugLogger } from '@utils/debugLogger'; +import { emptyZone } from '@utils/zones'; import { EventEmitter } from './eventEmitter'; import { Android, AndroidDevice, AndroidSocket } from './android'; import { Artifact } from './artifact'; @@ -46,7 +50,6 @@ import { ValidationError, findValidator, maybeFindValidator } from '../protocol/ import type { ClientInstrumentation } from './clientInstrumentation'; import type { HeadersArray } from './types'; import type { ValidatorContext } from '../protocol/validator'; -import type { Platform } from '@isomorphic/platform'; import type * as channels from './channels'; class Root extends ChannelOwner { @@ -83,9 +86,10 @@ export class Connection extends EventEmitter { // Used from @playwright/test fixtures -> TODO remove? readonly headers: HeadersArray; private _objectFactories = new Map(); + private _lastWaitId = 0; - constructor(platform: Platform, localUtils?: LocalUtils, instrumentation?: ClientInstrumentation, headers: HeadersArray = []) { - super(platform); + constructor(localUtils?: LocalUtils, instrumentation?: ClientInstrumentation, headers: HeadersArray = []) { + super(); this._instrumentation = instrumentation || createInstrumentation(); this._localUtils = localUtils; this._rootObject = new Root(this); @@ -136,6 +140,10 @@ export class Connection extends EventEmitter { this._objectFactories.set(type, factory); } + nextWaitId(): string { + return 'wait@' + (++this._lastWaitId); + } + markAsRemote() { this._isRemote = true; } @@ -185,9 +193,9 @@ export class Connection extends EventEmitter { const type = object._type; const id = ++this._lastId; const message = { id, guid, method, params }; - if (this._platform.isLogEnabled('channel')) { + if (debugLogger.isEnabled('channel')) { // Do not include metadata in debug logs to avoid noise. - this._platform.log('channel', 'SEND> ' + JSON.stringify(message)); + debugLogger.log('channel', 'SEND> ' + JSON.stringify(message)); } const location = options.frames?.[0] ? { file: options.frames[0].file, line: options.frames[0].line, column: options.frames[0].column } : undefined; const metadata: channels.Metadata = { title: options.title, location, internal: options.internal, stepId: options.stepId }; @@ -195,7 +203,7 @@ export class Connection extends EventEmitter { this._localUtils?.addStackToTracingNoReply({ callData: { stack: options.frames ?? [], id } }).catch(() => {}); // We need to exit zones before calling into the server, otherwise // when we receive events from the server, we would be in an API zone. - this._platform.zones.empty.run(() => this.onmessage({ ...message, metadata })); + emptyZone.run(() => this.onmessage({ ...message, metadata })); // Fire-and-forget: server intentionally never replies to __waitInfo__. if (method === '__waitInfo__') return; @@ -206,7 +214,7 @@ export class Connection extends EventEmitter { return { tChannelImpl: this._tChannelImplFromWire.bind(this), binary: this._rawBuffers ? 'buffer' : 'fromBase64', - isUnderTest: () => this._platform.isUnderTest(), + isUnderTest, }; } @@ -216,8 +224,8 @@ export class Connection extends EventEmitter { const { id, guid, method, params, result, error, errorDetails, log } = message as any; if (id) { - if (this._platform.isLogEnabled('channel')) - this._platform.log('channel', ' !!l)) return ''; return ` Call log: -${platform.colors.dim(log.join('\n'))} +${colors.dim(log.join('\n'))} `; } diff --git a/packages/playwright-core/src/client/consoleMessage.ts b/packages/playwright-core/src/client/consoleMessage.ts index 6c50bc8168436..85ec2a41e5e1b 100644 --- a/packages/playwright-core/src/client/consoleMessage.ts +++ b/packages/playwright-core/src/client/consoleMessage.ts @@ -14,10 +14,11 @@ * limitations under the License. */ +import { inspect } from 'util'; + import { JSHandle } from './jsHandle'; import type * as api from '../../types/types'; -import type { Platform } from '@isomorphic/platform'; import type * as channels from './channels'; import type { Page } from './page'; import type { Worker } from './worker'; @@ -28,12 +29,12 @@ export class ConsoleMessage implements api.ConsoleMessage { private _worker: Worker | null; private _event: channels.BrowserContextConsoleEvent | channels.WorkerConsoleEvent | channels.ElectronApplicationConsoleEvent; - constructor(platform: Platform, event: channels.BrowserContextConsoleEvent | channels.WorkerConsoleEvent | channels.ElectronApplicationConsoleEvent, page: Page | null, worker: Worker | null) { + constructor(event: channels.BrowserContextConsoleEvent | channels.WorkerConsoleEvent | channels.ElectronApplicationConsoleEvent, page: Page | null, worker: Worker | null) { this._page = page; this._worker = worker; this._event = event; - if (platform.inspectCustom) - (this as any)[platform.inspectCustom] = () => this._inspect(); + if (inspect.custom) + (this as any)[inspect.custom] = () => this._inspect(); } worker() { diff --git a/packages/playwright-core/src/client/electron.ts b/packages/playwright-core/src/client/electron.ts index a40923600932a..01af87aa2a8ac 100644 --- a/packages/playwright-core/src/client/electron.ts +++ b/packages/playwright-core/src/client/electron.ts @@ -58,11 +58,11 @@ export class Electron extends ChannelOwner implements async launch(options: ElectronOptions = {}): Promise { options = this._playwright.selectors._withSelectorOptions(options); const params: channels.ElectronLaunchParams = { - ...await prepareBrowserContextParams(this._platform, options), - env: envObjectToArray(options.env ? options.env : this._platform.env), + ...await prepareBrowserContextParams(options), + env: options.env ? envObjectToArray(options.env) : undefined, tracesDir: options.tracesDir, artifactsDir: options.artifactsDir, - timeout: new TimeoutSettings(this._platform).launchTimeout(options), + timeout: new TimeoutSettings().launchTimeout(options), }; const app = ElectronApplication.from((await this._channel.launch(params)).electronApplication); this._playwright.selectors._contextsForSelectors.add(app._context); @@ -85,7 +85,7 @@ export class ElectronApplication extends ChannelOwner { this.emit(Events.ElectronApplication.Close); }); - this._channel.on('console', event => this.emit(Events.ElectronApplication.Console, new ConsoleMessage(this._platform, event, null, null))); + this._channel.on('console', event => this.emit(Events.ElectronApplication.Console, new ConsoleMessage(event, null, null))); this._setEventToSubscriptionMapping(new Map([ [Events.ElectronApplication.Console, 'console'], ])); diff --git a/packages/playwright-core/src/client/elementHandle.ts b/packages/playwright-core/src/client/elementHandle.ts index afaf107a6f2e8..d75d64366e400 100644 --- a/packages/playwright-core/src/client/elementHandle.ts +++ b/packages/playwright-core/src/client/elementHandle.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +import fs from 'fs'; +import path from 'path'; +import stream from 'stream'; + import { assert } from '@isomorphic/assert'; import { isString } from '@isomorphic/rtti'; import { getMimeTypeForPath } from '@isomorphic/mimeType'; @@ -28,7 +32,6 @@ import type { Locator } from './locator'; import type { FilePayload, Rect, SelectOption, SelectOptionOptions, TimeoutOptions } from './types'; import type * as structs from '../../types/structs'; import type * as api from '../../types/types'; -import type { Platform } from '@isomorphic/platform'; import type * as channels from './channels'; export class ElementHandle extends JSHandle implements api.ElementHandle { @@ -148,7 +151,7 @@ export class ElementHandle extends JSHandle implements const frame = await this.ownerFrame(); if (!frame) throw new Error('Cannot set input files to detached element'); - const converted = await convertInputFiles(this._platform, files, frame.page().context()); + const converted = await convertInputFiles(files, frame.page().context()); await this._elementChannel.setInputFiles({ ...converted, ...options, timeout: this._frame._timeout(options) }); } @@ -197,8 +200,8 @@ export class ElementHandle extends JSHandle implements } const result = await this._elementChannel.screenshot(copy); if (options.path) { - await mkdirIfNeeded(this._platform, options.path); - await this._platform.fs().promises.writeFile(options.path, result.binary); + await mkdirIfNeeded(options.path); + await fs.promises.writeFile(options.path, result.binary); } return result.binary; } @@ -256,18 +259,18 @@ function filePayloadExceedsSizeLimit(payloads: FilePayload[]) { return payloads.reduce((size, item) => size + (item.buffer ? item.buffer.byteLength : 0), 0) >= fileUploadSizeLimit; } -async function resolvePathsAndDirectoryForInputFiles(platform: Platform, items: string[]): Promise<[string[] | undefined, string | undefined]> { +async function resolvePathsAndDirectoryForInputFiles(items: string[]): Promise<[string[] | undefined, string | undefined]> { let localPaths: string[] | undefined; let localDirectory: string | undefined; for (const item of items) { - const stat = await platform.fs().promises.stat(item as string); + const stat = await fs.promises.stat(item as string); if (stat.isDirectory()) { if (localDirectory) throw new Error('Multiple directories are not supported'); - localDirectory = platform.path().resolve(item as string); + localDirectory = path.resolve(item as string); } else { localPaths ??= []; - localPaths.push(platform.path().resolve(item as string)); + localPaths.push(path.resolve(item as string)); } } if (localPaths?.length && localDirectory) @@ -275,30 +278,30 @@ async function resolvePathsAndDirectoryForInputFiles(platform: Platform, items: return [localPaths, localDirectory]; } -export async function convertInputFiles(platform: Platform, files: string | FilePayload | string[] | FilePayload[], context: BrowserContext): Promise { +export async function convertInputFiles(files: string | FilePayload | string[] | FilePayload[], context: BrowserContext): Promise { const items: (string | FilePayload)[] = Array.isArray(files) ? files.slice() : [files]; if (items.some(item => typeof item === 'string')) { if (!items.every(item => typeof item === 'string')) throw new Error('File paths cannot be mixed with buffers'); - const [localPaths, localDirectory] = await resolvePathsAndDirectoryForInputFiles(platform, items); + const [localPaths, localDirectory] = await resolvePathsAndDirectoryForInputFiles(items); if (context._connection.isRemote()) { - const files = localDirectory ? (await platform.fs().promises.readdir(localDirectory, { withFileTypes: true, recursive: true })).filter(f => f.isFile()).map(f => platform.path().join(f.parentPath, f.name)) : localPaths!; + const files = localDirectory ? (await fs.promises.readdir(localDirectory, { withFileTypes: true, recursive: true })).filter(f => f.isFile()).map(f => path.join(f.parentPath, f.name)) : localPaths!; const { writableStreams, rootDir } = await context._wrapApiCall(async () => context._channel.createTempFiles({ - rootDirName: localDirectory ? platform.path().basename(localDirectory) : undefined, + rootDirName: localDirectory ? path.basename(localDirectory) : undefined, items: await Promise.all(files.map(async file => { - const lastModifiedMs = (await platform.fs().promises.stat(file)).mtimeMs; + const lastModifiedMs = (await fs.promises.stat(file)).mtimeMs; return { - name: localDirectory ? platform.path().relative(localDirectory, file) : platform.path().basename(file), + name: localDirectory ? path.relative(localDirectory, file) : path.basename(file), lastModifiedMs }; })), }), { internal: true }); for (let i = 0; i < files.length; i++) { const writable = WritableStream.from(writableStreams[i]); - await platform.streamFile(files[i], writable.stream()); + await stream.promises.pipeline(fs.createReadStream(files[i]), writable.stream()); } return { directoryStream: rootDir, diff --git a/packages/playwright-core/src/client/eventEmitter.ts b/packages/playwright-core/src/client/eventEmitter.ts index 45fb486eb9bf2..127b44d7aee49 100644 --- a/packages/playwright-core/src/client/eventEmitter.ts +++ b/packages/playwright-core/src/client/eventEmitter.ts @@ -22,8 +22,11 @@ * USE OR OTHER DEALINGS IN THE SOFTWARE. */ +import { EventEmitter as NodeEventEmitter } from 'events'; + +import { isUnderTest } from '@utils/debug'; + import type { EventEmitter as EventEmitterType } from 'events'; -import type { Platform } from '@isomorphic/platform'; type EventType = string | symbol; type Listener = (...args: any[]) => any; @@ -36,10 +39,8 @@ export class EventEmitter implements EventEmitterType { private _maxListeners: number | undefined = undefined; readonly _pendingHandlers = new Map>>(); private _rejectionHandler: ((error: Error) => void) | undefined; - readonly _platform: Platform; - constructor(platform: Platform) { - this._platform = platform; + constructor() { if (this._events === undefined || this._events === Object.getPrototypeOf(this)._events) { this._events = Object.create(null); this._eventsCount = 0; @@ -57,7 +58,7 @@ export class EventEmitter implements EventEmitterType { } getMaxListeners(): number { - return this._maxListeners === undefined ? this._platform.defaultMaxListeners() : this._maxListeners; + return this._maxListeners === undefined ? NodeEventEmitter.defaultMaxListeners : this._maxListeners; } emit(type: EventType, ...args: any[]): boolean { @@ -155,7 +156,7 @@ export class EventEmitter implements EventEmitterType { w.emitter = this; w.type = type; w.count = existing.length; - if (!this._platform.isUnderTest()) { + if (!isUnderTest()) { // eslint-disable-next-line no-console console.warn(w); } diff --git a/packages/playwright-core/src/client/fetch.ts b/packages/playwright-core/src/client/fetch.ts index 8f94e622e1853..dd061e5848c25 100644 --- a/packages/playwright-core/src/client/fetch.ts +++ b/packages/playwright-core/src/client/fetch.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +import * as fs from 'fs'; +import path from 'path'; +import { inspect } from 'util'; + import { assert } from '@isomorphic/assert'; import { headersObjectToArray } from '@isomorphic/headers'; import { isString } from '@isomorphic/rtti'; @@ -30,9 +34,7 @@ import type { ClientCertificate, FilePayload, Headers, RemoteAddr, SecurityDetai import type { Serializable } from '../../types/structs'; import type * as api from '../../types/types'; import type { HeadersArray, NameValue } from '@isomorphic/types'; -import type { Platform } from '@isomorphic/platform'; import type * as channels from './channels'; -import type * as fs from 'fs'; export type FetchOptions = { params?: { [key: string]: string | number | boolean; } | URLSearchParams | string, @@ -68,14 +70,14 @@ export class APIRequest implements api.APIRequest { options = { ...options }; await this._playwright._instrumentation.runBeforeCreateRequestContext(options); const storageState = typeof options.storageState === 'string' ? - JSON.parse(await this._playwright._platform.fs().promises.readFile(options.storageState, 'utf8')) : + JSON.parse(await fs.promises.readFile(options.storageState, 'utf8')) : options.storageState; const context = APIRequestContext.from((await this._playwright._channel.newRequest({ ...options, extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined, storageState, tracesDir: this._playwright._defaultLaunchOptions?.tracesDir, // We do not expose tracesDir in the API, so do not allow options to accidentally override it. - clientCertificates: await toClientCertificatesProtocol(this._playwright._platform, options.clientCertificates), + clientCertificates: await toClientCertificatesProtocol(options.clientCertificates), })).request); this._contexts.add(context); context._request = this; @@ -99,7 +101,7 @@ export class APIRequestContext extends ChannelOwner { const state = await this._channel.storageState({ indexedDB: options.indexedDB }); if (options.path) { - await mkdirIfNeeded(this._platform, options.path); - await this._platform.fs().promises.writeFile(options.path, JSON.stringify(state, undefined, 2), 'utf8'); + await mkdirIfNeeded(options.path); + await fs.promises.writeFile(options.path, JSON.stringify(state, undefined, 2), 'utf8'); } return state; } } -async function toFormField(platform: Platform, name: string, value: string | number | boolean | fs.ReadStream | FilePayload): Promise { +async function toFormField(name: string, value: string | number | boolean | fs.ReadStream | FilePayload): Promise { const typeOfValue = typeof value; if (isFilePayload(value)) { const payload = value as FilePayload; @@ -283,7 +285,7 @@ async function toFormField(platform: Platform, name: string, value: string | num } else if (typeOfValue === 'string' || typeOfValue === 'number' || typeOfValue === 'boolean') { return { name, value: String(value) }; } else { - return { name, file: await readStreamToJson(platform, value as fs.ReadStream) }; + return { name, file: await readStreamToJson(value as fs.ReadStream) }; } } @@ -312,8 +314,8 @@ export class APIResponse implements api.APIResponse { this._initializer = initializer; this._headers = new RawHeaders(this._initializer.headers); - if (context._platform.inspectCustom) - (this as any)[context._platform.inspectCustom] = () => this._inspect(); + if (inspect.custom) + (this as any)[inspect.custom] = () => this._inspect(); } ok(): boolean { @@ -406,7 +408,7 @@ function filePayloadToJson(payload: FilePayload): ServerFilePayload { }; } -async function readStreamToJson(platform: Platform, stream: fs.ReadStream): Promise { +async function readStreamToJson(stream: fs.ReadStream): Promise { const buffer = await new Promise((resolve, reject) => { const chunks: Buffer[] = []; stream.on('data', chunk => chunks.push(chunk as Buffer)); @@ -415,7 +417,7 @@ async function readStreamToJson(platform: Platform, stream: fs.ReadStream): Prom }); const streamPath: string = Buffer.isBuffer(stream.path) ? stream.path.toString('utf8') : stream.path; return { - name: platform.path().basename(streamPath), + name: path.basename(streamPath), buffer, }; } diff --git a/packages/playwright-core/src/client/fileUtils.ts b/packages/playwright-core/src/client/fileUtils.ts index a291046c9f42e..87191f7acb402 100644 --- a/packages/playwright-core/src/client/fileUtils.ts +++ b/packages/playwright-core/src/client/fileUtils.ts @@ -14,12 +14,13 @@ * limitations under the License. */ -import type { Platform } from '@isomorphic/platform'; +import fs from 'fs'; +import path from 'path'; // Keep in sync with the server. export const fileUploadSizeLimit = 50 * 1024 * 1024; -export async function mkdirIfNeeded(platform: Platform, filePath: string) { +export async function mkdirIfNeeded(filePath: string) { // This will harmlessly throw on windows if the dirname is the root directory. - await platform.fs().promises.mkdir(platform.path().dirname(filePath), { recursive: true }).catch(() => {}); + await fs.promises.mkdir(path.dirname(filePath), { recursive: true }).catch(() => {}); } diff --git a/packages/playwright-core/src/client/frame.ts b/packages/playwright-core/src/client/frame.ts index c6b9695ff997d..6248d008d3c57 100644 --- a/packages/playwright-core/src/client/frame.ts +++ b/packages/playwright-core/src/client/frame.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +import fs from 'fs'; + import { assert } from '@isomorphic/assert'; import { getByAltTextSelector, getByLabelSelector, getByPlaceholderSelector, getByRoleSelector, getByTestIdSelector, getByTextSelector, getByTitleSelector } from '@isomorphic/locatorUtils'; import { urlMatches } from '@isomorphic/urlMatch'; @@ -67,7 +69,7 @@ export class Frame extends ChannelOwner implements api.Fr constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.FrameInitializer) { super(parent, type, guid, initializer); - this._eventEmitter = new EventEmitter(parent._platform); + this._eventEmitter = new EventEmitter(); this._eventEmitter.setMaxListeners(0); this._parentFrame = Frame.fromNullable(initializer.parentFrame); if (this._parentFrame) @@ -105,12 +107,12 @@ export class Frame extends ChannelOwner implements api.Fr } _timeout(options?: TimeoutOptions): number { - const timeoutSettings = this._page?._timeoutSettings || new TimeoutSettings(this._platform); + const timeoutSettings = this._page?._timeoutSettings || new TimeoutSettings(); return timeoutSettings.timeout(options || {}); } _navigationTimeout(options?: TimeoutOptions): number { - const timeoutSettings = this._page?._timeoutSettings || new TimeoutSettings(this._platform); + const timeoutSettings = this._page?._timeoutSettings || new TimeoutSettings(); return timeoutSettings.navigationTimeout(options || {}); } @@ -286,7 +288,7 @@ export class Frame extends ChannelOwner implements api.Fr async addScriptTag(options: { url?: string, path?: string, content?: string, type?: string } = {}): Promise { const copy = { ...options }; if (copy.path) { - copy.content = (await this._platform.fs().promises.readFile(copy.path)).toString(); + copy.content = (await fs.promises.readFile(copy.path)).toString(); copy.content = addSourceUrlToScript(copy.content, copy.path); } return ElementHandle.from((await this._channel.addScriptTag({ ...copy })).element); @@ -295,7 +297,7 @@ export class Frame extends ChannelOwner implements api.Fr async addStyleTag(options: { url?: string; path?: string; content?: string; } = {}): Promise { const copy = { ...options }; if (copy.path) { - copy.content = (await this._platform.fs().promises.readFile(copy.path)).toString(); + copy.content = (await fs.promises.readFile(copy.path)).toString(); copy.content += '/*# sourceURL=' + copy.path.replace(/\n/g, '') + '*/'; } return ElementHandle.from((await this._channel.addStyleTag({ ...copy })).element); @@ -316,7 +318,7 @@ export class Frame extends ChannelOwner implements api.Fr async _drop(selector: string, payload: DropPayload, options: Omit & TimeoutOptions = {}) { let fileParams: { payloads?: channels.FrameDropParams['payloads'], localPaths?: string[], streams?: channels.FrameDropParams['streams'] } = {}; if (payload.files !== undefined) { - const converted = await convertInputFiles(this._platform, payload.files, this.page().context()); + const converted = await convertInputFiles(payload.files, this.page().context()); if (converted.localDirectory || converted.directoryStream) throw new Error('Dropping a directory is not supported — pass individual files.'); fileParams = { payloads: converted.payloads, localPaths: converted.localPaths, streams: converted.streams }; @@ -442,7 +444,7 @@ export class Frame extends ChannelOwner implements api.Fr } async setInputFiles(selector: string, files: string | FilePayload | string[] | FilePayload[], options: channels.FrameSetInputFilesOptions & TimeoutOptions = {}): Promise { - const converted = await convertInputFiles(this._platform, files, this.page().context()); + const converted = await convertInputFiles(files, this.page().context()); await this._channel.setInputFiles({ selector, ...converted, ...options, timeout: this._timeout(options) }); } diff --git a/packages/playwright-core/src/client/harRouter.ts b/packages/playwright-core/src/client/harRouter.ts index b6a25bd3bee30..e4350ad5e3f8c 100644 --- a/packages/playwright-core/src/client/harRouter.ts +++ b/packages/playwright-core/src/client/harRouter.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { debugLogger } from '@utils/debugLogger'; + import type { BrowserContext } from './browserContext'; import type { LocalUtils } from './localUtils'; import type { Route } from './network'; @@ -55,7 +57,7 @@ export class HarRouter { }); if (response.action === 'redirect') { - route._platform.log('api', `HAR: ${route.request().url()} redirected to ${response.redirectURL}`); + debugLogger.log('api', `HAR: ${route.request().url()} redirected to ${response.redirectURL}`); await route._redirectNavigationRequest(response.redirectURL!); return; } @@ -96,7 +98,7 @@ export class HarRouter { } if (response.action === 'error') - route._platform.log('api', 'HAR: ' + response.message!); + debugLogger.log('api', 'HAR: ' + response.message!); // Report the error, but fall through to the default handler. if (this._notFoundAction === 'abort') { diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index 89c892c540b12..11eaefb5e2175 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { inspect } from 'util'; + import { asLocatorDescription, locatorCustomDescription } from '@isomorphic/locatorGenerators'; import { getByAltTextSelector, getByLabelSelector, getByPlaceholderSelector, getByRoleSelector, getByTestIdSelector, getByTextSelector, getByTitleSelector } from '@isomorphic/locatorUtils'; import { escapeForTextSelector } from '@isomorphic/stringUtils'; @@ -70,8 +72,8 @@ export class Locator implements api.Locator { if (options?.visible !== undefined) this._selector += ` >> visible=${options.visible ? 'true' : 'false'}`; - if (this._frame._platform.inspectCustom) - (this as any)[this._frame._platform.inspectCustom] = () => this._inspect(); + if (inspect.custom) + (this as any)[inspect.custom] = () => this._inspect(); } private async _withElement(task: (handle: ElementHandle, timeout?: number) => Promise, options: { title: string, internal?: boolean, timeout?: number }): Promise { diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts index 38b14c38664c1..820d136c179d9 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import fs from 'fs'; + import { assert } from '@isomorphic/assert'; import { headersObjectToArray } from '@isomorphic/headers'; import { resolveGlobToRegexPattern, serializeURLMatch, urlMatches } from '@isomorphic/urlMatch'; @@ -22,6 +24,7 @@ import { MultiMap } from '@isomorphic/multimap'; import { isString } from '@isomorphic/rtti'; import { rewriteErrorMessage } from '@isomorphic/stackTrace'; import { getMimeTypeForPath } from '@isomorphic/mimeType'; +import { currentZone } from '@utils/zones'; import { Worker } from './worker'; import { Waiter } from './waiter'; import { Frame } from './frame'; @@ -38,7 +41,7 @@ import type * as api from '../../types/types'; import type { HeadersArray } from '@isomorphic/types'; import type { URLMatch } from '@isomorphic/urlMatch'; import type * as channels from './channels'; -import type { Platform, Zone } from '@isomorphic/platform'; +import type { Zone } from '@utils/zones'; export type NetworkCookie = { name: string, @@ -389,7 +392,7 @@ export class Route extends ChannelOwner implements api.Ro let isBase64 = false; let length = 0; if (options.path) { - const buffer = await this._platform.fs().promises.readFile(options.path); + const buffer = await fs.promises.readFile(options.path); body = buffer.toString('base64'); isBase64 = true; length = buffer.length; @@ -836,12 +839,12 @@ export class RouteHandler { private _activeInvocations: Set<{ complete: Promise, route: Route }> = new Set(); private _savedZone: Zone; - constructor(platform: Platform, baseURL: string | undefined, url: URLMatch, handler: RouteHandlerCallback, times: number = Number.MAX_SAFE_INTEGER) { + constructor(baseURL: string | undefined, url: URLMatch, handler: RouteHandlerCallback, times: number = Number.MAX_SAFE_INTEGER) { this._baseURL = baseURL; this._times = times; this.url = url; this.handler = handler; - this._savedZone = platform.zones.current().pop(); + this._savedZone = currentZone().without('apiZone'); // Eagerly validate string globs so that invalid patterns throw at the call site // (e.g. page.route()) rather than silently aborting requests later. if (typeof url === 'string') diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 3f571d72a83a6..8fcdb1a937001 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -15,6 +15,10 @@ * limitations under the License. */ +import fs from 'fs'; +import * as inspector from 'inspector'; +import path from 'path'; + import { assert } from '@isomorphic/assert'; import { headersObjectToArray } from '@isomorphic/headers'; import { trimStringWithEllipsis } from '@isomorphic/stringUtils'; @@ -124,7 +128,7 @@ export class Page extends ChannelOwner implements api.Page super(parent, type, guid, initializer); this._instrumentation.onPage(this); this._browserContext = parent as unknown as BrowserContext; - this._timeoutSettings = new TimeoutSettings(this._platform, this._browserContext._timeoutSettings); + this._timeoutSettings = new TimeoutSettings(this._browserContext._timeoutSettings); this.keyboard = new Keyboard(this); this.mouse = new Mouse(this); @@ -140,7 +144,7 @@ export class Page extends ChannelOwner implements api.Page this._viewportSize = initializer.viewportSize; this._closed = initializer.isClosed; this._opener = Page.fromNullable(initializer.opener); - this._video = new Video(this, this._connection, initializer.video ? Artifact.from(initializer.video) : undefined); + this._video = new Video(this._connection, initializer.video ? Artifact.from(initializer.video) : undefined); this.screencast = new Screencast(this); this._channel.on('bindingCall', ({ binding }) => this._onBinding(BindingCall.from(binding))); @@ -530,12 +534,12 @@ export class Page extends ChannelOwner implements api.Page } async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) { - const source = await evaluationScript(this._platform, script, arg); + const source = await evaluationScript(script, arg); return DisposableObject.from((await this._channel.addInitScript({ source })).disposable); } async route(url: URLMatch, handler: RouteHandlerCallback, options: { times?: number } = {}): Promise { - this._routes.unshift(new RouteHandler(this._platform, this._browserContext._options.baseURL, url, handler, options.times)); + this._routes.unshift(new RouteHandler(this._browserContext._options.baseURL, url, handler, options.times)); await this._updateInterceptionPatterns({ title: 'Route requests' }); return new DisposableStub(() => this.unroute(url, handler)); } @@ -612,8 +616,8 @@ export class Page extends ChannelOwner implements api.Page } const result = await this._channel.screenshot(copy); if (options.path) { - await mkdirIfNeeded(this._platform, options.path); - await this._platform.fs().promises.writeFile(options.path, result.binary); + await mkdirIfNeeded(options.path); + await fs.promises.writeFile(options.path, result.binary); } return result.binary; } @@ -703,7 +707,7 @@ export class Page extends ChannelOwner implements api.Page async consoleMessages(options?: { filter?: 'all' | 'since-navigation' }): Promise { const { messages } = await this._channel.consoleMessages({ filter: options?.filter }); - return messages.map(message => new ConsoleMessage(this._platform, message, this, null)); + return messages.map(message => new ConsoleMessage(message, this, null)); } async clearPageErrors(): Promise { @@ -849,7 +853,7 @@ export class Page extends ChannelOwner implements api.Page } async pause(_options?: { __testHookKeepTestTimeout: boolean }) { - if (this._platform.isJSDebuggerAttached()) + if (inspector.url()) return; const defaultNavigationTimeout = this._browserContext._timeoutSettings.defaultNavigationTimeout(); const defaultTimeout = this._browserContext._timeoutSettings.defaultTimeout(); @@ -876,9 +880,8 @@ export class Page extends ChannelOwner implements api.Page } const result = await this._channel.pdf(transportOptions); if (options.path) { - const platform = this._platform; - await platform.fs().promises.mkdir(platform.path().dirname(options.path), { recursive: true }); - await platform.fs().promises.writeFile(options.path, result.pdf); + await fs.promises.mkdir(path.dirname(options.path), { recursive: true }); + await fs.promises.writeFile(options.path, result.pdf); } return result.pdf; } diff --git a/packages/playwright-core/src/client/playwright.ts b/packages/playwright-core/src/client/playwright.ts index 5e375daa1813e..f2bd2773f4dca 100644 --- a/packages/playwright-core/src/client/playwright.ts +++ b/packages/playwright-core/src/client/playwright.ts @@ -56,7 +56,7 @@ export class Playwright extends ChannelOwner { this._electron = Electron.from(initializer.electron); this._electron._playwright = this; this.devices = this._connection.localUtils()?.devices ?? {}; - this.selectors = new Selectors(this._connection._platform); + this.selectors = new Selectors(); this.errors = { TimeoutError }; } diff --git a/packages/playwright-core/src/client/selectors.ts b/packages/playwright-core/src/client/selectors.ts index 7e5f17f139491..a75e14c4fb25c 100644 --- a/packages/playwright-core/src/client/selectors.ts +++ b/packages/playwright-core/src/client/selectors.ts @@ -21,23 +21,17 @@ import type { SelectorEngine } from './types'; import type * as api from '../../types/types'; import type * as channels from './channels'; import type { BrowserContext } from './browserContext'; -import type { Platform } from '@isomorphic/platform'; export class Selectors implements api.Selectors { - private _platform: Platform; private _selectorEngines: channels.SelectorEngine[] = []; private _testIdAttributeName: string | undefined; readonly _contextsForSelectors = new Set(); - constructor(platform: Platform) { - this._platform = platform; - } - async register(name: string, script: string | (() => SelectorEngine) | { path?: string, content?: string }, options: { contentScript?: boolean } = {}): Promise { if (this._selectorEngines.some(engine => engine.name === name)) throw new Error(`selectors.register: "${name}" selector engine has been already registered`); - const source = await evaluationScript(this._platform, script, undefined, false); + const source = await evaluationScript(script, undefined, false); const selectorEngine: channels.SelectorEngine = { ...options, name, source }; for (const context of this._contextsForSelectors) await context._channel.registerSelectorEngine({ selectorEngine }); diff --git a/packages/playwright-core/src/client/stream.ts b/packages/playwright-core/src/client/stream.ts index bbfe8f3045cec..86e8b538b9c99 100644 --- a/packages/playwright-core/src/client/stream.ts +++ b/packages/playwright-core/src/client/stream.ts @@ -30,6 +30,29 @@ export class Stream extends ChannelOwner { } stream(): Readable { - return this._platform.streamReadable(this._channel); + return new ReadableStreamImpl(this._channel); + } +} + +class ReadableStreamImpl extends Readable { + private _channel: channels.StreamChannel; + + constructor(channel: channels.StreamChannel) { + super(); + this._channel = channel; + } + + override async _read() { + const result = await this._channel.read({ size: 1024 * 1024 }); + if (result.binary.byteLength) + this.push(result.binary); + else + this.push(null); + } + + override _destroy(error: Error | null, callback: (error: Error | null | undefined) => void): void { + // Stream might be destroyed after the connection was closed. + this._channel.close().catch(e => null); + super._destroy(error, callback); } } diff --git a/packages/playwright-core/src/client/timeoutSettings.ts b/packages/playwright-core/src/client/timeoutSettings.ts index ee384b24cb36e..f5c4b4f5cccef 100644 --- a/packages/playwright-core/src/client/timeoutSettings.ts +++ b/packages/playwright-core/src/client/timeoutSettings.ts @@ -16,18 +16,15 @@ */ import { DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT, DEFAULT_PLAYWRIGHT_TIMEOUT } from '@isomorphic/time'; - -import type { Platform } from '@isomorphic/platform'; +import { debugMode } from '@utils/debug'; export class TimeoutSettings { private _parent: TimeoutSettings | undefined; private _defaultTimeout: number | undefined; private _defaultNavigationTimeout: number | undefined; - private _platform: Platform; - constructor(platform: Platform, parent?: TimeoutSettings) { + constructor(parent?: TimeoutSettings) { this._parent = parent; - this._platform = platform; } setDefaultTimeout(timeout: number | undefined) { @@ -51,7 +48,7 @@ export class TimeoutSettings { return options.timeout; if (this._defaultNavigationTimeout !== undefined) return this._defaultNavigationTimeout; - if (this._platform.isDebugMode()) + if (debugMode() === 'inspector') return 0; if (this._defaultTimeout !== undefined) return this._defaultTimeout; @@ -63,7 +60,7 @@ export class TimeoutSettings { timeout(options: { timeout?: number }): number { if (typeof options.timeout === 'number') return options.timeout; - if (this._platform.isDebugMode()) + if (debugMode() === 'inspector') return 0; if (this._defaultTimeout !== undefined) return this._defaultTimeout; @@ -75,7 +72,7 @@ export class TimeoutSettings { launchTimeout(options: { timeout?: number }): number { if (typeof options.timeout === 'number') return options.timeout; - if (this._platform.isDebugMode()) + if (debugMode() === 'inspector') return 0; if (this._parent) return this._parent.launchTimeout(options); diff --git a/packages/playwright-core/src/client/video.ts b/packages/playwright-core/src/client/video.ts index 76a45a195080c..9c8604b318d38 100644 --- a/packages/playwright-core/src/client/video.ts +++ b/packages/playwright-core/src/client/video.ts @@ -18,15 +18,14 @@ import { Artifact } from './artifact'; import { EventEmitter } from './eventEmitter'; import type { Connection } from './connection'; -import type { Page } from './page'; import type * as api from '../../types/types'; export class Video extends EventEmitter implements api.Video { private _artifact: Artifact | undefined; private _isRemote = false; - constructor(page: Page, connection: Connection, artifact: Artifact | undefined) { - super(page._platform); + constructor(connection: Connection, artifact: Artifact | undefined) { + super(); this._isRemote = connection.isRemote(); this._artifact = artifact; } diff --git a/packages/playwright-core/src/client/waiter.ts b/packages/playwright-core/src/client/waiter.ts index 1ab3bd02945f5..77c67565d3193 100644 --- a/packages/playwright-core/src/client/waiter.ts +++ b/packages/playwright-core/src/client/waiter.ts @@ -15,12 +15,13 @@ */ import { rewriteErrorMessage } from '@isomorphic/stackTrace'; +import { currentZone } from '@utils/zones'; import { TimeoutError } from './errors'; import type { ChannelOwner } from './channelOwner'; import type * as channels from './channels'; import type { EventEmitter } from 'events'; -import type { Zone } from '@isomorphic/platform'; +import type { Zone } from '@utils/zones'; export class Waiter { private _dispose: (() => void)[]; @@ -33,9 +34,9 @@ export class Waiter { private _savedZone: Zone; constructor(channelOwner: ChannelOwner, event: string) { - this._waitId = channelOwner._platform.createGuid(); + this._waitId = channelOwner._connection.nextWaitId(); this._channelOwner = channelOwner; - this._savedZone = channelOwner._platform.zones.current().pop(); + this._savedZone = currentZone().without('apiZone'); const title = `Wait for event "${event}"`; this._sendWaitInfo({ waitId: this._waitId, phase: 'before', event }, { title }); diff --git a/packages/playwright-core/src/client/worker.ts b/packages/playwright-core/src/client/worker.ts index 2b5ba55dd26bb..66b4bcb60ef69 100644 --- a/packages/playwright-core/src/client/worker.ts +++ b/packages/playwright-core/src/client/worker.ts @@ -52,7 +52,7 @@ export class Worker extends ChannelOwner implements api. ])); this._channel.on('console', event => { // Note: we only receive console events here for workers from "chromium._connectToWorker". - this.emit(Events.Worker.Console, new ConsoleMessage(this._platform, event, null, this)); + this.emit(Events.Worker.Console, new ConsoleMessage(event, null, this)); }); this._channel.on('close', () => { if (this._page) @@ -88,7 +88,7 @@ export class Worker extends ChannelOwner implements api. async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise { return await this._wrapApiCall(async () => { - const timeoutSettings = this._page?._timeoutSettings ?? this._context?._timeoutSettings ?? new TimeoutSettings(this._platform); + const timeoutSettings = this._page?._timeoutSettings ?? this._context?._timeoutSettings ?? new TimeoutSettings(); const timeout = timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate); const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate; const signal = typeof optionsOrPredicate === 'function' ? undefined : (optionsOrPredicate as TimeoutOptions).signal; diff --git a/packages/playwright-core/src/client/writableStream.ts b/packages/playwright-core/src/client/writableStream.ts index c346ee353c0c6..475fea38f423e 100644 --- a/packages/playwright-core/src/client/writableStream.ts +++ b/packages/playwright-core/src/client/writableStream.ts @@ -14,10 +14,11 @@ * limitations under the License. */ +import { Writable } from 'stream'; + import { ChannelOwner } from './channelOwner'; import type * as channels from './channels'; -import type { Writable } from 'stream'; export class WritableStream extends ChannelOwner { static from(Stream: channels.WritableStreamChannel): WritableStream { @@ -29,6 +30,26 @@ export class WritableStream extends ChannelOwner } stream(): Writable { - return this._platform.streamWritable(this._channel); + return new WritableStreamImpl(this._channel); + } +} + +class WritableStreamImpl extends Writable { + private _channel: channels.WritableStreamChannel; + + constructor(channel: channels.WritableStreamChannel) { + super(); + this._channel = channel; + } + + override async _write(chunk: Buffer | string, encoding: BufferEncoding, callback: (error?: Error | null) => void) { + const error = await this._channel.write({ binary: typeof chunk === 'string' ? Buffer.from(chunk) : chunk }).catch(e => e); + callback(error || null); + } + + override async _final(callback: (error?: Error | null) => void) { + // Stream might be destroyed after the connection was closed. + const error = await this._channel.close().catch(e => e); + callback(error || null); } } diff --git a/packages/playwright-core/src/inprocess.ts b/packages/playwright-core/src/inprocess.ts index 49be0eb9d775d..8701615340736 100644 --- a/packages/playwright-core/src/inprocess.ts +++ b/packages/playwright-core/src/inprocess.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { nodePlatform } from '@utils/nodePlatform'; +import { setCoreDir, setShowInternalStackFrames } from '@isomorphic/stackTrace'; import { AndroidServerLauncherImpl } from './androidServerImpl'; import { BrowserServerLauncherImpl } from './browserServerImpl'; import { DispatcherConnection, PlaywrightDispatcher, RootDispatcher, createPlaywright } from './server'; @@ -26,7 +26,9 @@ import type { Language } from '@isomorphic/locatorGenerators'; export function createInProcessPlaywright(): PlaywrightAPI { const playwright = createPlaywright({ sdkLanguage: (process.env.PW_LANG_NAME as Language | undefined) || 'javascript', isClientCollocatedWithServer: true }); - const clientConnection = new Connection(nodePlatform(packageRoot)); + setCoreDir(packageRoot); + setShowInternalStackFrames(!!process.env.PWDEBUGIMPL); + const clientConnection = new Connection(); clientConnection.useRawBuffers(); const dispatcherConnection = new DispatcherConnection(true /* in process */); diff --git a/packages/playwright-core/src/outofprocess.ts b/packages/playwright-core/src/outofprocess.ts index fd1cbef350bee..6ecbd3d520dae 100644 --- a/packages/playwright-core/src/outofprocess.ts +++ b/packages/playwright-core/src/outofprocess.ts @@ -18,8 +18,8 @@ import * as childProcess from 'child_process'; import path from 'path'; import { PipeTransport } from '@utils/pipeTransport'; -import { nodePlatform } from '@utils/nodePlatform'; import { ManualPromise } from '@isomorphic/manualPromise'; +import { setCoreDir, setShowInternalStackFrames } from '@isomorphic/stackTrace'; import { Connection } from './client/connection'; import { packageRoot } from './package'; @@ -51,7 +51,9 @@ class PlaywrightClient { // eslint-disable-next-line no-restricted-properties this._driverProcess.stderr!.on('data', data => process.stderr.write(data)); - const connection = new Connection(nodePlatform(packageRoot)); + setCoreDir(packageRoot); + setShowInternalStackFrames(!!process.env.PWDEBUGIMPL); + const connection = new Connection(); const transport = new PipeTransport(this._driverProcess.stdin!, this._driverProcess.stdout!); connection.onmessage = message => transport.send(JSON.stringify(message)); transport.onmessage = message => connection.dispatch(JSON.parse(message)); diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index 7aa9ccc3d3243..c53f7a29e284f 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -20,11 +20,11 @@ import path from 'path'; import * as playwrightLibrary from 'playwright-core'; import { asLocatorDescription } from '@isomorphic/locatorGenerators'; import { getActionGroup, renderTitleForCall } from '@isomorphic/protocolFormatter'; +import { setBoxedStackPrefixes } from '@isomorphic/stackTrace'; import { escapeHTML } from '@isomorphic/stringUtils'; import { jsonStringifyForceASCII } from '@utils/ascii'; import { createGuid } from '@utils/crypto'; import { debugMode } from '@utils/debug'; -import { setBoxedStackPrefixes } from '@utils/nodePlatform'; import { currentZone } from '@utils/zones'; import { buildErrorContext } from './errorContext'; import { config, testType } from './common'; diff --git a/packages/utils/index.ts b/packages/utils/index.ts index 00fd5ca902cc2..d8de918d859e9 100644 --- a/packages/utils/index.ts +++ b/packages/utils/index.ts @@ -26,7 +26,6 @@ export * from './fileUtils'; export * from './hostPlatform'; export * from './httpServer'; export * from './network'; -export * from './nodePlatform'; export * from './processLauncher'; export * from './profiler'; export * from './serializedFS'; diff --git a/packages/utils/nodePlatform.ts b/packages/utils/nodePlatform.ts deleted file mode 100644 index ab0811e1efae0..0000000000000 --- a/packages/utils/nodePlatform.ts +++ /dev/null @@ -1,170 +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 crypto from 'crypto'; -import fs from 'fs'; -import path from 'path'; -import * as util from 'util'; -import { Readable, Writable, pipeline } from 'stream'; -import { EventEmitter } from 'events'; - -import colors from 'colors/safe'; -import { debugLogger } from './debugLogger'; -import { currentZone, emptyZone } from './zones'; -import { debugMode, isUnderTest } from './debug'; - -import type { Platform, StreamChannel, WritableStreamChannel, Zone } from '@isomorphic/platform'; -import type { Zone as ZoneImpl } from './zones'; - -const pipelineAsync = util.promisify(pipeline); - -class NodeZone implements Zone { - private _zone: ZoneImpl; - - constructor(zone: ZoneImpl) { - this._zone = zone; - } - - push(data: T) { - return new NodeZone(this._zone.with('apiZone', data)); - } - - pop() { - return new NodeZone(this._zone.without('apiZone')); - } - - run(func: () => R): R { - return this._zone.run(func); - } - - data(): T | undefined { - return this._zone.data('apiZone'); - } -} - -let boxedStackPrefixes: string[] = []; -export function setBoxedStackPrefixes(prefixes: string[]) { - boxedStackPrefixes = prefixes; -} - -export const nodePlatform: (coreDir: string) => Platform = coreDir => ({ - name: 'node', - - boxedStackPrefixes: () => { - if (process.env.PWDEBUGIMPL) - return []; - return [coreDir, ...boxedStackPrefixes]; - }, - - calculateSha1: (text: string) => { - const sha1 = crypto.createHash('sha1'); - sha1.update(text); - return Promise.resolve(sha1.digest('hex')); - }, - - colors, - - coreDir, - - createGuid: () => crypto.randomBytes(16).toString('hex'), - - defaultMaxListeners: () => EventEmitter.defaultMaxListeners, - fs: () => fs, - - env: process.env, - - inspectCustom: util.inspect.custom, - - isDebugMode: () => debugMode() === 'inspector', - - isJSDebuggerAttached: () => !!require('inspector').url(), - - isLogEnabled(name: 'api' | 'channel') { - return debugLogger.isEnabled(name); - }, - - isUnderTest: () => isUnderTest(), - - log(name: 'api' | 'channel', message: string | Error | object) { - debugLogger.log(name, message); - }, - - path: () => path, - - pathSeparator: path.sep, - - showInternalStackFrames: () => !!process.env.PWDEBUGIMPL, - - async streamFile(path: string, stream: Writable): Promise { - await pipelineAsync(fs.createReadStream(path), stream); - }, - - streamReadable: (channel: StreamChannel) => { - return new ReadableStreamImpl(channel); - }, - - streamWritable: (channel: WritableStreamChannel) => { - return new WritableStreamImpl(channel); - }, - - zones: { - current: () => new NodeZone(currentZone()), - empty: new NodeZone(emptyZone), - } -}); - -class ReadableStreamImpl extends Readable { - private _channel: StreamChannel; - - constructor(channel: StreamChannel) { - super(); - this._channel = channel; - } - - override async _read() { - const result = await this._channel.read({ size: 1024 * 1024 }); - if (result.binary.byteLength) - this.push(result.binary); - else - this.push(null); - } - - override _destroy(error: Error | null, callback: (error: Error | null | undefined) => void): void { - // Stream might be destroyed after the connection was closed. - this._channel.close().catch(e => null); - super._destroy(error, callback); - } -} - -class WritableStreamImpl extends Writable { - private _channel: WritableStreamChannel; - - constructor(channel: WritableStreamChannel) { - super(); - this._channel = channel; - } - - override async _write(chunk: Buffer | string, encoding: BufferEncoding, callback: (error?: Error | null) => void) { - const error = await this._channel.write({ binary: typeof chunk === 'string' ? Buffer.from(chunk) : chunk }).catch(e => e); - callback(error || null); - } - - override async _final(callback: (error?: Error | null) => void) { - // Stream might be destroyed after the connection was closed. - const error = await this._channel.close().catch(e => e); - callback(error || null); - } -} diff --git a/tests/library/events/utils.ts b/tests/library/events/utils.ts index c4f2d581c5b59..d63cbc68bdac3 100644 --- a/tests/library/events/utils.ts +++ b/tests/library/events/utils.ts @@ -20,9 +20,7 @@ // USE OR OTHER DEALINGS IN THE SOFTWARE. import { expect } from '@playwright/test'; -import { clientEventEmitter, utils } from '../../../packages/playwright-core/lib/coreBundle'; - -const { nodePlatform } = utils; +import { clientEventEmitter } from '../../../packages/playwright-core/lib/coreBundle'; export const mustNotCall = (msg?: string) => { return function mustNotCall() { @@ -51,6 +49,6 @@ export const mustCall = (fn?: Function, exact?: number) => { /** as any breaks long TS resolution chain that makes tests unhappy */ export class EventEmitter extends (clientEventEmitter as any) { constructor() { - super(nodePlatform(process.cwd())); + super(); } } diff --git a/tests/library/playwright-client.spec.ts b/tests/library/playwright-client.spec.ts new file mode 100644 index 0000000000000..567f8ab425506 --- /dev/null +++ b/tests/library/playwright-client.spec.ts @@ -0,0 +1,55 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import path from 'path'; + +import { playwrightTest as it, expect } from '../config/browserTest'; + +it.skip(({ mode }) => mode !== 'default'); + +const kBundlePath = path.join(__dirname, '..', '..', 'packages', 'playwright-client', 'lib', 'index.mjs'); + +it('should connect from a page and drive the same browser', async ({ browser, browserName, server }) => { + // Expose this very browser over a WebSocket endpoint. + const { endpoint } = await browser.bind('playwright-client-test', { port: 0 }); + + // Serve the built browser client bundle. + server.setRoute('/playwright-client.mjs', (req, res) => { + res.writeHead(200, { 'content-type': 'text/javascript' }); + res.end(fs.readFileSync(kBundlePath)); + }); + + // A page we keep a direct handle to — the in-page client will click its button. + server.setContent('/button.html', ``, 'text/html'); + const target = await browser.newPage(); + await target.goto(server.PREFIX + '/button.html'); + + // The host page loads the client bundle and connects back to this same browser. + const hostPage = await browser.newPage(); + await hostPage.goto(server.EMPTY_PAGE); + await hostPage.evaluate(async ({ bundleUrl, endpoint, browserName }) => { + const { connect } = await import(bundleUrl); + const remoteBrowser = await connect(endpoint, browserName, {}); + const page = remoteBrowser.contexts().flatMap(context => context.pages()).find(page => page.url().endsWith('/button.html')); + await page.click('button'); + }, { bundleUrl: server.PREFIX + '/playwright-client.mjs', endpoint, browserName }); + + // Verify directly through our own handle to the automated browser. + expect(await target.locator('button').textContent()).toBe('clicked by client'); + + await browser.unbind(); +}); diff --git a/utils/build/build.js b/utils/build/build.js index e748eca2529c2..e30ad620e7d6b 100644 --- a/utils/build/build.js +++ b/utils/build/build.js @@ -558,6 +558,40 @@ for (const pkg of workspace.packages()) { })); } +// @playwright/client — browser-targeted ESM bundle. The client tree is written +// isomorphically; the few genuine node builtins it still imports are swapped for +// browser stubs here (see packages/playwright-client/src/nodeStubs). esbuild's +// `platform: 'browser'` additionally fails the build if any other builtin leaks. +{ + const clientNodeStub = name => filePath(`packages/playwright-client/src/nodeStubs/${name}.ts`); + steps.push(new EsbuildStep({ + bundle: true, + format: 'esm', + platform: 'browser', + target: 'es2020', + entryPoints: [filePath('packages/playwright-client/src/index.ts')], + outfile: filePath('packages/playwright-client/lib/index.mjs'), + alias: { + 'fs': clientNodeStub('fs'), + 'path': clientNodeStub('path'), + 'stream': clientNodeStub('stream'), + 'util': clientNodeStub('util'), + 'inspector': clientNodeStub('inspector'), + 'async_hooks': clientNodeStub('async_hooks'), + 'events': clientNodeStub('events'), + // Vendored npm dep used for terminal colors; no-op in the browser. + 'colors/safe': clientNodeStub('colors'), + }, + // Provide a `process` global so isomorphic/@utils code that reads + // `process.env` works in the browser. + inject: [clientNodeStub('processShim')], + }, [ + filePath('packages/playwright-client/src'), + filePath('packages/playwright-core/src/client'), + filePath('packages/isomorphic'), + ])); +} + // Build playwright-core exported entry points. steps.push(new EsbuildStep({ entryPoints: [