chore: introduce platform for client (1) (#34683)

This commit is contained in:
Pavel Feldman 2025-02-10 10:22:32 -08:00 committed by GitHub
parent 0672f1ce67
commit 5d500dde22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
75 changed files with 499 additions and 381 deletions

View File

@ -7,7 +7,11 @@
[inProcessFactory.ts]
**
[inprocess.ts]
common/
[outofprocess.ts]
client/
protocol/
utils/
common/

View File

@ -4,6 +4,7 @@
../common
../debug/injected
../generated/
../server/
../server/injected/
../server/trace
../utils

View File

@ -22,7 +22,7 @@ import * as playwright from '../..';
import { PipeTransport } from '../protocol/transport';
import { PlaywrightServer } from '../remote/playwrightServer';
import { DispatcherConnection, PlaywrightDispatcher, RootDispatcher, createPlaywright } from '../server';
import { gracefullyProcessExitDoNotHang } from '../utils/processLauncher';
import { gracefullyProcessExitDoNotHang } from '../server/processLauncher';
import type { BrowserType } from '../client/browserType';
import type { LaunchServerOptions } from '../client/types';

View File

@ -21,11 +21,11 @@ import * as os from 'os';
import * as path from 'path';
import * as playwright from '../..';
import { registry, writeDockerVersion } from '../server';
import { launchBrowserServer, printApiJson, runDriver, runServer } from './driver';
import { isTargetClosedError } from '../client/errors';
import { gracefullyProcessExitDoNotHang, registry, writeDockerVersion } from '../server';
import { runTraceInBrowser, runTraceViewerApp } from '../server/trace/viewer/traceViewer';
import { assert, getPackageManagerExecCommand, gracefullyProcessExitDoNotHang, isLikelyNpxGlobal, wrapInASCIIBox } from '../utils';
import { assert, getPackageManagerExecCommand, isLikelyNpxGlobal, wrapInASCIIBox } from '../utils';
import { dotenv, program } from '../utilsBundle';
import type { Browser } from '../client/browser';

View File

@ -16,7 +16,8 @@
/* eslint-disable no-console */
import { getPackageManager, gracefullyProcessExitDoNotHang } from '../utils';
import { gracefullyProcessExitDoNotHang } from '../server';
import { getPackageManager } from '../utils';
import { program } from './program';
export { program } from './program';

View File

@ -15,9 +15,7 @@
*/
import { EventEmitter } from 'events';
import * as fs from 'fs';
import { isRegExp, isString, monotonicTime } from '../utils';
import { BrowserContext, prepareBrowserContextParams } from './browserContext';
import { ChannelOwner } from './channelOwner';
import { Connection } from './connection';
@ -25,12 +23,15 @@ import { TargetClosedError, isTargetClosedError } from './errors';
import { Events } from './events';
import { Waiter } from './waiter';
import { TimeoutSettings } from '../common/timeoutSettings';
import { isRegExp, isString } from '../utils/rtti';
import { monotonicTime } from '../utils/time';
import { raceAgainstDeadline } from '../utils/timeoutRunner';
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 '../common/platform';
import type * as channels from '@protocol/channels';
type Direction = 'down' | 'up' | 'left' | 'right';
@ -73,7 +74,7 @@ export class Android extends ChannelOwner<channels.AndroidChannel> implements ap
const connectParams: channels.LocalUtilsConnectParams = { wsEndpoint, headers, slowMo: options.slowMo, timeout: options.timeout };
const { pipe } = await localUtils._channel.connect(connectParams);
const closePipe = () => pipe.close().catch(() => {});
const connection = new Connection(localUtils, this._instrumentation);
const connection = new Connection(localUtils, this._platform, this._instrumentation);
connection.markAsRemote();
connection.on('close', closePipe);
@ -232,7 +233,7 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
async screenshot(options: { path?: string } = {}): Promise<Buffer> {
const { binary } = await this._channel.screenshot();
if (options.path)
await fs.promises.writeFile(options.path, binary);
await this._platform.fs().promises.writeFile(options.path, binary);
return binary;
}
@ -267,15 +268,15 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
}
async installApk(file: string | Buffer, options?: { args: string[] }): Promise<void> {
await this._channel.installApk({ file: await loadFile(file), args: options && options.args });
await this._channel.installApk({ file: await loadFile(this._platform, file), args: options && options.args });
}
async push(file: string | Buffer, path: string, options?: { mode: number }): Promise<void> {
await this._channel.push({ file: await loadFile(file), path, mode: options ? options.mode : undefined });
await this._channel.push({ file: await loadFile(this._platform, file), path, mode: options ? options.mode : undefined });
}
async launchBrowser(options: types.BrowserContextOptions & { pkg?: string } = {}): Promise<BrowserContext> {
const contextOptions = await prepareBrowserContextParams(options);
const contextOptions = await prepareBrowserContextParams(this._platform, options);
const result = await this._channel.launchBrowser(contextOptions);
const context = BrowserContext.from(result.context) as BrowserContext;
context._setOptions(contextOptions, {});
@ -321,9 +322,9 @@ export class AndroidSocket extends ChannelOwner<channels.AndroidSocketChannel> i
}
}
async function loadFile(file: string | Buffer): Promise<Buffer> {
async function loadFile(platform: Platform, file: string | Buffer): Promise<Buffer> {
if (isString(file))
return await fs.promises.readFile(file);
return await platform.fs().promises.readFile(file);
return file;
}

View File

@ -14,8 +14,6 @@
* limitations under the License.
*/
import * as fs from 'fs';
import { ChannelOwner } from './channelOwner';
import { Stream } from './stream';
import { mkdirIfNeeded } from '../utils/fileUtils';
@ -42,9 +40,9 @@ export class Artifact extends ChannelOwner<channels.ArtifactChannel> {
const result = await this._channel.saveAsStream();
const stream = Stream.from(result.stream);
await mkdirIfNeeded(path);
await mkdirIfNeeded(this._platform, path);
await new Promise((resolve, reject) => {
stream.stream().pipe(fs.createWriteStream(path))
stream.stream().pipe(this._platform.fs().createWriteStream(path))
.on('finish' as any, resolve)
.on('error' as any, reject);
});

View File

@ -14,15 +14,13 @@
* limitations under the License.
*/
import * as fs from 'fs';
import { Artifact } from './artifact';
import { BrowserContext, prepareBrowserContextParams } from './browserContext';
import { CDPSession } from './cdpSession';
import { ChannelOwner } from './channelOwner';
import { isTargetClosedError } from './errors';
import { Events } from './events';
import { mkdirIfNeeded } from '../utils';
import { mkdirIfNeeded } from '../utils/fileUtils';
import type { BrowserType } from './browserType';
import type { Page } from './page';
@ -83,7 +81,7 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
async _innerNewContext(options: BrowserContextOptions = {}, forReuse: boolean): Promise<BrowserContext> {
options = { ...this._browserType._playwright._defaultContextOptions, ...options };
const contextOptions = await prepareBrowserContextParams(options);
const contextOptions = await prepareBrowserContextParams(this._platform, options);
const response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions);
const context = BrowserContext.from(response.context);
await this._browserType._didCreateContext(context, contextOptions, this._options, options.logger || this._logger);
@ -126,8 +124,8 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
const buffer = await artifact.readIntoBuffer();
await artifact.delete();
if (this._path) {
await mkdirIfNeeded(this._path);
await fs.promises.writeFile(this._path, buffer);
await mkdirIfNeeded(this._platform, this._path);
await this._platform.fs().promises.writeFile(this._path, buffer);
this._path = undefined;
}
return buffer;

View File

@ -15,9 +15,6 @@
* limitations under the License.
*/
import * as fs from 'fs';
import * as path from 'path';
import { Artifact } from './artifact';
import { Browser } from './browser';
import { CDPSession } from './cdpSession';
@ -38,14 +35,18 @@ import { Waiter } from './waiter';
import { WebError } from './webError';
import { Worker } from './worker';
import { TimeoutSettings } from '../common/timeoutSettings';
import { headersObjectToArray, isRegExp, isString, mkdirIfNeeded, urlMatchesEqual } from '../utils';
import { mkdirIfNeeded } from '../utils/fileUtils';
import { headersObjectToArray } from '../utils/headers';
import { urlMatchesEqual } from '../utils/isomorphic/urlMatch';
import { isRegExp, isString } from '../utils/rtti';
import { rewriteErrorMessage } from '../utils/stackTrace';
import type { BrowserType } from './browserType';
import type { BrowserContextOptions, Headers, LaunchOptions, StorageState, WaitForEventOptions } from './types';
import type * as structs from '../../types/structs';
import type * as api from '../../types/types';
import type { URLMatch } from '../utils';
import type { Platform } from '../common/platform';
import type { URLMatch } from '../utils/isomorphic/urlMatch';
import type * as channels from '@protocol/channels';
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel> implements api.BrowserContext {
@ -107,7 +108,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
this.emit(Events.BrowserContext.ServiceWorker, serviceWorker);
});
this._channel.on('console', event => {
const consoleMessage = new ConsoleMessage(event);
const consoleMessage = new ConsoleMessage(this._platform, event);
this.emit(Events.BrowserContext.Console, consoleMessage);
const page = consoleMessage.page();
if (page)
@ -321,7 +322,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
}
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any): Promise<void> {
const source = await evaluationScript(script, arg);
const source = await evaluationScript(this._platform, script, arg);
await this._channel.addInitScript({ source });
}
@ -431,8 +432,8 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
async storageState(options: { path?: string, indexedDB?: boolean } = {}): Promise<StorageState> {
const state = await this._channel.storageState({ indexedDB: options.indexedDB });
if (options.path) {
await mkdirIfNeeded(options.path);
await fs.promises.writeFile(options.path, JSON.stringify(state, undefined, 2), 'utf8');
await mkdirIfNeeded(this._platform, options.path);
await this._platform.fs().promises.writeFile(options.path, JSON.stringify(state, undefined, 2), 'utf8');
}
return state;
}
@ -500,11 +501,11 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
}
}
async function prepareStorageState(options: BrowserContextOptions): Promise<channels.BrowserNewContextParams['storageState']> {
async function prepareStorageState(platform: Platform, options: BrowserContextOptions): Promise<channels.BrowserNewContextParams['storageState']> {
if (typeof options.storageState !== 'string')
return options.storageState;
try {
return JSON.parse(await fs.promises.readFile(options.storageState, 'utf8'));
return JSON.parse(await platform.fs().promises.readFile(options.storageState, 'utf8'));
} catch (e) {
rewriteErrorMessage(e, `Error reading storage state from ${options.storageState}:\n` + e.message);
throw e;
@ -524,7 +525,7 @@ function prepareRecordHarOptions(options: BrowserContextOptions['recordHar']): c
};
}
export async function prepareBrowserContextParams(options: BrowserContextOptions): Promise<channels.BrowserNewContextParams> {
export async function prepareBrowserContextParams(platform: Platform, options: BrowserContextOptions): Promise<channels.BrowserNewContextParams> {
if (options.videoSize && !options.videosPath)
throw new Error(`"videoSize" option requires "videosPath" to be specified`);
if (options.extraHTTPHeaders)
@ -534,7 +535,7 @@ export async function prepareBrowserContextParams(options: BrowserContextOptions
viewport: options.viewport === null ? undefined : options.viewport,
noDefaultViewport: options.viewport === null,
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined,
storageState: await prepareStorageState(options),
storageState: await prepareStorageState(platform, options),
serviceWorkers: options.serviceWorkers,
recordHar: prepareRecordHarOptions(options.recordHar),
colorScheme: options.colorScheme === null ? 'no-override' : options.colorScheme,
@ -542,7 +543,7 @@ export async function prepareBrowserContextParams(options: BrowserContextOptions
forcedColors: options.forcedColors === null ? 'no-override' : options.forcedColors,
contrast: options.contrast === null ? 'no-override' : options.contrast,
acceptDownloads: toAcceptDownloadsProtocol(options.acceptDownloads),
clientCertificates: await toClientCertificatesProtocol(options.clientCertificates),
clientCertificates: await toClientCertificatesProtocol(platform, options.clientCertificates),
};
if (!contextParams.recordVideo && options.videosPath) {
contextParams.recordVideo = {
@ -551,7 +552,7 @@ export async function prepareBrowserContextParams(options: BrowserContextOptions
};
}
if (contextParams.recordVideo && contextParams.recordVideo.dir)
contextParams.recordVideo.dir = path.resolve(process.cwd(), contextParams.recordVideo.dir);
contextParams.recordVideo.dir = platform.path().resolve(process.cwd(), contextParams.recordVideo.dir);
return contextParams;
}
@ -563,7 +564,7 @@ function toAcceptDownloadsProtocol(acceptDownloads?: boolean) {
return 'deny';
}
export async function toClientCertificatesProtocol(certs?: BrowserContextOptions['clientCertificates']): Promise<channels.PlaywrightNewRequestParams['clientCertificates']> {
export async function toClientCertificatesProtocol(platform: Platform, certs?: BrowserContextOptions['clientCertificates']): Promise<channels.PlaywrightNewRequestParams['clientCertificates']> {
if (!certs)
return undefined;
@ -571,7 +572,7 @@ export async function toClientCertificatesProtocol(certs?: BrowserContextOptions
if (value)
return value;
if (path)
return await fs.promises.readFile(path);
return await platform.fs().promises.readFile(path);
};
return await Promise.all(certs.map(async cert => ({

View File

@ -20,7 +20,9 @@ import { ChannelOwner } from './channelOwner';
import { envObjectToArray } from './clientHelper';
import { Connection } from './connection';
import { Events } from './events';
import { assert, headersObjectToArray, monotonicTime } from '../utils';
import { assert } from '../utils/debug';
import { headersObjectToArray } from '../utils/headers';
import { monotonicTime } from '../utils/time';
import { raceAgainstDeadline } from '../utils/timeoutRunner';
import type { Playwright } from './playwright';
@ -90,7 +92,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
const logger = options.logger || this._playwright._defaultLaunchOptions?.logger;
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
options = { ...this._playwright._defaultLaunchOptions, ...this._playwright._defaultContextOptions, ...options };
const contextParams = await prepareBrowserContextParams(options);
const contextParams = await prepareBrowserContextParams(this._platform, options);
const persistentParams: channels.BrowserTypeLaunchPersistentContextParams = {
...contextParams,
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined,
@ -133,7 +135,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
connectParams.socksProxyRedirectPortForTest = (params as any).__testHookRedirectPortForwarding;
const { pipe, headers: connectHeaders } = await localUtils._channel.connect(connectParams);
const closePipe = () => pipe.close().catch(() => {});
const connection = new Connection(localUtils, this._instrumentation);
const connection = new Connection(localUtils, this._platform, this._instrumentation);
connection.markAsRemote();
connection.on('close', closePipe);

View File

@ -16,7 +16,7 @@
import { EventEmitter } from './eventEmitter';
import { ValidationError, maybeFindValidator } from '../protocol/validator';
import { isUnderTest } from '../utils';
import { isUnderTest } from '../utils/debug';
import { debugLogger } from '../utils/debugLogger';
import { captureLibraryStackTrace, stringifyStackFrames } from '../utils/stackTrace';
import { zones } from '../utils/zones';
@ -24,6 +24,7 @@ import { zones } from '../utils/zones';
import type { ClientInstrumentation } from './clientInstrumentation';
import type { Connection } from './connection';
import type { Logger } from './types';
import type { Platform } from '../common/platform';
import type { ValidatorContext } from '../protocol/validator';
import type * as channels from '@protocol/channels';
@ -39,6 +40,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
readonly _channel: T;
readonly _initializer: channels.InitializerTraits<T>;
_logger: Logger | undefined;
readonly _platform: Platform;
readonly _instrumentation: ClientInstrumentation;
private _eventToSubscriptionMapping: Map<string, string> = new Map();
private _isInternalType = false;
@ -52,6 +54,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
this._guid = guid;
this._parent = parent instanceof ChannelOwner ? parent : undefined;
this._instrumentation = this._connection._instrumentation;
this._platform = this._connection.platform;
this._connection._objects.set(guid, this);
if (this._parent) {

View File

@ -15,11 +15,10 @@
* limitations under the License.
*/
import * as fs from 'fs';
import { isString } from '../utils';
import { isString } from '../utils/rtti';
import type * as types from './types';
import type { Platform } from '../common/platform';
export function envObjectToArray(env: types.Env): { name: string, value: string }[] {
const result: { name: string, value: string }[] = [];
@ -30,7 +29,7 @@ export function envObjectToArray(env: types.Env): { name: string, value: string
return result;
}
export async function evaluationScript(fun: Function | string | { path?: string, content?: string }, arg?: any, addSourceUrl: boolean = true): Promise<string> {
export async function evaluationScript(platform: Platform, fun: Function | string | { path?: string, content?: string }, arg?: any, addSourceUrl: boolean = true): Promise<string> {
if (typeof fun === 'function') {
const source = fun.toString();
const argString = Object.is(arg, undefined) ? 'undefined' : JSON.stringify(arg);
@ -43,7 +42,7 @@ export async function evaluationScript(fun: Function | string | { path?: string,
if (fun.content !== undefined)
return fun.content;
if (fun.path !== undefined) {
let source = await fs.promises.readFile(fun.path, 'utf8');
let source = await platform.fs().promises.readFile(fun.path, 'utf8');
if (addSourceUrl)
source = addSourceUrlToScript(source, fun.path);
return source;

View File

@ -42,10 +42,12 @@ import { Tracing } from './tracing';
import { Worker } from './worker';
import { WritableStream } from './writableStream';
import { ValidationError, findValidator } from '../protocol/validator';
import { formatCallLog, rewriteErrorMessage, zones } from '../utils';
import { debugLogger } from '../utils/debugLogger';
import { formatCallLog, rewriteErrorMessage } from '../utils/stackTrace';
import { zones } from '../utils/zones';
import type { ClientInstrumentation } from './clientInstrumentation';
import type { Platform } from '../common/platform';
import type { ValidatorContext } from '../protocol/validator';
import type * as channels from '@protocol/channels';
@ -78,11 +80,13 @@ export class Connection extends EventEmitter {
toImpl: ((client: ChannelOwner) => any) | undefined;
private _tracingCount = 0;
readonly _instrumentation: ClientInstrumentation;
readonly platform: Platform;
constructor(localUtils: LocalUtils | undefined, instrumentation: ClientInstrumentation | undefined) {
constructor(localUtils: LocalUtils | undefined, platform: Platform, instrumentation: ClientInstrumentation | undefined) {
super();
this._instrumentation = instrumentation || createInstrumentation();
this._localUtils = localUtils;
this.platform = platform;
this._rootObject = new Root(this);
}

View File

@ -14,12 +14,11 @@
* limitations under the License.
*/
import * as util from 'util';
import { JSHandle } from './jsHandle';
import { Page } from './page';
import type * as api from '../../types/types';
import type { Platform } from '../common/platform';
import type * as channels from '@protocol/channels';
type ConsoleMessageLocation = channels.BrowserContextConsoleEvent['location'];
@ -29,9 +28,11 @@ export class ConsoleMessage implements api.ConsoleMessage {
private _page: Page | null;
private _event: channels.BrowserContextConsoleEvent | channels.ElectronApplicationConsoleEvent;
constructor(event: channels.BrowserContextConsoleEvent | channels.ElectronApplicationConsoleEvent) {
constructor(platform: Platform, event: channels.BrowserContextConsoleEvent | channels.ElectronApplicationConsoleEvent) {
this._page = ('page' in event && event.page) ? Page.from(event.page) : null;
this._event = event;
if (platform.inspectCustom)
(this as any)[platform.inspectCustom] = () => this._inspect();
}
page() {
@ -54,7 +55,7 @@ export class ConsoleMessage implements api.ConsoleMessage {
return this._event.location;
}
[util.inspect.custom]() {
private _inspect() {
return this.text();
}
}

View File

@ -53,7 +53,7 @@ export class Electron extends ChannelOwner<channels.ElectronChannel> implements
async launch(options: ElectronOptions = {}): Promise<ElectronApplication> {
const params: channels.ElectronLaunchParams = {
...await prepareBrowserContextParams(options),
...await prepareBrowserContextParams(this._platform, options),
env: envObjectToArray(options.env ? options.env : process.env),
tracesDir: options.tracesDir,
};
@ -81,7 +81,7 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati
this._channel.on('close', () => {
this.emit(Events.ElectronApplication.Close);
});
this._channel.on('console', event => this.emit(Events.ElectronApplication.Console, new ConsoleMessage(event)));
this._channel.on('console', event => this.emit(Events.ElectronApplication.Console, new ConsoleMessage(this._platform, event)));
this._setEventToSubscriptionMapping(new Map<string, channels.ElectronApplicationUpdateSubscriptionParams['event']>([
[Events.ElectronApplication.Console, 'console'],
]));

View File

@ -14,15 +14,14 @@
* limitations under the License.
*/
import * as fs from 'fs';
import * as path from 'path';
import { pipeline } from 'stream';
import { promisify } from 'util';
import { Frame } from './frame';
import { JSHandle, parseResult, serializeArgument } from './jsHandle';
import { assert, isString } from '../utils';
import { assert } from '../utils/debug';
import { fileUploadSizeLimit, mkdirIfNeeded } from '../utils/fileUtils';
import { isString } from '../utils/rtti';
import { mime } from '../utilsBundle';
import { WritableStream } from './writableStream';
@ -32,6 +31,7 @@ import type { Locator } from './locator';
import type { FilePayload, Rect, SelectOption, SelectOptionOptions } from './types';
import type * as structs from '../../types/structs';
import type * as api from '../../types/types';
import type { Platform } from '../common/platform';
import type * as channels from '@protocol/channels';
const pipelineAsync = promisify(pipeline);
@ -156,7 +156,7 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> implements
const frame = await this.ownerFrame();
if (!frame)
throw new Error('Cannot set input files to detached element');
const converted = await convertInputFiles(files, frame.page().context());
const converted = await convertInputFiles(this._platform, files, frame.page().context());
await this._elementChannel.setInputFiles({ ...converted, ...options });
}
@ -204,8 +204,8 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> implements
}
const result = await this._elementChannel.screenshot(copy);
if (options.path) {
await mkdirIfNeeded(options.path);
await fs.promises.writeFile(options.path, result.binary);
await mkdirIfNeeded(this._platform, options.path);
await this._platform.fs().promises.writeFile(options.path, result.binary);
}
return result.binary;
}
@ -263,18 +263,18 @@ function filePayloadExceedsSizeLimit(payloads: FilePayload[]) {
return payloads.reduce((size, item) => size + (item.buffer ? item.buffer.byteLength : 0), 0) >= fileUploadSizeLimit;
}
async function resolvePathsAndDirectoryForInputFiles(items: string[]): Promise<[string[] | undefined, string | undefined]> {
async function resolvePathsAndDirectoryForInputFiles(platform: Platform, items: string[]): Promise<[string[] | undefined, string | undefined]> {
let localPaths: string[] | undefined;
let localDirectory: string | undefined;
for (const item of items) {
const stat = await fs.promises.stat(item as string);
const stat = await platform.fs().promises.stat(item as string);
if (stat.isDirectory()) {
if (localDirectory)
throw new Error('Multiple directories are not supported');
localDirectory = path.resolve(item as string);
localDirectory = platform.path().resolve(item as string);
} else {
localPaths ??= [];
localPaths.push(path.resolve(item as string));
localPaths.push(platform.path().resolve(item as string));
}
}
if (localPaths?.length && localDirectory)
@ -282,30 +282,30 @@ async function resolvePathsAndDirectoryForInputFiles(items: string[]): Promise<[
return [localPaths, localDirectory];
}
export async function convertInputFiles(files: string | FilePayload | string[] | FilePayload[], context: BrowserContext): Promise<SetInputFilesFiles> {
export async function convertInputFiles(platform: Platform, files: string | FilePayload | string[] | FilePayload[], context: BrowserContext): Promise<SetInputFilesFiles> {
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(items);
const [localPaths, localDirectory] = await resolvePathsAndDirectoryForInputFiles(platform, items);
if (context._connection.isRemote()) {
const files = localDirectory ? (await fs.promises.readdir(localDirectory, { withFileTypes: true, recursive: true })).filter(f => f.isFile()).map(f => path.join(f.path, f.name)) : localPaths!;
const files = localDirectory ? (await platform.fs().promises.readdir(localDirectory, { withFileTypes: true, recursive: true })).filter(f => f.isFile()).map(f => platform.path().join(f.path, f.name)) : localPaths!;
const { writableStreams, rootDir } = await context._wrapApiCall(async () => context._channel.createTempFiles({
rootDirName: localDirectory ? path.basename(localDirectory) : undefined,
rootDirName: localDirectory ? platform.path().basename(localDirectory) : undefined,
items: await Promise.all(files.map(async file => {
const lastModifiedMs = (await fs.promises.stat(file)).mtimeMs;
const lastModifiedMs = (await platform.fs().promises.stat(file)).mtimeMs;
return {
name: localDirectory ? path.relative(localDirectory, file) : path.basename(file),
name: localDirectory ? platform.path().relative(localDirectory, file) : platform.path().basename(file),
lastModifiedMs
};
})),
}), true);
for (let i = 0; i < files.length; i++) {
const writable = WritableStream.from(writableStreams[i]);
await pipelineAsync(fs.createReadStream(files[i]), writable.stream());
await pipelineAsync(platform.fs().createReadStream(files[i]), writable.stream());
}
return {
directoryStream: rootDir,

View File

@ -15,7 +15,7 @@
*/
import { parseSerializedValue, serializeValue } from '../protocol/serializers';
import { isError } from '../utils';
import { isError } from '../utils/rtti';
import type { SerializedError } from '@protocol/channels';

View File

@ -24,7 +24,7 @@
import { EventEmitter as OriginalEventEmitter } from 'events';
import { isUnderTest } from '../utils';
import { isUnderTest } from '../utils/debug';
import type { EventEmitter as EventEmitterType } from 'events';

View File

@ -14,24 +14,24 @@
* limitations under the License.
*/
import * as fs from 'fs';
import * as path from 'path';
import * as util from 'util';
import { assert, headersObjectToArray, isString } from '../utils';
import { toClientCertificatesProtocol } from './browserContext';
import { ChannelOwner } from './channelOwner';
import { TargetClosedError, isTargetClosedError } from './errors';
import { RawHeaders } from './network';
import { Tracing } from './tracing';
import { assert } from '../utils/debug';
import { mkdirIfNeeded } from '../utils/fileUtils';
import { headersObjectToArray } from '../utils/headers';
import { isString } from '../utils/rtti';
import type { Playwright } from './playwright';
import type { ClientCertificate, FilePayload, Headers, SetStorageState, StorageState } from './types';
import type { Serializable } from '../../types/structs';
import type * as api from '../../types/types';
import type { Platform } from '../common/platform';
import type { HeadersArray, NameValue } from '../common/types';
import type * as channels from '@protocol/channels';
import type * as fs from 'fs';
export type FetchOptions = {
params?: { [key: string]: string | number | boolean; } | URLSearchParams | string,
@ -70,14 +70,14 @@ export class APIRequest implements api.APIRequest {
...options,
};
const storageState = typeof options.storageState === 'string' ?
JSON.parse(await fs.promises.readFile(options.storageState, 'utf8')) :
JSON.parse(await this._playwright._platform.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(options.clientCertificates),
clientCertificates: await toClientCertificatesProtocol(this._playwright._platform, options.clientCertificates),
})).request);
this._contexts.add(context);
context._request = this;
@ -232,7 +232,7 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
} else {
// Convert file-like values to ServerFilePayload structs.
for (const [name, value] of Object.entries(options.multipart))
multipartData.push(await toFormField(name, value));
multipartData.push(await toFormField(this._platform, name, value));
}
}
if (postDataBuffer === undefined && jsonData === undefined && formData === undefined && multipartData === undefined)
@ -264,23 +264,24 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
async storageState(options: { path?: string, indexedDB?: boolean } = {}): Promise<StorageState> {
const state = await this._channel.storageState({ indexedDB: options.indexedDB });
if (options.path) {
await mkdirIfNeeded(options.path);
await fs.promises.writeFile(options.path, JSON.stringify(state, undefined, 2), 'utf8');
await mkdirIfNeeded(this._platform, options.path);
await this._platform.fs().promises.writeFile(options.path, JSON.stringify(state, undefined, 2), 'utf8');
}
return state;
}
}
async function toFormField(name: string, value: string|number|boolean|fs.ReadStream|FilePayload): Promise<channels.FormField> {
async function toFormField(platform: Platform, name: string, value: string | number | boolean | fs.ReadStream | FilePayload): Promise<channels.FormField> {
const typeOfValue = typeof value;
if (isFilePayload(value)) {
const payload = value as FilePayload;
if (!Buffer.isBuffer(payload.buffer))
throw new Error(`Unexpected buffer type of 'data.${name}'`);
return { name, file: filePayloadToJson(payload) };
} else if (value instanceof fs.ReadStream) {
return { name, file: await readStreamToJson(value as fs.ReadStream) };
} else {
} else if (typeOfValue === 'string' || typeOfValue === 'number' || typeOfValue === 'boolean') {
return { name, value: String(value) };
} else {
return { name, file: await readStreamToJson(platform, value as fs.ReadStream) };
}
}
@ -307,6 +308,9 @@ export class APIResponse implements api.APIResponse {
this._request = context;
this._initializer = initializer;
this._headers = new RawHeaders(this._initializer.headers);
if (context._platform.inspectCustom)
(this as any)[context._platform.inspectCustom] = () => this._inspect();
}
ok(): boolean {
@ -364,7 +368,7 @@ export class APIResponse implements api.APIResponse {
await this._request._channel.disposeAPIResponse({ fetchUid: this._fetchUid() });
}
[util.inspect.custom]() {
private _inspect() {
const headers = this.headersArray().map(({ name, value }) => ` ${name}: ${value}`);
return `APIResponse: ${this.status()} ${this.statusText()}\n${headers.join('\n')}`;
}
@ -389,7 +393,7 @@ function filePayloadToJson(payload: FilePayload): ServerFilePayload {
};
}
async function readStreamToJson(stream: fs.ReadStream): Promise<ServerFilePayload> {
async function readStreamToJson(platform: Platform, stream: fs.ReadStream): Promise<ServerFilePayload> {
const buffer = await new Promise<Buffer>((resolve, reject) => {
const chunks: Buffer[] = [];
stream.on('data', chunk => chunks.push(chunk as Buffer));
@ -398,7 +402,7 @@ async function readStreamToJson(stream: fs.ReadStream): Promise<ServerFilePayloa
});
const streamPath: string = Buffer.isBuffer(stream.path) ? stream.path.toString('utf8') : stream.path;
return {
name: path.basename(streamPath),
name: platform.path().basename(streamPath),
buffer,
};
}

View File

@ -16,28 +16,27 @@
*/
import { EventEmitter } from 'events';
import * as fs from 'fs';
import { ChannelOwner } from './channelOwner';
import { FrameLocator, Locator, testIdAttributeName } from './locator';
import { assert } from '../utils';
import { urlMatches } from '../utils';
import { addSourceUrlToScript } from './clientHelper';
import { ElementHandle, convertInputFiles, convertSelectOptionValues } from './elementHandle';
import { Events } from './events';
import { JSHandle, assertMaxArguments, parseResult, serializeArgument } from './jsHandle';
import { FrameLocator, Locator, testIdAttributeName } from './locator';
import * as network from './network';
import { kLifecycleEvents } from './types';
import { Waiter } from './waiter';
import { assert } from '../utils/debug';
import { getByAltTextSelector, getByLabelSelector, getByPlaceholderSelector, getByRoleSelector, getByTestIdSelector, getByTextSelector, getByTitleSelector } from '../utils/isomorphic/locatorUtils';
import { urlMatches } from '../utils/isomorphic/urlMatch';
import type { LocatorOptions } from './locator';
import type { Page } from './page';
import type { FilePayload, LifecycleEvent, SelectOption, SelectOptionOptions, StrictOptions, WaitForFunctionOptions } from './types';
import type * as structs from '../../types/structs';
import type * as api from '../../types/types';
import type { URLMatch } from '../utils';
import type { ByRoleOptions } from '../utils/isomorphic/locatorUtils';
import type { URLMatch } from '../utils/isomorphic/urlMatch';
import type * as channels from '@protocol/channels';
export type WaitForNavigationOptions = {
@ -269,7 +268,7 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
async addScriptTag(options: { url?: string, path?: string, content?: string, type?: string } = {}): Promise<ElementHandle> {
const copy = { ...options };
if (copy.path) {
copy.content = (await fs.promises.readFile(copy.path)).toString();
copy.content = (await this._platform.fs().promises.readFile(copy.path)).toString();
copy.content = addSourceUrlToScript(copy.content, copy.path);
}
return ElementHandle.from((await this._channel.addScriptTag({ ...copy })).element);
@ -278,7 +277,7 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
async addStyleTag(options: { url?: string; path?: string; content?: string; } = {}): Promise<ElementHandle> {
const copy = { ...options };
if (copy.path) {
copy.content = (await fs.promises.readFile(copy.path)).toString();
copy.content = (await this._platform.fs().promises.readFile(copy.path)).toString();
copy.content += '/*# sourceURL=' + copy.path.replace(/\n/g, '') + '*/';
}
return ElementHandle.from((await this._channel.addStyleTag({ ...copy })).element);
@ -403,7 +402,7 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
}
async setInputFiles(selector: string, files: string | FilePayload | string[] | FilePayload[], options: channels.FrameSetInputFilesOptions = {}): Promise<void> {
const converted = await convertInputFiles(files, this.page().context());
const converted = await convertInputFiles(this._platform, files, this.page().context());
await this._channel.setInputFiles({ selector, ...converted, ...options });
}

View File

@ -19,8 +19,8 @@ import { debugLogger } from '../utils/debugLogger';
import type { BrowserContext } from './browserContext';
import type { LocalUtils } from './localUtils';
import type { Route } from './network';
import type { URLMatch } from '../utils';
import type { Page } from './page';
import type { URLMatch } from '../utils/isomorphic/urlMatch';
type HarNotFoundAction = 'abort' | 'fallback';

View File

@ -14,13 +14,13 @@
* limitations under the License.
*/
import * as util from 'util';
import { asLocator, isString, monotonicTime } from '../utils';
import { ElementHandle } from './elementHandle';
import { parseResult, serializeArgument } from './jsHandle';
import { asLocator } from '../utils/isomorphic/locatorGenerators';
import { getByAltTextSelector, getByLabelSelector, getByPlaceholderSelector, getByRoleSelector, getByTestIdSelector, getByTextSelector, getByTitleSelector } from '../utils/isomorphic/locatorUtils';
import { escapeForTextSelector } from '../utils/isomorphic/stringUtils';
import { isString } from '../utils/rtti';
import { monotonicTime } from '../utils/time';
import type { Frame } from './frame';
import type { FilePayload, FrameExpectParams, Rect, SelectOption, SelectOptionOptions, TimeoutOptions } from './types';
@ -64,6 +64,9 @@ export class Locator implements api.Locator {
throw new Error(`Inner "hasNot" locator must belong to the same frame.`);
this._selector += ` >> internal:has-not=` + JSON.stringify(locator._selector);
}
if (this._frame._platform.inspectCustom)
(this as any)[this._frame._platform.inspectCustom] = () => this._inspect();
}
private async _withElement<R>(task: (handle: ElementHandle<SVGElement | HTMLElement>, timeout?: number) => Promise<R>, timeout?: number): Promise<R> {
@ -291,8 +294,9 @@ export class Locator implements api.Locator {
return await this._frame.press(this._selector, key, { strict: true, ...options });
}
async screenshot(options: Omit<channels.ElementHandleScreenshotOptions, 'mask'> & { path?: string, mask?: Locator[] } = {}): Promise<Buffer> {
return await this._withElement((h, timeout) => h.screenshot({ ...options, timeout }), options.timeout);
async screenshot(options: Omit<channels.ElementHandleScreenshotOptions, 'mask'> & { path?: string, mask?: api.Locator[] } = {}): Promise<Buffer> {
const mask = options.mask as Locator[] | undefined;
return await this._withElement((h, timeout) => h.screenshot({ ...options, mask, timeout }), options.timeout);
}
async ariaSnapshot(options?: { _id?: boolean, _mode?: 'raw' | 'regex' } & TimeoutOptions): Promise<string> {
@ -370,7 +374,7 @@ export class Locator implements api.Locator {
return result;
}
[util.inspect.custom]() {
private _inspect() {
return this.toString();
}

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
import * as fs from 'fs';
import { URLSearchParams } from 'url';
import { ChannelOwner } from './channelOwner';
@ -22,19 +21,26 @@ import { isTargetClosedError } from './errors';
import { Events } from './events';
import { APIResponse } from './fetch';
import { Frame } from './frame';
import { Worker } from './worker';
import { MultiMap, assert, headersObjectToArray, isRegExp, isString, rewriteErrorMessage, urlMatches, zones } from '../utils';
import { Waiter } from './waiter';
import { Worker } from './worker';
import { assert } from '../utils/debug';
import { headersObjectToArray } from '../utils/headers';
import { urlMatches } from '../utils/isomorphic/urlMatch';
import { LongStandingScope, ManualPromise } from '../utils/manualPromise';
import { MultiMap } from '../utils/multimap';
import { isRegExp, isString } from '../utils/rtti';
import { rewriteErrorMessage } from '../utils/stackTrace';
import { zones } from '../utils/zones';
import { mime } from '../utilsBundle';
import type { Headers, RemoteAddr, SecurityDetails, WaitForEventOptions } from './types';
import type { URLMatch, Zone } from '../utils';
import type { BrowserContext } from './browserContext';
import type { Page } from './page';
import type { Headers, RemoteAddr, SecurityDetails, WaitForEventOptions } from './types';
import type { Serializable } from '../../types/structs';
import type * as api from '../../types/types';
import type { HeadersArray } from '../common/types';
import type { URLMatch } from '../utils/isomorphic/urlMatch';
import type { Zone } from '../utils/zones';
import type * as channels from '@protocol/channels';
export type NetworkCookie = {
@ -387,7 +393,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
let isBase64 = false;
let length = 0;
if (options.path) {
const buffer = await fs.promises.readFile(options.path);
const buffer = await this._platform.fs().promises.readFile(options.path);
body = buffer.toString('base64');
isBase64 = true;
length = buffer.length;

View File

@ -15,12 +15,6 @@
* limitations under the License.
*/
import * as fs from 'fs';
import * as path from 'path';
import { TargetClosedError, isTargetClosedError, serializeError } from './errors';
import { TimeoutSettings } from '../common/timeoutSettings';
import { LongStandingScope, assert, headersObjectToArray, isObject, isRegExp, isString, mkdirIfNeeded, trimStringWithEllipsis, urlMatches, urlMatchesEqual } from '../utils';
import { Accessibility } from './accessibility';
import { Artifact } from './artifact';
import { ChannelOwner } from './channelOwner';
@ -28,16 +22,25 @@ import { evaluationScript } from './clientHelper';
import { Coverage } from './coverage';
import { Download } from './download';
import { ElementHandle, determineScreenshotType } from './elementHandle';
import { TargetClosedError, isTargetClosedError, serializeError } from './errors';
import { Events } from './events';
import { FileChooser } from './fileChooser';
import { Frame, verifyLoadState } from './frame';
import { HarRouter } from './harRouter';
import { Keyboard, Mouse, Touchscreen } from './input';
import { JSHandle, assertMaxArguments, parseResult, serializeArgument } from './jsHandle';
import { Response, Route, RouteHandler, WebSocket, WebSocketRoute, WebSocketRouteHandler, validateHeaders } from './network';
import { Response, Route, RouteHandler, WebSocket, WebSocketRoute, WebSocketRouteHandler, validateHeaders } from './network';
import { Video } from './video';
import { Waiter } from './waiter';
import { Worker } from './worker';
import { TimeoutSettings } from '../common/timeoutSettings';
import { assert } from '../utils/debug';
import { mkdirIfNeeded } from '../utils/fileUtils';
import { headersObjectToArray } from '../utils/headers';
import { trimStringWithEllipsis } from '../utils/isomorphic/stringUtils';
import { urlMatches, urlMatchesEqual } from '../utils/isomorphic/urlMatch';
import { LongStandingScope } from '../utils/manualPromise';
import { isObject, isRegExp, isString } from '../utils/rtti';
import type { BrowserContext } from './browserContext';
import type { Clock } from './clock';
@ -48,8 +51,8 @@ import type { Request, RouteHandlerCallback, WebSocketRouteHandlerCallback } fro
import type { FilePayload, Headers, LifecycleEvent, SelectOption, SelectOptionOptions, Size, WaitForEventOptions, WaitForFunctionOptions } from './types';
import type * as structs from '../../types/structs';
import type * as api from '../../types/types';
import type { URLMatch } from '../utils';
import type { ByRoleOptions } from '../utils/isomorphic/locatorUtils';
import type { URLMatch } from '../utils/isomorphic/urlMatch';
import type * as channels from '@protocol/channels';
type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> & {
@ -512,7 +515,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
}
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) {
const source = await evaluationScript(script, arg);
const source = await evaluationScript(this._platform, script, arg);
await this._channel.addInitScript({ source });
}
@ -590,8 +593,8 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
}
const result = await this._channel.screenshot(copy);
if (options.path) {
await mkdirIfNeeded(options.path);
await fs.promises.writeFile(options.path, result.binary);
await mkdirIfNeeded(this._platform, options.path);
await this._platform.fs().promises.writeFile(options.path, result.binary);
}
return result.binary;
}
@ -820,8 +823,9 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
}
const result = await this._channel.pdf(transportOptions);
if (options.path) {
await fs.promises.mkdir(path.dirname(options.path), { recursive: true });
await fs.promises.writeFile(options.path, result.pdf);
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);
}
return result.pdf;
}

View File

@ -17,6 +17,7 @@
import { ChannelOwner } from './channelOwner';
import { evaluationScript } from './clientHelper';
import { setTestIdAttribute, testIdAttributeName } from './locator';
import { nodePlatform } from '../common/platform';
import type { SelectorEngine } from './types';
import type * as api from '../../types/types';
@ -28,7 +29,7 @@ export class Selectors implements api.Selectors {
private _registrations: channels.SelectorsRegisterParams[] = [];
async register(name: string, script: string | (() => SelectorEngine) | { path?: string, content?: string }, options: { contentScript?: boolean } = {}): Promise<void> {
const source = await evaluationScript(script, undefined, false);
const source = await evaluationScript(nodePlatform, script, undefined, false);
const params = { ...options, name, source };
for (const channel of this._channels)
await channel._channel.register(params);

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { ManualPromise } from '../utils';
import { ManualPromise } from '../utils/manualPromise';
import type { Artifact } from './artifact';
import type { Connection } from './connection';

View File

@ -15,11 +15,12 @@
*/
import { TimeoutError } from './errors';
import { createGuid, zones } from '../utils';
import { createGuid } from '../utils/crypto';
import { rewriteErrorMessage } from '../utils/stackTrace';
import { zones } from '../utils/zones';
import type { Zone } from '../utils';
import type { ChannelOwner } from './channelOwner';
import type { Zone } from '../utils/zones';
import type * as channels from '@protocol/channels';
import type { EventEmitter } from 'events';

View File

@ -15,10 +15,10 @@
*/
import { ChannelOwner } from './channelOwner';
import { TargetClosedError } from './errors';
import { Events } from './events';
import { JSHandle, assertMaxArguments, parseResult, serializeArgument } from './jsHandle';
import { LongStandingScope } from '../utils';
import { TargetClosedError } from './errors';
import { LongStandingScope } from '../utils/manualPromise';
import type { BrowserContext } from './browserContext';
import type { Page } from './page';

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as fs from 'fs';
import * as path from 'path';
import * as util from 'util';
export type Platform = {
fs: () => typeof fs;
path: () => typeof path;
inspectCustom: symbol | undefined;
};
export const emptyPlatform: Platform = {
fs: () => {
throw new Error('File system is not available');
},
path: () => {
throw new Error('Path module is not available');
},
inspectCustom: undefined,
};
export const nodePlatform: Platform = {
fs: () => fs,
path: () => path,
inspectCustom: util.inspect.custom,
};

View File

@ -20,12 +20,13 @@ import { Connection } from './client/connection';
import { DispatcherConnection, PlaywrightDispatcher, RootDispatcher, createPlaywright } from './server';
import type { Playwright as PlaywrightAPI } from './client/playwright';
import type { Platform } from './common/platform';
import type { Language } from './utils';
export function createInProcessPlaywright(): PlaywrightAPI {
export function createInProcessPlaywright(platform: Platform): PlaywrightAPI {
const playwright = createPlaywright({ sdkLanguage: (process.env.PW_LANG_NAME as Language | undefined) || 'javascript' });
const clientConnection = new Connection(undefined, undefined);
const clientConnection = new Connection(undefined, platform, undefined);
clientConnection.useRawBuffers();
const dispatcherConnection = new DispatcherConnection(true /* local */);

View File

@ -14,6 +14,7 @@
* limitations under the License.
*/
import { nodePlatform } from './common/platform';
import { createInProcessPlaywright } from './inProcessFactory';
module.exports = createInProcessPlaywright();
module.exports = createInProcessPlaywright(nodePlatform);

View File

@ -18,12 +18,12 @@ import * as childProcess from 'child_process';
import * as path from 'path';
import { Connection } from './client/connection';
import { nodePlatform } from './common/platform';
import { PipeTransport } from './protocol/transport';
import { ManualPromise } from './utils/manualPromise';
import type { Playwright } from './client/playwright';
export async function start(env: any = {}): Promise<{ playwright: Playwright, stop: () => Promise<void> }> {
const client = new PlaywrightClient(env);
const playwright = await client._playwright;
@ -48,7 +48,7 @@ class PlaywrightClient {
this._driverProcess.unref();
this._driverProcess.stderr!.on('data', data => process.stderr.write(data));
const connection = new Connection(undefined, undefined);
const connection = new Connection(undefined, nodePlatform, undefined);
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));

View File

@ -23,15 +23,15 @@ import { TimeoutSettings } from '../../common/timeoutSettings';
import { PipeTransport } from '../../protocol/transport';
import { createGuid, getPackageManagerExecCommand, isUnderTest, makeWaitForNextTask } from '../../utils';
import { RecentLogsCollector } from '../../utils/debugLogger';
import { removeFolders } from '../../utils/fileUtils';
import { gracefullyCloseSet } from '../../utils/processLauncher';
import { debug } from '../../utilsBundle';
import { wsReceiver, wsSender } from '../../utilsBundle';
import { validateBrowserContextOptions } from '../browserContext';
import { chromiumSwitches } from '../chromium/chromiumSwitches';
import { CRBrowser } from '../chromium/crBrowser';
import { removeFolders } from '../fileUtils';
import { helper } from '../helper';
import { SdkObject, serverSideCallMetadata } from '../instrumentation';
import { gracefullyCloseSet } from '../processLauncher';
import { ProgressController } from '../progress';
import { registry } from '../registry';

View File

@ -22,9 +22,9 @@ import { BidiBrowser } from './bidiBrowser';
import { kBrowserCloseMessageId } from './bidiConnection';
import { chromiumSwitches } from '../chromium/chromiumSwitches';
import type { Env } from '../../utils/processLauncher';
import type { BrowserOptions } from '../browser';
import type { SdkObject } from '../instrumentation';
import type { Env } from '../processLauncher';
import type { ProtocolError } from '../protocolError';
import type { ConnectionTransport } from '../transport';
import type * as types from '../types';

View File

@ -23,9 +23,9 @@ import { BidiBrowser } from './bidiBrowser';
import { kBrowserCloseMessageId } from './bidiConnection';
import { createProfile } from './third_party/firefoxPrefs';
import type { Env } from '../../utils/processLauncher';
import type { BrowserOptions } from '../browser';
import type { SdkObject } from '../instrumentation';
import type { Env } from '../processLauncher';
import type { ProtocolError } from '../protocolError';
import type { ConnectionTransport } from '../transport';
import type * as types from '../types';

View File

@ -23,6 +23,7 @@ import { createGuid, debugMode } from '../utils';
import { Clock } from './clock';
import { Debugger } from './debugger';
import { BrowserContextAPIRequestContext } from './fetch';
import { mkdirIfNeeded } from './fileUtils';
import { HarRecorder } from './har/harRecorder';
import { helper } from './helper';
import { SdkObject, serverSideCallMetadata } from './instrumentation';
@ -31,9 +32,8 @@ import * as network from './network';
import { InitScript } from './page';
import { Page, PageBinding } from './page';
import { Recorder } from './recorder';
import * as storageScript from './storageScript';
import { mkdirIfNeeded } from '../utils/fileUtils';
import { RecorderApp } from './recorder/recorderApp';
import * as storageScript from './storageScript';
import * as consoleApiSource from '../generated/consoleApiSource';
import { Tracing } from './trace/recorder/tracing';

View File

@ -21,27 +21,27 @@ import * as path from 'path';
import { normalizeProxySettings, validateBrowserContextOptions } from './browserContext';
import { DEFAULT_TIMEOUT, TimeoutSettings } from '../common/timeoutSettings';
import { ManualPromise, debugMode } from '../utils';
import { existsAsync } from './fileUtils';
import { helper } from './helper';
import { SdkObject } from './instrumentation';
import { PipeTransport } from './pipeTransport';
import { envArrayToObject, launchProcess } from './processLauncher';
import { ProgressController } from './progress';
import { isProtocolError } from './protocolError';
import { registry } from './registry';
import { ClientCertificatesProxy } from './socksClientCertificatesInterceptor';
import { WebSocketTransport } from './transport';
import { RecentLogsCollector } from '../utils/debugLogger';
import { existsAsync } from '../utils/fileUtils';
import { envArrayToObject, launchProcess } from '../utils/processLauncher';
import type { Browser, BrowserOptions, BrowserProcess } from './browser';
import type { BrowserContext } from './browserContext';
import type { CallMetadata } from './instrumentation';
import type { Env } from './processLauncher';
import type { Progress } from './progress';
import type { ProtocolError } from './protocolError';
import type { BrowserName } from './registry';
import type { ConnectionTransport } from './transport';
import type * as types from './types';
import type { Env } from '../utils/processLauncher';
import type * as channels from '@protocol/channels';
export const kNoXServerRunningError = 'Looks like you launched a headed browser without having a XServer running.\n' +

View File

@ -26,10 +26,8 @@ import { TimeoutSettings } from '../../common/timeoutSettings';
import { debugMode, headersArrayToObject, headersObjectToArray, } from '../../utils';
import { wrapInASCIIBox } from '../../utils/ascii';
import { RecentLogsCollector } from '../../utils/debugLogger';
import { removeFolders } from '../../utils/fileUtils';
import { ManualPromise } from '../../utils/manualPromise';
import { fetchData } from '../../utils/network';
import { gracefullyCloseSet } from '../../utils/processLauncher';
import { getUserAgent } from '../../utils/userAgent';
import { validateBrowserContextOptions } from '../browserContext';
import { BrowserType, kNoXServerRunningError } from '../browserType';
@ -39,12 +37,14 @@ import { registry } from '../registry';
import { WebSocketTransport } from '../transport';
import { CRDevTools } from './crDevTools';
import { Browser } from '../browser';
import { removeFolders } from '../fileUtils';
import { gracefullyCloseSet } from '../processLauncher';
import { ProgressController } from '../progress';
import type { HTTPRequestParams } from '../../utils/network';
import type { Env } from '../../utils/processLauncher';
import type { BrowserOptions, BrowserProcess } from '../browser';
import type { CallMetadata, SdkObject } from '../instrumentation';
import type { Env } from '../processLauncher';
import type { Progress } from '../progress';
import type { ProtocolError } from '../protocolError';
import type { ConnectionTransport, ProtocolRequest } from '../transport';

View File

@ -17,8 +17,8 @@
import * as fs from 'fs';
import { mkdirIfNeeded } from '../../utils/fileUtils';
import { splitErrorMessage } from '../../utils/stackTrace';
import { mkdirIfNeeded } from '../fileUtils';
import type { CRSession } from './crConnection';
import type { Protocol } from './protocol';

View File

@ -15,9 +15,9 @@
*/
import { assert, monotonicTime } from '../../utils';
import { launchProcess } from '../../utils/processLauncher';
import { serverSideCallMetadata } from '../instrumentation';
import { Page } from '../page';
import { launchProcess } from '../processLauncher';
import { ProgressController } from '../progress';
import type { Progress } from '../progress';

View File

@ -15,13 +15,13 @@
*/
import { SdkObject, createInstrumentation, serverSideCallMetadata } from './instrumentation';
import { gracefullyProcessExitDoNotHang } from './processLauncher';
import { Recorder } from './recorder';
import { asLocator } from '../utils';
import { parseAriaSnapshotUnsafe } from '../utils/isomorphic/ariaSnapshot';
import { yaml } from '../utilsBundle';
import { EmptyRecorderApp } from './recorder/recorderApp';
import { unsafeLocatorOrSelectorAsSelector } from '../utils/isomorphic/locatorParser';
import { gracefullyProcessExitDoNotHang } from '../utils/processLauncher';
import type { Language } from '../utils';
import type { Browser } from './browser';

View File

@ -18,7 +18,7 @@ import * as fs from 'fs';
import { Dispatcher, existingDispatcher } from './dispatcher';
import { StreamDispatcher } from './streamDispatcher';
import { mkdirIfNeeded } from '../../utils/fileUtils';
import { mkdirIfNeeded } from '../fileUtils';
import type { DispatcherScope } from './dispatcher';
import type { Artifact } from '../artifact';

View File

@ -20,7 +20,7 @@ import * as path from 'path';
import { Dispatcher } from './dispatcher';
import { SdkObject } from '../../server/instrumentation';
import { assert, calculateSha1, createGuid, removeFolders } from '../../utils';
import { assert, calculateSha1, createGuid } from '../../utils';
import { serializeClientSideCallMetadata } from '../../utils';
import { ManualPromise } from '../../utils/manualPromise';
import { fetchData } from '../../utils/network';
@ -29,6 +29,7 @@ import { ZipFile } from '../../utils/zipFile';
import { yauzl, yazl } from '../../zipBundle';
import { deviceDescriptors as descriptors } from '../deviceDescriptors';
import { JsonPipeDispatcher } from '../dispatchers/jsonPipeDispatcher';
import { removeFolders } from '../fileUtils';
import { ProgressController } from '../progress';
import { SocksInterceptor } from '../socksInterceptor';
import { WebSocketTransport } from '../transport';

View File

@ -23,7 +23,6 @@ import { TimeoutSettings } from '../../common/timeoutSettings';
import { ManualPromise, wrapInASCIIBox } from '../../utils';
import { RecentLogsCollector } from '../../utils/debugLogger';
import { eventsHelper } from '../../utils/eventsHelper';
import { envArrayToObject, launchProcess } from '../../utils/processLauncher';
import { validateBrowserContextOptions } from '../browserContext';
import { CRBrowser } from '../chromium/crBrowser';
import { CRConnection } from '../chromium/crConnection';
@ -33,6 +32,7 @@ import { ConsoleMessage } from '../console';
import { helper } from '../helper';
import { SdkObject, serverSideCallMetadata } from '../instrumentation';
import * as js from '../javascript';
import { envArrayToObject, launchProcess } from '../processLauncher';
import { ProgressController } from '../progress';
import { WebSocketTransport } from '../transport';

View File

@ -0,0 +1,205 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as fs from 'fs';
import * as path from 'path';
import { ManualPromise } from '../utils/manualPromise';
import { yazl } from '../zipBundle';
import type { EventEmitter } from 'events';
export const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => fs.stat(path, err => resolve(!err)));
export async function mkdirIfNeeded(filePath: string) {
// This will harmlessly throw on windows if the dirname is the root directory.
await fs.promises.mkdir(path.dirname(filePath), { recursive: true }).catch(() => {});
}
export async function removeFolders(dirs: string[]): Promise<Error[]> {
return await Promise.all(dirs.map((dir: string) =>
fs.promises.rm(dir, { recursive: true, force: true, maxRetries: 10 }).catch(e => e)
));
}
export function canAccessFile(file: string) {
if (!file)
return false;
try {
fs.accessSync(file);
return true;
} catch (e) {
return false;
}
}
export async function copyFileAndMakeWritable(from: string, to: string) {
await fs.promises.copyFile(from, to);
await fs.promises.chmod(to, 0o664);
}
export function sanitizeForFilePath(s: string) {
return s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-');
}
export function toPosixPath(aPath: string): string {
return aPath.split(path.sep).join(path.posix.sep);
}
type NameValue = { name: string, value: string };
type SerializedFSOperation = {
op: 'mkdir', dir: string,
} | {
op: 'writeFile', file: string, content: string | Buffer, skipIfExists?: boolean,
} | {
op: 'appendFile', file: string, content: string,
} | {
op: 'copyFile', from: string, to: string,
} | {
op: 'zip', entries: NameValue[], zipFileName: string,
};
export class SerializedFS {
private _buffers = new Map<string, string[]>(); // Should never be accessed from within appendOperation.
private _error: Error | undefined;
private _operations: SerializedFSOperation[] = [];
private _operationsDone: ManualPromise<void>;
constructor() {
this._operationsDone = new ManualPromise();
this._operationsDone.resolve(); // No operations scheduled yet.
}
mkdir(dir: string) {
this._appendOperation({ op: 'mkdir', dir });
}
writeFile(file: string, content: string | Buffer, skipIfExists?: boolean) {
this._buffers.delete(file); // No need to flush the buffer since we'll overwrite anyway.
this._appendOperation({ op: 'writeFile', file, content, skipIfExists });
}
appendFile(file: string, text: string, flush?: boolean) {
if (!this._buffers.has(file))
this._buffers.set(file, []);
this._buffers.get(file)!.push(text);
if (flush)
this._flushFile(file);
}
private _flushFile(file: string) {
const buffer = this._buffers.get(file);
if (buffer === undefined)
return;
const content = buffer.join('');
this._buffers.delete(file);
this._appendOperation({ op: 'appendFile', file, content });
}
copyFile(from: string, to: string) {
this._flushFile(from);
this._buffers.delete(to); // No need to flush the buffer since we'll overwrite anyway.
this._appendOperation({ op: 'copyFile', from, to });
}
async syncAndGetError() {
for (const file of this._buffers.keys())
this._flushFile(file);
await this._operationsDone;
return this._error;
}
zip(entries: NameValue[], zipFileName: string) {
for (const file of this._buffers.keys())
this._flushFile(file);
// Chain the export operation against write operations,
// so that files do not change during the export.
this._appendOperation({ op: 'zip', entries, zipFileName });
}
// This method serializes all writes to the trace.
private _appendOperation(op: SerializedFSOperation): void {
const last = this._operations[this._operations.length - 1];
if (last?.op === 'appendFile' && op.op === 'appendFile' && last.file === op.file) {
// Merge pending appendFile operations for performance.
last.content += op.content;
return;
}
this._operations.push(op);
if (this._operationsDone.isDone())
this._performOperations();
}
private async _performOperations() {
this._operationsDone = new ManualPromise();
while (this._operations.length) {
const op = this._operations.shift()!;
// Ignore all operations after the first error.
if (this._error)
continue;
try {
await this._performOperation(op);
} catch (e) {
this._error = e;
}
}
this._operationsDone.resolve();
}
private async _performOperation(op: SerializedFSOperation) {
switch (op.op) {
case 'mkdir': {
await fs.promises.mkdir(op.dir, { recursive: true });
return;
}
case 'writeFile': {
// Note: 'wx' flag only writes when the file does not exist.
// See https://nodejs.org/api/fs.html#file-system-flags.
// This way tracing never have to write the same resource twice.
if (op.skipIfExists)
await fs.promises.writeFile(op.file, op.content, { flag: 'wx' }).catch(() => {});
else
await fs.promises.writeFile(op.file, op.content);
return;
}
case 'copyFile': {
await fs.promises.copyFile(op.from, op.to);
return;
}
case 'appendFile': {
await fs.promises.appendFile(op.file, op.content);
return;
}
case 'zip': {
const zipFile = new yazl.ZipFile();
const result = new ManualPromise<void>();
(zipFile as any as EventEmitter).on('error', error => result.reject(error));
for (const entry of op.entries)
zipFile.addFile(entry.value, entry.name);
zipFile.end();
zipFile.outputStream
.pipe(fs.createWriteStream(op.zipFileName))
.on('close', () => result.resolve())
.on('error', error => result.reject(error));
await result;
return;
}
}
}
}

View File

@ -24,9 +24,9 @@ import { wrapInASCIIBox } from '../../utils';
import { BrowserType, kNoXServerRunningError } from '../browserType';
import { BrowserReadyState } from '../browserType';
import type { Env } from '../../utils/processLauncher';
import type { BrowserOptions } from '../browser';
import type { SdkObject } from '../instrumentation';
import type { Env } from '../processLauncher';
import type { ProtocolError } from '../protocolError';
import type { ConnectionTransport } from '../transport';
import type * as types from '../types';

View File

@ -31,3 +31,5 @@ export type { Playwright } from './playwright';
export { installRootRedirect, openTraceInBrowser, openTraceViewerApp, runTraceViewerApp, startTraceViewerServer } from './trace/viewer/traceViewer';
export { serverSideCallMetadata } from './instrumentation';
export { SocksProxy } from '../common/socksProxy';
export * from './fileUtils';
export * from './processLauncher';

View File

@ -20,8 +20,7 @@ import * as fs from 'fs';
import * as readline from 'readline';
import { removeFolders } from './fileUtils';
import { isUnderTest } from './';
import { isUnderTest } from '../utils';
export type Env = {[key: string]: string | number | boolean | undefined};

View File

@ -21,10 +21,10 @@ import * as os from 'os';
import * as path from 'path';
import { debugLogger } from '../../utils/debugLogger';
import { existsAsync } from '../../utils/fileUtils';
import { ManualPromise } from '../../utils/manualPromise';
import { getUserAgent } from '../../utils/userAgent';
import { colors, progress as ProgressBar } from '../../utilsBundle';
import { existsAsync } from '../fileUtils';
import { browserDirectoryToMarkerFilePath } from '.';

View File

@ -25,12 +25,12 @@ import { dockerVersion, readDockerVersionSync, transformCommandsForRoot } from '
import { installDependenciesLinux, installDependenciesWindows, validateDependenciesLinux, validateDependenciesWindows } from './dependencies';
import { calculateSha1, getAsBooleanFromENV, getFromENV, getPackageManagerExecCommand, wrapInASCIIBox } from '../../utils';
import { debugLogger } from '../../utils/debugLogger';
import { canAccessFile, existsAsync, removeFolders } from '../../utils/fileUtils';
import { hostPlatform, isOfficiallySupportedPlatform } from '../../utils/hostPlatform';
import { fetchData } from '../../utils/network';
import { spawnAsync } from '../../utils/spawnAsync';
import { getEmbedderName } from '../../utils/userAgent';
import { lockfile } from '../../utilsBundle';
import { canAccessFile, existsAsync, removeFolders } from '../fileUtils';
import type { DependencyGroup } from './dependencies';
import type { HostPlatform } from '../../utils/hostPlatform';

View File

@ -20,11 +20,12 @@ import * as path from 'path';
import { Snapshotter } from './snapshotter';
import { commandsWithTracingSnapshots } from '../../../protocol/debug';
import { SerializedFS, assert, createGuid, eventsHelper, monotonicTime, removeFolders } from '../../../utils';
import { assert, createGuid, eventsHelper, monotonicTime } from '../../../utils';
import { Artifact } from '../../artifact';
import { BrowserContext } from '../../browserContext';
import { Dispatcher } from '../../dispatchers/dispatcher';
import { serializeError } from '../../errors';
import { SerializedFS, removeFolders } from '../../fileUtils';
import { HarTracer } from '../../har/harTracer';
import { SdkObject } from '../../instrumentation';
import { Page } from '../../page';

View File

@ -17,7 +17,8 @@
import * as fs from 'fs';
import * as path from 'path';
import { gracefullyProcessExitDoNotHang, isUnderTest } from '../../../utils';
import { gracefullyProcessExitDoNotHang } from '../../../server';
import { isUnderTest } from '../../../utils';
import { HttpServer } from '../../../utils/httpServer';
import { open } from '../../../utilsBundle';
import { serverSideCallMetadata } from '../../instrumentation';

View File

@ -22,9 +22,9 @@ import { wrapInASCIIBox } from '../../utils';
import { BrowserType, kNoXServerRunningError } from '../browserType';
import { WKBrowser } from '../webkit/wkBrowser';
import type { Env } from '../../utils/processLauncher';
import type { BrowserOptions } from '../browser';
import type { SdkObject } from '../instrumentation';
import type { Env } from '../processLauncher';
import type { ProtocolError } from '../protocolError';
import type { ConnectionTransport } from '../transport';
import type * as types from '../types';

View File

@ -14,194 +14,11 @@
* limitations under the License.
*/
import * as fs from 'fs';
import * as path from 'path';
import { ManualPromise } from './manualPromise';
import { yazl } from '../zipBundle';
import type { EventEmitter } from 'events';
import type { Platform } from '../common/platform';
export const fileUploadSizeLimit = 50 * 1024 * 1024;
export const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => fs.stat(path, err => resolve(!err)));
export async function mkdirIfNeeded(filePath: string) {
export async function mkdirIfNeeded(platform: Platform, filePath: string) {
// This will harmlessly throw on windows if the dirname is the root directory.
await fs.promises.mkdir(path.dirname(filePath), { recursive: true }).catch(() => {});
}
export async function removeFolders(dirs: string[]): Promise<Error[]> {
return await Promise.all(dirs.map((dir: string) =>
fs.promises.rm(dir, { recursive: true, force: true, maxRetries: 10 }).catch(e => e)
));
}
export function canAccessFile(file: string) {
if (!file)
return false;
try {
fs.accessSync(file);
return true;
} catch (e) {
return false;
}
}
export async function copyFileAndMakeWritable(from: string, to: string) {
await fs.promises.copyFile(from, to);
await fs.promises.chmod(to, 0o664);
}
export function sanitizeForFilePath(s: string) {
return s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-');
}
export function toPosixPath(aPath: string): string {
return aPath.split(path.sep).join(path.posix.sep);
}
type NameValue = { name: string, value: string };
type SerializedFSOperation = {
op: 'mkdir', dir: string,
} | {
op: 'writeFile', file: string, content: string | Buffer, skipIfExists?: boolean,
} | {
op: 'appendFile', file: string, content: string,
} | {
op: 'copyFile', from: string, to: string,
} | {
op: 'zip', entries: NameValue[], zipFileName: string,
};
export class SerializedFS {
private _buffers = new Map<string, string[]>(); // Should never be accessed from within appendOperation.
private _error: Error | undefined;
private _operations: SerializedFSOperation[] = [];
private _operationsDone: ManualPromise<void>;
constructor() {
this._operationsDone = new ManualPromise();
this._operationsDone.resolve(); // No operations scheduled yet.
}
mkdir(dir: string) {
this._appendOperation({ op: 'mkdir', dir });
}
writeFile(file: string, content: string | Buffer, skipIfExists?: boolean) {
this._buffers.delete(file); // No need to flush the buffer since we'll overwrite anyway.
this._appendOperation({ op: 'writeFile', file, content, skipIfExists });
}
appendFile(file: string, text: string, flush?: boolean) {
if (!this._buffers.has(file))
this._buffers.set(file, []);
this._buffers.get(file)!.push(text);
if (flush)
this._flushFile(file);
}
private _flushFile(file: string) {
const buffer = this._buffers.get(file);
if (buffer === undefined)
return;
const content = buffer.join('');
this._buffers.delete(file);
this._appendOperation({ op: 'appendFile', file, content });
}
copyFile(from: string, to: string) {
this._flushFile(from);
this._buffers.delete(to); // No need to flush the buffer since we'll overwrite anyway.
this._appendOperation({ op: 'copyFile', from, to });
}
async syncAndGetError() {
for (const file of this._buffers.keys())
this._flushFile(file);
await this._operationsDone;
return this._error;
}
zip(entries: NameValue[], zipFileName: string) {
for (const file of this._buffers.keys())
this._flushFile(file);
// Chain the export operation against write operations,
// so that files do not change during the export.
this._appendOperation({ op: 'zip', entries, zipFileName });
}
// This method serializes all writes to the trace.
private _appendOperation(op: SerializedFSOperation): void {
const last = this._operations[this._operations.length - 1];
if (last?.op === 'appendFile' && op.op === 'appendFile' && last.file === op.file) {
// Merge pending appendFile operations for performance.
last.content += op.content;
return;
}
this._operations.push(op);
if (this._operationsDone.isDone())
this._performOperations();
}
private async _performOperations() {
this._operationsDone = new ManualPromise();
while (this._operations.length) {
const op = this._operations.shift()!;
// Ignore all operations after the first error.
if (this._error)
continue;
try {
await this._performOperation(op);
} catch (e) {
this._error = e;
}
}
this._operationsDone.resolve();
}
private async _performOperation(op: SerializedFSOperation) {
switch (op.op) {
case 'mkdir': {
await fs.promises.mkdir(op.dir, { recursive: true });
return;
}
case 'writeFile': {
// Note: 'wx' flag only writes when the file does not exist.
// See https://nodejs.org/api/fs.html#file-system-flags.
// This way tracing never have to write the same resource twice.
if (op.skipIfExists)
await fs.promises.writeFile(op.file, op.content, { flag: 'wx' }).catch(() => {});
else
await fs.promises.writeFile(op.file, op.content);
return;
}
case 'copyFile': {
await fs.promises.copyFile(op.from, op.to);
return;
}
case 'appendFile': {
await fs.promises.appendFile(op.file, op.content);
return;
}
case 'zip': {
const zipFile = new yazl.ZipFile();
const result = new ManualPromise<void>();
(zipFile as any as EventEmitter).on('error', error => result.reject(error));
for (const entry of op.entries)
zipFile.addFile(entry.value, entry.name);
zipFile.end();
zipFile.outputStream
.pipe(fs.createWriteStream(op.zipFileName))
.on('close', () => result.resolve())
.on('error', error => result.reject(error));
await result;
return;
}
}
}
await platform.fs().promises.mkdir(platform.path().dirname(filePath), { recursive: true }).catch(() => {});
}

View File

@ -33,7 +33,6 @@ export * from './isomorphic/stringUtils';
export * from './isomorphic/urlMatch';
export * from './multimap';
export * from './network';
export * from './processLauncher';
export * from './profiler';
export * from './rtti';
export * from './semaphore';

View File

@ -17,7 +17,8 @@
import * as fs from 'fs';
import * as path from 'path';
import { gracefullyProcessExitDoNotHang, isRegExp } from 'playwright-core/lib/utils';
import { gracefullyProcessExitDoNotHang } from 'playwright-core/lib/server';
import { isRegExp } from 'playwright-core/lib/utils';
import { requireOrImport, setSingleTSConfig, setTransformConfig } from '../transform/transform';
import { errorWithFile, fileIsModule } from '../util';

View File

@ -16,7 +16,8 @@
import * as path from 'path';
import { calculateSha1, toPosixPath } from 'playwright-core/lib/utils';
import { toPosixPath } from 'playwright-core/lib/server';
import { calculateSha1 } from 'playwright-core/lib/utils';
import { createFileMatcher } from '../util';

View File

@ -18,7 +18,8 @@
import * as fs from 'fs';
import * as path from 'path';
import { escapeTemplateString, isString, sanitizeForFilePath } from 'playwright-core/lib/utils';
import { sanitizeForFilePath } from 'playwright-core/lib/server';
import { escapeTemplateString, isString } from 'playwright-core/lib/utils';
import { kNoElementsFoundError, matcherHint } from './matcherHint';
import { EXPECTED_COLOR } from '../common/expectBundle';

View File

@ -17,7 +17,8 @@
import * as fs from 'fs';
import * as path from 'path';
import { compareBuffersOrStrings, getComparator, isString, sanitizeForFilePath } from 'playwright-core/lib/utils';
import { sanitizeForFilePath } from 'playwright-core/lib/server';
import { compareBuffersOrStrings, getComparator, isString } from 'playwright-core/lib/utils';
import { colors } from 'playwright-core/lib/utilsBundle';
import { mime } from 'playwright-core/lib/utilsBundle';

View File

@ -16,7 +16,8 @@
import * as net from 'net';
import * as path from 'path';
import { isURLAvailable, launchProcess, monotonicTime, raceAgainstDeadline } from 'playwright-core/lib/utils';
import { launchProcess } from 'playwright-core/lib/server';
import { isURLAvailable, monotonicTime, raceAgainstDeadline } from 'playwright-core/lib/utils';
import { colors, debug } from 'playwright-core/lib/utilsBundle';
import type { TestRunnerPlugin } from '.';

View File

@ -20,7 +20,8 @@ import * as fs from 'fs';
import * as path from 'path';
import { program } from 'playwright-core/lib/cli/program';
import { gracefullyProcessExitDoNotHang, startProfiling, stopProfiling } from 'playwright-core/lib/utils';
import { gracefullyProcessExitDoNotHang } from 'playwright-core/lib/server';
import { startProfiling, stopProfiling } from 'playwright-core/lib/utils';
import { builtInReporters, defaultReporter, defaultTimeout } from './common/config';
import { loadConfigFromFileRestartIfNeeded, loadEmptyConfigForMergeReports, resolveConfigLocation } from './common/configLoader';

View File

@ -18,7 +18,8 @@ import * as fs from 'fs';
import * as path from 'path';
import { Readable } from 'stream';
import { ManualPromise, calculateSha1, createGuid, getUserAgent, removeFolders, sanitizeForFilePath } from 'playwright-core/lib/utils';
import { removeFolders, sanitizeForFilePath } from 'playwright-core/lib/server';
import { ManualPromise, calculateSha1, createGuid, getUserAgent } from 'playwright-core/lib/utils';
import { mime } from 'playwright-core/lib/utilsBundle';
import { yazl } from 'playwright-core/lib/zipBundle';

View File

@ -18,8 +18,8 @@ import * as fs from 'fs';
import * as path from 'path';
import { Transform } from 'stream';
import { MultiMap, getPackageManagerExecCommand } from 'playwright-core/lib/utils';
import { HttpServer, assert, calculateSha1, copyFileAndMakeWritable, gracefullyProcessExitDoNotHang, removeFolders, sanitizeForFilePath, toPosixPath } from 'playwright-core/lib/utils';
import { copyFileAndMakeWritable, gracefullyProcessExitDoNotHang, removeFolders, sanitizeForFilePath, toPosixPath } from 'playwright-core/lib/server';
import { HttpServer, MultiMap, assert, calculateSha1, getPackageManagerExecCommand } from 'playwright-core/lib/utils';
import { colors, open } from 'playwright-core/lib/utilsBundle';
import { mime } from 'playwright-core/lib/utilsBundle';
import { yazl } from 'playwright-core/lib/zipBundle';

View File

@ -17,7 +17,8 @@
import * as fs from 'fs';
import * as path from 'path';
import { MultiMap, toPosixPath } from 'playwright-core/lib/utils';
import { toPosixPath } from 'playwright-core/lib/server';
import { MultiMap } from 'playwright-core/lib/utils';
import { formatError, nonTerminalScreen, prepareErrorStack, resolveOutputFile } from './base';
import { getProjectId } from '../common/config';

View File

@ -18,7 +18,8 @@ import * as fs from 'fs';
import * as path from 'path';
import { promisify } from 'util';
import { monotonicTime, removeFolders } from 'playwright-core/lib/utils';
import { removeFolders } from 'playwright-core/lib/server';
import { monotonicTime } from 'playwright-core/lib/utils';
import { debug } from 'playwright-core/lib/utilsBundle';
import { Dispatcher } from './dispatcher';
@ -26,12 +27,12 @@ import { FailureTracker } from './failureTracker';
import { collectProjectsAndTestFiles, createRootSuite, loadFileSuites, loadGlobalHook } from './loadUtils';
import { buildDependentProjects, buildTeardownToSetupsMap, filterProjects } from './projectUtils';
import { applySuggestedRebaselines, clearSuggestedRebaselines } from './rebase';
import { Suite } from '../common/test';
import { createTestGroups } from '../runner/testGroups';
import { removeDirAndLogToConsole } from '../util';
import { TaskRunner } from './taskRunner';
import { detectChangedTestFiles } from './vcs';
import { Suite } from '../common/test';
import { createTestGroups } from '../runner/testGroups';
import { cacheDir } from '../transform/compilationCache';
import { removeDirAndLogToConsole } from '../util';
import type { TestGroup } from '../runner/testGroups';
import type { Matcher } from '../util';

View File

@ -17,8 +17,8 @@
import * as fs from 'fs';
import * as path from 'path';
import { installRootRedirect, openTraceInBrowser, openTraceViewerApp, registry, startTraceViewerServer } from 'playwright-core/lib/server';
import { ManualPromise, gracefullyProcessExitDoNotHang, isUnderTest } from 'playwright-core/lib/utils';
import { gracefullyProcessExitDoNotHang, installRootRedirect, openTraceInBrowser, openTraceViewerApp, registry, startTraceViewerServer } from 'playwright-core/lib/server';
import { ManualPromise, isUnderTest } from 'playwright-core/lib/utils';
import { open } from 'playwright-core/lib/utilsBundle';
import { createErrorCollectingReporter, createReporterForTestServer, createReporters } from './reporters';

View File

@ -17,7 +17,7 @@
import * as fs from 'fs';
import * as path from 'path';
import { removeFolders } from 'playwright-core/lib/utils';
import { removeFolders } from 'playwright-core/lib/server';
import { ProcessHost } from './processHost';
import { stdioChunkToParams } from '../common/ipc';

View File

@ -19,8 +19,8 @@ import * as path from 'path';
import * as url from 'url';
import util from 'util';
import { formatCallLog } from 'playwright-core/lib/utils';
import { calculateSha1, isRegExp, isString, sanitizeForFilePath, stringifyStackFrames } from 'playwright-core/lib/utils';
import { sanitizeForFilePath } from 'playwright-core/lib/server';
import { calculateSha1, formatCallLog, isRegExp, isString, stringifyStackFrames } from 'playwright-core/lib/utils';
import { debug, mime, minimatch, parseStackTraceLine } from 'playwright-core/lib/utilsBundle';
import type { Location } from './../types/testReporter';

View File

@ -17,7 +17,8 @@
import * as fs from 'fs';
import * as path from 'path';
import { captureRawStack, monotonicTime, sanitizeForFilePath, stringifyStackFrames, zones } from 'playwright-core/lib/utils';
import { sanitizeForFilePath } from 'playwright-core/lib/server';
import { captureRawStack, monotonicTime, stringifyStackFrames, zones } from 'playwright-core/lib/utils';
import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutManager';
import { debugTest, filteredStackTrace, formatLocation, getContainedPath, normalizeAndSaveAttachment, trimLongString, windowsFilesystemFriendlyLength } from '../util';

View File

@ -17,7 +17,8 @@
import * as fs from 'fs';
import * as path from 'path';
import { ManualPromise, SerializedFS, calculateSha1, createGuid, monotonicTime } from 'playwright-core/lib/utils';
import { SerializedFS } from 'playwright-core/lib/server';
import { ManualPromise, calculateSha1, createGuid, monotonicTime } from 'playwright-core/lib/utils';
import { yauzl, yazl } from 'playwright-core/lib/zipBundle';
import { filteredStackTrace } from '../util';

View File

@ -14,7 +14,9 @@
* limitations under the License.
*/
import { ManualPromise, gracefullyCloseAll, removeFolders } from 'playwright-core/lib/utils';
import { removeFolders } from 'playwright-core/lib/server';
import { gracefullyCloseAll } from 'playwright-core/lib/server';
import { ManualPromise } from 'playwright-core/lib/utils';
import { colors } from 'playwright-core/lib/utilsBundle';
import { deserializeConfig } from '../common/configLoader';

View File

@ -16,15 +16,17 @@
import * as fs from 'fs';
import * as os from 'os';
import type { PageTestFixtures, PageWorkerFixtures } from '../page/pageTestApi';
import * as path from 'path';
import type { BrowserContext, BrowserContextOptions, BrowserType, Page } from 'playwright-core';
import { removeFolders } from '../../packages/playwright-core/lib/utils/fileUtils';
import { baseTest } from './baseTest';
import { type RemoteServerOptions, type PlaywrightServer, RunServer, RemoteServer } from './remoteServer';
import type { Log } from '../../packages/trace/src/har';
import { RunServer, RemoteServer } from './remoteServer';
import { removeFolders } from '../../packages/playwright-core/lib/server/fileUtils';
import { parseHar } from '../config/utils';
import { createSkipTestPredicate } from '../bidi/expectationUtil';
import type { PageTestFixtures, PageWorkerFixtures } from '../page/pageTestApi';
import type { RemoteServerOptions, PlaywrightServer } from './remoteServer';
import type { BrowserContext, BrowserContextOptions, BrowserType, Page } from 'playwright-core';
import type { Log } from '../../packages/trace/src/har';
import type { TestInfo } from '@playwright/test';
export type BrowserTestWorkerFixtures = PageWorkerFixtures & {

View File

@ -17,7 +17,7 @@
import path from 'path';
import fs from 'fs';
import { spawnAsync } from '../../packages/playwright-core/lib/utils/spawnAsync';
import { removeFolders } from '../../packages/playwright-core/lib/utils/fileUtils';
import { removeFolders } from '../../packages/playwright-core/lib/server/fileUtils';
import { TMP_WORKSPACES } from './npmTest';
const PACKAGE_BUILDER_SCRIPT = path.join(__dirname, '..', '..', 'utils', 'pack_package.js');

View File

@ -22,7 +22,7 @@ import debugLogger from 'debug';
import { Registry } from './registry';
import type { CommonFixtures, CommonWorkerFixtures } from '../config/commonFixtures';
import { commonFixtures } from '../config/commonFixtures';
import { removeFolders } from '../../packages/playwright-core/lib/utils/fileUtils';
import { removeFolders } from '../../packages/playwright-core/lib/server/fileUtils';
import { spawnAsync } from '../../packages/playwright-core/lib/utils/spawnAsync';
import type { SpawnOptions } from 'child_process';

View File

@ -22,6 +22,7 @@ import type { Source } from '../../../packages/recorder/src/recorderTypes';
import type { CommonFixtures, TestChildProcess } from '../../config/commonFixtures';
import { stripAnsi } from '../../config/utils';
import { expect } from '@playwright/test';
import { nodePlatform } from '../../../packages/playwright-core/lib/common/platform';
export { expect } from '@playwright/test';
type CLITestArgs = {
@ -46,7 +47,7 @@ const codegenLang2Id: Map<string, string> = new Map([
]);
const codegenLangId2lang = new Map([...codegenLang2Id.entries()].map(([lang, langId]) => [langId, lang]));
const playwrightToAutomateInspector = require('../../../packages/playwright-core/lib/inProcessFactory').createInProcessPlaywright();
const playwrightToAutomateInspector = require('../../../packages/playwright-core/lib/inProcessFactory').createInProcessPlaywright(nodePlatform);
export const test = contextTest.extend<CLITestArgs>({
recorderPageGetter: async ({ context, toImpl, mode }, run, testInfo) => {