From e03402f7a7973145176f42271d299220680e2b37 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 13 Feb 2025 12:11:05 -0800 Subject: [PATCH 01/25] chore(bidi): implement setInputFile(FilePayload) (#34785) --- packages/playwright-core/src/server/bidi/bidiPage.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 { From 9ecf2f69bafd2217a66a61117ccb64bb09d8cae7 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 13 Feb 2025 12:30:53 -0800 Subject: [PATCH 02/25] chore: move pipe transport utils to server/ (#34787) --- packages/playwright-core/src/cli/driver.ts | 2 +- packages/playwright-core/src/outofprocess.ts | 2 +- packages/playwright-core/src/server/android/android.ts | 4 ++-- .../playwright-core/src/{ => server}/utils/pipeTransport.ts | 0 packages/playwright-core/src/{ => server}/utils/task.ts | 0 packages/playwright-core/src/utils.ts | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) rename packages/playwright-core/src/{ => server}/utils/pipeTransport.ts (100%) rename packages/playwright-core/src/{ => server}/utils/task.ts (100%) 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/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/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/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.ts b/packages/playwright-core/src/utils.ts index 1a0c446c6b..a61f84801c 100644 --- a/packages/playwright-core/src/utils.ts +++ b/packages/playwright-core/src/utils.ts @@ -29,7 +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'; @@ -49,6 +48,7 @@ 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'; From 90ec8383189d4d122e68c51ef7f43528ccdfa754 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 13 Feb 2025 13:06:03 -0800 Subject: [PATCH 03/25] chore: move zones into platform (#34786) --- .../src/client/browserContext.ts | 2 +- .../src/client/channelOwner.ts | 5 +-- .../playwright-core/src/client/connection.ts | 3 +- .../playwright-core/src/client/network.ts | 11 +++-- packages/playwright-core/src/client/page.ts | 2 +- packages/playwright-core/src/client/waiter.ts | 5 +-- .../playwright-core/src/common/platform.ts | 20 +++++++++- .../src/server/utils/nodePlatform.ts | 40 ++++++++++++++++++- .../src/{ => server}/utils/zones.ts | 33 ++++----------- packages/playwright-core/src/utils.ts | 2 +- packages/playwright/src/common/testType.ts | 4 +- packages/playwright/src/index.ts | 4 +- packages/playwright/src/matchers/expect.ts | 4 +- packages/playwright/src/worker/testInfo.ts | 4 +- 14 files changed, 85 insertions(+), 54 deletions(-) rename packages/playwright-core/src/{ => server}/utils/zones.ts (63%) diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 02aa857366..d03cfcdeca 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -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(); } 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..963b13a77f 100644 --- a/packages/playwright-core/src/client/connection.ts +++ b/packages/playwright-core/src/client/connection.ts @@ -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'; @@ -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/network.ts b/packages/playwright-core/src/client/network.ts index a2c64c08b1..3770ca1320 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -30,7 +30,6 @@ 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 type { BrowserContext } from './browserContext'; @@ -40,8 +39,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, @@ -821,14 +820,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 +851,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..062217937c 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -520,7 +520,7 @@ 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(); } 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/common/platform.ts b/packages/playwright-core/src/common/platform.ts index 64cef77aab..bb73ec6d31 100644 --- a/packages/playwright-core/src/common/platform.ts +++ b/packages/playwright-core/src/common/platform.ts @@ -22,6 +22,19 @@ import { webColors, noColors } from '../utils/isomorphic/colors'; import type { Colors } from '../utils/isomorphic/colors'; +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 = { calculateSha1(text: string): Promise; @@ -34,6 +47,7 @@ export type Platform = { path: () => typeof path; pathSeparator: string; ws?: (url: string) => WebSocket; + zones: { empty: Zone, current: () => Zone; }; }; export const webPlatform: Platform = { @@ -69,6 +83,8 @@ export const webPlatform: Platform = { pathSeparator: '/', ws: (url: string) => new WebSocket(url), + + zones: { empty: noopZone, current: () => noopZone }, }; export const emptyPlatform: Platform = { @@ -98,5 +114,7 @@ export const emptyPlatform: Platform = { throw new Error('Function not implemented.'); }, - pathSeparator: '/' + pathSeparator: '/', + + zones: { empty: noopZone, current: () => noopZone }, }; diff --git a/packages/playwright-core/src/server/utils/nodePlatform.ts b/packages/playwright-core/src/server/utils/nodePlatform.ts index d4b8b5d7ad..49f6b23ffe 100644 --- a/packages/playwright-core/src/server/utils/nodePlatform.ts +++ b/packages/playwright-core/src/server/utils/nodePlatform.ts @@ -20,8 +20,39 @@ import * as path from 'path'; import * as util from 'util'; 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'; + +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); + } + + runIgnoreCurrent(func: () => R): R { + return emptyZone.run(func); + } + + data(): T | undefined { + return this._zone.data('apiZone'); + } +} export const nodePlatform: Platform = { calculateSha1: (text: string) => { @@ -48,5 +79,10 @@ export const nodePlatform: Platform = { path: () => path, - pathSeparator: path.sep + pathSeparator: path.sep, + + zones: { + current: () => new NodeZone(currentZone()), + empty: new NodeZone(emptyZone), + } }; 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 a61f84801c..839aa390a8 100644 --- a/packages/playwright-core/src/utils.ts +++ b/packages/playwright-core/src/utils.ts @@ -30,7 +30,6 @@ export * from './utils/isomorphic/headers'; export * from './utils/isomorphic/semaphore'; export * from './utils/isomorphic/stackTrace'; export * from './utils/zipFile'; -export * from './utils/zones'; export * from './server/utils/ascii'; export * from './server/utils/comparators'; @@ -51,5 +50,6 @@ export * from './server/utils/spawnAsync'; export * from './server/utils/task'; export * from './server/utils/userAgent'; export * from './server/utils/wsServer'; +export * from './server/utils/zones'; export { colors } from './utilsBundle'; 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. } From 163aacf4b68a9155a6a9fe2474d4bf2a9c130b10 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 13 Feb 2025 16:15:11 -0800 Subject: [PATCH 04/25] chore: allow client operation w/o local utils (#34790) --- packages/playwright-core/src/client/DEPS.list | 3 +- .../playwright-core/src/client/android.ts | 4 +- .../playwright-core/src/client/artifact.ts | 2 +- .../playwright-core/src/client/browser.ts | 2 +- .../src/client/browserContext.ts | 12 +- .../playwright-core/src/client/browserType.ts | 4 +- .../playwright-core/src/client/connection.ts | 4 +- .../src/client/elementHandle.ts | 6 +- packages/playwright-core/src/client/fetch.ts | 2 +- .../src/{common => client}/fileUtils.ts | 9 +- .../playwright-core/src/client/localUtils.ts | 122 +---- .../playwright-core/src/client/network.ts | 4 +- packages/playwright-core/src/client/page.ts | 7 +- .../playwright-core/src/client/tracing.ts | 16 +- .../playwright-core/src/client/webSocket.ts | 116 +++++ packages/playwright-core/src/common/DEPS.list | 3 - .../playwright-core/src/common/platform.ts | 19 +- .../playwright-core/src/common/progress.ts | 23 - .../playwright-core/src/protocol/DEPS.list | 3 +- .../src/protocol/validatorPrimitives.ts | 2 +- .../dispatchers/localUtilsDispatcher.ts | 11 +- .../src/server/fileUploadUtils.ts | 4 +- .../src/{common => server}/harBackend.ts | 17 +- .../src/{common => server}/localUtils.ts | 28 +- .../playwright-core/src/server/progress.ts | 8 +- .../src/server/utils/nodePlatform.ts | 6 +- .../src/{ => server}/utils/zipFile.ts | 4 +- packages/playwright-core/src/utils.ts | 2 +- .../src/utils/isomorphic/mimeType.ts | 427 +++++++++++++++++- tests/config/utils.ts | 2 +- 30 files changed, 646 insertions(+), 226 deletions(-) rename packages/playwright-core/src/{common => client}/fileUtils.ts (75%) create mode 100644 packages/playwright-core/src/client/webSocket.ts delete mode 100644 packages/playwright-core/src/common/progress.ts rename packages/playwright-core/src/{common => server}/harBackend.ts (92%) rename packages/playwright-core/src/{common => server}/localUtils.ts (84%) rename packages/playwright-core/src/{ => server}/utils/zipFile.ts (95%) 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..3aedc5c4ac 100644 --- a/packages/playwright-core/src/client/android.ts +++ b/packages/playwright-core/src/client/android.ts @@ -25,6 +25,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 +70,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 d03cfcdeca..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'; @@ -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..39a9a3f1cc 100644 --- a/packages/playwright-core/src/client/browserType.ts +++ b/packages/playwright-core/src/client/browserType.ts @@ -25,6 +25,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'; @@ -124,7 +125,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 +134,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/connection.ts b/packages/playwright-core/src/client/connection.ts index 963b13a77f..929acff25f 100644 --- a/packages/playwright-core/src/client/connection.ts +++ b/packages/playwright-core/src/client/connection.ts @@ -108,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 { diff --git a/packages/playwright-core/src/client/elementHandle.ts b/packages/playwright-core/src/client/elementHandle.ts index 206fc33084..b1ef721ce3 100644 --- a/packages/playwright-core/src/client/elementHandle.ts +++ b/packages/playwright-core/src/client/elementHandle.ts @@ -20,10 +20,10 @@ 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'; @@ -327,7 +327,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/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/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 3770ca1320..ff992e7ae7 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -30,7 +30,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 { mime } from '../utilsBundle'; +import { getMimeTypeForPath } from '../utils/isomorphic/mimeType'; import type { BrowserContext } from './browserContext'; import type { Page } from './page'; @@ -413,7 +413,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); diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 062217937c..43c43ee50c 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'; @@ -525,11 +525,14 @@ export class Page extends ChannelOwner implements api.Page } 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); } 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/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/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 bb73ec6d31..330b5d6323 100644 --- a/packages/playwright-core/src/common/platform.ts +++ b/packages/playwright-core/src/common/platform.ts @@ -14,12 +14,10 @@ * 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'; export type Zone = { @@ -37,6 +35,8 @@ const noopZone: Zone = { }; export type Platform = { + name: 'node' | 'web' | 'empty'; + calculateSha1(text: string): Promise; colors: Colors; createGuid: () => string; @@ -46,21 +46,22 @@ export type Platform = { log(name: 'api' | 'channel', message: string | Error | object): void; path: () => typeof path; pathSeparator: string; - ws?: (url: string) => WebSocket; zones: { empty: Zone, current: () => Zone; }; }; export const webPlatform: Platform = { + name: 'web', + calculateSha1: async (text: string) => { const bytes = new TextEncoder().encode(text); - const hashBuffer = await crypto.subtle.digest('SHA-1', bytes); + 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(crypto.getRandomValues(new Uint8Array(16)), b => b.toString(16).padStart(2, '0')).join(''); + return Array.from(window.crypto.getRandomValues(new Uint8Array(16)), b => b.toString(16).padStart(2, '0')).join(''); }, fs: () => { @@ -82,12 +83,12 @@ export const webPlatform: Platform = { pathSeparator: '/', - ws: (url: string) => new WebSocket(url), - zones: { empty: noopZone, current: () => noopZone }, }; export const emptyPlatform: Platform = { + name: 'empty', + calculateSha1: async () => { throw new Error('Not implemented'); }, 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/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/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 49f6b23ffe..1c2a4dad0c 100644 --- a/packages/playwright-core/src/server/utils/nodePlatform.ts +++ b/packages/playwright-core/src/server/utils/nodePlatform.ts @@ -45,16 +45,14 @@ class NodeZone implements Zone { return this._zone.run(func); } - runIgnoreCurrent(func: () => R): R { - return emptyZone.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); 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.ts b/packages/playwright-core/src/utils.ts index 839aa390a8..dc5f284955 100644 --- a/packages/playwright-core/src/utils.ts +++ b/packages/playwright-core/src/utils.ts @@ -29,7 +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/zipFile'; export * from './server/utils/ascii'; export * from './server/utils/comparators'; @@ -50,6 +49,7 @@ 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/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'; From 6833b664e373b3137e7789aaaccd7fcf388b4bba Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 13 Feb 2025 16:37:15 -0800 Subject: [PATCH 05/25] test: update modernizr expectations with actual mobile Safari 18.3 values (#34789) --- tests/assets/modernizr/README.md | 4 ++-- tests/assets/modernizr/mobile-safari-18.json | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) 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, From 4a9b3361689a4a18db23a743c7c3b56fefdb35cf Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 13 Feb 2025 16:46:24 -0800 Subject: [PATCH 06/25] chore: for not use Node's events in client (#34791) --- packages/playwright-core/src/client/android.ts | 3 +-- packages/playwright-core/src/client/browserType.ts | 4 +--- packages/playwright-core/src/client/connection.ts | 2 +- packages/playwright-core/src/client/eventEmitter.ts | 10 +++++++--- packages/playwright-core/src/client/frame.ts | 3 +-- packages/playwright-core/src/client/network.ts | 2 -- packages/playwright-core/src/client/page.ts | 2 +- packages/playwright-core/src/common/platform.ts | 4 ++++ packages/playwright-core/src/inProcessFactory.ts | 5 ++++- .../playwright-core/src/server/utils/nodePlatform.ts | 2 ++ 10 files changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/playwright-core/src/client/android.ts b/packages/playwright-core/src/client/android.ts index 3aedc5c4ac..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'; diff --git a/packages/playwright-core/src/client/browserType.ts b/packages/playwright-core/src/client/browserType.ts index 39a9a3f1cc..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'; @@ -101,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); diff --git a/packages/playwright-core/src/client/connection.ts b/packages/playwright-core/src/client/connection.ts index 929acff25f..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'; 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/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/network.ts b/packages/playwright-core/src/client/network.ts index ff992e7ae7..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'; diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 43c43ee50c..cb6d0656b8 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -799,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/common/platform.ts b/packages/playwright-core/src/common/platform.ts index 330b5d6323..10bdc35922 100644 --- a/packages/playwright-core/src/common/platform.ts +++ b/packages/playwright-core/src/common/platform.ts @@ -42,6 +42,7 @@ export type Platform = { 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; @@ -70,6 +71,7 @@ export const webPlatform: Platform = { inspectCustom: undefined, + isDebuggerAttached: () => false, isLogEnabled(name: 'api' | 'channel') { return false; @@ -105,6 +107,8 @@ export const emptyPlatform: Platform = { inspectCustom: undefined, + isDebuggerAttached: () => false, + isLogEnabled(name: 'api' | 'channel') { return false; }, 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/server/utils/nodePlatform.ts b/packages/playwright-core/src/server/utils/nodePlatform.ts index 1c2a4dad0c..e9883da361 100644 --- a/packages/playwright-core/src/server/utils/nodePlatform.ts +++ b/packages/playwright-core/src/server/utils/nodePlatform.ts @@ -67,6 +67,8 @@ export const nodePlatform: Platform = { inspectCustom: util.inspect.custom, + isDebuggerAttached: () => !!require('inspector').url(), + isLogEnabled(name: 'api' | 'channel') { return debugLogger.isEnabled(name); }, From 7f742a04b0d94d29d7dedb84403a2e2a3fbad18a Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 13 Feb 2025 18:33:17 -0800 Subject: [PATCH 07/25] chore: make client compile for web (#34792) --- .../src/client/elementHandle.ts | 7 +- packages/playwright-core/src/client/stream.ts | 25 +------ .../src/client/writableStream.ts | 25 +------ .../playwright-core/src/common/platform.ts | 73 +++++++++---------- .../src/server/utils/nodePlatform.ts | 59 +++++++++++++++ 5 files changed, 98 insertions(+), 91 deletions(-) diff --git a/packages/playwright-core/src/client/elementHandle.ts b/packages/playwright-core/src/client/elementHandle.ts index b1ef721ce3..5c0988f438 100644 --- a/packages/playwright-core/src/client/elementHandle.ts +++ b/packages/playwright-core/src/client/elementHandle.ts @@ -14,9 +14,6 @@ * 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'; @@ -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, 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/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/platform.ts b/packages/playwright-core/src/common/platform.ts index 10bdc35922..25819b9a6f 100644 --- a/packages/playwright-core/src/common/platform.ts +++ b/packages/playwright-core/src/common/platform.ts @@ -19,6 +19,8 @@ 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; @@ -47,47 +49,12 @@ export type Platform = { log(name: 'api' | 'channel', message: string | Error | object): void; path: () => typeof path; pathSeparator: string; + streamFile(path: string, writable: Writable): Promise, + streamReadable: (channel: channels.StreamChannel) => Readable, + streamWritable: (channel: channels.WritableStreamChannel) => Writable, zones: { empty: Zone, current: () => Zone; }; }; -export const webPlatform: Platform = { - 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(''); - }, - - fs: () => { - throw new Error('File system is not available'); - }, - - inspectCustom: undefined, - - isDebuggerAttached: () => false, - - isLogEnabled(name: 'api' | 'channel') { - return false; - }, - - log(name: 'api' | 'channel', message: string | Error | object) {}, - - path: () => { - throw new Error('Path module is not available'); - }, - - pathSeparator: '/', - - zones: { empty: noopZone, current: () => noopZone }, -}; - export const emptyPlatform: Platform = { name: 'empty', @@ -121,5 +88,35 @@ export const emptyPlatform: Platform = { 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/server/utils/nodePlatform.ts b/packages/playwright-core/src/server/utils/nodePlatform.ts index e9883da361..fcdfebde2a 100644 --- a/packages/playwright-core/src/server/utils/nodePlatform.ts +++ b/packages/playwright-core/src/server/utils/nodePlatform.ts @@ -18,6 +18,7 @@ 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 { debugLogger } from './debugLogger'; @@ -25,6 +26,9 @@ 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; @@ -81,8 +85,63 @@ export const nodePlatform: Platform = { 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); + } +} From e276d92dd3fe02c72b81ad0b88305f4e27050418 Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Fri, 14 Feb 2025 00:33:27 -0800 Subject: [PATCH 08/25] feat(chromium-tip-of-tree): roll to r1303 (#34783) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- packages/playwright-core/browsers.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 38c5d71834..1299c8f0a0 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -15,15 +15,15 @@ }, { "name": "chromium-tip-of-tree", - "revision": "1302", + "revision": "1303", "installByDefault": false, - "browserVersion": "135.0.7011.0" + "browserVersion": "135.0.7015.0" }, { "name": "chromium-tip-of-tree-headless-shell", - "revision": "1302", + "revision": "1303", "installByDefault": false, - "browserVersion": "135.0.7011.0" + "browserVersion": "135.0.7015.0" }, { "name": "firefox", From 32c299c89d37f40a0adb63255febf6a42311bf2d Mon Sep 17 00:00:00 2001 From: Playwright Service <89237858+playwrightmachine@users.noreply.github.com> Date: Fri, 14 Feb 2025 00:33:55 -0800 Subject: [PATCH 09/25] feat(chromium): roll to r1159 (#34780) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Max Schmitt --- README.md | 4 +- packages/playwright-core/browsers.json | 8 +- .../src/server/deviceDescriptorsSource.json | 96 +++++++++---------- 3 files changed, 54 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index f236eafd1a..e8a9231fd0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🎭 Playwright -[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-134.0.6998.3-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-135.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-18.2-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord) +[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-134.0.6998.15-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-135.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-18.2-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord) ## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright) @@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 134.0.6998.3 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium 134.0.6998.15 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 18.2 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Firefox 135.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 1299c8f0a0..8d864d05ca 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -3,15 +3,15 @@ "browsers": [ { "name": "chromium", - "revision": "1158", + "revision": "1159", "installByDefault": true, - "browserVersion": "134.0.6998.3" + "browserVersion": "134.0.6998.15" }, { "name": "chromium-headless-shell", - "revision": "1158", + "revision": "1159", "installByDefault": true, - "browserVersion": "134.0.6998.3" + "browserVersion": "134.0.6998.15" }, { "name": "chromium-tip-of-tree", diff --git a/packages/playwright-core/src/server/deviceDescriptorsSource.json b/packages/playwright-core/src/server/deviceDescriptorsSource.json index 1e6d0be7b8..cbbf0608ec 100644 --- a/packages/playwright-core/src/server/deviceDescriptorsSource.json +++ b/packages/playwright-core/src/server/deviceDescriptorsSource.json @@ -110,7 +110,7 @@ "defaultBrowserType": "webkit" }, "Galaxy S5": { - "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -121,7 +121,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -132,7 +132,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S8": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 360, "height": 740 @@ -143,7 +143,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S8 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 740, "height": 360 @@ -154,7 +154,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S9+": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 320, "height": 658 @@ -165,7 +165,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S9+ landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 658, "height": 320 @@ -176,7 +176,7 @@ "defaultBrowserType": "chromium" }, "Galaxy Tab S4": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36", "viewport": { "width": 712, "height": 1138 @@ -187,7 +187,7 @@ "defaultBrowserType": "chromium" }, "Galaxy Tab S4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36", "viewport": { "width": 1138, "height": 712 @@ -1098,7 +1098,7 @@ "defaultBrowserType": "webkit" }, "LG Optimus L70": { - "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 384, "height": 640 @@ -1109,7 +1109,7 @@ "defaultBrowserType": "chromium" }, "LG Optimus L70 landscape": { - "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 640, "height": 384 @@ -1120,7 +1120,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 550": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 360, "height": 640 @@ -1131,7 +1131,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 550 landscape": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 640, "height": 360 @@ -1142,7 +1142,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 950": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 360, "height": 640 @@ -1153,7 +1153,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 950 landscape": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 640, "height": 360 @@ -1164,7 +1164,7 @@ "defaultBrowserType": "chromium" }, "Nexus 10": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36", "viewport": { "width": 800, "height": 1280 @@ -1175,7 +1175,7 @@ "defaultBrowserType": "chromium" }, "Nexus 10 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36", "viewport": { "width": 1280, "height": 800 @@ -1186,7 +1186,7 @@ "defaultBrowserType": "chromium" }, "Nexus 4": { - "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 384, "height": 640 @@ -1197,7 +1197,7 @@ "defaultBrowserType": "chromium" }, "Nexus 4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 640, "height": 384 @@ -1208,7 +1208,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -1219,7 +1219,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -1230,7 +1230,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5X": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1241,7 +1241,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5X landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1252,7 +1252,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1263,7 +1263,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1274,7 +1274,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6P": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1285,7 +1285,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6P landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1296,7 +1296,7 @@ "defaultBrowserType": "chromium" }, "Nexus 7": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36", "viewport": { "width": 600, "height": 960 @@ -1307,7 +1307,7 @@ "defaultBrowserType": "chromium" }, "Nexus 7 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36", "viewport": { "width": 960, "height": 600 @@ -1362,7 +1362,7 @@ "defaultBrowserType": "webkit" }, "Pixel 2": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 411, "height": 731 @@ -1373,7 +1373,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 731, "height": 411 @@ -1384,7 +1384,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 XL": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 411, "height": 823 @@ -1395,7 +1395,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 XL landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 823, "height": 411 @@ -1406,7 +1406,7 @@ "defaultBrowserType": "chromium" }, "Pixel 3": { - "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 393, "height": 786 @@ -1417,7 +1417,7 @@ "defaultBrowserType": "chromium" }, "Pixel 3 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 786, "height": 393 @@ -1428,7 +1428,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4": { - "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 353, "height": 745 @@ -1439,7 +1439,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 745, "height": 353 @@ -1450,7 +1450,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4a (5G)": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "screen": { "width": 412, "height": 892 @@ -1465,7 +1465,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4a (5G) landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "screen": { "height": 892, "width": 412 @@ -1480,7 +1480,7 @@ "defaultBrowserType": "chromium" }, "Pixel 5": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "screen": { "width": 393, "height": 851 @@ -1495,7 +1495,7 @@ "defaultBrowserType": "chromium" }, "Pixel 5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "screen": { "width": 851, "height": 393 @@ -1510,7 +1510,7 @@ "defaultBrowserType": "chromium" }, "Pixel 7": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "screen": { "width": 412, "height": 915 @@ -1525,7 +1525,7 @@ "defaultBrowserType": "chromium" }, "Pixel 7 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "screen": { "width": 915, "height": 412 @@ -1540,7 +1540,7 @@ "defaultBrowserType": "chromium" }, "Moto G4": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -1551,7 +1551,7 @@ "defaultBrowserType": "chromium" }, "Moto G4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -1562,7 +1562,7 @@ "defaultBrowserType": "chromium" }, "Desktop Chrome HiDPI": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36", "screen": { "width": 1792, "height": 1120 @@ -1577,7 +1577,7 @@ "defaultBrowserType": "chromium" }, "Desktop Edge HiDPI": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36 Edg/134.0.6998.3", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36 Edg/134.0.6998.15", "screen": { "width": 1792, "height": 1120 @@ -1622,7 +1622,7 @@ "defaultBrowserType": "webkit" }, "Desktop Chrome": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36", "screen": { "width": 1920, "height": 1080 @@ -1637,7 +1637,7 @@ "defaultBrowserType": "chromium" }, "Desktop Edge": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36 Edg/134.0.6998.3", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.15 Safari/537.36 Edg/134.0.6998.15", "screen": { "width": 1920, "height": 1080 From e6b405c0124698e7e3e0dc2be6103f415066835e Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 14 Feb 2025 09:32:06 +0000 Subject: [PATCH 10/25] chore: show gha pull request title instead of a merge commit (#34781) --- packages/html-reporter/src/metadataView.tsx | 30 +++++---- packages/playwright/src/isomorphic/types.d.ts | 1 + .../src/plugins/gitCommitInfoPlugin.ts | 22 +++++-- .../playwright-test-fixtures.ts | 1 + tests/playwright-test/reporter-html.spec.ts | 65 +++++++++++++++++-- 5 files changed, 95 insertions(+), 24 deletions(-) diff --git a/packages/html-reporter/src/metadataView.tsx b/packages/html-reporter/src/metadataView.tsx index 03a5ed06f4..0f54bb4249 100644 --- a/packages/html-reporter/src/metadataView.tsx +++ b/packages/html-reporter/src/metadataView.tsx @@ -107,14 +107,24 @@ const InnerMetadataView = () => { const GitCommitInfoView: React.FC<{ info: GitCommitInfo }> = ({ info }) => { const email = info['revision.email'] ? ` <${info['revision.email']}>` : ''; const author = `${info['revision.author'] || ''}${email}`; - const subject = info['revision.subject'] || ''; + + let subject = info['revision.subject'] || ''; + let link = info['revision.link']; + let shortSubject = info['revision.id']?.slice(0, 7) || 'unknown'; + + if (info['pull.link'] && info['pull.title']) { + subject = info['pull.title']; + link = info['pull.link']; + shortSubject = link ? 'Pull Request' : ''; + } + const shortTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(info['revision.timestamp']); const longTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'long' }).format(info['revision.timestamp']); return
- {info['revision.link'] ? ( - + {link ? ( + {subject} ) : @@ -130,18 +140,12 @@ const GitCommitInfoView: React.FC<{ info: GitCommitInfo }> = ({ info }) => { Logs )} - {info['pull.link'] && ( - <> - · - Pull Request - - )}
- {!!info['revision.link'] ? ( - - {info['revision.id']?.slice(0, 7) || 'unknown'} + {link ? ( + + {shortSubject} - ) : !!info['revision.id'] && {info['revision.id'].slice(0, 7)}} + ) : !!shortSubject && {shortSubject}}
; }; diff --git a/packages/playwright/src/isomorphic/types.d.ts b/packages/playwright/src/isomorphic/types.d.ts index 213f350514..2619c0df33 100644 --- a/packages/playwright/src/isomorphic/types.d.ts +++ b/packages/playwright/src/isomorphic/types.d.ts @@ -25,5 +25,6 @@ export interface GitCommitInfo { 'pull.link'?: string; 'pull.diff'?: string; 'pull.base'?: string; + 'pull.title'?: string; 'ci.link'?: string; } diff --git a/packages/playwright/src/plugins/gitCommitInfoPlugin.ts b/packages/playwright/src/plugins/gitCommitInfoPlugin.ts index 29010183c7..7ade4a005c 100644 --- a/packages/playwright/src/plugins/gitCommitInfoPlugin.ts +++ b/packages/playwright/src/plugins/gitCommitInfoPlugin.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import fs from 'fs'; + import { createGuid, spawnAsync } from 'playwright-core/lib/utils'; import type { TestRunnerPlugin } from './'; @@ -33,7 +35,7 @@ export const gitCommitInfo = (options?: GitCommitInfoPluginOptions): TestRunnerP name: 'playwright:git-commit-info', setup: async (config: FullConfig, configDir: string) => { - const fromEnv = linksFromEnv(); + const fromEnv = await linksFromEnv(); const fromCLI = await gitStatusFromCLI(options?.directory || configDir, fromEnv); config.metadata = config.metadata || {}; config.metadata['git.commit.info'] = { ...fromEnv, ...fromCLI }; @@ -45,7 +47,7 @@ interface GitCommitInfoPluginOptions { directory?: string; } -function linksFromEnv() { +async function linksFromEnv() { const out: Partial = {}; // Jenkins: https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables if (process.env.BUILD_URL) @@ -55,15 +57,21 @@ function linksFromEnv() { out['revision.link'] = `${process.env.CI_PROJECT_URL}/-/commit/${process.env.CI_COMMIT_SHA}`; if (process.env.CI_JOB_URL) out['ci.link'] = process.env.CI_JOB_URL; - // GitHub: https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables + // GitHub: https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables if (process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_SHA) out['revision.link'] = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/commit/${process.env.GITHUB_SHA}`; if (process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID) out['ci.link'] = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`; - if (process.env.GITHUB_REF_NAME && process.env.GITHUB_REF_NAME.endsWith('/merge')) { - const pullId = process.env.GITHUB_REF_NAME.substring(0, process.env.GITHUB_REF_NAME.indexOf('/merge')); - out['pull.link'] = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/pull/${pullId}`; - out['pull.base'] = process.env.GITHUB_BASE_REF; + if (process.env.GITHUB_EVENT_PATH) { + try { + const json = JSON.parse(await fs.promises.readFile(process.env.GITHUB_EVENT_PATH, 'utf8')); + if (json.pull_request) { + out['pull.title'] = json.pull_request.title; + out['pull.link'] = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/pull/${json.pull_request.number}`; + out['pull.base'] = json.pull_request.base.ref; + } + } catch { + } } return out; } diff --git a/tests/playwright-test/playwright-test-fixtures.ts b/tests/playwright-test/playwright-test-fixtures.ts index 789ed1feb6..43503497d2 100644 --- a/tests/playwright-test/playwright-test-fixtures.ts +++ b/tests/playwright-test/playwright-test-fixtures.ts @@ -227,6 +227,7 @@ export function cleanEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { GITHUB_RUN_ID: undefined, GITHUB_SERVER_URL: undefined, GITHUB_SHA: undefined, + GITHUB_EVENT_PATH: undefined, // END: Reserved CI PW_TEST_HTML_REPORT_OPEN: undefined, PLAYWRIGHT_HTML_OPEN: undefined, diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index bf88f52ade..78ef741415 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -1221,11 +1221,8 @@ for (const useIntermediateMergeReport of [true, false] as const) { const result = await runInlineTest(files, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never', GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test', - GITHUB_RUN_ID: 'example-run-id', GITHUB_SERVER_URL: 'https://playwright.dev', GITHUB_SHA: 'example-sha', - GITHUB_REF_NAME: '42/merge', - GITHUB_BASE_REF: 'HEAD~1', }); await showReport(); @@ -1235,9 +1232,69 @@ for (const useIntermediateMergeReport of [true, false] as const) { await expect(page.locator('.metadata-view')).toMatchAriaSnapshot(` - 'link "chore(html): make this test look nice"' - text: /^William on/ + - link /^[a-f0-9]{7}$/ + - text: 'foo : value1 bar : {"prop":"value2"} baz : ["value3",123]' + `); + }); + + test('should include metadata with populateGitInfo on GHA', async ({ runInlineTest, writeFiles, showReport, page }) => { + const files = { + 'uncommitted.txt': `uncommitted file`, + 'playwright.config.ts': ` + export default { + populateGitInfo: true, + metadata: { foo: 'value1', bar: { prop: 'value2' }, baz: ['value3', 123] } + }; + `, + 'example.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('sample', async ({}) => { expect(2).toBe(2); }); + `, + }; + const baseDir = await writeFiles(files); + + const execGit = async (args: string[]) => { + const { code, stdout, stderr } = await spawnAsync('git', args, { stdio: 'pipe', cwd: baseDir }); + if (!!code) + throw new Error(`Non-zero exit of:\n$ git ${args.join(' ')}\nConsole:\nstdout:\n${stdout}\n\nstderr:\n${stderr}\n\n`); + return; + }; + + await execGit(['init']); + await execGit(['config', '--local', 'user.email', 'shakespeare@example.local']); + await execGit(['config', '--local', 'user.name', 'William']); + await execGit(['add', 'playwright.config.ts']); + await execGit(['commit', '-m', 'init']); + await execGit(['add', '*.ts']); + await execGit(['commit', '-m', 'chore(html): make this test look nice']); + + const eventPath = path.join(baseDir, 'event.json'); + await fs.promises.writeFile(eventPath, JSON.stringify({ + pull_request: { + title: 'My PR', + number: 42, + base: { ref: 'main' }, + }, + })); + + const result = await runInlineTest(files, { reporter: 'dot,html' }, { + PLAYWRIGHT_HTML_OPEN: 'never', + GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test', + GITHUB_RUN_ID: 'example-run-id', + GITHUB_SERVER_URL: 'https://playwright.dev', + GITHUB_SHA: 'example-sha', + GITHUB_EVENT_PATH: eventPath, + }); + + await showReport(); + + expect(result.exitCode).toBe(0); + await page.getByRole('button', { name: 'Metadata' }).click(); + await expect(page.locator('.metadata-view')).toMatchAriaSnapshot(` + - 'link "My PR"' + - text: /^William on/ - link "Logs" - link "Pull Request" - - link /^[a-f0-9]{7}$/ - text: 'foo : value1 bar : {"prop":"value2"} baz : ["value3",123]' `); }); From df6e3f043acc287443c479ce7901be336a6ee5e1 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 14 Feb 2025 11:58:37 +0100 Subject: [PATCH 11/25] devops: migrate away from merge.config.ts (#34802) --- .github/workflows/create_test_report.yml | 2 +- .github/workflows/merge.config.ts | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) delete mode 100644 .github/workflows/merge.config.ts diff --git a/.github/workflows/create_test_report.yml b/.github/workflows/create_test_report.yml index dfc6961c2c..3799f14159 100644 --- a/.github/workflows/create_test_report.yml +++ b/.github/workflows/create_test_report.yml @@ -33,7 +33,7 @@ jobs: - name: Merge reports run: | - npx playwright merge-reports --config .github/workflows/merge.config.ts ./all-blob-reports + npx playwright merge-reports --reporter=html,packages/playwright/lib/reporters/markdown.js ./all-blob-reports env: NODE_OPTIONS: --max-old-space-size=8192 diff --git a/.github/workflows/merge.config.ts b/.github/workflows/merge.config.ts deleted file mode 100644 index e8582ed521..0000000000 --- a/.github/workflows/merge.config.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default { - testDir: '../../tests', - reporter: [[require.resolve('../../packages/playwright/lib/reporters/markdown')], ['html']] -}; \ No newline at end of file From fe0b327770517d9440695b6c96e69ddc31c81386 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Fri, 14 Feb 2025 16:59:26 +0100 Subject: [PATCH 12/25] feat(ui): llm conversation about error (#34750) --- package-lock.json | 16 +- package.json | 1 + .../src/server/trace/viewer/traceViewer.ts | 10 + packages/trace-viewer/package.json | 3 +- packages/trace-viewer/src/sw/main.ts | 6 + .../trace-viewer/src/ui/aiConversation.css | 128 +++++++++ .../trace-viewer/src/ui/aiConversation.tsx | 84 ++++++ packages/trace-viewer/src/ui/errorsTab.tsx | 144 +++++++--- packages/trace-viewer/src/ui/llm.tsx | 267 ++++++++++++++++++ packages/trace-viewer/src/ui/uiModeView.tsx | 5 +- packages/trace-viewer/src/ui/workbench.tsx | 2 +- .../trace-viewer/src/ui/workbenchLoader.tsx | 5 +- packages/web/src/uiUtils.ts | 10 + tests/playwright-test/ui-mode-llm.spec.ts | 99 +++++++ 14 files changed, 731 insertions(+), 49 deletions(-) create mode 100644 packages/trace-viewer/src/ui/aiConversation.css create mode 100644 packages/trace-viewer/src/ui/aiConversation.tsx create mode 100644 packages/trace-viewer/src/ui/llm.tsx create mode 100644 tests/playwright-test/ui-mode-llm.spec.ts diff --git a/package-lock.json b/package-lock.json index eb69ce2f36..e1edd82bf3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,6 +59,7 @@ "formidable": "^2.1.1", "immutable": "^4.3.7", "license-checker": "^25.0.1", + "markdown-to-jsx": "^7.7.3", "mime": "^3.0.0", "node-stream-zip": "^1.15.0", "react": "^18.1.0", @@ -5897,7 +5898,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -5953,6 +5953,18 @@ "semver": "bin/semver" } }, + "node_modules/markdown-to-jsx": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.7.3.tgz", + "integrity": "sha512-o35IhJDFP6Fv60zPy+hbvZSQMmgvSGdK5j8NRZ7FeZMY+Bgqw+dSg7SC1ZEzC26++CiOUCqkbq96/c3j/FfTEQ==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -6784,7 +6796,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dev": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -8975,6 +8986,7 @@ "packages/trace-viewer": { "version": "0.0.0", "dependencies": { + "markdown-to-jsx": "^7.7.3", "yaml": "^2.6.0" } }, diff --git a/package.json b/package.json index 8b54d54e6c..f27ad12a76 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "formidable": "^2.1.1", "immutable": "^4.3.7", "license-checker": "^25.0.1", + "markdown-to-jsx": "^7.7.3", "mime": "^3.0.0", "node-stream-zip": "^1.15.0", "react": "^18.1.0", diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index e0f05bac41..3f322e7125 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -142,6 +142,16 @@ export async function installRootRedirect(server: HttpServer, traceUrls: string[ server.routePath('/', (_, response) => { response.statusCode = 302; response.setHeader('Location', urlPath); + + if (process.env.OPENAI_API_KEY) + response.appendHeader('Set-Cookie', `openai_api_key=${process.env.OPENAI_API_KEY}`); + if (process.env.OPENAI_BASE_URL) + response.appendHeader('Set-Cookie', `openai_base_url=${process.env.OPENAI_BASE_URL}`); + if (process.env.ANTHROPIC_API_KEY) + response.appendHeader('Set-Cookie', `anthropic_api_key=${process.env.ANTHROPIC_API_KEY}`); + if (process.env.ANTHROPIC_BASE_URL) + response.appendHeader('Set-Cookie', `anthropic_base_url=${process.env.ANTHROPIC_BASE_URL}`); + response.end(); return true; }); diff --git a/packages/trace-viewer/package.json b/packages/trace-viewer/package.json index 57a3dfd409..fde4eb6766 100644 --- a/packages/trace-viewer/package.json +++ b/packages/trace-viewer/package.json @@ -4,6 +4,7 @@ "version": "0.0.0", "type": "module", "dependencies": { - "yaml": "^2.6.0" + "yaml": "^2.6.0", + "markdown-to-jsx": "^7.7.3" } } diff --git a/packages/trace-viewer/src/sw/main.ts b/packages/trace-viewer/src/sw/main.ts index cd23082d91..cdce3261fe 100644 --- a/packages/trace-viewer/src/sw/main.ts +++ b/packages/trace-viewer/src/sw/main.ts @@ -78,6 +78,12 @@ async function doFetch(event: FetchEvent): Promise { if (event.request.url.startsWith('chrome-extension://')) return fetch(event.request); + if (event.request.headers.get('x-pw-serviceworker') === 'forward') { + const request = new Request(event.request); + request.headers.delete('x-pw-serviceworker'); + return fetch(request); + } + const request = event.request; const client = await self.clients.get(event.clientId); diff --git a/packages/trace-viewer/src/ui/aiConversation.css b/packages/trace-viewer/src/ui/aiConversation.css new file mode 100644 index 0000000000..214496017a --- /dev/null +++ b/packages/trace-viewer/src/ui/aiConversation.css @@ -0,0 +1,128 @@ +/** + * 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. + */ + +.chat-container { + flex: 1; + display: flex; + flex-direction: column; + justify-content: space-between; + color: #e0e0e0; +} + +.chat-disclaimer { + text-align: center; + color: var(--vscode-editorBracketMatch-border); + margin: 0px; +} + +.chat-container hr { + width: 100%; + border: none; + border-top: 1px solid var(--vscode-titleBar-inactiveBackground); +} + +.messages-container { + flex: 1; + overflow-y: auto; + padding: 16px; + + display: flex; + flex-direction: column; + gap: 16px; +} + +.message { + gap: 12px; + max-width: 85%; +} + +.user-message { + flex-direction: row-reverse; + margin-left: auto; + width: fit-content +} + +.message-icon { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--vscode-titleBar-inactiveBackground); + flex-shrink: 0; +} + +.message-content { + background-color: var(--vscode-titleBar-inactiveBackground); + color: var(--vscode-titleBar-activeForeground); + padding: 2px 8px; +} + +.message-content pre { + text-wrap: auto; + overflow-wrap: anywhere; +} + +.user-message .message-content { + background-color: var(--vscode-titleBar-activeBackground); +} + +/* Input form styles */ +.input-form { + position: sticky; + bottom: 0; + display: flex; + height: 64px; + gap: 8px; + padding: 10px; + background-color: var(--vscode-sideBar-background); + border-top: 1px solid var(--vscode-sideBarSectionHeader-border); +} + +.message-input { + flex: 1; + padding: 8px 12px; + border: 1px solid var(--vscode-settings-textInputBorder); + background-color: var(--vscode-settings-textInputBackground); + font-size: 14px; + outline: none; + transition: border-color 0.2s; +} + +.message-input:focus { + border-color: #0078d4; +} + +.send-button { + display: flex; + align-items: center; + justify-content: center; + padding: 8px; + background-color: var(--vscode-button-background); + border: none; + color: white; + cursor: pointer; + transition: background-color 0.2s; +} + +.send-button:hover { + background-color: var(--vscode-button-hoverBackground); +} + +.send-button:disabled { + background-color: var(--vscode-disabledForeground); + cursor: not-allowed; +} diff --git a/packages/trace-viewer/src/ui/aiConversation.tsx b/packages/trace-viewer/src/ui/aiConversation.tsx new file mode 100644 index 0000000000..548ed626f0 --- /dev/null +++ b/packages/trace-viewer/src/ui/aiConversation.tsx @@ -0,0 +1,84 @@ +/** + * 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 { useCallback, useState } from 'react'; +import Markdown from 'markdown-to-jsx'; +import './aiConversation.css'; +import { clsx } from '@web/uiUtils'; +import { useLLMConversation } from './llm'; + +export function AIConversation({ conversationId }: { conversationId: string }) { + const [history, conversation] = useLLMConversation(conversationId); + const [input, setInput] = useState(''); + + const onSubmit = useCallback(() => { + setInput(content => { + conversation.send(content); + return ''; + }); + }, [conversation]); + + return ( +
+

Chat based on {conversation.chat.api.name}. Check for mistakes.

+
+
+ {history.filter(({ role }) => role !== 'developer').map((message, index) => ( +
+ {message.role === 'assistant' && ( +
+ +
+ )} +
+ {message.displayContent ?? message.content} +
+
+ ))} +
+ +
+