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 f9fc0964cd0e3..80722450a1473 100644 --- a/src/common/FrameManager.ts +++ b/src/common/FrameManager.ts @@ -449,7 +449,7 @@ export class FrameManager extends EventEmitter { } } const context = new ExecutionContext( - frame._client || this._client, + frame?._client || this._client, contextPayload, world ); diff --git a/src/common/HTTPResponse.ts b/src/common/HTTPResponse.ts index 9620c0cafee6e..cf8fe2a7b84b0 100644 --- a/src/common/HTTPResponse.ts +++ b/src/common/HTTPResponse.ts @@ -55,7 +55,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 +69,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/NetworkManager.ts b/src/common/NetworkManager.ts index a3cfb81306aa6..af93668284705 100644 --- a/src/common/NetworkManager.ts +++ b/src/common/NetworkManager.ts @@ -110,6 +110,31 @@ 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[] + >(); + _requestIdToResponseReceived = new Map< + string, + Protocol.Network.ResponseReceivedEvent + >(); + _requestIdToRedirectInfoMap = new Map< + string, + Array<{ + request: HTTPRequest; + response: Protocol.Network.Response; + }> + >(); + _extraHTTPHeaders: Record = {}; _credentials?: Credentials = null; _attemptedAuthentications = new Set(); @@ -152,6 +177,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,18 +390,82 @@ export class NetworkManager extends EventEmitter { } } + _requestIdToRedirectInfo(requestId: string): Array<{ + request: HTTPRequest; + response: Protocol.Network.Response; + }> { + if (!this._requestIdToRedirectInfoMap.has(requestId)) { + this._requestIdToRedirectInfoMap.set(requestId, []); + } + return this._requestIdToRedirectInfoMap.get(requestId); + } + + _requestIdToResponseExtraInfo( + requestId: string + ): Protocol.Network.ResponseReceivedExtraInfoEvent[] { + if (!this._requestIdToResponseReceivedExtraInfo.has(requestId)) { + this._requestIdToResponseReceivedExtraInfo.set(requestId, []); + } + return this._requestIdToResponseReceivedExtraInfo.get(requestId); + } + + _emitRedirectResponse( + request: HTTPRequest, + responsePayload: Protocol.Network.Response, + extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent + ): void { + const response = new HTTPResponse( + this._client, + request, + responsePayload, + extraInfo + ); + request._response = response; + request._redirectChain.push(request); + response._resolveBody( + new Error('Response body is unavailable for redirect responses') + ); + this._forgetRequest(request, false); + this.emit(NetworkManagerEmittedEvents.Response, response); + this.emit(NetworkManagerEmittedEvents.RequestFinished, request); + } + + _handleRequestWithRedirect( + event: Protocol.Network.RequestWillBeSentEvent + ): void { + const lastRequest = this._requestIdToRequest.get(event.requestId); + // If we connect late to the target, we could have missed the + // requestWillBeSent event. + if (!lastRequest) return; + + let extraInfo = null; + if (event.redirectHasExtraInfo) { + extraInfo = this._requestIdToResponseExtraInfo(event.requestId).shift(); + if (!extraInfo) { + // Wait for the corresponding ExtraInfo event before emitting the response. + this._requestIdToRedirectInfo(event.requestId).push({ + request: lastRequest, + response: event.redirectResponse, + }); + return; + } + } + + this._emitRedirectResponse(lastRequest, event.redirectResponse, null); + } + _onRequest( event: Protocol.Network.RequestWillBeSentEvent, interceptionId?: string ): void { let redirectChain = []; if (event.redirectResponse) { - const request = this._requestIdToRequest.get(event.requestId); + const lastRequest = 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); - redirectChain = request._redirectChain; + if (lastRequest) { + this._handleRequestWithRedirect(event); + redirectChain = lastRequest._redirectChain; } } const frame = event.frameId @@ -399,28 +492,61 @@ export class NetworkManager extends EventEmitter { this.emit(NetworkManagerEmittedEvents.RequestServedFromCache, request); } - _handleRequestRedirect( - request: HTTPRequest, - responsePayload: Protocol.Network.Response + _emitResponseEvent( + responseReceived: Protocol.Network.ResponseReceivedEvent, + extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent ): void { - const response = new HTTPResponse(this._client, request, responsePayload); - request._response = response; - request._redirectChain.push(request); - response._resolveBody( - new Error('Response body is unavailable for redirect responses') + 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, + responseReceived.response, + extraInfo ); - this._forgetRequest(request, false); + request._response = response; this.emit(NetworkManagerEmittedEvents.Response, response); - this.emit(NetworkManagerEmittedEvents.RequestFinished, request); } _onResponseReceived(event: Protocol.Network.ResponseReceivedEvent): void { const request = this._requestIdToRequest.get(event.requestId); - // FileUpload sends a response without a matching request. - if (!request) return; - const response = new HTTPResponse(this._client, request, event.response); - request._response = response; - this.emit(NetworkManagerEmittedEvents.Response, response); + 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. + this._requestIdToResponseReceived.set(event.requestId, event); + return; + } + } + this._emitResponseEvent(event, extraInfo); + } + + _onResponseReceivedExtraInfo( + event: Protocol.Network.ResponseReceivedExtraInfoEvent + ): void { + const redirectInfo = this._requestIdToRedirectInfo(event.requestId).shift(); + if (redirectInfo) { + this._emitRedirectResponse( + redirectInfo.request, + redirectInfo.response, + event + ); + return; + } + + const responseReceived = this._requestIdToResponseReceived.get( + event.requestId + ); + if (responseReceived) { + this._emitResponseEvent(responseReceived, event); + return; + } + + // Wait until we get a corresponding response event. + this._requestIdToResponseExtraInfo(event.requestId).push(event); } _forgetRequest(request: HTTPRequest, events: boolean): void { @@ -433,6 +559,9 @@ export class NetworkManager extends EventEmitter { if (events) { this._requestIdToRequestWillBeSentEvent.delete(requestId); this._requestIdToRequestPausedEvent.delete(requestId); + this._requestIdToResponseReceived.delete(requestId); + this._requestIdToResponseReceivedExtraInfo.delete(requestId); + this._requestIdToRedirectInfoMap.delete(requestId); } } 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/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..0bb305595315a 100644 --- a/test/jshandle.spec.ts +++ b/test/jshandle.spec.ts @@ -390,6 +390,12 @@ describe('JSHandle', function () { y: 15, }, }); + for (let i = 0; i < 3; i++) { + if (clicks.length >= 2) { + break; + } + await new Promise((resolve) => setTimeout(resolve, 100)); + } expect(clicks).toEqual([ [45 + 60, 45 + 30], // margin + middle point offset [30 + 10, 30 + 15], // margin + offset diff --git a/test/network.spec.ts b/test/network.spec.ts index f5b875783a5eb..e811745931d2f 100644 --- a/test/network.spec.ts +++ b/test/network.spec.ts @@ -529,6 +529,12 @@ describe('network', function () { server.setRedirect('/foo.html', '/empty.html'); const FOO_URL = server.PREFIX + '/foo.html'; const response = await page.goto(FOO_URL); + for (let i = 0; i < 3; i++) { + if (events.length >= 6) { + break; + } + await new Promise((resolve) => setTimeout(resolve, 100)); + } expect(events).toEqual([ `GET ${FOO_URL}`, `302 ${FOO_URL}`, @@ -686,4 +692,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'],