diff --git a/src/server/dialog.ts b/src/server/dialog.ts index 84f022cdaa..93a0d7859d 100644 --- a/src/server/dialog.ts +++ b/src/server/dialog.ts @@ -38,6 +38,7 @@ export class Dialog extends SdkObject { this._message = message; this._onHandle = onHandle; this._defaultValue = defaultValue || ''; + this._page._frameManager.dialogDidOpen(); } type(): string { @@ -55,12 +56,14 @@ export class Dialog extends SdkObject { async accept(promptText: string | undefined) { assert(!this._handled, 'Cannot accept dialog which is already handled!'); this._handled = true; + this._page._frameManager.dialogWillClose(); await this._onHandle(true, promptText); } async dismiss() { assert(!this._handled, 'Cannot dismiss dialog which is already handled!'); this._handled = true; + this._page._frameManager.dialogWillClose(); await this._onHandle(false); } } diff --git a/src/server/frames.ts b/src/server/frames.ts index 295bc00c47..26d77f0283 100644 --- a/src/server/frames.ts +++ b/src/server/frames.ts @@ -74,6 +74,7 @@ export class FrameManager { readonly _signalBarriers = new Set(); private _webSockets = new Map(); readonly _responses: network.Response[] = []; + _dialogCounter = 0; constructor(page: Page) { this._page = page; @@ -297,6 +298,17 @@ export class FrameManager { this._page._browserContext.emit(BrowserContext.Events.RequestFailed, request); } + dialogDidOpen() { + // Any ongoing evaluations will be stalled until the dialog is closed. + for (const frame of this._frames.values()) + frame._invalidateNonStallingEvaluations('JavaScript dialog interrupted evaluation'); + this._dialogCounter++; + } + + dialogWillClose() { + this._dialogCounter--; + } + removeChildFramesRecursively(frame: Frame) { for (const child of frame.childFrames()) this._removeFramesRecursively(child); @@ -461,17 +473,17 @@ export class Frame extends SdkObject { setPendingDocument(documentInfo: DocumentInfo | undefined) { this._pendingDocument = documentInfo; if (documentInfo) - this._invalidateNonStallingEvaluations(); + this._invalidateNonStallingEvaluations('Navigation interrupted the evaluation'); } pendingDocument(): DocumentInfo | undefined { return this._pendingDocument; } - private async _invalidateNonStallingEvaluations() { + _invalidateNonStallingEvaluations(message: string) { if (!this._nonStallingEvaluations) return; - const error = new Error('Navigation interrupted the evaluation'); + const error = new Error(message); for (const callback of this._nonStallingEvaluations) callback(error); } @@ -479,6 +491,8 @@ export class Frame extends SdkObject { async nonStallingRawEvaluateInExistingMainContext(expression: string): Promise { if (this._pendingDocument) throw new Error('Frame is currently attempting a navigation'); + if (this._page._frameManager._dialogCounter) + throw new Error('Open JavaScript dialog prevents evaluation'); const context = this._existingMainContext(); if (!context) throw new Error('Frame does not yet have a main execution context'); diff --git a/tests/tracing.spec.ts b/tests/tracing.spec.ts index 33bd4292f6..1601f1bf26 100644 --- a/tests/tracing.spec.ts +++ b/tests/tracing.spec.ts @@ -264,6 +264,16 @@ test('should export trace concurrently to second navigation', async ({ context, } }); +test('should not hang for clicks that open dialogs', async ({ context, page }) => { + await context.tracing.start({ screenshots: true, snapshots: true }); + const dialogPromise = page.waitForEvent('dialog'); + await page.setContent(`
Click me
`); + await page.click('div', { timeout: 2000 }).catch(() => {}); + const dialog = await dialogPromise; + await dialog.dismiss(); + await context.tracing.stop(); +}); + async function parseTrace(file: string): Promise<{ events: any[], resources: Map }> { const entries = await new Promise(f => { const entries: Promise[] = [];