diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 8d87d25b0e513..40ebd2c4b28f7 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -397,8 +397,10 @@ export class FrameManager { if (this._isExcludedFromNetworkIdle(request) || !frame) return; frame._inflightRequests.add(request); - if (frame._inflightRequests.size === 1) + if (frame._inflightRequests.size === 1) { frame._stopNetworkIdleTimer(); + this._page.mainFrame()._recalculateNetworkIdle(); + } } private _isExcludedFromNetworkIdle(request: network.Request): boolean { @@ -578,7 +580,7 @@ export class Frame extends SdkObject { this._stopNetworkIdleTimer(); if (this._inflightRequests.size === 0) this._startNetworkIdleTimer(); - this._page.mainFrame()._recalculateNetworkIdle(this); + this._page.mainFrame()._recalculateNetworkIdle(); this.onLifecycleEvent('commit'); } @@ -635,10 +637,10 @@ export class Frame extends SdkObject { }); } - _recalculateNetworkIdle(frameThatAllowsRemovingNetworkIdle?: Frame) { + _recalculateNetworkIdle() { let isNetworkIdle = this._firedNetworkIdleSelf; for (const child of this._childFrames) { - child._recalculateNetworkIdle(frameThatAllowsRemovingNetworkIdle); + child._recalculateNetworkIdle(); // We require networkidle event to be fired in the whole frame subtree, and then consider it done. if (!child._firedLifecycleEvents.has('networkidle')) isNetworkIdle = false; @@ -649,9 +651,8 @@ export class Frame extends SdkObject { if (this === this._page.mainFrame() && this._url !== 'about:blank') this.apiLog(` "networkidle" event fired`); } - if (frameThatAllowsRemovingNetworkIdle !== this && this._firedLifecycleEvents.has('networkidle') && !isNetworkIdle) { - // Usually, networkidle is fired once and not removed after that. - // However, when we clear them right before a new commit, this is allowed for a particular frame. + if (this._firedLifecycleEvents.has('networkidle') && !isNetworkIdle) { + // A new busy period invalidates networkidle for the frame and its ancestors. this._firedLifecycleEvents.delete('networkidle'); this.emit(Frame.Events.RemoveLifecycle, 'networkidle'); } diff --git a/tests/library/chromium/network-idle.spec.ts b/tests/library/chromium/network-idle.spec.ts new file mode 100644 index 0000000000000..174e5f57060b0 --- /dev/null +++ b/tests/library/chromium/network-idle.spec.ts @@ -0,0 +1,80 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { browserTest as test, expect } from '../../config/browserTest'; +import type { ServerResponse } from 'http'; + +for (const finishBeforeDCL of [true, false]) { + test(`should reach networkidle when requests finish ${finishBeforeDCL ? 'before' : 'after'} DOMContentLoaded following an initial idle`, async ({ page, server }) => { + test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/42598' }); + + let schemaResponse: ServerResponse; + const schemaFinished = page.waitForEvent('requestfinished', request => request.url().endsWith('/schema.json')); + const scriptFinished = page.waitForEvent('requestfinished', request => request.url().endsWith('/finish.js')); + server.setRoute('/network-idle.html', (req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.end(''); + }); + server.setRoute('/app.js', (req, res) => { + res.setHeader('Content-Type', 'text/javascript'); + res.end(` + debugger; + window.result = fetch('/schema.json').then(r => r.json()); + document.write('