diff --git a/eslint.config.mjs b/eslint.config.mjs index 09c05422a9fc1..1ef61d5453c25 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -430,7 +430,7 @@ export default [ }, languageOptions: languageOptionsWithTsConfig, rules: { - "progress/await-must-use-progress": "warn", + "progress/await-must-use-progress": "error", }, }, { diff --git a/packages/playwright-core/src/server/android/android.ts b/packages/playwright-core/src/server/android/android.ts index 90d8ccf115c38..a503e4b6d3168 100644 --- a/packages/playwright-core/src/server/android/android.ts +++ b/packages/playwright-core/src/server/android/android.ts @@ -276,7 +276,7 @@ export class AndroidDevice extends SdkObject { 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}`); + await progress.race(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); @@ -394,21 +394,21 @@ export class AndroidDevice extends SdkObject { async push(progress: Progress, content: Buffer, path: string, mode = 0o644): Promise { const socket = await this._open(progress, `sync:`); - const sendHeader = async (command: string, length: number) => { + const sendHeader = async (progress: Progress, command: string, length: number) => { const buffer = Buffer.alloc(command.length + 4); buffer.write(command, 0); buffer.writeUInt32LE(length, command.length); await progress.race(socket.write(buffer)); }; - const send = async (command: string, data: Buffer) => { - await sendHeader(command, data.length); + const send = async (progress: Progress, command: string, data: Buffer) => { + await sendHeader(progress, command, data.length); await progress.race(socket.write(data)); }; - await send('SEND', Buffer.from(`${path},${mode}`)); + await send(progress, '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); + await send(progress, 'DATA', content.slice(i, i + maxChunk)); + await sendHeader(progress, 'DONE', (Date.now() / 1000) | 0); const result = await progress.race(new Promise(f => socket.once('data', f))); const code = result.slice(0, 4).toString(); if (code !== 'OKAY') diff --git a/packages/playwright-core/src/server/bidi/bidiPage.ts b/packages/playwright-core/src/server/bidi/bidiPage.ts index 67e3e3b4b20c9..0050576f5593a 100644 --- a/packages/playwright-core/src/server/bidi/bidiPage.ts +++ b/packages/playwright-core/src/server/bidi/bidiPage.ts @@ -601,11 +601,11 @@ export class BidiPage implements PageDelegate { async setInputFilePaths(progress: Progress, handle: dom.ElementHandle, paths: string[]): Promise { const fromContext = toBidiExecutionContext(handle._context); - await this._session.send('input.setFiles', { + await progress.race(this._session.send('input.setFiles', { context: this._session.sessionId, - element: await fromContext.nodeIdForElementHandle(handle), + element: await progress.race(fromContext.nodeIdForElementHandle(handle)), files: paths, - }); + })); } async adoptElementHandle(handle: dom.ElementHandle, to: dom.FrameExecutionContext): Promise> { diff --git a/packages/playwright-core/src/server/browser.ts b/packages/playwright-core/src/server/browser.ts index 5ce1d28f78c6b..f310d16c79d2a 100644 --- a/packages/playwright-core/src/server/browser.ts +++ b/packages/playwright-core/src/server/browser.ts @@ -194,7 +194,7 @@ export abstract class Browser extends SdkObject { } async killForTests(progress: Progress) { - await this.options.browserProcess.kill(); + await progress.race(this.options.browserProcess.kill()); } } diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 39d7382f9fe42..7dd7f88165dbe 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -436,7 +436,7 @@ export abstract class BrowserContext extends Sdk if (!this.possiblyUninitializedPages().length) { const waitForEvent = helper.waitForEvent(progress, this, BrowserContext.Events.Page); // Race against BrowserContext.close - await Promise.race([waitForEvent.promise, this._closePromise]); + await progress.race(Promise.race([waitForEvent.promise, this._closePromise])); } const page = this.possiblyUninitializedPages()[0]; if (!page) @@ -509,7 +509,7 @@ export abstract class BrowserContext extends Sdk async addRequestInterceptor(progress: Progress, handler: network.RouteHandler): Promise { // Note: progress is intentionally ignored, because this operation is not cancellable and should not block in the browser anyway. this.requestInterceptors.push(handler); - await this.doUpdateRequestInterception(); + await progress.race(this.doUpdateRequestInterception()); } async removeRequestInterceptor(handler: network.RouteHandler): Promise { @@ -545,15 +545,15 @@ export abstract class BrowserContext extends Sdk this._closedStatus = 'closing'; for (const harRecorder of this._harRecorders.values()) - await harRecorder.flush(); - await this.tracing.flush(); - await Promise.all(this.pages().map(page => page.screencast.handlePageOrContextClose())); + await progress.race(harRecorder.flush()); + await progress.race(this.tracing.flush()); + await progress.race(Promise.all(this.pages().map(page => page.screencast.handlePageOrContextClose()))); if (this._customCloseHandler) { - await this._customCloseHandler(); + await progress.race(this._customCloseHandler()); } else { // Close the context. - const disposition = await this.doClose(options.reason); + const disposition = await progress.race(this.doClose(options.reason)); if (disposition === 'close-browser') await this._browser.close(progress, { reason: options.reason }); } @@ -563,7 +563,7 @@ export abstract class BrowserContext extends Sdk const promises: Promise[] = []; promises.push(this._deleteAllDownloads()); promises.push(this._deleteAllTempDirs()); - await Promise.all(promises); + await progress.race(Promise.all(promises)); // Custom handler should trigger didCloseInternal itself. if (!this._customCloseHandler) @@ -616,7 +616,7 @@ export abstract class BrowserContext extends Sdk if (!origin || !originsToSave.has(origin)) continue; try { - const storage: SerializedStorage = await page.mainFrame().nonStallingEvaluateInExistingContext(collectScript, 'utility'); + const storage: SerializedStorage = await progress.race(page.mainFrame().nonStallingEvaluateInExistingContext(collectScript, 'utility')); if (storage.localStorage.length || storage.indexedDB?.length) result.origins.push({ origin, localStorage: storage.localStorage, indexedDB: storage.indexedDB }); originsToSave.delete(origin); diff --git a/packages/playwright-core/src/server/browserType.ts b/packages/playwright-core/src/server/browserType.ts index 96e8901bedb58..cddebdb2c8624 100644 --- a/packages/playwright-core/src/server/browserType.ts +++ b/packages/playwright-core/src/server/browserType.ts @@ -141,7 +141,7 @@ export abstract class BrowserType extends SdkObject { await browser._defaultContext!.loadDefaultContext(progress); return browser; } catch (error) { - await browserProcess.close().catch(() => {}); + await progress.race(browserProcess.close().catch(() => {})); throw error; } } @@ -212,7 +212,7 @@ export abstract class BrowserType extends SdkObject { let transport: ConnectionTransport | undefined = undefined; let browserProcess: BrowserProcess | undefined = undefined; const exitPromise = new ManualPromise(); - const { launchedProcess, gracefullyClose, kill } = await launchProcess({ + const { launchedProcess, gracefullyClose, kill } = await progress.race(launchProcess({ command: prepared.executable, args: prepared.browserArguments, env: this.amendEnvironment(env, prepared.userDataDir, isPersistent, options), @@ -241,7 +241,7 @@ export abstract class BrowserType extends SdkObject { if (browserProcess && browserProcess.onclose) browserProcess.onclose(exitCode, signal); }, - }); + })); async function closeOrKill(timeout: number): Promise { let timer: NodeJS.Timeout; @@ -280,7 +280,7 @@ export abstract class BrowserType extends SdkObject { } return { browserProcess, artifactsDir: prepared.artifactsDir, userDataDir: prepared.userDataDir, transport }; } catch (error) { - await closeOrKill(DEFAULT_PLAYWRIGHT_TIMEOUT).catch(() => {}); + await progress.race(closeOrKill(DEFAULT_PLAYWRIGHT_TIMEOUT).catch(() => {})); throw error; } } diff --git a/packages/playwright-core/src/server/chromium/chromium.ts b/packages/playwright-core/src/server/chromium/chromium.ts index 058c9a1af3def..efd94490a081d 100644 --- a/packages/playwright-core/src/server/chromium/chromium.ts +++ b/packages/playwright-core/src/server/chromium/chromium.ts @@ -132,7 +132,7 @@ export class Chromium extends BrowserType { browser.on(Browser.Events.Disconnected, doCleanup); return browser; } catch (error) { - await doClose().catch(() => {}); + await progress.race(doClose().catch(() => {})); throw error; } } @@ -289,7 +289,7 @@ export class Chromium extends BrowserType { headers: headersObjectToArray(headers), }, disconnectFromSelenium); } catch (e) { - await disconnectFromSelenium(); + await progress.race(disconnectFromSelenium()); throw e; } } diff --git a/packages/playwright-core/src/server/chromium/crDragDrop.ts b/packages/playwright-core/src/server/chromium/crDragDrop.ts index 893968917a4c0..1bc75ef8b862a 100644 --- a/packages/playwright-core/src/server/chromium/crDragDrop.ts +++ b/packages/playwright-core/src/server/chromium/crDragDrop.ts @@ -95,15 +95,15 @@ export class DragManager { let expectingDrag = false; await progress.race(this._crPage._page.safeNonStallingEvaluateInAllFrames(`(${setupDragListeners.toString()})()`, 'utility')); client.on('Input.dragIntercepted', onDragIntercepted!); - await client.send('Input.setInterceptDrags', { enabled: true }); + await progress.race(client.send('Input.setInterceptDrags', { enabled: true })); try { await progress.race(moveCallback()); - expectingDrag = (await Promise.all(this._crPage._page.frames().map(async frame => { + expectingDrag = (await progress.race(Promise.all(this._crPage._page.frames().map(async frame => { return frame.nonStallingEvaluateInExistingContext('window.__cleanupDrag?.()', 'utility').catch(() => false); - }))).some(x => x); + })))).some(x => x); } finally { client.off('Input.dragIntercepted', onDragIntercepted!); - await client.send('Input.setInterceptDrags', { enabled: false }); + await progress.race(client.send('Input.setInterceptDrags', { enabled: false })); } this._dragState = expectingDrag ? (await dragInterceptedPromise).data : null; } catch (error) { diff --git a/packages/playwright-core/src/server/chromium/crInput.ts b/packages/playwright-core/src/server/chromium/crInput.ts index beb030c9bff9a..f26717a441872 100644 --- a/packages/playwright-core/src/server/chromium/crInput.ts +++ b/packages/playwright-core/src/server/chromium/crInput.ts @@ -116,7 +116,7 @@ export class RawMouseImpl implements input.RawMouse { if (forClick) { // Avoid extra protocol calls related to drag and drop, because click relies on // move-down-up protocol commands being sent synchronously. - await actualMove(); + await progress.race(actualMove()); return; } await this._dragManager.interceptDragCausedByMove(progress, x, y, button, buttons, modifiers, actualMove); diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index 700fe894d81f5..69399efd0adeb 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -314,10 +314,10 @@ export class CRPage implements PageDelegate { if (!frame) throw new Error('Cannot set input files to detached input element'); const parentSession = this._sessionForFrame(frame); - await parentSession._client.send('DOM.setFileInputFiles', { + await progress.race(parentSession._client.send('DOM.setFileInputFiles', { objectId: handle._objectId, files - }); + })); } async adoptElementHandle(handle: dom.ElementHandle, to: dom.FrameExecutionContext): Promise> { diff --git a/packages/playwright-core/src/server/clock.ts b/packages/playwright-core/src/server/clock.ts index 130864fddabbe..b14fbb4cdd1e8 100644 --- a/packages/playwright-core/src/server/clock.ts +++ b/packages/playwright-core/src/server/clock.ts @@ -66,9 +66,9 @@ export class Clock { } async resume(progress: Progress) { - await this._installIfNeeded(); + await progress.race(this._installIfNeeded()); this._initScripts.push(await this._browserContext.addInitScript(nullProgress, `globalThis.__pwClock.controller.log('resume', ${Date.now()})`)); - await this._evaluateInFrames(`globalThis.__pwClock.controller.resume()`); + await progress.race(this._evaluateInFrames(`globalThis.__pwClock.controller.resume()`)); } async setFixedTime(time: string | number) { diff --git a/packages/playwright-core/src/server/debugController.ts b/packages/playwright-core/src/server/debugController.ts index 5e004ff602f91..4f85d90dcb83a 100644 --- a/packages/playwright-core/src/server/debugController.ts +++ b/packages/playwright-core/src/server/debugController.ts @@ -88,7 +88,7 @@ export class DebugController extends SdkObject { promises.push(recorder.hideHighlightedSelector()); promises.push(recorder.setMode('none')); } - await Promise.all(promises); + await progress.race(Promise.all(promises)); return; } @@ -125,7 +125,7 @@ export class DebugController extends SdkObject { else if (params.selector) promises.push(recorder.setHighlightedSelector(params.selector)); } - await Promise.all(promises); + await progress.race(Promise.all(promises)); } async hideHighlight(progress: Progress) { @@ -135,7 +135,7 @@ export class DebugController extends SdkObject { promises.push(recorder.hideHighlightedSelector()); // Hide all locator.highlight highlights. promises.push(...this._playwright.allPages().map(p => p.hideHighlight().catch(() => {}))); - await Promise.all(promises); + await progress.race(Promise.all(promises)); } async resume(progress: Progress) { diff --git a/packages/playwright-core/src/server/dispatchers/androidDispatcher.ts b/packages/playwright-core/src/server/dispatchers/androidDispatcher.ts index 9dd3cef399ebd..abf9e0f607d6b 100644 --- a/packages/playwright-core/src/server/dispatchers/androidDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/androidDispatcher.ts @@ -116,7 +116,7 @@ export class AndroidDeviceDispatcher extends Dispatcher this._object.send(progress, 'inputPress', { keyCode }))); + await progress.race(Promise.all(keyCodes.map(keyCode => this._object.send(progress, 'inputPress', { keyCode })))); } async inputPress(params: channels.AndroidDeviceInputPressParams, progress: Progress) { diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index fa5d4e094c983..bfb715958a2c2 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -224,6 +224,7 @@ export class BrowserContextDispatcher extends Dispatcher { await progress.race(fs.promises.mkdir(path.dirname(path.join(tempDirWithRootName, item.name)), { recursive: true })); const file = fs.createWriteStream(path.join(tempDirWithRootName, item.name)); @@ -257,7 +258,7 @@ export class BrowserContextDispatcher extends Dispatcher { // Note: progress is ignored because this operation is not cancellable and should not block in the browser anyway. - await this._context.addCookies(params.cookies); + await progress.race(this._context.addCookies(params.cookies)); } async clearCookies(params: channels.BrowserContextClearCookiesParams, progress: Progress): Promise { @@ -265,26 +266,26 @@ export class BrowserContextDispatcher extends Dispatcher { // Note: progress is ignored because this operation is not cancellable and should not block in the browser anyway. - await this._context.grantPermissions(params.permissions, params.origin); + await progress.race(this._context.grantPermissions(params.permissions, params.origin)); } async clearPermissions(params: channels.BrowserContextClearPermissionsParams, progress: Progress): Promise { // Note: progress is ignored because this operation is not cancellable and should not block in the browser anyway. - await this._context.clearPermissions(); + await progress.race(this._context.clearPermissions()); } async setGeolocation(params: channels.BrowserContextSetGeolocationParams, progress: Progress): Promise { // Note: progress is ignored because this operation is not cancellable and should not block in the browser anyway. - await this._context.setGeolocation(params.geolocation); + await progress.race(this._context.setGeolocation(params.geolocation)); } async setExtraHTTPHeaders(params: channels.BrowserContextSetExtraHTTPHeadersParams, progress: Progress): Promise { @@ -312,7 +313,7 @@ export class BrowserContextDispatcher extends Dispatcher { - await RecorderApp.show(this._context, params); + await progress.race(RecorderApp.show(this._context, params)); } async disableRecorder(params: channels.BrowserContextDisableRecorderParams, progress: Progress): Promise { - const recorder = await Recorder.existingForContext(this._context); - await recorder?.setMode('none'); + const recorder = await progress.race(Recorder.existingForContext(this._context)); + if (recorder) + await progress.race(recorder.setMode('none')); } async exposeConsoleApi(params: channels.BrowserContextExposeConsoleApiParams, progress: Progress): Promise { @@ -379,15 +381,15 @@ export class BrowserContextDispatcher extends Dispatcher { - await this._context.clock.fastForward(params.ticksString ?? params.ticksNumber ?? 0); + await progress.race(this._context.clock.fastForward(params.ticksString ?? params.ticksNumber ?? 0)); } async clockInstall(params: channels.BrowserContextClockInstallParams, progress: Progress): Promise { - await this._context.clock.install(params.timeString ?? params.timeNumber ?? undefined); + await progress.race(this._context.clock.install(params.timeString ?? params.timeNumber ?? undefined)); } async clockPauseAt(params: channels.BrowserContextClockPauseAtParams, progress: Progress): Promise { - await this._context.clock.pauseAt(params.timeString ?? params.timeNumber ?? 0); + await progress.race(this._context.clock.pauseAt(params.timeString ?? params.timeNumber ?? 0)); this._clockPaused = true; } @@ -397,15 +399,15 @@ export class BrowserContextDispatcher extends Dispatcher { - await this._context.clock.runFor(params.ticksString ?? params.ticksNumber ?? 0); + await progress.race(this._context.clock.runFor(params.ticksString ?? params.ticksNumber ?? 0)); } async clockSetFixedTime(params: channels.BrowserContextClockSetFixedTimeParams, progress: Progress): Promise { - await this._context.clock.setFixedTime(params.timeString ?? params.timeNumber ?? 0); + await progress.race(this._context.clock.setFixedTime(params.timeString ?? params.timeNumber ?? 0)); } async clockSetSystemTime(params: channels.BrowserContextClockSetSystemTimeParams, progress: Progress): Promise { - await this._context.clock.setSystemTime(params.timeString ?? params.timeNumber ?? 0); + await progress.race(this._context.clock.setSystemTime(params.timeString ?? params.timeNumber ?? 0)); } async updateSubscription(params: channels.BrowserContextUpdateSubscriptionParams, progress: Progress): Promise { diff --git a/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts index 36ebb8ae3b429..e72853efc7adc 100644 --- a/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts @@ -88,7 +88,7 @@ export class BrowserDispatcher extends Dispatcher(context) : undefined; if (contextDispatcher) { - await contextDispatcher.stopPendingOperations(new Error(params.reason)); + await progress.race(contextDispatcher.stopPendingOperations(new Error(params.reason))); contextDispatcher._dispose(); } } @@ -116,7 +116,7 @@ export class BrowserDispatcher extends Dispatcher { @@ -124,7 +124,7 @@ export class BrowserDispatcher extends Dispatcher { @@ -132,7 +132,7 @@ export class BrowserDispatcher extends Dispatcher { diff --git a/packages/playwright-core/src/server/dispatchers/dispatcher.ts b/packages/playwright-core/src/server/dispatchers/dispatcher.ts index 4ba12386dd7ff..d26c7066ba210 100644 --- a/packages/playwright-core/src/server/dispatchers/dispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/dispatcher.ts @@ -189,7 +189,7 @@ export class RootDispatcher extends Dispatcher { assert(!this._initialized); this._initialized = true; return { - playwright: await this.createPlaywright(this, params), + playwright: await progress.race(this.createPlaywright(this, params)), }; } } diff --git a/packages/playwright-core/src/server/dispatchers/disposableDispatcher.ts b/packages/playwright-core/src/server/dispatchers/disposableDispatcher.ts index 344f8e41a0f63..5238769a2b6a5 100644 --- a/packages/playwright-core/src/server/dispatchers/disposableDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/disposableDispatcher.ts @@ -30,7 +30,7 @@ export class DisposableDispatcher extends Dispatcher { if (URL.canParse(params.endpoint)) - return await this._connectOverWebSocket(params, progress); - return await this._connectOverPipe(params, progress); + return await this._connectOverWebSocket(progress, params); + return await progress.race(this._connectOverPipe(params)); } - private async _connectOverWebSocket(params: channels.LocalUtilsConnectParams, progress: Progress): Promise { + private async _connectOverWebSocket(progress: Progress, params: channels.LocalUtilsConnectParams): Promise { const wsHeaders = { 'User-Agent': getUserAgent(), 'x-playwright-proxy': params.exposeNetwork ?? '', @@ -126,7 +126,7 @@ export class LocalUtilsDispatcher extends Dispatcher { + private async _connectOverPipe(params: channels.LocalUtilsConnectParams): Promise { const socket = await new Promise((resolve, reject) => { const conn = net.connect(params.endpoint, () => resolve(conn)); conn.on('error', reject); diff --git a/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts b/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts index 3eaaf0c29aeba..ce51ffec96262 100644 --- a/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts +++ b/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts @@ -147,25 +147,25 @@ export class RouteDispatcher extends Dispatcher { // Note: progress is ignored because this operation is not cancellable and should not block in the browser anyway. this._checkNotHandled(); - await this._object.continue({ + await progress.race(this._object.continue({ url: params.url, method: params.method, headers: params.headers, postData: params.postData, isFallback: params.isFallback, - }); + })); } 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); + await progress.race(this._object.fulfill(params)); } 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'); + await progress.race(this._object.abort(params.errorCode || 'failed')); } async redirectNavigationRequest(params: channels.RouteRedirectNavigationRequestParams, progress: Progress): Promise { @@ -218,7 +218,7 @@ export class APIRequestContextDispatcher extends Dispatcher { progress.metadata.potentiallyClosesScope = true; - await this._object.dispose(params); + await progress.race(this._object.dispose(params)); this._dispose(); } diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index 130bd89805f37..1b351dc616dad 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -208,7 +208,7 @@ export class PageDispatcher extends Dispatcher { - const recorder = await Recorder.forContext(this._page.browserContext, { omitCallTracking: true, hideToolbar: true }); + const recorder = await progress.race(Recorder.forContext(this._page.browserContext, { omitCallTracking: true, hideToolbar: true })); const selector = await recorder.pickLocator(progress, this._page); return { selector }; } async cancelPickLocator(params: channels.PageCancelPickLocatorParams, progress: Progress): Promise { - const recorder = await Recorder.existingForContext(this._page.browserContext); - await recorder?.setMode('none'); + const recorder = await progress.race(Recorder.existingForContext(this._page.browserContext)); + if (recorder) + await progress.race(recorder.setMode('none')); } async screencastShowOverlay(params: channels.PageScreencastShowOverlayParams): Promise { @@ -428,7 +429,7 @@ export class PageDispatcher extends Dispatcher { this._jsCoverageActive = false; const coverage = this._page.coverage as CRCoverage; - return await coverage.stopJSCoverage(); + return await progress.race(coverage.stopJSCoverage()); } async startCSSCoverage(params: channels.PageStartCSSCoverageParams, progress: Progress): Promise { @@ -440,7 +441,7 @@ export class PageDispatcher extends Dispatcher { this._cssCoverageActive = false; const coverage = this._page.coverage as CRCoverage; - return await coverage.stopCSSCoverage(); + return await progress.race(coverage.stopCSSCoverage()); } _onFrameAttached(frame: Frame) { diff --git a/packages/playwright-core/src/server/dispatchers/streamDispatcher.ts b/packages/playwright-core/src/server/dispatchers/streamDispatcher.ts index f5cecf84f6896..b84ab2ddbc33d 100644 --- a/packages/playwright-core/src/server/dispatchers/streamDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/streamDispatcher.ts @@ -53,11 +53,13 @@ export class StreamDispatcher extends Dispatcher { + try { + await progress.race(readyPromise); + } finally { stream.off('readable', done); stream.off('end', done); stream.off('error', done); - }); + } } const buffer = stream.read(Math.min(stream.readableLength, params.size || stream.readableLength)); return { binary: buffer || Buffer.from('') }; diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index 8a709367b6e99..390777b2046ec 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -255,10 +255,10 @@ export class ElementHandle extends js.JSHandle { return Math.abs(area); }; - const [quads, metrics] = await Promise.all([ + const [quads, metrics] = await progress.race(Promise.all([ this._page.delegate.getContentQuads(this), this._page.mainFrame().utilityContext().then(utility => utility.evaluate(() => ({ width: innerWidth, height: innerHeight }))), - ] as const); + ] as const)); if (quads === 'error:notconnected') return quads; if (!quads || !quads.length) @@ -289,10 +289,10 @@ export class ElementHandle extends js.JSHandle { } private async _offsetPoint(progress: Progress, offset: types.Point): Promise<{ point: types.Point, box: types.Rect } | 'error:notvisible' | 'error:notconnected'> { - const [box, border] = await Promise.all([ + const [box, border] = await progress.race(Promise.all([ this.boundingBox(progress), this.evaluateInUtility(([injected, node]) => injected.getElementBorderWidth(node), {}).catch(e => {}), - ]); + ])); if (!box || !border) return 'error:notvisible'; if (border === 'error:notconnected') @@ -328,7 +328,7 @@ export class ElementHandle extends js.JSHandle { } if (!options.skipActionPreChecks && !options.force && !noAutoWaiting) await this._frame._page.performActionPreChecks(progress); - const result = await action(retry); + const result = await progress.race(action(retry)); ++retry; if (result === 'error:notvisible') { if (options.force || noAutoWaiting) @@ -763,7 +763,7 @@ export class ElementHandle extends js.JSHandle { return { matches: result.matches, isRadio: result.isRadio }; }; await this._markAsTargetElement(progress); - const checkedState = await isChecked(); + const checkedState = await progress.race(isChecked()); if (checkedState.matches === state) return 'done'; if (!state && checkedState.isRadio) @@ -773,7 +773,7 @@ export class ElementHandle extends js.JSHandle { return result; if (options.trial) return 'done'; - const finalState = await isChecked(); + const finalState = await progress.race(isChecked()); if (finalState.matches !== state) throw new NonRecoverableDOMError('Clicking the checkbox did not change its state'); return 'done'; diff --git a/packages/playwright-core/src/server/electron/electron.ts b/packages/playwright-core/src/server/electron/electron.ts index 6b737379232c2..879a729beea9b 100644 --- a/packages/playwright-core/src/server/electron/electron.ts +++ b/packages/playwright-core/src/server/electron/electron.ts @@ -140,11 +140,11 @@ export class ElectronApplication extends SdkObject { async browserWindow(progress: Progress, page: Page): Promise> { // Assume CRPage as Electron is always Chromium. const targetId = (page.delegate as CRPage)._targetId; - const electronHandle = await this._nodeElectronHandlePromise; - return await electronHandle.evaluateHandle(({ BrowserWindow, webContents }, targetId) => { + const electronHandle = await progress.race(this._nodeElectronHandlePromise); + return await progress.race(electronHandle.evaluateHandle(({ BrowserWindow, webContents }, targetId) => { const wc = webContents.fromDevToolsTargetId(targetId); return BrowserWindow.fromWebContents(wc!)!; - }, targetId); + }, targetId)); } } @@ -212,7 +212,7 @@ export class Electron extends SdkObject { // 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({ + const { launchedProcess, gracefullyClose, kill } = await progress.race(launchProcess({ command, args: electronArguments, env, @@ -229,7 +229,7 @@ export class Electron extends SdkObject { 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. @@ -257,10 +257,10 @@ export class Electron extends SdkObject { nodeTransport.close(); }).catch(() => {}); - const chromeMatch = await Promise.race([ + const chromeMatch = await progress.race(Promise.race([ chromeMatchPromise, waitForXserverError, - ]); + ])); const chromeTransport = await WebSocketTransport.connect(progress, chromeMatch[1]); const browserProcess: BrowserProcess = { onclose: undefined, @@ -291,7 +291,7 @@ export class Electron extends SdkObject { await progress.race(app.initialize()); return app; } catch (error) { - await kill(); + await progress.race(kill()); throw error; } } diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts index 4baab0b63783c..98cc613e2889b 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -511,11 +511,11 @@ export class FFPage implements PageDelegate { } async setInputFilePaths(progress: Progress, handle: dom.ElementHandle, files: string[]): Promise { - await this._session.send('Page.setFileInputFiles', { + await progress.race(this._session.send('Page.setFileInputFiles', { frameId: handle._context.frame._id, objectId: handle._objectId, files - }); + })); } async adoptElementHandle(handle: dom.ElementHandle, to: dom.FrameExecutionContext): Promise> { diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index f3905b63077cf..24f8a4f478285 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -25,7 +25,7 @@ import { SdkObject } from './instrumentation'; import * as js from './javascript'; import * as network from './network'; import { Page, ariaSnapshotForFrame } from './page'; -import { isAbortError, ProgressController } from './progress'; +import { isAbortError, nullProgress, ProgressController } from './progress'; import * as types from './types'; import { LongStandingScope, asLocator, assert, constructURLBasedOnBaseURL, makeWaitForNextTask, renderTitleForCall } from '../utils'; import { isSessionClosedError } from './protocolError'; @@ -172,9 +172,9 @@ export class FrameManager { const barrier = new SignalBarrier(progress); this._signalBarriers.add(barrier); try { - const result = await action(); + const result = await progress.race(action()); await progress.race(this._page.delegate.inputActionEpilogue()); - await barrier.waitFor(); + await progress.race(barrier.waitFor()); // Resolve in the next task, after all waitForNavigations. await new Promise(makeWaitForNextTask()); return result; @@ -649,8 +649,12 @@ export class Frame extends SdkObject { const navigationEvents: NavigationEvent[] = []; const collectNavigations = (arg: NavigationEvent) => navigationEvents.push(arg); this.on(Frame.Events.InternalNavigation, collectNavigations); - const navigateResult = await progress.race(this._page.delegate.navigateFrame(this, url, referer)).finally( - () => this.off(Frame.Events.InternalNavigation, collectNavigations)); + let navigateResult; + try { + navigateResult = await progress.race(this._page.delegate.navigateFrame(this, url, referer)); + } finally { + this.off(Frame.Events.InternalNavigation, collectNavigations); + } let event: NavigationEvent; if (navigateResult.newDocumentId) { @@ -777,7 +781,7 @@ export class Frame extends SdkObject { throw new Error(`state: expected one of (attached|detached|visible|hidden)`); if (performActionPreChecksAndLog) progress.log(`waiting for ${this._asLocator(selector)}${state === 'attached' ? '' : ' to be ' + state}`); - const promise = this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => { + const promise = this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async (progress, continuePolling) => { if (performActionPreChecksAndLog) await this._page.performActionPreChecks(progress); @@ -1085,7 +1089,7 @@ export class Frame extends SdkObject { return result!; } - async retryWithProgressAndTimeouts(progress: Progress, timeouts: number[], action: (continuePolling: symbol) => Promise): Promise { + async retryWithProgressAndTimeouts(progress: Progress, timeouts: number[], action: (progress: Progress, continuePolling: symbol) => Promise): Promise { const continuePolling = Symbol('continuePolling'); timeouts = [0, ...timeouts]; let timeoutIndex = 0; @@ -1101,7 +1105,7 @@ export class Frame extends SdkObject { ], actionPromise)); } try { - const result = await action(continuePolling); + const result = await action(progress, continuePolling); if (result === continuePolling) continue; return result as R; @@ -1133,11 +1137,11 @@ export class Frame extends SdkObject { progress: Progress, selector: string, options: { strict?: boolean, noAutoWaiting?: boolean, force?: boolean, performActionPreChecks?: boolean }, - action: (handle: dom.ElementHandle) => Promise): Promise { + action: (progress: Progress, handle: dom.ElementHandle) => Promise): Promise { progress.log(`waiting for ${this._asLocator(selector)}`); const noAutoWaiting = (options as any).__testHookNoAutoWaiting ?? options.noAutoWaiting; const performActionPreChecks = (options.performActionPreChecks ?? !options.force) && !noAutoWaiting; - return this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => { + return this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async (progress, continuePolling) => { if (performActionPreChecks) await this._page.performActionPreChecks(progress); @@ -1175,7 +1179,7 @@ export class Frame extends SdkObject { const element = await progress.race(result.evaluateHandle(r => r.element)) as dom.ElementHandle; result.dispose(); try { - const result = await action(element); + const result = await action(progress, element); if (result === 'error:notconnected') { if (noAutoWaiting) throw new dom.NonRecoverableDOMError('Element is not attached to the DOM'); @@ -1190,22 +1194,22 @@ export class Frame extends SdkObject { } async rafrafTimeoutScreenshotElementWithProgress(progress: Progress, selector: string, timeout: number, options: ScreenshotOptions): Promise { - return await this._retryWithProgressIfNotConnected(progress, selector, { strict: true, performActionPreChecks: true }, async handle => { + return await this._retryWithProgressIfNotConnected(progress, selector, { strict: true, performActionPreChecks: true }, async (progress, handle) => { await handle._frame.rafrafTimeout(progress, timeout); return await this._page.screenshotter.screenshotElement(progress, handle, options); }); } async click(progress: Progress, selector: string, options: { noWaitAfter?: boolean } & types.MouseClickOptions & types.PointerActionWaitOptions) { - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, handle => handle._click(progress, { ...options, waitAfter: !options.noWaitAfter }))); + return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle) => handle._click(progress, { ...options, waitAfter: !options.noWaitAfter }))); } async dblclick(progress: Progress, selector: string, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions) { - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, handle => handle._dblclick(progress, options))); + return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle) => handle._dblclick(progress, options))); } async dragAndDrop(progress: Progress, source: string, target: string, options: types.DragActionOptions & types.PointerActionWaitOptions) { - dom.assertDone(await this._retryWithProgressIfNotConnected(progress, source, options, async handle => { + dom.assertDone(await this._retryWithProgressIfNotConnected(progress, source, options, async (progress, handle) => { return handle._retryPointerAction(progress, 'move and down', false, async point => { await this._page.mouse.move(progress, point.x, point.y); await this._page.mouse.down(progress); @@ -1216,7 +1220,7 @@ export class Frame extends SdkObject { }); })); // Note: do not perform locator handlers checkpoint to avoid moving the mouse in the middle of a drag operation. - dom.assertDone(await this._retryWithProgressIfNotConnected(progress, target, { ...options, performActionPreChecks: false }, async handle => { + dom.assertDone(await this._retryWithProgressIfNotConnected(progress, target, { ...options, performActionPreChecks: false }, async (progress, handle) => { return handle._retryPointerAction(progress, 'move and up', false, async point => { await this._page.mouse.move(progress, point.x, point.y, { steps: options.steps }); await this._page.mouse.up(progress); @@ -1231,19 +1235,19 @@ export class Frame extends SdkObject { async tap(progress: Progress, selector: string, options: types.PointerActionWaitOptions) { if (!this._page.browserContext._options.hasTouch) throw new Error('The page does not support tap. Use hasTouch context option to enable touch support.'); - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, handle => handle._tap(progress, options))); + return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle) => handle._tap(progress, options))); } async fill(progress: Progress, selector: string, value: string, options: types.CommonActionOptions) { - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, handle => handle._fill(progress, value, options))); + return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle) => handle._fill(progress, value, options))); } async focus(progress: Progress, selector: string, options: types.StrictOptions & { noAutoWaiting?: boolean }) { - dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, handle => handle._focus(progress))); + dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle) => handle._focus(progress))); } async blur(progress: Progress, selector: string, options: types.StrictOptions & { noAutoWaiting?: boolean }) { - dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, handle => handle._blur(progress))); + dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle) => handle._blur(progress))); } async resolveSelector(progress: Progress, selector: string, options: { mainWorld?: boolean } = {}): Promise<{ resolvedSelector: string }> { @@ -1376,32 +1380,32 @@ export class Frame extends SdkObject { } async hover(progress: Progress, selector: string, options: types.PointerActionOptions & types.PointerActionWaitOptions) { - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, handle => handle._hover(progress, options))); + return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle) => handle._hover(progress, options))); } async selectOption(progress: Progress, selector: string, elements: dom.ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise { - return await this._retryWithProgressIfNotConnected(progress, selector, options, handle => handle._selectOption(progress, elements, values, options)); + return await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle) => handle._selectOption(progress, elements, values, options)); } async setInputFiles(progress: Progress, selector: string, params: Omit & { noAutoWaiting?: boolean }): Promise { - const inputFileItems = await prepareFilesForUpload(this, params); - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, params, handle => handle._setInputFiles(progress, inputFileItems))); + const inputFileItems = await progress.race(prepareFilesForUpload(this, params)); + return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, params, (progress, handle) => handle._setInputFiles(progress, inputFileItems))); } async type(progress: Progress, selector: string, text: string, options: { delay?: number, noAutoWaiting?: boolean } & types.StrictOptions) { - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, handle => handle._type(progress, text, options))); + return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle) => handle._type(progress, text, options))); } async press(progress: Progress, selector: string, key: string, options: { delay?: number, noWaitAfter?: boolean, noAutoWaiting?: boolean } & types.StrictOptions) { - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, handle => handle._press(progress, key, options))); + return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle) => handle._press(progress, key, options))); } async check(progress: Progress, selector: string, options: types.PointerActionWaitOptions) { - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, handle => handle._setChecked(progress, true, options))); + return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle) => handle._setChecked(progress, true, options))); } async uncheck(progress: Progress, selector: string, options: types.PointerActionWaitOptions) { - return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, handle => handle._setChecked(progress, false, options))); + return dom.assertDone(await this._retryWithProgressIfNotConnected(progress, selector, options, (progress, handle) => handle._setChecked(progress, false, options))); } async waitForTimeout(progress: Progress, timeout: number) { @@ -1437,7 +1441,7 @@ export class Frame extends SdkObject { } // Step 3: auto-retry expect with increasing timeouts. Bounded by the total remaining time. - const result = await this.retryWithProgressAndTimeouts(progress, [100, 250, 500, 1000], async continuePolling => { + const result = await this.retryWithProgressAndTimeouts(progress, [100, 250, 500, 1000], async (progress, continuePolling) => { if (!options.noAutoWaiting) await this._page.performActionPreChecks(progress); const { matches, received } = await this._expectInternal(progress, selector, options, lastIntermediateResult, false); @@ -1471,16 +1475,19 @@ export class Frame extends SdkObject { } private async _expectInternal(progress: Progress, selector: string | undefined, options: FrameExpectParams, lastIntermediateResult: { received?: any, isSet: boolean, errorMessage?: string }, noAbort: boolean) { + const progressLog = (text: string) => progress.log(text); + const callId = progress.metadata.id; // The first expect check, a.k.a. one-shot, always finishes - even when progress is aborted. - const race = (p: Promise) => noAbort ? p : progress.race(p); - const selectorInFrame = selector ? await race(this.selectors.resolveFrameForSelector(selector, { strict: true })) : undefined; + if (noAbort) + progress = nullProgress; + const selectorInFrame = selector ? await progress.race(this.selectors.resolveFrameForSelector(selector, { strict: true })) : undefined; const { frame, info } = selectorInFrame || { frame: this, info: undefined }; const world = options.expression === 'to.have.property' ? 'main' : (info?.world ?? 'utility'); - const context = await race(frame.context(world)); - const injected = await race(context.injectedScript()); + const context = await progress.race(frame.context(world)); + const injected = await progress.race(context.injectedScript()); - const { log, matches, received, missingReceived } = await race(injected.evaluate(async (injected, { info, options, callId }) => { + const { log, matches, received, missingReceived } = await progress.race(injected.evaluate(async (injected, { info, options, callId }) => { const elements = info ? injected.querySelectorAll(info.parsed, document) : []; if (callId) injected.markTargetElements(new Set(elements), callId); @@ -1495,10 +1502,10 @@ export class Frame extends SdkObject { if (info) injected.checkDeprecatedSelectorUsage(info.parsed, elements); return { log, ...await injected.expect(elements[0], options, elements) }; - }, { info, options, callId: progress.metadata.id })); + }, { info, options, callId })); if (log) - progress.log(log); + progressLog(log); // Note: missingReceived avoids `unexpected value "undefined"` when element was not found. if (matches === options.isNot) { if (missingReceived) { @@ -1509,7 +1516,7 @@ export class Frame extends SdkObject { } lastIntermediateResult.isSet = true; if (!missingReceived && !Array.isArray(received)) - progress.log(` unexpected value "${renderUnexpectedValue(options.expression, received)}"`); + progressLog(` unexpected value "${renderUnexpectedValue(options.expression, received)}"`); } return { matches, received }; } @@ -1615,6 +1622,8 @@ export class Frame extends SdkObject { if (timeout === 0) return; const context = await progress.race(this.utilityContext()); + + // eslint-disable-next-line progress/await-must-use-progress --- all promises are awaited with progress race. await Promise.all([ // wait for double raf progress.race(context.evaluate(() => new Promise(x => { @@ -1642,7 +1651,7 @@ export class Frame extends SdkObject { private async _callOnElementOnceMatches(progress: Progress, selector: string, body: ElementCallback, taskData: T, options: types.StrictOptions & { mainWorld?: boolean }, scope?: dom.ElementHandle): Promise { const callbackText = body.toString(); progress.log(`waiting for ${this._asLocator(selector)}`); - const promise = this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async continuePolling => { + const promise = this.retryWithProgressAndTimeouts(progress, [0, 20, 50, 100, 100, 500], async (progress, continuePolling) => { const resolved = await progress.race(this.selectors.resolveInjectedForSelector(selector, options, scope)); if (!resolved) return continuePolling; @@ -1732,7 +1741,7 @@ export class Frame extends SdkObject { if (options.selector && options.mode !== 'ai') { // Non-ai locator snapshot is auto-waiting and does not include iframes. - const snapshot = await this._retryWithProgressIfNotConnected(progress, options.selector, { strict: true, performActionPreChecks: true }, async handle => { + const snapshot = await this._retryWithProgressIfNotConnected(progress, options.selector, { strict: true, performActionPreChecks: true }, async (progress, handle) => { return await progress.race(handle.evaluateInUtility(([injected, element, opts]) => injected.ariaSnapshot(element, opts), { mode: 'default' as const, depth: options.depth })); }); return { snapshot }; @@ -1741,7 +1750,7 @@ export class Frame extends SdkObject { let targetFrame: Frame; let info: SelectorInfo | undefined; if (options.selector) { - const resolved = await this.selectors.resolveInjectedForSelector(options.selector, { strict: true }); + const resolved = await progress.race(this.selectors.resolveInjectedForSelector(options.selector, { strict: true })); if (!resolved) throw new Error(`Selector "${options.selector}" did not resolve to any element`); targetFrame = resolved.frame; @@ -1770,7 +1779,7 @@ class SignalBarrier { this.retain(); } - waitFor(): PromiseLike { + waitFor(): Promise { this.release(); return this._progress.race(this._promise); } diff --git a/packages/playwright-core/src/server/input.ts b/packages/playwright-core/src/server/input.ts index 80ff89b6ddef6..2dfbf672ebb0d 100644 --- a/packages/playwright-core/src/server/input.ts +++ b/packages/playwright-core/src/server/input.ts @@ -54,7 +54,7 @@ export class Keyboard { } async apiDown(progress: Progress, key: string) { - await this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata); + await progress.race(this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata)); await this.down(progress, key); } @@ -82,7 +82,7 @@ export class Keyboard { } async apiUp(progress: Progress, key: string) { - await this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata); + await progress.race(this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata)); await this.up(progress, key); } @@ -95,7 +95,7 @@ export class Keyboard { } async apiInsertText(progress: Progress, text: string) { - await this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata); + await progress.race(this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata)); await this.insertText(progress, text); } @@ -104,7 +104,7 @@ export class Keyboard { } async apiType(progress: Progress, text: string, options?: { delay?: number }) { - await this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata); + await progress.race(this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata)); await this.type(progress, text, options); } @@ -122,7 +122,7 @@ export class Keyboard { } async apiPress(progress: Progress, key: string, options: { delay?: number } = {}) { - await this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata); + await progress.race(this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata)); await this.press(progress, key, options); } @@ -215,7 +215,7 @@ export class Mouse { async apiMove(progress: Progress, x: number, y: number, options: { steps?: number, forClick?: boolean } = {}) { progress.metadata.point = { x, y }; - await this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata); + await progress.race(this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata)); await this.move(progress, x, y, options); } @@ -234,7 +234,7 @@ export class Mouse { async apiDown(progress: Progress, options: { button?: types.MouseButton, clickCount?: number } = {}) { progress.metadata.point = this._currentPoint(); - await this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata); + await progress.race(this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata)); await this.down(progress, options); } @@ -247,7 +247,7 @@ export class Mouse { async apiUp(progress: Progress, options: { button?: types.MouseButton, clickCount?: number } = {}) { progress.metadata.point = this._currentPoint(); - await this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata); + await progress.race(this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata)); await this.up(progress, options); } @@ -260,7 +260,7 @@ export class Mouse { async apiClick(progress: Progress, x: number, y: number, options: { delay?: number, button?: types.MouseButton, clickCount?: number, steps?: number } = {}) { progress.metadata.point = { x, y }; - await this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata); + await progress.race(this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata)); await this.click(progress, x, y, options); } @@ -286,12 +286,12 @@ export class Mouse { promises.push(this.down(progress, { ...options, clickCount: cc })); promises.push(this.up(progress, { ...options, clickCount: cc })); } - await Promise.all(promises); + await progress.race(Promise.all(promises)); } } async apiWheel(progress: Progress, deltaX: number, deltaY: number) { - await this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata); + await progress.race(this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata)); await this._raw.wheel(progress, this._x, this._y, this._buttons, this._keyboard._modifiers(), deltaX, deltaY); } } @@ -372,7 +372,7 @@ export class Touchscreen { async apiTap(progress: Progress, x: number, y: number) { if (!this._page.browserContext._options.hasTouch) throw new Error('hasTouch must be enabled on the browser context before using the touchscreen.'); - await this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata); + await progress.race(this._page.instrumentation.onBeforeInputAction(this._page, progress.metadata)); await this.tap(progress, x, y); } diff --git a/packages/playwright-core/src/server/launchApp.ts b/packages/playwright-core/src/server/launchApp.ts index eade44ef12300..58227d32abee3 100644 --- a/packages/playwright-core/src/server/launchApp.ts +++ b/packages/playwright-core/src/server/launchApp.ts @@ -107,8 +107,8 @@ export async function syncLocalStorageWithSettings(page: Page, appName: string) fs.writeFileSync(settingsFile, settings); }); - const settings = await fs.promises.readFile(settingsFile, 'utf-8').catch(() => ('{}')); - await page.addInitScript(nullProgress, + const settings = await progress.race(fs.promises.readFile(settingsFile, 'utf-8').catch(() => ('{}'))); + await page.addInitScript(progress, `(${String((settings: any) => { // iframes w/ snapshots, etc. if (location && location.protocol === 'data:') diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index c6ea477fd0f56..0c298919d39fb 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -271,11 +271,11 @@ export class Page extends SdkObject { this._emulatedSize = undefined; this._emulatedMedia = {}; this._extraHTTPHeaders = undefined; - await Promise.all([ + await progress.race(Promise.all([ this.delegate.updateEmulatedViewportSize(), this.delegate.updateEmulateMedia(), this.delegate.updateExtraHTTPHeaders(), - ]); + ])); await this.delegate.resetForReuse(progress); } @@ -567,7 +567,11 @@ export class Page extends SdkObject { progress.log(` locator handler has finished`); } }); - await progress.race(this.openScope.race(promise)).finally(() => --this._locatorHandlerRunningCounter); + try { + await progress.race(this.openScope.race(promise)); + } finally { + --this._locatorHandlerRunningCounter; + } progress.log(` interception handler has finished, continuing`); } } @@ -673,7 +677,7 @@ export class Page extends SdkObject { this.requestInterceptors.unshift(handler); else this.requestInterceptors.push(handler); - await this.delegate.updateRequestInterception(); + await progress.race(this.delegate.updateRequestInterception()); } async removeRequestInterceptor(handler: network.RouteHandler): Promise { @@ -737,12 +741,12 @@ export class Page extends SdkObject { if (screenshotTimeout) progress.log(`waiting ${screenshotTimeout}ms before taking screenshot`); previous = actual; - actual = await rafrafScreenshot(screenshotTimeout).catch(e => { + actual = await progress.race(rafrafScreenshot(screenshotTimeout).catch(e => { if (this.mainFrame().isNonRetriableError(e)) throw e; progress.log(`failed to take screenshot - ` + e.message); return undefined; - }); + })); if (!actual) continue; // Compare against expectation for the first iteration. @@ -1051,7 +1055,7 @@ export class InitScript extends DisposableObject { export async function ariaSnapshotForFrame(progress: Progress, frame: frames.Frame, options: { mode?: 'ai' | 'default', track?: string, doNotRenderActive?: boolean, info?: SelectorInfo, depth?: number } = {}): Promise<{ full: string[], incremental?: string[] }> { // Only await the topmost navigations, inner frames will be empty when racing. - const snapshot = await frame.retryWithProgressAndTimeouts(progress, [1000, 2000, 4000, 8000], async continuePolling => { + const snapshot = await frame.retryWithProgressAndTimeouts(progress, [1000, 2000, 4000, 8000], async (progress, continuePolling) => { try { const context = await progress.race(frame.utilityContext()); const injectedScript = await progress.race(context.injectedScript()); @@ -1093,6 +1097,7 @@ export async function ariaSnapshotForFrame(progress: Progress, frame: frames.Fra const childDepth = options.depth ? options.depth - iframeDepth - 1 : undefined; return ariaSnapshotFrameRef(progress, frame, ref, { ...options, depth: childDepth }); }); + // eslint-disable-next-line progress/await-must-use-progress --- all promises are callbacks w/ progress. const childSnapshots = await Promise.all(childSnapshotPromises); const full = []; diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index 8427d9824630e..4e6c1241a69ce 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -233,7 +233,7 @@ export class Recorder extends EventEmitter implements Instrume await this._context.exposeBinding(progress, '__pw_recorderRecordAction', false, (source: BindingSource, action: actions.Action) => this._recordAction(progress, source.frame, action)); - await this._context.extendInjectedScript(rawRecorderSource.source, { recorderMode: this._recorderMode, hideToolbar: !!this._params.hideToolbar }); + await progress.race(this._context.extendInjectedScript(rawRecorderSource.source, { recorderMode: this._recorderMode, hideToolbar: !!this._params.hideToolbar })); }); if (this._debugger.isPaused()) @@ -274,7 +274,7 @@ export class Recorder extends EventEmitter implements Instrume async pickLocator(progress: Progress, page: Page): Promise { if (this._mode !== 'none') - await this.setMode('none'); + await progress.race(this.setMode('none')); const selectorPromise = new ManualPromise(); let recorderChangedState = false; @@ -304,7 +304,7 @@ export class Recorder extends EventEmitter implements Instrume eventsHelper.removeEventListeners(listeners); this._pickLocatorPage = undefined; if (!recorderChangedState) - await this.setMode('none'); + await progress.race(this.setMode('none')); } } diff --git a/packages/playwright-core/src/server/recorder/recorderUtils.ts b/packages/playwright-core/src/server/recorder/recorderUtils.ts index 793124a968d0e..dadeb21a48471 100644 --- a/packages/playwright-core/src/server/recorder/recorderUtils.ts +++ b/packages/playwright-core/src/server/recorder/recorderUtils.ts @@ -125,7 +125,7 @@ export async function generateFrameSelector(progress: Progress, frame: Frame): P selectorPromises.push(generateFrameSelectorInParent(progress, parent, frame)); frame = parent; } - const result = await Promise.all(selectorPromises); + const result = await progress.race(Promise.all(selectorPromises)); return result.reverse(); } diff --git a/packages/playwright-core/src/server/screenshotter.ts b/packages/playwright-core/src/server/screenshotter.ts index f2e0611398f16..ad1103c71e1ad 100644 --- a/packages/playwright-core/src/server/screenshotter.ts +++ b/packages/playwright-core/src/server/screenshotter.ts @@ -262,7 +262,7 @@ export class Screenshotter { progress.log('fonts loaded'); } } catch (error) { - await this._restorePageAfterScreenshot(); + await progress.race(this._restorePageAfterScreenshot()); throw error; } } @@ -309,9 +309,9 @@ export class Screenshotter { try { const quality = format === 'jpeg' ? options.quality ?? 80 : undefined; const buffer = await this._page.delegate.takeScreenshot(progress, format, documentRect, viewportRect, quality, fitsViewport, options.scale || 'device'); - await cleanupHighlight(); + await progress.race(cleanupHighlight()); if (shouldSetDefaultBackground) - await this._page.delegate.setBackgroundColor(); + await progress.race(this._page.delegate.setBackgroundColor()); if ((options as any).__testHookAfterScreenshot) await progress.race((options as any).__testHookAfterScreenshot()); return buffer; diff --git a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts index f44294a384b86..38c10b6ccbf6a 100644 --- a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts +++ b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts @@ -345,7 +345,7 @@ export class ClientCertificatesProxy { await progress.race(proxy._socksProxy.listen(0, '127.0.0.1')); return proxy; } catch (error) { - await proxy.close(); + await progress.race(proxy.close()); throw error; } } diff --git a/packages/playwright-core/src/server/trace/recorder/snapshotter.ts b/packages/playwright-core/src/server/trace/recorder/snapshotter.ts index d7331e7a18173..9fea21453ee39 100644 --- a/packages/playwright-core/src/server/trace/recorder/snapshotter.ts +++ b/packages/playwright-core/src/server/trace/recorder/snapshotter.ts @@ -64,7 +64,7 @@ export class Snapshotter { this._started = true; if (!this._initScript) await this._initialize(progress); - await this.reset(); + await progress.race(this.reset()); } async reset() { @@ -95,7 +95,7 @@ export class Snapshotter { const { javaScriptEnabled } = this._context._options; const initScriptSource = `(${frameSnapshotStreamer})("${this._snapshotStreamer}", ${javaScriptEnabled || javaScriptEnabled === undefined})`; this._initScript = await this._context.addInitScript(progress, initScriptSource); - await this._context.safeNonStallingEvaluateInAllFrames(initScriptSource, 'main'); + await progress.race(this._context.safeNonStallingEvaluateInAllFrames(initScriptSource, 'main')); } dispose() { diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index de3d5905059d7..6e976e7389285 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -136,7 +136,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps async resetForReuse(progress: Progress) { // Discard previous chunk if any and ignore any errors there. await this.stopChunk(progress, { mode: 'discard' }).catch(() => {}); - await this._stop(); + await progress.race(this._stop()); if (this._snapshotter) await progress.race(this._snapshotter.resetForReuse()); } @@ -409,8 +409,12 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps this._fs.zip(entries, zipFileName); // Make sure all file operations complete. - const promise = progress.race(this._fs.syncAndGetError()); - const error = await promise.catch(e => e); + let error: Error | undefined; + try { + await progress.race(this._fs.syncAndGetError()); + } catch (e) { + error = e as Error; + } this._isStopping = false; if (this._state) diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index 212fac3bcf69b..c0c5f997694fc 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -193,7 +193,7 @@ export async function openTraceViewerApp(url: string, browserName: string, optio } if (!isUnderTest()) - await syncLocalStorageWithSettings(page, 'traceviewer'); + await progress.race(syncLocalStorageWithSettings(page, 'traceviewer')); if (isUnderTest()) page.on('close', () => context.close(nullProgress, { reason: 'Trace viewer closed' }).catch(() => {})); diff --git a/packages/playwright-core/src/server/webkit/wkInput.ts b/packages/playwright-core/src/server/webkit/wkInput.ts index 5a762a82ddfca..23b4e36c73994 100644 --- a/packages/playwright-core/src/server/webkit/wkInput.ts +++ b/packages/playwright-core/src/server/webkit/wkInput.ts @@ -155,7 +155,7 @@ export class RawMouseImpl implements input.RawMouse { async wheel(progress: Progress, x: number, y: number, buttons: Set, modifiers: Set, deltaX: number, deltaY: number): Promise { if (this._page?.browserContext._options.isMobile) throw new Error('Mouse wheel is not supported in mobile WebKit'); - await this._session!.send('Page.updateScrollingState'); + await progress.race(this._session!.send('Page.updateScrollingState')); // Wheel events hit the compositor first, so wait one frame for it to be synced. await this._page!.mainFrame().evaluateExpression(progress, `new Promise(requestAnimationFrame)`, { world: 'utility' }); await progress.race(this._pageProxySession.send('Input.dispatchWheelEvent', { diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index 4455efcee4d72..71e25667c2cf3 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -976,11 +976,11 @@ export class WKPage implements PageDelegate { const pageProxyId = this._pageProxySession.sessionId; const objectId = handle._objectId; if (this._browserContext._browser?.options.channel === 'webkit-wsl') - paths = await Promise.all(paths.map(path => translatePathToWSL(path))); - await Promise.all([ + paths = await progress.race(Promise.all(paths.map(path => translatePathToWSL(path)))); + await progress.race(Promise.all([ this._pageProxySession.connection.browserSession.send('Playwright.grantFileReadAccess', { pageProxyId, paths }), this._session.send('DOM.setInputFiles', { objectId, paths }) - ]); + ])); } async adoptElementHandle(handle: dom.ElementHandle, to: dom.FrameExecutionContext): Promise> { diff --git a/utils/eslint-plugin-progress/index.js b/utils/eslint-plugin-progress/index.js index d976b137eb953..0930cf7a31d61 100644 --- a/utils/eslint-plugin-progress/index.js +++ b/utils/eslint-plugin-progress/index.js @@ -55,13 +55,34 @@ function isProgressRace(node) { } /** - * Checks whether `progress` is passed as first argument to a call. + * Unwraps .then()/.catch()/.finally() chains to get the root call. */ -function passesProgressAsFirstArg(node) { - if (node.type !== 'CallExpression') +function unwrapPromiseChain(node) { + while (node.type === 'CallExpression' && + node.callee.type === 'MemberExpression' && + node.callee.property.type === 'Identifier' && + ['then', 'catch', 'finally'].includes(node.callee.property.name)) { + node = node.callee.object; + } + return node; +} + +/** + * Checks whether a Progress-typed value is passed as first argument to a call, + * unwrapping any .then/.catch/.finally chains. + */ +function passesProgressAsFirstArg(node, services) { + const root = unwrapPromiseChain(node); + if (root.type !== 'CallExpression') + return false; + const firstArg = root.arguments[0]; + if (!firstArg) return false; - const firstArg = node.arguments[0]; - return firstArg?.type === 'Identifier' && firstArg.name === 'progress'; + const checker = services.program.getTypeChecker(); + const tsNode = services.esTreeNodeToTSNodeMap.get(firstArg); + const type = checker.getTypeAtLocation(tsNode); + const typeName = type.symbol?.name || type.aliasSymbol?.name || checker.typeToString(type); + return typeName === 'Progress'; } /** @@ -137,12 +158,15 @@ const rule = createRule({ const awaited = node.argument; - // await progress.race(...) is always fine. - if (isProgressRace(awaited)) + // await progress.anything(...) is always fine — calls on the progress object itself. + if (awaited.type === 'CallExpression' && + awaited.callee.type === 'MemberExpression' && + awaited.callee.object.type === 'Identifier' && + awaited.callee.object.name === 'progress') return; // await someCall(progress, ...) is fine. - if (passesProgressAsFirstArg(awaited)) + if (passesProgressAsFirstArg(awaited, services)) return; // Check if this await is inside a progress.race() call higher up.