parent
4c3caaa3f9
commit
ac162c561e
@ -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",
|
||||
|
@ -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(
|
||||
|
@ -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<Protocol.Network.ResourceType>;
|
||||
|
||||
interface CDPSession extends EventEmitter {
|
||||
send<T extends keyof ProtocolMapping.Commands>(
|
||||
method: T,
|
||||
...paramArgs: ProtocolMapping.Commands[T]['paramsType']
|
||||
): Promise<ProtocolMapping.Commands[T]['returnType']>;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Represents an HTTP request sent by a page.
|
||||
|
@ -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<T extends keyof ProtocolMapping.Commands>(
|
||||
method: T,
|
||||
...paramArgs: ProtocolMapping.Commands[T]['paramsType']
|
||||
): Promise<ProtocolMapping.Commands[T]['returnType']>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
@ -171,7 +171,8 @@ export class LifecycleWatcher {
|
||||
this._checkLifecycleComplete();
|
||||
}
|
||||
|
||||
navigationResponse(): HTTPResponse | null {
|
||||
async navigationResponse(): Promise<HTTPResponse | null> {
|
||||
// We may need to wait for ExtraInfo events before the request is complete.
|
||||
return this._navigationRequest ? this._navigationRequest.response() : null;
|
||||
}
|
||||
|
||||
|
@ -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<T extends keyof ProtocolMapping.Commands>(
|
||||
method: T,
|
||||
...paramArgs: ProtocolMapping.Commands[T]['paramsType']
|
||||
): Promise<ProtocolMapping.Commands[T]['returnType']>;
|
||||
}
|
||||
|
||||
interface FrameManager {
|
||||
frame(frameId: string): Frame | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@ -110,6 +122,37 @@ export class NetworkManager extends EventEmitter {
|
||||
>();
|
||||
_requestIdToRequest = new Map<string, HTTPRequest>();
|
||||
|
||||
/*
|
||||
* 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<void>;
|
||||
resolver: () => void;
|
||||
loadingFinished?: Protocol.Network.LoadingFinishedEvent;
|
||||
loadingFailed?: Protocol.Network.LoadingFailedEvent;
|
||||
}
|
||||
>();
|
||||
|
||||
_extraHTTPHeaders: Record<string, string> = {};
|
||||
_credentials?: Credentials = null;
|
||||
_attemptedAuthentications = new Set<string>();
|
||||
@ -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<void> {
|
||||
@ -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<void>((resolve) => (resolver = resolve));
|
||||
this._requestIdToQueuedEvents.set(event.requestId, {
|
||||
responseReceived: event,
|
||||
promise,
|
||||
resolver,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._emitResponseEvent(event, extraInfo);
|
||||
}
|
||||
|
||||
responseWaitingForExtraInfoPromise(requestId: string): Promise<void> {
|
||||
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
|
||||
|
@ -20,6 +20,6 @@ type Revisions = Readonly<{
|
||||
}>;
|
||||
|
||||
export const PUPPETEER_REVISIONS: Revisions = {
|
||||
chromium: '901912',
|
||||
chromium: '938248',
|
||||
firefox: 'latest',
|
||||
};
|
||||
|
459
test/NetworkManager.spec.ts
Normal file
459
test/NetworkManager.spec.ts
Normal file
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
@ -168,7 +168,8 @@ describeFailsFirefox('Accessibility', function () {
|
||||
'<div tabIndex=-1 aria-roledescription="foo">Hi</div>'
|
||||
);
|
||||
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();
|
||||
|
@ -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
|
||||
|
@ -319,3 +319,17 @@ export const expectCookieEquals = (
|
||||
|
||||
expect(cookies).toEqual(expectedCookies);
|
||||
};
|
||||
|
||||
export const shortWaitForArrayToHaveAtLeastNElements = async (
|
||||
data: unknown[],
|
||||
minLength: number,
|
||||
attempts = 3,
|
||||
timeout = 50
|
||||
): Promise<void> => {
|
||||
for (let i = 0; i < attempts; i++) {
|
||||
if (data.length >= minLength) {
|
||||
break;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, timeout));
|
||||
}
|
||||
};
|
||||
|
@ -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<HTTPResponse>((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<HTTPResponse>((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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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'],
|
||||
|
Loading…
Reference in New Issue
Block a user