feat(chromium): Service Worker Network Instrumentation and Inspection (#14716)
Adds Chromium support for Service Worker Networking (interception/routing, Request/Response events, and HAR). Resolves #1090. Depends on #14714 and #14714. Supercedes #14321. Follow up #14711. Landed upstream patches: - https://chromium-review.googlesource.com/c/chromium/src/+/3510917 - https://chromium-review.googlesource.com/c/chromium/src/+/3526571 - https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3566669 - https://chromium-review.googlesource.com/c/chromium/src/+/3544685 - https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3610924 - https://chromium-review.googlesource.com/c/chromium/src/+/3689949
This commit is contained in:
parent
295ea7a3cb
commit
6cb3236acd
|
|
@ -209,6 +209,15 @@ following: `document`, `stylesheet`, `image`, `media`, `font`, `script`, `texttr
|
|||
|
||||
Returns the matching [Response] object, or `null` if the response was not received due to error.
|
||||
|
||||
## method: Request.serviceWorker
|
||||
- returns: <[null]|[Worker]>
|
||||
|
||||
:::note
|
||||
This field is Chromium only. It's safe to call when using other browsers, but it will always be `null`.
|
||||
:::
|
||||
|
||||
The Service [Worker] that is performing the request.
|
||||
|
||||
## async method: Request.sizes
|
||||
- returns: <[Object]>
|
||||
- `requestBodySize` <[int]> Size of the request body (POST data payload) in bytes. Set to 0 if there was no body.
|
||||
|
|
|
|||
|
|
@ -18,10 +18,11 @@ import { URLSearchParams } from 'url';
|
|||
import type * as channels from '../protocol/channels';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { Frame } from './frame';
|
||||
import { Worker } from './worker';
|
||||
import type { Headers, RemoteAddr, SecurityDetails, WaitForEventOptions } from './types';
|
||||
import fs from 'fs';
|
||||
import { mime } from '../utilsBundle';
|
||||
import { isString, headersObjectToArray } from '../utils';
|
||||
import { assert, isString, headersObjectToArray } from '../utils';
|
||||
import { ManualPromise } from '../utils/manualPromise';
|
||||
import { Events } from './events';
|
||||
import type { Page } from './page';
|
||||
|
|
@ -197,9 +198,17 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
|
|||
}
|
||||
|
||||
frame(): Frame {
|
||||
if (!this._initializer.frame) {
|
||||
assert(this.serviceWorker());
|
||||
throw new Error('Service Worker requests do not have an associated frame.');
|
||||
}
|
||||
return Frame.from(this._initializer.frame);
|
||||
}
|
||||
|
||||
serviceWorker(): Worker | null {
|
||||
return this._initializer.serviceWorker ? Worker.from(this._initializer.serviceWorker) : null;
|
||||
}
|
||||
|
||||
isNavigationRequest(): boolean {
|
||||
return this._initializer.isNavigationRequest;
|
||||
}
|
||||
|
|
@ -259,14 +268,13 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
|||
return Request.from(this._initializer.request);
|
||||
}
|
||||
|
||||
private _raceWithPageClose(promise: Promise<any>): Promise<void> {
|
||||
const page = this.request().frame()._page;
|
||||
private _raceWithTargetClose(promise: Promise<any>): Promise<void> {
|
||||
// When page closes or crashes, we catch any potential rejects from this Route.
|
||||
// Note that page could be missing when routing popup's initial request that
|
||||
// does not have a Page initialized just yet.
|
||||
return Promise.race([
|
||||
promise,
|
||||
page ? page._closedOrCrashedPromise : Promise.resolve(),
|
||||
this.request().serviceWorker()?._closedPromise || this.request().frame()._page?._closedOrCrashedPromise || Promise.resolve(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -283,13 +291,13 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
|||
|
||||
async abort(errorCode?: string) {
|
||||
this._checkNotHandled();
|
||||
await this._raceWithPageClose(this._channel.abort({ errorCode }));
|
||||
await this._raceWithTargetClose(this._channel.abort({ errorCode }));
|
||||
this._reportHandled(true);
|
||||
}
|
||||
|
||||
async _redirectNavigationRequest(url: string) {
|
||||
this._checkNotHandled();
|
||||
await this._raceWithPageClose(this._channel.redirectNavigationRequest({ url }));
|
||||
await this._raceWithTargetClose(this._channel.redirectNavigationRequest({ url }));
|
||||
this._reportHandled(true);
|
||||
}
|
||||
|
||||
|
|
@ -342,7 +350,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
|||
if (length && !('content-length' in headers))
|
||||
headers['content-length'] = String(length);
|
||||
|
||||
await this._raceWithPageClose(this._channel.fulfill({
|
||||
await this._raceWithTargetClose(this._channel.fulfill({
|
||||
status: statusOption || 200,
|
||||
headers: headersObjectToArray(headers),
|
||||
body,
|
||||
|
|
@ -373,7 +381,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
|||
const options = this.request()._fallbackOverridesForContinue();
|
||||
return await this._wrapApiCall(async () => {
|
||||
const postDataBuffer = isString(options.postData) ? Buffer.from(options.postData, 'utf8') : options.postData;
|
||||
await this._raceWithPageClose(this._channel.continue({
|
||||
await this._raceWithTargetClose(this._channel.continue({
|
||||
url: options.url,
|
||||
method: options.method,
|
||||
headers: options.headers ? headersObjectToArray(options.headers) : undefined,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import type * as structs from '../../types/structs';
|
|||
export class Worker extends ChannelOwner<channels.WorkerChannel> implements api.Worker {
|
||||
_page: Page | undefined; // Set for web workers.
|
||||
_context: BrowserContext | undefined; // Set for service workers.
|
||||
_closedPromise: Promise<void>;
|
||||
|
||||
static from(worker: channels.WorkerChannel): Worker {
|
||||
return (worker as any)._object;
|
||||
|
|
@ -40,6 +41,7 @@ export class Worker extends ChannelOwner<channels.WorkerChannel> implements api.
|
|||
this._context._serviceWorkers.delete(this);
|
||||
this.emit(Events.Worker.Close, this);
|
||||
});
|
||||
this._closedPromise = new Promise(f => this.once(Events.Worker.Close, f));
|
||||
}
|
||||
|
||||
url(): string {
|
||||
|
|
|
|||
|
|
@ -3145,7 +3145,8 @@ export interface ElementHandleEvents {
|
|||
|
||||
// ----------- Request -----------
|
||||
export type RequestInitializer = {
|
||||
frame: FrameChannel,
|
||||
frame?: FrameChannel,
|
||||
serviceWorker?: WorkerChannel,
|
||||
url: string,
|
||||
resourceType: string,
|
||||
method: string,
|
||||
|
|
|
|||
|
|
@ -2478,7 +2478,8 @@ Request:
|
|||
type: interface
|
||||
|
||||
initializer:
|
||||
frame: Frame
|
||||
frame: Frame?
|
||||
serviceWorker: Worker?
|
||||
url: string
|
||||
resourceType: string
|
||||
method: string
|
||||
|
|
@ -2538,7 +2539,6 @@ Route:
|
|||
isBase64: boolean?
|
||||
fetchResponseUid: string?
|
||||
|
||||
|
||||
ResourceTiming:
|
||||
type: object
|
||||
properties:
|
||||
|
|
|
|||
|
|
@ -1766,7 +1766,8 @@ scheme.ElementHandleWaitForSelectorResult = tObject({
|
|||
element: tOptional(tChannel(['ElementHandle'])),
|
||||
});
|
||||
scheme.RequestInitializer = tObject({
|
||||
frame: tChannel(['Frame']),
|
||||
frame: tOptional(tChannel(['Frame'])),
|
||||
serviceWorker: tOptional(tChannel(['Worker'])),
|
||||
url: tString,
|
||||
resourceType: tString,
|
||||
method: tString,
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ import { Browser } from '../browser';
|
|||
import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext';
|
||||
import { assert } from '../../utils';
|
||||
import * as network from '../network';
|
||||
import type { PageBinding, PageDelegate } from '../page';
|
||||
import { Page, Worker } from '../page';
|
||||
import type { PageBinding, PageDelegate , Worker } from '../page';
|
||||
import { Page } from '../page';
|
||||
import { Frame } from '../frames';
|
||||
import type { Dialog } from '../dialog';
|
||||
import type { ConnectionTransport } from '../transport';
|
||||
|
|
@ -32,8 +32,8 @@ import { ConnectionEvents, CRConnection } from './crConnection';
|
|||
import { CRPage } from './crPage';
|
||||
import { readProtocolStream } from './crProtocolHelper';
|
||||
import type { Protocol } from './protocol';
|
||||
import { CRExecutionContext } from './crExecutionContext';
|
||||
import type { CRDevTools } from './crDevTools';
|
||||
import { CRServiceWorker } from './crServiceWorker';
|
||||
|
||||
export class CRBrowser extends Browser {
|
||||
readonly _connection: CRConnection;
|
||||
|
|
@ -307,21 +307,6 @@ export class CRBrowser extends Browser {
|
|||
}
|
||||
}
|
||||
|
||||
class CRServiceWorker extends Worker {
|
||||
readonly _browserContext: CRBrowserContext;
|
||||
|
||||
constructor(browserContext: CRBrowserContext, session: CRSession, url: string) {
|
||||
super(browserContext, url);
|
||||
this._browserContext = browserContext;
|
||||
session.once('Runtime.executionContextCreated', event => {
|
||||
this._createExecutionContext(new CRExecutionContext(session, event.context));
|
||||
});
|
||||
// This might fail if the target is closed before we receive all execution contexts.
|
||||
session.send('Runtime.enable', {}).catch(e => {});
|
||||
session.send('Runtime.runIfWaitingForDebugger').catch(e => {});
|
||||
}
|
||||
}
|
||||
|
||||
export class CRBrowserContext extends BrowserContext {
|
||||
static CREvents = {
|
||||
BackgroundPage: 'backgroundpage',
|
||||
|
|
@ -451,18 +436,24 @@ export class CRBrowserContext extends BrowserContext {
|
|||
this._options.extraHTTPHeaders = headers;
|
||||
for (const page of this.pages())
|
||||
await (page._delegate as CRPage).updateExtraHTTPHeaders();
|
||||
for (const sw of this.serviceWorkers())
|
||||
await (sw as CRServiceWorker).updateExtraHTTPHeaders(false);
|
||||
}
|
||||
|
||||
async setOffline(offline: boolean): Promise<void> {
|
||||
this._options.offline = offline;
|
||||
for (const page of this.pages())
|
||||
await (page._delegate as CRPage).updateOffline();
|
||||
for (const sw of this.serviceWorkers())
|
||||
await (sw as CRServiceWorker).updateOffline(false);
|
||||
}
|
||||
|
||||
async doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise<void> {
|
||||
this._options.httpCredentials = httpCredentials;
|
||||
for (const page of this.pages())
|
||||
await (page._delegate as CRPage).updateHttpCredentials();
|
||||
for (const sw of this.serviceWorkers())
|
||||
await (sw as CRServiceWorker).updateHttpCredentials(false);
|
||||
}
|
||||
|
||||
async doAddInitScript(source: string) {
|
||||
|
|
@ -488,6 +479,8 @@ export class CRBrowserContext extends BrowserContext {
|
|||
async doUpdateRequestInterception(): Promise<void> {
|
||||
for (const page of this.pages())
|
||||
await (page._delegate as CRPage).updateRequestInterception();
|
||||
for (const sw of this.serviceWorkers())
|
||||
await (sw as CRServiceWorker).updateRequestInterception();
|
||||
}
|
||||
|
||||
async doClose() {
|
||||
|
|
|
|||
|
|
@ -22,14 +22,17 @@ import type { RegisteredListener } from '../../utils/eventsHelper';
|
|||
import { eventsHelper } from '../../utils/eventsHelper';
|
||||
import type { Protocol } from './protocol';
|
||||
import * as network from '../network';
|
||||
import type * as contexts from '../browserContext';
|
||||
import type * as frames from '../frames';
|
||||
import type * as types from '../types';
|
||||
import type { CRPage } from './crPage';
|
||||
import { assert, headersObjectToArray } from '../../utils';
|
||||
import type { CRServiceWorker } from './crServiceWorker';
|
||||
|
||||
export class CRNetworkManager {
|
||||
private _client: CRSession;
|
||||
private _page: Page;
|
||||
private _page: Page | null;
|
||||
private _serviceWorker: CRServiceWorker | null;
|
||||
private _parentManager: CRNetworkManager | null;
|
||||
private _requestIdToRequest = new Map<string, InterceptableRequest>();
|
||||
private _requestIdToRequestWillBeSentEvent = new Map<string, Protocol.Network.requestWillBeSentPayload>();
|
||||
|
|
@ -41,15 +44,16 @@ export class CRNetworkManager {
|
|||
private _eventListeners: RegisteredListener[];
|
||||
private _responseExtraInfoTracker = new ResponseExtraInfoTracker();
|
||||
|
||||
constructor(client: CRSession, page: Page, parentManager: CRNetworkManager | null) {
|
||||
constructor(client: CRSession, page: Page | null, serviceWorker: CRServiceWorker | null, parentManager: CRNetworkManager | null) {
|
||||
this._client = client;
|
||||
this._page = page;
|
||||
this._serviceWorker = serviceWorker;
|
||||
this._parentManager = parentManager;
|
||||
this._eventListeners = this.instrumentNetworkEvents(client);
|
||||
}
|
||||
|
||||
instrumentNetworkEvents(session: CRSession, workerFrame?: frames.Frame): RegisteredListener[] {
|
||||
return [
|
||||
const listeners = [
|
||||
eventsHelper.addEventListener(session, 'Fetch.requestPaused', this._onRequestPaused.bind(this, workerFrame)),
|
||||
eventsHelper.addEventListener(session, 'Fetch.authRequired', this._onAuthRequired.bind(this)),
|
||||
eventsHelper.addEventListener(session, 'Network.requestWillBeSent', this._onRequestWillBeSent.bind(this, workerFrame)),
|
||||
|
|
@ -59,14 +63,19 @@ export class CRNetworkManager {
|
|||
eventsHelper.addEventListener(session, 'Network.responseReceivedExtraInfo', this._onResponseReceivedExtraInfo.bind(this)),
|
||||
eventsHelper.addEventListener(session, 'Network.loadingFinished', this._onLoadingFinished.bind(this)),
|
||||
eventsHelper.addEventListener(session, 'Network.loadingFailed', this._onLoadingFailed.bind(this, workerFrame)),
|
||||
eventsHelper.addEventListener(session, 'Network.webSocketCreated', e => this._page._frameManager.onWebSocketCreated(e.requestId, e.url)),
|
||||
eventsHelper.addEventListener(session, 'Network.webSocketWillSendHandshakeRequest', e => this._page._frameManager.onWebSocketRequest(e.requestId)),
|
||||
eventsHelper.addEventListener(session, 'Network.webSocketHandshakeResponseReceived', e => this._page._frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText)),
|
||||
eventsHelper.addEventListener(session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page._frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData)),
|
||||
eventsHelper.addEventListener(session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page._frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData)),
|
||||
eventsHelper.addEventListener(session, 'Network.webSocketClosed', e => this._page._frameManager.webSocketClosed(e.requestId)),
|
||||
eventsHelper.addEventListener(session, 'Network.webSocketFrameError', e => this._page._frameManager.webSocketError(e.requestId, e.errorMessage)),
|
||||
];
|
||||
if (this._page) {
|
||||
listeners.push(...[
|
||||
eventsHelper.addEventListener(session, 'Network.webSocketCreated', e => this._page!._frameManager.onWebSocketCreated(e.requestId, e.url)),
|
||||
eventsHelper.addEventListener(session, 'Network.webSocketWillSendHandshakeRequest', e => this._page!._frameManager.onWebSocketRequest(e.requestId)),
|
||||
eventsHelper.addEventListener(session, 'Network.webSocketHandshakeResponseReceived', e => this._page!._frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText)),
|
||||
eventsHelper.addEventListener(session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page!._frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData)),
|
||||
eventsHelper.addEventListener(session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page!._frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData)),
|
||||
eventsHelper.addEventListener(session, 'Network.webSocketClosed', e => this._page!._frameManager.webSocketClosed(e.requestId)),
|
||||
eventsHelper.addEventListener(session, 'Network.webSocketFrameError', e => this._page!._frameManager.webSocketError(e.requestId, e.errorMessage)),
|
||||
]);
|
||||
}
|
||||
return listeners;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
|
|
@ -193,15 +202,15 @@ export class CRNetworkManager {
|
|||
redirectedFrom = request;
|
||||
}
|
||||
}
|
||||
let frame = requestWillBeSentEvent.frameId ? this._page._frameManager.frame(requestWillBeSentEvent.frameId) : workerFrame;
|
||||
let frame = requestWillBeSentEvent.frameId ? this._page?._frameManager.frame(requestWillBeSentEvent.frameId) : workerFrame;
|
||||
// Requests from workers lack frameId, because we receive Network.requestWillBeSent
|
||||
// on the worker target. However, we receive Fetch.requestPaused on the page target,
|
||||
// and lack workerFrame there. Luckily, Fetch.requestPaused provides a frameId.
|
||||
if (!frame && requestPausedEvent && requestPausedEvent.frameId)
|
||||
if (!frame && this._page && requestPausedEvent && requestPausedEvent.frameId)
|
||||
frame = this._page._frameManager.frame(requestPausedEvent.frameId);
|
||||
|
||||
// Check if it's main resource request interception (targetId === main frame id).
|
||||
if (!frame && requestWillBeSentEvent.frameId === (this._page._delegate as CRPage)._targetId) {
|
||||
if (!frame && this._page && requestWillBeSentEvent.frameId === (this._page?._delegate as CRPage)._targetId) {
|
||||
// Main resource request for the page is being intercepted so the Frame is not created
|
||||
// yet. Precreate it here for the purposes of request interception. It will be updated
|
||||
// later as soon as the request continues and we receive frame tree from the page.
|
||||
|
|
@ -213,7 +222,7 @@ export class CRNetworkManager {
|
|||
//
|
||||
// Note: it would be better to match the URL against interception patterns, but
|
||||
// that information is only available to the client. Perhaps we can just route to the client?
|
||||
if (requestPausedEvent && requestPausedEvent.request.method === 'OPTIONS' && this._page._needsRequestInterception()) {
|
||||
if (requestPausedEvent && requestPausedEvent.request.method === 'OPTIONS' && (this._page || this._serviceWorker)!.needsRequestInterception()) {
|
||||
const requestHeaders = requestPausedEvent.request.headers;
|
||||
const responseHeaders: Protocol.Fetch.HeaderEntry[] = [
|
||||
{ name: 'Access-Control-Allow-Origin', value: requestHeaders['Origin'] || '*' },
|
||||
|
|
@ -232,7 +241,8 @@ export class CRNetworkManager {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!frame) {
|
||||
// Non-service-worker requests MUST have a frame—if they don't, we pretend there was no request
|
||||
if (!frame && !this._serviceWorker) {
|
||||
if (requestPausedEvent)
|
||||
this._client._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId });
|
||||
return;
|
||||
|
|
@ -249,7 +259,9 @@ export class CRNetworkManager {
|
|||
const isNavigationRequest = requestWillBeSentEvent.requestId === requestWillBeSentEvent.loaderId && requestWillBeSentEvent.type === 'Document';
|
||||
const documentId = isNavigationRequest ? requestWillBeSentEvent.loaderId : undefined;
|
||||
const request = new InterceptableRequest({
|
||||
frame,
|
||||
context: (this._page || this._serviceWorker)!._browserContext,
|
||||
frame: frame || null,
|
||||
serviceWorker: this._serviceWorker || null,
|
||||
documentId,
|
||||
route,
|
||||
requestWillBeSentEvent,
|
||||
|
|
@ -257,13 +269,14 @@ export class CRNetworkManager {
|
|||
redirectedFrom
|
||||
});
|
||||
this._requestIdToRequest.set(requestWillBeSentEvent.requestId, request);
|
||||
|
||||
if (requestPausedEvent && !requestPausedEvent.responseStatusCode && !requestPausedEvent.responseErrorReason) {
|
||||
// We will not receive extra info when intercepting the request.
|
||||
// Use the headers from the Fetch.requestPausedPayload and release the allHeaders()
|
||||
// right away, so that client can call it from the route handler.
|
||||
request.request.setRawRequestHeaders(headersObjectToArray(requestPausedEvent.request.headers, '\n'));
|
||||
}
|
||||
this._page._frameManager.requestStarted(request.request, route || undefined);
|
||||
(this._page?._frameManager || this._serviceWorker)!.requestStarted(request.request, route || undefined);
|
||||
}
|
||||
|
||||
_createResponse(request: InterceptableRequest, responsePayload: Protocol.Network.Response, hasExtraInfo: boolean): network.Response {
|
||||
|
|
@ -276,7 +289,7 @@ export class CRNetworkManager {
|
|||
return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');
|
||||
|
||||
// For <link prefetch we are going to receive empty body with non-emtpy content-length expectation. Reach out for the actual content.
|
||||
const resource = await this._client.send('Network.loadNetworkResource', { url: request.request.url(), frameId: request.request.frame()._id, options: { disableCache: false, includeCredentials: true } });
|
||||
const resource = await this._client.send('Network.loadNetworkResource', { url: request.request.url(), frameId: this._serviceWorker ? undefined : request.request.frame()!._id, options: { disableCache: false, includeCredentials: true } });
|
||||
const chunks: Buffer[] = [];
|
||||
while (resource.resource.stream) {
|
||||
const chunk = await this._client.send('IO.read', { handle: resource.resource.stream });
|
||||
|
|
@ -341,8 +354,8 @@ export class CRNetworkManager {
|
|||
this._requestIdToRequest.delete(request._requestId);
|
||||
if (request._interceptionId)
|
||||
this._attemptedAuthentications.delete(request._interceptionId);
|
||||
this._page._frameManager.requestReceivedResponse(response);
|
||||
this._page._frameManager.reportRequestFinished(request.request, response);
|
||||
(this._page?._frameManager || this._serviceWorker)!.requestReceivedResponse(response);
|
||||
(this._page?._frameManager || this._serviceWorker)!.reportRequestFinished(request.request, response);
|
||||
}
|
||||
|
||||
_onResponseReceivedExtraInfo(event: Protocol.Network.responseReceivedExtraInfoPayload) {
|
||||
|
|
@ -355,7 +368,7 @@ export class CRNetworkManager {
|
|||
if (!request)
|
||||
return;
|
||||
const response = this._createResponse(request, event.response, event.hasExtraInfo);
|
||||
this._page._frameManager.requestReceivedResponse(response);
|
||||
(this._page?._frameManager || this._serviceWorker)!.requestReceivedResponse(response);
|
||||
}
|
||||
|
||||
_onLoadingFinished(event: Protocol.Network.loadingFinishedPayload) {
|
||||
|
|
@ -380,7 +393,7 @@ export class CRNetworkManager {
|
|||
this._requestIdToRequest.delete(request._requestId);
|
||||
if (request._interceptionId)
|
||||
this._attemptedAuthentications.delete(request._interceptionId);
|
||||
this._page._frameManager.reportRequestFinished(request.request, response);
|
||||
(this._page?._frameManager || this._serviceWorker)!.reportRequestFinished(request.request, response);
|
||||
}
|
||||
|
||||
_onLoadingFailed(workerFrame: frames.Frame | undefined, event: Protocol.Network.loadingFailedPayload) {
|
||||
|
|
@ -416,7 +429,7 @@ export class CRNetworkManager {
|
|||
if (request._interceptionId)
|
||||
this._attemptedAuthentications.delete(request._interceptionId);
|
||||
request.request._setFailureText(event.errorText);
|
||||
this._page._frameManager.requestFailed(request.request, !!event.canceled);
|
||||
(this._page?._frameManager || this._serviceWorker)!.requestFailed(request.request, !!event.canceled);
|
||||
}
|
||||
|
||||
private _maybeAdoptMainRequest(requestId: Protocol.Network.RequestId): InterceptableRequest | undefined {
|
||||
|
|
@ -448,14 +461,16 @@ class InterceptableRequest {
|
|||
private _redirectedFrom: InterceptableRequest | null;
|
||||
|
||||
constructor(options: {
|
||||
frame: frames.Frame;
|
||||
context: contexts.BrowserContext;
|
||||
frame: frames.Frame | null;
|
||||
serviceWorker: CRServiceWorker | null;
|
||||
documentId?: string;
|
||||
route: RouteImpl | null;
|
||||
requestWillBeSentEvent: Protocol.Network.requestWillBeSentPayload;
|
||||
requestPausedEvent: Protocol.Fetch.requestPausedPayload | null;
|
||||
redirectedFrom: InterceptableRequest | null;
|
||||
}) {
|
||||
const { frame, documentId, route, requestWillBeSentEvent, requestPausedEvent, redirectedFrom } = options;
|
||||
const { context, frame, documentId, route, requestWillBeSentEvent, requestPausedEvent, redirectedFrom, serviceWorker } = options;
|
||||
this._timestamp = requestWillBeSentEvent.timestamp;
|
||||
this._wallTime = requestWillBeSentEvent.wallTime;
|
||||
this._requestId = requestWillBeSentEvent.requestId;
|
||||
|
|
@ -475,7 +490,7 @@ class InterceptableRequest {
|
|||
if (postDataEntries && postDataEntries.length && postDataEntries[0].bytes)
|
||||
postDataBuffer = Buffer.from(postDataEntries[0].bytes, 'base64');
|
||||
|
||||
this.request = new network.Request(frame, redirectedFrom?.request || null, documentId, url, type, method, postDataBuffer, headersObjectToArray(headers));
|
||||
this.request = new network.Request(context, frame, serviceWorker, redirectedFrom?.request || null, documentId, url, type, method, postDataBuffer, headersObjectToArray(headers));
|
||||
}
|
||||
|
||||
_routeForRedirectChain(): RouteImpl | null {
|
||||
|
|
|
|||
|
|
@ -406,7 +406,7 @@ class FrameSession {
|
|||
this._crPage = crPage;
|
||||
this._page = crPage._page;
|
||||
this._targetId = targetId;
|
||||
this._networkManager = new CRNetworkManager(client, this._page, parentSession ? parentSession._networkManager : null);
|
||||
this._networkManager = new CRNetworkManager(client, this._page, null, parentSession ? parentSession._networkManager : null);
|
||||
this._parentSession = parentSession;
|
||||
if (parentSession)
|
||||
parentSession._childSessions.add(this);
|
||||
|
|
@ -1077,7 +1077,7 @@ class FrameSession {
|
|||
}
|
||||
|
||||
async _updateRequestInterception(): Promise<void> {
|
||||
await this._networkManager.setRequestInterception(this._page._needsRequestInterception());
|
||||
await this._networkManager.setRequestInterception(this._page.needsRequestInterception());
|
||||
}
|
||||
|
||||
async _updateFileChooserInterception(initial: boolean) {
|
||||
|
|
|
|||
121
packages/playwright-core/src/server/chromium/crServiceWorker.ts
Normal file
121
packages/playwright-core/src/server/chromium/crServiceWorker.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
/**
|
||||
* 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 { Worker } from '../page';
|
||||
import type { CRBrowserContext } from './crBrowser';
|
||||
import type { CRSession } from './crConnection';
|
||||
import type * as types from '../types';
|
||||
import { CRExecutionContext } from './crExecutionContext';
|
||||
import { CRNetworkManager } from './crNetworkManager';
|
||||
import * as network from '../network';
|
||||
import { BrowserContext } from '../browserContext';
|
||||
import { headersArrayToObject } from '../../utils';
|
||||
|
||||
export class CRServiceWorker extends Worker {
|
||||
readonly _browserContext: CRBrowserContext;
|
||||
readonly _networkManager: CRNetworkManager;
|
||||
private _session: CRSession;
|
||||
private _extraHTTPHeaders: types.HeadersArray | null = null;
|
||||
|
||||
constructor(browserContext: CRBrowserContext, session: CRSession, url: string) {
|
||||
super(browserContext, url);
|
||||
this._session = session;
|
||||
this._browserContext = browserContext;
|
||||
this._networkManager = new CRNetworkManager(session, null, this, null);
|
||||
session.once('Runtime.executionContextCreated', event => {
|
||||
this._createExecutionContext(new CRExecutionContext(session, event.context));
|
||||
});
|
||||
|
||||
if (this._isNetworkInspectionEnabled()) {
|
||||
this._networkManager.initialize().catch(() => {});
|
||||
this.updateRequestInterception();
|
||||
this.updateExtraHTTPHeaders(true);
|
||||
this.updateHttpCredentials(true);
|
||||
this.updateOffline(true);
|
||||
}
|
||||
|
||||
session.send('Runtime.enable', {}).catch(e => { });
|
||||
session.send('Runtime.runIfWaitingForDebugger').catch(e => { });
|
||||
}
|
||||
|
||||
async updateOffline(initial: boolean): Promise<void> {
|
||||
if (!this._isNetworkInspectionEnabled())
|
||||
return;
|
||||
|
||||
const offline = !!this._browserContext._options.offline;
|
||||
if (!initial || offline)
|
||||
await this._networkManager.setOffline(offline);
|
||||
}
|
||||
|
||||
async updateHttpCredentials(initial: boolean): Promise<void> {
|
||||
if (!this._isNetworkInspectionEnabled())
|
||||
return;
|
||||
|
||||
const credentials = this._browserContext._options.httpCredentials || null;
|
||||
if (!initial || credentials)
|
||||
await this._networkManager.authenticate(credentials);
|
||||
}
|
||||
|
||||
async updateExtraHTTPHeaders(initial: boolean): Promise<void> {
|
||||
if (!this._isNetworkInspectionEnabled())
|
||||
return;
|
||||
|
||||
const headers = network.mergeHeaders([
|
||||
this._browserContext._options.extraHTTPHeaders,
|
||||
this._extraHTTPHeaders,
|
||||
]);
|
||||
if (!initial || headers.length)
|
||||
await this._session.send('Network.setExtraHTTPHeaders', { headers: headersArrayToObject(headers, false /* lowerCase */) });
|
||||
}
|
||||
|
||||
updateRequestInterception(): Promise<void> {
|
||||
if (!this._isNetworkInspectionEnabled())
|
||||
return Promise.resolve();
|
||||
|
||||
return this._networkManager.setRequestInterception(this.needsRequestInterception()).catch(e => { });
|
||||
}
|
||||
|
||||
needsRequestInterception(): boolean {
|
||||
return this._isNetworkInspectionEnabled() && !!this._browserContext._requestInterceptor;
|
||||
}
|
||||
|
||||
reportRequestFinished(request: network.Request, response: network.Response | null) {
|
||||
this._browserContext.emit(BrowserContext.Events.RequestFinished, { request, response });
|
||||
}
|
||||
|
||||
requestFailed(request: network.Request, _canceled: boolean) {
|
||||
this._browserContext.emit(BrowserContext.Events.RequestFailed, request);
|
||||
}
|
||||
|
||||
requestReceivedResponse(response: network.Response) {
|
||||
this._browserContext.emit(BrowserContext.Events.Response, response);
|
||||
}
|
||||
|
||||
requestStarted(request: network.Request, route?: network.RouteDelegate) {
|
||||
this._browserContext.emit(BrowserContext.Events.Request, request);
|
||||
if (route) {
|
||||
const r = new network.Route(request, route);
|
||||
if (this._browserContext._requestInterceptor) {
|
||||
this._browserContext._requestInterceptor(r, request);
|
||||
return;
|
||||
}
|
||||
r.continue();
|
||||
}
|
||||
}
|
||||
|
||||
private _isNetworkInspectionEnabled(): boolean {
|
||||
return this._browserContext._options.serviceWorkers === 'allow';
|
||||
}
|
||||
}
|
||||
|
|
@ -80,24 +80,24 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
|||
context.on(BrowserContext.Events.Request, (request: Request) => {
|
||||
return this._dispatchEvent('request', {
|
||||
request: RequestDispatcher.from(this._scope, request),
|
||||
page: PageDispatcher.fromNullable(this._scope, request.frame()._page.initializedOrUndefined())
|
||||
page: PageDispatcher.fromNullable(this._scope, request.frame()?._page.initializedOrUndefined())
|
||||
});
|
||||
});
|
||||
context.on(BrowserContext.Events.Response, (response: Response) => this._dispatchEvent('response', {
|
||||
response: ResponseDispatcher.from(this._scope, response),
|
||||
page: PageDispatcher.fromNullable(this._scope, response.frame()._page.initializedOrUndefined())
|
||||
page: PageDispatcher.fromNullable(this._scope, response.frame()?._page.initializedOrUndefined())
|
||||
}));
|
||||
context.on(BrowserContext.Events.RequestFailed, (request: Request) => this._dispatchEvent('requestFailed', {
|
||||
request: RequestDispatcher.from(this._scope, request),
|
||||
failureText: request._failureText || undefined,
|
||||
responseEndTiming: request._responseEndTiming,
|
||||
page: PageDispatcher.fromNullable(this._scope, request.frame()._page.initializedOrUndefined())
|
||||
page: PageDispatcher.fromNullable(this._scope, request.frame()?._page.initializedOrUndefined())
|
||||
}));
|
||||
context.on(BrowserContext.Events.RequestFinished, ({ request, response }: { request: Request, response: Response | null }) => this._dispatchEvent('requestFinished', {
|
||||
request: RequestDispatcher.from(scope, request),
|
||||
response: ResponseDispatcher.fromNullable(scope, response),
|
||||
responseEndTiming: request._responseEndTiming,
|
||||
page: PageDispatcher.fromNullable(this._scope, request.frame()._page.initializedOrUndefined()),
|
||||
page: PageDispatcher.fromNullable(this._scope, request.frame()?._page.initializedOrUndefined()),
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { WebSocket } from '../network';
|
|||
import type { DispatcherScope } from './dispatcher';
|
||||
import { Dispatcher, existingDispatcher, lookupNullableDispatcher } from './dispatcher';
|
||||
import { FrameDispatcher } from './frameDispatcher';
|
||||
import { WorkerDispatcher } from './pageDispatcher';
|
||||
import { TracingDispatcher } from './tracingDispatcher';
|
||||
|
||||
export class RequestDispatcher extends Dispatcher<Request, channels.RequestChannel> implements channels.RequestChannel {
|
||||
|
|
@ -39,7 +40,8 @@ export class RequestDispatcher extends Dispatcher<Request, channels.RequestChann
|
|||
private constructor(scope: DispatcherScope, request: Request) {
|
||||
const postData = request.postDataBuffer();
|
||||
super(scope, request, 'Request', {
|
||||
frame: FrameDispatcher.from(scope, request.frame()),
|
||||
frame: FrameDispatcher.fromNullable(scope, request.frame()),
|
||||
serviceWorker: WorkerDispatcher.fromNullable(scope, request.serviceWorker()),
|
||||
url: request.url(),
|
||||
resourceType: request.resourceType(),
|
||||
method: request.method(),
|
||||
|
|
|
|||
|
|
@ -296,6 +296,13 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel> imple
|
|||
|
||||
|
||||
export class WorkerDispatcher extends Dispatcher<Worker, channels.WorkerChannel> implements channels.WorkerChannel {
|
||||
static fromNullable(scope: DispatcherScope, worker: Worker | null): WorkerDispatcher | undefined {
|
||||
if (!worker)
|
||||
return undefined;
|
||||
const result = existingDispatcher<WorkerDispatcher>(worker);
|
||||
return result || new WorkerDispatcher(scope, worker);
|
||||
}
|
||||
|
||||
_type_Worker = true;
|
||||
constructor(scope: DispatcherScope, worker: Worker) {
|
||||
super(scope, worker, 'Worker', {
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ class InterceptableRequest {
|
|||
let postDataBuffer = null;
|
||||
if (payload.postData)
|
||||
postDataBuffer = Buffer.from(payload.postData, 'base64');
|
||||
this.request = new network.Request(frame, redirectedFrom ? redirectedFrom.request : null, payload.navigationId,
|
||||
this.request = new network.Request(frame._page._browserContext, frame, null, redirectedFrom ? redirectedFrom.request : null, payload.navigationId,
|
||||
payload.url, internalCauseToResourceType[payload.internalCause] || causeToResourceType[payload.cause] || 'other', payload.method, postDataBuffer, payload.headers);
|
||||
// "raw" headers are the same as "provisional" headers in Firefox.
|
||||
this.request.setRawRequestHeaders(null);
|
||||
|
|
|
|||
|
|
@ -383,7 +383,7 @@ export class FFPage implements PageDelegate {
|
|||
}
|
||||
|
||||
async updateRequestInterception(): Promise<void> {
|
||||
await this._networkManager.setRequestInterception(this._page._needsRequestInterception());
|
||||
await this._networkManager.setRequestInterception(this._page.needsRequestInterception());
|
||||
}
|
||||
|
||||
async updateFileChooserInterception(enabled: boolean) {
|
||||
|
|
|
|||
|
|
@ -293,7 +293,7 @@ export class FrameManager {
|
|||
}
|
||||
|
||||
requestStarted(request: network.Request, route?: network.RouteDelegate) {
|
||||
const frame = request.frame();
|
||||
const frame = request.frame()!;
|
||||
this._inflightRequestStarted(request);
|
||||
if (request._documentId)
|
||||
frame.setPendingDocument({ documentId: request._documentId, request });
|
||||
|
|
@ -303,8 +303,22 @@ export class FrameManager {
|
|||
return;
|
||||
}
|
||||
this._page.emitOnContext(BrowserContext.Events.Request, request);
|
||||
if (route)
|
||||
this._page._requestStarted(request, route);
|
||||
if (route) {
|
||||
const r = new network.Route(request, route);
|
||||
if (this._page._serverRequestInterceptor) {
|
||||
this._page._serverRequestInterceptor(r, request);
|
||||
return;
|
||||
}
|
||||
if (this._page._clientRequestInterceptor) {
|
||||
this._page._clientRequestInterceptor(r, request);
|
||||
return;
|
||||
}
|
||||
if (this._page._browserContext._requestInterceptor) {
|
||||
this._page._browserContext._requestInterceptor(r, request);
|
||||
return;
|
||||
}
|
||||
r.continue();
|
||||
}
|
||||
}
|
||||
|
||||
requestReceivedResponse(response: network.Response) {
|
||||
|
|
@ -321,7 +335,7 @@ export class FrameManager {
|
|||
}
|
||||
|
||||
requestFailed(request: network.Request, canceled: boolean) {
|
||||
const frame = request.frame();
|
||||
const frame = request.frame()!;
|
||||
this._inflightRequestFinished(request);
|
||||
if (frame.pendingDocument() && frame.pendingDocument()!.request === request) {
|
||||
let errorText = request.failure()!.errorText;
|
||||
|
|
@ -359,7 +373,7 @@ export class FrameManager {
|
|||
}
|
||||
|
||||
private _inflightRequestFinished(request: network.Request) {
|
||||
const frame = request.frame();
|
||||
const frame = request.frame()!;
|
||||
if (request._isFavicon)
|
||||
return;
|
||||
if (!frame._inflightRequests.has(request))
|
||||
|
|
@ -370,7 +384,7 @@ export class FrameManager {
|
|||
}
|
||||
|
||||
private _inflightRequestStarted(request: network.Request) {
|
||||
const frame = request.frame();
|
||||
const frame = request.frame()!;
|
||||
if (request._isFavicon)
|
||||
return;
|
||||
frame._inflightRequests.add(request);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import type { APIRequestEvent, APIRequestFinishedEvent } from '../fetch';
|
|||
import { APIRequestContext } from '../fetch';
|
||||
import { helper } from '../helper';
|
||||
import * as network from '../network';
|
||||
import type { Worker } from '../page';
|
||||
import type { Page } from '../page';
|
||||
import type * as har from './har';
|
||||
import { assert, calculateSha1, monotonicTime } from '../../utils';
|
||||
|
|
@ -110,7 +111,9 @@ export class HarTracer {
|
|||
return (request as any)[this._entrySymbol];
|
||||
}
|
||||
|
||||
private _createPageEntryIfNeeded(page: Page): har.Page | undefined {
|
||||
private _createPageEntryIfNeeded(page?: Page): har.Page | undefined {
|
||||
if (!page)
|
||||
return;
|
||||
if (this._options.omitPages)
|
||||
return;
|
||||
if (this._page && page !== this._page)
|
||||
|
|
@ -167,11 +170,13 @@ export class HarTracer {
|
|||
this._addBarrier(page, promise);
|
||||
}
|
||||
|
||||
private _addBarrier(page: Page, promise: Promise<void>) {
|
||||
private _addBarrier(target: Page | Worker | null, promise: Promise<void>) {
|
||||
if (!target)
|
||||
return null;
|
||||
if (!this._options.waitForContentOnStop)
|
||||
return;
|
||||
const race = Promise.race([
|
||||
new Promise<void>(f => page.on('close', () => {
|
||||
new Promise<void>(f => target.on('close', () => {
|
||||
this._barrierPromises.delete(race);
|
||||
f();
|
||||
})),
|
||||
|
|
@ -231,7 +236,7 @@ export class HarTracer {
|
|||
private _onRequest(request: network.Request) {
|
||||
if (!this._shouldIncludeEntryWithUrl(request.url()))
|
||||
return;
|
||||
const page = request.frame()._page;
|
||||
const page = request.frame()?._page;
|
||||
if (this._page && page !== this._page)
|
||||
return;
|
||||
const url = network.parsedURL(request.url());
|
||||
|
|
@ -239,7 +244,7 @@ export class HarTracer {
|
|||
return;
|
||||
|
||||
const pageEntry = this._createPageEntryIfNeeded(page);
|
||||
const harEntry = createHarEntry(request.method(), url, request.frame().guid, this._options);
|
||||
const harEntry = createHarEntry(request.method(), url, request.frame()?.guid, this._options);
|
||||
if (pageEntry)
|
||||
harEntry.pageref = pageEntry.id;
|
||||
harEntry.request.postData = this._postDataForRequest(request, this._options.content);
|
||||
|
|
@ -261,7 +266,7 @@ export class HarTracer {
|
|||
const harEntry = this._entryForRequest(request);
|
||||
if (!harEntry)
|
||||
return;
|
||||
const page = request.frame()._page;
|
||||
const page = request.frame()?._page;
|
||||
|
||||
const httpVersion = response.httpVersion();
|
||||
harEntry.request.httpVersion = httpVersion;
|
||||
|
|
@ -287,7 +292,7 @@ export class HarTracer {
|
|||
}
|
||||
};
|
||||
if (compressionCalculationBarrier)
|
||||
this._addBarrier(page, compressionCalculationBarrier.barrier);
|
||||
this._addBarrier(page || request.serviceWorker(), compressionCalculationBarrier.barrier);
|
||||
|
||||
const promise = response.body().then(buffer => {
|
||||
if (this._options.skipScripts && request.resourceType() === 'script') {
|
||||
|
|
@ -304,10 +309,10 @@ export class HarTracer {
|
|||
if (this._started)
|
||||
this._delegate.onEntryFinished(harEntry);
|
||||
});
|
||||
this._addBarrier(page, promise);
|
||||
this._addBarrier(page || request.serviceWorker(), promise);
|
||||
|
||||
if (!this._options.omitSizes) {
|
||||
this._addBarrier(page, response.sizes().then(sizes => {
|
||||
this._addBarrier(page || request.serviceWorker(), response.sizes().then(sizes => {
|
||||
harEntry.response.bodySize = sizes.responseBodySize;
|
||||
harEntry.response.headersSize = sizes.responseHeadersSize;
|
||||
harEntry.response._transferSize = sizes.transferSize;
|
||||
|
|
@ -361,7 +366,7 @@ export class HarTracer {
|
|||
const harEntry = this._entryForRequest(response.request());
|
||||
if (!harEntry)
|
||||
return;
|
||||
const page = response.frame()._page;
|
||||
const page = response.frame()?._page;
|
||||
const pageEntry = this._createPageEntryIfNeeded(page);
|
||||
const request = response.request();
|
||||
|
||||
|
|
@ -404,7 +409,7 @@ export class HarTracer {
|
|||
}
|
||||
|
||||
if (!this._options.omitServerIP) {
|
||||
this._addBarrier(page, response.serverAddr().then(server => {
|
||||
this._addBarrier(page || request.serviceWorker(), response.serverAddr().then(server => {
|
||||
if (server?.ipAddress)
|
||||
harEntry.serverIPAddress = server.ipAddress;
|
||||
if (server?.port)
|
||||
|
|
@ -412,19 +417,19 @@ export class HarTracer {
|
|||
}));
|
||||
}
|
||||
if (!this._options.omitSecurityDetails) {
|
||||
this._addBarrier(page, response.securityDetails().then(details => {
|
||||
this._addBarrier(page || request.serviceWorker(), response.securityDetails().then(details => {
|
||||
if (details)
|
||||
harEntry._securityDetails = details;
|
||||
}));
|
||||
}
|
||||
this._addBarrier(page, request.rawRequestHeaders().then(headers => {
|
||||
this._addBarrier(page || request.serviceWorker(), request.rawRequestHeaders().then(headers => {
|
||||
if (!this._options.omitCookies) {
|
||||
for (const header of headers.filter(header => header.name.toLowerCase() === 'cookie'))
|
||||
harEntry.request.cookies.push(...header.value.split(';').map(parseCookie));
|
||||
}
|
||||
harEntry.request.headers = headers;
|
||||
}));
|
||||
this._addBarrier(page, response.rawResponseHeaders().then(headers => {
|
||||
this._addBarrier(page || request.serviceWorker(), response.rawResponseHeaders().then(headers => {
|
||||
if (!this._options.omitCookies) {
|
||||
for (const header of headers.filter(header => header.name.toLowerCase() === 'set-cookie'))
|
||||
harEntry.response.cookies.push(parseCookie(header.value));
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type * as contexts from './browserContext';
|
||||
import type * as pages from './page';
|
||||
import type * as frames from './frames';
|
||||
import type * as types from './types';
|
||||
import type * as channels from '../protocol/channels';
|
||||
|
|
@ -97,16 +99,20 @@ export class Request extends SdkObject {
|
|||
private _postData: Buffer | null;
|
||||
readonly _headers: types.HeadersArray;
|
||||
private _headersMap = new Map<string, string>();
|
||||
readonly _frame: frames.Frame | null = null;
|
||||
readonly _serviceWorker: pages.Worker | null = null;
|
||||
readonly _context: contexts.BrowserContext;
|
||||
private _rawRequestHeadersPromise = new ManualPromise<types.HeadersArray>();
|
||||
private _frame: frames.Frame;
|
||||
private _waitForResponsePromise = new ManualPromise<Response | null>();
|
||||
_responseEndTiming = -1;
|
||||
|
||||
constructor(frame: frames.Frame, redirectedFrom: Request | null, documentId: string | undefined,
|
||||
constructor(context: contexts.BrowserContext, frame: frames.Frame | null, serviceWorker: pages.Worker | null, redirectedFrom: Request | null, documentId: string | undefined,
|
||||
url: string, resourceType: string, method: string, postData: Buffer | null, headers: types.HeadersArray) {
|
||||
super(frame, 'request');
|
||||
super(frame || context, 'request');
|
||||
assert(!url.startsWith('data:'), 'Data urls should not fire requests');
|
||||
this._context = context;
|
||||
this._frame = frame;
|
||||
this._serviceWorker = serviceWorker;
|
||||
this._redirectedFrom = redirectedFrom;
|
||||
if (redirectedFrom)
|
||||
redirectedFrom._redirectedTo = this;
|
||||
|
|
@ -177,10 +183,14 @@ export class Request extends SdkObject {
|
|||
return this._redirectedTo ? this._redirectedTo._finalRequest() : this;
|
||||
}
|
||||
|
||||
frame(): frames.Frame {
|
||||
frame(): frames.Frame | null {
|
||||
return this._frame;
|
||||
}
|
||||
|
||||
serviceWorker(): pages.Worker | null {
|
||||
return this._serviceWorker;
|
||||
}
|
||||
|
||||
isNavigationRequest(): boolean {
|
||||
return !!this._documentId;
|
||||
}
|
||||
|
|
@ -219,7 +229,7 @@ export class Route extends SdkObject {
|
|||
private _handled = false;
|
||||
|
||||
constructor(request: Request, delegate: RouteDelegate) {
|
||||
super(request.frame(), 'route');
|
||||
super(request._frame || request._context , 'route');
|
||||
this._request = request;
|
||||
this._delegate = delegate;
|
||||
}
|
||||
|
|
@ -236,7 +246,7 @@ export class Route extends SdkObject {
|
|||
async redirectNavigationRequest(url: string) {
|
||||
this._startHandling();
|
||||
assert(this._request.isNavigationRequest());
|
||||
this._request.frame().redirectNavigation(url, this._request._documentId!, this._request.headerValue('referer'));
|
||||
this._request.frame()!.redirectNavigation(url, this._request._documentId!, this._request.headerValue('referer'));
|
||||
}
|
||||
|
||||
async fulfill(overrides: channels.RouteFulfillParams) {
|
||||
|
|
@ -245,8 +255,7 @@ export class Route extends SdkObject {
|
|||
let isBase64 = overrides.isBase64 || false;
|
||||
if (body === undefined) {
|
||||
if (overrides.fetchResponseUid) {
|
||||
const context = this._request.frame()._page._browserContext;
|
||||
const buffer = context.fetchRequest.fetchResponses.get(overrides.fetchResponseUid) || APIRequestContext.findResponseBody(overrides.fetchResponseUid);
|
||||
const buffer = this._request._context.fetchRequest.fetchResponses.get(overrides.fetchResponseUid) || APIRequestContext.findResponseBody(overrides.fetchResponseUid);
|
||||
assert(buffer, 'Fetch response has been disposed');
|
||||
body = buffer.toString('base64');
|
||||
isBase64 = true;
|
||||
|
|
@ -357,7 +366,7 @@ export class Response extends SdkObject {
|
|||
private _responseHeadersSizePromise = new ManualPromise<number | null>();
|
||||
|
||||
constructor(request: Request, status: number, statusText: string, headers: types.HeadersArray, timing: ResourceTiming, getResponseBodyCallback: GetResponseBodyCallback, fromServiceWorker: boolean, httpVersion?: string) {
|
||||
super(request.frame(), 'response');
|
||||
super(request.frame() || request._context, 'response');
|
||||
this._request = request;
|
||||
this._timing = timing;
|
||||
this._status = status;
|
||||
|
|
@ -458,7 +467,7 @@ export class Response extends SdkObject {
|
|||
return this._request;
|
||||
}
|
||||
|
||||
frame(): frames.Frame {
|
||||
frame(): frames.Frame | null {
|
||||
return this._request.frame();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -161,8 +161,8 @@ export class Page extends SdkObject {
|
|||
private _workers = new Map<string, Worker>();
|
||||
readonly pdf: ((options: channels.PagePdfParams) => Promise<Buffer>) | undefined;
|
||||
readonly coverage: any;
|
||||
private _clientRequestInterceptor: network.RouteHandler | undefined;
|
||||
private _serverRequestInterceptor: network.RouteHandler | undefined;
|
||||
_clientRequestInterceptor: network.RouteHandler | undefined;
|
||||
_serverRequestInterceptor: network.RouteHandler | undefined;
|
||||
_ownedContext: BrowserContext | undefined;
|
||||
readonly selectors: Selectors;
|
||||
_pageIsError: Error | undefined;
|
||||
|
|
@ -439,7 +439,7 @@ export class Page extends SdkObject {
|
|||
await this._delegate.removeInitScripts();
|
||||
}
|
||||
|
||||
_needsRequestInterception(): boolean {
|
||||
needsRequestInterception(): boolean {
|
||||
return !!this._clientRequestInterceptor || !!this._serverRequestInterceptor || !!this._browserContext._requestInterceptor;
|
||||
}
|
||||
|
||||
|
|
@ -453,23 +453,6 @@ export class Page extends SdkObject {
|
|||
await this._delegate.updateRequestInterception();
|
||||
}
|
||||
|
||||
_requestStarted(request: network.Request, routeDelegate: network.RouteDelegate) {
|
||||
const route = new network.Route(request, routeDelegate);
|
||||
if (this._serverRequestInterceptor) {
|
||||
this._serverRequestInterceptor(route, request);
|
||||
return;
|
||||
}
|
||||
if (this._clientRequestInterceptor) {
|
||||
this._clientRequestInterceptor(route, request);
|
||||
return;
|
||||
}
|
||||
if (this._browserContext._requestInterceptor) {
|
||||
this._browserContext._requestInterceptor(route, request);
|
||||
return;
|
||||
}
|
||||
route.continue();
|
||||
}
|
||||
|
||||
async expectScreenshot(metadata: CallMetadata, options: ExpectScreenshotOptions = {}): Promise<{ actual?: Buffer, previous?: Buffer, diff?: Buffer, errorMessage?: string, log?: string[] }> {
|
||||
const locator = options.locator;
|
||||
const rafrafScreenshot = locator ? async (progress: Progress, timeout: number) => {
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ export class WKInterceptableRequest {
|
|||
this._wallTime = event.walltime * 1000;
|
||||
if (event.request.postData)
|
||||
postDataBuffer = Buffer.from(event.request.postData, 'base64');
|
||||
this.request = new network.Request(frame, redirectedFrom?.request || null, documentId, event.request.url,
|
||||
this.request = new network.Request(frame._page._browserContext, frame, null, redirectedFrom?.request || null, documentId, event.request.url,
|
||||
resourceType, event.request.method, postDataBuffer, headersObjectToArray(event.request.headers));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ export class WKPage implements PageDelegate {
|
|||
session.send('Network.enable'),
|
||||
this._workers.initializeSession(session)
|
||||
];
|
||||
if (this._page._needsRequestInterception()) {
|
||||
if (this._page.needsRequestInterception()) {
|
||||
promises.push(session.send('Network.setInterceptionEnabled', { enabled: true }));
|
||||
promises.push(session.send('Network.addInterception', { url: '.*', stage: 'request', isRegex: true }));
|
||||
}
|
||||
|
|
@ -708,7 +708,7 @@ export class WKPage implements PageDelegate {
|
|||
}
|
||||
|
||||
async updateRequestInterception(): Promise<void> {
|
||||
const enabled = this._page._needsRequestInterception();
|
||||
const enabled = this._page.needsRequestInterception();
|
||||
await Promise.all([
|
||||
this._updateState('Network.setInterceptionEnabled', { enabled }),
|
||||
this._updateState('Network.addInterception', { url: '.*', stage: 'request', isRegex: true }),
|
||||
|
|
@ -1011,7 +1011,7 @@ export class WKPage implements PageDelegate {
|
|||
const documentId = isNavigationRequest ? event.loaderId : undefined;
|
||||
let route = null;
|
||||
// We do not support intercepting redirects.
|
||||
if (this._page._needsRequestInterception() && !redirectedFrom)
|
||||
if (this._page.needsRequestInterception() && !redirectedFrom)
|
||||
route = new WKRouteImpl(session, event.requestId);
|
||||
const request = new WKInterceptableRequest(session, route, frame, event, redirectedFrom, documentId);
|
||||
this._requestIdToRequest.set(event.requestId, request);
|
||||
|
|
|
|||
7
packages/playwright-core/types/types.d.ts
vendored
7
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -14999,6 +14999,13 @@ export interface Request {
|
|||
*/
|
||||
response(): Promise<null|Response>;
|
||||
|
||||
/**
|
||||
* > NOTE: This field is Chromium only. It's safe to call when using other browsers, but it will always be `null`.
|
||||
*
|
||||
* The Service [Worker] that is performing the request.
|
||||
*/
|
||||
serviceWorker(): null|Worker;
|
||||
|
||||
/**
|
||||
* Returns resource size information for given request.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
self.intercepted = [];
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
self.intercepted.push(event.request.url)
|
||||
event.respondWith(fetch(event.request));
|
||||
});
|
||||
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(clients.claim());
|
||||
});
|
||||
|
||||
fetch('/request-from-within-worker.txt')
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ import { removeFolders } from '../../packages/playwright-core/lib/utils/fileUtil
|
|||
import { baseTest } from './baseTest';
|
||||
import type { RemoteServerOptions } from './remoteServer';
|
||||
import { RemoteServer } from './remoteServer';
|
||||
import type { Log } from '../../packages/playwright-core/src/server/har/har';
|
||||
import { parseHar } from '../config/utils';
|
||||
|
||||
export type BrowserTestWorkerFixtures = PageWorkerFixtures & {
|
||||
browserVersion: string;
|
||||
|
|
@ -38,6 +40,7 @@ type BrowserTestTestFixtures = PageTestFixtures & {
|
|||
launchPersistent: (options?: Parameters<BrowserType['launchPersistentContext']>[1]) => Promise<{ context: BrowserContext, page: Page }>;
|
||||
startRemoteServer: (options?: RemoteServerOptions) => Promise<RemoteServer>;
|
||||
contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>;
|
||||
pageWithHar(options?: { outputPath?: string, content?: 'embed' | 'attach' | 'omit', omitContent?: boolean }): Promise<{ context: BrowserContext, page: Page, getLog: () => Promise<Log>, getZip: () => Promise<Map<string, Buffer>> }>
|
||||
};
|
||||
|
||||
const test = baseTest.extend<BrowserTestTestFixtures, BrowserTestWorkerFixtures>({
|
||||
|
|
@ -110,6 +113,26 @@ const test = baseTest.extend<BrowserTestTestFixtures, BrowserTestWorkerFixtures>
|
|||
await new Promise(f => setTimeout(f, 1000));
|
||||
}
|
||||
},
|
||||
pageWithHar: async ({ contextFactory }, use, testInfo) => {
|
||||
const pageWithHar = async (options: { outputPath?: string, content?: 'embed' | 'attach' | 'omit', omitContent?: boolean } = {}) => {
|
||||
const harPath = testInfo.outputPath(options.outputPath || 'test.har');
|
||||
const context = await contextFactory({ recordHar: { path: harPath, content: options.content, omitContent: options.omitContent }, ignoreHTTPSErrors: true });
|
||||
const page = await context.newPage();
|
||||
return {
|
||||
page,
|
||||
context,
|
||||
getLog: async () => {
|
||||
await context.close();
|
||||
return JSON.parse(fs.readFileSync(harPath).toString())['log'] as Log;
|
||||
},
|
||||
getZip: async () => {
|
||||
await context.close();
|
||||
return parseHar(harPath);
|
||||
},
|
||||
};
|
||||
};
|
||||
await use(pageWithHar);
|
||||
}
|
||||
});
|
||||
|
||||
export const playwrightTest = test;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,257 @@ test('should create a worker from a service worker', async ({ page, server }) =>
|
|||
expect(await worker.evaluate(() => self.toString())).toBe('[object ServiceWorkerGlobalScope]');
|
||||
});
|
||||
|
||||
test('should create a worker from service worker with noop routing', async ({ context, page, server }) => {
|
||||
await context.route('**', route => route.continue());
|
||||
const [worker] = await Promise.all([
|
||||
page.context().waitForEvent('serviceworker'),
|
||||
page.goto(server.PREFIX + '/serviceworkers/empty/sw.html')
|
||||
]);
|
||||
expect(await worker.evaluate(() => self.toString())).toBe('[object ServiceWorkerGlobalScope]');
|
||||
});
|
||||
|
||||
test('serviceWorker(), and fromServiceWorker() work', async ({ context, page, server }) => {
|
||||
const [worker, html, main, inWorker] = await Promise.all([
|
||||
context.waitForEvent('serviceworker'),
|
||||
context.waitForEvent('request', r => r.url().endsWith('/sw.html')),
|
||||
context.waitForEvent('request', r => r.url().endsWith('/sw.js')),
|
||||
context.waitForEvent('request', r => r.url().endsWith('/request-from-within-worker.txt')),
|
||||
page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html')
|
||||
]);
|
||||
const [inner] = await Promise.all([
|
||||
context.waitForEvent('request', r => r.url().endsWith('/inner.txt')),
|
||||
page.evaluate(() => fetch('/inner.txt')),
|
||||
]);
|
||||
expect(html.frame()).toBeTruthy();
|
||||
expect(html.serviceWorker()).toBe(null);
|
||||
expect((await html.response()).fromServiceWorker()).toBe(false);
|
||||
|
||||
expect(main.frame).toThrow();
|
||||
expect(main.serviceWorker()).toBe(worker);
|
||||
expect((await main.response()).fromServiceWorker()).toBe(false);
|
||||
|
||||
expect(inner.frame()).toBeTruthy();
|
||||
expect(inner.serviceWorker()).toBe(null);
|
||||
expect((await inner.response()).fromServiceWorker()).toBe(true);
|
||||
|
||||
expect(inWorker.frame).toThrow();
|
||||
expect(inWorker.serviceWorker()).toBe(worker);
|
||||
expect((await inWorker.response()).fromServiceWorker()).toBe(false);
|
||||
|
||||
await page.evaluate(() => window['activationPromise']);
|
||||
const [innerSW, innerPage] = await Promise.all([
|
||||
context.waitForEvent('request', r => r.url().endsWith('/inner.txt') && !!r.serviceWorker()),
|
||||
context.waitForEvent('request', r => r.url().endsWith('/inner.txt') && !r.serviceWorker()),
|
||||
page.evaluate(() => fetch('/inner.txt')),
|
||||
]);
|
||||
expect(innerPage.serviceWorker()).toBe(null);
|
||||
expect((await innerPage.response()).fromServiceWorker()).toBe(true);
|
||||
|
||||
expect(innerSW.serviceWorker()).toBe(worker);
|
||||
expect((await innerSW.response()).fromServiceWorker()).toBe(false);
|
||||
});
|
||||
|
||||
test('should intercept service worker requests (main and within)', async ({ context, page, server }) => {
|
||||
await context.route('**/request-from-within-worker', route =>
|
||||
route.fulfill({
|
||||
contentType: 'application/json',
|
||||
status: 200,
|
||||
body: '"intercepted!"',
|
||||
})
|
||||
);
|
||||
|
||||
await context.route('**/sw.js', route =>
|
||||
route.fulfill({
|
||||
contentType: 'text/javascript',
|
||||
status: 200,
|
||||
body: `
|
||||
self.contentPromise = new Promise(res => fetch('/request-from-within-worker').then(r => r.json()).then(res));
|
||||
`,
|
||||
})
|
||||
);
|
||||
|
||||
const [ sw ] = await Promise.all([
|
||||
context.waitForEvent('serviceworker'),
|
||||
page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'),
|
||||
]);
|
||||
|
||||
await expect(sw.evaluate(() => self['contentPromise'])).resolves.toBe('intercepted!');
|
||||
});
|
||||
|
||||
test('should report failure (due to content-type) of main service worker request', async ({ server, page, context, browserMajorVersion }) => {
|
||||
test.skip(browserMajorVersion < 104, 'Requires http://crrev.com/1012503 or later.');
|
||||
server.setRoute('/serviceworkers/fetch/sw.js', (req, res) => {
|
||||
res.writeHead(200, 'OK', { 'Content-Type': 'text/html' });
|
||||
res.write(`console.log('hi from sw');`);
|
||||
res.end();
|
||||
});
|
||||
const [, main] = await Promise.all([
|
||||
server.waitForRequest('/serviceworkers/fetch/sw.js'),
|
||||
context.waitForEvent('request', r => r.url().endsWith('sw.js')),
|
||||
page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html'),
|
||||
]);
|
||||
// This will timeout today
|
||||
await main.response();
|
||||
});
|
||||
|
||||
test('should report failure (due to redirect) of main service worker request', async ({ server, page, context, browserMajorVersion }) => {
|
||||
test.skip(browserMajorVersion < 104, 'Requires http://crrev.com/1012503 or later.');
|
||||
server.setRedirect('/serviceworkers/empty/sw.js', '/dev/null');
|
||||
const [, main] = await Promise.all([
|
||||
server.waitForRequest('/serviceworkers/empty/sw.js'),
|
||||
context.waitForEvent('request', r => r.url().endsWith('sw.js')),
|
||||
page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'),
|
||||
]);
|
||||
// This will timeout today
|
||||
const resp = await main.response();
|
||||
expect(resp.status()).toBe(302);
|
||||
});
|
||||
|
||||
test('should intercept service worker importScripts', async ({ context, page, server }) => {
|
||||
await context.route('**/import.js', route =>
|
||||
route.fulfill({
|
||||
contentType: 'text/javascript',
|
||||
status: 200,
|
||||
body: 'self.exportedValue = 47;',
|
||||
})
|
||||
);
|
||||
|
||||
await context.route('**/sw.js', route =>
|
||||
route.fulfill({
|
||||
contentType: 'text/javascript',
|
||||
status: 200,
|
||||
body: `
|
||||
importScripts('/import.js');
|
||||
self.importedValue = self.exportedValue;
|
||||
`,
|
||||
})
|
||||
);
|
||||
|
||||
const [ sw ] = await Promise.all([
|
||||
context.waitForEvent('serviceworker'),
|
||||
page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'),
|
||||
]);
|
||||
|
||||
await expect(sw.evaluate(() => self['importedValue'])).resolves.toBe(47);
|
||||
});
|
||||
|
||||
test('should report intercepted service worker requests in HAR', async ({ pageWithHar, server }) => {
|
||||
const { context, page, getLog } = await pageWithHar();
|
||||
await context.route('**/request-from-within-worker', route =>
|
||||
route.fulfill({
|
||||
contentType: 'application/json',
|
||||
headers: {
|
||||
'x-pw-test': 'request-within-worker',
|
||||
},
|
||||
status: 200,
|
||||
body: '"intercepted!"',
|
||||
})
|
||||
);
|
||||
|
||||
await context.route('**/sw.js', route =>
|
||||
route.fulfill({
|
||||
contentType: 'text/javascript',
|
||||
headers: {
|
||||
'x-pw-test': 'intercepted-main',
|
||||
},
|
||||
status: 200,
|
||||
body: `
|
||||
self.contentPromise = new Promise(res => fetch('/request-from-within-worker').then(r => r.json()).then(res));
|
||||
`,
|
||||
})
|
||||
);
|
||||
|
||||
const [ sw ] = await Promise.all([
|
||||
context.waitForEvent('serviceworker'),
|
||||
page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'),
|
||||
]);
|
||||
|
||||
await expect(sw.evaluate(() => self['contentPromise'])).resolves.toBe('intercepted!');
|
||||
|
||||
const log = await getLog();
|
||||
{
|
||||
const sw = log.entries.filter(e => e.request.url.endsWith('sw.js'));
|
||||
expect.soft(sw).toHaveLength(1);
|
||||
expect.soft(sw[0].response.headers.filter(v => v.name === 'x-pw-test')).toEqual([{ name: 'x-pw-test', value: 'intercepted-main' }]);
|
||||
}
|
||||
{
|
||||
const req = log.entries.filter(e => e.request.url.endsWith('request-from-within-worker'));
|
||||
expect.soft(req).toHaveLength(1);
|
||||
expect.soft(req[0].response.headers.filter(v => v.name === 'x-pw-test')).toEqual([{ name: 'x-pw-test', value: 'request-within-worker' }]);
|
||||
expect.soft(req[0].response.content.text).toBe('"intercepted!"');
|
||||
}
|
||||
});
|
||||
|
||||
test('should intercept only serviceworker request, not page', async ({ context, page, server }) => {
|
||||
await context.route('**/data.json', async route => {
|
||||
if (route.request().serviceWorker()) {
|
||||
return route.fulfill({
|
||||
contentType: 'text/plain',
|
||||
status: 200,
|
||||
body: 'from sw',
|
||||
});
|
||||
} else {
|
||||
return route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
const [ sw ] = await Promise.all([
|
||||
context.waitForEvent('serviceworker'),
|
||||
page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html'),
|
||||
]);
|
||||
await page.evaluate(() => window['activationPromise']);
|
||||
const response = await page.evaluate(() => fetch('/data.json').then(r => r.text()));
|
||||
const [ url ] = await sw.evaluate(() => self['intercepted']);
|
||||
expect(url).toMatch(/\/data\.json$/);
|
||||
expect(response).toBe('from sw');
|
||||
});
|
||||
|
||||
test('setOffline', async ({ context, page, server }) => {
|
||||
const [worker] = await Promise.all([
|
||||
context.waitForEvent('serviceworker'),
|
||||
page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html')
|
||||
]);
|
||||
|
||||
await page.evaluate(() => window['activationPromise']);
|
||||
await context.setOffline(true);
|
||||
const [,error] = await Promise.all([
|
||||
context.waitForEvent('request', r => r.url().endsWith('/inner.txt') && !!r.serviceWorker()),
|
||||
worker.evaluate(() => fetch('/inner.txt').catch(e => `REJECTED: ${e}`)),
|
||||
]);
|
||||
expect(error).toMatch(/REJECTED.*Failed to fetch/);
|
||||
});
|
||||
|
||||
|
||||
test('setExtraHTTPHeaders', async ({ context, page, server }) => {
|
||||
const [worker] = await Promise.all([
|
||||
context.waitForEvent('serviceworker'),
|
||||
page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html')
|
||||
]);
|
||||
|
||||
await page.evaluate(() => window['activationPromise']);
|
||||
await context.setExtraHTTPHeaders({ 'x-custom-header': 'custom!' });
|
||||
const requestPromise = server.waitForRequest('/inner.txt');
|
||||
await worker.evaluate(() => fetch('/inner.txt'));
|
||||
const req = await requestPromise;
|
||||
expect(req.headers['x-custom-header']).toBe('custom!');
|
||||
});
|
||||
|
||||
test.describe('http credentials', () => {
|
||||
test.use({ httpCredentials: { username: 'user', password: 'pass' } });
|
||||
|
||||
test('httpCredentials', async ({ context, page, server }) => {
|
||||
server.setAuth('/serviceworkers/fetch/sw.html', 'user', 'pass');
|
||||
server.setAuth('/empty.html', 'user', 'pass');
|
||||
const [worker] = await Promise.all([
|
||||
context.waitForEvent('serviceworker'),
|
||||
page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html')
|
||||
]);
|
||||
|
||||
await page.evaluate(() => window['activationPromise']);
|
||||
expect(await worker.evaluate(() => fetch('/empty.html').then(r => r.status))).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
test('serviceWorkers() should return current workers', async ({ page, server }) => {
|
||||
const context = page.context();
|
||||
const [worker1] = await Promise.all([
|
||||
|
|
|
|||
Loading…
Reference in a new issue