From ac162c561ee43dd69eff38e1b354a41bb42c9eba Mon Sep 17 00:00:00 2001 From: Alex Rudenko Date: Tue, 23 Nov 2021 08:19:14 +0100 Subject: [PATCH] feat(chromium)!: roll to Chromium 97.0.4692.0 (r938248) Issues: #7458 --- package.json | 2 +- src/common/FrameManager.ts | 4 +- src/common/HTTPRequest.ts | 11 +- src/common/HTTPResponse.ts | 24 +- src/common/LifecycleWatcher.ts | 3 +- src/common/NetworkManager.ts | 216 +++++++++++++++- src/revisions.ts | 2 +- test/NetworkManager.spec.ts | 459 +++++++++++++++++++++++++++++++++ test/accessibility.spec.ts | 3 +- test/jshandle.spec.ts | 2 + test/mocha-utils.ts | 14 + test/network.spec.ts | 73 ++++++ versions.js | 1 + 13 files changed, 794 insertions(+), 20 deletions(-) create mode 100644 test/NetworkManager.spec.ts diff --git a/package.json b/package.json index 22dc5a499f72b..38549881a93b1 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "license": "Apache-2.0", "dependencies": { "debug": "4.3.2", - "devtools-protocol": "0.0.901419", + "devtools-protocol": "0.0.937139", "extract-zip": "2.0.1", "https-proxy-agent": "5.0.0", "node-fetch": "2.6.5", diff --git a/src/common/FrameManager.ts b/src/common/FrameManager.ts index 80722450a1473..19f2a3b1d0ea5 100644 --- a/src/common/FrameManager.ts +++ b/src/common/FrameManager.ts @@ -199,7 +199,7 @@ export class FrameManager extends EventEmitter { } watcher.dispose(); if (error) throw error; - return watcher.navigationResponse(); + return await watcher.navigationResponse(); async function navigate( client: CDPSession, @@ -243,7 +243,7 @@ export class FrameManager extends EventEmitter { ]); watcher.dispose(); if (error) throw error; - return watcher.navigationResponse(); + return await watcher.navigationResponse(); } private async _onAttachedToTarget( diff --git a/src/common/HTTPRequest.ts b/src/common/HTTPRequest.ts index 6f1dd4b543f62..0d451594e8def 100644 --- a/src/common/HTTPRequest.ts +++ b/src/common/HTTPRequest.ts @@ -13,7 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { CDPSession } from './Connection.js'; +import { ProtocolMapping } from 'devtools-protocol/types/protocol-mapping.js'; + +import { EventEmitter } from './EventEmitter.js'; import { Frame } from './FrameManager.js'; import { HTTPResponse } from './HTTPResponse.js'; import { assert } from './assert.js'; @@ -56,6 +58,13 @@ export interface ResponseForRequest { */ export type ResourceType = Lowercase; +interface CDPSession extends EventEmitter { + send( + method: T, + ...paramArgs: ProtocolMapping.Commands[T]['paramsType'] + ): Promise; +} + /** * * Represents an HTTP request sent by a page. diff --git a/src/common/HTTPResponse.ts b/src/common/HTTPResponse.ts index 9620c0cafee6e..b88ff2dc5fd42 100644 --- a/src/common/HTTPResponse.ts +++ b/src/common/HTTPResponse.ts @@ -13,7 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { CDPSession } from './Connection.js'; +import { ProtocolMapping } from 'devtools-protocol/types/protocol-mapping.js'; + +import { EventEmitter } from './EventEmitter.js'; import { Frame } from './FrameManager.js'; import { HTTPRequest } from './HTTPRequest.js'; import { SecurityDetails } from './SecurityDetails.js'; @@ -28,6 +30,13 @@ export interface RemoteAddress { port: number; } +interface CDPSession extends EventEmitter { + send( + method: T, + ...paramArgs: ProtocolMapping.Commands[T]['paramsType'] + ): Promise; +} + /** * The HTTPResponse class represents responses which are received by the * {@link Page} class. @@ -55,7 +64,8 @@ export class HTTPResponse { constructor( client: CDPSession, request: HTTPRequest, - responsePayload: Protocol.Network.Response + responsePayload: Protocol.Network.Response, + extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null ) { this._client = client; this._request = request; @@ -68,13 +78,17 @@ export class HTTPResponse { ip: responsePayload.remoteIPAddress, port: responsePayload.remotePort, }; - this._status = responsePayload.status; + // TODO extract statusText from extraInfo.headersText instead if present this._statusText = responsePayload.statusText; this._url = request.url(); this._fromDiskCache = !!responsePayload.fromDiskCache; this._fromServiceWorker = !!responsePayload.fromServiceWorker; - for (const key of Object.keys(responsePayload.headers)) - this._headers[key.toLowerCase()] = responsePayload.headers[key]; + + this._status = extraInfo ? extraInfo.statusCode : responsePayload.status; + const headers = extraInfo ? extraInfo.headers : responsePayload.headers; + for (const key of Object.keys(headers)) + this._headers[key.toLowerCase()] = headers[key]; + this._securityDetails = responsePayload.securityDetails ? new SecurityDetails(responsePayload.securityDetails) : null; diff --git a/src/common/LifecycleWatcher.ts b/src/common/LifecycleWatcher.ts index 6e45a44217331..0ab639dfee212 100644 --- a/src/common/LifecycleWatcher.ts +++ b/src/common/LifecycleWatcher.ts @@ -171,7 +171,8 @@ export class LifecycleWatcher { this._checkLifecycleComplete(); } - navigationResponse(): HTTPResponse | null { + async navigationResponse(): Promise { + // We may need to wait for ExtraInfo events before the request is complete. return this._navigationRequest ? this._navigationRequest.response() : null; } diff --git a/src/common/NetworkManager.ts b/src/common/NetworkManager.ts index a3cfb81306aa6..1b048c01fa564 100644 --- a/src/common/NetworkManager.ts +++ b/src/common/NetworkManager.ts @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import { ProtocolMapping } from 'devtools-protocol/types/protocol-mapping.js'; import { EventEmitter } from './EventEmitter.js'; +import { Frame } from './FrameManager.js'; import { assert } from './assert.js'; import { helper, debugError } from './helper.js'; import { Protocol } from 'devtools-protocol'; -import { CDPSession } from './Connection.js'; -import { FrameManager } from './FrameManager.js'; import { HTTPRequest } from './HTTPRequest.js'; import { HTTPResponse } from './HTTPResponse.js'; @@ -62,6 +63,17 @@ export const NetworkManagerEmittedEvents = { RequestFinished: Symbol('NetworkManager.RequestFinished'), } as const; +interface CDPSession extends EventEmitter { + send( + method: T, + ...paramArgs: ProtocolMapping.Commands[T]['paramsType'] + ): Promise; +} + +interface FrameManager { + frame(frameId: string): Frame | null; +} + /** * @internal */ @@ -110,6 +122,37 @@ export class NetworkManager extends EventEmitter { >(); _requestIdToRequest = new Map(); + /* + * The below maps are used to reconcile Network.responseReceivedExtraInfo + * events with their corresponding request. Each response and redirect + * response gets an ExtraInfo event, and we don't know which will come first. + * This means that we have to store a Response or an ExtraInfo for each + * response, and emit the event when we get both of them. In addition, to + * handle redirects, we have to make them Arrays to represent the chain of + * events. + */ + _requestIdToResponseReceivedExtraInfo = new Map< + string, + Protocol.Network.ResponseReceivedExtraInfoEvent[] + >(); + _requestIdToQueuedRedirectInfoMap = new Map< + string, + Array<{ + event: Protocol.Network.RequestWillBeSentEvent; + interceptionId?: string; + }> + >(); + _requestIdToQueuedEvents = new Map< + string, + { + responseReceived: Protocol.Network.ResponseReceivedEvent; + promise: Promise; + resolver: () => void; + loadingFinished?: Protocol.Network.LoadingFinishedEvent; + loadingFailed?: Protocol.Network.LoadingFailedEvent; + } + >(); + _extraHTTPHeaders: Record = {}; _credentials?: Credentials = null; _attemptedAuthentications = new Set(); @@ -152,6 +195,10 @@ export class NetworkManager extends EventEmitter { this._onLoadingFinished.bind(this) ); this._client.on('Network.loadingFailed', this._onLoadingFailed.bind(this)); + this._client.on( + 'Network.responseReceivedExtraInfo', + this._onResponseReceivedExtraInfo.bind(this) + ); } async initialize(): Promise { @@ -361,17 +408,61 @@ export class NetworkManager extends EventEmitter { } } + _requestIdToQueuedRedirectInfo(requestId: string): Array<{ + event: Protocol.Network.RequestWillBeSentEvent; + interceptionId?: string; + }> { + if (!this._requestIdToQueuedRedirectInfoMap.has(requestId)) { + this._requestIdToQueuedRedirectInfoMap.set(requestId, []); + } + return this._requestIdToQueuedRedirectInfoMap.get(requestId); + } + + _requestIdToResponseExtraInfo( + requestId: string + ): Protocol.Network.ResponseReceivedExtraInfoEvent[] { + if (!this._requestIdToResponseReceivedExtraInfo.has(requestId)) { + this._requestIdToResponseReceivedExtraInfo.set(requestId, []); + } + return this._requestIdToResponseReceivedExtraInfo.get(requestId); + } + _onRequest( event: Protocol.Network.RequestWillBeSentEvent, interceptionId?: string ): void { let redirectChain = []; if (event.redirectResponse) { + // We want to emit a response and requestfinished for the + // redirectResponse, but we can't do so unless we have a + // responseExtraInfo ready to pair it up with. If we don't have any + // responseExtraInfos saved in our queue, they we have to wait until + // the next one to emit response and requestfinished, *and* we should + // also wait to emit this Request too because it should come after the + // response/requestfinished. + let redirectResponseExtraInfo = null; + if (event.redirectHasExtraInfo) { + redirectResponseExtraInfo = this._requestIdToResponseExtraInfo( + event.requestId + ).shift(); + if (!redirectResponseExtraInfo) { + this._requestIdToQueuedRedirectInfo(event.requestId).push({ + event, + interceptionId, + }); + return; + } + } + const request = this._requestIdToRequest.get(event.requestId); // If we connect late to the target, we could have missed the // requestWillBeSent event. if (request) { - this._handleRequestRedirect(request, event.redirectResponse); + this._handleRequestRedirect( + request, + event.redirectResponse, + redirectResponseExtraInfo + ); redirectChain = request._redirectChain; } } @@ -401,9 +492,15 @@ export class NetworkManager extends EventEmitter { _handleRequestRedirect( request: HTTPRequest, - responsePayload: Protocol.Network.Response + responsePayload: Protocol.Network.Response, + extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent ): void { - const response = new HTTPResponse(this._client, request, responsePayload); + const response = new HTTPResponse( + this._client, + request, + responsePayload, + extraInfo + ); request._response = response; request._redirectChain.push(request); response._resolveBody( @@ -414,15 +511,93 @@ export class NetworkManager extends EventEmitter { this.emit(NetworkManagerEmittedEvents.RequestFinished, request); } - _onResponseReceived(event: Protocol.Network.ResponseReceivedEvent): void { - const request = this._requestIdToRequest.get(event.requestId); + _emitResponseEvent( + responseReceived: Protocol.Network.ResponseReceivedEvent, + extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent + ): void { + const request = this._requestIdToRequest.get(responseReceived.requestId); // FileUpload sends a response without a matching request. if (!request) return; - const response = new HTTPResponse(this._client, request, event.response); + + const extraInfos = this._requestIdToResponseExtraInfo( + responseReceived.requestId + ); + if (extraInfos.length) { + throw new Error( + 'Unexpected extraInfo events for request ' + responseReceived.requestId + ); + } + + const response = new HTTPResponse( + this._client, + request, + responseReceived.response, + extraInfo + ); request._response = response; this.emit(NetworkManagerEmittedEvents.Response, response); } + _onResponseReceived(event: Protocol.Network.ResponseReceivedEvent): void { + const request = this._requestIdToRequest.get(event.requestId); + let extraInfo = null; + if (request && !request._fromMemoryCache && event.hasExtraInfo) { + extraInfo = this._requestIdToResponseExtraInfo(event.requestId).shift(); + if (!extraInfo) { + // Wait until we get the corresponding ExtraInfo event. + let resolver = null; + const promise = new Promise((resolve) => (resolver = resolve)); + this._requestIdToQueuedEvents.set(event.requestId, { + responseReceived: event, + promise, + resolver, + }); + return; + } + } + this._emitResponseEvent(event, extraInfo); + } + + responseWaitingForExtraInfoPromise(requestId: string): Promise { + const responseReceived = this._requestIdToQueuedEvents.get(requestId); + if (!responseReceived) return Promise.resolve(); + return responseReceived.promise; + } + + _onResponseReceivedExtraInfo( + event: Protocol.Network.ResponseReceivedExtraInfoEvent + ): void { + // We may have skipped a redirect response/request pair due to waiting for + // this ExtraInfo event. If so, continue that work now that we have the + // request. + const redirectInfo = this._requestIdToQueuedRedirectInfo( + event.requestId + ).shift(); + if (redirectInfo) { + this._requestIdToResponseExtraInfo(event.requestId).push(event); + this._onRequest(redirectInfo.event, redirectInfo.interceptionId); + return; + } + + // We may have skipped response and loading events because we didn't have + // this ExtraInfo event yet. If so, emit those events now. + const queuedEvents = this._requestIdToQueuedEvents.get(event.requestId); + if (queuedEvents) { + this._emitResponseEvent(queuedEvents.responseReceived, event); + if (queuedEvents.loadingFinished) { + this._emitLoadingFinished(queuedEvents.loadingFinished); + } + if (queuedEvents.loadingFailed) { + this._emitLoadingFailed(queuedEvents.loadingFailed); + } + queuedEvents.resolver(); + return; + } + + // Wait until we get another event that can use this ExtraInfo event. + this._requestIdToResponseExtraInfo(event.requestId).push(event); + } + _forgetRequest(request: HTTPRequest, events: boolean): void { const requestId = request._requestId; const interceptionId = request._interceptionId; @@ -433,10 +608,24 @@ export class NetworkManager extends EventEmitter { if (events) { this._requestIdToRequestWillBeSentEvent.delete(requestId); this._requestIdToRequestPausedEvent.delete(requestId); + this._requestIdToQueuedEvents.delete(requestId); + this._requestIdToQueuedRedirectInfoMap.delete(requestId); + this._requestIdToResponseReceivedExtraInfo.delete(requestId); } } _onLoadingFinished(event: Protocol.Network.LoadingFinishedEvent): void { + // If the response event for this request is still waiting on a + // corresponding ExtraInfo event, then wait to emit this event too. + const queuedEvents = this._requestIdToQueuedEvents.get(event.requestId); + if (queuedEvents) { + queuedEvents.loadingFinished = event; + } else { + this._emitLoadingFinished(event); + } + } + + _emitLoadingFinished(event: Protocol.Network.LoadingFinishedEvent): void { const request = this._requestIdToRequest.get(event.requestId); // For certain requestIds we never receive requestWillBeSent event. // @see https://crbug.com/750469 @@ -450,6 +639,17 @@ export class NetworkManager extends EventEmitter { } _onLoadingFailed(event: Protocol.Network.LoadingFailedEvent): void { + // If the response event for this request is still waiting on a + // corresponding ExtraInfo event, then wait to emit this event too. + const queuedEvents = this._requestIdToQueuedEvents.get(event.requestId); + if (queuedEvents) { + queuedEvents.loadingFailed = event; + } else { + this._emitLoadingFailed(event); + } + } + + _emitLoadingFailed(event: Protocol.Network.LoadingFailedEvent): void { const request = this._requestIdToRequest.get(event.requestId); // For certain requestIds we never receive requestWillBeSent event. // @see https://crbug.com/750469 diff --git a/src/revisions.ts b/src/revisions.ts index 5c4694ea39032..e76f7a84f3531 100644 --- a/src/revisions.ts +++ b/src/revisions.ts @@ -20,6 +20,6 @@ type Revisions = Readonly<{ }>; export const PUPPETEER_REVISIONS: Revisions = { - chromium: '901912', + chromium: '938248', firefox: 'latest', }; diff --git a/test/NetworkManager.spec.ts b/test/NetworkManager.spec.ts new file mode 100644 index 0000000000000..a934c80a25221 --- /dev/null +++ b/test/NetworkManager.spec.ts @@ -0,0 +1,459 @@ +/** + * Copyright 2020 Google Inc. All rights reserved. + * + * 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 { describeChromeOnly } from './mocha-utils'; // eslint-disable-line import/extensions + +import { NetworkManager } from '../lib/cjs/puppeteer/common/NetworkManager.js'; +import { EventEmitter } from '../lib/cjs/puppeteer/common/EventEmitter.js'; +import { Frame } from '../lib/cjs/puppeteer/common/FrameManager.js'; + +describeChromeOnly('NetworkManager', () => { + it('should process extra info on multiple redirects', async () => { + class MockCDPSession extends EventEmitter { + send(): any {} + } + + const mockCDPSession = new MockCDPSession(); + new NetworkManager(mockCDPSession, true, { + frame(): Frame | null { + return null; + }, + }); + mockCDPSession.emit('Network.requestWillBeSent', { + requestId: '7760711DEFCFA23132D98ABA6B4E175C', + loaderId: '7760711DEFCFA23132D98ABA6B4E175C', + documentURL: 'http://localhost:8907/redirect/1.html', + request: { + url: 'http://localhost:8907/redirect/1.html', + method: 'GET', + headers: { + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/97.0.4691.0 Safari/537.36', + }, + mixedContentType: 'none', + initialPriority: 'VeryHigh', + referrerPolicy: 'strict-origin-when-cross-origin', + isSameSite: true, + }, + timestamp: 2111.55635, + wallTime: 1637315638.473634, + initiator: { type: 'other' }, + redirectHasExtraInfo: false, + type: 'Document', + frameId: '099A5216AF03AAFEC988F214B024DF08', + hasUserGesture: false, + }); + + mockCDPSession.emit('Network.requestWillBeSentExtraInfo', { + requestId: '7760711DEFCFA23132D98ABA6B4E175C', + associatedCookies: [], + headers: { + Host: 'localhost:8907', + Connection: 'keep-alive', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/97.0.4691.0 Safari/537.36', + 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', + 'Sec-Fetch-Site': 'none', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-User': '?1', + 'Sec-Fetch-Dest': 'document', + 'Accept-Encoding': 'gzip, deflate, br', + }, + connectTiming: { requestTime: 2111.557593 }, + }); + mockCDPSession.emit('Network.responseReceivedExtraInfo', { + requestId: '7760711DEFCFA23132D98ABA6B4E175C', + blockedCookies: [], + headers: { + location: '/redirect/2.html', + Date: 'Fri, 19 Nov 2021 09:53:58 GMT', + Connection: 'keep-alive', + 'Keep-Alive': 'timeout=5', + 'Transfer-Encoding': 'chunked', + }, + resourceIPAddressSpace: 'Local', + statusCode: 302, + headersText: + 'HTTP/1.1 302 Found\r\nlocation: /redirect/2.html\r\nDate: Fri, 19 Nov 2021 09:53:58 GMT\r\nConnection: keep-alive\r\nKeep-Alive: timeout=5\r\nTransfer-Encoding: chunked\r\n\r\n', + }); + mockCDPSession.emit('Network.requestWillBeSent', { + requestId: '7760711DEFCFA23132D98ABA6B4E175C', + loaderId: '7760711DEFCFA23132D98ABA6B4E175C', + documentURL: 'http://localhost:8907/redirect/2.html', + request: { + url: 'http://localhost:8907/redirect/2.html', + method: 'GET', + headers: { + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/97.0.4691.0 Safari/537.36', + }, + mixedContentType: 'none', + initialPriority: 'VeryHigh', + referrerPolicy: 'strict-origin-when-cross-origin', + isSameSite: true, + }, + timestamp: 2111.559124, + wallTime: 1637315638.47642, + initiator: { type: 'other' }, + redirectHasExtraInfo: true, + redirectResponse: { + url: 'http://localhost:8907/redirect/1.html', + status: 302, + statusText: 'Found', + headers: { + location: '/redirect/2.html', + Date: 'Fri, 19 Nov 2021 09:53:58 GMT', + Connection: 'keep-alive', + 'Keep-Alive': 'timeout=5', + 'Transfer-Encoding': 'chunked', + }, + mimeType: '', + connectionReused: false, + connectionId: 322, + remoteIPAddress: '[::1]', + remotePort: 8907, + fromDiskCache: false, + fromServiceWorker: false, + fromPrefetchCache: false, + encodedDataLength: 162, + timing: { + requestTime: 2111.557593, + proxyStart: -1, + proxyEnd: -1, + dnsStart: 0.241, + dnsEnd: 0.251, + connectStart: 0.251, + connectEnd: 0.47, + sslStart: -1, + sslEnd: -1, + workerStart: -1, + workerReady: -1, + workerFetchStart: -1, + workerRespondWithSettled: -1, + sendStart: 0.537, + sendEnd: 0.611, + pushStart: 0, + pushEnd: 0, + receiveHeadersEnd: 0.939, + }, + responseTime: 1.637315638475744e12, + protocol: 'http/1.1', + securityState: 'secure', + }, + type: 'Document', + frameId: '099A5216AF03AAFEC988F214B024DF08', + hasUserGesture: false, + }); + mockCDPSession.emit('Network.requestWillBeSentExtraInfo', { + requestId: '7760711DEFCFA23132D98ABA6B4E175C', + associatedCookies: [], + headers: { + Host: 'localhost:8907', + Connection: 'keep-alive', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/97.0.4691.0 Safari/537.36', + 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', + 'Sec-Fetch-Site': 'none', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-User': '?1', + 'Sec-Fetch-Dest': 'document', + 'Accept-Encoding': 'gzip, deflate, br', + }, + connectTiming: { requestTime: 2111.559346 }, + }); + mockCDPSession.emit('Network.requestWillBeSent', { + requestId: '7760711DEFCFA23132D98ABA6B4E175C', + loaderId: '7760711DEFCFA23132D98ABA6B4E175C', + documentURL: 'http://localhost:8907/redirect/3.html', + request: { + url: 'http://localhost:8907/redirect/3.html', + method: 'GET', + headers: { + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/97.0.4691.0 Safari/537.36', + }, + mixedContentType: 'none', + initialPriority: 'VeryHigh', + referrerPolicy: 'strict-origin-when-cross-origin', + isSameSite: true, + }, + timestamp: 2111.560249, + wallTime: 1637315638.477543, + initiator: { type: 'other' }, + redirectHasExtraInfo: true, + redirectResponse: { + url: 'http://localhost:8907/redirect/2.html', + status: 302, + statusText: 'Found', + headers: { + location: '/redirect/3.html', + Date: 'Fri, 19 Nov 2021 09:53:58 GMT', + Connection: 'keep-alive', + 'Keep-Alive': 'timeout=5', + 'Transfer-Encoding': 'chunked', + }, + mimeType: '', + connectionReused: true, + connectionId: 322, + remoteIPAddress: '[::1]', + remotePort: 8907, + fromDiskCache: false, + fromServiceWorker: false, + fromPrefetchCache: false, + encodedDataLength: 162, + timing: { + requestTime: 2111.559346, + 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.15, + sendEnd: 0.196, + pushStart: 0, + pushEnd: 0, + receiveHeadersEnd: 0.507, + }, + responseTime: 1.637315638477063e12, + protocol: 'http/1.1', + securityState: 'secure', + }, + type: 'Document', + frameId: '099A5216AF03AAFEC988F214B024DF08', + hasUserGesture: false, + }); + mockCDPSession.emit('Network.responseReceivedExtraInfo', { + requestId: '7760711DEFCFA23132D98ABA6B4E175C', + blockedCookies: [], + headers: { + location: '/redirect/3.html', + Date: 'Fri, 19 Nov 2021 09:53:58 GMT', + Connection: 'keep-alive', + 'Keep-Alive': 'timeout=5', + 'Transfer-Encoding': 'chunked', + }, + resourceIPAddressSpace: 'Local', + statusCode: 302, + headersText: + 'HTTP/1.1 302 Found\r\nlocation: /redirect/3.html\r\nDate: Fri, 19 Nov 2021 09:53:58 GMT\r\nConnection: keep-alive\r\nKeep-Alive: timeout=5\r\nTransfer-Encoding: chunked\r\n\r\n', + }); + mockCDPSession.emit('Network.requestWillBeSentExtraInfo', { + requestId: '7760711DEFCFA23132D98ABA6B4E175C', + associatedCookies: [], + headers: { + Host: 'localhost:8907', + Connection: 'keep-alive', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/97.0.4691.0 Safari/537.36', + 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', + 'Sec-Fetch-Site': 'none', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-User': '?1', + 'Sec-Fetch-Dest': 'document', + 'Accept-Encoding': 'gzip, deflate, br', + }, + connectTiming: { requestTime: 2111.560482 }, + }); + mockCDPSession.emit('Network.requestWillBeSent', { + requestId: '7760711DEFCFA23132D98ABA6B4E175C', + loaderId: '7760711DEFCFA23132D98ABA6B4E175C', + documentURL: 'http://localhost:8907/empty.html', + request: { + url: 'http://localhost:8907/empty.html', + method: 'GET', + headers: { + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/97.0.4691.0 Safari/537.36', + }, + mixedContentType: 'none', + initialPriority: 'VeryHigh', + referrerPolicy: 'strict-origin-when-cross-origin', + isSameSite: true, + }, + timestamp: 2111.561542, + wallTime: 1637315638.478837, + initiator: { type: 'other' }, + redirectHasExtraInfo: true, + redirectResponse: { + url: 'http://localhost:8907/redirect/3.html', + status: 302, + statusText: 'Found', + headers: { + location: 'http://localhost:8907/empty.html', + Date: 'Fri, 19 Nov 2021 09:53:58 GMT', + Connection: 'keep-alive', + 'Keep-Alive': 'timeout=5', + 'Transfer-Encoding': 'chunked', + }, + mimeType: '', + connectionReused: true, + connectionId: 322, + remoteIPAddress: '[::1]', + remotePort: 8907, + fromDiskCache: false, + fromServiceWorker: false, + fromPrefetchCache: false, + encodedDataLength: 178, + timing: { + requestTime: 2111.560482, + 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.149, + sendEnd: 0.198, + pushStart: 0, + pushEnd: 0, + receiveHeadersEnd: 0.478, + }, + responseTime: 1.637315638478184e12, + protocol: 'http/1.1', + securityState: 'secure', + }, + type: 'Document', + frameId: '099A5216AF03AAFEC988F214B024DF08', + hasUserGesture: false, + }); + mockCDPSession.emit('Network.responseReceivedExtraInfo', { + requestId: '7760711DEFCFA23132D98ABA6B4E175C', + blockedCookies: [], + headers: { + location: 'http://localhost:8907/empty.html', + Date: 'Fri, 19 Nov 2021 09:53:58 GMT', + Connection: 'keep-alive', + 'Keep-Alive': 'timeout=5', + 'Transfer-Encoding': 'chunked', + }, + resourceIPAddressSpace: 'Local', + statusCode: 302, + headersText: + 'HTTP/1.1 302 Found\r\nlocation: http://localhost:8907/empty.html\r\nDate: Fri, 19 Nov 2021 09:53:58 GMT\r\nConnection: keep-alive\r\nKeep-Alive: timeout=5\r\nTransfer-Encoding: chunked\r\n\r\n', + }); + mockCDPSession.emit('Network.requestWillBeSentExtraInfo', { + requestId: '7760711DEFCFA23132D98ABA6B4E175C', + associatedCookies: [], + headers: { + Host: 'localhost:8907', + Connection: 'keep-alive', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/97.0.4691.0 Safari/537.36', + 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', + 'Sec-Fetch-Site': 'none', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-User': '?1', + 'Sec-Fetch-Dest': 'document', + 'Accept-Encoding': 'gzip, deflate, br', + }, + connectTiming: { requestTime: 2111.561759 }, + }); + mockCDPSession.emit('Network.responseReceivedExtraInfo', { + requestId: '7760711DEFCFA23132D98ABA6B4E175C', + blockedCookies: [], + headers: { + 'Cache-Control': 'no-cache, no-store', + 'Content-Type': 'text/html; charset=utf-8', + Date: 'Fri, 19 Nov 2021 09:53:58 GMT', + Connection: 'keep-alive', + 'Keep-Alive': 'timeout=5', + 'Content-Length': '0', + }, + resourceIPAddressSpace: 'Local', + 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: Fri, 19 Nov 2021 09:53:58 GMT\r\nConnection: keep-alive\r\nKeep-Alive: timeout=5\r\nContent-Length: 0\r\n\r\n', + }); + mockCDPSession.emit('Network.responseReceived', { + requestId: '7760711DEFCFA23132D98ABA6B4E175C', + loaderId: '7760711DEFCFA23132D98ABA6B4E175C', + timestamp: 2111.563565, + type: 'Document', + response: { + url: 'http://localhost:8907/empty.html', + status: 200, + statusText: 'OK', + headers: { + 'Cache-Control': 'no-cache, no-store', + 'Content-Type': 'text/html; charset=utf-8', + Date: 'Fri, 19 Nov 2021 09:53:58 GMT', + Connection: 'keep-alive', + 'Keep-Alive': 'timeout=5', + 'Content-Length': '0', + }, + mimeType: 'text/html', + connectionReused: true, + connectionId: 322, + remoteIPAddress: '[::1]', + remotePort: 8907, + fromDiskCache: false, + fromServiceWorker: false, + fromPrefetchCache: false, + encodedDataLength: 197, + timing: { + requestTime: 2111.561759, + 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.148, + sendEnd: 0.19, + pushStart: 0, + pushEnd: 0, + receiveHeadersEnd: 0.925, + }, + responseTime: 1.637315638479928e12, + protocol: 'http/1.1', + securityState: 'secure', + }, + hasExtraInfo: true, + frameId: '099A5216AF03AAFEC988F214B024DF08', + }); + }); +}); diff --git a/test/accessibility.spec.ts b/test/accessibility.spec.ts index 5198e255a9cff..6aa8b7cb5f350 100644 --- a/test/accessibility.spec.ts +++ b/test/accessibility.spec.ts @@ -168,7 +168,8 @@ describeFailsFirefox('Accessibility', function () { '
Hi
' ); const snapshot = await page.accessibility.snapshot(); - expect(snapshot.children[0].roledescription).toEqual('foo'); + // See https://chromium-review.googlesource.com/c/chromium/src/+/3088862 + expect(snapshot.children[0].roledescription).toEqual(undefined); }); it('orientation', async () => { const { page } = getTestState(); diff --git a/test/jshandle.spec.ts b/test/jshandle.spec.ts index a3f201aeb7f90..ae1c186e43097 100644 --- a/test/jshandle.spec.ts +++ b/test/jshandle.spec.ts @@ -21,6 +21,7 @@ import { setupTestBrowserHooks, setupTestPageAndContextHooks, itFailsFirefox, + shortWaitForArrayToHaveAtLeastNElements, } from './mocha-utils'; // eslint-disable-line import/extensions describe('JSHandle', function () { @@ -390,6 +391,7 @@ describe('JSHandle', function () { y: 15, }, }); + await shortWaitForArrayToHaveAtLeastNElements(clicks, 2); expect(clicks).toEqual([ [45 + 60, 45 + 30], // margin + middle point offset [30 + 10, 30 + 15], // margin + offset diff --git a/test/mocha-utils.ts b/test/mocha-utils.ts index 6c405ac052f1a..97909df98f092 100644 --- a/test/mocha-utils.ts +++ b/test/mocha-utils.ts @@ -319,3 +319,17 @@ export const expectCookieEquals = ( expect(cookies).toEqual(expectedCookies); }; + +export const shortWaitForArrayToHaveAtLeastNElements = async ( + data: unknown[], + minLength: number, + attempts = 3, + timeout = 50 +): Promise => { + for (let i = 0; i < attempts; i++) { + if (data.length >= minLength) { + break; + } + await new Promise((resolve) => setTimeout(resolve, timeout)); + } +}; diff --git a/test/network.spec.ts b/test/network.spec.ts index f5b875783a5eb..c73a043aa8698 100644 --- a/test/network.spec.ts +++ b/test/network.spec.ts @@ -686,4 +686,77 @@ describe('network', function () { expect(responses.get('one-style.html').fromCache()).toBe(false); }); }); + + describeFailsFirefox('raw network headers', async () => { + it('Same-origin set-cookie navigation', async () => { + const { page, server } = getTestState(); + + const setCookieString = 'foo=bar'; + server.setRoute('/empty.html', (req, res) => { + res.setHeader('set-cookie', setCookieString); + res.end('hello world'); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.headers()['set-cookie']).toBe(setCookieString); + }); + + it('Same-origin set-cookie subresource', async () => { + const { page, server } = getTestState(); + await page.goto(server.EMPTY_PAGE); + + const setCookieString = 'foo=bar'; + server.setRoute('/foo', (req, res) => { + res.setHeader('set-cookie', setCookieString); + res.end('hello world'); + }); + + const responsePromise = new Promise((resolve) => + page.on('response', (response) => resolve(response)) + ); + page.evaluate(() => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', '/foo'); + xhr.send(); + }); + const subresourceResponse = await responsePromise; + expect(subresourceResponse.headers()['set-cookie']).toBe(setCookieString); + }); + + it('Cross-origin set-cookie', async () => { + const { httpsServer, puppeteer, defaultBrowserOptions } = getTestState(); + + const browser = await puppeteer.launch({ + ...defaultBrowserOptions, + ignoreHTTPSErrors: true, + }); + + const page = await browser.newPage(); + + try { + await page.goto(httpsServer.PREFIX + '/empty.html'); + + const setCookieString = 'hello=world'; + httpsServer.setRoute('/setcookie.html', (req, res) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('set-cookie', setCookieString); + res.end(); + }); + await page.goto(httpsServer.PREFIX + '/setcookie.html'); + + const response = await new Promise((resolve) => { + page.on('response', resolve); + const url = httpsServer.CROSS_PROCESS_PREFIX + '/setcookie.html'; + page.evaluate<(src: string) => void>((src) => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', src); + xhr.send(); + }, url); + }); + expect(response.headers()['set-cookie']).toBe(setCookieString); + } finally { + await page.close(); + await browser.close(); + } + }); + }); }); diff --git a/versions.js b/versions.js index 27828bc886126..85eaace84b8ca 100644 --- a/versions.js +++ b/versions.js @@ -17,6 +17,7 @@ const versionsPerRelease = new Map([ // This is a mapping from Chromium version => Puppeteer version. // In Chromium roll patches, use 'NEXT' for the Puppeteer version. + ['97.0.4692.0', 'NEXT'], ['93.0.4577.0', 'v10.2.0'], ['92.0.4512.0', 'v10.0.0'], ['91.0.4469.0', 'v9.0.0'],