chore: reuse network Request and Response between browsers (#101)
This commit is contained in:
parent
490db5bc18
commit
b2c31b7317
|
|
@ -20,16 +20,15 @@ import { helper } from '../helper';
|
||||||
import { valueFromRemoteObject, getExceptionMessage, releaseObject } from './protocolHelper';
|
import { valueFromRemoteObject, getExceptionMessage, releaseObject } from './protocolHelper';
|
||||||
import { createJSHandle, ElementHandle } from './JSHandle';
|
import { createJSHandle, ElementHandle } from './JSHandle';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import { Response } from './NetworkManager';
|
|
||||||
import * as js from '../javascript';
|
import * as js from '../javascript';
|
||||||
|
|
||||||
export const EVALUATION_SCRIPT_URL = '__playwright_evaluation_script__';
|
export const EVALUATION_SCRIPT_URL = '__playwright_evaluation_script__';
|
||||||
const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
|
const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
|
||||||
|
|
||||||
export type ExecutionContext = js.ExecutionContext<ElementHandle, Response>;
|
export type ExecutionContext = js.ExecutionContext<ElementHandle>;
|
||||||
export type JSHandle = js.JSHandle<ElementHandle, Response>;
|
export type JSHandle = js.JSHandle<ElementHandle>;
|
||||||
|
|
||||||
export class ExecutionContextDelegate implements js.ExecutionContextDelegate<ElementHandle, Response> {
|
export class ExecutionContextDelegate implements js.ExecutionContextDelegate<ElementHandle> {
|
||||||
_client: CDPSession;
|
_client: CDPSession;
|
||||||
_contextId: number;
|
_contextId: number;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,9 +45,9 @@ type FrameData = {
|
||||||
lifecycleEvents: Set<string>,
|
lifecycleEvents: Set<string>,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Frame = frames.Frame<ElementHandle, Response>;
|
export type Frame = frames.Frame<ElementHandle>;
|
||||||
|
|
||||||
export class FrameManager extends EventEmitter implements frames.FrameDelegate<ElementHandle, Response> {
|
export class FrameManager extends EventEmitter implements frames.FrameDelegate<ElementHandle> {
|
||||||
_client: CDPSession;
|
_client: CDPSession;
|
||||||
private _page: Page;
|
private _page: Page;
|
||||||
private _networkManager: NetworkManager;
|
private _networkManager: NetworkManager;
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ import { FrameManager } from './FrameManager';
|
||||||
import { Page } from './Page';
|
import { Page } from './Page';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import { JSHandle, ExecutionContext, ExecutionContextDelegate, markJSHandle } from './ExecutionContext';
|
import { JSHandle, ExecutionContext, ExecutionContextDelegate, markJSHandle } from './ExecutionContext';
|
||||||
import { Response } from './NetworkManager';
|
|
||||||
|
|
||||||
type SelectorRoot = Element | ShadowRoot | Document;
|
type SelectorRoot = Element | ShadowRoot | Document;
|
||||||
|
|
||||||
|
|
@ -47,7 +46,7 @@ export function createJSHandle(context: ExecutionContext, remoteObject: Protocol
|
||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ElementHandle extends js.JSHandle<ElementHandle, Response> {
|
export class ElementHandle extends js.JSHandle<ElementHandle> {
|
||||||
private _client: CDPSession;
|
private _client: CDPSession;
|
||||||
private _remoteObject: Protocol.Runtime.RemoteObject;
|
private _remoteObject: Protocol.Runtime.RemoteObject;
|
||||||
private _page: Page;
|
private _page: Page;
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ import { Frame } from './FrameManager';
|
||||||
import { FrameManager } from './FrameManager';
|
import { FrameManager } from './FrameManager';
|
||||||
import { assert, debugError, helper } from '../helper';
|
import { assert, debugError, helper } from '../helper';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
|
import * as network from '../network';
|
||||||
|
import { ElementHandle } from './JSHandle';
|
||||||
|
|
||||||
export const NetworkManagerEvents = {
|
export const NetworkManagerEvents = {
|
||||||
Request: Symbol('Events.NetworkManager.Request'),
|
Request: Symbol('Events.NetworkManager.Request'),
|
||||||
|
|
@ -29,13 +31,16 @@ export const NetworkManagerEvents = {
|
||||||
RequestFinished: Symbol('Events.NetworkManager.RequestFinished'),
|
RequestFinished: Symbol('Events.NetworkManager.RequestFinished'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Request = network.Request<ElementHandle>;
|
||||||
|
export type Response = network.Response<ElementHandle>;
|
||||||
|
|
||||||
export class NetworkManager extends EventEmitter {
|
export class NetworkManager extends EventEmitter {
|
||||||
private _client: CDPSession;
|
private _client: CDPSession;
|
||||||
private _ignoreHTTPSErrors: boolean;
|
private _ignoreHTTPSErrors: boolean;
|
||||||
private _frameManager: FrameManager;
|
private _frameManager: FrameManager;
|
||||||
private _requestIdToRequest = new Map<string, Request>();
|
private _requestIdToRequest = new Map<string, InterceptableRequest>();
|
||||||
private _requestIdToRequestWillBeSentEvent = new Map<string, Protocol.Network.requestWillBeSentPayload>();
|
private _requestIdToRequestWillBeSentEvent = new Map<string, Protocol.Network.requestWillBeSentPayload>();
|
||||||
private _extraHTTPHeaders: {[key: string]: string} = {};
|
private _extraHTTPHeaders: network.Headers = {};
|
||||||
private _offline = false;
|
private _offline = false;
|
||||||
private _credentials: {username: string, password: string} | null = null;
|
private _credentials: {username: string, password: string} | null = null;
|
||||||
private _attemptedAuthentications = new Set<string>();
|
private _attemptedAuthentications = new Set<string>();
|
||||||
|
|
@ -69,7 +74,7 @@ export class NetworkManager extends EventEmitter {
|
||||||
await this._updateProtocolRequestInterception();
|
await this._updateProtocolRequestInterception();
|
||||||
}
|
}
|
||||||
|
|
||||||
async setExtraHTTPHeaders(extraHTTPHeaders: { [s: string]: string; }) {
|
async setExtraHTTPHeaders(extraHTTPHeaders: network.Headers) {
|
||||||
this._extraHTTPHeaders = {};
|
this._extraHTTPHeaders = {};
|
||||||
for (const key of Object.keys(extraHTTPHeaders)) {
|
for (const key of Object.keys(extraHTTPHeaders)) {
|
||||||
const value = extraHTTPHeaders[key];
|
const value = extraHTTPHeaders[key];
|
||||||
|
|
@ -79,7 +84,7 @@ export class NetworkManager extends EventEmitter {
|
||||||
await this._client.send('Network.setExtraHTTPHeaders', { headers: this._extraHTTPHeaders });
|
await this._client.send('Network.setExtraHTTPHeaders', { headers: this._extraHTTPHeaders });
|
||||||
}
|
}
|
||||||
|
|
||||||
extraHTTPHeaders(): { [s: string]: string; } {
|
extraHTTPHeaders(): network.Headers {
|
||||||
return Object.assign({}, this._extraHTTPHeaders);
|
return Object.assign({}, this._extraHTTPHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -187,31 +192,38 @@ export class NetworkManager extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRequest(event: Protocol.Network.requestWillBeSentPayload, interceptionId: string | null) {
|
_onRequest(event: Protocol.Network.requestWillBeSentPayload, interceptionId: string | null) {
|
||||||
let redirectChain = [];
|
let redirectChain: Request[] = [];
|
||||||
if (event.redirectResponse) {
|
if (event.redirectResponse) {
|
||||||
const request = this._requestIdToRequest.get(event.requestId);
|
const request = this._requestIdToRequest.get(event.requestId);
|
||||||
// If we connect late to the target, we could have missed the requestWillBeSent event.
|
// If we connect late to the target, we could have missed the requestWillBeSent event.
|
||||||
if (request) {
|
if (request) {
|
||||||
this._handleRequestRedirect(request, event.redirectResponse);
|
this._handleRequestRedirect(request, event.redirectResponse);
|
||||||
redirectChain = request._redirectChain;
|
redirectChain = request.request._redirectChain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const frame = event.frameId ? this._frameManager.frame(event.frameId) : null;
|
const frame = event.frameId ? this._frameManager.frame(event.frameId) : null;
|
||||||
const request = new Request(this._client, frame, interceptionId, this._userRequestInterceptionEnabled, event, redirectChain);
|
const request = new InterceptableRequest(this._client, frame, interceptionId, this._userRequestInterceptionEnabled, event, redirectChain);
|
||||||
this._requestIdToRequest.set(event.requestId, request);
|
this._requestIdToRequest.set(event.requestId, request);
|
||||||
this.emit(NetworkManagerEvents.Request, request);
|
this.emit(NetworkManagerEvents.Request, request.request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_createResponse(request: InterceptableRequest, responsePayload: Protocol.Network.Response): Response {
|
||||||
|
const remoteAddress: network.RemoteAddress = { ip: responsePayload.remoteIPAddress, port: responsePayload.remotePort };
|
||||||
|
const getResponseBody = async () => {
|
||||||
|
const response = await this._client.send('Network.getResponseBody', { requestId: request._requestId });
|
||||||
|
return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');
|
||||||
|
};
|
||||||
|
return new network.Response(request.request, responsePayload.status, responsePayload.statusText, headersObject(responsePayload.headers), remoteAddress, getResponseBody);
|
||||||
|
}
|
||||||
|
|
||||||
_handleRequestRedirect(request: Request, responsePayload: Protocol.Network.Response) {
|
_handleRequestRedirect(request: InterceptableRequest, responsePayload: Protocol.Network.Response) {
|
||||||
const response = new Response(this._client, request, responsePayload);
|
const response = this._createResponse(request, responsePayload);
|
||||||
request._response = response;
|
request.request._redirectChain.push(request.request);
|
||||||
request._redirectChain.push(request);
|
response._bodyLoaded(new Error('Response body is unavailable for redirect responses'));
|
||||||
response._bodyLoadedPromiseFulfill.call(null, new Error('Response body is unavailable for redirect responses'));
|
|
||||||
this._requestIdToRequest.delete(request._requestId);
|
this._requestIdToRequest.delete(request._requestId);
|
||||||
this._attemptedAuthentications.delete(request._interceptionId);
|
this._attemptedAuthentications.delete(request._interceptionId);
|
||||||
this.emit(NetworkManagerEvents.Response, response);
|
this.emit(NetworkManagerEvents.Response, response);
|
||||||
this.emit(NetworkManagerEvents.RequestFinished, request);
|
this.emit(NetworkManagerEvents.RequestFinished, request.request);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onResponseReceived(event: Protocol.Network.responseReceivedPayload) {
|
_onResponseReceived(event: Protocol.Network.responseReceivedPayload) {
|
||||||
|
|
@ -219,8 +231,7 @@ export class NetworkManager extends EventEmitter {
|
||||||
// FileUpload sends a response without a matching request.
|
// FileUpload sends a response without a matching request.
|
||||||
if (!request)
|
if (!request)
|
||||||
return;
|
return;
|
||||||
const response = new Response(this._client, request, event.response);
|
const response = this._createResponse(request, event.response);
|
||||||
request._response = response;
|
|
||||||
this.emit(NetworkManagerEvents.Response, response);
|
this.emit(NetworkManagerEvents.Response, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,11 +244,11 @@ export class NetworkManager extends EventEmitter {
|
||||||
|
|
||||||
// Under certain conditions we never get the Network.responseReceived
|
// Under certain conditions we never get the Network.responseReceived
|
||||||
// event from protocol. @see https://crbug.com/883475
|
// event from protocol. @see https://crbug.com/883475
|
||||||
if (request.response())
|
if (request.request.response())
|
||||||
request.response()._bodyLoadedPromiseFulfill.call(null);
|
request.request.response()._bodyLoaded();
|
||||||
this._requestIdToRequest.delete(request._requestId);
|
this._requestIdToRequest.delete(request._requestId);
|
||||||
this._attemptedAuthentications.delete(request._interceptionId);
|
this._attemptedAuthentications.delete(request._interceptionId);
|
||||||
this.emit(NetworkManagerEvents.RequestFinished, request);
|
this.emit(NetworkManagerEvents.RequestFinished, request.request);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onLoadingFailed(event: Protocol.Network.loadingFailedPayload) {
|
_onLoadingFailed(event: Protocol.Network.loadingFailedPayload) {
|
||||||
|
|
@ -246,97 +257,44 @@ export class NetworkManager extends EventEmitter {
|
||||||
// @see https://crbug.com/750469
|
// @see https://crbug.com/750469
|
||||||
if (!request)
|
if (!request)
|
||||||
return;
|
return;
|
||||||
request._failureText = event.errorText;
|
request.request._setFailureText(event.errorText);
|
||||||
const response = request.response();
|
const response = request.request.response();
|
||||||
if (response)
|
if (response)
|
||||||
response._bodyLoadedPromiseFulfill.call(null);
|
response._bodyLoaded();
|
||||||
this._requestIdToRequest.delete(request._requestId);
|
this._requestIdToRequest.delete(request._requestId);
|
||||||
this._attemptedAuthentications.delete(request._interceptionId);
|
this._attemptedAuthentications.delete(request._interceptionId);
|
||||||
this.emit(NetworkManagerEvents.RequestFailed, request);
|
this.emit(NetworkManagerEvents.RequestFailed, request.request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Request {
|
const interceptableRequestSymbol = Symbol('interceptableRequest');
|
||||||
_response: Response | null = null;
|
|
||||||
_redirectChain: Request[];
|
export function toInterceptableRequest(request: network.Request<ElementHandle>): InterceptableRequest {
|
||||||
|
return (request as any)[interceptableRequestSymbol];
|
||||||
|
}
|
||||||
|
|
||||||
|
class InterceptableRequest {
|
||||||
|
readonly request: Request;
|
||||||
_requestId: string;
|
_requestId: string;
|
||||||
_interceptionId: string;
|
_interceptionId: string;
|
||||||
private _client: CDPSession;
|
private _client: CDPSession;
|
||||||
private _isNavigationRequest: boolean;
|
|
||||||
private _allowInterception: boolean;
|
private _allowInterception: boolean;
|
||||||
private _interceptionHandled = false;
|
private _interceptionHandled = false;
|
||||||
_failureText: string | null = null;
|
|
||||||
private _url: string;
|
|
||||||
private _resourceType: string;
|
|
||||||
private _method: string;
|
|
||||||
private _postData: string;
|
|
||||||
private _headers: {[key: string]: string} = {};
|
|
||||||
private _frame: Frame;
|
|
||||||
|
|
||||||
constructor(client: CDPSession, frame: Frame | null, interceptionId: string, allowInterception: boolean, event: Protocol.Network.requestWillBeSentPayload, redirectChain: Request[]) {
|
constructor(client: CDPSession, frame: Frame | null, interceptionId: string, allowInterception: boolean, event: Protocol.Network.requestWillBeSentPayload, redirectChain: Request[]) {
|
||||||
this._client = client;
|
this._client = client;
|
||||||
this._requestId = event.requestId;
|
this._requestId = event.requestId;
|
||||||
this._isNavigationRequest = event.requestId === event.loaderId && event.type === 'Document';
|
|
||||||
this._interceptionId = interceptionId;
|
this._interceptionId = interceptionId;
|
||||||
this._allowInterception = allowInterception;
|
this._allowInterception = allowInterception;
|
||||||
|
|
||||||
this._url = event.request.url;
|
this.request = new network.Request(frame, redirectChain, event.requestId === event.loaderId && event.type === 'Document',
|
||||||
this._resourceType = event.type.toLowerCase();
|
event.request.url, event.type.toLowerCase(), event.request.method, event.request.postData, headersObject(event.request.headers));
|
||||||
this._method = event.request.method;
|
(this.request as any)[interceptableRequestSymbol] = this;
|
||||||
this._postData = event.request.postData;
|
|
||||||
this._frame = frame;
|
|
||||||
this._redirectChain = redirectChain;
|
|
||||||
for (const key of Object.keys(event.request.headers))
|
|
||||||
this._headers[key.toLowerCase()] = event.request.headers[key];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
url(): string {
|
async continue(overrides: { url?: string; method?: string; postData?: string; headers?: {[key: string]: string}; } = {}) {
|
||||||
return this._url;
|
|
||||||
}
|
|
||||||
|
|
||||||
resourceType(): string {
|
|
||||||
return this._resourceType;
|
|
||||||
}
|
|
||||||
|
|
||||||
method(): string {
|
|
||||||
return this._method;
|
|
||||||
}
|
|
||||||
|
|
||||||
postData(): string | undefined {
|
|
||||||
return this._postData;
|
|
||||||
}
|
|
||||||
|
|
||||||
headers(): {[key: string]: string} {
|
|
||||||
return this._headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
response(): Response | null {
|
|
||||||
return this._response;
|
|
||||||
}
|
|
||||||
|
|
||||||
frame(): Frame | null {
|
|
||||||
return this._frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
isNavigationRequest(): boolean {
|
|
||||||
return this._isNavigationRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
redirectChain(): Request[] {
|
|
||||||
return this._redirectChain.slice();
|
|
||||||
}
|
|
||||||
|
|
||||||
failure(): { errorText: string; } | null {
|
|
||||||
if (!this._failureText)
|
|
||||||
return null;
|
|
||||||
return {
|
|
||||||
errorText: this._failureText
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async _continue(overrides: { url?: string; method?: string; postData?: string; headers?: {[key: string]: string}; } = {}) {
|
|
||||||
// Request interception is not supported for data: urls.
|
// Request interception is not supported for data: urls.
|
||||||
if (this._url.startsWith('data:'))
|
if (this.request.url().startsWith('data:'))
|
||||||
return;
|
return;
|
||||||
assert(this._allowInterception, 'Request Interception is not enabled!');
|
assert(this._allowInterception, 'Request Interception is not enabled!');
|
||||||
assert(!this._interceptionHandled, 'Request is already handled!');
|
assert(!this._interceptionHandled, 'Request is already handled!');
|
||||||
|
|
@ -360,9 +318,9 @@ export class Request {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async _fulfill(response: { status: number; headers: {[key: string]: string}; contentType: string; body: (string | Buffer); }) {
|
async fulfill(response: { status: number; headers: {[key: string]: string}; contentType: string; body: (string | Buffer); }) {
|
||||||
// Mocking responses for dataURL requests is not currently supported.
|
// Mocking responses for dataURL requests is not currently supported.
|
||||||
if (this._url.startsWith('data:'))
|
if (this.request.url().startsWith('data:'))
|
||||||
return;
|
return;
|
||||||
assert(this._allowInterception, 'Request Interception is not enabled!');
|
assert(this._allowInterception, 'Request Interception is not enabled!');
|
||||||
assert(!this._interceptionHandled, 'Request is already handled!');
|
assert(!this._interceptionHandled, 'Request is already handled!');
|
||||||
|
|
@ -393,9 +351,9 @@ export class Request {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async _abort(errorCode: string = 'failed') {
|
async abort(errorCode: string = 'failed') {
|
||||||
// Request interception is not supported for data: urls.
|
// Request interception is not supported for data: urls.
|
||||||
if (this._url.startsWith('data:'))
|
if (this.request.url().startsWith('data:'))
|
||||||
return;
|
return;
|
||||||
const errorReason = errorReasons[errorCode];
|
const errorReason = errorReasons[errorCode];
|
||||||
assert(errorReason, 'Unknown error code: ' + errorCode);
|
assert(errorReason, 'Unknown error code: ' + errorCode);
|
||||||
|
|
@ -430,94 +388,6 @@ const errorReasons = {
|
||||||
'failed': 'Failed',
|
'failed': 'Failed',
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Response {
|
|
||||||
_bodyLoadedPromiseFulfill: any;
|
|
||||||
private _client: CDPSession;
|
|
||||||
private _request: Request;
|
|
||||||
private _contentPromise: Promise<Buffer> | null = null;
|
|
||||||
private _bodyLoadedPromise: Promise<Error | null>;
|
|
||||||
private _remoteAddress: { ip: string; port: number; };
|
|
||||||
private _status: number;
|
|
||||||
private _statusText: string;
|
|
||||||
private _url: string;
|
|
||||||
private _headers: {[key: string]: string} = {};
|
|
||||||
|
|
||||||
constructor(client: CDPSession, request: Request, responsePayload: Protocol.Network.Response) {
|
|
||||||
this._client = client;
|
|
||||||
this._request = request;
|
|
||||||
|
|
||||||
this._bodyLoadedPromise = new Promise(fulfill => {
|
|
||||||
this._bodyLoadedPromiseFulfill = fulfill;
|
|
||||||
});
|
|
||||||
|
|
||||||
this._remoteAddress = {
|
|
||||||
ip: responsePayload.remoteIPAddress,
|
|
||||||
port: responsePayload.remotePort,
|
|
||||||
};
|
|
||||||
this._status = responsePayload.status;
|
|
||||||
this._statusText = responsePayload.statusText;
|
|
||||||
this._url = request.url();
|
|
||||||
for (const key of Object.keys(responsePayload.headers))
|
|
||||||
this._headers[key.toLowerCase()] = responsePayload.headers[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
remoteAddress(): { ip: string; port: number; } {
|
|
||||||
return this._remoteAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
url(): string {
|
|
||||||
return this._url;
|
|
||||||
}
|
|
||||||
|
|
||||||
ok(): boolean {
|
|
||||||
return this._status === 0 || (this._status >= 200 && this._status <= 299);
|
|
||||||
}
|
|
||||||
|
|
||||||
status(): number {
|
|
||||||
return this._status;
|
|
||||||
}
|
|
||||||
|
|
||||||
statusText(): string {
|
|
||||||
return this._statusText;
|
|
||||||
}
|
|
||||||
|
|
||||||
headers(): object {
|
|
||||||
return this._headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer(): Promise<Buffer> {
|
|
||||||
if (!this._contentPromise) {
|
|
||||||
this._contentPromise = this._bodyLoadedPromise.then(async error => {
|
|
||||||
if (error)
|
|
||||||
throw error;
|
|
||||||
const response = await this._client.send('Network.getResponseBody', {
|
|
||||||
requestId: this._request._requestId
|
|
||||||
});
|
|
||||||
return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this._contentPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
async text(): Promise<string> {
|
|
||||||
const content = await this.buffer();
|
|
||||||
return content.toString('utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
async json(): Promise<object> {
|
|
||||||
const content = await this.text();
|
|
||||||
return JSON.parse(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
request(): Request {
|
|
||||||
return this._request;
|
|
||||||
}
|
|
||||||
|
|
||||||
frame(): Frame | null {
|
|
||||||
return this._request.frame();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function headersArray(headers: { [s: string]: string; }): { name: string; value: string; }[] {
|
function headersArray(headers: { [s: string]: string; }): { name: string; value: string; }[] {
|
||||||
const result = [];
|
const result = [];
|
||||||
for (const name in headers) {
|
for (const name in headers) {
|
||||||
|
|
@ -527,6 +397,13 @@ function headersArray(headers: { [s: string]: string; }): { name: string; value:
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function headersObject(headers: Protocol.Network.Headers): network.Headers {
|
||||||
|
const result: network.Headers = {};
|
||||||
|
for (const key of Object.keys(headers))
|
||||||
|
result[key.toLowerCase()] = headers[key];
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// List taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml with extra 306 and 418 codes.
|
// List taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml with extra 306 and 418 codes.
|
||||||
const STATUS_TEXTS = {
|
const STATUS_TEXTS = {
|
||||||
'100': 'Continue',
|
'100': 'Continue',
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ export { Worker, Workers } from './features/workers';
|
||||||
export { Frame } from '../frames';
|
export { Frame } from '../frames';
|
||||||
export { Keyboard, Mouse } from '../input';
|
export { Keyboard, Mouse } from '../input';
|
||||||
export { ElementHandle } from './JSHandle';
|
export { ElementHandle } from './JSHandle';
|
||||||
export { Request, Response } from './NetworkManager';
|
export { Request, Response } from '../network';
|
||||||
export { ConsoleMessage, FileChooser, Page } from './Page';
|
export { ConsoleMessage, FileChooser, Page } from './Page';
|
||||||
export { Playwright } from './Playwright';
|
export { Playwright } from './Playwright';
|
||||||
export { Target } from './Target';
|
export { Target } from './Target';
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright (c) Microsoft Corporation.
|
// Copyright (c) Microsoft Corporation.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
import { NetworkManager, Request } from '../NetworkManager';
|
import { NetworkManager, Request, toInterceptableRequest } from '../NetworkManager';
|
||||||
|
|
||||||
export class Interception {
|
export class Interception {
|
||||||
private _networkManager: NetworkManager;
|
private _networkManager: NetworkManager;
|
||||||
|
|
@ -19,15 +19,15 @@ export class Interception {
|
||||||
}
|
}
|
||||||
|
|
||||||
async continue(request: Request, overrides: { url?: string; method?: string; postData?: string; headers?: {[key: string]: string}; } = {}) {
|
async continue(request: Request, overrides: { url?: string; method?: string; postData?: string; headers?: {[key: string]: string}; } = {}) {
|
||||||
return request._continue(overrides);
|
return toInterceptableRequest(request).continue(overrides);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fulfill(request: Request, response: { status: number; headers: {[key: string]: string}; contentType: string; body: (string | Buffer); }) {
|
async fulfill(request: Request, response: { status: number; headers: {[key: string]: string}; contentType: string; body: (string | Buffer); }) {
|
||||||
return request._fulfill(response);
|
return toInterceptableRequest(request).fulfill(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
async abort(request: Request, errorCode: string = 'failed') {
|
async abort(request: Request, errorCode: string = 'failed') {
|
||||||
return request._abort(errorCode);
|
return toInterceptableRequest(request).abort(errorCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
setOfflineMode(enabled: boolean) {
|
setOfflineMode(enabled: boolean) {
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,13 @@
|
||||||
|
|
||||||
import {helper, debugError} from '../helper';
|
import {helper, debugError} from '../helper';
|
||||||
import { createHandle, ElementHandle } from './JSHandle';
|
import { createHandle, ElementHandle } from './JSHandle';
|
||||||
import { Response } from './NetworkManager';
|
|
||||||
import * as js from '../javascript';
|
import * as js from '../javascript';
|
||||||
import { JugglerSession } from './Connection';
|
import { JugglerSession } from './Connection';
|
||||||
|
|
||||||
export type ExecutionContext = js.ExecutionContext<ElementHandle, Response>;
|
export type ExecutionContext = js.ExecutionContext<ElementHandle>;
|
||||||
export type JSHandle = js.JSHandle<ElementHandle, Response>;
|
export type JSHandle = js.JSHandle<ElementHandle>;
|
||||||
|
|
||||||
export class ExecutionContextDelegate implements js.ExecutionContextDelegate<ElementHandle, Response> {
|
export class ExecutionContextDelegate implements js.ExecutionContextDelegate<ElementHandle> {
|
||||||
_session: JugglerSession;
|
_session: JugglerSession;
|
||||||
_executionContextId: string;
|
_executionContextId: string;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ import { JSHandle, ExecutionContext, ExecutionContextDelegate } from './Executio
|
||||||
import {NavigationWatchdog, NextNavigationWatchdog} from './NavigationWatchdog';
|
import {NavigationWatchdog, NextNavigationWatchdog} from './NavigationWatchdog';
|
||||||
import { ElementHandle } from './JSHandle';
|
import { ElementHandle } from './JSHandle';
|
||||||
import { TimeoutSettings } from '../TimeoutSettings';
|
import { TimeoutSettings } from '../TimeoutSettings';
|
||||||
import { Response } from './NetworkManager';
|
|
||||||
import * as frames from '../frames';
|
import * as frames from '../frames';
|
||||||
import * as js from '../javascript';
|
import * as js from '../javascript';
|
||||||
|
|
||||||
|
|
@ -43,9 +42,9 @@ type FrameData = {
|
||||||
firedEvents: Set<string>,
|
firedEvents: Set<string>,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Frame = frames.Frame<ElementHandle, Response>;
|
export type Frame = frames.Frame<ElementHandle>;
|
||||||
|
|
||||||
export class FrameManager extends EventEmitter implements frames.FrameDelegate<ElementHandle, Response> {
|
export class FrameManager extends EventEmitter implements frames.FrameDelegate<ElementHandle> {
|
||||||
_session: JugglerSession;
|
_session: JugglerSession;
|
||||||
_page: Page;
|
_page: Page;
|
||||||
_networkManager: any;
|
_networkManager: any;
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,10 @@ import { JugglerSession } from './Connection';
|
||||||
import { Frame, FrameManager } from './FrameManager';
|
import { Frame, FrameManager } from './FrameManager';
|
||||||
import { Page } from './Page';
|
import { Page } from './Page';
|
||||||
import { JSHandle, ExecutionContext, markJSHandle, ExecutionContextDelegate } from './ExecutionContext';
|
import { JSHandle, ExecutionContext, markJSHandle, ExecutionContextDelegate } from './ExecutionContext';
|
||||||
import { Response } from './NetworkManager';
|
|
||||||
|
|
||||||
type SelectorRoot = Element | ShadowRoot | Document;
|
type SelectorRoot = Element | ShadowRoot | Document;
|
||||||
|
|
||||||
export class ElementHandle extends js.JSHandle<ElementHandle, Response> {
|
export class ElementHandle extends js.JSHandle<ElementHandle> {
|
||||||
_frame: Frame;
|
_frame: Frame;
|
||||||
_frameId: string;
|
_frameId: string;
|
||||||
_page: Page;
|
_page: Page;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,29 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2019 Google Inc. All rights reserved.
|
||||||
|
* Modifications copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { assert, debugError, helper, RegisteredListener } from '../helper';
|
import { assert, debugError, helper, RegisteredListener } from '../helper';
|
||||||
import { JugglerSession } from './Connection';
|
import { JugglerSession } from './Connection';
|
||||||
import { FrameManager } from './FrameManager';
|
import { FrameManager, Frame } from './FrameManager';
|
||||||
|
import * as network from '../network';
|
||||||
|
import { ElementHandle } from './JSHandle';
|
||||||
|
|
||||||
|
export type Request = network.Request<ElementHandle>;
|
||||||
|
export type Response = network.Response<ElementHandle>;
|
||||||
|
|
||||||
export const NetworkManagerEvents = {
|
export const NetworkManagerEvents = {
|
||||||
RequestFailed: Symbol('NetworkManagerEvents.RequestFailed'),
|
RequestFailed: Symbol('NetworkManagerEvents.RequestFailed'),
|
||||||
|
|
@ -12,7 +34,7 @@ export const NetworkManagerEvents = {
|
||||||
|
|
||||||
export class NetworkManager extends EventEmitter {
|
export class NetworkManager extends EventEmitter {
|
||||||
private _session: JugglerSession;
|
private _session: JugglerSession;
|
||||||
private _requests: Map<string, Request>;
|
private _requests: Map<string, InterceptableRequest>;
|
||||||
private _frameManager: FrameManager;
|
private _frameManager: FrameManager;
|
||||||
private _eventListeners: RegisteredListener[];
|
private _eventListeners: RegisteredListener[];
|
||||||
|
|
||||||
|
|
@ -39,7 +61,7 @@ export class NetworkManager extends EventEmitter {
|
||||||
this._frameManager = frameManager;
|
this._frameManager = frameManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setExtraHTTPHeaders(headers) {
|
async setExtraHTTPHeaders(headers: network.Headers) {
|
||||||
const array = [];
|
const array = [];
|
||||||
for (const [name, value] of Object.entries(headers)) {
|
for (const [name, value] of Object.entries(headers)) {
|
||||||
assert(helper.isString(value), `Expected value of header "${name}" to be String, but "${typeof value}" is found.`);
|
assert(helper.isString(value), `Expected value of header "${name}" to be String, but "${typeof value}" is found.`);
|
||||||
|
|
@ -54,26 +76,37 @@ export class NetworkManager extends EventEmitter {
|
||||||
|
|
||||||
_onRequestWillBeSent(event) {
|
_onRequestWillBeSent(event) {
|
||||||
const redirected = event.redirectedFrom ? this._requests.get(event.redirectedFrom) : null;
|
const redirected = event.redirectedFrom ? this._requests.get(event.redirectedFrom) : null;
|
||||||
const frame = redirected ? redirected.frame() : (this._frameManager && event.frameId ? this._frameManager.frame(event.frameId) : null);
|
const frame = redirected ? redirected.request.frame() : (this._frameManager && event.frameId ? this._frameManager.frame(event.frameId) : null);
|
||||||
if (!frame)
|
if (!frame)
|
||||||
return;
|
return;
|
||||||
let redirectChain = [];
|
let redirectChain: Request[] = [];
|
||||||
if (redirected) {
|
if (redirected) {
|
||||||
redirectChain = redirected._redirectChain;
|
redirectChain = redirected.request._redirectChain;
|
||||||
redirectChain.push(redirected);
|
redirectChain.push(redirected.request);
|
||||||
this._requests.delete(redirected._id);
|
this._requests.delete(redirected._id);
|
||||||
}
|
}
|
||||||
const request = new Request(this._session, frame, redirectChain, event);
|
const request = new InterceptableRequest(this._session, frame, redirectChain, event);
|
||||||
this._requests.set(request._id, request);
|
this._requests.set(request._id, request);
|
||||||
this.emit(NetworkManagerEvents.Request, request);
|
this.emit(NetworkManagerEvents.Request, request.request);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onResponseReceived(event) {
|
_onResponseReceived(event) {
|
||||||
const request = this._requests.get(event.requestId);
|
const request = this._requests.get(event.requestId);
|
||||||
if (!request)
|
if (!request)
|
||||||
return;
|
return;
|
||||||
const response = new Response(this._session, request, event);
|
const remoteAddress: network.RemoteAddress = { ip: event.remoteIPAddress, port: event.remotePort };
|
||||||
request._response = response;
|
const getResponseBody = async () => {
|
||||||
|
const response = await this._session.send('Network.getResponseBody', {
|
||||||
|
requestId: request._id
|
||||||
|
});
|
||||||
|
if (response.evicted)
|
||||||
|
throw new Error(`Response body for ${request.request.method()} ${request.request.url()} was evicted!`);
|
||||||
|
return Buffer.from(response.base64body, 'base64');
|
||||||
|
};
|
||||||
|
const headers: network.Headers = {};
|
||||||
|
for (const {name, value} of event.headers)
|
||||||
|
headers[name.toLowerCase()] = value;
|
||||||
|
const response = new network.Response(request.request, event.status, event.statusText, headers, remoteAddress, getResponseBody);
|
||||||
this.emit(NetworkManagerEvents.Response, response);
|
this.emit(NetworkManagerEvents.Response, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,15 +114,16 @@ export class NetworkManager extends EventEmitter {
|
||||||
const request = this._requests.get(event.requestId);
|
const request = this._requests.get(event.requestId);
|
||||||
if (!request)
|
if (!request)
|
||||||
return;
|
return;
|
||||||
|
const response = request.request.response();
|
||||||
// Keep redirected requests in the map for future reference in redirectChain.
|
// Keep redirected requests in the map for future reference in redirectChain.
|
||||||
const isRedirected = request.response().status() >= 300 && request.response().status() <= 399;
|
const isRedirected = response.status() >= 300 && response.status() <= 399;
|
||||||
if (isRedirected) {
|
if (isRedirected) {
|
||||||
request.response()._bodyLoadedPromiseFulfill.call(null, new Error('Response body is unavailable for redirect responses'));
|
response._bodyLoaded(new Error('Response body is unavailable for redirect responses'));
|
||||||
} else {
|
} else {
|
||||||
this._requests.delete(request._id);
|
this._requests.delete(request._id);
|
||||||
request.response()._bodyLoadedPromiseFulfill.call(null);
|
response._bodyLoaded();
|
||||||
}
|
}
|
||||||
this.emit(NetworkManagerEvents.RequestFinished, request);
|
this.emit(NetworkManagerEvents.RequestFinished, request.request);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRequestFailed(event) {
|
_onRequestFailed(event) {
|
||||||
|
|
@ -97,10 +131,10 @@ export class NetworkManager extends EventEmitter {
|
||||||
if (!request)
|
if (!request)
|
||||||
return;
|
return;
|
||||||
this._requests.delete(request._id);
|
this._requests.delete(request._id);
|
||||||
if (request.response())
|
if (request.request.response())
|
||||||
request.response()._bodyLoadedPromiseFulfill.call(null);
|
request.request.response()._bodyLoaded();
|
||||||
request._errorText = event.errorCode;
|
request.request._setFailureText(event.errorCode);
|
||||||
this.emit(NetworkManagerEvents.RequestFailed, request);
|
this.emit(NetworkManagerEvents.RequestFailed, request.request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,46 +164,35 @@ const causeToResourceType = {
|
||||||
TYPE_WEB_MANIFEST: 'manifest',
|
TYPE_WEB_MANIFEST: 'manifest',
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Request {
|
const interceptableRequestSymbol = Symbol('interceptableRequest');
|
||||||
|
|
||||||
|
export function toInterceptableRequest(request: network.Request<ElementHandle>): InterceptableRequest {
|
||||||
|
return (request as any)[interceptableRequestSymbol];
|
||||||
|
}
|
||||||
|
|
||||||
|
class InterceptableRequest {
|
||||||
|
readonly request: Request;
|
||||||
_id: string;
|
_id: string;
|
||||||
private _session: any;
|
private _session: JugglerSession;
|
||||||
private _frame: any;
|
private _suspended: boolean;
|
||||||
_redirectChain: any;
|
|
||||||
private _url: any;
|
|
||||||
private _postData: any;
|
|
||||||
private _suspended: any;
|
|
||||||
_response: any;
|
|
||||||
_errorText: any;
|
|
||||||
private _isNavigationRequest: any;
|
|
||||||
private _method: any;
|
|
||||||
private _resourceType: any;
|
|
||||||
private _headers: {};
|
|
||||||
private _interceptionHandled: boolean;
|
private _interceptionHandled: boolean;
|
||||||
|
|
||||||
constructor(session, frame, redirectChain, payload) {
|
constructor(session: JugglerSession, frame: Frame, redirectChain: Request[], payload: any) {
|
||||||
this._session = session;
|
|
||||||
this._frame = frame;
|
|
||||||
this._id = payload.requestId;
|
this._id = payload.requestId;
|
||||||
this._redirectChain = redirectChain;
|
this._session = session;
|
||||||
this._url = payload.url;
|
|
||||||
this._postData = payload.postData;
|
|
||||||
this._suspended = payload.suspended;
|
this._suspended = payload.suspended;
|
||||||
this._response = null;
|
|
||||||
this._errorText = null;
|
|
||||||
this._isNavigationRequest = payload.isNavigationRequest;
|
|
||||||
this._method = payload.method;
|
|
||||||
this._resourceType = causeToResourceType[payload.cause] || 'other';
|
|
||||||
this._headers = {};
|
|
||||||
this._interceptionHandled = false;
|
this._interceptionHandled = false;
|
||||||
|
|
||||||
|
const headers: network.Headers = {};
|
||||||
for (const {name, value} of payload.headers)
|
for (const {name, value} of payload.headers)
|
||||||
this._headers[name.toLowerCase()] = value;
|
headers[name.toLowerCase()] = value;
|
||||||
|
|
||||||
|
this.request = new network.Request(frame, redirectChain, payload.isNavigationRequest,
|
||||||
|
payload.url, causeToResourceType[payload.cause] || 'other', payload.method, payload.postData, headers);
|
||||||
|
(this.request as any)[interceptableRequestSymbol] = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
failure() {
|
async continue(overrides: any = {}) {
|
||||||
return this._errorText ? {errorText: this._errorText} : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async _continue(overrides: any = {}) {
|
|
||||||
assert(!overrides.url, 'Playwright-Firefox does not support overriding URL');
|
assert(!overrides.url, 'Playwright-Firefox does not support overriding URL');
|
||||||
assert(!overrides.method, 'Playwright-Firefox does not support overriding method');
|
assert(!overrides.method, 'Playwright-Firefox does not support overriding method');
|
||||||
assert(!overrides.postData, 'Playwright-Firefox does not support overriding postData');
|
assert(!overrides.postData, 'Playwright-Firefox does not support overriding postData');
|
||||||
|
|
@ -187,7 +210,7 @@ export class Request {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async _abort() {
|
async abort() {
|
||||||
assert(this._suspended, 'Request Interception is not enabled!');
|
assert(this._suspended, 'Request Interception is not enabled!');
|
||||||
assert(!this._interceptionHandled, 'Request is already handled!');
|
assert(!this._interceptionHandled, 'Request is already handled!');
|
||||||
this._interceptionHandled = true;
|
this._interceptionHandled = true;
|
||||||
|
|
@ -197,129 +220,4 @@ export class Request {
|
||||||
debugError(error);
|
debugError(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
postData() {
|
|
||||||
return this._postData;
|
|
||||||
}
|
|
||||||
|
|
||||||
headers() {
|
|
||||||
return {...this._headers};
|
|
||||||
}
|
|
||||||
|
|
||||||
redirectChain() {
|
|
||||||
return this._redirectChain.slice();
|
|
||||||
}
|
|
||||||
|
|
||||||
resourceType() {
|
|
||||||
return this._resourceType;
|
|
||||||
}
|
|
||||||
|
|
||||||
url() {
|
|
||||||
return this._url;
|
|
||||||
}
|
|
||||||
|
|
||||||
method() {
|
|
||||||
return this._method;
|
|
||||||
}
|
|
||||||
|
|
||||||
isNavigationRequest() {
|
|
||||||
return this._isNavigationRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
frame() {
|
|
||||||
return this._frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
response() {
|
|
||||||
return this._response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Response {
|
|
||||||
private _session: any;
|
|
||||||
private _request: any;
|
|
||||||
private _remoteIPAddress: any;
|
|
||||||
private _remotePort: any;
|
|
||||||
private _status: any;
|
|
||||||
private _statusText: any;
|
|
||||||
private _headers: {};
|
|
||||||
private _bodyLoadedPromise: Promise<unknown>;
|
|
||||||
private _bodyLoadedPromiseFulfill: (value?: unknown) => void;
|
|
||||||
private _contentPromise: any;
|
|
||||||
|
|
||||||
constructor(session, request, payload) {
|
|
||||||
this._session = session;
|
|
||||||
this._request = request;
|
|
||||||
this._remoteIPAddress = payload.remoteIPAddress;
|
|
||||||
this._remotePort = payload.remotePort;
|
|
||||||
this._status = payload.status;
|
|
||||||
this._statusText = payload.statusText;
|
|
||||||
this._headers = {};
|
|
||||||
for (const {name, value} of payload.headers)
|
|
||||||
this._headers[name.toLowerCase()] = value;
|
|
||||||
this._bodyLoadedPromise = new Promise(fulfill => {
|
|
||||||
this._bodyLoadedPromiseFulfill = fulfill;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer(): Promise<Buffer> {
|
|
||||||
if (!this._contentPromise) {
|
|
||||||
this._contentPromise = this._bodyLoadedPromise.then(async error => {
|
|
||||||
if (error)
|
|
||||||
throw error;
|
|
||||||
const response = await this._session.send('Network.getResponseBody', {
|
|
||||||
requestId: this._request._id
|
|
||||||
});
|
|
||||||
if (response.evicted)
|
|
||||||
throw new Error(`Response body for ${this._request.method()} ${this._request.url()} was evicted!`);
|
|
||||||
return Buffer.from(response.base64body, 'base64');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this._contentPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
async text(): Promise<string> {
|
|
||||||
const content = await this.buffer();
|
|
||||||
return content.toString('utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
async json(): Promise<object> {
|
|
||||||
const content = await this.text();
|
|
||||||
return JSON.parse(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
headers() {
|
|
||||||
return {...this._headers};
|
|
||||||
}
|
|
||||||
|
|
||||||
status() {
|
|
||||||
return this._status;
|
|
||||||
}
|
|
||||||
|
|
||||||
statusText() {
|
|
||||||
return this._statusText;
|
|
||||||
}
|
|
||||||
|
|
||||||
ok() {
|
|
||||||
return this._status >= 200 && this._status <= 299;
|
|
||||||
}
|
|
||||||
|
|
||||||
remoteAddress() {
|
|
||||||
return {
|
|
||||||
ip: this._remoteIPAddress,
|
|
||||||
port: this._remotePort,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
frame() {
|
|
||||||
return this._request.frame();
|
|
||||||
}
|
|
||||||
|
|
||||||
url() {
|
|
||||||
return this._request.url();
|
|
||||||
}
|
|
||||||
|
|
||||||
request() {
|
|
||||||
return this._request;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export { Interception } from './features/interception';
|
||||||
export { Permissions } from './features/permissions';
|
export { Permissions } from './features/permissions';
|
||||||
export { Frame } from './FrameManager';
|
export { Frame } from './FrameManager';
|
||||||
export { ElementHandle } from './JSHandle';
|
export { ElementHandle } from './JSHandle';
|
||||||
export { Request, Response } from './NetworkManager';
|
export { Request, Response } from '../network';
|
||||||
export { ConsoleMessage, FileChooser, Page } from './Page';
|
export { ConsoleMessage, FileChooser, Page } from './Page';
|
||||||
export { Playwright } from './Playwright';
|
export { Playwright } from './Playwright';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright (c) Microsoft Corporation.
|
// Copyright (c) Microsoft Corporation.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
import { NetworkManager, Request } from '../NetworkManager';
|
import { NetworkManager, Request, toInterceptableRequest } from '../NetworkManager';
|
||||||
|
|
||||||
export class Interception {
|
export class Interception {
|
||||||
private _networkManager: NetworkManager;
|
private _networkManager: NetworkManager;
|
||||||
|
|
@ -19,7 +19,7 @@ export class Interception {
|
||||||
}
|
}
|
||||||
|
|
||||||
async continue(request: Request, overrides: { url?: string; method?: string; postData?: string; headers?: {[key: string]: string}; } = {}) {
|
async continue(request: Request, overrides: { url?: string; method?: string; postData?: string; headers?: {[key: string]: string}; } = {}) {
|
||||||
return request._continue(overrides);
|
return toInterceptableRequest(request).continue(overrides);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fulfill(request: Request, response: { status: number; headers: {[key: string]: string}; contentType: string; body: (string | Buffer); }) {
|
async fulfill(request: Request, response: { status: number; headers: {[key: string]: string}; contentType: string; body: (string | Buffer); }) {
|
||||||
|
|
@ -27,6 +27,6 @@ export class Interception {
|
||||||
}
|
}
|
||||||
|
|
||||||
async abort(request: Request, errorCode: string = 'failed') {
|
async abort(request: Request, errorCode: string = 'failed') {
|
||||||
return request._abort();
|
return toInterceptableRequest(request).abort();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
import * as types from './types';
|
import * as types from './types';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as js from './javascript';
|
import * as js from './javascript';
|
||||||
|
import * as network from './network';
|
||||||
import { helper, assert } from './helper';
|
import { helper, assert } from './helper';
|
||||||
import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption } from './input';
|
import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption } from './input';
|
||||||
import { waitForSelectorOrXPath, WaitTaskParams, WaitTask } from './waitTask';
|
import { waitForSelectorOrXPath, WaitTaskParams, WaitTask } from './waitTask';
|
||||||
|
|
@ -26,11 +27,11 @@ import { TimeoutSettings } from './TimeoutSettings';
|
||||||
const readFileAsync = helper.promisify(fs.readFile);
|
const readFileAsync = helper.promisify(fs.readFile);
|
||||||
|
|
||||||
type WorldType = 'main' | 'utility';
|
type WorldType = 'main' | 'utility';
|
||||||
type World<ElementHandle extends types.ElementHandle<ElementHandle, Response>, Response> = {
|
type World<ElementHandle extends types.ElementHandle<ElementHandle>> = {
|
||||||
contextPromise: Promise<js.ExecutionContext<ElementHandle, Response>>;
|
contextPromise: Promise<js.ExecutionContext<ElementHandle>>;
|
||||||
contextResolveCallback: (c: js.ExecutionContext<ElementHandle, Response>) => void;
|
contextResolveCallback: (c: js.ExecutionContext<ElementHandle>) => void;
|
||||||
context: js.ExecutionContext<ElementHandle, Response> | null;
|
context: js.ExecutionContext<ElementHandle> | null;
|
||||||
waitTasks: Set<WaitTask<ElementHandle, Response>>;
|
waitTasks: Set<WaitTask<ElementHandle>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NavigateOptions = {
|
export type NavigateOptions = {
|
||||||
|
|
@ -42,24 +43,24 @@ export type GotoOptions = NavigateOptions & {
|
||||||
referer?: string,
|
referer?: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface FrameDelegate<ElementHandle extends types.ElementHandle<ElementHandle, Response>, Response> {
|
export interface FrameDelegate<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
||||||
timeoutSettings(): TimeoutSettings;
|
timeoutSettings(): TimeoutSettings;
|
||||||
navigateFrame(frame: Frame<ElementHandle, Response>, url: string, options?: GotoOptions): Promise<Response | null>;
|
navigateFrame(frame: Frame<ElementHandle>, url: string, options?: GotoOptions): Promise<network.Response<ElementHandle> | null>;
|
||||||
waitForFrameNavigation(frame: Frame<ElementHandle, Response>, options?: NavigateOptions): Promise<Response | null>;
|
waitForFrameNavigation(frame: Frame<ElementHandle>, options?: NavigateOptions): Promise<network.Response<ElementHandle> | null>;
|
||||||
setFrameContent(frame: Frame<ElementHandle, Response>, html: string, options?: NavigateOptions): Promise<void>;
|
setFrameContent(frame: Frame<ElementHandle>, html: string, options?: NavigateOptions): Promise<void>;
|
||||||
adoptElementHandle(elementHandle: ElementHandle, context: js.ExecutionContext<ElementHandle, Response>): Promise<ElementHandle>;
|
adoptElementHandle(elementHandle: ElementHandle, context: js.ExecutionContext<ElementHandle>): Promise<ElementHandle>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Frame<ElementHandle extends types.ElementHandle<ElementHandle, Response>, Response> {
|
export class Frame<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
||||||
_delegate: FrameDelegate<ElementHandle, Response>;
|
_delegate: FrameDelegate<ElementHandle>;
|
||||||
private _parentFrame: Frame<ElementHandle, Response>;
|
private _parentFrame: Frame<ElementHandle>;
|
||||||
private _url = '';
|
private _url = '';
|
||||||
private _detached = false;
|
private _detached = false;
|
||||||
private _worlds = new Map<WorldType, World<ElementHandle, Response>>();
|
private _worlds = new Map<WorldType, World<ElementHandle>>();
|
||||||
private _childFrames = new Set<Frame<ElementHandle, Response>>();
|
private _childFrames = new Set<Frame<ElementHandle>>();
|
||||||
private _name: string;
|
private _name: string;
|
||||||
|
|
||||||
constructor(delegate: FrameDelegate<ElementHandle, Response>, parentFrame: Frame<ElementHandle, Response> | null) {
|
constructor(delegate: FrameDelegate<ElementHandle>, parentFrame: Frame<ElementHandle> | null) {
|
||||||
this._delegate = delegate;
|
this._delegate = delegate;
|
||||||
this._parentFrame = parentFrame;
|
this._parentFrame = parentFrame;
|
||||||
|
|
||||||
|
|
@ -72,36 +73,36 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle, Resp
|
||||||
this._parentFrame._childFrames.add(this);
|
this._parentFrame._childFrames.add(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
goto(url: string, options?: GotoOptions): Promise<Response | null> {
|
goto(url: string, options?: GotoOptions): Promise<network.Response<ElementHandle> | null> {
|
||||||
return this._delegate.navigateFrame(this, url, options);
|
return this._delegate.navigateFrame(this, url, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForNavigation(options?: NavigateOptions): Promise<Response | null> {
|
waitForNavigation(options?: NavigateOptions): Promise<network.Response<ElementHandle> | null> {
|
||||||
return this._delegate.waitForFrameNavigation(this, options);
|
return this._delegate.waitForFrameNavigation(this, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
_mainContext(): Promise<js.ExecutionContext<ElementHandle, Response>> {
|
_mainContext(): Promise<js.ExecutionContext<ElementHandle>> {
|
||||||
if (this._detached)
|
if (this._detached)
|
||||||
throw new Error(`Execution Context is not available in detached frame "${this.url()}" (are you trying to evaluate?)`);
|
throw new Error(`Execution Context is not available in detached frame "${this.url()}" (are you trying to evaluate?)`);
|
||||||
return this._worlds.get('main').contextPromise;
|
return this._worlds.get('main').contextPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
_utilityContext(): Promise<js.ExecutionContext<ElementHandle, Response>> {
|
_utilityContext(): Promise<js.ExecutionContext<ElementHandle>> {
|
||||||
if (this._detached)
|
if (this._detached)
|
||||||
throw new Error(`Execution Context is not available in detached frame "${this.url()}" (are you trying to evaluate?)`);
|
throw new Error(`Execution Context is not available in detached frame "${this.url()}" (are you trying to evaluate?)`);
|
||||||
return this._worlds.get('utility').contextPromise;
|
return this._worlds.get('utility').contextPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
executionContext(): Promise<js.ExecutionContext<ElementHandle, Response>> {
|
executionContext(): Promise<js.ExecutionContext<ElementHandle>> {
|
||||||
return this._mainContext();
|
return this._mainContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
evaluateHandle: types.EvaluateHandle<js.JSHandle<ElementHandle, Response>> = async (pageFunction, ...args) => {
|
evaluateHandle: types.EvaluateHandle<js.JSHandle<ElementHandle>> = async (pageFunction, ...args) => {
|
||||||
const context = await this._mainContext();
|
const context = await this._mainContext();
|
||||||
return context.evaluateHandle(pageFunction, ...args as any);
|
return context.evaluateHandle(pageFunction, ...args as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
evaluate: types.Evaluate<js.JSHandle<ElementHandle, Response>> = async (pageFunction, ...args) => {
|
evaluate: types.Evaluate<js.JSHandle<ElementHandle>> = async (pageFunction, ...args) => {
|
||||||
const context = await this._mainContext();
|
const context = await this._mainContext();
|
||||||
return context.evaluate(pageFunction, ...args as any);
|
return context.evaluate(pageFunction, ...args as any);
|
||||||
}
|
}
|
||||||
|
|
@ -118,13 +119,13 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle, Resp
|
||||||
return document.$x(expression);
|
return document.$x(expression);
|
||||||
}
|
}
|
||||||
|
|
||||||
$eval: types.$Eval<js.JSHandle<ElementHandle, Response>> = async (selector, pageFunction, ...args) => {
|
$eval: types.$Eval<js.JSHandle<ElementHandle>> = async (selector, pageFunction, ...args) => {
|
||||||
const context = await this._mainContext();
|
const context = await this._mainContext();
|
||||||
const document = await context._document();
|
const document = await context._document();
|
||||||
return document.$eval(selector, pageFunction, ...args as any);
|
return document.$eval(selector, pageFunction, ...args as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
$$eval: types.$$Eval<js.JSHandle<ElementHandle, Response>> = async (selector, pageFunction, ...args) => {
|
$$eval: types.$$Eval<js.JSHandle<ElementHandle>> = async (selector, pageFunction, ...args) => {
|
||||||
const context = await this._mainContext();
|
const context = await this._mainContext();
|
||||||
const document = await context._document();
|
const document = await context._document();
|
||||||
return document.$$eval(selector, pageFunction, ...args as any);
|
return document.$$eval(selector, pageFunction, ...args as any);
|
||||||
|
|
@ -160,11 +161,11 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle, Resp
|
||||||
return this._url;
|
return this._url;
|
||||||
}
|
}
|
||||||
|
|
||||||
parentFrame(): Frame<ElementHandle, Response> | null {
|
parentFrame(): Frame<ElementHandle> | null {
|
||||||
return this._parentFrame;
|
return this._parentFrame;
|
||||||
}
|
}
|
||||||
|
|
||||||
childFrames(): Frame<ElementHandle, Response>[] {
|
childFrames(): Frame<ElementHandle>[] {
|
||||||
return Array.from(this._childFrames);
|
return Array.from(this._childFrames);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -368,7 +369,7 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle, Resp
|
||||||
await handle.dispose();
|
await handle.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options: any = {}, ...args: any[]): Promise<js.JSHandle<ElementHandle, Response> | null> {
|
waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options: any = {}, ...args: any[]): Promise<js.JSHandle<ElementHandle> | null> {
|
||||||
const xPathPattern = '//';
|
const xPathPattern = '//';
|
||||||
|
|
||||||
if (helper.isString(selectorOrFunctionOrTimeout)) {
|
if (helper.isString(selectorOrFunctionOrTimeout)) {
|
||||||
|
|
@ -415,7 +416,7 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle, Resp
|
||||||
waitForFunction(
|
waitForFunction(
|
||||||
pageFunction: Function | string,
|
pageFunction: Function | string,
|
||||||
options: { polling?: string | number; timeout?: number; } = {},
|
options: { polling?: string | number; timeout?: number; } = {},
|
||||||
...args): Promise<js.JSHandle<ElementHandle, Response>> {
|
...args): Promise<js.JSHandle<ElementHandle>> {
|
||||||
const {
|
const {
|
||||||
polling = 'raf',
|
polling = 'raf',
|
||||||
timeout = this._delegate.timeoutSettings().timeout(),
|
timeout = this._delegate.timeoutSettings().timeout(),
|
||||||
|
|
@ -451,7 +452,7 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle, Resp
|
||||||
this._parentFrame = null;
|
this._parentFrame = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _scheduleWaitTask(params: WaitTaskParams, world: World<ElementHandle, Response>): Promise<js.JSHandle<ElementHandle, Response>> {
|
private _scheduleWaitTask(params: WaitTaskParams, world: World<ElementHandle>): Promise<js.JSHandle<ElementHandle>> {
|
||||||
const task = new WaitTask(params, () => world.waitTasks.delete(task));
|
const task = new WaitTask(params, () => world.waitTasks.delete(task));
|
||||||
world.waitTasks.add(task);
|
world.waitTasks.add(task);
|
||||||
if (world.context)
|
if (world.context)
|
||||||
|
|
@ -459,7 +460,7 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle, Resp
|
||||||
return task.promise;
|
return task.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setContext(worldType: WorldType, context: js.ExecutionContext<ElementHandle, Response> | null) {
|
private _setContext(worldType: WorldType, context: js.ExecutionContext<ElementHandle> | null) {
|
||||||
const world = this._worlds.get(worldType);
|
const world = this._worlds.get(worldType);
|
||||||
world.context = context;
|
world.context = context;
|
||||||
if (context) {
|
if (context) {
|
||||||
|
|
@ -473,7 +474,7 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle, Resp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_contextCreated(worldType: WorldType, context: js.ExecutionContext<ElementHandle, Response>) {
|
_contextCreated(worldType: WorldType, context: js.ExecutionContext<ElementHandle>) {
|
||||||
const world = this._worlds.get(worldType);
|
const world = this._worlds.get(worldType);
|
||||||
// In case of multiple sessions to the same target, there's a race between
|
// In case of multiple sessions to the same target, there's a race between
|
||||||
// connections so we might end up creating multiple isolated worlds.
|
// connections so we might end up creating multiple isolated worlds.
|
||||||
|
|
@ -482,14 +483,14 @@ export class Frame<ElementHandle extends types.ElementHandle<ElementHandle, Resp
|
||||||
this._setContext(worldType, context);
|
this._setContext(worldType, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
_contextDestroyed(context: js.ExecutionContext<ElementHandle, Response>) {
|
_contextDestroyed(context: js.ExecutionContext<ElementHandle>) {
|
||||||
for (const [worldType, world] of this._worlds) {
|
for (const [worldType, world] of this._worlds) {
|
||||||
if (world.context === context)
|
if (world.context === context)
|
||||||
this._setContext(worldType, null);
|
this._setContext(worldType, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _adoptElementHandle(elementHandle: ElementHandle, context: js.ExecutionContext<ElementHandle, Response>, dispose: boolean): Promise<ElementHandle> {
|
private async _adoptElementHandle(elementHandle: ElementHandle, context: js.ExecutionContext<ElementHandle>, dispose: boolean): Promise<ElementHandle> {
|
||||||
if (elementHandle.executionContext() === context)
|
if (elementHandle.executionContext() === context)
|
||||||
return elementHandle;
|
return elementHandle;
|
||||||
const handle = this._delegate.adoptElementHandle(elementHandle, context);
|
const handle = this._delegate.adoptElementHandle(elementHandle, context);
|
||||||
|
|
|
||||||
|
|
@ -7,38 +7,38 @@ import * as injectedSource from './generated/injectedSource';
|
||||||
import * as cssSelectorEngineSource from './generated/cssSelectorEngineSource';
|
import * as cssSelectorEngineSource from './generated/cssSelectorEngineSource';
|
||||||
import * as xpathSelectorEngineSource from './generated/xpathSelectorEngineSource';
|
import * as xpathSelectorEngineSource from './generated/xpathSelectorEngineSource';
|
||||||
|
|
||||||
export interface ExecutionContextDelegate<ElementHandle extends types.ElementHandle<ElementHandle, Response>, Response> {
|
export interface ExecutionContextDelegate<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
||||||
evaluate(context: ExecutionContext<ElementHandle, Response>, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise<any>;
|
evaluate(context: ExecutionContext<ElementHandle>, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise<any>;
|
||||||
getProperties(handle: JSHandle<ElementHandle, Response>): Promise<Map<string, JSHandle<ElementHandle, Response>>>;
|
getProperties(handle: JSHandle<ElementHandle>): Promise<Map<string, JSHandle<ElementHandle>>>;
|
||||||
releaseHandle(handle: JSHandle<ElementHandle, Response>): Promise<void>;
|
releaseHandle(handle: JSHandle<ElementHandle>): Promise<void>;
|
||||||
handleToString(handle: JSHandle<ElementHandle, Response>): string;
|
handleToString(handle: JSHandle<ElementHandle>): string;
|
||||||
handleJSONValue(handle: JSHandle<ElementHandle, Response>): Promise<any>;
|
handleJSONValue(handle: JSHandle<ElementHandle>): Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExecutionContext<ElementHandle extends types.ElementHandle<ElementHandle, Response>, Response> {
|
export class ExecutionContext<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
||||||
_delegate: ExecutionContextDelegate<ElementHandle, Response>;
|
_delegate: ExecutionContextDelegate<ElementHandle>;
|
||||||
private _frame: frames.Frame<ElementHandle, Response>;
|
private _frame: frames.Frame<ElementHandle>;
|
||||||
private _injectedPromise: Promise<JSHandle<ElementHandle, Response>> | null = null;
|
private _injectedPromise: Promise<JSHandle<ElementHandle>> | null = null;
|
||||||
private _documentPromise: Promise<ElementHandle> | null = null;
|
private _documentPromise: Promise<ElementHandle> | null = null;
|
||||||
|
|
||||||
constructor(delegate: ExecutionContextDelegate<ElementHandle, Response>, frame: frames.Frame<ElementHandle, Response> | null) {
|
constructor(delegate: ExecutionContextDelegate<ElementHandle>, frame: frames.Frame<ElementHandle> | null) {
|
||||||
this._delegate = delegate;
|
this._delegate = delegate;
|
||||||
this._frame = frame;
|
this._frame = frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
frame(): frames.Frame<ElementHandle, Response> | null {
|
frame(): frames.Frame<ElementHandle> | null {
|
||||||
return this._frame;
|
return this._frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
evaluate: types.Evaluate<JSHandle<ElementHandle, Response>> = (pageFunction, ...args) => {
|
evaluate: types.Evaluate<JSHandle<ElementHandle>> = (pageFunction, ...args) => {
|
||||||
return this._delegate.evaluate(this, true /* returnByValue */, pageFunction, ...args);
|
return this._delegate.evaluate(this, true /* returnByValue */, pageFunction, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
evaluateHandle: types.EvaluateHandle<JSHandle<ElementHandle, Response>> = (pageFunction, ...args) => {
|
evaluateHandle: types.EvaluateHandle<JSHandle<ElementHandle>> = (pageFunction, ...args) => {
|
||||||
return this._delegate.evaluate(this, false /* returnByValue */, pageFunction, ...args);
|
return this._delegate.evaluate(this, false /* returnByValue */, pageFunction, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
_injected(): Promise<JSHandle<ElementHandle, Response>> {
|
_injected(): Promise<JSHandle<ElementHandle>> {
|
||||||
if (!this._injectedPromise) {
|
if (!this._injectedPromise) {
|
||||||
const engineSources = [cssSelectorEngineSource.source, xpathSelectorEngineSource.source];
|
const engineSources = [cssSelectorEngineSource.source, xpathSelectorEngineSource.source];
|
||||||
const source = `
|
const source = `
|
||||||
|
|
@ -58,27 +58,27 @@ export class ExecutionContext<ElementHandle extends types.ElementHandle<ElementH
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class JSHandle<ElementHandle extends types.ElementHandle<ElementHandle, Response>, Response> {
|
export class JSHandle<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
||||||
_context: ExecutionContext<ElementHandle, Response>;
|
_context: ExecutionContext<ElementHandle>;
|
||||||
_disposed = false;
|
_disposed = false;
|
||||||
|
|
||||||
constructor(context: ExecutionContext<ElementHandle, Response>) {
|
constructor(context: ExecutionContext<ElementHandle>) {
|
||||||
this._context = context;
|
this._context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
executionContext(): ExecutionContext<ElementHandle, Response> {
|
executionContext(): ExecutionContext<ElementHandle> {
|
||||||
return this._context;
|
return this._context;
|
||||||
}
|
}
|
||||||
|
|
||||||
evaluate: types.EvaluateOn<JSHandle<ElementHandle, Response>> = (pageFunction, ...args) => {
|
evaluate: types.EvaluateOn<JSHandle<ElementHandle>> = (pageFunction, ...args) => {
|
||||||
return this._context.evaluate(pageFunction, this, ...args);
|
return this._context.evaluate(pageFunction, this, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
evaluateHandle: types.EvaluateHandleOn<JSHandle<ElementHandle, Response>> = (pageFunction, ...args) => {
|
evaluateHandle: types.EvaluateHandleOn<JSHandle<ElementHandle>> = (pageFunction, ...args) => {
|
||||||
return this._context.evaluateHandle(pageFunction, this, ...args);
|
return this._context.evaluateHandle(pageFunction, this, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProperty(propertyName: string): Promise<JSHandle<ElementHandle, Response> | null> {
|
async getProperty(propertyName: string): Promise<JSHandle<ElementHandle> | null> {
|
||||||
const objectHandle = await this.evaluateHandle((object, propertyName) => {
|
const objectHandle = await this.evaluateHandle((object, propertyName) => {
|
||||||
const result = {__proto__: null};
|
const result = {__proto__: null};
|
||||||
result[propertyName] = object[propertyName];
|
result[propertyName] = object[propertyName];
|
||||||
|
|
@ -90,7 +90,7 @@ export class JSHandle<ElementHandle extends types.ElementHandle<ElementHandle, R
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
getProperties(): Promise<Map<string, JSHandle<ElementHandle, Response>>> {
|
getProperties(): Promise<Map<string, JSHandle<ElementHandle>>> {
|
||||||
return this._context._delegate.getProperties(this);
|
return this._context._delegate.getProperties(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
169
src/network.ts
169
src/network.ts
|
|
@ -1,6 +1,9 @@
|
||||||
// Copyright (c) Microsoft Corporation.
|
// Copyright (c) Microsoft Corporation.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
import * as types from './types';
|
||||||
|
import * as frames from './frames';
|
||||||
|
|
||||||
export type NetworkCookie = {
|
export type NetworkCookie = {
|
||||||
name: string,
|
name: string,
|
||||||
value: string,
|
value: string,
|
||||||
|
|
@ -44,3 +47,169 @@ export function filterCookies(cookies: NetworkCookie[], urls: string[]) {
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Headers = { [key: string]: string };
|
||||||
|
|
||||||
|
export class Request<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
||||||
|
_response: Response<ElementHandle> | null = null;
|
||||||
|
_redirectChain: Request<ElementHandle>[];
|
||||||
|
private _isNavigationRequest: boolean;
|
||||||
|
private _failureText: string | null = null;
|
||||||
|
private _url: string;
|
||||||
|
private _resourceType: string;
|
||||||
|
private _method: string;
|
||||||
|
private _postData: string;
|
||||||
|
private _headers: Headers;
|
||||||
|
private _frame: frames.Frame<ElementHandle>;
|
||||||
|
|
||||||
|
constructor(frame: frames.Frame<ElementHandle> | null, redirectChain: Request<ElementHandle>[], isNavigationRequest: boolean,
|
||||||
|
url: string, resourceType: string, method: string, postData: string, headers: Headers) {
|
||||||
|
this._frame = frame;
|
||||||
|
this._redirectChain = redirectChain;
|
||||||
|
this._isNavigationRequest = isNavigationRequest;
|
||||||
|
this._url = url;
|
||||||
|
this._resourceType = resourceType;
|
||||||
|
this._method = method;
|
||||||
|
this._postData = postData;
|
||||||
|
this._headers = headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setFailureText(failureText: string) {
|
||||||
|
this._failureText = failureText;
|
||||||
|
}
|
||||||
|
|
||||||
|
url(): string {
|
||||||
|
return this._url;
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceType(): string {
|
||||||
|
return this._resourceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
method(): string {
|
||||||
|
return this._method;
|
||||||
|
}
|
||||||
|
|
||||||
|
postData(): string | undefined {
|
||||||
|
return this._postData;
|
||||||
|
}
|
||||||
|
|
||||||
|
headers(): {[key: string]: string} {
|
||||||
|
return this._headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
response(): Response<ElementHandle> | null {
|
||||||
|
return this._response;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame(): frames.Frame<ElementHandle> | null {
|
||||||
|
return this._frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
isNavigationRequest(): boolean {
|
||||||
|
return this._isNavigationRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectChain(): Request<ElementHandle>[] {
|
||||||
|
return this._redirectChain.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
failure(): { errorText: string; } | null {
|
||||||
|
if (!this._failureText)
|
||||||
|
return null;
|
||||||
|
return {
|
||||||
|
errorText: this._failureText
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RemoteAddress = {
|
||||||
|
ip: string,
|
||||||
|
port: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
type GetResponseBodyCallback = () => Promise<Buffer>;
|
||||||
|
|
||||||
|
export class Response<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
||||||
|
private _request: Request<ElementHandle>;
|
||||||
|
private _contentPromise: Promise<Buffer> | null = null;
|
||||||
|
private _bodyLoadedPromise: Promise<Error | null>;
|
||||||
|
private _bodyLoadedPromiseFulfill: any;
|
||||||
|
private _remoteAddress: RemoteAddress;
|
||||||
|
private _status: number;
|
||||||
|
private _statusText: string;
|
||||||
|
private _url: string;
|
||||||
|
private _headers: Headers;
|
||||||
|
private _getResponseBodyCallback: GetResponseBodyCallback;
|
||||||
|
|
||||||
|
constructor(request: Request<ElementHandle>, status: number, statusText: string, headers: Headers, remoteAddress: RemoteAddress, getResponseBodyCallback: GetResponseBodyCallback) {
|
||||||
|
this._request = request;
|
||||||
|
this._request._response = this;
|
||||||
|
this._status = status;
|
||||||
|
this._statusText = statusText;
|
||||||
|
this._url = request.url();
|
||||||
|
this._headers = headers;
|
||||||
|
this._remoteAddress = remoteAddress;
|
||||||
|
this._getResponseBodyCallback = getResponseBodyCallback;
|
||||||
|
this._bodyLoadedPromise = new Promise(fulfill => {
|
||||||
|
this._bodyLoadedPromiseFulfill = fulfill;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_bodyLoaded(error?: Error) {
|
||||||
|
this._bodyLoadedPromiseFulfill.call(null, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteAddress(): RemoteAddress {
|
||||||
|
return this._remoteAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
url(): string {
|
||||||
|
return this._url;
|
||||||
|
}
|
||||||
|
|
||||||
|
ok(): boolean {
|
||||||
|
return this._status === 0 || (this._status >= 200 && this._status <= 299);
|
||||||
|
}
|
||||||
|
|
||||||
|
status(): number {
|
||||||
|
return this._status;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusText(): string {
|
||||||
|
return this._statusText;
|
||||||
|
}
|
||||||
|
|
||||||
|
headers(): object {
|
||||||
|
return this._headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer(): Promise<Buffer> {
|
||||||
|
if (!this._contentPromise) {
|
||||||
|
this._contentPromise = this._bodyLoadedPromise.then(async error => {
|
||||||
|
if (error)
|
||||||
|
throw error;
|
||||||
|
return this._getResponseBodyCallback();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this._contentPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async text(): Promise<string> {
|
||||||
|
const content = await this.buffer();
|
||||||
|
return content.toString('utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
async json(): Promise<object> {
|
||||||
|
const content = await this.text();
|
||||||
|
return JSON.parse(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
request(): Request<ElementHandle> {
|
||||||
|
return this._request;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame(): frames.Frame<ElementHandle> | null {
|
||||||
|
return this._request.frame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,12 @@ export type $$Eval<Handle> = <Args extends any[], R>(selector: string, pageFunct
|
||||||
export type EvaluateOn<Handle> = <Args extends any[], R>(pageFunction: PageFunctionOn<any, Args, R>, ...args: Boxed<Args, Handle>) => Promise<R>;
|
export type EvaluateOn<Handle> = <Args extends any[], R>(pageFunction: PageFunctionOn<any, Args, R>, ...args: Boxed<Args, Handle>) => Promise<R>;
|
||||||
export type EvaluateHandleOn<Handle> = <Args extends any[]>(pageFunction: PageFunctionOn<any, Args>, ...args: Boxed<Args, Handle>) => Promise<Handle>;
|
export type EvaluateHandleOn<Handle> = <Args extends any[]>(pageFunction: PageFunctionOn<any, Args>, ...args: Boxed<Args, Handle>) => Promise<Handle>;
|
||||||
|
|
||||||
export interface ElementHandle<EHandle extends ElementHandle<EHandle, Response>, Response> extends js.JSHandle<EHandle, Response> {
|
export interface ElementHandle<EHandle extends ElementHandle<EHandle>> extends js.JSHandle<EHandle> {
|
||||||
$(selector: string): Promise<EHandle | null>;
|
$(selector: string): Promise<EHandle | null>;
|
||||||
$x(expression: string): Promise<EHandle[]>;
|
$x(expression: string): Promise<EHandle[]>;
|
||||||
$$(selector: string): Promise<EHandle[]>;
|
$$(selector: string): Promise<EHandle[]>;
|
||||||
$eval: $Eval<js.JSHandle<EHandle, Response>>;
|
$eval: $Eval<js.JSHandle<EHandle>>;
|
||||||
$$eval: $$Eval<js.JSHandle<EHandle, Response>>;
|
$$eval: $$Eval<js.JSHandle<EHandle>>;
|
||||||
click(options?: input.ClickOptions): Promise<void>;
|
click(options?: input.ClickOptions): Promise<void>;
|
||||||
dblclick(options?: input.MultiClickOptions): Promise<void>;
|
dblclick(options?: input.MultiClickOptions): Promise<void>;
|
||||||
tripleclick(options?: input.MultiClickOptions): Promise<void>;
|
tripleclick(options?: input.MultiClickOptions): Promise<void>;
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,12 @@ export type WaitTaskParams = {
|
||||||
args: any[];
|
args: any[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export class WaitTask<ElementHandle extends types.ElementHandle<ElementHandle, Response>, Response> {
|
export class WaitTask<ElementHandle extends types.ElementHandle<ElementHandle>> {
|
||||||
readonly promise: Promise<js.JSHandle<ElementHandle, Response>>;
|
readonly promise: Promise<js.JSHandle<ElementHandle>>;
|
||||||
private _cleanup: () => void;
|
private _cleanup: () => void;
|
||||||
private _params: WaitTaskParams & { predicateBody: string };
|
private _params: WaitTaskParams & { predicateBody: string };
|
||||||
private _runCount: number;
|
private _runCount: number;
|
||||||
private _resolve: (result: js.JSHandle<ElementHandle, Response>) => void;
|
private _resolve: (result: js.JSHandle<ElementHandle>) => void;
|
||||||
private _reject: (reason: Error) => void;
|
private _reject: (reason: Error) => void;
|
||||||
private _timeoutTimer: NodeJS.Timer;
|
private _timeoutTimer: NodeJS.Timer;
|
||||||
private _terminated: boolean;
|
private _terminated: boolean;
|
||||||
|
|
@ -39,7 +39,7 @@ export class WaitTask<ElementHandle extends types.ElementHandle<ElementHandle, R
|
||||||
};
|
};
|
||||||
this._cleanup = cleanup;
|
this._cleanup = cleanup;
|
||||||
this._runCount = 0;
|
this._runCount = 0;
|
||||||
this.promise = new Promise<js.JSHandle<ElementHandle, Response>>((resolve, reject) => {
|
this.promise = new Promise<js.JSHandle<ElementHandle>>((resolve, reject) => {
|
||||||
this._resolve = resolve;
|
this._resolve = resolve;
|
||||||
this._reject = reject;
|
this._reject = reject;
|
||||||
});
|
});
|
||||||
|
|
@ -57,9 +57,9 @@ export class WaitTask<ElementHandle extends types.ElementHandle<ElementHandle, R
|
||||||
this._doCleanup();
|
this._doCleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
async rerun(context: js.ExecutionContext<ElementHandle, Response>) {
|
async rerun(context: js.ExecutionContext<ElementHandle>) {
|
||||||
const runCount = ++this._runCount;
|
const runCount = ++this._runCount;
|
||||||
let success: js.JSHandle<ElementHandle, Response> | null = null;
|
let success: js.JSHandle<ElementHandle> | null = null;
|
||||||
let error = null;
|
let error = null;
|
||||||
try {
|
try {
|
||||||
success = await context.evaluateHandle(waitForPredicatePageFunction, this._params.predicateBody, this._params.polling, this._params.timeout, ...this._params.args);
|
success = await context.evaluateHandle(waitForPredicatePageFunction, this._params.predicateBody, this._params.polling, this._params.timeout, ...this._params.args);
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,15 @@ import { helper } from '../helper';
|
||||||
import { valueFromRemoteObject, releaseObject } from './protocolHelper';
|
import { valueFromRemoteObject, releaseObject } from './protocolHelper';
|
||||||
import { createJSHandle, ElementHandle } from './JSHandle';
|
import { createJSHandle, ElementHandle } from './JSHandle';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import { Response } from './NetworkManager';
|
|
||||||
import * as js from '../javascript';
|
import * as js from '../javascript';
|
||||||
|
|
||||||
export const EVALUATION_SCRIPT_URL = '__playwright_evaluation_script__';
|
export const EVALUATION_SCRIPT_URL = '__playwright_evaluation_script__';
|
||||||
const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
|
const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
|
||||||
|
|
||||||
export type ExecutionContext = js.ExecutionContext<ElementHandle, Response>;
|
export type ExecutionContext = js.ExecutionContext<ElementHandle>;
|
||||||
export type JSHandle = js.JSHandle<ElementHandle, Response>;
|
export type JSHandle = js.JSHandle<ElementHandle>;
|
||||||
|
|
||||||
export class ExecutionContextDelegate implements js.ExecutionContextDelegate<ElementHandle, Response> {
|
export class ExecutionContextDelegate implements js.ExecutionContextDelegate<ElementHandle> {
|
||||||
private _globalObjectId?: string;
|
private _globalObjectId?: string;
|
||||||
_session: TargetSession;
|
_session: TargetSession;
|
||||||
private _contextId: number;
|
private _contextId: number;
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,9 @@ type FrameData = {
|
||||||
id: string,
|
id: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Frame = frames.Frame<ElementHandle, Response>;
|
export type Frame = frames.Frame<ElementHandle>;
|
||||||
|
|
||||||
export class FrameManager extends EventEmitter implements frames.FrameDelegate<ElementHandle, Response> {
|
export class FrameManager extends EventEmitter implements frames.FrameDelegate<ElementHandle> {
|
||||||
_session: TargetSession;
|
_session: TargetSession;
|
||||||
_page: Page;
|
_page: Page;
|
||||||
_networkManager: NetworkManager;
|
_networkManager: NetworkManager;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import { assert, debugError, helper } from '../helper';
|
||||||
import * as input from '../input';
|
import * as input from '../input';
|
||||||
import { TargetSession } from './Connection';
|
import { TargetSession } from './Connection';
|
||||||
import { JSHandle, ExecutionContext, ExecutionContextDelegate, markJSHandle } from './ExecutionContext';
|
import { JSHandle, ExecutionContext, ExecutionContextDelegate, markJSHandle } from './ExecutionContext';
|
||||||
import { Response } from './NetworkManager';
|
|
||||||
import { FrameManager } from './FrameManager';
|
import { FrameManager } from './FrameManager';
|
||||||
import { Page } from './Page';
|
import { Page } from './Page';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
|
|
@ -44,7 +43,7 @@ export function createJSHandle(context: ExecutionContext, remoteObject: Protocol
|
||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ElementHandle extends js.JSHandle<ElementHandle, Response> {
|
export class ElementHandle extends js.JSHandle<ElementHandle> {
|
||||||
private _client: TargetSession;
|
private _client: TargetSession;
|
||||||
private _remoteObject: Protocol.Runtime.RemoteObject;
|
private _remoteObject: Protocol.Runtime.RemoteObject;
|
||||||
private _page: Page;
|
private _page: Page;
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ import { TargetSession } from './Connection';
|
||||||
import { Frame, FrameManager } from './FrameManager';
|
import { Frame, FrameManager } from './FrameManager';
|
||||||
import { assert, helper, RegisteredListener } from '../helper';
|
import { assert, helper, RegisteredListener } from '../helper';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
|
import * as network from '../network';
|
||||||
|
import { ElementHandle } from './JSHandle';
|
||||||
|
|
||||||
export const NetworkManagerEvents = {
|
export const NetworkManagerEvents = {
|
||||||
Request: Symbol('Events.NetworkManager.Request'),
|
Request: Symbol('Events.NetworkManager.Request'),
|
||||||
|
|
@ -28,11 +30,14 @@ export const NetworkManagerEvents = {
|
||||||
RequestFinished: Symbol('Events.NetworkManager.RequestFinished'),
|
RequestFinished: Symbol('Events.NetworkManager.RequestFinished'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Request = network.Request<ElementHandle>;
|
||||||
|
export type Response = network.Response<ElementHandle>;
|
||||||
|
|
||||||
export class NetworkManager extends EventEmitter {
|
export class NetworkManager extends EventEmitter {
|
||||||
private _sesssion: TargetSession;
|
private _sesssion: TargetSession;
|
||||||
private _frameManager: FrameManager;
|
private _frameManager: FrameManager;
|
||||||
private _requestIdToRequest = new Map<string, Request>();
|
private _requestIdToRequest = new Map<string, InterceptableRequest>();
|
||||||
private _extraHTTPHeaders: {[key: string]: string} = {};
|
private _extraHTTPHeaders: network.Headers = {};
|
||||||
private _attemptedAuthentications = new Set<string>();
|
private _attemptedAuthentications = new Set<string>();
|
||||||
private _userCacheDisabled = false;
|
private _userCacheDisabled = false;
|
||||||
private _sessionListeners: RegisteredListener[] = [];
|
private _sessionListeners: RegisteredListener[] = [];
|
||||||
|
|
@ -89,30 +94,38 @@ export class NetworkManager extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRequestWillBeSent(event: Protocol.Network.requestWillBeSentPayload, interceptionId: string | null) {
|
_onRequestWillBeSent(event: Protocol.Network.requestWillBeSentPayload, interceptionId: string | null) {
|
||||||
let redirectChain = [];
|
let redirectChain: Request[] = [];
|
||||||
if (event.redirectResponse) {
|
if (event.redirectResponse) {
|
||||||
const request = this._requestIdToRequest.get(event.requestId);
|
const request = this._requestIdToRequest.get(event.requestId);
|
||||||
// If we connect late to the target, we could have missed the requestWillBeSent event.
|
// If we connect late to the target, we could have missed the requestWillBeSent event.
|
||||||
if (request) {
|
if (request) {
|
||||||
this._handleRequestRedirect(request, event.redirectResponse);
|
this._handleRequestRedirect(request, event.redirectResponse);
|
||||||
redirectChain = request._redirectChain;
|
redirectChain = request.request._redirectChain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const frame = event.frameId && this._frameManager ? this._frameManager.frame(event.frameId) : null;
|
const frame = event.frameId && this._frameManager ? this._frameManager.frame(event.frameId) : null;
|
||||||
const request = new Request(frame, interceptionId, event, redirectChain);
|
const request = new InterceptableRequest(frame, interceptionId, event, redirectChain);
|
||||||
this._requestIdToRequest.set(event.requestId, request);
|
this._requestIdToRequest.set(event.requestId, request);
|
||||||
this.emit(NetworkManagerEvents.Request, request);
|
this.emit(NetworkManagerEvents.Request, request.request);
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleRequestRedirect(request: Request, responsePayload: Protocol.Network.Response) {
|
_createResponse(request: InterceptableRequest, responsePayload: Protocol.Network.Response): Response {
|
||||||
const response = new Response(this._sesssion, request, responsePayload);
|
const remoteAddress: network.RemoteAddress = { ip: '', port: 0 };
|
||||||
request._response = response;
|
const getResponseBody = async () => {
|
||||||
request._redirectChain.push(request);
|
const response = await this._sesssion.send('Network.getResponseBody', { requestId: request._requestId });
|
||||||
response._bodyLoadedPromiseFulfill.call(null, new Error('Response body is unavailable for redirect responses'));
|
return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');
|
||||||
|
};
|
||||||
|
return new network.Response(request.request, responsePayload.status, responsePayload.statusText, headersObject(responsePayload.headers), remoteAddress, getResponseBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleRequestRedirect(request: InterceptableRequest, responsePayload: Protocol.Network.Response) {
|
||||||
|
const response = this._createResponse(request, responsePayload);
|
||||||
|
request.request._redirectChain.push(request.request);
|
||||||
|
response._bodyLoaded(new Error('Response body is unavailable for redirect responses'));
|
||||||
this._requestIdToRequest.delete(request._requestId);
|
this._requestIdToRequest.delete(request._requestId);
|
||||||
this._attemptedAuthentications.delete(request._interceptionId);
|
this._attemptedAuthentications.delete(request._interceptionId);
|
||||||
this.emit(NetworkManagerEvents.Response, response);
|
this.emit(NetworkManagerEvents.Response, response);
|
||||||
this.emit(NetworkManagerEvents.RequestFinished, request);
|
this.emit(NetworkManagerEvents.RequestFinished, request.request);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onResponseReceived(event: Protocol.Network.responseReceivedPayload) {
|
_onResponseReceived(event: Protocol.Network.responseReceivedPayload) {
|
||||||
|
|
@ -120,8 +133,7 @@ export class NetworkManager extends EventEmitter {
|
||||||
// FileUpload sends a response without a matching request.
|
// FileUpload sends a response without a matching request.
|
||||||
if (!request)
|
if (!request)
|
||||||
return;
|
return;
|
||||||
const response = new Response(this._sesssion, request, event.response);
|
const response = this._createResponse(request, event.response);
|
||||||
request._response = response;
|
|
||||||
this.emit(NetworkManagerEvents.Response, response);
|
this.emit(NetworkManagerEvents.Response, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,11 +146,11 @@ export class NetworkManager extends EventEmitter {
|
||||||
|
|
||||||
// Under certain conditions we never get the Network.responseReceived
|
// Under certain conditions we never get the Network.responseReceived
|
||||||
// event from protocol. @see https://crbug.com/883475
|
// event from protocol. @see https://crbug.com/883475
|
||||||
if (request.response())
|
if (request.request.response())
|
||||||
request.response()._bodyLoadedPromiseFulfill.call(null);
|
request.request.response()._bodyLoaded();
|
||||||
this._requestIdToRequest.delete(request._requestId);
|
this._requestIdToRequest.delete(request._requestId);
|
||||||
this._attemptedAuthentications.delete(request._interceptionId);
|
this._attemptedAuthentications.delete(request._interceptionId);
|
||||||
this.emit(NetworkManagerEvents.RequestFinished, request);
|
this.emit(NetworkManagerEvents.RequestFinished, request.request);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onLoadingFailed(event: Protocol.Network.loadingFailedPayload) {
|
_onLoadingFailed(event: Protocol.Network.loadingFailedPayload) {
|
||||||
|
|
@ -147,166 +159,41 @@ export class NetworkManager extends EventEmitter {
|
||||||
// @see https://crbug.com/750469
|
// @see https://crbug.com/750469
|
||||||
if (!request)
|
if (!request)
|
||||||
return;
|
return;
|
||||||
request._failureText = event.errorText;
|
request.request._setFailureText(event.errorText);
|
||||||
const response = request.response();
|
const response = request.request.response();
|
||||||
if (response)
|
if (response)
|
||||||
response._bodyLoadedPromiseFulfill.call(null);
|
response._bodyLoaded();
|
||||||
this._requestIdToRequest.delete(request._requestId);
|
this._requestIdToRequest.delete(request._requestId);
|
||||||
this._attemptedAuthentications.delete(request._interceptionId);
|
this._attemptedAuthentications.delete(request._interceptionId);
|
||||||
this.emit(NetworkManagerEvents.RequestFailed, request);
|
this.emit(NetworkManagerEvents.RequestFailed, request.request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Request {
|
const interceptableRequestSymbol = Symbol('interceptableRequest');
|
||||||
_response: Response | null = null;
|
|
||||||
_redirectChain: Request[];
|
export function toInterceptableRequest(request: network.Request<ElementHandle>): InterceptableRequest {
|
||||||
|
return (request as any)[interceptableRequestSymbol];
|
||||||
|
}
|
||||||
|
|
||||||
|
class InterceptableRequest {
|
||||||
|
readonly request: Request;
|
||||||
_requestId: string;
|
_requestId: string;
|
||||||
_interceptionId: string;
|
_interceptionId: string;
|
||||||
private _isNavigationRequest: boolean;
|
|
||||||
_failureText: string | null = null;
|
|
||||||
private _url: string;
|
|
||||||
private _resourceType: string;
|
|
||||||
private _method: string;
|
|
||||||
private _postData: string;
|
|
||||||
private _headers: {[key: string]: string} = {};
|
|
||||||
private _frame: Frame;
|
|
||||||
|
|
||||||
constructor(frame: Frame | null, interceptionId: string, event: Protocol.Network.requestWillBeSentPayload, redirectChain: Request[]) {
|
constructor(frame: Frame | null, interceptionId: string, event: Protocol.Network.requestWillBeSentPayload, redirectChain: Request[]) {
|
||||||
this._requestId = event.requestId;
|
this._requestId = event.requestId;
|
||||||
// TODO(einbinder) this will fail if we are an XHR document request
|
// TODO(einbinder) this will fail if we are an XHR document request
|
||||||
this._isNavigationRequest = event.type === 'Document';
|
const isNavigationRequest = event.type === 'Document';
|
||||||
this._interceptionId = interceptionId;
|
this._interceptionId = interceptionId;
|
||||||
|
this.request = new network.Request(frame, redirectChain, isNavigationRequest, event.request.url,
|
||||||
this._url = event.request.url;
|
event.type ? event.type.toLowerCase() : 'Unknown', event.request.method, event.request.postData, headersObject(event.request.headers));
|
||||||
this._resourceType = event.type ? event.type.toLowerCase() : 'Unknown';
|
(this.request as any)[interceptableRequestSymbol] = this;
|
||||||
this._method = event.request.method;
|
|
||||||
this._postData = event.request.postData;
|
|
||||||
this._frame = frame;
|
|
||||||
this._redirectChain = redirectChain;
|
|
||||||
for (const key of Object.keys(event.request.headers))
|
|
||||||
this._headers[key.toLowerCase()] = event.request.headers[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
url(): string {
|
|
||||||
return this._url;
|
|
||||||
}
|
|
||||||
|
|
||||||
resourceType(): string {
|
|
||||||
return this._resourceType;
|
|
||||||
}
|
|
||||||
|
|
||||||
method(): string {
|
|
||||||
return this._method;
|
|
||||||
}
|
|
||||||
|
|
||||||
postData(): string | undefined {
|
|
||||||
return this._postData;
|
|
||||||
}
|
|
||||||
|
|
||||||
headers(): {[key: string]: string} {
|
|
||||||
return this._headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
response(): Response | null {
|
|
||||||
return this._response;
|
|
||||||
}
|
|
||||||
|
|
||||||
frame(): Frame | null {
|
|
||||||
return this._frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
isNavigationRequest(): boolean {
|
|
||||||
return this._isNavigationRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
redirectChain(): Request[] {
|
|
||||||
return this._redirectChain.slice();
|
|
||||||
}
|
|
||||||
|
|
||||||
failure(): { errorText: string; } | null {
|
|
||||||
if (!this._failureText)
|
|
||||||
return null;
|
|
||||||
return {
|
|
||||||
errorText: this._failureText
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Response {
|
function headersObject(headers: Protocol.Network.Headers): network.Headers {
|
||||||
_bodyLoadedPromiseFulfill: any;
|
const result: network.Headers = {};
|
||||||
private _client: TargetSession;
|
for (const key of Object.keys(headers))
|
||||||
private _request: Request;
|
result[key.toLowerCase()] = headers[key];
|
||||||
private _contentPromise: Promise<Buffer> | null = null;
|
return result;
|
||||||
private _bodyLoadedPromise: Promise<Error | null>;
|
|
||||||
private _status: number;
|
|
||||||
private _statusText: string;
|
|
||||||
private _url: string;
|
|
||||||
private _headers: {[key: string]: string} = {};
|
|
||||||
|
|
||||||
constructor(client: TargetSession, request: Request, responsePayload: Protocol.Network.Response) {
|
|
||||||
this._client = client;
|
|
||||||
this._request = request;
|
|
||||||
|
|
||||||
this._bodyLoadedPromise = new Promise(fulfill => {
|
|
||||||
this._bodyLoadedPromiseFulfill = fulfill;
|
|
||||||
});
|
|
||||||
|
|
||||||
this._status = responsePayload.status;
|
|
||||||
this._statusText = responsePayload.statusText;
|
|
||||||
this._url = request.url();
|
|
||||||
for (const key of Object.keys(responsePayload.headers))
|
|
||||||
this._headers[key.toLowerCase()] = responsePayload.headers[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
url(): string {
|
|
||||||
return this._url;
|
|
||||||
}
|
|
||||||
|
|
||||||
ok(): boolean {
|
|
||||||
return this._status === 0 || (this._status >= 200 && this._status <= 299);
|
|
||||||
}
|
|
||||||
|
|
||||||
status(): number {
|
|
||||||
return this._status;
|
|
||||||
}
|
|
||||||
|
|
||||||
statusText(): string {
|
|
||||||
return this._statusText;
|
|
||||||
}
|
|
||||||
|
|
||||||
headers(): object {
|
|
||||||
return this._headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer(): Promise<Buffer> {
|
|
||||||
if (!this._contentPromise) {
|
|
||||||
this._contentPromise = this._bodyLoadedPromise.then(async error => {
|
|
||||||
if (error)
|
|
||||||
throw error;
|
|
||||||
const response = await this._client.send('Network.getResponseBody', {
|
|
||||||
requestId: this._request._requestId
|
|
||||||
});
|
|
||||||
return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this._contentPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
async text(): Promise<string> {
|
|
||||||
const content = await this.buffer();
|
|
||||||
return content.toString('utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
async json(): Promise<object> {
|
|
||||||
const content = await this.text();
|
|
||||||
return JSON.parse(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
request(): Request {
|
|
||||||
return this._request;
|
|
||||||
}
|
|
||||||
|
|
||||||
frame(): Frame | null {
|
|
||||||
return this._request.frame();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export { ExecutionContext, JSHandle } from '../javascript';
|
||||||
export { Frame } from './FrameManager';
|
export { Frame } from './FrameManager';
|
||||||
export { Mouse, Keyboard } from '../input';
|
export { Mouse, Keyboard } from '../input';
|
||||||
export { ElementHandle } from './JSHandle';
|
export { ElementHandle } from './JSHandle';
|
||||||
export { Request, Response } from './NetworkManager';
|
export { Request, Response } from '../network';
|
||||||
export { ConsoleMessage, Page } from './Page';
|
export { ConsoleMessage, Page } from './Page';
|
||||||
export { Playwright } from './Playwright';
|
export { Playwright } from './Playwright';
|
||||||
export { Target } from './Target';
|
export { Target } from './Target';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue