diff --git a/packages/playwright-core/src/androidServerImpl.ts b/packages/playwright-core/src/androidServerImpl.ts index 5f3758119a2b6..dc044735114ae 100644 --- a/packages/playwright-core/src/androidServerImpl.ts +++ b/packages/playwright-core/src/androidServerImpl.ts @@ -18,6 +18,8 @@ import { PlaywrightServer } from './remote/playwrightServer'; import { createPlaywright } from './server/playwright'; import { createGuid } from './server/utils/crypto'; import { ws } from './utilsBundle'; +import { ProgressController } from './server/progress'; +import { serverSideCallMetadata } from './server'; import type { BrowserServer } from './client/browserType'; import type { LaunchAndroidServerOptions } from './client/types'; @@ -27,11 +29,12 @@ export class AndroidServerLauncherImpl { async launchServer(options: LaunchAndroidServerOptions = {}): Promise { const playwright = createPlaywright({ sdkLanguage: 'javascript', isServer: true }); // 1. Pre-connect to the device - let devices = await playwright.android.devices({ + const controller = new ProgressController(serverSideCallMetadata(), playwright); + let devices = await controller.run(progress => playwright.android.devices(progress, { host: options.adbHost, port: options.adbPort, omitDriverInstall: options.omitDriverInstall, - }); + })); if (devices.length === 0) throw new Error('No devices found'); diff --git a/packages/playwright-core/src/browserServerImpl.ts b/packages/playwright-core/src/browserServerImpl.ts index 9a8b295202e40..d8c36f71747b0 100644 --- a/packages/playwright-core/src/browserServerImpl.ts +++ b/packages/playwright-core/src/browserServerImpl.ts @@ -24,6 +24,7 @@ import { rewriteErrorMessage } from './utils/isomorphic/stackTrace'; import { DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT } from './utils/isomorphic/time'; import { ws } from './utilsBundle'; import * as validatorPrimitives from './protocol/validatorPrimitives'; +import { ProgressController } from './server/progress'; import type { BrowserServer, BrowserServerLauncher } from './client/browserType'; import type { LaunchServerOptions, Logger, Env } from './client/types'; @@ -59,16 +60,19 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher { let browser: Browser; try { - if (options._userDataDir !== undefined) { - const validator = validatorPrimitives.scheme['BrowserTypeLaunchPersistentContextParams']; - launchOptions = validator({ ...launchOptions, userDataDir: options._userDataDir }, '', validatorContext); - const context = await playwright[this._browserName].launchPersistentContext(metadata, options._userDataDir, launchOptions); - browser = context._browser; - } else { - const validator = validatorPrimitives.scheme['BrowserTypeLaunchParams']; - launchOptions = validator(launchOptions, '', validatorContext); - browser = await playwright[this._browserName].launch(metadata, launchOptions, toProtocolLogger(options.logger)); - } + const controller = new ProgressController(metadata, playwright[this._browserName]); + browser = await controller.run(async progress => { + if (options._userDataDir !== undefined) { + const validator = validatorPrimitives.scheme['BrowserTypeLaunchPersistentContextParams']; + launchOptions = validator({ ...launchOptions, userDataDir: options._userDataDir }, '', validatorContext); + const context = await playwright[this._browserName].launchPersistentContext(progress, options._userDataDir, launchOptions); + return context._browser; + } else { + const validator = validatorPrimitives.scheme['BrowserTypeLaunchParams']; + launchOptions = validator(launchOptions, '', validatorContext); + return await playwright[this._browserName].launch(progress, launchOptions, toProtocolLogger(options.logger)); + } + }); } catch (e) { const log = helper.formatBrowserLogs(metadata.log); rewriteErrorMessage(e, `${e.message} Failed to launch browser.${log}`); diff --git a/packages/playwright-core/src/remote/playwrightServer.ts b/packages/playwright-core/src/remote/playwrightServer.ts index 9839cff9b3fcc..dd339aec9d592 100644 --- a/packages/playwright-core/src/remote/playwrightServer.ts +++ b/packages/playwright-core/src/remote/playwrightServer.ts @@ -25,11 +25,13 @@ import { debugLogger, isUnderTest } from '../utils'; import { serverSideCallMetadata } from '../server'; import { SocksProxy } from '../server/utils/socksProxy'; import { Browser } from '../server/browser'; +import { ProgressController } from '../server/progress'; import type { AndroidDevice } from '../server/android/android'; import type { Playwright } from '../server/playwright'; -import type { LaunchOptions } from '../server/types'; +import type { LaunchOptions as LaunchOptionsWithoutTimeout } from '../server/types'; +type LaunchOptionsWithTimeout = LaunchOptionsWithoutTimeout & { timeout: number }; type ServerOptions = { path: string; @@ -93,9 +95,11 @@ export class PlaywrightServer { const launchOptionsHeader = request.headers['x-playwright-launch-options'] || ''; const launchOptionsHeaderValue = Array.isArray(launchOptionsHeader) ? launchOptionsHeader[0] : launchOptionsHeader; const launchOptionsParam = url.searchParams.get('launch-options'); - let launchOptions: LaunchOptions = { timeout: DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT }; + let launchOptions: LaunchOptionsWithTimeout = { timeout: DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT }; try { launchOptions = JSON.parse(launchOptionsParam || launchOptionsHeaderValue); + if (!launchOptions.timeout) + launchOptions.timeout = DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT; } catch (e) { } @@ -172,7 +176,7 @@ export class PlaywrightServer { }); } - private async _initReuseBrowsersMode(browserName: string | null, launchOptions: LaunchOptions, id: string): Promise { + private async _initReuseBrowsersMode(browserName: string | null, launchOptions: LaunchOptionsWithTimeout, id: string): Promise { // Note: reuse browser mode does not support socks proxy, because // clients come and go, while the browser stays the same. @@ -184,7 +188,7 @@ export class PlaywrightServer { return false; if (this._dontReuseBrowsers.has(b)) return false; - const existingOptions = launchOptionsHash(b.options.originalLaunchOptions); + const existingOptions = launchOptionsHash({ ...b.options.originalLaunchOptions, timeout: DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT }); return existingOptions === requestedOptions; }); @@ -199,10 +203,12 @@ export class PlaywrightServer { } if (!browser) { - browser = await this._playwright[(browserName || 'chromium') as 'chromium'].launch(serverSideCallMetadata(), { + const browserType = this._playwright[(browserName || 'chromium') as 'chromium']; + const controller = new ProgressController(serverSideCallMetadata(), browserType); + browser = await controller.run(progress => browserType.launch(progress, { ...launchOptions, headless: !!process.env.PW_DEBUG_CONTROLLER_HEADLESS, - }); + }), launchOptions.timeout); } return { @@ -222,14 +228,16 @@ export class PlaywrightServer { }; } - private async _initConnectMode(id: string, filter: 'first', browserName: string | null, launchOptions: LaunchOptions): Promise { + private async _initConnectMode(id: string, filter: 'first', browserName: string | null, launchOptions: LaunchOptionsWithTimeout): Promise { browserName ??= 'chromium'; debugLogger.log('server', `[${id}] engaged connect mode`); let browser = this._playwright.allBrowsers().find(b => b.options.name === browserName); if (!browser) { - browser = await this._playwright[browserName as 'chromium'].launch(serverSideCallMetadata(), launchOptions); + const browserType = this._playwright[browserName as 'chromium']; + const controller = new ProgressController(serverSideCallMetadata(), browserType); + browser = await controller.run(progress => browserType.launch(progress, launchOptions), launchOptions.timeout); this._dontReuse(browser); } @@ -268,7 +276,7 @@ export class PlaywrightServer { }; } - private async _initLaunchBrowserMode(browserName: string | null, proxyValue: string | undefined, launchOptions: LaunchOptions, id: string): Promise { + private async _initLaunchBrowserMode(browserName: string | null, proxyValue: string | undefined, launchOptions: LaunchOptionsWithTimeout, id: string): Promise { debugLogger.log('server', `[${id}] engaged launch mode for "${browserName}"`); let socksProxy: SocksProxy | undefined; if (proxyValue) { @@ -279,7 +287,9 @@ export class PlaywrightServer { } else { launchOptions.socksProxyPort = undefined; } - const browser = await this._playwright[browserName as 'chromium'].launch(serverSideCallMetadata(), launchOptions); + const browserType = this._playwright[browserName as 'chromium']; + const controller = new ProgressController(serverSideCallMetadata(), browserType); + const browser = await controller.run(progress => browserType.launch(progress, launchOptions), launchOptions.timeout); this._dontReuseBrowsers.add(browser); return { preLaunchedBrowser: browser, @@ -334,10 +344,10 @@ function userAgentVersionMatchesErrorMessage(userAgent: string) { } } -function launchOptionsHash(options: LaunchOptions) { +function launchOptionsHash(options: LaunchOptionsWithTimeout) { const copy = { ...options }; for (const k of Object.keys(copy)) { - const key = k as keyof LaunchOptions; + const key = k as keyof LaunchOptionsWithTimeout; if (copy[key] === defaultLaunchOptions[key]) delete copy[key]; } @@ -346,7 +356,7 @@ function launchOptionsHash(options: LaunchOptions) { return JSON.stringify(copy); } -function filterLaunchOptions(options: LaunchOptions, allowFSPaths: boolean): LaunchOptions { +function filterLaunchOptions(options: LaunchOptionsWithTimeout, allowFSPaths: boolean): LaunchOptionsWithTimeout { return { channel: options.channel, args: options.args, @@ -363,7 +373,7 @@ function filterLaunchOptions(options: LaunchOptions, allowFSPaths: boolean): Lau }; } -const defaultLaunchOptions: Partial = { +const defaultLaunchOptions: Partial = { ignoreAllDefaultArgs: false, handleSIGINT: false, handleSIGTERM: false, @@ -372,7 +382,7 @@ const defaultLaunchOptions: Partial = { devtools: false, }; -const optionsThatAllowBrowserReuse: (keyof LaunchOptions)[] = [ +const optionsThatAllowBrowserReuse: (keyof LaunchOptionsWithTimeout)[] = [ 'headless', 'timeout', 'tracesDir', diff --git a/packages/playwright-core/src/server/android/android.ts b/packages/playwright-core/src/server/android/android.ts index 69452e4e3b442..3f8785ae62516 100644 --- a/packages/playwright-core/src/server/android/android.ts +++ b/packages/playwright-core/src/server/android/android.ts @@ -32,9 +32,9 @@ import { chromiumSwitches } from '../chromium/chromiumSwitches'; import { CRBrowser } from '../chromium/crBrowser'; import { removeFolders } from '../utils/fileUtils'; import { helper } from '../helper'; -import { CallMetadata, SdkObject } from '../instrumentation'; +import { SdkObject, serverSideCallMetadata } from '../instrumentation'; import { gracefullyCloseSet } from '../utils/processLauncher'; -import { Progress, ProgressController } from '../progress'; +import { isAbortError, Progress, ProgressController } from '../progress'; import { registry } from '../registry'; import type { BrowserOptions, BrowserProcess } from '../browser'; @@ -72,14 +72,14 @@ export class Android extends SdkObject { this._backend = backend; } - async devices(options: channels.AndroidDevicesOptions): Promise { - const devices = (await this._backend.devices(options)).filter(d => d.status === 'device'); + async devices(progress: Progress, options: channels.AndroidDevicesOptions): Promise { + const devices = (await progress.race(this._backend.devices(options))).filter(d => d.status === 'device'); const newSerials = new Set(); for (const d of devices) { newSerials.add(d.serial); if (this._devices.has(d.serial)) continue; - const device = await AndroidDevice.create(this, d, options); + const device = await progress.raceWithCleanup(AndroidDevice.create(this, d, options), device => device.close()); this._devices.set(d.serial, device); } for (const d of this._devices.keys()) { @@ -151,8 +151,8 @@ export class AndroidDevice extends SdkObject { return result; } - async open(command: string): Promise { - return await this._backend.open(`${command}`); + async open(progress: Progress, command: string): Promise { + return await progress.raceWithCleanup(this._backend.open(`${command}`), socket => socket.close()); } async screenshot(): Promise { @@ -162,20 +162,22 @@ export class AndroidDevice extends SdkObject { private async _driver(): Promise { if (this._isClosed) return; - if (!this._driverPromise) - this._driverPromise = this._installDriver(); + if (!this._driverPromise) { + const controller = new ProgressController(serverSideCallMetadata(), this); + this._driverPromise = controller.run(progress => this._installDriver(progress)); + } return this._driverPromise; } - private async _installDriver(): Promise { + private async _installDriver(progress: Progress): Promise { debug('pw:android')('Stopping the old driver'); - await this.shell(`am force-stop com.microsoft.playwright.androiddriver`); + await progress.race(this.shell(`am force-stop com.microsoft.playwright.androiddriver`)); // uninstall and install driver on every execution if (!this._options.omitDriverInstall) { debug('pw:android')('Uninstalling the old driver'); - await this.shell(`cmd package uninstall com.microsoft.playwright.androiddriver`); - await this.shell(`cmd package uninstall com.microsoft.playwright.androiddriver.test`); + await progress.race(this.shell(`cmd package uninstall com.microsoft.playwright.androiddriver`)); + await progress.race(this.shell(`cmd package uninstall com.microsoft.playwright.androiddriver.test`)); debug('pw:android')('Installing the new driver'); const executable = registry.findExecutable('android')!; @@ -184,7 +186,7 @@ export class AndroidDevice extends SdkObject { const fullName = path.join(executable.directory!, file); if (!fs.existsSync(fullName)) throw new Error(`Please install Android driver apk using '${packageManagerCommand} playwright install android'`); - await this.installApk(await fs.promises.readFile(fullName)); + await this.installApk(progress, await progress.race(fs.promises.readFile(fullName))); } } else { debug('pw:android')('Skipping the driver installation'); @@ -192,7 +194,7 @@ export class AndroidDevice extends SdkObject { debug('pw:android')('Starting the new driver'); this.shell('am instrument -w com.microsoft.playwright.androiddriver.test/androidx.test.runner.AndroidJUnitRunner').catch(e => debug('pw:android')(e)); - const socket = await this._waitForLocalAbstract('playwright_android_driver_socket'); + const socket = await this._waitForLocalAbstract(progress, 'playwright_android_driver_socket'); const transport = new PipeTransport(socket, socket, socket, 'be'); transport.onmessage = message => { const response = JSON.parse(message); @@ -209,14 +211,16 @@ export class AndroidDevice extends SdkObject { return transport; } - private async _waitForLocalAbstract(socketName: string): Promise { + private async _waitForLocalAbstract(progress: Progress, socketName: string): Promise { let socket: SocketBackend | undefined; debug('pw:android')(`Polling the socket localabstract:${socketName}`); while (!socket) { try { - socket = await this._backend.open(`localabstract:${socketName}`); + socket = await progress.raceWithCleanup(this._backend.open(`localabstract:${socketName}`), socket => socket.close()); } catch (e) { - await new Promise(f => setTimeout(f, 250)); + if (isAbortError(e)) + throw e; + await progress.wait(250); } } debug('pw:android')(`Connected to localabstract:${socketName}`); @@ -259,21 +263,18 @@ export class AndroidDevice extends SdkObject { this.emit(AndroidDevice.Events.Close); } - async launchBrowser(metadata: CallMetadata, pkg: string = 'com.android.chrome', options: channels.AndroidDeviceLaunchBrowserParams): Promise { - const controller = new ProgressController(metadata, this); - return controller.run(async progress => { - debug('pw:android')('Force-stopping', pkg); - await this._backend.runCommand(`shell:am force-stop ${pkg}`); - const socketName = isUnderTest() ? 'webview_devtools_remote_playwright_test' : ('playwright_' + createGuid() + '_devtools_remote'); - const commandLine = this._defaultArgs(options, socketName).join(' '); - debug('pw:android')('Starting', pkg, commandLine); - // encode commandLine to base64 to avoid issues (bash encoding) with special characters - await progress.race(this._backend.runCommand(`shell:echo "${Buffer.from(commandLine).toString('base64')}" | base64 -d > /data/local/tmp/chrome-command-line`)); - await progress.race(this._backend.runCommand(`shell:am start -a android.intent.action.VIEW -d about:blank ${pkg}`)); - const browserContext = await this._connectToBrowser(progress, socketName, options); - await progress.race(this._backend.runCommand(`shell:rm /data/local/tmp/chrome-command-line`)); - return browserContext; - }); + async launchBrowser(progress: Progress, pkg: string = 'com.android.chrome', options: channels.AndroidDeviceLaunchBrowserParams): Promise { + debug('pw:android')('Force-stopping', pkg); + await this._backend.runCommand(`shell:am force-stop ${pkg}`); + const socketName = isUnderTest() ? 'webview_devtools_remote_playwright_test' : ('playwright_' + createGuid() + '_devtools_remote'); + const commandLine = this._defaultArgs(options, socketName).join(' '); + debug('pw:android')('Starting', pkg, commandLine); + // encode commandLine to base64 to avoid issues (bash encoding) with special characters + await progress.race(this._backend.runCommand(`shell:echo "${Buffer.from(commandLine).toString('base64')}" | base64 -d > /data/local/tmp/chrome-command-line`)); + await progress.race(this._backend.runCommand(`shell:am start -a android.intent.action.VIEW -d about:blank ${pkg}`)); + const browserContext = await this._connectToBrowser(progress, socketName, options); + await progress.race(this._backend.runCommand(`shell:rm /data/local/tmp/chrome-command-line`)); + return browserContext; } private _defaultArgs(options: channels.AndroidDeviceLaunchBrowserParams, socketName: string): string[] { @@ -305,18 +306,15 @@ export class AndroidDevice extends SdkObject { return chromeArguments; } - async connectToWebView(metadata: CallMetadata, socketName: string): Promise { - const controller = new ProgressController(metadata, this); - return controller.run(async progress => { - const webView = this._webViews.get(socketName); - if (!webView) - throw new Error('WebView has been closed'); - return await this._connectToBrowser(progress, socketName); - }); + async connectToWebView(progress: Progress, socketName: string): Promise { + const webView = this._webViews.get(socketName); + if (!webView) + throw new Error('WebView has been closed'); + return await this._connectToBrowser(progress, socketName); } private async _connectToBrowser(progress: Progress, socketName: string, options: types.BrowserContextOptions = {}): Promise { - const socket = await progress.race(this._waitForLocalAbstract(socketName)); + const socket = await this._waitForLocalAbstract(progress, socketName); const androidBrowser = new AndroidBrowser(this, socket); progress.cleanupWhenAborted(() => androidBrowser.close()); await progress.race(androidBrowser._init()); @@ -346,7 +344,7 @@ export class AndroidDevice extends SdkObject { proxy: options.proxy, protocolLogger: helper.debugProtocolLogger(), browserLogsCollector: new RecentLogsCollector(), - originalLaunchOptions: { timeout: 0 }, + originalLaunchOptions: {}, }; validateBrowserContextOptions(options, browserOptions); @@ -360,35 +358,35 @@ export class AndroidDevice extends SdkObject { return [...this._webViews.values()]; } - async installApk(content: Buffer, options?: { args?: string[] }): Promise { + async installApk(progress: Progress, content: Buffer, options?: { args?: string[] }): Promise { const args = options && options.args ? options.args : ['-r', '-t', '-S']; debug('pw:android')('Opening install socket'); - const installSocket = await this._backend.open(`shell:cmd package install ${args.join(' ')} ${content.length}`); + const installSocket = await progress.raceWithCleanup(this._backend.open(`shell:cmd package install ${args.join(' ')} ${content.length}`), socket => socket.close()); debug('pw:android')('Writing driver bytes: ' + content.length); - await installSocket.write(content); - const success = await new Promise(f => installSocket.on('data', f)); + await progress.race(installSocket.write(content)); + const success = await progress.race(new Promise(f => installSocket.on('data', f))); debug('pw:android')('Written driver bytes: ' + success); installSocket.close(); } - async push(content: Buffer, path: string, mode = 0o644): Promise { - const socket = await this._backend.open(`sync:`); + async push(progress: Progress, content: Buffer, path: string, mode = 0o644): Promise { + const socket = await progress.raceWithCleanup(this._backend.open(`sync:`), socket => socket.close()); const sendHeader = async (command: string, length: number) => { const buffer = Buffer.alloc(command.length + 4); buffer.write(command, 0); buffer.writeUInt32LE(length, command.length); - await socket.write(buffer); + await progress.race(socket.write(buffer)); }; const send = async (command: string, data: Buffer) => { await sendHeader(command, data.length); - await socket.write(data); + await progress.race(socket.write(data)); }; await send('SEND', Buffer.from(`${path},${mode}`)); const maxChunk = 65535; for (let i = 0; i < content.length; i += maxChunk) await send('DATA', content.slice(i, i + maxChunk)); await sendHeader('DONE', (Date.now() / 1000) | 0); - const result = await new Promise(f => socket.once('data', f)); + const result = await progress.race(new Promise(f => socket.once('data', f))); const code = result.slice(0, 4).toString(); if (code !== 'OKAY') throw new Error('Could not push: ' + code); diff --git a/packages/playwright-core/src/server/browser.ts b/packages/playwright-core/src/server/browser.ts index f19c124afdfeb..b989bb0eb07ea 100644 --- a/packages/playwright-core/src/server/browser.ts +++ b/packages/playwright-core/src/server/browser.ts @@ -116,12 +116,12 @@ export abstract class Browser extends SdkObject { return context; } - async newContextForReuse(params: channels.BrowserNewContextForReuseParams, metadata: CallMetadata): Promise<{ context: BrowserContext, needsReset: boolean }> { + async newContextForReuse(progress: Progress, params: channels.BrowserNewContextForReuseParams): Promise<{ context: BrowserContext, needsReset: boolean }> { const hash = BrowserContext.reusableContextHash(params); if (!this._contextForReuse || hash !== this._contextForReuse.hash || !this._contextForReuse.context.canResetForReuse()) { if (this._contextForReuse) await this._contextForReuse.context.close({ reason: 'Context reused' }); - this._contextForReuse = { context: await this.newContextFromMetadata(metadata, params), hash }; + this._contextForReuse = { context: await this.newContext(progress, params), hash }; return { context: this._contextForReuse.context, needsReset: false }; } await this._contextForReuse.context.stopPendingOperations('Context recreated'); diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index f556f83716158..32cc2caf469e8 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -27,7 +27,7 @@ import { BrowserContextAPIRequestContext } from './fetch'; import { mkdirIfNeeded } from './utils/fileUtils'; import { HarRecorder } from './har/harRecorder'; import { helper } from './helper'; -import { SdkObject, serverSideCallMetadata } from './instrumentation'; +import { SdkObject } from './instrumentation'; import * as network from './network'; import { InitScript } from './page'; import { Page, PageBinding } from './page'; @@ -42,7 +42,6 @@ import type { Artifact } from './artifact'; import type { Browser, BrowserOptions } from './browser'; import type { Download } from './download'; import type * as frames from './frames'; -import type { CallMetadata } from './instrumentation'; import type { Progress } from './progress'; import type { ClientCertificatesProxy } from './socksClientCertificatesInterceptor'; import type { SerializedStorage } from '@injected/storageScript'; @@ -190,12 +189,7 @@ export abstract class BrowserContext extends SdkObject { return JSON.stringify(paramsCopy); } - async resetForReuse(metadata: CallMetadata, params: channels.BrowserNewContextForReuseParams | null) { - const controller = new ProgressController(metadata, this); - return controller.run(progress => this.resetForReuseImpl(progress, params)); - } - - async resetForReuseImpl(progress: Progress, params: channels.BrowserNewContextForReuseParams | null) { + async resetForReuse(progress: Progress, params: channels.BrowserNewContextForReuseParams | null) { await this.tracing.resetForReuse(progress); if (params) { @@ -207,7 +201,7 @@ export abstract class BrowserContext extends SdkObject { // Close extra pages early. let page: Page | undefined = this.pages()[0]; - const [, ...otherPages] = this.pages(); + const otherPages = this.possiblyUninitializedPages().filter(p => p !== page); for (const p of otherPages) await p.close(); if (page && page.hasCrashed()) { @@ -539,11 +533,6 @@ export abstract class BrowserContext extends SdkObject { await this._closePromise; } - newPageFromMetadata(metadata: CallMetadata): Promise { - const contoller = new ProgressController(metadata, this); - return contoller.run(progress => this.newPage(progress, false)); - } - async newPage(progress: Progress, isServerSide: boolean): Promise { const page = await progress.raceWithCleanup(this.doCreateNewPage(isServerSide), page => page.close()); const pageOrError = await progress.race(page.waitForInitializedOrError()); @@ -559,12 +548,7 @@ export abstract class BrowserContext extends SdkObject { this._origins.add(origin); } - storageState(indexedDB = false): Promise { - const controller = new ProgressController(serverSideCallMetadata(), this); - return controller.run(progress => this.storageStateImpl(progress, indexedDB)); - } - - async storageStateImpl(progress: Progress, indexedDB: boolean): Promise { + async storageState(progress: Progress, indexedDB = false): Promise { const result: channels.BrowserContextStorageStateResult = { cookies: await this.cookies(), origins: [] diff --git a/packages/playwright-core/src/server/browserType.ts b/packages/playwright-core/src/server/browserType.ts index ba0cfa4aa148e..554382b3a27ca 100644 --- a/packages/playwright-core/src/server/browserType.ts +++ b/packages/playwright-core/src/server/browserType.ts @@ -28,7 +28,6 @@ import { helper } from './helper'; import { SdkObject } from './instrumentation'; import { PipeTransport } from './pipeTransport'; import { envArrayToObject, launchProcess } from './utils/processLauncher'; -import { ProgressController } from './progress'; import { isProtocolError } from './protocolError'; import { registry } from './registry'; import { ClientCertificatesProxy } from './socksClientCertificatesInterceptor'; @@ -37,7 +36,6 @@ import { RecentLogsCollector } from './utils/debugLogger'; import type { Browser, BrowserOptions, BrowserProcess } from './browser'; import type { BrowserContext } from './browserContext'; -import type { CallMetadata } from './instrumentation'; import type { Env } from './utils/processLauncher'; import type { Progress } from './progress'; import type { ProtocolError } from './protocolError'; @@ -67,34 +65,26 @@ export abstract class BrowserType extends SdkObject { return this._name; } - async launch(metadata: CallMetadata, options: types.LaunchOptions, protocolLogger?: types.ProtocolLogger): Promise { + async launch(progress: Progress, options: types.LaunchOptions, protocolLogger?: types.ProtocolLogger): Promise { options = this._validateLaunchOptions(options); - const controller = new ProgressController(metadata, this); - const browser = await controller.run(progress => { - const seleniumHubUrl = (options as any).__testHookSeleniumRemoteURL || process.env.SELENIUM_REMOTE_URL; - if (seleniumHubUrl) - return this._launchWithSeleniumHub(progress, seleniumHubUrl, options); - return this._innerLaunchWithRetries(progress, options, undefined, helper.debugProtocolLogger(protocolLogger)).catch(e => { throw this._rewriteStartupLog(e); }); - }, options.timeout); - return browser; + const seleniumHubUrl = (options as any).__testHookSeleniumRemoteURL || process.env.SELENIUM_REMOTE_URL; + if (seleniumHubUrl) + return this._launchWithSeleniumHub(progress, seleniumHubUrl, options); + return this._innerLaunchWithRetries(progress, options, undefined, helper.debugProtocolLogger(protocolLogger)).catch(e => { throw this._rewriteStartupLog(e); }); } - async launchPersistentContext(metadata: CallMetadata, userDataDir: string, options: channels.BrowserTypeLaunchPersistentContextOptions & { timeout: number, cdpPort?: number, internalIgnoreHTTPSErrors?: boolean, socksProxyPort?: number }): Promise { + async launchPersistentContext(progress: Progress, userDataDir: string, options: channels.BrowserTypeLaunchPersistentContextOptions & { cdpPort?: number, internalIgnoreHTTPSErrors?: boolean, socksProxyPort?: number }): Promise { const launchOptions = this._validateLaunchOptions(options); - const controller = new ProgressController(metadata, this); - const browser = await controller.run(async progress => { - // Note: Any initial TLS requests will fail since we rely on the Page/Frames initialize which sets ignoreHTTPSErrors. - let clientCertificatesProxy: ClientCertificatesProxy | undefined; - if (options.clientCertificates?.length) { - clientCertificatesProxy = await progress.raceWithCleanup(ClientCertificatesProxy.create(options), proxy => proxy.close()); - launchOptions.proxyOverride = clientCertificatesProxy.proxySettings(); - options = { ...options }; - options.internalIgnoreHTTPSErrors = true; - } - const browser = await this._innerLaunchWithRetries(progress, launchOptions, options, helper.debugProtocolLogger(), userDataDir).catch(e => { throw this._rewriteStartupLog(e); }); - browser._defaultContext!._clientCertificatesProxy = clientCertificatesProxy; - return browser; - }, launchOptions.timeout); + // Note: Any initial TLS requests will fail since we rely on the Page/Frames initialize which sets ignoreHTTPSErrors. + let clientCertificatesProxy: ClientCertificatesProxy | undefined; + if (options.clientCertificates?.length) { + clientCertificatesProxy = await progress.raceWithCleanup(ClientCertificatesProxy.create(options), proxy => proxy.close()); + launchOptions.proxyOverride = clientCertificatesProxy.proxySettings(); + options = { ...options }; + options.internalIgnoreHTTPSErrors = true; + } + const browser = await this._innerLaunchWithRetries(progress, launchOptions, options, helper.debugProtocolLogger(), userDataDir).catch(e => { throw this._rewriteStartupLog(e); }); + browser._defaultContext!._clientCertificatesProxy = clientCertificatesProxy; return browser._defaultContext!; } @@ -281,7 +271,7 @@ export abstract class BrowserType extends SdkObject { await fs.promises.mkdir(options.tracesDir, { recursive: true }); } - async connectOverCDP(metadata: CallMetadata, endpointURL: string, options: { slowMo?: number, timeout?: number, headers?: types.HeadersArray }): Promise { + async connectOverCDP(progress: Progress, endpointURL: string, options: { slowMo?: number, timeout?: number, headers?: types.HeadersArray }): Promise { throw new Error('CDP connections are only supported by Chromium'); } diff --git a/packages/playwright-core/src/server/chromium/chromium.ts b/packages/playwright-core/src/server/chromium/chromium.ts index 8ae2cdc864629..fa1f5213a224e 100644 --- a/packages/playwright-core/src/server/chromium/chromium.ts +++ b/packages/playwright-core/src/server/chromium/chromium.ts @@ -37,11 +37,10 @@ import { CRDevTools } from './crDevTools'; import { Browser } from '../browser'; import { removeFolders } from '../utils/fileUtils'; import { gracefullyCloseSet } from '../utils/processLauncher'; -import { ProgressController } from '../progress'; import type { HTTPRequestParams } from '../utils/network'; import type { BrowserOptions, BrowserProcess } from '../browser'; -import type { CallMetadata, SdkObject } from '../instrumentation'; +import type { SdkObject } from '../instrumentation'; import type { Env } from '../utils/processLauncher'; import type { Progress } from '../progress'; import type { ProtocolError } from '../protocolError'; @@ -62,11 +61,8 @@ export class Chromium extends BrowserType { this._devtools = this._createDevTools(); } - override async connectOverCDP(metadata: CallMetadata, endpointURL: string, options: { slowMo?: number, headers?: types.HeadersArray, timeout: number }) { - const controller = new ProgressController(metadata, this); - return controller.run(async progress => { - return await this._connectOverCDPInternal(progress, endpointURL, options); - }, options.timeout); + override async connectOverCDP(progress: Progress, endpointURL: string, options: { slowMo?: number, headers?: types.HeadersArray }) { + return await this._connectOverCDPInternal(progress, endpointURL, options); } async _connectOverCDPInternal(progress: Progress, endpointURL: string, options: types.LaunchOptions & { headers?: types.HeadersArray }, onClose?: () => Promise) { @@ -107,7 +103,7 @@ export class Chromium extends BrowserType { artifactsDir, downloadsPath: options.downloadsPath || artifactsDir, tracesDir: options.tracesDir || artifactsDir, - originalLaunchOptions: { timeout: options.timeout }, + originalLaunchOptions: {}, }; validateBrowserContextOptions(persistent, browserOptions); const browser = await progress.race(CRBrowser.connect(this.attribution.playwright, chromeTransport, browserOptions)); diff --git a/packages/playwright-core/src/server/debugController.ts b/packages/playwright-core/src/server/debugController.ts index 14cd66b2fc7e9..e1f92b13983bf 100644 --- a/packages/playwright-core/src/server/debugController.ts +++ b/packages/playwright-core/src/server/debugController.ts @@ -14,15 +14,14 @@ * limitations under the License. */ -import { SdkObject, createInstrumentation, serverSideCallMetadata } from './instrumentation'; +import { SdkObject, createInstrumentation } from './instrumentation'; import { gracefullyProcessExitDoNotHang } from './utils/processLauncher'; import { Recorder } from './recorder'; -import { asLocator, DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT, DEFAULT_PLAYWRIGHT_TIMEOUT } from '../utils'; +import { asLocator } from '../utils'; import { parseAriaSnapshotUnsafe } from '../utils/isomorphic/ariaSnapshot'; import { yaml } from '../utilsBundle'; import { EmptyRecorderApp } from './recorder/recorderApp'; import { unsafeLocatorOrSelectorAsSelector } from '../utils/isomorphic/locatorParser'; -import { ProgressController } from './progress'; import type { Language } from '../utils'; import type { Browser } from './browser'; @@ -30,8 +29,7 @@ import type { BrowserContext } from './browserContext'; import type { InstrumentationListener } from './instrumentation'; import type { Playwright } from './playwright'; import type { ElementInfo, Mode, Source } from '@recorder/recorderTypes'; - -const internalMetadata = serverSideCallMetadata(); +import type { Progress } from '@protocol/progress'; export class DebugController extends SdkObject { static Events = { @@ -75,27 +73,25 @@ export class DebugController extends SdkObject { } } - async resetForReuse() { + async resetForReuse(progress: Progress) { const contexts = new Set(); for (const page of this._playwright.allPages()) contexts.add(page.browserContext); for (const context of contexts) - await context.resetForReuse(internalMetadata, null); + await context.resetForReuse(progress, null); } - async navigate(url: string) { - for (const p of this._playwright.allPages()) { - const controller = new ProgressController(internalMetadata, this); - await controller.run(progress => p.mainFrame().goto(progress, url), DEFAULT_PLAYWRIGHT_TIMEOUT); - } + async navigate(progress: Progress, url: string) { + for (const p of this._playwright.allPages()) + await p.mainFrame().goto(progress, url); } - async setRecorderMode(params: { mode: Mode, file?: string, testIdAttributeName?: string }) { + async setRecorderMode(progress: Progress, params: { mode: Mode, file?: string, testIdAttributeName?: string }) { // TODO: |file| is only used in the legacy mode. - await this._closeBrowsersWithoutPages(); + await progress.race(this._closeBrowsersWithoutPages()); if (params.mode === 'none') { - for (const recorder of await this._allRecorders()) { + for (const recorder of await progress.race(this._allRecorders())) { recorder.hideHighlightedSelector(); recorder.setMode('none'); } @@ -103,14 +99,13 @@ export class DebugController extends SdkObject { } if (!this._playwright.allBrowsers().length) - await this._playwright.chromium.launch(internalMetadata, { headless: !!process.env.PW_DEBUG_CONTROLLER_HEADLESS, timeout: DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT }); + await this._playwright.chromium.launch(progress, { headless: !!process.env.PW_DEBUG_CONTROLLER_HEADLESS }); // Create page if none. const pages = this._playwright.allPages(); if (!pages.length) { const [browser] = this._playwright.allBrowsers(); - const { context } = await browser.newContextForReuse({}, internalMetadata); - const controller = new ProgressController(internalMetadata, this); - await controller.run(progress => context.newPage(progress, false /* isServerSide */)); + const { context } = await browser.newContextForReuse(progress, {}); + await context.newPage(progress, false /* isServerSide */); } // Update test id attribute. if (params.testIdAttributeName) { @@ -118,7 +113,7 @@ export class DebugController extends SdkObject { page.browserContext.selectors().setTestIdAttributeName(params.testIdAttributeName); } // Toggle the mode. - for (const recorder of await this._allRecorders()) { + for (const recorder of await progress.race(this._allRecorders())) { recorder.hideHighlightedSelector(); if (params.mode !== 'inspecting') recorder.setOutput(this._codegenId, params.file); @@ -126,12 +121,12 @@ export class DebugController extends SdkObject { } } - async highlight(params: { selector?: string, ariaTemplate?: string }) { + async highlight(progress: Progress, params: { selector?: string, ariaTemplate?: string }) { // Assert parameters validity. if (params.selector) unsafeLocatorOrSelectorAsSelector(this._sdkLanguage, params.selector, 'data-testid'); const ariaTemplate = params.ariaTemplate ? parseAriaSnapshotUnsafe(yaml, params.ariaTemplate) : undefined; - for (const recorder of await this._allRecorders()) { + for (const recorder of await progress.race(this._allRecorders())) { if (ariaTemplate) recorder.setHighlightedAriaTemplate(ariaTemplate); else if (params.selector) @@ -139,9 +134,9 @@ export class DebugController extends SdkObject { } } - async hideHighlight() { + async hideHighlight(progress: Progress) { // Hide all active recorder highlights. - for (const recorder of await this._allRecorders()) + for (const recorder of await progress.race(this._allRecorders())) recorder.hideHighlightedSelector(); // Hide all locator.highlight highlights. await this._playwright.hideHighlight(); @@ -151,12 +146,12 @@ export class DebugController extends SdkObject { return [...this._playwright.allBrowsers()]; } - async resume() { - for (const recorder of await this._allRecorders()) + async resume(progress: Progress) { + for (const recorder of await progress.race(this._allRecorders())) recorder.resume(); } - async kill() { + kill() { gracefullyProcessExitDoNotHang(0); } diff --git a/packages/playwright-core/src/server/dispatchers/androidDispatcher.ts b/packages/playwright-core/src/server/dispatchers/androidDispatcher.ts index 6972d50f2d21c..246c1e6d68808 100644 --- a/packages/playwright-core/src/server/dispatchers/androidDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/androidDispatcher.ts @@ -22,8 +22,8 @@ import { SdkObject } from '../instrumentation'; import type { RootDispatcher } from './dispatcher'; import type { Android, SocketBackend } from '../android/android'; -import type { CallMetadata } from '../instrumentation'; import type * as channels from '@protocol/channels'; +import type { Progress } from '@protocol/progress'; export class AndroidDispatcher extends Dispatcher implements channels.AndroidChannel { _type_Android = true; @@ -33,8 +33,8 @@ export class AndroidDispatcher extends Dispatcher { - const devices = await this._object.devices(params); + async devices(params: channels.AndroidDevicesParams, progress: Progress): Promise { + const devices = await this._object.devices(progress, params); return { devices: devices.map(d => AndroidDeviceDispatcher.from(this, d)) }; @@ -59,57 +59,57 @@ export class AndroidDeviceDispatcher extends Dispatcher this._dispatchEvent('webViewAdded', { webView })); this.addObjectListener(AndroidDevice.Events.WebViewRemoved, socketName => this._dispatchEvent('webViewRemoved', { socketName })); - this.addObjectListener(AndroidDevice.Events.Close, socketName => this._dispatchEvent('close')); + this.addObjectListener(AndroidDevice.Events.Close, () => this._dispatchEvent('close')); } - async wait(params: channels.AndroidDeviceWaitParams) { - await this._object.send('wait', params); + async wait(params: channels.AndroidDeviceWaitParams, progress: Progress) { + await progress.race(this._object.send('wait', params)); } - async fill(params: channels.AndroidDeviceFillParams) { - await this._object.send('click', { selector: params.androidSelector }); - await this._object.send('fill', params); + async fill(params: channels.AndroidDeviceFillParams, progress: Progress) { + await progress.race(this._object.send('click', { selector: params.androidSelector })); + await progress.race(this._object.send('fill', params)); } - async tap(params: channels.AndroidDeviceTapParams) { - await this._object.send('click', params); + async tap(params: channels.AndroidDeviceTapParams, progress: Progress) { + await progress.race(this._object.send('click', params)); } - async drag(params: channels.AndroidDeviceDragParams) { - await this._object.send('drag', params); + async drag(params: channels.AndroidDeviceDragParams, progress: Progress) { + await progress.race(this._object.send('drag', params)); } - async fling(params: channels.AndroidDeviceFlingParams) { - await this._object.send('fling', params); + async fling(params: channels.AndroidDeviceFlingParams, progress: Progress) { + await progress.race(this._object.send('fling', params)); } - async longTap(params: channels.AndroidDeviceLongTapParams) { - await this._object.send('longClick', params); + async longTap(params: channels.AndroidDeviceLongTapParams, progress: Progress) { + await progress.race(this._object.send('longClick', params)); } - async pinchClose(params: channels.AndroidDevicePinchCloseParams) { - await this._object.send('pinchClose', params); + async pinchClose(params: channels.AndroidDevicePinchCloseParams, progress: Progress) { + await progress.race(this._object.send('pinchClose', params)); } - async pinchOpen(params: channels.AndroidDevicePinchOpenParams) { - await this._object.send('pinchOpen', params); + async pinchOpen(params: channels.AndroidDevicePinchOpenParams, progress: Progress) { + await progress.race(this._object.send('pinchOpen', params)); } - async scroll(params: channels.AndroidDeviceScrollParams) { - await this._object.send('scroll', params); + async scroll(params: channels.AndroidDeviceScrollParams, progress: Progress) { + await progress.race(this._object.send('scroll', params)); } - async swipe(params: channels.AndroidDeviceSwipeParams) { - await this._object.send('swipe', params); + async swipe(params: channels.AndroidDeviceSwipeParams, progress: Progress) { + await progress.race(this._object.send('swipe', params)); } - async info(params: channels.AndroidDeviceTapParams): Promise { - const info = await this._object.send('info', params); + async info(params: channels.AndroidDeviceTapParams, progress: Progress): Promise { + const info = await progress.race(this._object.send('info', params)); fixupAndroidElementInfo(info); return { info }; } - async inputType(params: channels.AndroidDeviceInputTypeParams) { + async inputType(params: channels.AndroidDeviceInputTypeParams, progress: Progress) { const text = params.text; const keyCodes: number[] = []; for (let i = 0; i < text.length; ++i) { @@ -118,63 +118,63 @@ export class AndroidDeviceDispatcher extends Dispatcher this._object.send('inputPress', { keyCode }))); + await progress.race(Promise.all(keyCodes.map(keyCode => this._object.send('inputPress', { keyCode })))); } - async inputPress(params: channels.AndroidDeviceInputPressParams) { + async inputPress(params: channels.AndroidDeviceInputPressParams, progress: Progress) { if (!keyMap.has(params.key)) throw new Error('Unknown key: ' + params.key); - await this._object.send('inputPress', { keyCode: keyMap.get(params.key) }); + await progress.race(this._object.send('inputPress', { keyCode: keyMap.get(params.key) })); } - async inputTap(params: channels.AndroidDeviceInputTapParams) { - await this._object.send('inputClick', params); + async inputTap(params: channels.AndroidDeviceInputTapParams, progress: Progress) { + await progress.race(this._object.send('inputClick', params)); } - async inputSwipe(params: channels.AndroidDeviceInputSwipeParams) { - await this._object.send('inputSwipe', params); + async inputSwipe(params: channels.AndroidDeviceInputSwipeParams, progress: Progress) { + await progress.race(this._object.send('inputSwipe', params)); } - async inputDrag(params: channels.AndroidDeviceInputDragParams) { - await this._object.send('inputDrag', params); + async inputDrag(params: channels.AndroidDeviceInputDragParams, progress: Progress) { + await progress.race(this._object.send('inputDrag', params)); } - async screenshot(params: channels.AndroidDeviceScreenshotParams): Promise { - return { binary: await this._object.screenshot() }; + async screenshot(params: channels.AndroidDeviceScreenshotParams, progress: Progress): Promise { + return { binary: await progress.race(this._object.screenshot()) }; } - async shell(params: channels.AndroidDeviceShellParams): Promise { - return { result: await this._object.shell(params.command) }; + async shell(params: channels.AndroidDeviceShellParams, progress: Progress): Promise { + return { result: await progress.race(this._object.shell(params.command)) }; } - async open(params: channels.AndroidDeviceOpenParams, metadata: CallMetadata): Promise { - const socket = await this._object.open(params.command); + async open(params: channels.AndroidDeviceOpenParams, progress: Progress): Promise { + const socket = await this._object.open(progress, params.command); return { socket: new AndroidSocketDispatcher(this, new SocketSdkObject(this._object, socket)) }; } - async installApk(params: channels.AndroidDeviceInstallApkParams) { - await this._object.installApk(params.file, { args: params.args }); + async installApk(params: channels.AndroidDeviceInstallApkParams, progress: Progress) { + await this._object.installApk(progress, params.file, { args: params.args }); } - async push(params: channels.AndroidDevicePushParams) { - await this._object.push(params.file, params.path, params.mode); + async push(params: channels.AndroidDevicePushParams, progress: Progress) { + await progress.race(this._object.push(progress, params.file, params.path, params.mode)); } - async launchBrowser(params: channels.AndroidDeviceLaunchBrowserParams, metadata: CallMetadata): Promise { + async launchBrowser(params: channels.AndroidDeviceLaunchBrowserParams, progress: Progress): Promise { if (this.parentScope()._denyLaunch) throw new Error(`Launching more browsers is not allowed.`); - const context = await this._object.launchBrowser(metadata, params.pkg, params); + const context = await this._object.launchBrowser(progress, params.pkg, params); return { context: BrowserContextDispatcher.from(this, context) }; } - async close(params: channels.AndroidDeviceCloseParams) { + async close(params: channels.AndroidDeviceCloseParams, progress: Progress) { await this._object.close(); } - async connectToWebView(params: channels.AndroidDeviceConnectToWebViewParams, metadata: CallMetadata): Promise { + async connectToWebView(params: channels.AndroidDeviceConnectToWebViewParams, progress: Progress): Promise { if (this.parentScope()._denyLaunch) throw new Error(`Launching more browsers is not allowed.`); - return { context: BrowserContextDispatcher.from(this, await this._object.connectToWebView(metadata, params.socketName)) }; + return { context: BrowserContextDispatcher.from(this, await this._object.connectToWebView(progress, params.socketName)) }; } } @@ -215,11 +215,11 @@ export class AndroidSocketDispatcher extends Dispatcher { - await this._object.write(params.data); + async write(params: channels.AndroidSocketWriteParams, progress: Progress): Promise { + await progress.race(this._object.write(params.data)); } - async close(params: channels.AndroidSocketCloseParams, metadata: CallMetadata): Promise { + async close(params: channels.AndroidSocketCloseParams, progress: Progress): Promise { this._object.close(); } } diff --git a/packages/playwright-core/src/server/dispatchers/artifactDispatcher.ts b/packages/playwright-core/src/server/dispatchers/artifactDispatcher.ts index 478754a961d7b..556b0ae7ec408 100644 --- a/packages/playwright-core/src/server/dispatchers/artifactDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/artifactDispatcher.ts @@ -22,8 +22,8 @@ import { mkdirIfNeeded } from '../utils/fileUtils'; import type { DispatcherScope } from './dispatcher'; import type { Artifact } from '../artifact'; -import type { CallMetadata } from '../instrumentation'; import type * as channels from '@protocol/channels'; +import type { Progress } from '@protocol/progress'; export class ArtifactDispatcher extends Dispatcher implements channels.ArtifactChannel { _type_Artifact = true; @@ -45,13 +45,13 @@ export class ArtifactDispatcher extends Dispatcher { - const path = await this._object.localPathAfterFinished(); + async pathAfterFinished(params: channels.ArtifactPathAfterFinishedParams, progress: Progress): Promise { + const path = await progress.race(this._object.localPathAfterFinished()); return { value: path }; } - async saveAs(params: channels.ArtifactSaveAsParams): Promise { - return await new Promise((resolve, reject) => { + async saveAs(params: channels.ArtifactSaveAsParams, progress: Progress): Promise { + return await progress.race(new Promise((resolve, reject) => { this._object.saveAs(async (localPath, error) => { if (error) { reject(error); @@ -65,11 +65,11 @@ export class ArtifactDispatcher extends Dispatcher { - return await new Promise((resolve, reject) => { + async saveAsStream(params: channels.ArtifactSaveAsStreamParams, progress: Progress): Promise { + return await progress.race(new Promise((resolve, reject) => { this._object.saveAs(async (localPath, error) => { if (error) { reject(error); @@ -90,27 +90,27 @@ export class ArtifactDispatcher extends Dispatcher { - const fileName = await this._object.localPathAfterFinished(); + async stream(params: channels.ArtifactStreamParams, progress: Progress): Promise { + const fileName = await progress.race(this._object.localPathAfterFinished()); const readable = fs.createReadStream(fileName, { highWaterMark: 1024 * 1024 }); return { stream: new StreamDispatcher(this, readable) }; } - async failure(): Promise { - const error = await this._object.failureError(); + async failure(params: channels.ArtifactFailureParams, progress: Progress): Promise { + const error = await progress.race(this._object.failureError()); return { error: error || undefined }; } - async cancel(): Promise { - await this._object.cancel(); + async cancel(params: channels.ArtifactCancelParams, progress: Progress): Promise { + await progress.race(this._object.cancel()); } - async delete(_: any, metadata: CallMetadata): Promise { - metadata.potentiallyClosesScope = true; - await this._object.delete(); + async delete(params: channels.ArtifactDeleteParams, progress: Progress): Promise { + progress.metadata.potentiallyClosesScope = true; + await progress.race(this._object.delete()); this._dispose(); } } diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index 860d83852be15..87cf0941f0c1a 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -323,7 +323,7 @@ export class BrowserContextDispatcher extends Dispatcher { - return await progress.race(this._context.storageState(params.indexedDB)); + return await progress.race(this._context.storageState(progress, params.indexedDB)); } async close(params: channels.BrowserContextCloseParams, progress: Progress): Promise { diff --git a/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts index 448440069a939..f4f9062eb5cfc 100644 --- a/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts @@ -24,8 +24,8 @@ import { ArtifactDispatcher } from './artifactDispatcher'; import type { BrowserTypeDispatcher } from './browserTypeDispatcher'; import type { PageDispatcher } from './pageDispatcher'; import type { CRBrowser } from '../chromium/crBrowser'; -import type { CallMetadata } from '../instrumentation'; import type * as channels from '@protocol/channels'; +import type { Progress } from '@protocol/progress'; type BrowserDispatcherOptions = { // Do not allow to close this browser. @@ -58,16 +58,16 @@ export class BrowserDispatcher extends Dispatcher { + async newContext(params: channels.BrowserNewContextParams, progress: Progress): Promise { if (!this._options.isolateContexts) { - const context = await this._object.newContextFromMetadata(metadata, params); + const context = await this._object.newContext(progress, params); const contextDispatcher = BrowserContextDispatcher.from(this, context); return { context: contextDispatcher }; } if (params.recordVideo) params.recordVideo.dir = this._object.options.artifactsDir; - const context = await this._object.newContextFromMetadata(metadata, params); + const context = await this._object.newContext(progress, params); this._isolatedContexts.add(context); context.on(BrowserContext.Events.Close, () => this._isolatedContexts.delete(context)); const contextDispatcher = BrowserContextDispatcher.from(this, context); @@ -75,34 +75,34 @@ export class BrowserDispatcher extends Dispatcher { - const { context, needsReset } = await this._object.newContextForReuse(params, metadata); + async newContextForReuse(params: channels.BrowserNewContextForReuseParams, progress: Progress): Promise { + const { context, needsReset } = await this._object.newContextForReuse(progress, params); if (needsReset) { const oldContextDispatcher = this.connection.existingDispatcher(context); if (oldContextDispatcher) oldContextDispatcher._dispose(); - await context.resetForReuse(metadata, params); + await context.resetForReuse(progress, params); } const contextDispatcher = BrowserContextDispatcher.from(this, context); this._dispatchEvent('context', { context: contextDispatcher }); return { context: contextDispatcher }; } - async stopPendingOperations(params: channels.BrowserStopPendingOperationsParams, metadata: CallMetadata): Promise { + async stopPendingOperations(params: channels.BrowserStopPendingOperationsParams, progress: Progress): Promise { await this._object.stopPendingOperations(params.reason); } - async close(params: channels.BrowserCloseParams, metadata: CallMetadata): Promise { + async close(params: channels.BrowserCloseParams, progress: Progress): Promise { if (this._options.ignoreStopAndKill) return; - metadata.potentiallyClosesScope = true; + progress.metadata.potentiallyClosesScope = true; await this._object.close(params); } - async killForTests(_: any, metadata: CallMetadata): Promise { + async killForTests(params: channels.BrowserKillForTestsParams, progress: Progress): Promise { if (this._options.ignoreStopAndKill) return; - metadata.potentiallyClosesScope = true; + progress.metadata.potentiallyClosesScope = true; await this._object.killForTests(); } @@ -110,21 +110,24 @@ export class BrowserDispatcher extends Dispatcher { + async newBrowserCDPSession(params: channels.BrowserNewBrowserCDPSessionParams, progress: Progress): Promise { + // Note: progress is ignored because this operation is not cancellable and should not block in the browser anyway. if (!this._object.options.isChromium) throw new Error(`CDP session is only available in Chromium`); const crBrowser = this._object as CRBrowser; return { session: new CDPSessionDispatcher(this, await crBrowser.newBrowserCDPSession()) }; } - async startTracing(params: channels.BrowserStartTracingParams): Promise { + async startTracing(params: channels.BrowserStartTracingParams, progress: Progress): Promise { + // Note: progress is ignored because this operation is not cancellable and should not block in the browser anyway. if (!this._object.options.isChromium) throw new Error(`Tracing is only available in Chromium`); const crBrowser = this._object as CRBrowser; await crBrowser.startTracing(params.page ? (params.page as PageDispatcher)._object : undefined, params); } - async stopTracing(): Promise { + async stopTracing(params: channels.BrowserStopTracingParams, progress: Progress): Promise { + // Note: progress is ignored because this operation is not cancellable and should not block in the browser anyway. if (!this._object.options.isChromium) throw new Error(`Tracing is only available in Chromium`); const crBrowser = this._object as CRBrowser; diff --git a/packages/playwright-core/src/server/dispatchers/browserTypeDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserTypeDispatcher.ts index 3b71e04072ac7..80d92b7e821d0 100644 --- a/packages/playwright-core/src/server/dispatchers/browserTypeDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserTypeDispatcher.ts @@ -20,8 +20,8 @@ import { Dispatcher } from './dispatcher'; import type { BrowserType } from '../browserType'; import type { RootDispatcher } from './dispatcher'; -import type { CallMetadata } from '../instrumentation'; import type * as channels from '@protocol/channels'; +import type { Progress } from '@protocol/progress'; export class BrowserTypeDispatcher extends Dispatcher implements channels.BrowserTypeChannel { _type_BrowserType = true; @@ -34,29 +34,29 @@ export class BrowserTypeDispatcher extends Dispatcher { + async launch(params: channels.BrowserTypeLaunchParams, progress: Progress): Promise { if (this._denyLaunch) throw new Error(`Launching more browsers is not allowed.`); - const browser = await this._object.launch(metadata, params); + const browser = await this._object.launch(progress, params); return { browser: new BrowserDispatcher(this, browser) }; } - async launchPersistentContext(params: channels.BrowserTypeLaunchPersistentContextParams, metadata: CallMetadata): Promise { + async launchPersistentContext(params: channels.BrowserTypeLaunchPersistentContextParams, progress: Progress): Promise { if (this._denyLaunch) throw new Error(`Launching more browsers is not allowed.`); - const browserContext = await this._object.launchPersistentContext(metadata, params.userDataDir, params); + const browserContext = await this._object.launchPersistentContext(progress, params.userDataDir, params); const browserDispatcher = new BrowserDispatcher(this, browserContext._browser); const contextDispatcher = BrowserContextDispatcher.from(browserDispatcher, browserContext); return { browser: browserDispatcher, context: contextDispatcher }; } - async connectOverCDP(params: channels.BrowserTypeConnectOverCDPParams, metadata: CallMetadata): Promise { + async connectOverCDP(params: channels.BrowserTypeConnectOverCDPParams, progress: Progress): Promise { if (this._denyLaunch) throw new Error(`Launching more browsers is not allowed.`); - const browser = await this._object.connectOverCDP(metadata, params.endpointURL, params); + const browser = await this._object.connectOverCDP(progress, params.endpointURL, params); const browserDispatcher = new BrowserDispatcher(this, browser); return { browser: browserDispatcher, diff --git a/packages/playwright-core/src/server/dispatchers/debugControllerDispatcher.ts b/packages/playwright-core/src/server/dispatchers/debugControllerDispatcher.ts index 1636d9f77843c..cc55f9e28287a 100644 --- a/packages/playwright-core/src/server/dispatchers/debugControllerDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/debugControllerDispatcher.ts @@ -21,6 +21,7 @@ import { Dispatcher } from './dispatcher'; import type { DispatcherConnection, RootDispatcher } from './dispatcher'; import type { RegisteredListener } from '../utils/eventsHelper'; import type * as channels from '@protocol/channels'; +import type { Progress } from '@protocol/progress'; export class DebugControllerDispatcher extends Dispatcher implements channels.DebugControllerChannel { @@ -49,43 +50,43 @@ export class DebugControllerDispatcher extends Dispatcher { super(connection, createRootSdkObject(), 'Root', {}); } - async initialize(params: channels.RootInitializeParams): Promise { + async initialize(params: channels.RootInitializeParams, progress: Progress): Promise { + // Note: progress is deliberately ignored here. assert(this.createPlaywright); assert(!this._initialized); this._initialized = true; diff --git a/packages/playwright-core/src/server/dispatchers/electronDispatcher.ts b/packages/playwright-core/src/server/dispatchers/electronDispatcher.ts index 8ca84caadeca0..2c9cbcc9d2f93 100644 --- a/packages/playwright-core/src/server/dispatchers/electronDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/electronDispatcher.ts @@ -24,7 +24,7 @@ import type { PageDispatcher } from './pageDispatcher'; import type { ConsoleMessage } from '../console'; import type { Electron } from '../electron/electron'; import type * as channels from '@protocol/channels'; -import type { CallMetadata } from '@protocol/callMetadata'; +import type { Progress } from '@protocol/progress'; export class ElectronDispatcher extends Dispatcher implements channels.ElectronChannel { @@ -36,10 +36,10 @@ export class ElectronDispatcher extends Dispatcher { + async launch(params: channels.ElectronLaunchParams, progress: Progress): Promise { if (this._denyLaunch) throw new Error(`Launching more browsers is not allowed.`); - const electronApplication = await this._object.launch(metadata, params); + const electronApplication = await this._object.launch(progress, params); return { electronApplication: new ElectronApplicationDispatcher(this, electronApplication) }; } } @@ -69,30 +69,26 @@ export class ElectronApplicationDispatcher extends Dispatcher { - const handle = await this._object.browserWindow((params.page as PageDispatcher).page()); + async browserWindow(params: channels.ElectronApplicationBrowserWindowParams, progress: Progress): Promise { + const handle = await progress.race(this._object.browserWindow((params.page as PageDispatcher).page())); return { handle: JSHandleDispatcher.fromJSHandle(this, handle) }; } - async evaluateExpression(params: channels.ElectronApplicationEvaluateExpressionParams): Promise { - const handle = await this._object._nodeElectronHandlePromise; - return { value: serializeResult(await handle.evaluateExpression(params.expression, { isFunction: params.isFunction }, parseArgument(params.arg))) }; + async evaluateExpression(params: channels.ElectronApplicationEvaluateExpressionParams, progress: Progress): Promise { + const handle = await progress.race(this._object._nodeElectronHandlePromise); + return { value: serializeResult(await progress.race(handle.evaluateExpression(params.expression, { isFunction: params.isFunction }, parseArgument(params.arg)))) }; } - async evaluateExpressionHandle(params: channels.ElectronApplicationEvaluateExpressionHandleParams): Promise { - const handle = await this._object._nodeElectronHandlePromise; - const result = await handle.evaluateExpressionHandle(params.expression, { isFunction: params.isFunction }, parseArgument(params.arg)); + async evaluateExpressionHandle(params: channels.ElectronApplicationEvaluateExpressionHandleParams, progress: Progress): Promise { + const handle = await progress.race(this._object._nodeElectronHandlePromise); + const result = await progress.race(handle.evaluateExpressionHandle(params.expression, { isFunction: params.isFunction }, parseArgument(params.arg))); return { handle: JSHandleDispatcher.fromJSHandle(this, result) }; } - async updateSubscription(params: channels.ElectronApplicationUpdateSubscriptionParams): Promise { + async updateSubscription(params: channels.ElectronApplicationUpdateSubscriptionParams, progress: Progress): Promise { if (params.enabled) this._subscriptions.add(params.event); else this._subscriptions.delete(params.event); } - - async close(): Promise { - await this._object.close(); - } } diff --git a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts index 2b4f17f2d4eec..f919b63805a0b 100644 --- a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts @@ -266,7 +266,11 @@ export class FrameDispatcher extends Dispatcher { + if (result.received !== undefined) + result.received = serializeResult(result.received); + return result; + }); if (result.received !== undefined) result.received = serializeResult(result.received); return result; diff --git a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts index 850a339564323..94d0332b47a6e 100644 --- a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts @@ -20,14 +20,13 @@ import * as localUtils from '../localUtils'; import { getUserAgent } from '../utils/userAgent'; import { deviceDescriptors as descriptors } from '../deviceDescriptors'; import { JsonPipeDispatcher } from '../dispatchers/jsonPipeDispatcher'; -import { Progress, ProgressController } from '../progress'; +import { Progress } from '../progress'; import { SocksInterceptor } from '../socksInterceptor'; import { WebSocketTransport } from '../transport'; import { fetchData } from '../utils/network'; import { resolveGlobToRegexPattern } from '../../utils/isomorphic/urlMatch'; import type { HarBackend } from '../harBackend'; -import type { CallMetadata } from '../instrumentation'; import type { Playwright } from '../playwright'; import type { RootDispatcher } from './dispatcher'; import type * as channels from '@protocol/channels'; @@ -50,79 +49,76 @@ export class LocalUtilsDispatcher extends Dispatcher { - return await localUtils.zip(this._stackSessions, params); + async zip(params: channels.LocalUtilsZipParams, progress: Progress): Promise { + return await localUtils.zip(progress, this._stackSessions, params); } - async harOpen(params: channels.LocalUtilsHarOpenParams, metadata: CallMetadata): Promise { - return await localUtils.harOpen(this._harBackends, params); + async harOpen(params: channels.LocalUtilsHarOpenParams, progress: Progress): Promise { + return await localUtils.harOpen(progress, this._harBackends, params); } - async harLookup(params: channels.LocalUtilsHarLookupParams, metadata: CallMetadata): Promise { - return await localUtils.harLookup(this._harBackends, params); + async harLookup(params: channels.LocalUtilsHarLookupParams, progress: Progress): Promise { + return await localUtils.harLookup(progress, this._harBackends, params); } - async harClose(params: channels.LocalUtilsHarCloseParams, metadata: CallMetadata): Promise { - return await localUtils.harClose(this._harBackends, params); + async harClose(params: channels.LocalUtilsHarCloseParams, progress: Progress): Promise { + localUtils.harClose(this._harBackends, params); } - async harUnzip(params: channels.LocalUtilsHarUnzipParams, metadata: CallMetadata): Promise { - return await localUtils.harUnzip(params); + async harUnzip(params: channels.LocalUtilsHarUnzipParams, progress: Progress): Promise { + return await localUtils.harUnzip(progress, params); } - async tracingStarted(params: channels.LocalUtilsTracingStartedParams, metadata?: CallMetadata | undefined): Promise { - return await localUtils.tracingStarted(this._stackSessions, params); + async tracingStarted(params: channels.LocalUtilsTracingStartedParams, progress: Progress): Promise { + return await localUtils.tracingStarted(progress, this._stackSessions, params); } - async traceDiscarded(params: channels.LocalUtilsTraceDiscardedParams, metadata?: CallMetadata | undefined): Promise { - return await localUtils.traceDiscarded(this._stackSessions, params); + async traceDiscarded(params: channels.LocalUtilsTraceDiscardedParams, progress: Progress): Promise { + return await localUtils.traceDiscarded(progress, this._stackSessions, params); } - async addStackToTracingNoReply(params: channels.LocalUtilsAddStackToTracingNoReplyParams, metadata?: CallMetadata | undefined): Promise { - return await localUtils.addStackToTracingNoReply(this._stackSessions, params); + async addStackToTracingNoReply(params: channels.LocalUtilsAddStackToTracingNoReplyParams, progress: Progress): Promise { + localUtils.addStackToTracingNoReply(this._stackSessions, params); } - async connect(params: channels.LocalUtilsConnectParams, metadata: CallMetadata): Promise { - const controller = new ProgressController(metadata, this._object); - return await controller.run(async progress => { - const wsHeaders = { - 'User-Agent': getUserAgent(), - 'x-playwright-proxy': params.exposeNetwork ?? '', - ...params.headers, + async connect(params: channels.LocalUtilsConnectParams, progress: Progress): Promise { + const wsHeaders = { + 'User-Agent': getUserAgent(), + 'x-playwright-proxy': params.exposeNetwork ?? '', + ...params.headers, + }; + const wsEndpoint = await urlToWSEndpoint(progress, params.wsEndpoint); + + const transport = await WebSocketTransport.connect(progress, wsEndpoint, { headers: wsHeaders, followRedirects: true, debugLogHeader: 'x-playwright-debug-log' }); + const socksInterceptor = new SocksInterceptor(transport, params.exposeNetwork, params.socksProxyRedirectPortForTest); + const pipe = new JsonPipeDispatcher(this); + transport.onmessage = json => { + if (socksInterceptor.interceptMessage(json)) + return; + const cb = () => { + try { + pipe.dispatch(json); + } catch (e) { + transport.close(); + } }; - const wsEndpoint = await urlToWSEndpoint(progress, params.wsEndpoint); - - const transport = await WebSocketTransport.connect(progress, wsEndpoint, { headers: wsHeaders, followRedirects: true, debugLogHeader: 'x-playwright-debug-log' }); - const socksInterceptor = new SocksInterceptor(transport, params.exposeNetwork, params.socksProxyRedirectPortForTest); - const pipe = new JsonPipeDispatcher(this); - transport.onmessage = json => { - if (socksInterceptor.interceptMessage(json)) - return; - const cb = () => { - try { - pipe.dispatch(json); - } catch (e) { - transport.close(); - } - }; - if (params.slowMo) - setTimeout(cb, params.slowMo); - else - cb(); - }; - pipe.on('message', message => { - transport.send(message); - }); - transport.onclose = (reason?: string) => { - socksInterceptor?.cleanup(); - pipe.wasClosed(reason); - }; - pipe.on('close', () => transport.close()); - return { pipe, headers: transport.headers }; - }, params.timeout); + if (params.slowMo) + setTimeout(cb, params.slowMo); + else + cb(); + }; + pipe.on('message', message => { + transport.send(message); + }); + transport.onclose = (reason?: string) => { + socksInterceptor?.cleanup(); + pipe.wasClosed(reason); + }; + pipe.on('close', () => transport.close()); + return { pipe, headers: transport.headers }; } - async globToRegex(params: channels.LocalUtilsGlobToRegexParams, metadata?: CallMetadata): Promise { + async globToRegex(params: channels.LocalUtilsGlobToRegexParams, progress: Progress): Promise { const regex = resolveGlobToRegexPattern(params.baseURL, params.glob, params.webSocketUrl); return { regex }; } diff --git a/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts b/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts index 205ff2d71eca8..1e406f15a0a83 100644 --- a/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts +++ b/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts @@ -21,12 +21,12 @@ import { WorkerDispatcher } from './pageDispatcher'; import { TracingDispatcher } from './tracingDispatcher'; import type { APIRequestContext } from '../fetch'; -import type { CallMetadata } from '../instrumentation'; import type { Request, Response, Route } from '../network'; import type { BrowserContextDispatcher } from './browserContextDispatcher'; import type { RootDispatcher } from './dispatcher'; import type { PageDispatcher } from './pageDispatcher'; import type * as channels from '@protocol/channels'; +import type { Progress } from '@protocol/progress'; export class RequestDispatcher extends Dispatcher implements channels.RequestChannel { @@ -64,12 +64,12 @@ export class RequestDispatcher extends Dispatcher { - return { headers: await this._object.rawRequestHeaders() }; + async rawRequestHeaders(params: channels.RequestRawRequestHeadersParams, progress: Progress): Promise { + return { headers: await progress.race(this._object.rawRequestHeaders()) }; } - async response(): Promise { - return { response: ResponseDispatcher.fromNullable(this._browserContextDispatcher, await this._object.response()) }; + async response(params: channels.RequestResponseParams, progress: Progress): Promise { + return { response: ResponseDispatcher.fromNullable(this._browserContextDispatcher, await progress.race(this._object.response())) }; } } @@ -99,24 +99,24 @@ export class ResponseDispatcher extends Dispatcher { - return { binary: await this._object.body() }; + async body(params: channels.ResponseBodyParams, progress: Progress): Promise { + return { binary: await progress.race(this._object.body()) }; } - async securityDetails(): Promise { - return { value: await this._object.securityDetails() || undefined }; + async securityDetails(params: channels.ResponseSecurityDetailsParams, progress: Progress): Promise { + return { value: await progress.race(this._object.securityDetails()) || undefined }; } - async serverAddr(): Promise { - return { value: await this._object.serverAddr() || undefined }; + async serverAddr(params: channels.ResponseServerAddrParams, progress: Progress): Promise { + return { value: await progress.race(this._object.serverAddr()) || undefined }; } - async rawResponseHeaders(params?: channels.ResponseRawResponseHeadersParams): Promise { - return { headers: await this._object.rawResponseHeaders() }; + async rawResponseHeaders(params: channels.ResponseRawResponseHeadersParams, progress: Progress): Promise { + return { headers: await progress.race(this._object.rawResponseHeaders()) }; } - async sizes(params?: channels.ResponseSizesParams): Promise { - return { sizes: await this._object.sizes() }; + async sizes(params: channels.ResponseSizesParams, progress: Progress): Promise { + return { sizes: await progress.race(this._object.sizes()) }; } } @@ -138,7 +138,8 @@ export class RouteDispatcher extends Dispatcher { + async continue(params: channels.RouteContinueParams, progress: Progress): Promise { + // Note: progress is ignored because this operation is not cancellable and should not block in the browser anyway. this._checkNotHandled(); await this._object.continue({ url: params.url, @@ -149,19 +150,21 @@ export class RouteDispatcher extends Dispatcher { + async fulfill(params: channels.RouteFulfillParams, progress: Progress): Promise { + // Note: progress is ignored because this operation is not cancellable and should not block in the browser anyway. this._checkNotHandled(); await this._object.fulfill(params); } - async abort(params: channels.RouteAbortParams, metadata: CallMetadata): Promise { + async abort(params: channels.RouteAbortParams, progress: Progress): Promise { + // Note: progress is ignored because this operation is not cancellable and should not block in the browser anyway. this._checkNotHandled(); await this._object.abort(params.errorCode || 'failed'); } - async redirectNavigationRequest(params: channels.RouteRedirectNavigationRequestParams): Promise { + async redirectNavigationRequest(params: channels.RouteRedirectNavigationRequestParams, progress: Progress): Promise { this._checkNotHandled(); - await this._object.redirectNavigationRequest(params.url); + this._object.redirectNavigationRequest(params.url); } } @@ -203,18 +206,18 @@ export class APIRequestContextDispatcher extends Dispatcher { - return this._object.storageState(params.indexedDB); + async storageState(params: channels.APIRequestContextStorageStateParams, progress: Progress): Promise { + return await this._object.storageState(progress, params.indexedDB); } - async dispose(params: channels.APIRequestContextDisposeParams, metadata: CallMetadata): Promise { - metadata.potentiallyClosesScope = true; + async dispose(params: channels.APIRequestContextDisposeParams, progress: Progress): Promise { + progress.metadata.potentiallyClosesScope = true; await this._object.dispose(params); this._dispose(); } - async fetch(params: channels.APIRequestContextFetchParams, metadata: CallMetadata): Promise { - const fetchResponse = await this._object.fetch(params, metadata); + async fetch(params: channels.APIRequestContextFetchParams, progress: Progress): Promise { + const fetchResponse = await this._object.fetch(progress, params); return { response: { url: fetchResponse.url, @@ -226,16 +229,16 @@ export class APIRequestContextDispatcher extends Dispatcher { + async fetchResponseBody(params: channels.APIRequestContextFetchResponseBodyParams, progress: Progress): Promise { return { binary: this._object.fetchResponses.get(params.fetchUid) }; } - async fetchLog(params: channels.APIRequestContextFetchLogParams): Promise { + async fetchLog(params: channels.APIRequestContextFetchLogParams, progress: Progress): Promise { const log = this._object.fetchLog.get(params.fetchUid) || []; return { log }; } - async disposeAPIResponse(params: channels.APIRequestContextDisposeAPIResponseParams): Promise { + async disposeAPIResponse(params: channels.APIRequestContextDisposeAPIResponseParams, progress: Progress): Promise { this._object.disposeResponse(params.fetchUid); } } diff --git a/packages/playwright-core/src/server/electron/electron.ts b/packages/playwright-core/src/server/electron/electron.ts index b0c6e769ba18d..d88d61645c14e 100644 --- a/packages/playwright-core/src/server/electron/electron.ts +++ b/packages/playwright-core/src/server/electron/electron.ts @@ -30,10 +30,9 @@ import { createHandle, CRExecutionContext } from '../chromium/crExecutionContext import { toConsoleMessageLocation } from '../chromium/crProtocolHelper'; import { ConsoleMessage } from '../console'; import { helper } from '../helper'; -import { CallMetadata, SdkObject } from '../instrumentation'; +import { SdkObject } from '../instrumentation'; import * as js from '../javascript'; import { envArrayToObject, launchProcess } from '../utils/processLauncher'; -import { ProgressController } from '../progress'; import { WebSocketTransport } from '../transport'; import type { BrowserOptions, BrowserProcess } from '../browser'; @@ -155,141 +154,138 @@ export class Electron extends SdkObject { this.logName = 'browser'; } - async launch(metadata: CallMetadata, options: channels.ElectronLaunchParams): Promise { - const controller = new ProgressController(metadata, this); - return controller.run(async progress => { - let app: ElectronApplication | undefined = undefined; - // --remote-debugging-port=0 must be the last playwright's argument, loader.ts relies on it. - let electronArguments = ['--inspect=0', '--remote-debugging-port=0', ...(options.args || [])]; + async launch(progress: Progress, options: Omit): Promise { + let app: ElectronApplication | undefined = undefined; + // --remote-debugging-port=0 must be the last playwright's argument, loader.ts relies on it. + let electronArguments = ['--inspect=0', '--remote-debugging-port=0', ...(options.args || [])]; - if (os.platform() === 'linux') { - const runningAsRoot = process.geteuid && process.geteuid() === 0; - if (runningAsRoot && electronArguments.indexOf('--no-sandbox') === -1) - electronArguments.unshift('--no-sandbox'); - } + if (os.platform() === 'linux') { + const runningAsRoot = process.geteuid && process.geteuid() === 0; + if (runningAsRoot && electronArguments.indexOf('--no-sandbox') === -1) + electronArguments.unshift('--no-sandbox'); + } - const artifactsDir = await progress.race(fs.promises.mkdtemp(ARTIFACTS_FOLDER)); - progress.cleanupWhenAborted(() => removeFolders([artifactsDir])); + const artifactsDir = await progress.race(fs.promises.mkdtemp(ARTIFACTS_FOLDER)); + progress.cleanupWhenAborted(() => removeFolders([artifactsDir])); - const browserLogsCollector = new RecentLogsCollector(); - const env = options.env ? envArrayToObject(options.env) : process.env; + const browserLogsCollector = new RecentLogsCollector(); + const env = options.env ? envArrayToObject(options.env) : process.env; - let command: string; - if (options.executablePath) { - command = options.executablePath; - } else { - try { - // By default we fallback to the Electron App executable path. - // 'electron/index.js' resolves to the actual Electron App. - command = require('electron/index.js'); - } catch (error: any) { - if ((error as NodeJS.ErrnoException)?.code === 'MODULE_NOT_FOUND') { - throw new Error('\n' + wrapInASCIIBox([ - 'Electron executablePath not found!', - 'Please install it using `npm install -D electron` or set the executablePath to your Electron executable.', - ].join('\n'), 1)); - } - throw error; + let command: string; + if (options.executablePath) { + command = options.executablePath; + } else { + try { + // By default we fallback to the Electron App executable path. + // 'electron/index.js' resolves to the actual Electron App. + command = require('electron/index.js'); + } catch (error: any) { + if ((error as NodeJS.ErrnoException)?.code === 'MODULE_NOT_FOUND') { + throw new Error('\n' + wrapInASCIIBox([ + 'Electron executablePath not found!', + 'Please install it using `npm install -D electron` or set the executablePath to your Electron executable.', + ].join('\n'), 1)); } - // Only use our own loader for non-packaged apps. - // Packaged apps might have their own command line handling. - electronArguments.unshift('-r', require.resolve('./loader')); - } - let shell = false; - if (process.platform === 'win32') { - // On Windows in order to run .cmd files, shell: true is required. - // https://github.com/nodejs/node/issues/52554 - shell = true; - // On Windows, we need to quote the executable path due to shell: true. - command = `"${command}"`; - // On Windows, we need to quote the arguments due to shell: true. - electronArguments = electronArguments.map(arg => `"${arg}"`); + throw error; } + // Only use our own loader for non-packaged apps. + // Packaged apps might have their own command line handling. + electronArguments.unshift('-r', require.resolve('./loader')); + } + let shell = false; + if (process.platform === 'win32') { + // On Windows in order to run .cmd files, shell: true is required. + // https://github.com/nodejs/node/issues/52554 + shell = true; + // On Windows, we need to quote the executable path due to shell: true. + command = `"${command}"`; + // On Windows, we need to quote the arguments due to shell: true. + electronArguments = electronArguments.map(arg => `"${arg}"`); + } - // When debugging Playwright test that runs Electron, NODE_OPTIONS - // will make the debugger attach to Electron's Node. But Playwright - // also needs to attach to drive the automation. Disable external debugging. - delete env.NODE_OPTIONS; - const { launchedProcess, gracefullyClose, kill } = await launchProcess({ - command, - args: electronArguments, - env, - log: (message: string) => { - progress.log(message); - browserLogsCollector.log(message); - }, - shell, - stdio: 'pipe', - cwd: options.cwd, - tempDirectories: [artifactsDir], - attemptToGracefullyClose: () => app!.close(), - handleSIGINT: true, - handleSIGTERM: true, - handleSIGHUP: true, - onExit: () => app?.emit(ElectronApplication.Events.Close), - }); + // When debugging Playwright test that runs Electron, NODE_OPTIONS + // will make the debugger attach to Electron's Node. But Playwright + // also needs to attach to drive the automation. Disable external debugging. + delete env.NODE_OPTIONS; + const { launchedProcess, gracefullyClose, kill } = await launchProcess({ + command, + args: electronArguments, + env, + log: (message: string) => { + progress.log(message); + browserLogsCollector.log(message); + }, + shell, + stdio: 'pipe', + cwd: options.cwd, + tempDirectories: [artifactsDir], + attemptToGracefullyClose: () => app!.close(), + handleSIGINT: true, + handleSIGTERM: true, + handleSIGHUP: true, + onExit: () => app?.emit(ElectronApplication.Events.Close), + }); - // All waitForLines must be started immediately. - // Otherwise the lines might come before we are ready. - const waitForXserverError = waitForLine(progress, launchedProcess, /Unable to open X display/).then(() => { - throw new Error([ - 'Unable to open X display!', - `================================`, - 'Most likely this is because there is no X server available.', - "Use 'xvfb-run' on Linux to launch your tests with an emulated display server.", - "For example: 'xvfb-run npm run test:e2e'", - `================================`, - progress.metadata.log - ].join('\n')); - }); - const nodeMatchPromise = waitForLine(progress, launchedProcess, /^Debugger listening on (ws:\/\/.*)$/); - const chromeMatchPromise = waitForLine(progress, launchedProcess, /^DevTools listening on (ws:\/\/.*)$/); - const debuggerDisconnectPromise = waitForLine(progress, launchedProcess, /Waiting for the debugger to disconnect\.\.\./); + // All waitForLines must be started immediately. + // Otherwise the lines might come before we are ready. + const waitForXserverError = waitForLine(progress, launchedProcess, /Unable to open X display/).then(() => { + throw new Error([ + 'Unable to open X display!', + `================================`, + 'Most likely this is because there is no X server available.', + "Use 'xvfb-run' on Linux to launch your tests with an emulated display server.", + "For example: 'xvfb-run npm run test:e2e'", + `================================`, + progress.metadata.log + ].join('\n')); + }); + const nodeMatchPromise = waitForLine(progress, launchedProcess, /^Debugger listening on (ws:\/\/.*)$/); + const chromeMatchPromise = waitForLine(progress, launchedProcess, /^DevTools listening on (ws:\/\/.*)$/); + const debuggerDisconnectPromise = waitForLine(progress, launchedProcess, /Waiting for the debugger to disconnect\.\.\./); - const nodeMatch = await nodeMatchPromise; - const nodeTransport = await WebSocketTransport.connect(progress, nodeMatch[1]); - progress.cleanupWhenAborted(() => nodeTransport.close()); - const nodeConnection = new CRConnection(this, nodeTransport, helper.debugProtocolLogger(), browserLogsCollector); + const nodeMatch = await nodeMatchPromise; + const nodeTransport = await WebSocketTransport.connect(progress, nodeMatch[1]); + progress.cleanupWhenAborted(() => nodeTransport.close()); + const nodeConnection = new CRConnection(this, nodeTransport, helper.debugProtocolLogger(), browserLogsCollector); - // Immediately release exiting process under debug. - debuggerDisconnectPromise.then(() => { - nodeTransport.close(); - }).catch(() => {}); - const chromeMatch = await Promise.race([ - chromeMatchPromise, - waitForXserverError, - ]) as RegExpMatchArray; - const chromeTransport = await WebSocketTransport.connect(progress, chromeMatch[1]); - progress.cleanupWhenAborted(() => chromeTransport.close()); - const browserProcess: BrowserProcess = { - onclose: undefined, - process: launchedProcess, - close: gracefullyClose, - kill - }; - const contextOptions: types.BrowserContextOptions = { - ...options, - noDefaultViewport: true, - }; - const browserOptions: BrowserOptions = { - name: 'electron', - isChromium: true, - headful: true, - persistent: contextOptions, - browserProcess, - protocolLogger: helper.debugProtocolLogger(), - browserLogsCollector, - artifactsDir, - downloadsPath: artifactsDir, - tracesDir: options.tracesDir || artifactsDir, - originalLaunchOptions: { timeout: options.timeout }, - }; - validateBrowserContextOptions(contextOptions, browserOptions); - const browser = await progress.race(CRBrowser.connect(this.attribution.playwright, chromeTransport, browserOptions)); - app = new ElectronApplication(this, browser, nodeConnection, launchedProcess); - await progress.race(app.initialize()); - return app; - }, options.timeout); + // Immediately release exiting process under debug. + debuggerDisconnectPromise.then(() => { + nodeTransport.close(); + }).catch(() => {}); + const chromeMatch = await Promise.race([ + chromeMatchPromise, + waitForXserverError, + ]) as RegExpMatchArray; + const chromeTransport = await WebSocketTransport.connect(progress, chromeMatch[1]); + progress.cleanupWhenAborted(() => chromeTransport.close()); + const browserProcess: BrowserProcess = { + onclose: undefined, + process: launchedProcess, + close: gracefullyClose, + kill + }; + const contextOptions: types.BrowserContextOptions = { + ...options, + noDefaultViewport: true, + }; + const browserOptions: BrowserOptions = { + name: 'electron', + isChromium: true, + headful: true, + persistent: contextOptions, + browserProcess, + protocolLogger: helper.debugProtocolLogger(), + browserLogsCollector, + artifactsDir, + downloadsPath: artifactsDir, + tracesDir: options.tracesDir || artifactsDir, + originalLaunchOptions: {}, + }; + validateBrowserContextOptions(contextOptions, browserOptions); + const browser = await progress.race(CRBrowser.connect(this.attribution.playwright, chromeTransport, browserOptions)); + app = new ElectronApplication(this, browser, nodeConnection, launchedProcess); + await progress.race(app.initialize()); + return app; } } diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index 288d2d91b1af7..4fd6dcbe72e35 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -32,7 +32,6 @@ import { getMatchingTLSOptionsForOrigin, rewriteOpenSSLErrorIfNeeded } from './s import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent, timingForSocket } from './utils/happyEyeballs'; import { Tracing } from './trace/recorder/tracing'; -import type { CallMetadata } from './instrumentation'; import type { Playwright } from './playwright'; import type { Progress } from './progress'; import type * as types from './types'; @@ -137,7 +136,7 @@ export abstract class APIRequestContext extends SdkObject { abstract _defaultOptions(): FetchRequestOptions; abstract _addCookies(cookies: channels.NetworkCookie[]): Promise; abstract _cookies(url: URL): Promise; - abstract storageState(indexedDB?: boolean): Promise; + abstract storageState(progress: Progress, indexedDB?: boolean): Promise; private _storeResponseBody(body: Buffer): string { const uid = createGuid(); @@ -145,7 +144,7 @@ export abstract class APIRequestContext extends SdkObject { return uid; } - async fetch(params: channels.APIRequestContextFetchParams, metadata: CallMetadata): Promise { + async fetch(progress: Progress, params: channels.APIRequestContextFetchParams): Promise { const defaults = this._defaultOptions(); const headers: HeadersObject = { 'user-agent': defaults.userAgent, @@ -201,12 +200,9 @@ export abstract class APIRequestContext extends SdkObject { const postData = serializePostData(params, headers); if (postData) setHeader(headers, 'content-length', String(postData.byteLength)); - const controller = new ProgressController(metadata, this); - const fetchResponse = await controller.run(progress => { - return this._sendRequestWithRetries(progress, requestUrl, options, postData, params.maxRetries); - }, params.timeout); + const fetchResponse = await this._sendRequestWithRetries(progress, requestUrl, options, postData, params.maxRetries); const fetchUid = this._storeResponseBody(fetchResponse.body); - this.fetchLog.set(fetchUid, controller.metadata.log); + this.fetchLog.set(fetchUid, progress.metadata.log); const failOnStatusCode = params.failOnStatusCode !== undefined ? params.failOnStatusCode : !!defaults.failOnStatusCode; if (failOnStatusCode && (fetchResponse.status < 200 || fetchResponse.status >= 400)) { let responseText = ''; @@ -613,8 +609,8 @@ export class BrowserContextAPIRequestContext extends APIRequestContext { return await this._context.cookies(url.toString()); } - override async storageState(indexedDB?: boolean): Promise { - return this._context.storageState(indexedDB); + override async storageState(progress: Progress, indexedDB?: boolean): Promise { + return this._context.storageState(progress, indexedDB); } } @@ -670,7 +666,7 @@ export class GlobalAPIRequestContext extends APIRequestContext { return this._cookieStore.cookies(url); } - override async storageState(indexedDB = false): Promise { + override async storageState(progress: Progress, indexedDB = false): Promise { return { cookies: this._cookieStore.allCookies(), origins: (this._origins || []).map(origin => ({ ...origin, indexedDB: indexedDB ? origin.indexedDB : [] })), diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 86fd824eb795c..12e5bdeb7f601 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -90,6 +90,8 @@ export class NavigationAbortedError extends Error { } } +type ExpectResult = { matches: boolean, received?: any, log?: string[], timedOut?: boolean }; + const kDummyFrameId = ''; export class FrameManager { @@ -1336,6 +1338,7 @@ export class Frame extends SdkObject { async waitForTimeout(progress: Progress, timeout: number) { // Silent catch to avoid the self-inflicted timeout error. + progress.legacyDisableTimeout(); return progress.wait(timeout).catch(() => {}); } @@ -1343,17 +1346,35 @@ export class Frame extends SdkObject { return await this._retryWithProgressIfNotConnected(progress, selector, true /* strict */, true /* performActionPreChecks */, handle => progress.race(handle.ariaSnapshot(options))); } - async expect(progress: Progress, selector: string | undefined, options: FrameExpectParams, timeout?: number): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }> { + // TODO: remove errorResultHandler once legacy progress is removed. + async expect(progress: Progress, selector: string | undefined, options: FrameExpectParams, timeout?: number, errorResultHandler?: (result: ExpectResult) => ExpectResult): Promise { progress.log(`${renderTitleForCall(progress.metadata)}${timeout ? ` with timeout ${timeout}ms` : ''}`); - const result = await this._expectImpl(progress, selector, options); - // Library mode special case for the expect errors which are return values, not exceptions. - if (result.matches === options.isNot) - progress.metadata.error = { error: { name: 'Expect', message: 'Expect failed' } }; + const result = await this._expectImpl(progress, selector, options, errorResultHandler); return result; } - private async _expectImpl(progress: Progress, selector: string | undefined, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }> { + private async _expectImpl(progress: Progress, selector: string | undefined, options: FrameExpectParams, errorResultHandler?: (result: ExpectResult) => ExpectResult): Promise { const lastIntermediateResult: { received?: any, isSet: boolean } = { isSet: false }; + const fixupMetadataError = (result: ExpectResult) => { + // Library mode special case for the expect errors which are return values, not exceptions. + if (result.matches === options.isNot) + progress.metadata.error = { error: { name: 'Expect', message: 'Expect failed' } }; + }; + const handleError = (e: any, isProgressError: boolean) => { + // Q: Why not throw upon isNonRetriableError(e) as in other places? + // A: We want user to receive a friendly message containing the last intermediate result. + if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e)) + throw e; + const result: ExpectResult = { matches: options.isNot, log: compressCallLog(progress.metadata.log) }; + if (lastIntermediateResult.isSet) + result.received = lastIntermediateResult.received; + if (e instanceof TimeoutError) + result.timedOut = true; + fixupMetadataError(result); + return (isProgressError && errorResultHandler) ? errorResultHandler(result) : result; + }; + progress.legacySetErrorHandler(e => handleError(e, true)); + try { // Step 1: perform locator handlers checkpoint with a specified timeout. if (selector) @@ -1376,7 +1397,7 @@ export class Frame extends SdkObject { progress.legacyEnableTimeout(); // Step 3: auto-retry expect with increasing timeouts. Bounded by the total remaining time. - return await this.retryWithProgressAndTimeouts(progress, [100, 250, 500, 1000], async continuePolling => { + const result = await this.retryWithProgressAndTimeouts(progress, [100, 250, 500, 1000], async continuePolling => { await this._page.performActionPreChecks(progress); const { matches, received } = await this._expectInternal(progress, selector, options, lastIntermediateResult, false); if (matches === options.isNot) { @@ -1387,17 +1408,10 @@ export class Frame extends SdkObject { } return { matches, received }; }); - } catch (e) { - // Q: Why not throw upon isNonRetriableError(e) as in other places? - // A: We want user to receive a friendly message containing the last intermediate result. - if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e)) - throw e; - const result: { matches: boolean, received?: any, log?: string[], timedOut?: boolean } = { matches: options.isNot, log: compressCallLog(progress.metadata.log) }; - if (lastIntermediateResult.isSet) - result.received = lastIntermediateResult.received; - if (e instanceof TimeoutError) - result.timedOut = true; + fixupMetadataError(result); return result; + } catch (e) { + return handleError(e, false); } } diff --git a/packages/playwright-core/src/server/launchApp.ts b/packages/playwright-core/src/server/launchApp.ts index cdf0809b642fe..8eee438d84022 100644 --- a/packages/playwright-core/src/server/launchApp.ts +++ b/packages/playwright-core/src/server/launchApp.ts @@ -49,7 +49,8 @@ export async function launchApp(browserType: BrowserType, options: { channel = findChromiumChannel(options.sdkLanguage); } - const context = await browserType.launchPersistentContext(serverSideCallMetadata(), '', { + const controller = new ProgressController(serverSideCallMetadata(), browserType); + const context = await controller.run(progress => browserType.launchPersistentContext(progress, '', { ignoreDefaultArgs: ['--enable-automation'], ...options?.persistentContextOptions, channel, @@ -57,8 +58,7 @@ export async function launchApp(browserType: BrowserType, options: { acceptDownloads: options?.persistentContextOptions?.acceptDownloads ?? (isUnderTest() ? 'accept' : 'internal-browser-default'), colorScheme: options?.persistentContextOptions?.colorScheme ?? 'no-override', args, - timeout: 0, // Deliberately no timeout for our apps. - }); + }), 0); // Deliberately no timeout for our apps. const [page] = context.pages(); // Chromium on macOS opens a new tab when clicking on the dock icon. // See https://github.com/microsoft/playwright/issues/9434 diff --git a/packages/playwright-core/src/server/localUtils.ts b/packages/playwright-core/src/server/localUtils.ts index c16584b2785c6..2071260ceb2ba 100644 --- a/packages/playwright-core/src/server/localUtils.ts +++ b/packages/playwright-core/src/server/localUtils.ts @@ -30,6 +30,7 @@ import { removeFolders } from './utils/fileUtils'; import type * as channels from '@protocol/channels'; import type * as har from '@trace/har'; import type EventEmitter from 'events'; +import type { Progress } from '@protocol/progress'; export type StackSession = { @@ -39,7 +40,7 @@ export type StackSession = { callStacks: channels.ClientSideCallMetadata[]; }; -export async function zip(stackSessions: Map, params: channels.LocalUtilsZipParams): Promise { +export async function zip(progress: Progress, 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)); @@ -58,7 +59,7 @@ export async function zip(stackSessions: Map, params: chan // Add stacks and the sources. const stackSession = params.stacksId ? stackSessions.get(params.stacksId) : undefined; if (stackSession?.callStacks.length) { - await stackSession.writer; + await progress.race(stackSession.writer); if (process.env.PW_LIVE_TRACE_STACKS) { zipFile.addFile(stackSession.file, 'trace.stacks'); } else { @@ -82,20 +83,20 @@ export async function zip(stackSessions: Map, params: chan if (params.mode === 'write') { // New file, just compress the entries. - await fs.promises.mkdir(path.dirname(params.zipFile), { recursive: true }); + await progress.race(fs.promises.mkdir(path.dirname(params.zipFile), { recursive: true })); zipFile.end(undefined, () => { zipFile.outputStream.pipe(fs.createWriteStream(params.zipFile)) .on('close', () => promise.resolve()) .on('error', error => promise.reject(error)); }); - await promise; - await deleteStackSession(stackSessions, params.stacksId); + await progress.race(promise); + await deleteStackSession(progress, stackSessions, params.stacksId); return; } // File already exists. Repack and add new entries. const tempFile = params.zipFile + '.tmp'; - await fs.promises.rename(params.zipFile, tempFile); + await progress.race(fs.promises.rename(params.zipFile, tempFile)); yauzl.open(tempFile, (err, inZipFile) => { if (err) { @@ -123,21 +124,20 @@ export async function zip(stackSessions: Map, params: chan }); }); }); - await promise; - await deleteStackSession(stackSessions, params.stacksId); + await progress.race(promise); + await deleteStackSession(progress, stackSessions, params.stacksId); } -async function deleteStackSession(stackSessions: Map, stacksId?: string) { +async function deleteStackSession(progress: Progress, stackSessions: Map, stacksId?: string) { const session = stacksId ? stackSessions.get(stacksId) : undefined; if (!session) return; - await session.writer; - if (session.tmpDir) - await removeFolders([session.tmpDir]); stackSessions.delete(stacksId!); + if (session.tmpDir) + await progress.race(removeFolders([session.tmpDir])); } -export async function harOpen(harBackends: Map, params: channels.LocalUtilsHarOpenParams): Promise { +export async function harOpen(progress: Progress, harBackends: Map, params: channels.LocalUtilsHarOpenParams): Promise { let harBackend: HarBackend; if (params.file.endsWith('.zip')) { const zipFile = new ZipFile(params.file); @@ -145,25 +145,25 @@ export async function harOpen(harBackends: Map, params: chan const harEntryName = entryNames.find(e => e.endsWith('.har')); if (!harEntryName) return { error: 'Specified archive does not have a .har file' }; - const har = await zipFile.read(harEntryName); + const har = await progress.raceWithCleanup(zipFile.read(harEntryName), () => zipFile.close()); const harFile = JSON.parse(har.toString()) as har.HARFile; harBackend = new HarBackend(harFile, null, zipFile); } else { - const harFile = JSON.parse(await fs.promises.readFile(params.file, 'utf-8')) as har.HARFile; + const harFile = JSON.parse(await progress.race(fs.promises.readFile(params.file, 'utf-8'))) as har.HARFile; harBackend = new HarBackend(harFile, path.dirname(params.file), null); } harBackends.set(harBackend.id, harBackend); return { harId: harBackend.id }; } -export async function harLookup(harBackends: Map, params: channels.LocalUtilsHarLookupParams): Promise { +export async function harLookup(progress: Progress, harBackends: Map, params: channels.LocalUtilsHarLookupParams): Promise { const harBackend = harBackends.get(params.harId); if (!harBackend) return { action: 'error', message: `Internal error: har was not opened` }; - return await harBackend.lookup(params.url, params.method, params.headers, params.postData, params.isNavigationRequest); + return await progress.race(harBackend.lookup(params.url, params.method, params.headers, params.postData, params.isNavigationRequest)); } -export async function harClose(harBackends: Map, params: channels.LocalUtilsHarCloseParams): Promise { +export function harClose(harBackends: Map, params: channels.LocalUtilsHarCloseParams) { const harBackend = harBackends.get(params.harId); if (harBackend) { harBackends.delete(harBackend.id); @@ -171,34 +171,35 @@ export async function harClose(harBackends: Map, params: cha } } -export async function harUnzip(params: channels.LocalUtilsHarUnzipParams): Promise { +export async function harUnzip(progress: Progress, params: channels.LocalUtilsHarUnzipParams): Promise { const dir = path.dirname(params.zipFile); const zipFile = new ZipFile(params.zipFile); - for (const entry of await zipFile.entries()) { - const buffer = await zipFile.read(entry); + progress.cleanupWhenAborted(() => zipFile.close()); + for (const entry of await progress.race(zipFile.entries())) { + const buffer = await progress.race(zipFile.read(entry)); if (entry === 'har.har') - await fs.promises.writeFile(params.harFile, buffer); + await progress.race(fs.promises.writeFile(params.harFile, buffer)); else - await fs.promises.writeFile(path.join(dir, entry), buffer); + await progress.race(fs.promises.writeFile(path.join(dir, entry), buffer)); } + await progress.race(fs.promises.unlink(params.zipFile)); zipFile.close(); - await fs.promises.unlink(params.zipFile); } -export async function tracingStarted(stackSessions: Map, params: channels.LocalUtilsTracingStartedParams): Promise { +export async function tracingStarted(progress: Progress, stackSessions: Map, params: channels.LocalUtilsTracingStartedParams): Promise { let tmpDir = undefined; if (!params.tracesDir) - tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-tracing-')); + tmpDir = await progress.race(fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-tracing-'))); const traceStacksFile = path.join(params.tracesDir || tmpDir!, params.traceName + '.stacks'); stackSessions.set(traceStacksFile, { callStacks: [], file: traceStacksFile, writer: Promise.resolve(), tmpDir }); return { stacksId: traceStacksFile }; } -export async function traceDiscarded(stackSessions: Map, params: channels.LocalUtilsTraceDiscardedParams): Promise { - await deleteStackSession(stackSessions, params.stacksId); +export async function traceDiscarded(progress: Progress, stackSessions: Map, params: channels.LocalUtilsTraceDiscardedParams): Promise { + await deleteStackSession(progress, stackSessions, params.stacksId); } -export async function addStackToTracingNoReply(stackSessions: Map, params: channels.LocalUtilsAddStackToTracingNoReplyParams): Promise { +export function addStackToTracingNoReply(stackSessions: Map, params: channels.LocalUtilsAddStackToTracingNoReplyParams) { for (const session of stackSessions.values()) { session.callStacks.push(params.callData); if (process.env.PW_LIVE_TRACE_STACKS) { diff --git a/packages/playwright-core/src/server/network.ts b/packages/playwright-core/src/server/network.ts index 5536693c75ab7..6cd33962a6b86 100644 --- a/packages/playwright-core/src/server/network.ts +++ b/packages/playwright-core/src/server/network.ts @@ -290,7 +290,7 @@ export class Route extends SdkObject { this._endHandling(); } - async redirectNavigationRequest(url: string) { + redirectNavigationRequest(url: string) { this._startHandling(); assert(this._request.isNavigationRequest()); this._request.frame()!.redirectNavigation(url, this._request._documentId!, this._request.headerValue('referer')); diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index dd3dae03d14d2..c8c9b9ec293c1 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -634,6 +634,23 @@ export class Page extends SdkObject { intermediateResult = { errorMessage: comparatorResult.errorMessage, diff: comparatorResult.diff, actual, previous }; return false; }; + const handleError = (e: any) => { + // Q: Why not throw upon isNonRetriableError(e) as in other places? + // A: We want user to receive a friendly diff between actual and expected/previous. + if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e)) + throw e; + let errorMessage = e.message; + if (e instanceof TimeoutError && intermediateResult?.previous) + errorMessage = `Failed to take two consecutive stable screenshots.`; + return { + log: compressCallLog(e.message ? [...progress.metadata.log, e.message] : progress.metadata.log), + ...intermediateResult, + errorMessage, + timedOut: (e instanceof TimeoutError), + }; + }; + progress.legacySetErrorHandler(handleError); + try { let actual: Buffer | undefined; let previous: Buffer | undefined; @@ -685,19 +702,7 @@ export class Page extends SdkObject { } throw new Error(intermediateResult!.errorMessage); } catch (e) { - // Q: Why not throw upon isNonRetriableError(e) as in other places? - // A: We want user to receive a friendly diff between actual and expected/previous. - if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e)) - throw e; - let errorMessage = e.message; - if (e instanceof TimeoutError && intermediateResult?.previous) - errorMessage = `Failed to take two consecutive stable screenshots.`; - return { - log: compressCallLog(e.message ? [...progress.metadata.log, e.message] : progress.metadata.log), - ...intermediateResult, - errorMessage, - timedOut: (e instanceof TimeoutError), - }; + return handleError(e); } } diff --git a/packages/playwright-core/src/server/progress.ts b/packages/playwright-core/src/server/progress.ts index f427ff018249f..124099de8cf09 100644 --- a/packages/playwright-core/src/server/progress.ts +++ b/packages/playwright-core/src/server/progress.ts @@ -71,6 +71,7 @@ export class ProgressController { assert(this._state === 'before'); this._state = 'running'; this.sdkObject.attribution.context?._activeProgressControllers.add(this); + let customErrorHandler: ((error: Error) => any) | undefined; const deadline = timeout ? Math.min(monotonicTime() + timeout, 2147483647) : 0; // 2^31-1 safe setTimeout in Node. const timeoutError = new TimeoutError(`Timeout ${timeout}ms exceeded.`); @@ -118,7 +119,10 @@ export class ProgressController { }, raceWithCleanup: (promise: Promise, cleanup: (result: T) => any) => { return progress.race(promise.then(result => { - progress.cleanupWhenAborted(() => cleanup(result)); + if (this._state !== 'running') + cleanup(result); + else + this._cleanups.push(() => cleanup(result)); return result; })); }, @@ -137,6 +141,11 @@ export class ProgressController { return; startTimer(); }, + legacySetErrorHandler: (handler: (error: Error) => any) => { + if (this._strictMode) + return; + customErrorHandler = handler; + }, }; startTimer(); @@ -149,6 +158,8 @@ export class ProgressController { } catch (error) { this._state = { error }; await Promise.all(this._cleanups.splice(0).map(cleanup => runCleanup(error, cleanup))); + if (customErrorHandler) + return customErrorHandler(error); throw error; } finally { this.sdkObject.attribution.context?._activeProgressControllers.delete(this); diff --git a/packages/playwright-core/src/server/recorder/recorderApp.ts b/packages/playwright-core/src/server/recorder/recorderApp.ts index cc86c27de3315..0223f634fbb91 100644 --- a/packages/playwright-core/src/server/recorder/recorderApp.ts +++ b/packages/playwright-core/src/server/recorder/recorderApp.ts @@ -120,7 +120,6 @@ export class RecorderApp extends EventEmitter implements IRecorderApp { executablePath: inspectedContext._browser.options.isChromium ? inspectedContext._browser.options.customExecutablePath : undefined, // Use the same channel as the inspected context to guarantee that the browser is installed. channel: inspectedContext._browser.options.isChromium ? inspectedContext._browser.options.channel : undefined, - timeout: 0, } }); const controller = new ProgressController(serverSideCallMetadata(), context._browser); diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index 50e939d71b3c9..793cc8305f652 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -176,7 +176,6 @@ export async function openTraceViewerApp(url: string, browserName: string, optio cdpPort: isUnderTest() ? 0 : undefined, headless: !!options?.headless, colorScheme: isUnderTest() ? 'light' : undefined, - timeout: 0, }, }); diff --git a/packages/playwright-core/src/server/types.ts b/packages/playwright-core/src/server/types.ts index 80da3ddd2623a..59d2973311583 100644 --- a/packages/playwright-core/src/server/types.ts +++ b/packages/playwright-core/src/server/types.ts @@ -154,7 +154,7 @@ export type NormalizedContinueOverrides = { export type EmulatedSize = { viewport: Size, screen: Size }; -export type LaunchOptions = channels.BrowserTypeLaunchParams & { +export type LaunchOptions = Omit & { cdpPort?: number, proxyOverride?: ProxySettings, assistantMode?: boolean, diff --git a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts index a322b9c1de461..1f5865c39ec0d 100644 --- a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts +++ b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts @@ -311,20 +311,36 @@ export const methodMetainfo = new Map([ + 'APIRequestContext', + 'LocalUtils', + 'Root', 'Playwright', + 'DebugController', 'SocksSupport', + 'BrowserType', + 'Browser', 'BrowserContext', 'Page', 'Frame', 'Worker', 'JSHandle', 'ElementHandle', + 'Request', + 'Route', 'WebSocketRoute', + 'Response', + 'WebSocket', 'BindingCall', 'Dialog', 'Tracing', + 'Artifact', 'Stream', 'WritableStream', 'CDPSession', + 'Electron', + 'ElectronApplication', + 'Android', + 'AndroidSocket', + 'AndroidDevice', 'JsonPipe' ]); diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index 4a182fbdc72ee..e31e380ea986d 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -457,7 +457,6 @@ export async function runUIMode(configFile: string | undefined, configCLIOverrid persistentContextOptions: { handleSIGINT: false, channel, - timeout: 0, }, }); page.on('close', () => cancelPromise.resolve()); diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 6e2a4b16627db..75a076a8d4313 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -355,12 +355,12 @@ export interface APIRequestContextEventTarget { } export interface APIRequestContextChannel extends APIRequestContextEventTarget, Channel { _type_APIRequestContext: boolean; - fetch(params: APIRequestContextFetchParams, metadata?: CallMetadata): Promise; - fetchResponseBody(params: APIRequestContextFetchResponseBodyParams, metadata?: CallMetadata): Promise; - fetchLog(params: APIRequestContextFetchLogParams, metadata?: CallMetadata): Promise; - storageState(params: APIRequestContextStorageStateParams, metadata?: CallMetadata): Promise; - disposeAPIResponse(params: APIRequestContextDisposeAPIResponseParams, metadata?: CallMetadata): Promise; - dispose(params: APIRequestContextDisposeParams, metadata?: CallMetadata): Promise; + fetch(params: APIRequestContextFetchParams, progress?: Progress): Promise; + fetchResponseBody(params: APIRequestContextFetchResponseBodyParams, progress?: Progress): Promise; + fetchLog(params: APIRequestContextFetchLogParams, progress?: Progress): Promise; + storageState(params: APIRequestContextStorageStateParams, progress?: Progress): Promise; + disposeAPIResponse(params: APIRequestContextDisposeAPIResponseParams, progress?: Progress): Promise; + dispose(params: APIRequestContextDisposeParams, progress?: Progress): Promise; } export type APIRequestContextFetchParams = { url: string, @@ -475,16 +475,16 @@ export interface LocalUtilsEventTarget { } export interface LocalUtilsChannel extends LocalUtilsEventTarget, Channel { _type_LocalUtils: boolean; - zip(params: LocalUtilsZipParams, metadata?: CallMetadata): Promise; - harOpen(params: LocalUtilsHarOpenParams, metadata?: CallMetadata): Promise; - harLookup(params: LocalUtilsHarLookupParams, metadata?: CallMetadata): Promise; - harClose(params: LocalUtilsHarCloseParams, metadata?: CallMetadata): Promise; - harUnzip(params: LocalUtilsHarUnzipParams, metadata?: CallMetadata): Promise; - connect(params: LocalUtilsConnectParams, metadata?: CallMetadata): Promise; - tracingStarted(params: LocalUtilsTracingStartedParams, metadata?: CallMetadata): Promise; - addStackToTracingNoReply(params: LocalUtilsAddStackToTracingNoReplyParams, metadata?: CallMetadata): Promise; - traceDiscarded(params: LocalUtilsTraceDiscardedParams, metadata?: CallMetadata): Promise; - globToRegex(params: LocalUtilsGlobToRegexParams, metadata?: CallMetadata): Promise; + zip(params: LocalUtilsZipParams, progress?: Progress): Promise; + harOpen(params: LocalUtilsHarOpenParams, progress?: Progress): Promise; + harLookup(params: LocalUtilsHarLookupParams, progress?: Progress): Promise; + harClose(params: LocalUtilsHarCloseParams, progress?: Progress): Promise; + harUnzip(params: LocalUtilsHarUnzipParams, progress?: Progress): Promise; + connect(params: LocalUtilsConnectParams, progress?: Progress): Promise; + tracingStarted(params: LocalUtilsTracingStartedParams, progress?: Progress): Promise; + addStackToTracingNoReply(params: LocalUtilsAddStackToTracingNoReplyParams, progress?: Progress): Promise; + traceDiscarded(params: LocalUtilsTraceDiscardedParams, progress?: Progress): Promise; + globToRegex(params: LocalUtilsGlobToRegexParams, progress?: Progress): Promise; } export type LocalUtilsZipParams = { zipFile: string, @@ -605,7 +605,7 @@ export interface RootEventTarget { } export interface RootChannel extends RootEventTarget, Channel { _type_Root: boolean; - initialize(params: RootInitializeParams, metadata?: CallMetadata): Promise; + initialize(params: RootInitializeParams, progress?: Progress): Promise; } export type RootInitializeParams = { sdkLanguage: 'javascript' | 'python' | 'java' | 'csharp', @@ -736,16 +736,16 @@ export interface DebugControllerEventTarget { } export interface DebugControllerChannel extends DebugControllerEventTarget, Channel { _type_DebugController: boolean; - initialize(params: DebugControllerInitializeParams, metadata?: CallMetadata): Promise; - setReportStateChanged(params: DebugControllerSetReportStateChangedParams, metadata?: CallMetadata): Promise; - resetForReuse(params?: DebugControllerResetForReuseParams, metadata?: CallMetadata): Promise; - navigate(params: DebugControllerNavigateParams, metadata?: CallMetadata): Promise; - setRecorderMode(params: DebugControllerSetRecorderModeParams, metadata?: CallMetadata): Promise; - highlight(params: DebugControllerHighlightParams, metadata?: CallMetadata): Promise; - hideHighlight(params?: DebugControllerHideHighlightParams, metadata?: CallMetadata): Promise; - resume(params?: DebugControllerResumeParams, metadata?: CallMetadata): Promise; - kill(params?: DebugControllerKillParams, metadata?: CallMetadata): Promise; - closeAllBrowsers(params?: DebugControllerCloseAllBrowsersParams, metadata?: CallMetadata): Promise; + initialize(params: DebugControllerInitializeParams, progress?: Progress): Promise; + setReportStateChanged(params: DebugControllerSetReportStateChangedParams, progress?: Progress): Promise; + resetForReuse(params?: DebugControllerResetForReuseParams, progress?: Progress): Promise; + navigate(params: DebugControllerNavigateParams, progress?: Progress): Promise; + setRecorderMode(params: DebugControllerSetRecorderModeParams, progress?: Progress): Promise; + highlight(params: DebugControllerHighlightParams, progress?: Progress): Promise; + hideHighlight(params?: DebugControllerHideHighlightParams, progress?: Progress): Promise; + resume(params?: DebugControllerResumeParams, progress?: Progress): Promise; + kill(params?: DebugControllerKillParams, progress?: Progress): Promise; + closeAllBrowsers(params?: DebugControllerCloseAllBrowsersParams, progress?: Progress): Promise; } export type DebugControllerInspectRequestedEvent = { selector: string, @@ -913,9 +913,9 @@ export interface BrowserTypeEventTarget { } export interface BrowserTypeChannel extends BrowserTypeEventTarget, Channel { _type_BrowserType: boolean; - launch(params: BrowserTypeLaunchParams, metadata?: CallMetadata): Promise; - launchPersistentContext(params: BrowserTypeLaunchPersistentContextParams, metadata?: CallMetadata): Promise; - connectOverCDP(params: BrowserTypeConnectOverCDPParams, metadata?: CallMetadata): Promise; + launch(params: BrowserTypeLaunchParams, progress?: Progress): Promise; + launchPersistentContext(params: BrowserTypeLaunchPersistentContextParams, progress?: Progress): Promise; + connectOverCDP(params: BrowserTypeConnectOverCDPParams, progress?: Progress): Promise; } export type BrowserTypeLaunchParams = { channel?: string, @@ -1172,15 +1172,15 @@ export interface BrowserEventTarget { } export interface BrowserChannel extends BrowserEventTarget, Channel { _type_Browser: boolean; - close(params: BrowserCloseParams, metadata?: CallMetadata): Promise; - killForTests(params?: BrowserKillForTestsParams, metadata?: CallMetadata): Promise; - defaultUserAgentForTest(params?: BrowserDefaultUserAgentForTestParams, metadata?: CallMetadata): Promise; - newContext(params: BrowserNewContextParams, metadata?: CallMetadata): Promise; - newContextForReuse(params: BrowserNewContextForReuseParams, metadata?: CallMetadata): Promise; - stopPendingOperations(params: BrowserStopPendingOperationsParams, metadata?: CallMetadata): Promise; - newBrowserCDPSession(params?: BrowserNewBrowserCDPSessionParams, metadata?: CallMetadata): Promise; - startTracing(params: BrowserStartTracingParams, metadata?: CallMetadata): Promise; - stopTracing(params?: BrowserStopTracingParams, metadata?: CallMetadata): Promise; + close(params: BrowserCloseParams, progress?: Progress): Promise; + killForTests(params?: BrowserKillForTestsParams, progress?: Progress): Promise; + defaultUserAgentForTest(params?: BrowserDefaultUserAgentForTestParams, progress?: Progress): Promise; + newContext(params: BrowserNewContextParams, progress?: Progress): Promise; + newContextForReuse(params: BrowserNewContextForReuseParams, progress?: Progress): Promise; + stopPendingOperations(params: BrowserStopPendingOperationsParams, progress?: Progress): Promise; + newBrowserCDPSession(params?: BrowserNewBrowserCDPSessionParams, progress?: Progress): Promise; + startTracing(params: BrowserStartTracingParams, progress?: Progress): Promise; + stopTracing(params?: BrowserStopTracingParams, progress?: Progress): Promise; } export type BrowserContextEvent = { context: BrowserContextChannel, @@ -3820,8 +3820,8 @@ export interface RequestEventTarget { } export interface RequestChannel extends RequestEventTarget, Channel { _type_Request: boolean; - response(params?: RequestResponseParams, metadata?: CallMetadata): Promise; - rawRequestHeaders(params?: RequestRawRequestHeadersParams, metadata?: CallMetadata): Promise; + response(params?: RequestResponseParams, progress?: Progress): Promise; + rawRequestHeaders(params?: RequestRawRequestHeadersParams, progress?: Progress): Promise; } export type RequestResponseParams = {}; export type RequestResponseOptions = {}; @@ -3845,10 +3845,10 @@ export interface RouteEventTarget { } export interface RouteChannel extends RouteEventTarget, Channel { _type_Route: boolean; - redirectNavigationRequest(params: RouteRedirectNavigationRequestParams, metadata?: CallMetadata): Promise; - abort(params: RouteAbortParams, metadata?: CallMetadata): Promise; - continue(params: RouteContinueParams, metadata?: CallMetadata): Promise; - fulfill(params: RouteFulfillParams, metadata?: CallMetadata): Promise; + redirectNavigationRequest(params: RouteRedirectNavigationRequestParams, progress?: Progress): Promise; + abort(params: RouteAbortParams, progress?: Progress): Promise; + continue(params: RouteContinueParams, progress?: Progress): Promise; + fulfill(params: RouteFulfillParams, progress?: Progress): Promise; } export type RouteRedirectNavigationRequestParams = { url: string, @@ -4009,11 +4009,11 @@ export interface ResponseEventTarget { } export interface ResponseChannel extends ResponseEventTarget, Channel { _type_Response: boolean; - body(params?: ResponseBodyParams, metadata?: CallMetadata): Promise; - securityDetails(params?: ResponseSecurityDetailsParams, metadata?: CallMetadata): Promise; - serverAddr(params?: ResponseServerAddrParams, metadata?: CallMetadata): Promise; - rawResponseHeaders(params?: ResponseRawResponseHeadersParams, metadata?: CallMetadata): Promise; - sizes(params?: ResponseSizesParams, metadata?: CallMetadata): Promise; + body(params?: ResponseBodyParams, progress?: Progress): Promise; + securityDetails(params?: ResponseSecurityDetailsParams, progress?: Progress): Promise; + serverAddr(params?: ResponseServerAddrParams, progress?: Progress): Promise; + rawResponseHeaders(params?: ResponseRawResponseHeadersParams, progress?: Progress): Promise; + sizes(params?: ResponseSizesParams, progress?: Progress): Promise; } export type ResponseBodyParams = {}; export type ResponseBodyOptions = {}; @@ -4241,13 +4241,13 @@ export interface ArtifactEventTarget { } export interface ArtifactChannel extends ArtifactEventTarget, Channel { _type_Artifact: boolean; - pathAfterFinished(params?: ArtifactPathAfterFinishedParams, metadata?: CallMetadata): Promise; - saveAs(params: ArtifactSaveAsParams, metadata?: CallMetadata): Promise; - saveAsStream(params?: ArtifactSaveAsStreamParams, metadata?: CallMetadata): Promise; - failure(params?: ArtifactFailureParams, metadata?: CallMetadata): Promise; - stream(params?: ArtifactStreamParams, metadata?: CallMetadata): Promise; - cancel(params?: ArtifactCancelParams, metadata?: CallMetadata): Promise; - delete(params?: ArtifactDeleteParams, metadata?: CallMetadata): Promise; + pathAfterFinished(params?: ArtifactPathAfterFinishedParams, progress?: Progress): Promise; + saveAs(params: ArtifactSaveAsParams, progress?: Progress): Promise; + saveAsStream(params?: ArtifactSaveAsStreamParams, progress?: Progress): Promise; + failure(params?: ArtifactFailureParams, progress?: Progress): Promise; + stream(params?: ArtifactStreamParams, progress?: Progress): Promise; + cancel(params?: ArtifactCancelParams, progress?: Progress): Promise; + delete(params?: ArtifactDeleteParams, progress?: Progress): Promise; } export type ArtifactPathAfterFinishedParams = {}; export type ArtifactPathAfterFinishedOptions = {}; @@ -4372,7 +4372,7 @@ export interface ElectronEventTarget { } export interface ElectronChannel extends ElectronEventTarget, Channel { _type_Electron: boolean; - launch(params: ElectronLaunchParams, metadata?: CallMetadata): Promise; + launch(params: ElectronLaunchParams, progress?: Progress): Promise; } export type ElectronLaunchParams = { executablePath?: string, @@ -4462,10 +4462,10 @@ export interface ElectronApplicationEventTarget { } export interface ElectronApplicationChannel extends ElectronApplicationEventTarget, EventTargetChannel { _type_ElectronApplication: boolean; - browserWindow(params: ElectronApplicationBrowserWindowParams, metadata?: CallMetadata): Promise; - evaluateExpression(params: ElectronApplicationEvaluateExpressionParams, metadata?: CallMetadata): Promise; - evaluateExpressionHandle(params: ElectronApplicationEvaluateExpressionHandleParams, metadata?: CallMetadata): Promise; - updateSubscription(params: ElectronApplicationUpdateSubscriptionParams, metadata?: CallMetadata): Promise; + browserWindow(params: ElectronApplicationBrowserWindowParams, progress?: Progress): Promise; + evaluateExpression(params: ElectronApplicationEvaluateExpressionParams, progress?: Progress): Promise; + evaluateExpressionHandle(params: ElectronApplicationEvaluateExpressionHandleParams, progress?: Progress): Promise; + updateSubscription(params: ElectronApplicationUpdateSubscriptionParams, progress?: Progress): Promise; } export type ElectronApplicationCloseEvent = {}; export type ElectronApplicationConsoleEvent = { @@ -4529,7 +4529,7 @@ export interface AndroidEventTarget { } export interface AndroidChannel extends AndroidEventTarget, Channel { _type_Android: boolean; - devices(params: AndroidDevicesParams, metadata?: CallMetadata): Promise; + devices(params: AndroidDevicesParams, progress?: Progress): Promise; } export type AndroidDevicesParams = { host?: string, @@ -4556,8 +4556,8 @@ export interface AndroidSocketEventTarget { } export interface AndroidSocketChannel extends AndroidSocketEventTarget, Channel { _type_AndroidSocket: boolean; - write(params: AndroidSocketWriteParams, metadata?: CallMetadata): Promise; - close(params?: AndroidSocketCloseParams, metadata?: CallMetadata): Promise; + write(params: AndroidSocketWriteParams, progress?: Progress): Promise; + close(params?: AndroidSocketCloseParams, progress?: Progress): Promise; } export type AndroidSocketDataEvent = { data: Binary, @@ -4591,30 +4591,30 @@ export interface AndroidDeviceEventTarget { } export interface AndroidDeviceChannel extends AndroidDeviceEventTarget, EventTargetChannel { _type_AndroidDevice: boolean; - wait(params: AndroidDeviceWaitParams, metadata?: CallMetadata): Promise; - fill(params: AndroidDeviceFillParams, metadata?: CallMetadata): Promise; - tap(params: AndroidDeviceTapParams, metadata?: CallMetadata): Promise; - drag(params: AndroidDeviceDragParams, metadata?: CallMetadata): Promise; - fling(params: AndroidDeviceFlingParams, metadata?: CallMetadata): Promise; - longTap(params: AndroidDeviceLongTapParams, metadata?: CallMetadata): Promise; - pinchClose(params: AndroidDevicePinchCloseParams, metadata?: CallMetadata): Promise; - pinchOpen(params: AndroidDevicePinchOpenParams, metadata?: CallMetadata): Promise; - scroll(params: AndroidDeviceScrollParams, metadata?: CallMetadata): Promise; - swipe(params: AndroidDeviceSwipeParams, metadata?: CallMetadata): Promise; - info(params: AndroidDeviceInfoParams, metadata?: CallMetadata): Promise; - screenshot(params?: AndroidDeviceScreenshotParams, metadata?: CallMetadata): Promise; - inputType(params: AndroidDeviceInputTypeParams, metadata?: CallMetadata): Promise; - inputPress(params: AndroidDeviceInputPressParams, metadata?: CallMetadata): Promise; - inputTap(params: AndroidDeviceInputTapParams, metadata?: CallMetadata): Promise; - inputSwipe(params: AndroidDeviceInputSwipeParams, metadata?: CallMetadata): Promise; - inputDrag(params: AndroidDeviceInputDragParams, metadata?: CallMetadata): Promise; - launchBrowser(params: AndroidDeviceLaunchBrowserParams, metadata?: CallMetadata): Promise; - open(params: AndroidDeviceOpenParams, metadata?: CallMetadata): Promise; - shell(params: AndroidDeviceShellParams, metadata?: CallMetadata): Promise; - installApk(params: AndroidDeviceInstallApkParams, metadata?: CallMetadata): Promise; - push(params: AndroidDevicePushParams, metadata?: CallMetadata): Promise; - connectToWebView(params: AndroidDeviceConnectToWebViewParams, metadata?: CallMetadata): Promise; - close(params?: AndroidDeviceCloseParams, metadata?: CallMetadata): Promise; + wait(params: AndroidDeviceWaitParams, progress?: Progress): Promise; + fill(params: AndroidDeviceFillParams, progress?: Progress): Promise; + tap(params: AndroidDeviceTapParams, progress?: Progress): Promise; + drag(params: AndroidDeviceDragParams, progress?: Progress): Promise; + fling(params: AndroidDeviceFlingParams, progress?: Progress): Promise; + longTap(params: AndroidDeviceLongTapParams, progress?: Progress): Promise; + pinchClose(params: AndroidDevicePinchCloseParams, progress?: Progress): Promise; + pinchOpen(params: AndroidDevicePinchOpenParams, progress?: Progress): Promise; + scroll(params: AndroidDeviceScrollParams, progress?: Progress): Promise; + swipe(params: AndroidDeviceSwipeParams, progress?: Progress): Promise; + info(params: AndroidDeviceInfoParams, progress?: Progress): Promise; + screenshot(params?: AndroidDeviceScreenshotParams, progress?: Progress): Promise; + inputType(params: AndroidDeviceInputTypeParams, progress?: Progress): Promise; + inputPress(params: AndroidDeviceInputPressParams, progress?: Progress): Promise; + inputTap(params: AndroidDeviceInputTapParams, progress?: Progress): Promise; + inputSwipe(params: AndroidDeviceInputSwipeParams, progress?: Progress): Promise; + inputDrag(params: AndroidDeviceInputDragParams, progress?: Progress): Promise; + launchBrowser(params: AndroidDeviceLaunchBrowserParams, progress?: Progress): Promise; + open(params: AndroidDeviceOpenParams, progress?: Progress): Promise; + shell(params: AndroidDeviceShellParams, progress?: Progress): Promise; + installApk(params: AndroidDeviceInstallApkParams, progress?: Progress): Promise; + push(params: AndroidDevicePushParams, progress?: Progress): Promise; + connectToWebView(params: AndroidDeviceConnectToWebViewParams, progress?: Progress): Promise; + close(params?: AndroidDeviceCloseParams, progress?: Progress): Promise; } export type AndroidDeviceCloseEvent = {}; export type AndroidDeviceWebViewAddedEvent = { diff --git a/packages/protocol/src/progress.d.ts b/packages/protocol/src/progress.d.ts index 586b774cac4db..116da64593ce7 100644 --- a/packages/protocol/src/progress.d.ts +++ b/packages/protocol/src/progress.d.ts @@ -42,4 +42,5 @@ export interface Progress { // Legacy lenient mode api only. To be removed. legacyDisableTimeout(): void; legacyEnableTimeout(): void; + legacySetErrorHandler(handler: (error: Error) => void): void; } diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 851b15e7abe23..a27bcea5b6899 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -359,6 +359,7 @@ FormField: APIRequestContext: type: interface + progress: true initializer: tracing: Tracing @@ -629,6 +630,7 @@ ContextOptions: LocalUtils: type: interface + progress: true initializer: deviceDescriptors: @@ -768,6 +770,7 @@ LocalUtils: Root: type: interface + progress: true commands: @@ -878,6 +881,7 @@ RecorderSource: DebugController: type: interface + progress: true commands: initialize: @@ -1016,6 +1020,7 @@ SocksSupport: BrowserType: type: interface + progress: true initializer: executablePath: string @@ -1057,6 +1062,7 @@ BrowserType: Browser: type: interface + progress: true initializer: version: string @@ -3258,6 +3264,7 @@ ElementHandle: Request: type: interface + progress: true initializer: frame: Frame? @@ -3289,6 +3296,7 @@ Request: Route: type: interface + progress: true initializer: request: Request @@ -3408,6 +3416,7 @@ ResourceTiming: Response: type: interface + progress: true initializer: request: Request @@ -3478,6 +3487,7 @@ RemoteAddr: WebSocket: type: interface + progress: true extends: EventTarget @@ -3610,6 +3620,7 @@ Tracing: Artifact: type: interface + progress: true initializer: absolutePath: string @@ -3709,6 +3720,7 @@ CDPSession: Electron: type: interface + progress: true commands: @@ -3779,6 +3791,7 @@ Electron: ElectronApplication: type: interface + progress: true extends: EventTarget @@ -3829,6 +3842,7 @@ ElectronApplication: Android: type: interface + progress: true commands: @@ -3845,6 +3859,7 @@ Android: AndroidSocket: type: interface + progress: true commands: write: @@ -3863,6 +3878,7 @@ AndroidSocket: AndroidDevice: type: interface + progress: true extends: EventTarget