diff --git a/packages/playwright-core/src/cli/driver.ts b/packages/playwright-core/src/cli/driver.ts index 1e16de00bf..104db006cc 100644 --- a/packages/playwright-core/src/cli/driver.ts +++ b/packages/playwright-core/src/cli/driver.ts @@ -19,7 +19,7 @@ import * as fs from 'fs'; import * as playwright from '../..'; -import { PipeTransport } from '../utils/pipeTransport'; +import { PipeTransport } from '../server/utils/pipeTransport'; import { PlaywrightServer } from '../remote/playwrightServer'; import { DispatcherConnection, PlaywrightDispatcher, RootDispatcher, createPlaywright } from '../server'; import { gracefullyProcessExitDoNotHang } from '../server/utils/processLauncher'; diff --git a/packages/playwright-core/src/client/DEPS.list b/packages/playwright-core/src/client/DEPS.list index 4be2917927..e886cdbe63 100644 --- a/packages/playwright-core/src/client/DEPS.list +++ b/packages/playwright-core/src/client/DEPS.list @@ -1,5 +1,4 @@ [*] ../common/ ../protocol/ -../utils/** -../utilsBundle.ts +../utils/isomorphic diff --git a/packages/playwright-core/src/client/android.ts b/packages/playwright-core/src/client/android.ts index f0ca11848f..134be7c7bb 100644 --- a/packages/playwright-core/src/client/android.ts +++ b/packages/playwright-core/src/client/android.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { EventEmitter } from 'events'; - +import { EventEmitter } from './eventEmitter'; import { BrowserContext, prepareBrowserContextParams } from './browserContext'; import { ChannelOwner } from './channelOwner'; import { TargetClosedError, isTargetClosedError } from './errors'; @@ -25,6 +24,7 @@ import { TimeoutSettings } from '../utils/isomorphic/timeoutSettings'; import { isRegExp, isString } from '../utils/isomorphic/rtti'; import { monotonicTime } from '../utils/isomorphic/time'; import { raceAgainstDeadline } from '../utils/isomorphic/timeoutRunner'; +import { connectOverWebSocket } from './webSocket'; import type { Page } from './page'; import type * as types from './types'; @@ -69,9 +69,8 @@ export class Android extends ChannelOwner implements ap return await this._wrapApiCall(async () => { const deadline = options.timeout ? monotonicTime() + options.timeout : 0; const headers = { 'x-playwright-browser': 'android', ...options.headers }; - const localUtils = this._connection.localUtils(); const connectParams: channels.LocalUtilsConnectParams = { wsEndpoint, headers, slowMo: options.slowMo, timeout: options.timeout }; - const connection = await localUtils.connect(connectParams); + const connection = await connectOverWebSocket(this._connection, connectParams); let device: AndroidDevice; connection.on('close', () => { diff --git a/packages/playwright-core/src/client/artifact.ts b/packages/playwright-core/src/client/artifact.ts index 815c7a358b..7c6fd947cf 100644 --- a/packages/playwright-core/src/client/artifact.ts +++ b/packages/playwright-core/src/client/artifact.ts @@ -16,7 +16,7 @@ import { ChannelOwner } from './channelOwner'; import { Stream } from './stream'; -import { mkdirIfNeeded } from '../common/fileUtils'; +import { mkdirIfNeeded } from './fileUtils'; import type * as channels from '@protocol/channels'; import type { Readable } from 'stream'; diff --git a/packages/playwright-core/src/client/browser.ts b/packages/playwright-core/src/client/browser.ts index 88140621fe..a12bf108a8 100644 --- a/packages/playwright-core/src/client/browser.ts +++ b/packages/playwright-core/src/client/browser.ts @@ -20,7 +20,7 @@ import { CDPSession } from './cdpSession'; import { ChannelOwner } from './channelOwner'; import { isTargetClosedError } from './errors'; import { Events } from './events'; -import { mkdirIfNeeded } from '../common/fileUtils'; +import { mkdirIfNeeded } from './fileUtils'; import type { BrowserType } from './browserType'; import type { Page } from './page'; diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 02aa857366..21f5a200b7 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -35,7 +35,7 @@ import { Waiter } from './waiter'; import { WebError } from './webError'; import { Worker } from './worker'; import { TimeoutSettings } from '../utils/isomorphic/timeoutSettings'; -import { mkdirIfNeeded } from '../common/fileUtils'; +import { mkdirIfNeeded } from './fileUtils'; import { headersObjectToArray } from '../utils/isomorphic/headers'; import { urlMatchesEqual } from '../utils/isomorphic/urlMatch'; import { isRegExp, isString } from '../utils/isomorphic/rtti'; @@ -338,7 +338,7 @@ export class BrowserContext extends ChannelOwner } async route(url: URLMatch, handler: network.RouteHandlerCallback, options: { times?: number } = {}): Promise { - this._routes.unshift(new network.RouteHandler(this._options.baseURL, url, handler, options.times)); + this._routes.unshift(new network.RouteHandler(this._platform, this._options.baseURL, url, handler, options.times)); await this._updateInterceptionPatterns(); } @@ -361,11 +361,14 @@ export class BrowserContext extends ChannelOwner } async routeFromHAR(har: string, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean, updateContent?: 'attach' | 'embed', updateMode?: 'minimal' | 'full' } = {}): Promise { + const localUtils = this._connection.localUtils(); + if (!localUtils) + throw new Error('Route from har is not supported in thin clients'); if (options.update) { await this._recordIntoHAR(har, null, options); return; } - const harRouter = await HarRouter.create(this._connection.localUtils(), har, options.notFound || 'abort', { urlMatch: options.url }); + const harRouter = await HarRouter.create(localUtils, har, options.notFound || 'abort', { urlMatch: options.url }); this._harRouters.push(harRouter); await harRouter.addContextRoute(this); } @@ -484,8 +487,11 @@ export class BrowserContext extends ChannelOwner const isCompressed = harParams.content === 'attach' || harParams.path.endsWith('.zip'); const needCompressed = harParams.path.endsWith('.zip'); if (isCompressed && !needCompressed) { + const localUtils = this._connection.localUtils(); + if (!localUtils) + throw new Error('Uncompressed har is not supported in thin clients'); await artifact.saveAs(harParams.path + '.tmp'); - await this._connection.localUtils().harUnzip({ zipFile: harParams.path + '.tmp', harFile: harParams.path }); + await localUtils.harUnzip({ zipFile: harParams.path + '.tmp', harFile: harParams.path }); } else { await artifact.saveAs(harParams.path); } diff --git a/packages/playwright-core/src/client/browserType.ts b/packages/playwright-core/src/client/browserType.ts index 06bae0419f..e4942bd7f0 100644 --- a/packages/playwright-core/src/client/browserType.ts +++ b/packages/playwright-core/src/client/browserType.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import path from 'path'; - import { Browser } from './browser'; import { BrowserContext, prepareBrowserContextParams } from './browserContext'; import { ChannelOwner } from './channelOwner'; @@ -25,6 +23,7 @@ import { assert } from '../utils/isomorphic/debug'; import { headersObjectToArray } from '../utils/isomorphic/headers'; import { monotonicTime } from '../utils/isomorphic/time'; import { raceAgainstDeadline } from '../utils/isomorphic/timeoutRunner'; +import { connectOverWebSocket } from './webSocket'; import type { Playwright } from './playwright'; import type { ConnectOptions, LaunchOptions, LaunchPersistentContextOptions, LaunchServerOptions, Logger } from './types'; @@ -100,7 +99,7 @@ export class BrowserType extends ChannelOwner imple ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs), env: options.env ? envObjectToArray(options.env) : undefined, channel: options.channel, - userDataDir: (path.isAbsolute(userDataDir) || !userDataDir) ? userDataDir : path.resolve(userDataDir), + userDataDir: (this._platform.path().isAbsolute(userDataDir) || !userDataDir) ? userDataDir : this._platform.path().resolve(userDataDir), }; return await this._wrapApiCall(async () => { const result = await this._channel.launchPersistentContext(persistentParams); @@ -124,7 +123,6 @@ export class BrowserType extends ChannelOwner imple return await this._wrapApiCall(async () => { const deadline = params.timeout ? monotonicTime() + params.timeout : 0; const headers = { 'x-playwright-browser': this.name(), ...params.headers }; - const localUtils = this._connection.localUtils(); const connectParams: channels.LocalUtilsConnectParams = { wsEndpoint: params.wsEndpoint, headers, @@ -134,7 +132,7 @@ export class BrowserType extends ChannelOwner imple }; if ((params as any).__testHookRedirectPortForwarding) connectParams.socksProxyRedirectPortForTest = (params as any).__testHookRedirectPortForwarding; - const connection = await localUtils.connect(connectParams); + const connection = await connectOverWebSocket(this._connection, connectParams); let browser: Browser; connection.on('close', () => { // Emulate all pages, contexts and the browser closing upon disconnect. diff --git a/packages/playwright-core/src/client/channelOwner.ts b/packages/playwright-core/src/client/channelOwner.ts index 40bf226c65..92deaecabd 100644 --- a/packages/playwright-core/src/client/channelOwner.ts +++ b/packages/playwright-core/src/client/channelOwner.ts @@ -18,7 +18,6 @@ import { EventEmitter } from './eventEmitter'; import { ValidationError, maybeFindValidator } from '../protocol/validator'; import { isUnderTest } from '../utils/isomorphic/debug'; import { captureLibraryStackTrace, stringifyStackFrames } from '../utils/isomorphic/stackTrace'; -import { zones } from '../utils/zones'; import type { ClientInstrumentation } from './clientInstrumentation'; import type { Connection } from './connection'; @@ -176,7 +175,7 @@ export abstract class ChannelOwner(func: (apiZone: ApiZone) => Promise, isInternal?: boolean): Promise { const logger = this._logger; - const existingApiZone = zones.zoneData('apiZone'); + const existingApiZone = this._platform.zones.current().data(); if (existingApiZone) return await func(existingApiZone); @@ -186,7 +185,7 @@ export abstract class ChannelOwner await func(apiZone)); + const result = await this._platform.zones.current().push(apiZone).run(async () => await func(apiZone)); if (!isInternal) { logApiCall(this._platform, logger, `<= ${apiZone.apiName} succeeded`); this._instrumentation.onApiCallEnd(apiZone); diff --git a/packages/playwright-core/src/client/connection.ts b/packages/playwright-core/src/client/connection.ts index e37c949f1f..9f2745a8ec 100644 --- a/packages/playwright-core/src/client/connection.ts +++ b/packages/playwright-core/src/client/connection.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { EventEmitter } from 'events'; +import { EventEmitter } from './eventEmitter'; import { Android, AndroidDevice, AndroidSocket } from './android'; import { Artifact } from './artifact'; import { Browser } from './browser'; @@ -43,7 +43,6 @@ import { Worker } from './worker'; import { WritableStream } from './writableStream'; import { ValidationError, findValidator } from '../protocol/validator'; import { formatCallLog, rewriteErrorMessage } from '../utils/isomorphic/stackTrace'; -import { zones } from '../utils/zones'; import type { ClientInstrumentation } from './clientInstrumentation'; import type { HeadersArray } from './types'; @@ -109,8 +108,8 @@ export class Connection extends EventEmitter { return this._rawBuffers; } - localUtils(): LocalUtils { - return this._localUtils!; + localUtils(): LocalUtils | undefined { + return this._localUtils; } async initializePlaywright(): Promise { @@ -148,7 +147,7 @@ export class Connection extends EventEmitter { this._localUtils?.addStackToTracingNoReply({ callData: { stack: 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. - zones.empty().run(() => this.onmessage({ ...message, metadata })); + this.platform.zones.empty.run(() => this.onmessage({ ...message, metadata })); return await new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject, apiName, type, method })); } diff --git a/packages/playwright-core/src/client/elementHandle.ts b/packages/playwright-core/src/client/elementHandle.ts index 206fc33084..5c0988f438 100644 --- a/packages/playwright-core/src/client/elementHandle.ts +++ b/packages/playwright-core/src/client/elementHandle.ts @@ -14,16 +14,13 @@ * limitations under the License. */ -import { pipeline } from 'stream'; -import { promisify } from 'util'; - import { Frame } from './frame'; import { JSHandle, parseResult, serializeArgument } from './jsHandle'; import { assert } from '../utils/isomorphic/debug'; -import { fileUploadSizeLimit, mkdirIfNeeded } from '../common/fileUtils'; +import { fileUploadSizeLimit, mkdirIfNeeded } from './fileUtils'; import { isString } from '../utils/isomorphic/rtti'; -import { mime } from '../utilsBundle'; import { WritableStream } from './writableStream'; +import { getMimeTypeForPath } from '../utils/isomorphic/mimeType'; import type { BrowserContext } from './browserContext'; import type { ChannelOwner } from './channelOwner'; @@ -34,8 +31,6 @@ import type * as api from '../../types/types'; import type { Platform } from '../common/platform'; import type * as channels from '@protocol/channels'; -const pipelineAsync = promisify(pipeline); - export class ElementHandle extends JSHandle implements api.ElementHandle { readonly _elementChannel: channels.ElementHandleChannel; @@ -306,7 +301,7 @@ export async function convertInputFiles(platform: Platform, files: string | File }), true); for (let i = 0; i < files.length; i++) { const writable = WritableStream.from(writableStreams[i]); - await pipelineAsync(platform.fs().createReadStream(files[i]), writable.stream()); + await platform.streamFile(files[i], writable.stream()); } return { directoryStream: rootDir, @@ -327,7 +322,7 @@ export async function convertInputFiles(platform: Platform, files: string | File export function determineScreenshotType(options: { path?: string, type?: 'png' | 'jpeg' }): 'png' | 'jpeg' | undefined { if (options.path) { - const mimeType = mime.getType(options.path); + const mimeType = getMimeTypeForPath(options.path); if (mimeType === 'image/png') return 'png'; else if (mimeType === 'image/jpeg') diff --git a/packages/playwright-core/src/client/eventEmitter.ts b/packages/playwright-core/src/client/eventEmitter.ts index ce695490c2..a0781534e3 100644 --- a/packages/playwright-core/src/client/eventEmitter.ts +++ b/packages/playwright-core/src/client/eventEmitter.ts @@ -22,8 +22,6 @@ * USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { EventEmitter as OriginalEventEmitter } from 'events'; - import { isUnderTest } from '../utils/isomorphic/debug'; import type { EventEmitter as EventEmitterType } from 'events'; @@ -32,6 +30,12 @@ type EventType = string | symbol; type Listener = (...args: any[]) => any; type EventMap = Record; +let defaultMaxListenersProvider = () => 10; + +export function setDefaultMaxListenersProvider(provider: () => number) { + defaultMaxListenersProvider = provider; +} + export class EventEmitter implements EventEmitterType { private _events: EventMap | undefined = undefined; @@ -58,7 +62,7 @@ export class EventEmitter implements EventEmitterType { } getMaxListeners(): number { - return this._maxListeners === undefined ? OriginalEventEmitter.defaultMaxListeners : this._maxListeners; + return this._maxListeners === undefined ? defaultMaxListenersProvider() : this._maxListeners; } emit(type: EventType, ...args: any[]): boolean { diff --git a/packages/playwright-core/src/client/fetch.ts b/packages/playwright-core/src/client/fetch.ts index 8e02bc34a2..41070b8665 100644 --- a/packages/playwright-core/src/client/fetch.ts +++ b/packages/playwright-core/src/client/fetch.ts @@ -20,7 +20,7 @@ import { TargetClosedError, isTargetClosedError } from './errors'; import { RawHeaders } from './network'; import { Tracing } from './tracing'; import { assert } from '../utils/isomorphic/debug'; -import { mkdirIfNeeded } from '../common/fileUtils'; +import { mkdirIfNeeded } from './fileUtils'; import { headersObjectToArray } from '../utils/isomorphic/headers'; import { isString } from '../utils/isomorphic/rtti'; diff --git a/packages/playwright-core/src/common/fileUtils.ts b/packages/playwright-core/src/client/fileUtils.ts similarity index 75% rename from packages/playwright-core/src/common/fileUtils.ts rename to packages/playwright-core/src/client/fileUtils.ts index 261b72d84a..0d21b195c0 100644 --- a/packages/playwright-core/src/common/fileUtils.ts +++ b/packages/playwright-core/src/client/fileUtils.ts @@ -14,17 +14,12 @@ * limitations under the License. */ -import type { Platform } from './platform'; +import type { Platform } from '../common/platform'; +// Keep in sync with the server. export const fileUploadSizeLimit = 50 * 1024 * 1024; export async function mkdirIfNeeded(platform: Platform, 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(() => {}); } - -export async function removeFolders(platform: Platform, dirs: string[]): Promise { - return await Promise.all(dirs.map((dir: string) => - platform.fs().promises.rm(dir, { recursive: true, force: true, maxRetries: 10 }).catch(e => e) - )); -} diff --git a/packages/playwright-core/src/client/frame.ts b/packages/playwright-core/src/client/frame.ts index c302da02e4..ecbfe3cb11 100644 --- a/packages/playwright-core/src/client/frame.ts +++ b/packages/playwright-core/src/client/frame.ts @@ -15,8 +15,7 @@ * limitations under the License. */ -import { EventEmitter } from 'events'; - +import { EventEmitter } from './eventEmitter'; import { ChannelOwner } from './channelOwner'; import { addSourceUrlToScript } from './clientHelper'; import { ElementHandle, convertInputFiles, convertSelectOptionValues } from './elementHandle'; diff --git a/packages/playwright-core/src/client/localUtils.ts b/packages/playwright-core/src/client/localUtils.ts index ad66de6814..5ba982251f 100644 --- a/packages/playwright-core/src/client/localUtils.ts +++ b/packages/playwright-core/src/client/localUtils.ts @@ -15,12 +15,8 @@ */ import { ChannelOwner } from './channelOwner'; -import { Connection } from './connection'; -import * as localUtils from '../common/localUtils'; -import type { HeadersArray, Size } from './types'; -import type { HarBackend } from '../common/harBackend'; -import type { Platform } from '../common/platform'; +import type { Size } from './types'; import type * as channels from '@protocol/channels'; type DeviceDescriptor = { @@ -35,8 +31,6 @@ type Devices = { [name: string]: DeviceDescriptor }; export class LocalUtils extends ChannelOwner { readonly devices: Devices; - private _harBackends = new Map(); - private _stackSessions = new Map(); constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.LocalUtilsInitializer) { super(parent, type, guid, initializer); @@ -47,132 +41,34 @@ export class LocalUtils extends ChannelOwner { } async zip(params: channels.LocalUtilsZipParams): Promise { - return await localUtils.zip(this._platform, this._stackSessions, params); + return await this._channel.zip(params); } async harOpen(params: channels.LocalUtilsHarOpenParams): Promise { - return await localUtils.harOpen(this._platform, this._harBackends, params); + return await this._channel.harOpen(params); } async harLookup(params: channels.LocalUtilsHarLookupParams): Promise { - return await localUtils.harLookup(this._harBackends, params); + return await this._channel.harLookup(params); } async harClose(params: channels.LocalUtilsHarCloseParams): Promise { - return await localUtils.harClose(this._harBackends, params); + return await this._channel.harClose(params); } async harUnzip(params: channels.LocalUtilsHarUnzipParams): Promise { - return await localUtils.harUnzip(params); + return await this._channel.harUnzip(params); } async tracingStarted(params: channels.LocalUtilsTracingStartedParams): Promise { - return await localUtils.tracingStarted(this._stackSessions, params); + return await this._channel.tracingStarted(params); } async traceDiscarded(params: channels.LocalUtilsTraceDiscardedParams): Promise { - return await localUtils.traceDiscarded(this._platform, this._stackSessions, params); + return await this._channel.traceDiscarded(params); } async addStackToTracingNoReply(params: channels.LocalUtilsAddStackToTracingNoReplyParams): Promise { - return await localUtils.addStackToTracingNoReply(this._stackSessions, params); - } - - async connect(params: channels.LocalUtilsConnectParams): Promise { - const transport = this._platform.ws ? new WebSocketTransport(this._platform) : new JsonPipeTransport(this); - const connectHeaders = await transport.connect(params); - const connection = new Connection(this, this._platform, this._instrumentation, connectHeaders); - connection.markAsRemote(); - connection.on('close', () => transport.close()); - - let closeError: string | undefined; - const onTransportClosed = (reason?: string) => { - connection.close(reason || closeError); - }; - transport.onClose(reason => onTransportClosed(reason)); - connection.onmessage = message => transport.send(message).catch(() => onTransportClosed()); - transport.onMessage(message => { - try { - connection!.dispatch(message); - } catch (e) { - closeError = String(e); - transport.close(); - } - }); - return connection; - } -} -interface Transport { - connect(params: channels.LocalUtilsConnectParams): Promise; - send(message: any): Promise; - onMessage(callback: (message: object) => void): void; - onClose(callback: (reason?: string) => void): void; - close(): Promise; -} - -class JsonPipeTransport implements Transport { - private _pipe: channels.JsonPipeChannel | undefined; - private _owner: ChannelOwner; - - constructor(owner: ChannelOwner) { - this._owner = owner; - } - - async connect(params: channels.LocalUtilsConnectParams) { - const { pipe, headers: connectHeaders } = await this._owner._wrapApiCall(async () => { - return await this._owner._channel.connect(params); - }, /* isInternal */ true); - this._pipe = pipe; - return connectHeaders; - } - - async send(message: object) { - this._owner._wrapApiCall(async () => { - await this._pipe!.send({ message }); - }, /* isInternal */ true); - } - - onMessage(callback: (message: object) => void) { - this._pipe!.on('message', ({ message }) => callback(message)); - } - - onClose(callback: (reason?: string) => void) { - this._pipe!.on('closed', ({ reason }) => callback(reason)); - } - - async close() { - await this._owner._wrapApiCall(async () => { - await this._pipe!.close().catch(() => {}); - }, /* isInternal */ true); - } -} - -class WebSocketTransport implements Transport { - private _platform: Platform; - private _ws: WebSocket | undefined; - - constructor(platform: Platform) { - this._platform = platform; - } - - async connect(params: channels.LocalUtilsConnectParams) { - this._ws = this._platform.ws!(params.wsEndpoint); - return []; - } - - async send(message: object) { - this._ws!.send(JSON.stringify(message)); - } - - onMessage(callback: (message: object) => void) { - this._ws!.addEventListener('message', event => callback(JSON.parse(event.data))); - } - - onClose(callback: (reason?: string) => void) { - this._ws!.addEventListener('close', () => callback()); - } - - async close() { - this._ws!.close(); + return await this._channel.addStackToTracingNoReply(params); } } diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts index a2c64c08b1..a4f5ff68d0 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import { URLSearchParams } from 'url'; - import { ChannelOwner } from './channelOwner'; import { isTargetClosedError } from './errors'; import { Events } from './events'; @@ -30,8 +28,7 @@ import { LongStandingScope, ManualPromise } from '../utils/isomorphic/manualProm import { MultiMap } from '../utils/isomorphic/multimap'; import { isRegExp, isString } from '../utils/isomorphic/rtti'; import { rewriteErrorMessage } from '../utils/isomorphic/stackTrace'; -import { zones } from '../utils/zones'; -import { mime } from '../utilsBundle'; +import { getMimeTypeForPath } from '../utils/isomorphic/mimeType'; import type { BrowserContext } from './browserContext'; import type { Page } from './page'; @@ -40,8 +37,8 @@ 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'; +import type { Platform, Zone } from '../common/platform'; export type NetworkCookie = { name: string, @@ -414,7 +411,7 @@ export class Route extends ChannelOwner implements api.Ro else if (options.json) headers['content-type'] = 'application/json'; else if (options.path) - headers['content-type'] = mime.getType(options.path) || 'application/octet-stream'; + headers['content-type'] = getMimeTypeForPath(options.path) || 'application/octet-stream'; if (length && !('content-length' in headers)) headers['content-length'] = String(length); @@ -821,14 +818,14 @@ export class RouteHandler { readonly handler: RouteHandlerCallback; private _ignoreException: boolean = false; private _activeInvocations: Set<{ complete: Promise, route: Route }> = new Set(); - private _svedZone: Zone; + private _savedZone: Zone; - constructor(baseURL: string | undefined, url: URLMatch, handler: RouteHandlerCallback, times: number = Number.MAX_SAFE_INTEGER) { + constructor(platform: Platform, 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._svedZone = zones.current().without('apiZone'); + this._savedZone = platform.zones.current().pop(); } static prepareInterceptionPatterns(handlers: RouteHandler[]) { @@ -852,7 +849,7 @@ export class RouteHandler { } public async handle(route: Route): Promise { - return await this._svedZone.run(async () => this._handleImpl(route)); + return await this._savedZone.run(async () => this._handleImpl(route)); } private async _handleImpl(route: Route): Promise { diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index b954cca502..cb6d0656b8 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -35,7 +35,7 @@ import { Waiter } from './waiter'; import { Worker } from './worker'; import { TimeoutSettings } from '../utils/isomorphic/timeoutSettings'; import { assert } from '../utils/isomorphic/debug'; -import { mkdirIfNeeded } from '../common/fileUtils'; +import { mkdirIfNeeded } from './fileUtils'; import { headersObjectToArray } from '../utils/isomorphic/headers'; import { trimStringWithEllipsis } from '../utils/isomorphic/stringUtils'; import { urlMatches, urlMatchesEqual } from '../utils/isomorphic/urlMatch'; @@ -520,16 +520,19 @@ export class Page extends ChannelOwner implements api.Page } async route(url: URLMatch, handler: RouteHandlerCallback, options: { times?: number } = {}): Promise { - this._routes.unshift(new RouteHandler(this._browserContext._options.baseURL, url, handler, options.times)); + this._routes.unshift(new RouteHandler(this._platform, this._browserContext._options.baseURL, url, handler, options.times)); await this._updateInterceptionPatterns(); } async routeFromHAR(har: string, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean, updateContent?: 'attach' | 'embed', updateMode?: 'minimal' | 'full'} = {}): Promise { + const localUtils = this._connection.localUtils(); + if (!localUtils) + throw new Error('Route from har is not supported in thin clients'); if (options.update) { await this._browserContext._recordIntoHAR(har, this, options); return; } - const harRouter = await HarRouter.create(this._connection.localUtils(), har, options.notFound || 'abort', { urlMatch: options.url }); + const harRouter = await HarRouter.create(localUtils, har, options.notFound || 'abort', { urlMatch: options.url }); this._harRouters.push(harRouter); await harRouter.addPageRoute(this); } @@ -796,7 +799,7 @@ export class Page extends ChannelOwner implements api.Page } async pause(_options?: { __testHookKeepTestTimeout: boolean }) { - if (require('inspector').url()) + if (this._platform.isDebuggerAttached()) return; const defaultNavigationTimeout = this._browserContext._timeoutSettings.defaultNavigationTimeout(); const defaultTimeout = this._browserContext._timeoutSettings.defaultTimeout(); diff --git a/packages/playwright-core/src/client/stream.ts b/packages/playwright-core/src/client/stream.ts index 4f43b9afad..97c1e4b634 100644 --- a/packages/playwright-core/src/client/stream.ts +++ b/packages/playwright-core/src/client/stream.ts @@ -30,29 +30,6 @@ export class Stream extends ChannelOwner { } stream(): Readable { - return new StreamImpl(this._channel); - } -} - -class StreamImpl 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); + return this._platform.streamReadable(this._channel); } } diff --git a/packages/playwright-core/src/client/tracing.ts b/packages/playwright-core/src/client/tracing.ts index 61124107fa..5b80f30a0a 100644 --- a/packages/playwright-core/src/client/tracing.ts +++ b/packages/playwright-core/src/client/tracing.ts @@ -69,8 +69,8 @@ export class Tracing extends ChannelOwner implements ap this._isTracing = true; this._connection.setIsTracing(true); } - const result = await this._connection.localUtils().tracingStarted({ tracesDir: this._tracesDir, traceName }); - this._stacksId = result.stacksId; + const result = await this._connection.localUtils()?.tracingStarted({ tracesDir: this._tracesDir, traceName }); + this._stacksId = result?.stacksId; } async stopChunk(options: { path?: string } = {}) { @@ -89,15 +89,19 @@ export class Tracing extends ChannelOwner implements ap // Not interested in artifacts. await this._channel.tracingStopChunk({ mode: 'discard' }); if (this._stacksId) - await this._connection.localUtils().traceDiscarded({ stacksId: this._stacksId }); + await this._connection.localUtils()!.traceDiscarded({ stacksId: this._stacksId }); return; } + const localUtils = this._connection.localUtils(); + if (!localUtils) + throw new Error('Cannot save trace in thin clients'); + const isLocal = !this._connection.isRemote(); if (isLocal) { const result = await this._channel.tracingStopChunk({ mode: 'entries' }); - await this._connection.localUtils().zip({ zipFile: filePath, entries: result.entries!, mode: 'write', stacksId: this._stacksId, includeSources: this._includeSources }); + await localUtils.zip({ zipFile: filePath, entries: result.entries!, mode: 'write', stacksId: this._stacksId, includeSources: this._includeSources }); return; } @@ -106,7 +110,7 @@ export class Tracing extends ChannelOwner implements ap // The artifact may be missing if the browser closed while stopping tracing. if (!result.artifact) { if (this._stacksId) - await this._connection.localUtils().traceDiscarded({ stacksId: this._stacksId }); + await localUtils.traceDiscarded({ stacksId: this._stacksId }); return; } @@ -115,7 +119,7 @@ export class Tracing extends ChannelOwner implements ap await artifact.saveAs(filePath); await artifact.delete(); - await this._connection.localUtils().zip({ zipFile: filePath, entries: [], mode: 'append', stacksId: this._stacksId, includeSources: this._includeSources }); + await localUtils.zip({ zipFile: filePath, entries: [], mode: 'append', stacksId: this._stacksId, includeSources: this._includeSources }); } _resetStackCounter() { diff --git a/packages/playwright-core/src/client/waiter.ts b/packages/playwright-core/src/client/waiter.ts index e17f93d940..7d07d2128e 100644 --- a/packages/playwright-core/src/client/waiter.ts +++ b/packages/playwright-core/src/client/waiter.ts @@ -16,12 +16,11 @@ import { TimeoutError } from './errors'; import { rewriteErrorMessage } from '../utils/isomorphic/stackTrace'; -import { zones } from '../utils/zones'; import type { ChannelOwner } from './channelOwner'; -import type { Zone } from '../utils/zones'; import type * as channels from '@protocol/channels'; import type { EventEmitter } from 'events'; +import type { Zone } from '../common/platform'; export class Waiter { private _dispose: (() => void)[]; @@ -36,7 +35,7 @@ export class Waiter { constructor(channelOwner: ChannelOwner, event: string) { this._waitId = channelOwner._platform.createGuid(); this._channelOwner = channelOwner; - this._savedZone = zones.current().without('apiZone'); + this._savedZone = channelOwner._platform.zones.current().pop(); this._channelOwner._channel.waitForEventInfo({ info: { waitId: this._waitId, phase: 'before', event } }).catch(() => {}); this._dispose = [ diff --git a/packages/playwright-core/src/client/webSocket.ts b/packages/playwright-core/src/client/webSocket.ts new file mode 100644 index 0000000000..cef07a4dfa --- /dev/null +++ b/packages/playwright-core/src/client/webSocket.ts @@ -0,0 +1,116 @@ +/** + * 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 { ChannelOwner } from './channelOwner'; +import { Connection } from './connection'; + +import type { HeadersArray } from './types'; +import type * as channels from '@protocol/channels'; + +export async function connectOverWebSocket(parentConnection: Connection, params: channels.LocalUtilsConnectParams): Promise { + const localUtils = parentConnection.localUtils(); + const transport = localUtils ? new JsonPipeTransport(localUtils) : new WebSocketTransport(); + const connectHeaders = await transport.connect(params); + const connection = new Connection(localUtils, parentConnection.platform, parentConnection._instrumentation, connectHeaders); + connection.markAsRemote(); + connection.on('close', () => transport.close()); + + let closeError: string | undefined; + const onTransportClosed = (reason?: string) => { + connection.close(reason || closeError); + }; + transport.onClose(reason => onTransportClosed(reason)); + connection.onmessage = message => transport.send(message).catch(() => onTransportClosed()); + transport.onMessage(message => { + try { + connection!.dispatch(message); + } catch (e) { + closeError = String(e); + transport.close(); + } + }); + return connection; +} + +interface Transport { + connect(params: channels.LocalUtilsConnectParams): Promise; + send(message: any): Promise; + onMessage(callback: (message: object) => void): void; + onClose(callback: (reason?: string) => void): void; + close(): Promise; +} + +class JsonPipeTransport implements Transport { + private _pipe: channels.JsonPipeChannel | undefined; + private _owner: ChannelOwner; + + constructor(owner: ChannelOwner) { + this._owner = owner; + } + + async connect(params: channels.LocalUtilsConnectParams) { + const { pipe, headers: connectHeaders } = await this._owner._wrapApiCall(async () => { + return await this._owner._channel.connect(params); + }, /* isInternal */ true); + this._pipe = pipe; + return connectHeaders; + } + + async send(message: object) { + this._owner._wrapApiCall(async () => { + await this._pipe!.send({ message }); + }, /* isInternal */ true); + } + + onMessage(callback: (message: object) => void) { + this._pipe!.on('message', ({ message }) => callback(message)); + } + + onClose(callback: (reason?: string) => void) { + this._pipe!.on('closed', ({ reason }) => callback(reason)); + } + + async close() { + await this._owner._wrapApiCall(async () => { + await this._pipe!.close().catch(() => {}); + }, /* isInternal */ true); + } +} + +class WebSocketTransport implements Transport { + private _ws: WebSocket | undefined; + + async connect(params: channels.LocalUtilsConnectParams) { + this._ws = new window.WebSocket(params.wsEndpoint); + return []; + } + + async send(message: object) { + this._ws!.send(JSON.stringify(message)); + } + + onMessage(callback: (message: object) => void) { + this._ws!.addEventListener('message', event => callback(JSON.parse(event.data))); + } + + onClose(callback: (reason?: string) => void) { + this._ws!.addEventListener('close', () => callback()); + } + + async close() { + this._ws!.close(); + } +} diff --git a/packages/playwright-core/src/client/writableStream.ts b/packages/playwright-core/src/client/writableStream.ts index 66cf17201d..38a2d0214a 100644 --- a/packages/playwright-core/src/client/writableStream.ts +++ b/packages/playwright-core/src/client/writableStream.ts @@ -14,11 +14,10 @@ * limitations under the License. */ -import { Writable } from 'stream'; - import { ChannelOwner } from './channelOwner'; import type * as channels from '@protocol/channels'; +import type { Writable } from 'stream'; export class WritableStream extends ChannelOwner { static from(Stream: channels.WritableStreamChannel): WritableStream { @@ -30,26 +29,6 @@ export class WritableStream extends ChannelOwner } stream(): Writable { - 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); + return this._platform.streamWritable(this._channel); } } diff --git a/packages/playwright-core/src/common/DEPS.list b/packages/playwright-core/src/common/DEPS.list index a25dd41c36..686b88087b 100644 --- a/packages/playwright-core/src/common/DEPS.list +++ b/packages/playwright-core/src/common/DEPS.list @@ -1,5 +1,2 @@ [*] -../utils/ ../utils/isomorphic/ -../utilsBundle.ts -../zipBundle.ts diff --git a/packages/playwright-core/src/common/platform.ts b/packages/playwright-core/src/common/platform.ts index 64cef77aab..25819b9a6f 100644 --- a/packages/playwright-core/src/common/platform.ts +++ b/packages/playwright-core/src/common/platform.ts @@ -14,64 +14,50 @@ * limitations under the License. */ -import * as crypto from 'crypto'; -import * as fs from 'fs'; -import * as path from 'path'; - import { webColors, noColors } from '../utils/isomorphic/colors'; +import type * as fs from 'fs'; +import type * as path from 'path'; import type { Colors } from '../utils/isomorphic/colors'; +import type { Readable, Writable } from 'stream'; +import type * as channels from '@protocol/channels'; +export type Zone = { + push(data: unknown): Zone; + pop(): Zone; + run(func: () => R): R; + data(): T | undefined; +}; + +const noopZone: Zone = { + push: () => noopZone, + pop: () => noopZone, + run: func => func(), + data: () => undefined, +}; export type Platform = { + name: 'node' | 'web' | 'empty'; + calculateSha1(text: string): Promise; colors: Colors; createGuid: () => string; fs: () => typeof fs; inspectCustom: symbol | undefined; + isDebuggerAttached(): boolean; isLogEnabled(name: 'api' | 'channel'): boolean; log(name: 'api' | 'channel', message: string | Error | object): void; path: () => typeof path; pathSeparator: string; - ws?: (url: string) => WebSocket; -}; - -export const webPlatform: Platform = { - calculateSha1: async (text: string) => { - const bytes = new TextEncoder().encode(text); - const hashBuffer = await crypto.subtle.digest('SHA-1', bytes); - return Array.from(new Uint8Array(hashBuffer), b => b.toString(16).padStart(2, '0')).join(''); - }, - - colors: webColors, - - createGuid: () => { - return Array.from(crypto.getRandomValues(new Uint8Array(16)), b => b.toString(16).padStart(2, '0')).join(''); - }, - - fs: () => { - throw new Error('File system is not available'); - }, - - inspectCustom: undefined, - - - isLogEnabled(name: 'api' | 'channel') { - return false; - }, - - log(name: 'api' | 'channel', message: string | Error | object) {}, - - path: () => { - throw new Error('Path module is not available'); - }, - - pathSeparator: '/', - - ws: (url: string) => new WebSocket(url), + streamFile(path: string, writable: Writable): Promise, + streamReadable: (channel: channels.StreamChannel) => Readable, + streamWritable: (channel: channels.WritableStreamChannel) => Writable, + zones: { empty: Zone, current: () => Zone; }; }; export const emptyPlatform: Platform = { + name: 'empty', + calculateSha1: async () => { throw new Error('Not implemented'); }, @@ -88,6 +74,8 @@ export const emptyPlatform: Platform = { inspectCustom: undefined, + isDebuggerAttached: () => false, + isLogEnabled(name: 'api' | 'channel') { return false; }, @@ -98,5 +86,37 @@ export const emptyPlatform: Platform = { throw new Error('Function not implemented.'); }, - pathSeparator: '/' + pathSeparator: '/', + + streamFile(path: string, writable: Writable): Promise { + throw new Error('Streams are not available'); + }, + + streamReadable: (channel: channels.StreamChannel) => { + throw new Error('Streams are not available'); + }, + + streamWritable: (channel: channels.WritableStreamChannel) => { + throw new Error('Streams are not available'); + }, + + zones: { empty: noopZone, current: () => noopZone }, +}; + +export const webPlatform: Platform = { + ...emptyPlatform, + + name: 'web', + + 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(''); + }, + + colors: webColors, + + createGuid: () => { + return Array.from(window.crypto.getRandomValues(new Uint8Array(16)), b => b.toString(16).padStart(2, '0')).join(''); + }, }; diff --git a/packages/playwright-core/src/common/progress.ts b/packages/playwright-core/src/common/progress.ts deleted file mode 100644 index f09670e823..0000000000 --- a/packages/playwright-core/src/common/progress.ts +++ /dev/null @@ -1,23 +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. - */ - -export interface Progress { - log(message: string): void; - timeUntilDeadline(): number; - isRunning(): boolean; - cleanupWhenAborted(cleanup: () => any): void; - throwIfAborted(): void; -} diff --git a/packages/playwright-core/src/inProcessFactory.ts b/packages/playwright-core/src/inProcessFactory.ts index 81b40afe19..a53aa8c2e3 100644 --- a/packages/playwright-core/src/inProcessFactory.ts +++ b/packages/playwright-core/src/inProcessFactory.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import path from 'path'; +import * as path from 'path'; +import { EventEmitter } from 'events'; import { AndroidServerLauncherImpl } from './androidServerImpl'; import { BrowserServerLauncherImpl } from './browserServerImpl'; @@ -25,6 +26,7 @@ import { setDebugMode } from './utils/isomorphic/debug'; import { getFromENV } from './server/utils/env'; import { nodePlatform } from './server/utils/nodePlatform'; import { setPlatformForSelectors } from './client/selectors'; +import { setDefaultMaxListenersProvider } from './client/eventEmitter'; import type { Playwright as PlaywrightAPI } from './client/playwright'; import type { Language } from './utils'; @@ -35,6 +37,7 @@ export function createInProcessPlaywright(platform: Platform): PlaywrightAPI { const playwright = createPlaywright({ sdkLanguage: (process.env.PW_LANG_NAME as Language | undefined) || 'javascript' }); setDebugMode(getFromENV('PWDEBUG') || ''); setPlatformForSelectors(nodePlatform); + setDefaultMaxListenersProvider(() => EventEmitter.defaultMaxListeners); setLibraryStackPrefix(path.join(__dirname, '..')); diff --git a/packages/playwright-core/src/outofprocess.ts b/packages/playwright-core/src/outofprocess.ts index c24decb900..c2427b818d 100644 --- a/packages/playwright-core/src/outofprocess.ts +++ b/packages/playwright-core/src/outofprocess.ts @@ -18,7 +18,7 @@ import * as childProcess from 'child_process'; import * as path from 'path'; import { Connection } from './client/connection'; -import { PipeTransport } from './utils/pipeTransport'; +import { PipeTransport } from './server/utils/pipeTransport'; import { ManualPromise } from './utils/isomorphic/manualPromise'; import { nodePlatform } from './server/utils/nodePlatform'; diff --git a/packages/playwright-core/src/protocol/DEPS.list b/packages/playwright-core/src/protocol/DEPS.list index bf64b324f1..dbdeafe86c 100644 --- a/packages/playwright-core/src/protocol/DEPS.list +++ b/packages/playwright-core/src/protocol/DEPS.list @@ -1,4 +1,3 @@ [*] ../common/ -../utils/ - +../utils/isomorphic diff --git a/packages/playwright-core/src/protocol/validatorPrimitives.ts b/packages/playwright-core/src/protocol/validatorPrimitives.ts index 9d4614512b..f57a63acea 100644 --- a/packages/playwright-core/src/protocol/validatorPrimitives.ts +++ b/packages/playwright-core/src/protocol/validatorPrimitives.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { isUnderTest } from '../utils'; +import { isUnderTest } from '../utils/isomorphic/debug'; export class ValidationError extends Error {} export type Validator = (arg: any, path: string, context: ValidatorContext) => any; diff --git a/packages/playwright-core/src/server/android/android.ts b/packages/playwright-core/src/server/android/android.ts index c1516dea76..a4bf8f8ded 100644 --- a/packages/playwright-core/src/server/android/android.ts +++ b/packages/playwright-core/src/server/android/android.ts @@ -20,11 +20,11 @@ import * as os from 'os'; import * as path from 'path'; import { TimeoutSettings } from '../../utils/isomorphic/timeoutSettings'; -import { PipeTransport } from '../../utils/pipeTransport'; +import { PipeTransport } from '../utils/pipeTransport'; import { createGuid } from '../utils/crypto'; import { isUnderTest } from '../../utils/isomorphic/debug'; import { getPackageManagerExecCommand } from '../utils/env'; -import { makeWaitForNextTask } from '../../utils/task'; +import { makeWaitForNextTask } from '../utils/task'; import { RecentLogsCollector } from '../utils/debugLogger'; import { debug } from '../../utilsBundle'; import { wsReceiver, wsSender } from '../../utilsBundle'; diff --git a/packages/playwright-core/src/server/bidi/bidiPage.ts b/packages/playwright-core/src/server/bidi/bidiPage.ts index badd68d1a1..153bb830de 100644 --- a/packages/playwright-core/src/server/bidi/bidiPage.ts +++ b/packages/playwright-core/src/server/bidi/bidiPage.ts @@ -520,7 +520,8 @@ export class BidiPage implements PageDelegate { } async setInputFiles(handle: dom.ElementHandle, files: types.FilePayload[]): Promise { - throw new Error('Setting FilePayloads is not supported in Bidi.'); + await handle.evaluateInUtility(([injected, node, files]) => + injected.setInputFiles(node, files), files); } async setInputFilePaths(handle: dom.ElementHandle, paths: string[]): Promise { diff --git a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts index b582aa8788..1c74b7258c 100644 --- a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts @@ -16,8 +16,7 @@ import { Dispatcher } from './dispatcher'; import { SdkObject } from '../../server/instrumentation'; -import * as localUtils from '../../common/localUtils'; -import { nodePlatform } from '../utils/nodePlatform'; +import * as localUtils from '../localUtils'; import { getUserAgent } from '../utils/userAgent'; import { deviceDescriptors as descriptors } from '../deviceDescriptors'; import { JsonPipeDispatcher } from '../dispatchers/jsonPipeDispatcher'; @@ -26,7 +25,7 @@ import { SocksInterceptor } from '../socksInterceptor'; import { WebSocketTransport } from '../transport'; import { fetchData } from '../utils/network'; -import type { HarBackend } from '../../common/harBackend'; +import type { HarBackend } from '../harBackend'; import type { CallMetadata } from '../instrumentation'; import type { Playwright } from '../playwright'; import type { RootDispatcher } from './dispatcher'; @@ -50,11 +49,11 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels. } async zip(params: channels.LocalUtilsZipParams): Promise { - return await localUtils.zip(nodePlatform, this._stackSessions, params); + return await localUtils.zip(this._stackSessions, params); } async harOpen(params: channels.LocalUtilsHarOpenParams, metadata: CallMetadata): Promise { - return await localUtils.harOpen(nodePlatform, this._harBackends, params); + return await localUtils.harOpen(this._harBackends, params); } async harLookup(params: channels.LocalUtilsHarLookupParams, metadata: CallMetadata): Promise { @@ -74,7 +73,7 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels. } async traceDiscarded(params: channels.LocalUtilsTraceDiscardedParams, metadata?: CallMetadata | undefined): Promise { - return await localUtils.traceDiscarded(nodePlatform, this._stackSessions, params); + return await localUtils.traceDiscarded(this._stackSessions, params); } async addStackToTracingNoReply(params: channels.LocalUtilsAddStackToTracingNoReplyParams, metadata?: CallMetadata | undefined): Promise { diff --git a/packages/playwright-core/src/server/fileUploadUtils.ts b/packages/playwright-core/src/server/fileUploadUtils.ts index 908e7644ac..5a15617c8b 100644 --- a/packages/playwright-core/src/server/fileUploadUtils.ts +++ b/packages/playwright-core/src/server/fileUploadUtils.ts @@ -18,7 +18,6 @@ import * as fs from 'fs'; import * as path from 'path'; import { assert } from '../utils/isomorphic/debug'; -import { fileUploadSizeLimit } from '../common/fileUtils'; import { mime } from '../utilsBundle'; import type { WritableStreamDispatcher } from './dispatchers/writableStreamDispatcher'; @@ -27,6 +26,9 @@ import type { Frame } from './frames'; import type * as types from './types'; import type * as channels from '@protocol/channels'; +// Keep in sync with the client. +export const fileUploadSizeLimit = 50 * 1024 * 1024; + async function filesExceedUploadLimit(files: string[]) { const sizes = await Promise.all(files.map(async file => (await fs.promises.stat(file)).size)); return sizes.reduce((total, size) => total + size, 0) >= fileUploadSizeLimit; diff --git a/packages/playwright-core/src/common/harBackend.ts b/packages/playwright-core/src/server/harBackend.ts similarity index 92% rename from packages/playwright-core/src/common/harBackend.ts rename to packages/playwright-core/src/server/harBackend.ts index f59b7de5f1..0814be9c5f 100644 --- a/packages/playwright-core/src/common/harBackend.ts +++ b/packages/playwright-core/src/server/harBackend.ts @@ -14,11 +14,14 @@ * limitations under the License. */ -import { ZipFile } from '../utils/zipFile'; +import * as fs from 'fs'; +import * as path from 'path'; -import type { HeadersArray } from './types'; +import { createGuid } from './utils/crypto'; +import { ZipFile } from './utils/zipFile'; + +import type { HeadersArray } from '../common/types'; import type * as har from '@trace/har'; -import type { Platform } from './platform'; const redirectStatus = [301, 302, 303, 307, 308]; @@ -27,11 +30,9 @@ export class HarBackend { private _harFile: har.HARFile; private _zipFile: ZipFile | null; private _baseDir: string | null; - private _platform: Platform; - constructor(platform: Platform, harFile: har.HARFile, baseDir: string | null, zipFile: ZipFile | null) { - this._platform = platform; - this.id = platform.createGuid(); + constructor(harFile: har.HARFile, baseDir: string | null, zipFile: ZipFile | null) { + this.id = createGuid(); this._harFile = harFile; this._baseDir = baseDir; this._zipFile = zipFile; @@ -79,7 +80,7 @@ export class HarBackend { if (this._zipFile) buffer = await this._zipFile.read(file); else - buffer = await this._platform.fs().promises.readFile(this._platform.path().resolve(this._baseDir!, file)); + buffer = await fs.promises.readFile(path.resolve(this._baseDir!, file)); } else { buffer = Buffer.from(content.text || '', content.encoding === 'base64' ? 'base64' : 'utf-8'); } diff --git a/packages/playwright-core/src/common/localUtils.ts b/packages/playwright-core/src/server/localUtils.ts similarity index 84% rename from packages/playwright-core/src/common/localUtils.ts rename to packages/playwright-core/src/server/localUtils.ts index 5020d82b75..1f5f0064b5 100644 --- a/packages/playwright-core/src/common/localUtils.ts +++ b/packages/playwright-core/src/server/localUtils.ts @@ -18,15 +18,15 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { removeFolders } from './fileUtils'; +import { calculateSha1 } from './utils/crypto'; import { HarBackend } from './harBackend'; import { ManualPromise } from '../utils/isomorphic/manualPromise'; -import { ZipFile } from '../utils/zipFile'; +import { ZipFile } from './utils/zipFile'; import { yauzl, yazl } from '../zipBundle'; import { serializeClientSideCallMetadata } from '../utils/isomorphic/traceUtils'; import { assert } from '../utils/isomorphic/debug'; +import { removeFolders } from './utils/fileUtils'; -import type { Platform } from './platform'; import type * as channels from '@protocol/channels'; import type * as har from '@trace/har'; import type EventEmitter from 'events'; @@ -39,7 +39,7 @@ export type StackSession = { callStacks: channels.ClientSideCallMetadata[]; }; -export async function zip(platform: Platform, stackSessions: Map, params: channels.LocalUtilsZipParams): Promise { +export async function zip(stackSessions: Map, params: channels.LocalUtilsZipParams): Promise { const promise = new ManualPromise(); const zipFile = new yazl.ZipFile(); (zipFile as any as EventEmitter).on('error', error => promise.reject(error)); @@ -77,7 +77,7 @@ export async function zip(platform: Platform, stackSessions: Map promise.reject(error)); }); await promise; - await deleteStackSession(platform, stackSessions, params.stacksId); + await deleteStackSession(stackSessions, params.stacksId); return; } @@ -124,20 +124,20 @@ export async function zip(platform: Platform, stackSessions: Map, stacksId?: string) { +async function deleteStackSession(stackSessions: Map, stacksId?: string) { const session = stacksId ? stackSessions.get(stacksId) : undefined; if (!session) return; await session.writer; if (session.tmpDir) - await removeFolders(platform, [session.tmpDir]); + await removeFolders([session.tmpDir]); stackSessions.delete(stacksId!); } -export async function harOpen(platform: Platform, harBackends: Map, params: channels.LocalUtilsHarOpenParams): Promise { +export async function harOpen(harBackends: Map, params: channels.LocalUtilsHarOpenParams): Promise { let harBackend: HarBackend; if (params.file.endsWith('.zip')) { const zipFile = new ZipFile(params.file); @@ -147,10 +147,10 @@ export async function harOpen(platform: Platform, harBackends: Map, p return { stacksId: traceStacksFile }; } -export async function traceDiscarded(platform: Platform, stackSessions: Map, params: channels.LocalUtilsTraceDiscardedParams): Promise { - await deleteStackSession(platform, stackSessions, params.stacksId); +export async function traceDiscarded(stackSessions: Map, params: channels.LocalUtilsTraceDiscardedParams): Promise { + await deleteStackSession(stackSessions, params.stacksId); } export async function addStackToTracingNoReply(stackSessions: Map, params: channels.LocalUtilsAddStackToTracingNoReplyParams): Promise { diff --git a/packages/playwright-core/src/server/progress.ts b/packages/playwright-core/src/server/progress.ts index 30db144fbb..16dcc6fb1f 100644 --- a/packages/playwright-core/src/server/progress.ts +++ b/packages/playwright-core/src/server/progress.ts @@ -19,10 +19,14 @@ import { assert, monotonicTime } from '../utils'; import { ManualPromise } from '../utils/isomorphic/manualPromise'; import type { CallMetadata, Instrumentation, SdkObject } from './instrumentation'; -import type { Progress as CommonProgress } from '../common/progress'; import type { LogName } from './utils/debugLogger'; -export interface Progress extends CommonProgress { +export interface Progress { + log(message: string): void; + timeUntilDeadline(): number; + isRunning(): boolean; + cleanupWhenAborted(cleanup: () => any): void; + throwIfAborted(): void; metadata: CallMetadata; } diff --git a/packages/playwright-core/src/server/utils/nodePlatform.ts b/packages/playwright-core/src/server/utils/nodePlatform.ts index d4b8b5d7ad..fcdfebde2a 100644 --- a/packages/playwright-core/src/server/utils/nodePlatform.ts +++ b/packages/playwright-core/src/server/utils/nodePlatform.ts @@ -18,12 +18,45 @@ import * as crypto from 'crypto'; import * as fs from 'fs'; import * as path from 'path'; import * as util from 'util'; +import { Readable, Writable, pipeline } from 'stream'; import { colors } from '../../utilsBundle'; -import { Platform } from '../../common/platform'; import { debugLogger } from './debugLogger'; +import { currentZone, emptyZone } from './zones'; + +import type { Platform, Zone } from '../../common/platform'; +import type { Zone as ZoneImpl } from './zones'; +import type * as channels from '@protocol/channels'; + +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'); + } +} export const nodePlatform: Platform = { + name: 'node', + calculateSha1: (text: string) => { const sha1 = crypto.createHash('sha1'); sha1.update(text); @@ -38,6 +71,8 @@ export const nodePlatform: Platform = { inspectCustom: util.inspect.custom, + isDebuggerAttached: () => !!require('inspector').url(), + isLogEnabled(name: 'api' | 'channel') { return debugLogger.isEnabled(name); }, @@ -48,5 +83,65 @@ export const nodePlatform: Platform = { path: () => path, - pathSeparator: path.sep + pathSeparator: path.sep, + + async streamFile(path: string, stream: Writable): Promise { + await pipelineAsync(fs.createReadStream(path), stream); + }, + + streamReadable: (channel: channels.StreamChannel) => { + return new ReadableStreamImpl(channel); + }, + + streamWritable: (channel: channels.WritableStreamChannel) => { + return new WritableStreamImpl(channel); + }, + + zones: { + current: () => new NodeZone(currentZone()), + empty: new NodeZone(emptyZone), + } }; + +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); + } +} + +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/utils/pipeTransport.ts b/packages/playwright-core/src/server/utils/pipeTransport.ts similarity index 100% rename from packages/playwright-core/src/utils/pipeTransport.ts rename to packages/playwright-core/src/server/utils/pipeTransport.ts diff --git a/packages/playwright-core/src/utils/task.ts b/packages/playwright-core/src/server/utils/task.ts similarity index 100% rename from packages/playwright-core/src/utils/task.ts rename to packages/playwright-core/src/server/utils/task.ts diff --git a/packages/playwright-core/src/utils/zipFile.ts b/packages/playwright-core/src/server/utils/zipFile.ts similarity index 95% rename from packages/playwright-core/src/utils/zipFile.ts rename to packages/playwright-core/src/server/utils/zipFile.ts index 1f70c5e1bc..43b5a3430c 100644 --- a/packages/playwright-core/src/utils/zipFile.ts +++ b/packages/playwright-core/src/server/utils/zipFile.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { yauzl } from '../zipBundle'; +import { yauzl } from '../../zipBundle'; -import type { Entry, UnzipFile } from '../zipBundle'; +import type { Entry, UnzipFile } from '../../zipBundle'; export class ZipFile { private _fileName: string; diff --git a/packages/playwright-core/src/utils/zones.ts b/packages/playwright-core/src/server/utils/zones.ts similarity index 63% rename from packages/playwright-core/src/utils/zones.ts rename to packages/playwright-core/src/server/utils/zones.ts index 32664c3898..b5860167b0 100644 --- a/packages/playwright-core/src/utils/zones.ts +++ b/packages/playwright-core/src/server/utils/zones.ts @@ -18,36 +18,13 @@ import { AsyncLocalStorage } from 'async_hooks'; export type ZoneType = 'apiZone' | 'stepZone'; -class ZoneManager { - private readonly _asyncLocalStorage = new AsyncLocalStorage(); - private readonly _emptyZone = Zone.createEmpty(this._asyncLocalStorage); - - run(type: ZoneType, data: T, func: () => R): R { - return this.current().with(type, data).run(func); - } - - zoneData(type: ZoneType): T | undefined { - return this.current().data(type); - } - - current(): Zone { - return this._asyncLocalStorage.getStore() ?? this._emptyZone; - } - - empty(): Zone { - return this._emptyZone; - } -} +const asyncLocalStorage = new AsyncLocalStorage(); export class Zone { private readonly _asyncLocalStorage: AsyncLocalStorage; private readonly _data: ReadonlyMap; - static createEmpty(asyncLocalStorage: AsyncLocalStorage) { - return new Zone(asyncLocalStorage, new Map()); - } - - private constructor(asyncLocalStorage: AsyncLocalStorage, store: Map) { + constructor(asyncLocalStorage: AsyncLocalStorage, store: Map) { this._asyncLocalStorage = asyncLocalStorage; this._data = store; } @@ -71,4 +48,8 @@ export class Zone { } } -export const zones = new ZoneManager(); +export const emptyZone = new Zone(asyncLocalStorage, new Map()); + +export function currentZone(): Zone { + return asyncLocalStorage.getStore() ?? emptyZone; +} diff --git a/packages/playwright-core/src/utils.ts b/packages/playwright-core/src/utils.ts index 1a0c446c6b..dc5f284955 100644 --- a/packages/playwright-core/src/utils.ts +++ b/packages/playwright-core/src/utils.ts @@ -29,9 +29,6 @@ export * from './utils/isomorphic/urlMatch'; export * from './utils/isomorphic/headers'; export * from './utils/isomorphic/semaphore'; export * from './utils/isomorphic/stackTrace'; -export * from './utils/task'; -export * from './utils/zipFile'; -export * from './utils/zones'; export * from './server/utils/ascii'; export * from './server/utils/comparators'; @@ -49,7 +46,10 @@ export * from './server/utils/processLauncher'; export * from './server/utils/profiler'; export * from './server/utils/socksProxy'; export * from './server/utils/spawnAsync'; +export * from './server/utils/task'; export * from './server/utils/userAgent'; export * from './server/utils/wsServer'; +export * from './server/utils/zipFile'; +export * from './server/utils/zones'; export { colors } from './utilsBundle'; diff --git a/packages/playwright-core/src/utils/isomorphic/mimeType.ts b/packages/playwright-core/src/utils/isomorphic/mimeType.ts index 407d935281..45ac92d645 100644 --- a/packages/playwright-core/src/utils/isomorphic/mimeType.ts +++ b/packages/playwright-core/src/utils/isomorphic/mimeType.ts @@ -1,14 +1,14 @@ /** * Copyright (c) Microsoft Corporation. * - * Licensed under the Apache License, Version 2.0 (the "License"); + * 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, + * 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. @@ -21,3 +21,426 @@ export function isJsonMimeType(mimeType: string) { export function isTextualMimeType(mimeType: string) { return !!mimeType.match(/^(text\/.*?|application\/(json|(x-)?javascript|xml.*?|ecmascript|graphql|x-www-form-urlencoded)|image\/svg(\+xml)?|application\/.*?(\+json|\+xml))(;\s*charset=.*)?$/); } +export function getMimeTypeForPath(path: string): string | null { + const dotIndex = path.lastIndexOf('.'); + if (dotIndex === -1) + return null; + const extension = path.substring(dotIndex + 1); + return types.get(extension) || null; +} + +const types: Map = new Map([ + ['ez', 'application/andrew-inset'], + ['aw', 'application/applixware'], + ['atom', 'application/atom+xml'], + ['atomcat', 'application/atomcat+xml'], + ['atomdeleted', 'application/atomdeleted+xml'], + ['atomsvc', 'application/atomsvc+xml'], + ['dwd', 'application/atsc-dwd+xml'], + ['held', 'application/atsc-held+xml'], + ['rsat', 'application/atsc-rsat+xml'], + ['bdoc', 'application/bdoc'], + ['xcs', 'application/calendar+xml'], + ['ccxml', 'application/ccxml+xml'], + ['cdfx', 'application/cdfx+xml'], + ['cdmia', 'application/cdmi-capability'], + ['cdmic', 'application/cdmi-container'], + ['cdmid', 'application/cdmi-domain'], + ['cdmio', 'application/cdmi-object'], + ['cdmiq', 'application/cdmi-queue'], + ['cu', 'application/cu-seeme'], + ['mpd', 'application/dash+xml'], + ['davmount', 'application/davmount+xml'], + ['dbk', 'application/docbook+xml'], + ['dssc', 'application/dssc+der'], + ['xdssc', 'application/dssc+xml'], + ['ecma', 'application/ecmascript'], + ['es', 'application/ecmascript'], + ['emma', 'application/emma+xml'], + ['emotionml', 'application/emotionml+xml'], + ['epub', 'application/epub+zip'], + ['exi', 'application/exi'], + ['exp', 'application/express'], + ['fdt', 'application/fdt+xml'], + ['pfr', 'application/font-tdpfr'], + ['geojson', 'application/geo+json'], + ['gml', 'application/gml+xml'], + ['gpx', 'application/gpx+xml'], + ['gxf', 'application/gxf'], + ['gz', 'application/gzip'], + ['hjson', 'application/hjson'], + ['stk', 'application/hyperstudio'], + ['ink', 'application/inkml+xml'], + ['inkml', 'application/inkml+xml'], + ['ipfix', 'application/ipfix'], + ['its', 'application/its+xml'], + ['ear', 'application/java-archive'], + ['jar', 'application/java-archive'], + ['war', 'application/java-archive'], + ['ser', 'application/java-serialized-object'], + ['class', 'application/java-vm'], + ['js', 'application/javascript'], + ['mjs', 'application/javascript'], + ['json', 'application/json'], + ['map', 'application/json'], + ['json5', 'application/json5'], + ['jsonml', 'application/jsonml+json'], + ['jsonld', 'application/ld+json'], + ['lgr', 'application/lgr+xml'], + ['lostxml', 'application/lost+xml'], + ['hqx', 'application/mac-binhex40'], + ['cpt', 'application/mac-compactpro'], + ['mads', 'application/mads+xml'], + ['webmanifest', 'application/manifest+json'], + ['mrc', 'application/marc'], + ['mrcx', 'application/marcxml+xml'], + ['ma', 'application/mathematica'], + ['mb', 'application/mathematica'], + ['nb', 'application/mathematica'], + ['mathml', 'application/mathml+xml'], + ['mbox', 'application/mbox'], + ['mscml', 'application/mediaservercontrol+xml'], + ['metalink', 'application/metalink+xml'], + ['meta4', 'application/metalink4+xml'], + ['mets', 'application/mets+xml'], + ['maei', 'application/mmt-aei+xml'], + ['musd', 'application/mmt-usd+xml'], + ['mods', 'application/mods+xml'], + ['m21', 'application/mp21'], + ['mp21', 'application/mp21'], + ['m4p', 'application/mp4'], + ['mp4s', 'application/mp4'], + ['doc', 'application/msword'], + ['dot', 'application/msword'], + ['mxf', 'application/mxf'], + ['nq', 'application/n-quads'], + ['nt', 'application/n-triples'], + ['cjs', 'application/node'], + ['bin', 'application/octet-stream'], + ['bpk', 'application/octet-stream'], + ['buffer', 'application/octet-stream'], + ['deb', 'application/octet-stream'], + ['deploy', 'application/octet-stream'], + ['dist', 'application/octet-stream'], + ['distz', 'application/octet-stream'], + ['dll', 'application/octet-stream'], + ['dmg', 'application/octet-stream'], + ['dms', 'application/octet-stream'], + ['dump', 'application/octet-stream'], + ['elc', 'application/octet-stream'], + ['exe', 'application/octet-stream'], + ['img', 'application/octet-stream'], + ['iso', 'application/octet-stream'], + ['lrf', 'application/octet-stream'], + ['mar', 'application/octet-stream'], + ['msi', 'application/octet-stream'], + ['msm', 'application/octet-stream'], + ['msp', 'application/octet-stream'], + ['pkg', 'application/octet-stream'], + ['so', 'application/octet-stream'], + ['oda', 'application/oda'], + ['opf', 'application/oebps-package+xml'], + ['ogx', 'application/ogg'], + ['omdoc', 'application/omdoc+xml'], + ['onepkg', 'application/onenote'], + ['onetmp', 'application/onenote'], + ['onetoc', 'application/onenote'], + ['onetoc2', 'application/onenote'], + ['oxps', 'application/oxps'], + ['relo', 'application/p2p-overlay+xml'], + ['xer', 'application/patch-ops-error+xml'], + ['pdf', 'application/pdf'], + ['pgp', 'application/pgp-encrypted'], + ['asc', 'application/pgp-signature'], + ['sig', 'application/pgp-signature'], + ['prf', 'application/pics-rules'], + ['p10', 'application/pkcs10'], + ['p7c', 'application/pkcs7-mime'], + ['p7m', 'application/pkcs7-mime'], + ['p7s', 'application/pkcs7-signature'], + ['p8', 'application/pkcs8'], + ['ac', 'application/pkix-attr-cert'], + ['cer', 'application/pkix-cert'], + ['crl', 'application/pkix-crl'], + ['pkipath', 'application/pkix-pkipath'], + ['pki', 'application/pkixcmp'], + ['pls', 'application/pls+xml'], + ['ai', 'application/postscript'], + ['eps', 'application/postscript'], + ['ps', 'application/postscript'], + ['provx', 'application/provenance+xml'], + ['pskcxml', 'application/pskc+xml'], + ['raml', 'application/raml+yaml'], + ['owl', 'application/rdf+xml'], + ['rdf', 'application/rdf+xml'], + ['rif', 'application/reginfo+xml'], + ['rnc', 'application/relax-ng-compact-syntax'], + ['rl', 'application/resource-lists+xml'], + ['rld', 'application/resource-lists-diff+xml'], + ['rs', 'application/rls-services+xml'], + ['rapd', 'application/route-apd+xml'], + ['sls', 'application/route-s-tsid+xml'], + ['rusd', 'application/route-usd+xml'], + ['gbr', 'application/rpki-ghostbusters'], + ['mft', 'application/rpki-manifest'], + ['roa', 'application/rpki-roa'], + ['rsd', 'application/rsd+xml'], + ['rss', 'application/rss+xml'], + ['rtf', 'application/rtf'], + ['sbml', 'application/sbml+xml'], + ['scq', 'application/scvp-cv-request'], + ['scs', 'application/scvp-cv-response'], + ['spq', 'application/scvp-vp-request'], + ['spp', 'application/scvp-vp-response'], + ['sdp', 'application/sdp'], + ['senmlx', 'application/senml+xml'], + ['sensmlx', 'application/sensml+xml'], + ['setpay', 'application/set-payment-initiation'], + ['setreg', 'application/set-registration-initiation'], + ['shf', 'application/shf+xml'], + ['sieve', 'application/sieve'], + ['siv', 'application/sieve'], + ['smi', 'application/smil+xml'], + ['smil', 'application/smil+xml'], + ['rq', 'application/sparql-query'], + ['srx', 'application/sparql-results+xml'], + ['gram', 'application/srgs'], + ['grxml', 'application/srgs+xml'], + ['sru', 'application/sru+xml'], + ['ssdl', 'application/ssdl+xml'], + ['ssml', 'application/ssml+xml'], + ['swidtag', 'application/swid+xml'], + ['tei', 'application/tei+xml'], + ['teicorpus', 'application/tei+xml'], + ['tfi', 'application/thraud+xml'], + ['tsd', 'application/timestamped-data'], + ['toml', 'application/toml'], + ['trig', 'application/trig'], + ['ttml', 'application/ttml+xml'], + ['ubj', 'application/ubjson'], + ['rsheet', 'application/urc-ressheet+xml'], + ['td', 'application/urc-targetdesc+xml'], + ['vxml', 'application/voicexml+xml'], + ['wasm', 'application/wasm'], + ['wgt', 'application/widget'], + ['hlp', 'application/winhlp'], + ['wsdl', 'application/wsdl+xml'], + ['wspolicy', 'application/wspolicy+xml'], + ['xaml', 'application/xaml+xml'], + ['xav', 'application/xcap-att+xml'], + ['xca', 'application/xcap-caps+xml'], + ['xdf', 'application/xcap-diff+xml'], + ['xel', 'application/xcap-el+xml'], + ['xns', 'application/xcap-ns+xml'], + ['xenc', 'application/xenc+xml'], + ['xht', 'application/xhtml+xml'], + ['xhtml', 'application/xhtml+xml'], + ['xlf', 'application/xliff+xml'], + ['rng', 'application/xml'], + ['xml', 'application/xml'], + ['xsd', 'application/xml'], + ['xsl', 'application/xml'], + ['dtd', 'application/xml-dtd'], + ['xop', 'application/xop+xml'], + ['xpl', 'application/xproc+xml'], + ['*xsl', 'application/xslt+xml'], + ['xslt', 'application/xslt+xml'], + ['xspf', 'application/xspf+xml'], + ['mxml', 'application/xv+xml'], + ['xhvml', 'application/xv+xml'], + ['xvm', 'application/xv+xml'], + ['xvml', 'application/xv+xml'], + ['yang', 'application/yang'], + ['yin', 'application/yin+xml'], + ['zip', 'application/zip'], + ['*3gpp', 'audio/3gpp'], + ['adp', 'audio/adpcm'], + ['amr', 'audio/amr'], + ['au', 'audio/basic'], + ['snd', 'audio/basic'], + ['kar', 'audio/midi'], + ['mid', 'audio/midi'], + ['midi', 'audio/midi'], + ['rmi', 'audio/midi'], + ['mxmf', 'audio/mobile-xmf'], + ['*mp3', 'audio/mp3'], + ['m4a', 'audio/mp4'], + ['mp4a', 'audio/mp4'], + ['m2a', 'audio/mpeg'], + ['m3a', 'audio/mpeg'], + ['mp2', 'audio/mpeg'], + ['mp2a', 'audio/mpeg'], + ['mp3', 'audio/mpeg'], + ['mpga', 'audio/mpeg'], + ['oga', 'audio/ogg'], + ['ogg', 'audio/ogg'], + ['opus', 'audio/ogg'], + ['spx', 'audio/ogg'], + ['s3m', 'audio/s3m'], + ['sil', 'audio/silk'], + ['wav', 'audio/wav'], + ['*wav', 'audio/wave'], + ['weba', 'audio/webm'], + ['xm', 'audio/xm'], + ['ttc', 'font/collection'], + ['otf', 'font/otf'], + ['ttf', 'font/ttf'], + ['woff', 'font/woff'], + ['woff2', 'font/woff2'], + ['exr', 'image/aces'], + ['apng', 'image/apng'], + ['avif', 'image/avif'], + ['bmp', 'image/bmp'], + ['cgm', 'image/cgm'], + ['drle', 'image/dicom-rle'], + ['emf', 'image/emf'], + ['fits', 'image/fits'], + ['g3', 'image/g3fax'], + ['gif', 'image/gif'], + ['heic', 'image/heic'], + ['heics', 'image/heic-sequence'], + ['heif', 'image/heif'], + ['heifs', 'image/heif-sequence'], + ['hej2', 'image/hej2k'], + ['hsj2', 'image/hsj2'], + ['ief', 'image/ief'], + ['jls', 'image/jls'], + ['jp2', 'image/jp2'], + ['jpg2', 'image/jp2'], + ['jpe', 'image/jpeg'], + ['jpeg', 'image/jpeg'], + ['jpg', 'image/jpeg'], + ['jph', 'image/jph'], + ['jhc', 'image/jphc'], + ['jpm', 'image/jpm'], + ['jpf', 'image/jpx'], + ['jpx', 'image/jpx'], + ['jxr', 'image/jxr'], + ['jxra', 'image/jxra'], + ['jxrs', 'image/jxrs'], + ['jxs', 'image/jxs'], + ['jxsc', 'image/jxsc'], + ['jxsi', 'image/jxsi'], + ['jxss', 'image/jxss'], + ['ktx', 'image/ktx'], + ['ktx2', 'image/ktx2'], + ['png', 'image/png'], + ['sgi', 'image/sgi'], + ['svg', 'image/svg+xml'], + ['svgz', 'image/svg+xml'], + ['t38', 'image/t38'], + ['tif', 'image/tiff'], + ['tiff', 'image/tiff'], + ['tfx', 'image/tiff-fx'], + ['webp', 'image/webp'], + ['wmf', 'image/wmf'], + ['disposition-notification', 'message/disposition-notification'], + ['u8msg', 'message/global'], + ['u8dsn', 'message/global-delivery-status'], + ['u8mdn', 'message/global-disposition-notification'], + ['u8hdr', 'message/global-headers'], + ['eml', 'message/rfc822'], + ['mime', 'message/rfc822'], + ['3mf', 'model/3mf'], + ['gltf', 'model/gltf+json'], + ['glb', 'model/gltf-binary'], + ['iges', 'model/iges'], + ['igs', 'model/iges'], + ['mesh', 'model/mesh'], + ['msh', 'model/mesh'], + ['silo', 'model/mesh'], + ['mtl', 'model/mtl'], + ['obj', 'model/obj'], + ['stpx', 'model/step+xml'], + ['stpz', 'model/step+zip'], + ['stpxz', 'model/step-xml+zip'], + ['stl', 'model/stl'], + ['vrml', 'model/vrml'], + ['wrl', 'model/vrml'], + ['*x3db', 'model/x3d+binary'], + ['x3dbz', 'model/x3d+binary'], + ['x3db', 'model/x3d+fastinfoset'], + ['*x3dv', 'model/x3d+vrml'], + ['x3dvz', 'model/x3d+vrml'], + ['x3d', 'model/x3d+xml'], + ['x3dz', 'model/x3d+xml'], + ['x3dv', 'model/x3d-vrml'], + ['appcache', 'text/cache-manifest'], + ['manifest', 'text/cache-manifest'], + ['ics', 'text/calendar'], + ['ifb', 'text/calendar'], + ['coffee', 'text/coffeescript'], + ['litcoffee', 'text/coffeescript'], + ['css', 'text/css'], + ['csv', 'text/csv'], + ['htm', 'text/html'], + ['html', 'text/html'], + ['shtml', 'text/html'], + ['jade', 'text/jade'], + ['jsx', 'text/jsx'], + ['less', 'text/less'], + ['markdown', 'text/markdown'], + ['md', 'text/markdown'], + ['mml', 'text/mathml'], + ['mdx', 'text/mdx'], + ['n3', 'text/n3'], + ['conf', 'text/plain'], + ['def', 'text/plain'], + ['in', 'text/plain'], + ['ini', 'text/plain'], + ['list', 'text/plain'], + ['log', 'text/plain'], + ['text', 'text/plain'], + ['txt', 'text/plain'], + ['rtx', 'text/richtext'], + ['*rtf', 'text/rtf'], + ['sgm', 'text/sgml'], + ['sgml', 'text/sgml'], + ['shex', 'text/shex'], + ['slim', 'text/slim'], + ['slm', 'text/slim'], + ['spdx', 'text/spdx'], + ['styl', 'text/stylus'], + ['stylus', 'text/stylus'], + ['tsv', 'text/tab-separated-values'], + ['man', 'text/troff'], + ['me', 'text/troff'], + ['ms', 'text/troff'], + ['roff', 'text/troff'], + ['t', 'text/troff'], + ['tr', 'text/troff'], + ['ttl', 'text/turtle'], + ['uri', 'text/uri-list'], + ['uris', 'text/uri-list'], + ['urls', 'text/uri-list'], + ['vcard', 'text/vcard'], + ['vtt', 'text/vtt'], + ['*xml', 'text/xml'], + ['yaml', 'text/yaml'], + ['yml', 'text/yaml'], + ['3gp', 'video/3gpp'], + ['3gpp', 'video/3gpp'], + ['3g2', 'video/3gpp2'], + ['h261', 'video/h261'], + ['h263', 'video/h263'], + ['h264', 'video/h264'], + ['m4s', 'video/iso.segment'], + ['jpgv', 'video/jpeg'], + ['jpm', 'video/jpm'], + ['jpgm', 'video/jpm'], + ['mj2', 'video/mj2'], + ['mjp2', 'video/mj2'], + ['ts', 'video/mp2t'], + ['mp4', 'video/mp4'], + ['mp4v', 'video/mp4'], + ['mpg4', 'video/mp4'], + ['m1v', 'video/mpeg'], + ['m2v', 'video/mpeg'], + ['mpe', 'video/mpeg'], + ['mpeg', 'video/mpeg'], + ['mpg', 'video/mpeg'], + ['ogv', 'video/ogg'], + ['mov', 'video/quicktime'], + ['qt', 'video/quicktime'], + ['webm', 'video/webm'] +]); diff --git a/packages/playwright/src/common/testType.ts b/packages/playwright/src/common/testType.ts index 7395e38217..d9c828cc62 100644 --- a/packages/playwright/src/common/testType.ts +++ b/packages/playwright/src/common/testType.ts @@ -15,7 +15,7 @@ */ import { errors } from 'playwright-core'; -import { getPackageManagerExecCommand, monotonicTime, raceAgainstDeadline, zones } from 'playwright-core/lib/utils'; +import { getPackageManagerExecCommand, monotonicTime, raceAgainstDeadline, currentZone } from 'playwright-core/lib/utils'; import { currentTestInfo, currentlyLoadingFileSuite, setCurrentlyLoadingFileSuite } from './globals'; import { Suite, TestCase } from './test'; @@ -266,7 +266,7 @@ export class TestTypeImpl { if (!testInfo) throw new Error(`test.step() can only be called from a test`); const step = testInfo._addStep({ category: 'test.step', title, location: options.location, box: options.box }); - return await zones.run('stepZone', step, async () => { + return await currentZone().with('stepZone', step).run(async () => { try { let result: Awaited>> | undefined = undefined; result = await raceAgainstDeadline(async () => { diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index a8d0cc0387..9d6089614c 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -18,7 +18,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as playwrightLibrary from 'playwright-core'; -import { addInternalStackPrefix, asLocator, createGuid, debugMode, isString, jsonStringifyForceASCII, zones } from 'playwright-core/lib/utils'; +import { addInternalStackPrefix, asLocator, createGuid, currentZone, debugMode, isString, jsonStringifyForceASCII } from 'playwright-core/lib/utils'; import { currentTestInfo } from './common/globals'; import { rootTestType } from './common/testType'; @@ -260,7 +260,7 @@ const playwrightFixtures: Fixtures = ({ // Some special calls do not get into steps. if (!testInfo || data.apiName.includes('setTestIdAttribute') || data.apiName === 'tracing.groupEnd') return; - const zone = zones.zoneData('stepZone'); + const zone = currentZone().data('stepZone'); if (zone && zone.category === 'expect') { // Display the internal locator._expect call under the name of the enclosing expect call, // and connect it to the existing expect step. diff --git a/packages/playwright/src/matchers/expect.ts b/packages/playwright/src/matchers/expect.ts index 707e5457e1..58eb9be9fc 100644 --- a/packages/playwright/src/matchers/expect.ts +++ b/packages/playwright/src/matchers/expect.ts @@ -17,9 +17,9 @@ import { captureRawStack, createGuid, + currentZone, isString, pollAgainstDeadline } from 'playwright-core/lib/utils'; -import { zones } from 'playwright-core/lib/utils'; import { ExpectError, isJestError } from './matcherHint'; import { @@ -380,7 +380,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler { try { setMatcherCallContext({ expectInfo: this._info, testInfo, step: step.info }); const callback = () => matcher.call(target, ...args); - const result = zones.run('stepZone', step, callback); + const result = currentZone().with('stepZone', step).run(callback); if (result instanceof Promise) return result.then(finalizer).catch(reportStepError); finalizer(); diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index 5e964af5e0..cbb26b6c2f 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -17,7 +17,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import { captureRawStack, monotonicTime, sanitizeForFilePath, stringifyStackFrames, zones } from 'playwright-core/lib/utils'; +import { captureRawStack, monotonicTime, sanitizeForFilePath, stringifyStackFrames, currentZone } from 'playwright-core/lib/utils'; import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutManager'; import { debugTest, filteredStackTrace, formatLocation, getContainedPath, normalizeAndSaveAttachment, trimLongString, windowsFilesystemFriendlyLength } from '../util'; @@ -245,7 +245,7 @@ export class TestInfoImpl implements TestInfo { } private _parentStep() { - return zones.zoneData('stepZone') + return currentZone().data('stepZone') ?? this._findLastStageStep(this._steps); // If no parent step on stack, assume the current stage as parent. } diff --git a/tests/assets/modernizr/README.md b/tests/assets/modernizr/README.md index bf3241283d..2a986d23d4 100644 --- a/tests/assets/modernizr/README.md +++ b/tests/assets/modernizr/README.md @@ -7,8 +7,8 @@ ## Updating expectations -1. `npx http-server .` -1. Navigate to `http://127.0.0.1:8080/tests/assets/modernizr/index.html` +1. Serve `tests/assets/modernizr/index.html` from a remote (localhost results will be different) https origin (e.g. https://pages.github.com). +1. Navigate to `https://your-domain.com/tests/assets/modernizr/index.html` Do this with: diff --git a/tests/assets/modernizr/mobile-safari-18.json b/tests/assets/modernizr/mobile-safari-18.json index e86fc3de20..cd2822a65c 100644 --- a/tests/assets/modernizr/mobile-safari-18.json +++ b/tests/assets/modernizr/mobile-safari-18.json @@ -362,6 +362,7 @@ "EXT_polygon_offset_clamp": true, "EXT_shader_texture_lod": true, "EXT_texture_filter_anisotropic": true, + "EXT_texture_mirror_clamp_to_edge": true, "EXT_sRGB": true, "KHR_parallel_shader_compile": true, "OES_element_index_uint": true, @@ -371,6 +372,7 @@ "OES_texture_half_float": true, "OES_texture_half_float_linear": true, "OES_vertex_array_object": true, + "WEBGL_blend_func_extended": true, "WEBGL_color_buffer_float": true, "WEBGL_compressed_texture_astc": true, "WEBGL_compressed_texture_etc": true, diff --git a/tests/config/utils.ts b/tests/config/utils.ts index 3743e97d80..3ab51c9290 100644 --- a/tests/config/utils.ts +++ b/tests/config/utils.ts @@ -15,7 +15,7 @@ */ import type { Frame, Page } from 'playwright-core'; -import { ZipFile } from '../../packages/playwright-core/lib/utils/zipFile'; +import { ZipFile } from '../../packages/playwright-core/lib/server/utils/zipFile'; import type { TraceModelBackend } from '../../packages/trace-viewer/src/sw/traceModel'; import type { StackFrame } from '../../packages/protocol/src/channels'; import { parseClientSideCallMetadata } from '../../packages/playwright-core/lib/utils/isomorphic/traceUtils'; diff --git a/utils/build/build-playwright-driver.sh b/utils/build/build-playwright-driver.sh index c74e3bebf1..6f6082fc8b 100755 --- a/utils/build/build-playwright-driver.sh +++ b/utils/build/build-playwright-driver.sh @@ -4,7 +4,7 @@ set -x trap "cd $(pwd -P)" EXIT SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)" -NODE_VERSION="22.13.1" # autogenerated via ./update-playwright-driver-version.mjs +NODE_VERSION="22.14.0" # autogenerated via ./update-playwright-driver-version.mjs cd "$(dirname "$0")" PACKAGE_VERSION=$(node -p "require('../../package.json').version")