From ef9f1411deb1fd50a908d28e3352f3a982f4b5c7 Mon Sep 17 00:00:00 2001 From: Alex Rudenko Date: Wed, 10 Aug 2022 11:32:58 +0200 Subject: [PATCH] fix: resolve navigation flakiness Closes #8644 --- src/common/LifecycleWatcher.ts | 25 +++++- test/src/NetworkManager.spec.ts | 152 ++++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 1 deletion(-) diff --git a/src/common/LifecycleWatcher.ts b/src/common/LifecycleWatcher.ts index 15746c419e334..924c08f7e9874 100644 --- a/src/common/LifecycleWatcher.ts +++ b/src/common/LifecycleWatcher.ts @@ -19,6 +19,8 @@ import { addEventListener, PuppeteerEventListener, removeEventListeners, + createDeferredPromise, + DeferredPromise, } from './util.js'; import {TimeoutError} from './Errors.js'; import { @@ -100,6 +102,8 @@ export class LifecycleWatcher { #hasSameDocumentNavigation?: boolean; #swapped?: boolean; + #navigationRequestFinished?: DeferredPromise; + constructor( frameManager: FrameManager, frame: Frame, @@ -160,6 +164,11 @@ export class LifecycleWatcher { NetworkManagerEmittedEvents.Request, this.#onRequest.bind(this) ), + addEventListener( + this.#frameManager.networkManager(), + NetworkManagerEmittedEvents.RequestFinished, + this.#onRequestFinished.bind(this) + ), ]; this.#timeoutPromise = this.#createTimeoutPromise(); @@ -171,6 +180,20 @@ export class LifecycleWatcher { return; } this.#navigationRequest = request; + this.#navigationRequestFinished?.reject( + new Error('New navigation request was received') + ); + this.#navigationRequestFinished = createDeferredPromise(); + if (request.response() !== null) { + this.#navigationRequestFinished?.resolve(); + } + } + + #onRequestFinished(request: HTTPRequest): void { + if (this.#navigationRequest !== request) { + return; + } + this.#navigationRequestFinished?.resolve(); } #onFrameDetached(frame: Frame): void { @@ -185,7 +208,7 @@ export class LifecycleWatcher { } async navigationResponse(): Promise { - // We may need to wait for ExtraInfo events before the request is complete. + await this.#navigationRequestFinished; return this.#navigationRequest ? this.#navigationRequest.response() : null; } diff --git a/test/src/NetworkManager.spec.ts b/test/src/NetworkManager.spec.ts index 4776868a0519b..d99fd78bbd4f4 100644 --- a/test/src/NetworkManager.spec.ts +++ b/test/src/NetworkManager.spec.ts @@ -660,4 +660,156 @@ describeChromeOnly('NetworkManager', () => { expect(requests.length).toBe(1); }); + + it(`should resolve the response once the late responseReceivedExtraInfo event arrives`, async () => { + const mockCDPSession = new MockCDPSession(); + const manager = new NetworkManager(mockCDPSession, true, { + frame(): Frame | null { + return null; + }, + }); + + const finishedRequests: HTTPRequest[] = []; + const pendingRequests: HTTPRequest[] = []; + manager.on( + NetworkManagerEmittedEvents.RequestFinished, + (request: HTTPRequest) => { + finishedRequests.push(request); + } + ); + + manager.on(NetworkManagerEmittedEvents.Request, (request: HTTPRequest) => { + pendingRequests.push(request); + }); + + mockCDPSession.emit('Network.requestWillBeSent', { + requestId: 'LOADERID', + loaderId: 'LOADERID', + documentURL: 'http://10.1.0.39:42915/empty.html', + request: { + url: 'http://10.1.0.39:42915/empty.html', + method: 'GET', + headers: { + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36', + }, + mixedContentType: 'none', + initialPriority: 'VeryHigh', + referrerPolicy: 'strict-origin-when-cross-origin', + isSameSite: true, + }, + timestamp: 671.229856, + wallTime: 1660121157.913774, + initiator: {type: 'other'}, + redirectHasExtraInfo: false, + type: 'Document', + frameId: 'FRAMEID', + hasUserGesture: false, + }); + + mockCDPSession.emit('Network.responseReceived', { + requestId: 'LOADERID', + loaderId: 'LOADERID', + timestamp: 671.236025, + type: 'Document', + response: { + url: 'http://10.1.0.39:42915/empty.html', + status: 200, + statusText: 'OK', + headers: { + 'Cache-Control': 'no-cache, no-store', + Connection: 'keep-alive', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=utf-8', + Date: 'Wed, 10 Aug 2022 08:45:57 GMT', + 'Keep-Alive': 'timeout=5', + }, + mimeType: 'text/html', + connectionReused: true, + connectionId: 18, + remoteIPAddress: '10.1.0.39', + remotePort: 42915, + fromDiskCache: false, + fromServiceWorker: false, + fromPrefetchCache: false, + encodedDataLength: 197, + timing: { + requestTime: 671.232585, + proxyStart: -1, + proxyEnd: -1, + dnsStart: -1, + dnsEnd: -1, + connectStart: -1, + connectEnd: -1, + sslStart: -1, + sslEnd: -1, + workerStart: -1, + workerReady: -1, + workerFetchStart: -1, + workerRespondWithSettled: -1, + sendStart: 0.308, + sendEnd: 0.364, + pushStart: 0, + pushEnd: 0, + receiveHeadersEnd: 1.554, + }, + responseTime: 1.660121157917951e12, + protocol: 'http/1.1', + securityState: 'insecure', + }, + hasExtraInfo: true, + frameId: 'FRAMEID', + }); + + mockCDPSession.emit('Network.requestWillBeSentExtraInfo', { + requestId: 'LOADERID', + associatedCookies: [], + headers: { + Accept: + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', + 'Accept-Encoding': 'gzip, deflate', + 'Accept-Language': 'en-US,en;q=0.9', + Connection: 'keep-alive', + Host: '10.1.0.39:42915', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36', + }, + connectTiming: {requestTime: 671.232585}, + }); + + mockCDPSession.emit('Network.loadingFinished', { + requestId: 'LOADERID', + timestamp: 671.234448, + encodedDataLength: 197, + shouldReportCorbBlocking: false, + }); + + expect(pendingRequests.length).toBe(1); + expect(finishedRequests.length).toBe(0); + expect(pendingRequests[0]!.response()).toEqual(null); + + // The extra info might arrive late. + mockCDPSession.emit('Network.responseReceivedExtraInfo', { + requestId: 'LOADERID', + blockedCookies: [], + headers: { + 'Cache-Control': 'no-cache, no-store', + Connection: 'keep-alive', + 'Content-Length': '0', + 'Content-Type': 'text/html; charset=utf-8', + Date: 'Wed, 10 Aug 2022 09:04:39 GMT', + 'Keep-Alive': 'timeout=5', + }, + resourceIPAddressSpace: 'Private', + statusCode: 200, + headersText: + 'HTTP/1.1 200 OK\\r\\nCache-Control: no-cache, no-store\\r\\nContent-Type: text/html; charset=utf-8\\r\\nDate: Wed, 10 Aug 2022 09:04:39 GMT\\r\\nConnection: keep-alive\\r\\nKeep-Alive: timeout=5\\r\\nContent-Length: 0\\r\\n\\r\\n', + }); + + expect(pendingRequests.length).toBe(1); + expect(finishedRequests.length).toBe(1); + expect(pendingRequests[0]!.response()).not.toEqual(null); + }); });