diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts index 08d90f036c..376d4bec4b 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -23,7 +23,7 @@ import type { Headers, RemoteAddr, SecurityDetails, WaitForEventOptions } from ' import fs from 'fs'; import { mime } from '../utilsBundle'; import { assert, isString, headersObjectToArray, isRegExp } from '../utils'; -import { ManualPromise, ScopedRace } from '../utils/manualPromise'; +import { ManualPromise, LongStandingScope } from '../utils/manualPromise'; import { Events } from './events'; import type { Page } from './page'; import { Waiter } from './waiter'; @@ -270,8 +270,8 @@ export class Request extends ChannelOwner implements ap return this._fallbackOverrides; } - _targetClosedRace(): ScopedRace { - return this.serviceWorker()?._closedRace || this.frame()._page?._closedOrCrashedRace || new ScopedRace(); + _targetClosedScope(): LongStandingScope { + return this.serviceWorker()?._closedScope || this.frame()._page?._closedOrCrashedScope || new LongStandingScope(); } } @@ -294,7 +294,7 @@ export class Route extends ChannelOwner implements api.Ro // When page closes or crashes, we catch any potential rejects from this Route. // Note that page could be missing when routing popup's initial request that // does not have a Page initialized just yet. - return this.request()._targetClosedRace().safeRace(promise); + return this.request()._targetClosedScope().safeRace(promise); } _startHandling(): Promise { @@ -522,7 +522,7 @@ export class Response extends ChannelOwner implements } async finished(): Promise { - return this.request()._targetClosedRace().race(this._finishedPromise); + return this.request()._targetClosedScope().race(this._finishedPromise); } async body(): Promise { diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index f6bfa41d05..6373ebe779 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -24,7 +24,7 @@ import { urlMatches } from '../utils/network'; import { TimeoutSettings } from '../common/timeoutSettings'; import type * as channels from '@protocol/channels'; import { parseError, serializeError } from '../protocol/serializers'; -import { assert, headersObjectToArray, isObject, isRegExp, isString, ScopedRace, urlMatchesEqual } from '../utils'; +import { assert, headersObjectToArray, isObject, isRegExp, isString, LongStandingScope, urlMatchesEqual } from '../utils'; import { mkdirIfNeeded } from '../utils/fileUtils'; import { Accessibility } from './accessibility'; import { Artifact } from './artifact'; @@ -78,7 +78,7 @@ export class Page extends ChannelOwner implements api.Page private _frames = new Set(); _workers = new Set(); private _closed = false; - readonly _closedOrCrashedRace = new ScopedRace(); + readonly _closedOrCrashedScope = new LongStandingScope(); private _viewportSize: Size | null; private _routes: RouteHandler[] = []; @@ -141,8 +141,8 @@ export class Page extends ChannelOwner implements api.Page this.coverage = new Coverage(this._channel); - this.once(Events.Page.Close, () => this._closedOrCrashedRace.scopeClosed(new Error(kBrowserOrContextClosedError))); - this.once(Events.Page.Crash, () => this._closedOrCrashedRace.scopeClosed(new Error(kBrowserOrContextClosedError))); + this.once(Events.Page.Close, () => this._closedOrCrashedScope.close(kBrowserOrContextClosedError)); + this.once(Events.Page.Crash, () => this._closedOrCrashedScope.close(kBrowserOrContextClosedError)); this._setEventToSubscriptionMapping(new Map([ [Events.Page.Console, 'console'], @@ -686,7 +686,7 @@ export class Page extends ChannelOwner implements api.Page this._browserContext.setDefaultNavigationTimeout(0); this._browserContext.setDefaultTimeout(0); this._instrumentation?.onWillPause(); - await this._closedOrCrashedRace.safeRace(this.context()._channel.pause()); + await this._closedOrCrashedScope.safeRace(this.context()._channel.pause()); this._browserContext.setDefaultNavigationTimeout(defaultNavigationTimeout); this._browserContext.setDefaultTimeout(defaultTimeout); } diff --git a/packages/playwright-core/src/client/video.ts b/packages/playwright-core/src/client/video.ts index 2f8da237db..6c64a87be8 100644 --- a/packages/playwright-core/src/client/video.ts +++ b/packages/playwright-core/src/client/video.ts @@ -27,7 +27,7 @@ export class Video implements api.Video { constructor(page: Page, connection: Connection) { this._isRemote = connection.isRemote(); - this._artifact = page._closedOrCrashedRace.safeRace(this._artifactReadyPromise); + this._artifact = page._closedOrCrashedScope.safeRace(this._artifactReadyPromise); } _artifactReady(artifact: Artifact) { diff --git a/packages/playwright-core/src/client/worker.ts b/packages/playwright-core/src/client/worker.ts index 94f161dcbf..3a399323d1 100644 --- a/packages/playwright-core/src/client/worker.ts +++ b/packages/playwright-core/src/client/worker.ts @@ -22,13 +22,13 @@ import type { Page } from './page'; import type { BrowserContext } from './browserContext'; import type * as api from '../../types/types'; import type * as structs from '../../types/structs'; -import { ScopedRace } from '../utils'; +import { LongStandingScope } from '../utils'; import { kBrowserOrContextClosedError } from '../common/errors'; export class Worker extends ChannelOwner implements api.Worker { _page: Page | undefined; // Set for web workers. _context: BrowserContext | undefined; // Set for service workers. - readonly _closedRace = new ScopedRace(); + readonly _closedScope = new LongStandingScope(); static from(worker: channels.WorkerChannel): Worker { return (worker as any)._object; @@ -43,7 +43,7 @@ export class Worker extends ChannelOwner implements api. this._context._serviceWorkers.delete(this); this.emit(Events.Worker.Close, this); }); - this.once(Events.Worker.Close, () => this._closedRace.scopeClosed(new Error(kBrowserOrContextClosedError))); + this.once(Events.Worker.Close, () => this._closedScope.close(kBrowserOrContextClosedError)); } url(): string { diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 5ccef970d5..0a1e274045 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -190,7 +190,7 @@ export abstract class BrowserContext extends SdkObject { const [, ...otherPages] = this.pages(); for (const p of otherPages) await p.close(metadata); - if (page && page._crashedRace.isDone()) { + if (page && page._crashedScope.isClosed()) { await page.close(metadata); page = undefined; } diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index fe1a363fad..f076c4380a 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -29,7 +29,7 @@ import * as types from './types'; import { BrowserContext } from './browserContext'; import type { Progress } from './progress'; import { ProgressController } from './progress'; -import { ScopedRace, assert, constructURLBasedOnBaseURL, makeWaitForNextTask, monotonicTime } from '../utils'; +import { LongStandingScope, assert, constructURLBasedOnBaseURL, makeWaitForNextTask, monotonicTime } from '../utils'; import { ManualPromise } from '../utils/manualPromise'; import { debugLogger } from '../common/debugLogger'; import type { CallMetadata } from './instrumentation'; @@ -44,7 +44,7 @@ import { FrameSelectors } from './frameSelectors'; import { TimeoutError } from '../common/errors'; type ContextData = { - contextPromise: ManualPromise; + contextPromise: ManualPromise; context: dom.FrameExecutionContext | null; }; @@ -482,7 +482,7 @@ export class Frame extends SdkObject { _inflightRequests = new Set(); private _networkIdleTimer: NodeJS.Timer | undefined; private _setContentCounter = 0; - readonly _detachedRace = new ScopedRace(); + readonly _detachedScope = new LongStandingScope(); private _raceAgainstEvaluationStallingEventsPromises = new Set>(); readonly _redirectedNavigations = new Map }>(); // documentId -> data readonly selectors: FrameSelectors; @@ -510,7 +510,7 @@ export class Frame extends SdkObject { } isDetached(): boolean { - return this._detachedRace.isDone(); + return this._detachedScope.isClosed(); } _onLifecycleEvent(event: RegularLifecycleEvent) { @@ -613,10 +613,10 @@ export class Frame extends SdkObject { } async raceNavigationAction(progress: Progress, options: types.GotoOptions, action: () => Promise): Promise { - return ScopedRace.raceMultiple([ - this._detachedRace, - this._page._disconnectedRace, - this._page._crashedRace, + return LongStandingScope.raceMultiple([ + this._detachedScope, + this._page._disconnectedScope, + this._page._crashedScope, ], action().catch(e => { if (e instanceof NavigationAbortedError && e.documentId) { const data = this._redirectedNavigations.get(e.documentId); @@ -727,10 +727,10 @@ export class Frame extends SdkObject { } _context(world: types.World): Promise { - return this._contextData.get(world)!.contextPromise.then(contextOrError => { - if (contextOrError instanceof js.ExecutionContext) - return contextOrError; - throw contextOrError; + return this._contextData.get(world)!.contextPromise.then(contextOrDestroyedReason => { + if (contextOrDestroyedReason instanceof js.ExecutionContext) + return contextOrDestroyedReason; + throw new Error(contextOrDestroyedReason.destroyedReason); }); } @@ -1053,10 +1053,10 @@ export class Frame extends SdkObject { // Make sure we react immediately upon page close or frame detach. // We need this to show expected/received values in time. const actionPromise = new Promise(f => setTimeout(f, timeout)); - await ScopedRace.raceMultiple([ - this._page._disconnectedRace, - this._page._crashedRace, - this._detachedRace, + await LongStandingScope.raceMultiple([ + this._page._disconnectedScope, + this._page._crashedScope, + this._detachedScope, ], actionPromise); } progress.throwIfAborted(); @@ -1533,12 +1533,11 @@ export class Frame extends SdkObject { _onDetached() { this._stopNetworkIdleTimer(); - const error = new Error('Frame was detached'); - this._detachedRace.scopeClosed(error); + this._detachedScope.close('Frame was detached'); for (const data of this._contextData.values()) { if (data.context) - data.context.contextDestroyed(error); - data.contextPromise.resolve(error); + data.context.contextDestroyed('Frame was detached'); + data.contextPromise.resolve({ destroyedReason: 'Frame was detached' }); } if (this._parentFrame) this._parentFrame._childFrames.delete(this); @@ -1591,7 +1590,7 @@ export class Frame extends SdkObject { // connections so we might end up creating multiple isolated worlds. // We can use either. if (data.context) { - data.context.contextDestroyed(new Error('Execution context was destroyed, most likely because of a navigation')); + data.context.contextDestroyed('Execution context was destroyed, most likely because of a navigation'); this._setContext(world, null); } this._setContext(world, context); @@ -1600,9 +1599,9 @@ export class Frame extends SdkObject { _contextDestroyed(context: dom.FrameExecutionContext) { // Sometimes we get this after detach, in which case we should not reset // our already destroyed contexts to something that will never resolve. - if (this._detachedRace.isDone()) + if (this._detachedScope.isClosed()) return; - context.contextDestroyed(new Error('Execution context was destroyed, most likely because of a navigation')); + context.contextDestroyed('Execution context was destroyed, most likely because of a navigation'); for (const [world, data] of this._contextData) { if (data.context === context) this._setContext(world, null); @@ -1614,7 +1613,7 @@ export class Frame extends SdkObject { // We should not start a timer and report networkidle in detached frames. // This happens at least in Firefox for child frames, where we may get requestFinished // after the frame was detached - probably a race in the Firefox itself. - if (this._firedLifecycleEvents.has('networkidle') || this._detachedRace.isDone()) + if (this._firedLifecycleEvents.has('networkidle') || this._detachedScope.isClosed()) return; this._networkIdleTimer = setTimeout(() => { this._firedNetworkIdleSelf = true; @@ -1703,10 +1702,10 @@ class SignalBarrier { this._progress.log(` navigated to "${frame._url}"`); return true; }); - await ScopedRace.raceMultiple([ - frame._page._disconnectedRace, - frame._page._crashedRace, - frame._detachedRace, + await LongStandingScope.raceMultiple([ + frame._page._disconnectedScope, + frame._page._crashedScope, + frame._detachedScope, ], waiter.promise).catch(() => {}); waiter.dispose(); this.release(); diff --git a/packages/playwright-core/src/server/javascript.ts b/packages/playwright-core/src/server/javascript.ts index 3221ace0a4..cdb2872688 100644 --- a/packages/playwright-core/src/server/javascript.ts +++ b/packages/playwright-core/src/server/javascript.ts @@ -19,7 +19,7 @@ import * as utilityScriptSource from '../generated/utilityScriptSource'; import { serializeAsCallArgument } from './isomorphic/utilityScriptSerializers'; import type { UtilityScript } from './injected/utilityScript'; import { SdkObject } from './instrumentation'; -import { ScopedRace } from '../utils/manualPromise'; +import { LongStandingScope } from '../utils/manualPromise'; export type ObjectId = string; export type RemoteObject = { @@ -63,7 +63,7 @@ export interface ExecutionContextDelegate { export class ExecutionContext extends SdkObject { private _delegate: ExecutionContextDelegate; private _utilityScriptPromise: Promise | undefined; - private _contextDestroyedRace = new ScopedRace(); + private _contextDestroyedScope = new LongStandingScope(); readonly worldNameForTest: string; constructor(parent: SdkObject, delegate: ExecutionContextDelegate, worldNameForTest: string) { @@ -72,12 +72,12 @@ export class ExecutionContext extends SdkObject { this._delegate = delegate; } - contextDestroyed(error: Error) { - this._contextDestroyedRace.scopeClosed(error); + contextDestroyed(reason: string) { + this._contextDestroyedScope.close(reason); } async _raceAgainstContextDestroyed(promise: Promise): Promise { - return this._contextDestroyedRace.race(promise); + return this._contextDestroyedScope.race(promise); } rawEvaluateJSON(expression: string): Promise { diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index c5be62fe4e..588e77b2bd 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -31,7 +31,7 @@ import * as accessibility from './accessibility'; import { FileChooser } from './fileChooser'; import type { Progress } from './progress'; import { ProgressController } from './progress'; -import { ScopedRace, assert, isError } from '../utils'; +import { LongStandingScope, assert, isError } from '../utils'; import { ManualPromise } from '../utils/manualPromise'; import { debugLogger } from '../common/debugLogger'; import type { ImageComparatorOptions } from '../utils/comparators'; @@ -142,8 +142,8 @@ export class Page extends SdkObject { private _disconnected = false; private _initialized = false; private _eventsToEmitAfterInitialized: { event: string | symbol, args: any[] }[] = []; - readonly _disconnectedRace = new ScopedRace(); - readonly _crashedRace = new ScopedRace(); + readonly _disconnectedScope = new LongStandingScope(); + readonly _crashedScope = new LongStandingScope(); readonly _browserContext: BrowserContext; readonly keyboard: input.Keyboard; readonly mouse: input.Mouse; @@ -285,7 +285,7 @@ export class Page extends SdkObject { this._frameManager.dispose(); this._frameThrottler.dispose(); this.emit(Page.Events.Crash); - this._crashedRace.scopeClosed(new Error('Page crashed')); + this._crashedScope.close('Page crashed'); this.instrumentation.onPageClose(this); } @@ -294,7 +294,7 @@ export class Page extends SdkObject { this._frameThrottler.dispose(); assert(!this._disconnected, 'Page disconnected twice'); this._disconnected = true; - this._disconnectedRace.scopeClosed(new Error('Page closed')); + this._disconnectedScope.close('Page closed'); } async _onFileChooserOpened(handle: dom.ElementHandle) { @@ -632,7 +632,7 @@ export class Page extends SdkObject { } isClosedOrClosingOrCrashed() { - return this._closedState !== 'open' || this._crashedRace.isDone(); + return this._closedState !== 'open' || this._crashedScope.isClosed(); } _addWorker(workerId: string, worker: Worker) { @@ -737,7 +737,7 @@ export class Worker extends SdkObject { didClose() { if (this._existingExecutionContext) - this._existingExecutionContext.contextDestroyed(new Error('Worker was closed')); + this._existingExecutionContext.contextDestroyed('Worker was closed'); this.emit(Worker.Events.Close, this); } diff --git a/packages/playwright-core/src/utils/manualPromise.ts b/packages/playwright-core/src/utils/manualPromise.ts index ff6342056b..9decdab02b 100644 --- a/packages/playwright-core/src/utils/manualPromise.ts +++ b/packages/playwright-core/src/utils/manualPromise.ts @@ -56,25 +56,33 @@ export class ManualPromise extends Promise { } } -export class ScopedRace { +export class LongStandingScope { private _terminateError: Error | undefined; + private _terminateErrorMessage: string | undefined; private _terminatePromises = new Map, Error>(); - private _isDone = false; + private _isClosed = false; - scopeClosed(error: Error) { - this._isDone = true; + reject(error: Error) { + this._isClosed = true; this._terminateError = error; + for (const p of this._terminatePromises.keys()) + p.resolve(error); + } + + close(errorMessage: string) { + this._isClosed = true; + this._terminateErrorMessage = errorMessage; for (const [p, e] of this._terminatePromises) { - rewriteErrorMessage(e, error.message); + rewriteErrorMessage(e, errorMessage); p.resolve(e); } } - isDone() { - return this._isDone; + isClosed() { + return this._isClosed; } - static async raceMultiple(scopes: ScopedRace[], promise: Promise): Promise { + static async raceMultiple(scopes: LongStandingScope[], promise: Promise): Promise { return Promise.race(scopes.map(s => s.race(promise))); } @@ -90,8 +98,9 @@ export class ScopedRace { const terminatePromise = new ManualPromise(); if (this._terminateError) terminatePromise.resolve(this._terminateError); - const error = new Error(''); - this._terminatePromises.set(terminatePromise, error); + if (this._terminateErrorMessage) + terminatePromise.resolve(new Error(this._terminateErrorMessage)); + this._terminatePromises.set(terminatePromise, new Error('')); try { return await Promise.race([ terminatePromise.then(e => safe ? defaultValue : Promise.reject(e)),