diff --git a/src/chromium/ExecutionContext.ts b/src/chromium/ExecutionContext.ts index 37d5ee6d3d..5dc847ca8a 100644 --- a/src/chromium/ExecutionContext.ts +++ b/src/chromium/ExecutionContext.ts @@ -20,16 +20,15 @@ import { helper } from '../helper'; import { valueFromRemoteObject, getExceptionMessage, releaseObject } from './protocolHelper'; import { createJSHandle, ElementHandle } from './JSHandle'; import { Protocol } from './protocol'; -import { Response } from './NetworkManager'; import * as js from '../javascript'; export const EVALUATION_SCRIPT_URL = '__playwright_evaluation_script__'; const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m; -export type ExecutionContext = js.ExecutionContext; -export type JSHandle = js.JSHandle; +export type ExecutionContext = js.ExecutionContext; +export type JSHandle = js.JSHandle; -export class ExecutionContextDelegate implements js.ExecutionContextDelegate { +export class ExecutionContextDelegate implements js.ExecutionContextDelegate { _client: CDPSession; _contextId: number; diff --git a/src/chromium/FrameManager.ts b/src/chromium/FrameManager.ts index 906a793c09..ebfe4014af 100644 --- a/src/chromium/FrameManager.ts +++ b/src/chromium/FrameManager.ts @@ -45,9 +45,9 @@ type FrameData = { lifecycleEvents: Set, }; -export type Frame = frames.Frame; +export type Frame = frames.Frame; -export class FrameManager extends EventEmitter implements frames.FrameDelegate { +export class FrameManager extends EventEmitter implements frames.FrameDelegate { _client: CDPSession; private _page: Page; private _networkManager: NetworkManager; diff --git a/src/chromium/JSHandle.ts b/src/chromium/JSHandle.ts index e419e375c8..e89321975b 100644 --- a/src/chromium/JSHandle.ts +++ b/src/chromium/JSHandle.ts @@ -26,7 +26,6 @@ import { FrameManager } from './FrameManager'; import { Page } from './Page'; import { Protocol } from './protocol'; import { JSHandle, ExecutionContext, ExecutionContextDelegate, markJSHandle } from './ExecutionContext'; -import { Response } from './NetworkManager'; type SelectorRoot = Element | ShadowRoot | Document; @@ -47,7 +46,7 @@ export function createJSHandle(context: ExecutionContext, remoteObject: Protocol return handle; } -export class ElementHandle extends js.JSHandle { +export class ElementHandle extends js.JSHandle { private _client: CDPSession; private _remoteObject: Protocol.Runtime.RemoteObject; private _page: Page; diff --git a/src/chromium/NetworkManager.ts b/src/chromium/NetworkManager.ts index 347310a655..09deddea42 100644 --- a/src/chromium/NetworkManager.ts +++ b/src/chromium/NetworkManager.ts @@ -21,6 +21,8 @@ import { Frame } from './FrameManager'; import { FrameManager } from './FrameManager'; import { assert, debugError, helper } from '../helper'; import { Protocol } from './protocol'; +import * as network from '../network'; +import { ElementHandle } from './JSHandle'; export const NetworkManagerEvents = { Request: Symbol('Events.NetworkManager.Request'), @@ -29,13 +31,16 @@ export const NetworkManagerEvents = { RequestFinished: Symbol('Events.NetworkManager.RequestFinished'), }; +export type Request = network.Request; +export type Response = network.Response; + export class NetworkManager extends EventEmitter { private _client: CDPSession; private _ignoreHTTPSErrors: boolean; private _frameManager: FrameManager; - private _requestIdToRequest = new Map(); + private _requestIdToRequest = new Map(); private _requestIdToRequestWillBeSentEvent = new Map(); - private _extraHTTPHeaders: {[key: string]: string} = {}; + private _extraHTTPHeaders: network.Headers = {}; private _offline = false; private _credentials: {username: string, password: string} | null = null; private _attemptedAuthentications = new Set(); @@ -69,7 +74,7 @@ export class NetworkManager extends EventEmitter { await this._updateProtocolRequestInterception(); } - async setExtraHTTPHeaders(extraHTTPHeaders: { [s: string]: string; }) { + async setExtraHTTPHeaders(extraHTTPHeaders: network.Headers) { this._extraHTTPHeaders = {}; for (const key of Object.keys(extraHTTPHeaders)) { const value = extraHTTPHeaders[key]; @@ -79,7 +84,7 @@ export class NetworkManager extends EventEmitter { await this._client.send('Network.setExtraHTTPHeaders', { headers: this._extraHTTPHeaders }); } - extraHTTPHeaders(): { [s: string]: string; } { + extraHTTPHeaders(): network.Headers { return Object.assign({}, this._extraHTTPHeaders); } @@ -187,31 +192,38 @@ export class NetworkManager extends EventEmitter { } _onRequest(event: Protocol.Network.requestWillBeSentPayload, interceptionId: string | null) { - let redirectChain = []; + let redirectChain: Request[] = []; if (event.redirectResponse) { const request = this._requestIdToRequest.get(event.requestId); // If we connect late to the target, we could have missed the requestWillBeSent event. if (request) { this._handleRequestRedirect(request, event.redirectResponse); - redirectChain = request._redirectChain; + redirectChain = request.request._redirectChain; } } 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.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) { - const response = new Response(this._client, request, responsePayload); - request._response = response; - request._redirectChain.push(request); - response._bodyLoadedPromiseFulfill.call(null, new Error('Response body is unavailable for redirect responses')); + _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._attemptedAuthentications.delete(request._interceptionId); this.emit(NetworkManagerEvents.Response, response); - this.emit(NetworkManagerEvents.RequestFinished, request); + this.emit(NetworkManagerEvents.RequestFinished, request.request); } _onResponseReceived(event: Protocol.Network.responseReceivedPayload) { @@ -219,8 +231,7 @@ export class NetworkManager extends EventEmitter { // FileUpload sends a response without a matching request. if (!request) return; - const response = new Response(this._client, request, event.response); - request._response = response; + const response = this._createResponse(request, event.response); this.emit(NetworkManagerEvents.Response, response); } @@ -233,11 +244,11 @@ export class NetworkManager extends EventEmitter { // Under certain conditions we never get the Network.responseReceived // event from protocol. @see https://crbug.com/883475 - if (request.response()) - request.response()._bodyLoadedPromiseFulfill.call(null); + if (request.request.response()) + request.request.response()._bodyLoaded(); this._requestIdToRequest.delete(request._requestId); this._attemptedAuthentications.delete(request._interceptionId); - this.emit(NetworkManagerEvents.RequestFinished, request); + this.emit(NetworkManagerEvents.RequestFinished, request.request); } _onLoadingFailed(event: Protocol.Network.loadingFailedPayload) { @@ -246,97 +257,44 @@ export class NetworkManager extends EventEmitter { // @see https://crbug.com/750469 if (!request) return; - request._failureText = event.errorText; - const response = request.response(); + request.request._setFailureText(event.errorText); + const response = request.request.response(); if (response) - response._bodyLoadedPromiseFulfill.call(null); + response._bodyLoaded(); this._requestIdToRequest.delete(request._requestId); this._attemptedAuthentications.delete(request._interceptionId); - this.emit(NetworkManagerEvents.RequestFailed, request); + this.emit(NetworkManagerEvents.RequestFailed, request.request); } } -export class Request { - _response: Response | null = null; - _redirectChain: Request[]; +const interceptableRequestSymbol = Symbol('interceptableRequest'); + +export function toInterceptableRequest(request: network.Request): InterceptableRequest { + return (request as any)[interceptableRequestSymbol]; +} + +class InterceptableRequest { + readonly request: Request; _requestId: string; _interceptionId: string; private _client: CDPSession; - private _isNavigationRequest: boolean; private _allowInterception: boolean; 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[]) { this._client = client; this._requestId = event.requestId; - this._isNavigationRequest = event.requestId === event.loaderId && event.type === 'Document'; this._interceptionId = interceptionId; this._allowInterception = allowInterception; - this._url = event.request.url; - this._resourceType = event.type.toLowerCase(); - 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]; + this.request = new network.Request(frame, redirectChain, event.requestId === event.loaderId && event.type === 'Document', + event.request.url, event.type.toLowerCase(), event.request.method, event.request.postData, headersObject(event.request.headers)); + (this.request as any)[interceptableRequestSymbol] = this; } - 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 - }; - } - - async _continue(overrides: { url?: string; method?: string; postData?: string; headers?: {[key: string]: string}; } = {}) { + async continue(overrides: { url?: string; method?: string; postData?: string; headers?: {[key: string]: string}; } = {}) { // Request interception is not supported for data: urls. - if (this._url.startsWith('data:')) + if (this.request.url().startsWith('data:')) return; assert(this._allowInterception, 'Request Interception is not enabled!'); 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. - if (this._url.startsWith('data:')) + if (this.request.url().startsWith('data:')) return; assert(this._allowInterception, 'Request Interception is not enabled!'); 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. - if (this._url.startsWith('data:')) + if (this.request.url().startsWith('data:')) return; const errorReason = errorReasons[errorCode]; assert(errorReason, 'Unknown error code: ' + errorCode); @@ -430,94 +388,6 @@ const errorReasons = { 'failed': 'Failed', }; -export class Response { - _bodyLoadedPromiseFulfill: any; - private _client: CDPSession; - private _request: Request; - private _contentPromise: Promise | null = null; - private _bodyLoadedPromise: Promise; - 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 { - 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 { - const content = await this.buffer(); - return content.toString('utf8'); - } - - async json(): Promise { - 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; }[] { const result = []; for (const name in headers) { @@ -527,6 +397,13 @@ function headersArray(headers: { [s: string]: string; }): { name: string; value: 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. const STATUS_TEXTS = { '100': 'Continue', diff --git a/src/chromium/api.ts b/src/chromium/api.ts index ba8ceb3d4b..d15ccb9334 100644 --- a/src/chromium/api.ts +++ b/src/chromium/api.ts @@ -19,7 +19,7 @@ export { Worker, Workers } from './features/workers'; export { Frame } from '../frames'; export { Keyboard, Mouse } from '../input'; export { ElementHandle } from './JSHandle'; -export { Request, Response } from './NetworkManager'; +export { Request, Response } from '../network'; export { ConsoleMessage, FileChooser, Page } from './Page'; export { Playwright } from './Playwright'; export { Target } from './Target'; diff --git a/src/chromium/features/interception.ts b/src/chromium/features/interception.ts index c71279cbe7..0fc7e9426b 100644 --- a/src/chromium/features/interception.ts +++ b/src/chromium/features/interception.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { NetworkManager, Request } from '../NetworkManager'; +import { NetworkManager, Request, toInterceptableRequest } from '../NetworkManager'; export class Interception { 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}; } = {}) { - 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); }) { - return request._fulfill(response); + return toInterceptableRequest(request).fulfill(response); } async abort(request: Request, errorCode: string = 'failed') { - return request._abort(errorCode); + return toInterceptableRequest(request).abort(errorCode); } setOfflineMode(enabled: boolean) { diff --git a/src/firefox/ExecutionContext.ts b/src/firefox/ExecutionContext.ts index 72100e112d..fb785d3b47 100644 --- a/src/firefox/ExecutionContext.ts +++ b/src/firefox/ExecutionContext.ts @@ -17,14 +17,13 @@ import {helper, debugError} from '../helper'; import { createHandle, ElementHandle } from './JSHandle'; -import { Response } from './NetworkManager'; import * as js from '../javascript'; import { JugglerSession } from './Connection'; -export type ExecutionContext = js.ExecutionContext; -export type JSHandle = js.JSHandle; +export type ExecutionContext = js.ExecutionContext; +export type JSHandle = js.JSHandle; -export class ExecutionContextDelegate implements js.ExecutionContextDelegate { +export class ExecutionContextDelegate implements js.ExecutionContextDelegate { _session: JugglerSession; _executionContextId: string; diff --git a/src/firefox/FrameManager.ts b/src/firefox/FrameManager.ts index 6e5fedcdcf..da2c6e51c8 100644 --- a/src/firefox/FrameManager.ts +++ b/src/firefox/FrameManager.ts @@ -24,7 +24,6 @@ import { JSHandle, ExecutionContext, ExecutionContextDelegate } from './Executio import {NavigationWatchdog, NextNavigationWatchdog} from './NavigationWatchdog'; import { ElementHandle } from './JSHandle'; import { TimeoutSettings } from '../TimeoutSettings'; -import { Response } from './NetworkManager'; import * as frames from '../frames'; import * as js from '../javascript'; @@ -43,9 +42,9 @@ type FrameData = { firedEvents: Set, }; -export type Frame = frames.Frame; +export type Frame = frames.Frame; -export class FrameManager extends EventEmitter implements frames.FrameDelegate { +export class FrameManager extends EventEmitter implements frames.FrameDelegate { _session: JugglerSession; _page: Page; _networkManager: any; diff --git a/src/firefox/JSHandle.ts b/src/firefox/JSHandle.ts index 197d0101ed..e298f64e50 100644 --- a/src/firefox/JSHandle.ts +++ b/src/firefox/JSHandle.ts @@ -24,11 +24,10 @@ import { JugglerSession } from './Connection'; import { Frame, FrameManager } from './FrameManager'; import { Page } from './Page'; import { JSHandle, ExecutionContext, markJSHandle, ExecutionContextDelegate } from './ExecutionContext'; -import { Response } from './NetworkManager'; type SelectorRoot = Element | ShadowRoot | Document; -export class ElementHandle extends js.JSHandle { +export class ElementHandle extends js.JSHandle { _frame: Frame; _frameId: string; _page: Page; diff --git a/src/firefox/NetworkManager.ts b/src/firefox/NetworkManager.ts index aab635dc6f..412588c0b5 100644 --- a/src/firefox/NetworkManager.ts +++ b/src/firefox/NetworkManager.ts @@ -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 { assert, debugError, helper, RegisteredListener } from '../helper'; 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; +export type Response = network.Response; export const NetworkManagerEvents = { RequestFailed: Symbol('NetworkManagerEvents.RequestFailed'), @@ -12,7 +34,7 @@ export const NetworkManagerEvents = { export class NetworkManager extends EventEmitter { private _session: JugglerSession; - private _requests: Map; + private _requests: Map; private _frameManager: FrameManager; private _eventListeners: RegisteredListener[]; @@ -39,7 +61,7 @@ export class NetworkManager extends EventEmitter { this._frameManager = frameManager; } - async setExtraHTTPHeaders(headers) { + async setExtraHTTPHeaders(headers: network.Headers) { const array = []; 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.`); @@ -54,26 +76,37 @@ export class NetworkManager extends EventEmitter { _onRequestWillBeSent(event) { 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) return; - let redirectChain = []; + let redirectChain: Request[] = []; if (redirected) { - redirectChain = redirected._redirectChain; - redirectChain.push(redirected); + redirectChain = redirected.request._redirectChain; + redirectChain.push(redirected.request); 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.emit(NetworkManagerEvents.Request, request); + this.emit(NetworkManagerEvents.Request, request.request); } _onResponseReceived(event) { const request = this._requests.get(event.requestId); if (!request) return; - const response = new Response(this._session, request, event); - request._response = response; + const remoteAddress: network.RemoteAddress = { ip: event.remoteIPAddress, port: event.remotePort }; + 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); } @@ -81,15 +114,16 @@ export class NetworkManager extends EventEmitter { const request = this._requests.get(event.requestId); if (!request) return; + const response = request.request.response(); // 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) { - 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 { 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) { @@ -97,10 +131,10 @@ export class NetworkManager extends EventEmitter { if (!request) return; this._requests.delete(request._id); - if (request.response()) - request.response()._bodyLoadedPromiseFulfill.call(null); - request._errorText = event.errorCode; - this.emit(NetworkManagerEvents.RequestFailed, request); + if (request.request.response()) + request.request.response()._bodyLoaded(); + request.request._setFailureText(event.errorCode); + this.emit(NetworkManagerEvents.RequestFailed, request.request); } } @@ -130,46 +164,35 @@ const causeToResourceType = { TYPE_WEB_MANIFEST: 'manifest', }; -export class Request { +const interceptableRequestSymbol = Symbol('interceptableRequest'); + +export function toInterceptableRequest(request: network.Request): InterceptableRequest { + return (request as any)[interceptableRequestSymbol]; +} + +class InterceptableRequest { + readonly request: Request; _id: string; - private _session: any; - private _frame: any; - _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 _session: JugglerSession; + private _suspended: boolean; private _interceptionHandled: boolean; - constructor(session, frame, redirectChain, payload) { - this._session = session; - this._frame = frame; + constructor(session: JugglerSession, frame: Frame, redirectChain: Request[], payload: any) { this._id = payload.requestId; - this._redirectChain = redirectChain; - this._url = payload.url; - this._postData = payload.postData; + this._session = session; 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; + + const headers: network.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() { - return this._errorText ? {errorText: this._errorText} : null; - } - - async _continue(overrides: any = {}) { + async continue(overrides: any = {}) { assert(!overrides.url, 'Playwright-Firefox does not support overriding URL'); assert(!overrides.method, 'Playwright-Firefox does not support overriding method'); 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._interceptionHandled, 'Request is already handled!'); this._interceptionHandled = true; @@ -197,129 +220,4 @@ export class Request { 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; - 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 { - 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 { - const content = await this.buffer(); - return content.toString('utf8'); - } - - async json(): Promise { - 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; - } } diff --git a/src/firefox/api.ts b/src/firefox/api.ts index a553214777..f17df9e964 100644 --- a/src/firefox/api.ts +++ b/src/firefox/api.ts @@ -12,7 +12,7 @@ export { Interception } from './features/interception'; export { Permissions } from './features/permissions'; export { Frame } from './FrameManager'; export { ElementHandle } from './JSHandle'; -export { Request, Response } from './NetworkManager'; +export { Request, Response } from '../network'; export { ConsoleMessage, FileChooser, Page } from './Page'; export { Playwright } from './Playwright'; diff --git a/src/firefox/features/interception.ts b/src/firefox/features/interception.ts index 70b177bf68..f168ef05ba 100644 --- a/src/firefox/features/interception.ts +++ b/src/firefox/features/interception.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { NetworkManager, Request } from '../NetworkManager'; +import { NetworkManager, Request, toInterceptableRequest } from '../NetworkManager'; export class Interception { 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}; } = {}) { - 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); }) { @@ -27,6 +27,6 @@ export class Interception { } async abort(request: Request, errorCode: string = 'failed') { - return request._abort(); + return toInterceptableRequest(request).abort(); } } diff --git a/src/frames.ts b/src/frames.ts index 525f89249a..9dfb719c1b 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -18,6 +18,7 @@ import * as types from './types'; import * as fs from 'fs'; import * as js from './javascript'; +import * as network from './network'; import { helper, assert } from './helper'; import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption } from './input'; import { waitForSelectorOrXPath, WaitTaskParams, WaitTask } from './waitTask'; @@ -26,11 +27,11 @@ import { TimeoutSettings } from './TimeoutSettings'; const readFileAsync = helper.promisify(fs.readFile); type WorldType = 'main' | 'utility'; -type World, Response> = { - contextPromise: Promise>; - contextResolveCallback: (c: js.ExecutionContext) => void; - context: js.ExecutionContext | null; - waitTasks: Set>; +type World> = { + contextPromise: Promise>; + contextResolveCallback: (c: js.ExecutionContext) => void; + context: js.ExecutionContext | null; + waitTasks: Set>; }; export type NavigateOptions = { @@ -42,24 +43,24 @@ export type GotoOptions = NavigateOptions & { referer?: string, }; -export interface FrameDelegate, Response> { +export interface FrameDelegate> { timeoutSettings(): TimeoutSettings; - navigateFrame(frame: Frame, url: string, options?: GotoOptions): Promise; - waitForFrameNavigation(frame: Frame, options?: NavigateOptions): Promise; - setFrameContent(frame: Frame, html: string, options?: NavigateOptions): Promise; - adoptElementHandle(elementHandle: ElementHandle, context: js.ExecutionContext): Promise; + navigateFrame(frame: Frame, url: string, options?: GotoOptions): Promise | null>; + waitForFrameNavigation(frame: Frame, options?: NavigateOptions): Promise | null>; + setFrameContent(frame: Frame, html: string, options?: NavigateOptions): Promise; + adoptElementHandle(elementHandle: ElementHandle, context: js.ExecutionContext): Promise; } -export class Frame, Response> { - _delegate: FrameDelegate; - private _parentFrame: Frame; +export class Frame> { + _delegate: FrameDelegate; + private _parentFrame: Frame; private _url = ''; private _detached = false; - private _worlds = new Map>(); - private _childFrames = new Set>(); + private _worlds = new Map>(); + private _childFrames = new Set>(); private _name: string; - constructor(delegate: FrameDelegate, parentFrame: Frame | null) { + constructor(delegate: FrameDelegate, parentFrame: Frame | null) { this._delegate = delegate; this._parentFrame = parentFrame; @@ -72,36 +73,36 @@ export class Frame { + goto(url: string, options?: GotoOptions): Promise | null> { return this._delegate.navigateFrame(this, url, options); } - waitForNavigation(options?: NavigateOptions): Promise { + waitForNavigation(options?: NavigateOptions): Promise | null> { return this._delegate.waitForFrameNavigation(this, options); } - _mainContext(): Promise> { + _mainContext(): Promise> { if (this._detached) throw new Error(`Execution Context is not available in detached frame "${this.url()}" (are you trying to evaluate?)`); return this._worlds.get('main').contextPromise; } - _utilityContext(): Promise> { + _utilityContext(): Promise> { if (this._detached) throw new Error(`Execution Context is not available in detached frame "${this.url()}" (are you trying to evaluate?)`); return this._worlds.get('utility').contextPromise; } - executionContext(): Promise> { + executionContext(): Promise> { return this._mainContext(); } - evaluateHandle: types.EvaluateHandle> = async (pageFunction, ...args) => { + evaluateHandle: types.EvaluateHandle> = async (pageFunction, ...args) => { const context = await this._mainContext(); return context.evaluateHandle(pageFunction, ...args as any); } - evaluate: types.Evaluate> = async (pageFunction, ...args) => { + evaluate: types.Evaluate> = async (pageFunction, ...args) => { const context = await this._mainContext(); return context.evaluate(pageFunction, ...args as any); } @@ -118,13 +119,13 @@ export class Frame> = async (selector, pageFunction, ...args) => { + $eval: types.$Eval> = async (selector, pageFunction, ...args) => { const context = await this._mainContext(); const document = await context._document(); return document.$eval(selector, pageFunction, ...args as any); } - $$eval: types.$$Eval> = async (selector, pageFunction, ...args) => { + $$eval: types.$$Eval> = async (selector, pageFunction, ...args) => { const context = await this._mainContext(); const document = await context._document(); return document.$$eval(selector, pageFunction, ...args as any); @@ -160,11 +161,11 @@ export class Frame | null { + parentFrame(): Frame | null { return this._parentFrame; } - childFrames(): Frame[] { + childFrames(): Frame[] { return Array.from(this._childFrames); } @@ -368,7 +369,7 @@ export class Frame | null> { + waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options: any = {}, ...args: any[]): Promise | null> { const xPathPattern = '//'; if (helper.isString(selectorOrFunctionOrTimeout)) { @@ -415,7 +416,7 @@ export class Frame> { + ...args): Promise> { const { polling = 'raf', timeout = this._delegate.timeoutSettings().timeout(), @@ -451,7 +452,7 @@ export class Frame): Promise> { + private _scheduleWaitTask(params: WaitTaskParams, world: World): Promise> { const task = new WaitTask(params, () => world.waitTasks.delete(task)); world.waitTasks.add(task); if (world.context) @@ -459,7 +460,7 @@ export class Frame | null) { + private _setContext(worldType: WorldType, context: js.ExecutionContext | null) { const world = this._worlds.get(worldType); world.context = context; if (context) { @@ -473,7 +474,7 @@ export class Frame) { + _contextCreated(worldType: WorldType, context: js.ExecutionContext) { const world = this._worlds.get(worldType); // In case of multiple sessions to the same target, there's a race between // connections so we might end up creating multiple isolated worlds. @@ -482,14 +483,14 @@ export class Frame) { + _contextDestroyed(context: js.ExecutionContext) { for (const [worldType, world] of this._worlds) { if (world.context === context) this._setContext(worldType, null); } } - private async _adoptElementHandle(elementHandle: ElementHandle, context: js.ExecutionContext, dispose: boolean): Promise { + private async _adoptElementHandle(elementHandle: ElementHandle, context: js.ExecutionContext, dispose: boolean): Promise { if (elementHandle.executionContext() === context) return elementHandle; const handle = this._delegate.adoptElementHandle(elementHandle, context); diff --git a/src/javascript.ts b/src/javascript.ts index 9b773e71af..e47cb3660a 100644 --- a/src/javascript.ts +++ b/src/javascript.ts @@ -7,38 +7,38 @@ import * as injectedSource from './generated/injectedSource'; import * as cssSelectorEngineSource from './generated/cssSelectorEngineSource'; import * as xpathSelectorEngineSource from './generated/xpathSelectorEngineSource'; -export interface ExecutionContextDelegate, Response> { - evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise; - getProperties(handle: JSHandle): Promise>>; - releaseHandle(handle: JSHandle): Promise; - handleToString(handle: JSHandle): string; - handleJSONValue(handle: JSHandle): Promise; +export interface ExecutionContextDelegate> { + evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise; + getProperties(handle: JSHandle): Promise>>; + releaseHandle(handle: JSHandle): Promise; + handleToString(handle: JSHandle): string; + handleJSONValue(handle: JSHandle): Promise; } -export class ExecutionContext, Response> { - _delegate: ExecutionContextDelegate; - private _frame: frames.Frame; - private _injectedPromise: Promise> | null = null; +export class ExecutionContext> { + _delegate: ExecutionContextDelegate; + private _frame: frames.Frame; + private _injectedPromise: Promise> | null = null; private _documentPromise: Promise | null = null; - constructor(delegate: ExecutionContextDelegate, frame: frames.Frame | null) { + constructor(delegate: ExecutionContextDelegate, frame: frames.Frame | null) { this._delegate = delegate; this._frame = frame; } - frame(): frames.Frame | null { + frame(): frames.Frame | null { return this._frame; } - evaluate: types.Evaluate> = (pageFunction, ...args) => { + evaluate: types.Evaluate> = (pageFunction, ...args) => { return this._delegate.evaluate(this, true /* returnByValue */, pageFunction, ...args); } - evaluateHandle: types.EvaluateHandle> = (pageFunction, ...args) => { + evaluateHandle: types.EvaluateHandle> = (pageFunction, ...args) => { return this._delegate.evaluate(this, false /* returnByValue */, pageFunction, ...args); } - _injected(): Promise> { + _injected(): Promise> { if (!this._injectedPromise) { const engineSources = [cssSelectorEngineSource.source, xpathSelectorEngineSource.source]; const source = ` @@ -58,27 +58,27 @@ export class ExecutionContext, Response> { - _context: ExecutionContext; +export class JSHandle> { + _context: ExecutionContext; _disposed = false; - constructor(context: ExecutionContext) { + constructor(context: ExecutionContext) { this._context = context; } - executionContext(): ExecutionContext { + executionContext(): ExecutionContext { return this._context; } - evaluate: types.EvaluateOn> = (pageFunction, ...args) => { + evaluate: types.EvaluateOn> = (pageFunction, ...args) => { return this._context.evaluate(pageFunction, this, ...args); } - evaluateHandle: types.EvaluateHandleOn> = (pageFunction, ...args) => { + evaluateHandle: types.EvaluateHandleOn> = (pageFunction, ...args) => { return this._context.evaluateHandle(pageFunction, this, ...args); } - async getProperty(propertyName: string): Promise | null> { + async getProperty(propertyName: string): Promise | null> { const objectHandle = await this.evaluateHandle((object, propertyName) => { const result = {__proto__: null}; result[propertyName] = object[propertyName]; @@ -90,7 +90,7 @@ export class JSHandle>> { + getProperties(): Promise>> { return this._context._delegate.getProperties(this); } diff --git a/src/network.ts b/src/network.ts index f95870db08..0d3de397ec 100644 --- a/src/network.ts +++ b/src/network.ts @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +import * as types from './types'; +import * as frames from './frames'; + export type NetworkCookie = { name: string, value: string, @@ -44,3 +47,169 @@ export function filterCookies(cookies: NetworkCookie[], urls: string[]) { return false; }); } + +export type Headers = { [key: string]: string }; + +export class Request> { + _response: Response | null = null; + _redirectChain: Request[]; + 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; + + constructor(frame: frames.Frame | null, redirectChain: Request[], 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 | null { + return this._response; + } + + frame(): frames.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 type RemoteAddress = { + ip: string, + port: number, +}; + +type GetResponseBodyCallback = () => Promise; + +export class Response> { + private _request: Request; + private _contentPromise: Promise | null = null; + private _bodyLoadedPromise: Promise; + private _bodyLoadedPromiseFulfill: any; + private _remoteAddress: RemoteAddress; + private _status: number; + private _statusText: string; + private _url: string; + private _headers: Headers; + private _getResponseBodyCallback: GetResponseBodyCallback; + + constructor(request: Request, 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 { + if (!this._contentPromise) { + this._contentPromise = this._bodyLoadedPromise.then(async error => { + if (error) + throw error; + return this._getResponseBodyCallback(); + }); + } + return this._contentPromise; + } + + async text(): Promise { + const content = await this.buffer(); + return content.toString('utf8'); + } + + async json(): Promise { + const content = await this.text(); + return JSON.parse(content); + } + + request(): Request { + return this._request; + } + + frame(): frames.Frame | null { + return this._request.frame(); + } +} diff --git a/src/types.ts b/src/types.ts index a206169056..1d03323d08 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,12 +15,12 @@ export type $$Eval = (selector: string, pageFunct export type EvaluateOn = (pageFunction: PageFunctionOn, ...args: Boxed) => Promise; export type EvaluateHandleOn = (pageFunction: PageFunctionOn, ...args: Boxed) => Promise; -export interface ElementHandle, Response> extends js.JSHandle { +export interface ElementHandle> extends js.JSHandle { $(selector: string): Promise; $x(expression: string): Promise; $$(selector: string): Promise; - $eval: $Eval>; - $$eval: $$Eval>; + $eval: $Eval>; + $$eval: $$Eval>; click(options?: input.ClickOptions): Promise; dblclick(options?: input.MultiClickOptions): Promise; tripleclick(options?: input.MultiClickOptions): Promise; diff --git a/src/waitTask.ts b/src/waitTask.ts index 2c1b15ceac..fda9fb57f4 100644 --- a/src/waitTask.ts +++ b/src/waitTask.ts @@ -15,12 +15,12 @@ export type WaitTaskParams = { args: any[]; }; -export class WaitTask, Response> { - readonly promise: Promise>; +export class WaitTask> { + readonly promise: Promise>; private _cleanup: () => void; private _params: WaitTaskParams & { predicateBody: string }; private _runCount: number; - private _resolve: (result: js.JSHandle) => void; + private _resolve: (result: js.JSHandle) => void; private _reject: (reason: Error) => void; private _timeoutTimer: NodeJS.Timer; private _terminated: boolean; @@ -39,7 +39,7 @@ export class WaitTask>((resolve, reject) => { + this.promise = new Promise>((resolve, reject) => { this._resolve = resolve; this._reject = reject; }); @@ -57,9 +57,9 @@ export class WaitTask) { + async rerun(context: js.ExecutionContext) { const runCount = ++this._runCount; - let success: js.JSHandle | null = null; + let success: js.JSHandle | null = null; let error = null; try { success = await context.evaluateHandle(waitForPredicatePageFunction, this._params.predicateBody, this._params.polling, this._params.timeout, ...this._params.args); diff --git a/src/webkit/ExecutionContext.ts b/src/webkit/ExecutionContext.ts index 23c8518cdc..31ad9cc403 100644 --- a/src/webkit/ExecutionContext.ts +++ b/src/webkit/ExecutionContext.ts @@ -20,16 +20,15 @@ import { helper } from '../helper'; import { valueFromRemoteObject, releaseObject } from './protocolHelper'; import { createJSHandle, ElementHandle } from './JSHandle'; import { Protocol } from './protocol'; -import { Response } from './NetworkManager'; import * as js from '../javascript'; export const EVALUATION_SCRIPT_URL = '__playwright_evaluation_script__'; const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m; -export type ExecutionContext = js.ExecutionContext; -export type JSHandle = js.JSHandle; +export type ExecutionContext = js.ExecutionContext; +export type JSHandle = js.JSHandle; -export class ExecutionContextDelegate implements js.ExecutionContextDelegate { +export class ExecutionContextDelegate implements js.ExecutionContextDelegate { private _globalObjectId?: string; _session: TargetSession; private _contextId: number; diff --git a/src/webkit/FrameManager.ts b/src/webkit/FrameManager.ts index f1b30e9ba9..a6fa88e9b0 100644 --- a/src/webkit/FrameManager.ts +++ b/src/webkit/FrameManager.ts @@ -42,9 +42,9 @@ type FrameData = { id: string, }; -export type Frame = frames.Frame; +export type Frame = frames.Frame; -export class FrameManager extends EventEmitter implements frames.FrameDelegate { +export class FrameManager extends EventEmitter implements frames.FrameDelegate { _session: TargetSession; _page: Page; _networkManager: NetworkManager; diff --git a/src/webkit/JSHandle.ts b/src/webkit/JSHandle.ts index 55107b3bd6..f35c9689f1 100644 --- a/src/webkit/JSHandle.ts +++ b/src/webkit/JSHandle.ts @@ -20,7 +20,6 @@ import { assert, debugError, helper } from '../helper'; import * as input from '../input'; import { TargetSession } from './Connection'; import { JSHandle, ExecutionContext, ExecutionContextDelegate, markJSHandle } from './ExecutionContext'; -import { Response } from './NetworkManager'; import { FrameManager } from './FrameManager'; import { Page } from './Page'; import { Protocol } from './protocol'; @@ -44,7 +43,7 @@ export function createJSHandle(context: ExecutionContext, remoteObject: Protocol return handle; } -export class ElementHandle extends js.JSHandle { +export class ElementHandle extends js.JSHandle { private _client: TargetSession; private _remoteObject: Protocol.Runtime.RemoteObject; private _page: Page; diff --git a/src/webkit/NetworkManager.ts b/src/webkit/NetworkManager.ts index 561846e28f..eed204e280 100644 --- a/src/webkit/NetworkManager.ts +++ b/src/webkit/NetworkManager.ts @@ -20,6 +20,8 @@ import { TargetSession } from './Connection'; import { Frame, FrameManager } from './FrameManager'; import { assert, helper, RegisteredListener } from '../helper'; import { Protocol } from './protocol'; +import * as network from '../network'; +import { ElementHandle } from './JSHandle'; export const NetworkManagerEvents = { Request: Symbol('Events.NetworkManager.Request'), @@ -28,11 +30,14 @@ export const NetworkManagerEvents = { RequestFinished: Symbol('Events.NetworkManager.RequestFinished'), }; +export type Request = network.Request; +export type Response = network.Response; + export class NetworkManager extends EventEmitter { private _sesssion: TargetSession; private _frameManager: FrameManager; - private _requestIdToRequest = new Map(); - private _extraHTTPHeaders: {[key: string]: string} = {}; + private _requestIdToRequest = new Map(); + private _extraHTTPHeaders: network.Headers = {}; private _attemptedAuthentications = new Set(); private _userCacheDisabled = false; private _sessionListeners: RegisteredListener[] = []; @@ -89,30 +94,38 @@ export class NetworkManager extends EventEmitter { } _onRequestWillBeSent(event: Protocol.Network.requestWillBeSentPayload, interceptionId: string | null) { - let redirectChain = []; + let redirectChain: Request[] = []; if (event.redirectResponse) { const request = this._requestIdToRequest.get(event.requestId); // If we connect late to the target, we could have missed the requestWillBeSent event. if (request) { this._handleRequestRedirect(request, event.redirectResponse); - redirectChain = request._redirectChain; + redirectChain = request.request._redirectChain; } } 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.emit(NetworkManagerEvents.Request, request); + this.emit(NetworkManagerEvents.Request, request.request); } - _handleRequestRedirect(request: Request, responsePayload: Protocol.Network.Response) { - const response = new Response(this._sesssion, request, responsePayload); - request._response = response; - request._redirectChain.push(request); - response._bodyLoadedPromiseFulfill.call(null, new Error('Response body is unavailable for redirect responses')); + _createResponse(request: InterceptableRequest, responsePayload: Protocol.Network.Response): Response { + const remoteAddress: network.RemoteAddress = { ip: '', port: 0 }; + const getResponseBody = async () => { + const response = await this._sesssion.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: 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._attemptedAuthentications.delete(request._interceptionId); this.emit(NetworkManagerEvents.Response, response); - this.emit(NetworkManagerEvents.RequestFinished, request); + this.emit(NetworkManagerEvents.RequestFinished, request.request); } _onResponseReceived(event: Protocol.Network.responseReceivedPayload) { @@ -120,8 +133,7 @@ export class NetworkManager extends EventEmitter { // FileUpload sends a response without a matching request. if (!request) return; - const response = new Response(this._sesssion, request, event.response); - request._response = response; + const response = this._createResponse(request, event.response); this.emit(NetworkManagerEvents.Response, response); } @@ -134,11 +146,11 @@ export class NetworkManager extends EventEmitter { // Under certain conditions we never get the Network.responseReceived // event from protocol. @see https://crbug.com/883475 - if (request.response()) - request.response()._bodyLoadedPromiseFulfill.call(null); + if (request.request.response()) + request.request.response()._bodyLoaded(); this._requestIdToRequest.delete(request._requestId); this._attemptedAuthentications.delete(request._interceptionId); - this.emit(NetworkManagerEvents.RequestFinished, request); + this.emit(NetworkManagerEvents.RequestFinished, request.request); } _onLoadingFailed(event: Protocol.Network.loadingFailedPayload) { @@ -147,166 +159,41 @@ export class NetworkManager extends EventEmitter { // @see https://crbug.com/750469 if (!request) return; - request._failureText = event.errorText; - const response = request.response(); + request.request._setFailureText(event.errorText); + const response = request.request.response(); if (response) - response._bodyLoadedPromiseFulfill.call(null); + response._bodyLoaded(); this._requestIdToRequest.delete(request._requestId); this._attemptedAuthentications.delete(request._interceptionId); - this.emit(NetworkManagerEvents.RequestFailed, request); + this.emit(NetworkManagerEvents.RequestFailed, request.request); } } -export class Request { - _response: Response | null = null; - _redirectChain: Request[]; +const interceptableRequestSymbol = Symbol('interceptableRequest'); + +export function toInterceptableRequest(request: network.Request): InterceptableRequest { + return (request as any)[interceptableRequestSymbol]; +} + +class InterceptableRequest { + readonly request: Request; _requestId: 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[]) { this._requestId = event.requestId; // 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._url = event.request.url; - this._resourceType = event.type ? event.type.toLowerCase() : 'Unknown'; - 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 - }; + this.request = new network.Request(frame, redirectChain, isNavigationRequest, event.request.url, + event.type ? event.type.toLowerCase() : 'Unknown', event.request.method, event.request.postData, headersObject(event.request.headers)); + (this.request as any)[interceptableRequestSymbol] = this; } } -export class Response { - _bodyLoadedPromiseFulfill: any; - private _client: TargetSession; - private _request: Request; - private _contentPromise: Promise | null = null; - private _bodyLoadedPromise: Promise; - 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 { - 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 { - const content = await this.buffer(); - return content.toString('utf8'); - } - - async json(): Promise { - const content = await this.text(); - return JSON.parse(content); - } - - request(): Request { - return this._request; - } - - frame(): Frame | null { - return this._request.frame(); - } +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; } diff --git a/src/webkit/api.ts b/src/webkit/api.ts index d7581d3759..a0424f3a15 100644 --- a/src/webkit/api.ts +++ b/src/webkit/api.ts @@ -8,7 +8,7 @@ export { ExecutionContext, JSHandle } from '../javascript'; export { Frame } from './FrameManager'; export { Mouse, Keyboard } from '../input'; export { ElementHandle } from './JSHandle'; -export { Request, Response } from './NetworkManager'; +export { Request, Response } from '../network'; export { ConsoleMessage, Page } from './Page'; export { Playwright } from './Playwright'; export { Target } from './Target';