mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat: Page.routeFromHar (#14870)
This commit is contained in:
parent
06c8d8e31c
commit
259c8d64a5
@ -1025,6 +1025,35 @@ handler function to route the request.
|
||||
|
||||
How often a route should be used. By default it will be used every time.
|
||||
|
||||
## async method: BrowserContext.routeFromHar
|
||||
|
||||
Provides the capability to serve network requests that are made in the context from prerecorded HAR file.
|
||||
|
||||
:::note
|
||||
[`method: BrowserContext.routeFromHar`] will not intercept requests intercepted by Service Worker. See [this](https://github.com/microsoft/playwright/issues/1090) issue. We recommend disabling Service Workers when using request interception. Via `await context.addInitScript(() => delete window.navigator.serviceWorker);`
|
||||
:::
|
||||
|
||||
### param: BrowserContext.routeFromHar.harPath
|
||||
- `harPath` <[path]>
|
||||
|
||||
Path to the HAR file with prerecorded network data. If HAR file contains an entry with the matching url and HTTP method, then the entry's headers, status and body will be used to fulfill. An entry resulting in a redirect will be followed automatically. If there is no matching entry in the file the execution continues to try other configured HAR files and [Route] handlers.
|
||||
If `path` is a relative path, then it is resolved relative to the current working directory.
|
||||
|
||||
### option: BrowserContext.routeFromHar.strict
|
||||
- `strict` <[boolean]>
|
||||
|
||||
If set to true any request not found in the HAR file will be aborted. If set to
|
||||
false missing requests will continue normal flow and can be handled by other
|
||||
[Route] handlers or served from other HAR files configured with [`method: BrowserContext.routeFromHar`].
|
||||
Defaults to false.
|
||||
|
||||
### option: BrowserContext.routeFromHar.url
|
||||
- `url` <[string]|[RegExp]>
|
||||
|
||||
A glob pattern or regular expression to match request URL while routing. Only requests
|
||||
with URL matching the pattern will be surved from the HAR file. If not specified, all
|
||||
requests are served from the HAR file.
|
||||
|
||||
## method: BrowserContext.serviceWorkers
|
||||
* langs: js, python
|
||||
- returns: <[Array]<[Worker]>>
|
||||
@ -1191,6 +1220,15 @@ Optional handler function used to register a routing with [`method: BrowserConte
|
||||
|
||||
Optional handler function used to register a routing with [`method: BrowserContext.route`].
|
||||
|
||||
## async method: BrowserContext.unrouteFromHar
|
||||
|
||||
Removes HAR handler previously added with [`method: BrowserContext.routeFromHar`].
|
||||
|
||||
### param: BrowserContext.unrouteFromHar.harPath
|
||||
- `harPath` <[path]>
|
||||
|
||||
Path to the HAR file which was passed to [`method: BrowserContext.routeFromHar`].
|
||||
|
||||
## async method: BrowserContext.waitForEvent
|
||||
* langs: js, python
|
||||
- alias-python: expect_event
|
||||
|
||||
@ -2732,6 +2732,35 @@ handler function to route the request.
|
||||
|
||||
How often a route should be used. By default it will be used every time.
|
||||
|
||||
## async method: Page.routeFromHar
|
||||
|
||||
Provides the capability to serve network requests that are made by a page from prerecorded HAR file.
|
||||
|
||||
:::note
|
||||
[`method: Page.routeFromHar`] will not intercept requests intercepted by Service Worker. See [this](https://github.com/microsoft/playwright/issues/1090) issue. We recommend disabling Service Workers when using request interception. Via `await context.addInitScript(() => delete window.navigator.serviceWorker);`
|
||||
:::
|
||||
|
||||
### param: Page.routeFromHar.harPath
|
||||
- `harPath` <[path]>
|
||||
|
||||
Path to the HAR file with prerecorded network data. If HAR file contains an entry with the matching url and HTTP method, then the entry's headers, status and body will be used to fulfill. An entry resulting in a redirect will be followed automatically. If there is no matching entry in the file the execution continues to try other configured HAR files and [Route] handlers.
|
||||
If `path` is a relative path, then it is resolved relative to the current working directory.
|
||||
|
||||
### option: Page.routeFromHar.strict
|
||||
- `strict` <[boolean]>
|
||||
|
||||
If set to true any request not found in the HAR file will be aborted. If set to
|
||||
false missing requests will continue normal flow and can be handled by other
|
||||
[Route] handlers or served from other HAR files configured with [`method: Page.routeFromHar`].
|
||||
Defaults to false.
|
||||
|
||||
### option: Page.routeFromHar.url
|
||||
- `url` <[string]|[RegExp]>
|
||||
|
||||
A glob pattern or regular expression to match request URL while routing. Only requests
|
||||
with URL matching the pattern will be surved from the HAR file. If not specified, all
|
||||
requests are served from the HAR file.
|
||||
|
||||
## async method: Page.screenshot
|
||||
- returns: <[Buffer]>
|
||||
|
||||
@ -3118,6 +3147,15 @@ Optional handler function to route the request.
|
||||
|
||||
Optional handler function to route the request.
|
||||
|
||||
## async method: Page.unrouteFromHar
|
||||
|
||||
Removes HAR handler previously added with [`method: Page.routeFromHar`].
|
||||
|
||||
### param: Page.unrouteFromHar.harPath
|
||||
- `harPath` <[path]>
|
||||
|
||||
Path to the HAR file which was passed to [`method: Page.routeFromHar`].
|
||||
|
||||
## method: Page.url
|
||||
- returns: <[string]>
|
||||
|
||||
|
||||
@ -477,30 +477,6 @@ Optional response body as text.
|
||||
|
||||
Optional response body as raw bytes.
|
||||
|
||||
### option: Route.fulfill.har
|
||||
* langs: js
|
||||
- `har` <[Object]>
|
||||
- `path` <[string]> Path to the HAR file.
|
||||
- `fallback` ?<[RouteHARFallback]<"abort"|"continue"|"throw">> Behavior in the case where matching entry was not found in the HAR. Either [`method: Route.abort`] the request, [`method: Route.continue`] it, or throw an error. Defaults to "abort".
|
||||
|
||||
HAR file to extract the response from. If HAR file contains an entry with the matching url and HTTP method, then the entry's headers, status and body will be used to fulfill. An entry resulting in a redirect will be followed automatically. Individual fields such as headers can be overridden using fulfill options.
|
||||
If `path` is a relative path, then it is resolved relative to the current working directory.
|
||||
|
||||
### option: Route.fulfill.harPath
|
||||
* langs: csharp, java, python
|
||||
- alias-python: har_path
|
||||
- `harPath` <[path]>
|
||||
|
||||
HAR file to extract the response from. If HAR file contains an entry with the matching url and HTTP method, then the entry's headers, status and body will be used to fulfill. An entry resulting in a redirect will be followed automatically. Individual fields such as headers can be overridden using fulfill options.
|
||||
If `path` is a relative path, then it is resolved relative to the current working directory.
|
||||
|
||||
### option: Route.fulfill.harFallback
|
||||
* langs: csharp, java, python
|
||||
- alias-python: har_fallback
|
||||
- `harFallback` ?<[RouteHARFallback]<"abort"|"continue"|"throw">>
|
||||
|
||||
Behavior in the case where matching entry was not found in the HAR. Either [`method: Route.abort`] the request, [`method: Route.continue`] it, or throw an error. Defaults to "abort".
|
||||
|
||||
### option: Route.fulfill.path
|
||||
- `path` <[path]>
|
||||
|
||||
|
||||
@ -40,6 +40,7 @@ import { Artifact } from './artifact';
|
||||
import { APIRequestContext } from './fetch';
|
||||
import { createInstrumentation } from './clientInstrumentation';
|
||||
import { rewriteErrorMessage } from '../utils/stackTrace';
|
||||
import { HarRouter } from './harRouter';
|
||||
|
||||
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel> implements api.BrowserContext {
|
||||
_pages = new Set<Page>();
|
||||
@ -51,6 +52,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
||||
_ownerPage: Page | undefined;
|
||||
private _closedPromise: Promise<void>;
|
||||
_options: channels.BrowserNewContextParams = { };
|
||||
private readonly _harRouter = new HarRouter(this);
|
||||
|
||||
readonly request: APIRequestContext;
|
||||
readonly tracing: Tracing;
|
||||
@ -273,6 +275,14 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
||||
await this._disableInterception();
|
||||
}
|
||||
|
||||
async routeFromHar(harPath: string, options?: { strict?: boolean; url?: string|RegExp; }): Promise<void> {
|
||||
await this._harRouter.routeFromHar(harPath, options);
|
||||
}
|
||||
|
||||
async unrouteFromHar(harPath: string): Promise<void> {
|
||||
await this._harRouter.unrouteFromHar(harPath);
|
||||
}
|
||||
|
||||
async _unrouteAll() {
|
||||
this._routes = [];
|
||||
await this._disableInterception();
|
||||
@ -325,7 +335,6 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
||||
if (this._browser)
|
||||
this._browser._contexts.delete(this);
|
||||
this._browserType?._contexts?.delete(this);
|
||||
this._connection.localUtils()._channel.harClearCache({ cacheKey: this._guid }).catch(() => {});
|
||||
this.emit(Events.BrowserContext.Close, this);
|
||||
}
|
||||
|
||||
|
||||
107
packages/playwright-core/src/client/harRouter.ts
Normal file
107
packages/playwright-core/src/client/harRouter.ts
Normal file
@ -0,0 +1,107 @@
|
||||
/**
|
||||
* 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 fs from 'fs';
|
||||
import type { HAREntry, HARFile, HARResponse } from '../../types/types';
|
||||
import type { BrowserContext } from './browserContext';
|
||||
import type { Route } from './network';
|
||||
import type { Page } from './page';
|
||||
|
||||
type HarHandler = {
|
||||
pattern: string | RegExp;
|
||||
handler: (route: Route) => any;
|
||||
};
|
||||
|
||||
export class HarRouter {
|
||||
private _harPathToHandlers: Map<string, HarHandler[]> = new Map();
|
||||
private readonly owner: BrowserContext | Page;
|
||||
|
||||
constructor(owner: BrowserContext | Page) {
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
async routeFromHar(path: string, options?: { strict?: boolean; url?: string|RegExp; }): Promise<void> {
|
||||
const harFile = JSON.parse(await fs.promises.readFile(path, 'utf-8')) as HARFile;
|
||||
const harHandler = {
|
||||
pattern: options?.url ?? /.*/,
|
||||
handler: async (route: Route) => {
|
||||
let response;
|
||||
try {
|
||||
response = harFindResponse(harFile, {
|
||||
url: route.request().url(),
|
||||
method: route.request().method()
|
||||
});
|
||||
} catch (e) {
|
||||
// TODO: throw or at least error log?
|
||||
// rewriteErrorMessage(e, e.message + `\n\nFailed to find matching entry for ${route.request().method()} ${route.request().url()} in ${path}`);
|
||||
// throw e;
|
||||
}
|
||||
if (response)
|
||||
await route.fulfill({ response });
|
||||
else if (options?.strict === false)
|
||||
await route.fallback();
|
||||
else
|
||||
await route.abort();
|
||||
}
|
||||
};
|
||||
let handlers = this._harPathToHandlers.get(path);
|
||||
if (!handlers) {
|
||||
handlers = [];
|
||||
this._harPathToHandlers.set(path, handlers);
|
||||
}
|
||||
handlers.push(harHandler);
|
||||
await this.owner.route(harHandler.pattern, harHandler.handler);
|
||||
}
|
||||
|
||||
async unrouteFromHar(path: string): Promise<void> {
|
||||
const handlers = this._harPathToHandlers.get(path);
|
||||
if (!handlers)
|
||||
return;
|
||||
this._harPathToHandlers.delete(path);
|
||||
await Promise.all(handlers.map(h => this.owner.unroute(h.pattern, h.handler)));
|
||||
}
|
||||
}
|
||||
|
||||
const redirectStatus = [301, 302, 303, 307, 308];
|
||||
|
||||
function harFindResponse(har: HARFile, params: { url: string, method: string }): HARResponse {
|
||||
const harLog = har.log;
|
||||
const visited = new Set<HAREntry>();
|
||||
let url = params.url;
|
||||
let method = params.method;
|
||||
while (true) {
|
||||
const entry = harLog.entries.find(entry => entry.request.url === url && entry.request.method === method);
|
||||
if (!entry)
|
||||
throw new Error(`No entry matching ${params.url}`);
|
||||
if (visited.has(entry))
|
||||
throw new Error(`Found redirect cycle for ${params.url}`);
|
||||
visited.add(entry);
|
||||
|
||||
const locationHeader = entry.response.headers.find(h => h.name.toLowerCase() === 'location');
|
||||
if (redirectStatus.includes(entry.response.status) && locationHeader) {
|
||||
const locationURL = new URL(locationHeader.value, url);
|
||||
url = locationURL.toString();
|
||||
if ((entry.response.status === 301 || entry.response.status === 302) && method === 'POST' ||
|
||||
entry.response.status === 303 && !['GET', 'HEAD'].includes(method)) {
|
||||
// HTTP-redirect fetch step 13 (https://fetch.spec.whatwg.org/#http-redirect-fetch)
|
||||
method = 'GET';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
return entry.response;
|
||||
}
|
||||
}
|
||||
@ -56,11 +56,6 @@ export type SetNetworkCookieParam = {
|
||||
sameSite?: 'Strict' | 'Lax' | 'None'
|
||||
};
|
||||
|
||||
type RouteHAR = {
|
||||
fallback?: 'abort' | 'continue' | 'throw';
|
||||
path: string;
|
||||
};
|
||||
|
||||
type FallbackOverrides = {
|
||||
url?: string;
|
||||
method?: string;
|
||||
@ -293,49 +288,18 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
||||
this._reportHandled(true);
|
||||
}
|
||||
|
||||
async fulfill(options: { response?: api.APIResponse | HARResponse, status?: number, headers?: Headers, contentType?: string, body?: string | Buffer, path?: string, har?: RouteHAR } = {}) {
|
||||
async fulfill(options: { response?: api.APIResponse | HARResponse, status?: number, headers?: Headers, contentType?: string, body?: string | Buffer, path?: string } = {}) {
|
||||
this._checkNotHandled();
|
||||
await this._wrapApiCall(async () => {
|
||||
const fallback = await this._innerFulfill(options);
|
||||
switch (fallback) {
|
||||
case 'abort': await this.abort(); break;
|
||||
case 'continue': await this.continue(); break;
|
||||
case 'done': this._reportHandled(true); break;
|
||||
}
|
||||
await this._innerFulfill(options);
|
||||
this._reportHandled(true);
|
||||
});
|
||||
}
|
||||
|
||||
private async _innerFulfill(options: { response?: api.APIResponse | HARResponse, status?: number, headers?: Headers, contentType?: string, body?: string | Buffer, path?: string, har?: RouteHAR } = {}): Promise<'abort' | 'continue' | 'done'> {
|
||||
private async _innerFulfill(options: { response?: api.APIResponse | HARResponse, status?: number, headers?: Headers, contentType?: string, body?: string | Buffer, path?: string } = {}): Promise<void> {
|
||||
let fetchResponseUid;
|
||||
let { status: statusOption, headers: headersOption, body, contentType } = options;
|
||||
|
||||
if (options.har && options.response)
|
||||
throw new Error(`At most one of "har" and "response" options should be present`);
|
||||
|
||||
if (options.har) {
|
||||
const fallback = options.har.fallback ?? 'abort';
|
||||
if (!['abort', 'continue', 'throw'].includes(fallback))
|
||||
throw new Error(`har.fallback: expected one of "abort", "continue" or "throw", received "${fallback}"`);
|
||||
const entry = await this._connection.localUtils()._channel.harFindEntry({
|
||||
cacheKey: this.request()._context()._guid,
|
||||
harFile: options.har.path,
|
||||
url: this.request().url(),
|
||||
method: this.request().method(),
|
||||
needBody: body === undefined,
|
||||
});
|
||||
if (entry.error) {
|
||||
if (fallback === 'throw')
|
||||
throw new Error(entry.error);
|
||||
return fallback;
|
||||
}
|
||||
if (statusOption === undefined)
|
||||
statusOption = entry.status;
|
||||
if (headersOption === undefined && entry.headers)
|
||||
headersOption = headersArrayToObject(entry.headers, false);
|
||||
if (body === undefined && entry.body !== undefined)
|
||||
body = Buffer.from(entry.body, 'base64');
|
||||
}
|
||||
|
||||
if (options.response instanceof APIResponse) {
|
||||
statusOption ??= options.response.status();
|
||||
headersOption ??= options.response.headers();
|
||||
@ -390,7 +354,6 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
||||
isBase64,
|
||||
fetchResponseUid
|
||||
}));
|
||||
return 'done';
|
||||
}
|
||||
|
||||
async continue(options: FallbackOverrides = {}) {
|
||||
|
||||
@ -15,44 +15,44 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Events } from './events';
|
||||
import { assert } from '../utils';
|
||||
import { TimeoutSettings } from '../common/timeoutSettings';
|
||||
import type { ParsedStackTrace } from '../utils/stackTrace';
|
||||
import type * as channels from '../protocol/channels';
|
||||
import { parseError, serializeError } from '../protocol/serializers';
|
||||
import { Accessibility } from './accessibility';
|
||||
import type { BrowserContext } from './browserContext';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { ConsoleMessage } from './consoleMessage';
|
||||
import { Dialog } from './dialog';
|
||||
import { Download } from './download';
|
||||
import { ElementHandle, determineScreenshotType } from './elementHandle';
|
||||
import type { Locator, FrameLocator, LocatorOptions } from './locator';
|
||||
import { Worker } from './worker';
|
||||
import type { WaitForNavigationOptions } from './frame';
|
||||
import { Frame, verifyLoadState } from './frame';
|
||||
import { Keyboard, Mouse, Touchscreen } from './input';
|
||||
import { assertMaxArguments, serializeArgument, parseResult, JSHandle } from './jsHandle';
|
||||
import type { RouteHandlerCallback } from './network';
|
||||
import { Request, Response, Route, WebSocket, validateHeaders, RouteHandler } from './network';
|
||||
import { FileChooser } from './fileChooser';
|
||||
import { Buffer } from 'buffer';
|
||||
import { Coverage } from './coverage';
|
||||
import { Waiter } from './waiter';
|
||||
import type * as api from '../../types/types';
|
||||
import type * as structs from '../../types/structs';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import type { Size, URLMatch, Headers, LifecycleEvent, WaitForEventOptions, SelectOption, SelectOptionOptions, FilePayload, WaitForFunctionOptions } from './types';
|
||||
import { evaluationScript } from './clientHelper';
|
||||
import { isString, isRegExp, isObject, headersObjectToArray } from '../utils';
|
||||
import { mkdirIfNeeded } from '../utils/fileUtils';
|
||||
import type * as structs from '../../types/structs';
|
||||
import type * as api from '../../types/types';
|
||||
import { isSafeCloseError } from '../common/errors';
|
||||
import { Video } from './video';
|
||||
import { Artifact } from './artifact';
|
||||
import type { APIRequestContext } from './fetch';
|
||||
import { urlMatches } from '../common/netUtils';
|
||||
import { TimeoutSettings } from '../common/timeoutSettings';
|
||||
import type * as channels from '../protocol/channels';
|
||||
import { parseError, serializeError } from '../protocol/serializers';
|
||||
import { assert, headersObjectToArray, isObject, isRegExp, isString } from '../utils';
|
||||
import { mkdirIfNeeded } from '../utils/fileUtils';
|
||||
import type { ParsedStackTrace } from '../utils/stackTrace';
|
||||
import { Accessibility } from './accessibility';
|
||||
import { Artifact } from './artifact';
|
||||
import type { BrowserContext } from './browserContext';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { evaluationScript } from './clientHelper';
|
||||
import { ConsoleMessage } from './consoleMessage';
|
||||
import { Coverage } from './coverage';
|
||||
import { Dialog } from './dialog';
|
||||
import { Download } from './download';
|
||||
import { determineScreenshotType, ElementHandle } from './elementHandle';
|
||||
import { Events } from './events';
|
||||
import type { APIRequestContext } from './fetch';
|
||||
import { FileChooser } from './fileChooser';
|
||||
import type { WaitForNavigationOptions } from './frame';
|
||||
import { Frame, verifyLoadState } from './frame';
|
||||
import { HarRouter } from './harRouter';
|
||||
import { Keyboard, Mouse, Touchscreen } from './input';
|
||||
import { assertMaxArguments, JSHandle, parseResult, serializeArgument } from './jsHandle';
|
||||
import type { FrameLocator, Locator, LocatorOptions } from './locator';
|
||||
import type { RouteHandlerCallback } from './network';
|
||||
import { Request, Response, Route, RouteHandler, validateHeaders, WebSocket } from './network';
|
||||
import type { FilePayload, Headers, LifecycleEvent, SelectOption, SelectOptionOptions, Size, URLMatch, WaitForEventOptions, WaitForFunctionOptions } from './types';
|
||||
import { Video } from './video';
|
||||
import { Waiter } from './waiter';
|
||||
import { Worker } from './worker';
|
||||
|
||||
type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> & {
|
||||
width?: string | number,
|
||||
@ -74,6 +74,7 @@ type ExpectScreenshotOptions = Omit<channels.PageExpectScreenshotOptions, 'scree
|
||||
screenshotOptions: Omit<channels.PageExpectScreenshotOptions['screenshotOptions'], 'mask'> & { mask?: Locator[] }
|
||||
};
|
||||
|
||||
|
||||
export class Page extends ChannelOwner<channels.PageChannel> implements api.Page {
|
||||
private _browserContext: BrowserContext;
|
||||
_ownedContext: BrowserContext | undefined;
|
||||
@ -97,6 +98,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
||||
readonly _timeoutSettings: TimeoutSettings;
|
||||
private _video: Video | null = null;
|
||||
readonly _opener: Page | null;
|
||||
private readonly _harRouter = new HarRouter(this);
|
||||
|
||||
static from(page: channels.PageChannel): Page {
|
||||
return (page as any)._object;
|
||||
@ -473,6 +475,14 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
||||
await this._disableInterception();
|
||||
}
|
||||
|
||||
async routeFromHar(harPath: string, options?: { strict?: boolean; url?: string|RegExp; }): Promise<void> {
|
||||
await this._harRouter.routeFromHar(harPath, options);
|
||||
}
|
||||
|
||||
async unrouteFromHar(harPath: string): Promise<void> {
|
||||
await this._harRouter.unrouteFromHar(harPath);
|
||||
}
|
||||
|
||||
async _unrouteAll() {
|
||||
this._routes = [];
|
||||
await this._disableInterception();
|
||||
|
||||
@ -378,8 +378,6 @@ export interface LocalUtilsEventTarget {
|
||||
export interface LocalUtilsChannel extends LocalUtilsEventTarget, Channel {
|
||||
_type_LocalUtils: boolean;
|
||||
zip(params: LocalUtilsZipParams, metadata?: Metadata): Promise<LocalUtilsZipResult>;
|
||||
harFindEntry(params: LocalUtilsHarFindEntryParams, metadata?: Metadata): Promise<LocalUtilsHarFindEntryResult>;
|
||||
harClearCache(params: LocalUtilsHarClearCacheParams, metadata?: Metadata): Promise<LocalUtilsHarClearCacheResult>;
|
||||
}
|
||||
export type LocalUtilsZipParams = {
|
||||
zipFile: string,
|
||||
@ -389,29 +387,6 @@ export type LocalUtilsZipOptions = {
|
||||
|
||||
};
|
||||
export type LocalUtilsZipResult = void;
|
||||
export type LocalUtilsHarFindEntryParams = {
|
||||
cacheKey: string,
|
||||
harFile: string,
|
||||
url: string,
|
||||
method: string,
|
||||
needBody: boolean,
|
||||
};
|
||||
export type LocalUtilsHarFindEntryOptions = {
|
||||
|
||||
};
|
||||
export type LocalUtilsHarFindEntryResult = {
|
||||
error?: string,
|
||||
status?: number,
|
||||
headers?: NameValue[],
|
||||
body?: Binary,
|
||||
};
|
||||
export type LocalUtilsHarClearCacheParams = {
|
||||
cacheKey: string,
|
||||
};
|
||||
export type LocalUtilsHarClearCacheOptions = {
|
||||
|
||||
};
|
||||
export type LocalUtilsHarClearCacheResult = void;
|
||||
|
||||
export interface LocalUtilsEvents {
|
||||
}
|
||||
|
||||
@ -473,27 +473,6 @@ LocalUtils:
|
||||
type: array
|
||||
items: NameValue
|
||||
|
||||
harFindEntry:
|
||||
parameters:
|
||||
# HAR file is cached until clearHarCache is called
|
||||
cacheKey: string
|
||||
harFile: string
|
||||
url: string
|
||||
method: string
|
||||
needBody: boolean
|
||||
returns:
|
||||
error: string?
|
||||
status: number?
|
||||
headers:
|
||||
type: array?
|
||||
items: NameValue
|
||||
body: binary?
|
||||
|
||||
harClearCache:
|
||||
parameters:
|
||||
cacheKey: string
|
||||
|
||||
|
||||
Root:
|
||||
type: interface
|
||||
|
||||
|
||||
@ -205,16 +205,6 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||
zipFile: tString,
|
||||
entries: tArray(tType('NameValue')),
|
||||
});
|
||||
scheme.LocalUtilsHarFindEntryParams = tObject({
|
||||
cacheKey: tString,
|
||||
harFile: tString,
|
||||
url: tString,
|
||||
method: tString,
|
||||
needBody: tBoolean,
|
||||
});
|
||||
scheme.LocalUtilsHarClearCacheParams = tObject({
|
||||
cacheKey: tString,
|
||||
});
|
||||
scheme.RootInitializeParams = tObject({
|
||||
sdkLanguage: tString,
|
||||
});
|
||||
|
||||
@ -23,11 +23,9 @@ import { assert, createGuid } from '../../utils';
|
||||
import type { DispatcherScope } from './dispatcher';
|
||||
import { Dispatcher } from './dispatcher';
|
||||
import { yazl, yauzl } from '../../zipBundle';
|
||||
import type { Log, Entry } from '../har/har';
|
||||
|
||||
export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.LocalUtilsChannel> implements channels.LocalUtilsChannel {
|
||||
_type_LocalUtils: boolean;
|
||||
private _harCache = new Map<string, Map<string, Log>>();
|
||||
|
||||
constructor(scope: DispatcherScope) {
|
||||
super(scope, { guid: 'localUtils@' + createGuid() }, 'LocalUtils', {});
|
||||
@ -88,62 +86,4 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
async harFindEntry(params: channels.LocalUtilsHarFindEntryParams, metadata?: channels.Metadata): Promise<channels.LocalUtilsHarFindEntryResult> {
|
||||
try {
|
||||
let cache = this._harCache.get(params.cacheKey);
|
||||
if (!cache) {
|
||||
cache = new Map();
|
||||
this._harCache.set(params.cacheKey, cache);
|
||||
}
|
||||
|
||||
let harLog = cache.get(params.harFile);
|
||||
if (!harLog) {
|
||||
const contents = await fs.promises.readFile(params.harFile, 'utf-8');
|
||||
harLog = JSON.parse(contents).log as Log;
|
||||
cache.set(params.harFile, harLog);
|
||||
}
|
||||
|
||||
const visited = new Set<Entry>();
|
||||
let url = params.url;
|
||||
let method = params.method;
|
||||
while (true) {
|
||||
const entry = harLog.entries.find(entry => entry.request.url === url && entry.request.method === method);
|
||||
if (!entry)
|
||||
throw new Error(`No entry matching ${params.url}`);
|
||||
if (visited.has(entry))
|
||||
throw new Error(`Found redirect cycle for ${params.url}`);
|
||||
visited.add(entry);
|
||||
|
||||
const locationHeader = entry.response.headers.find(h => h.name.toLowerCase() === 'location');
|
||||
if (redirectStatus.includes(entry.response.status) && locationHeader) {
|
||||
const locationURL = new URL(locationHeader.value, url);
|
||||
url = locationURL.toString();
|
||||
if ((entry.response.status === 301 || entry.response.status === 302) && method === 'POST' ||
|
||||
entry.response.status === 303 && !['GET', 'HEAD'].includes(method)) {
|
||||
// HTTP-redirect fetch step 13 (https://fetch.spec.whatwg.org/#http-redirect-fetch)
|
||||
method = 'GET';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let base64body: string | undefined;
|
||||
if (params.needBody && entry.response.content && entry.response.content.text !== undefined) {
|
||||
if (entry.response.content.encoding === 'base64')
|
||||
base64body = entry.response.content.text;
|
||||
else
|
||||
base64body = Buffer.from(entry.response.content.text, 'utf8').toString('base64');
|
||||
}
|
||||
return { status: entry.response.status, headers: entry.response.headers, body: base64body };
|
||||
}
|
||||
} catch (e) {
|
||||
return { error: `Error reading HAR file ${params.harFile}: ` + e.message };
|
||||
}
|
||||
}
|
||||
|
||||
async harClearCache(params: channels.LocalUtilsHarClearCacheParams, metadata?: channels.Metadata): Promise<void> {
|
||||
this._harCache.delete(params.cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
const redirectStatus = [301, 302, 303, 307, 308];
|
||||
|
||||
92
packages/playwright-core/types/types.d.ts
vendored
92
packages/playwright-core/types/types.d.ts
vendored
@ -2968,6 +2968,34 @@ export interface Page {
|
||||
times?: number;
|
||||
}): Promise<void>;
|
||||
|
||||
/**
|
||||
* Provides the capability to serve network requests that are made by a page from prerecorded HAR file.
|
||||
*
|
||||
* > NOTE: [page.routeFromHar(harPath[, options])](https://playwright.dev/docs/api/class-page#page-route-from-har) will not
|
||||
* intercept requests intercepted by Service Worker. See [this](https://github.com/microsoft/playwright/issues/1090) issue.
|
||||
* We recommend disabling Service Workers when using request interception. Via `await context.addInitScript(() => delete
|
||||
* window.navigator.serviceWorker);`
|
||||
* @param harPath Path to the HAR file with prerecorded network data. If HAR file contains an entry with the matching url and HTTP method, then the entry's headers, status and body will be used to fulfill. An entry resulting in a redirect will be followed
|
||||
* automatically. If there is no matching entry in the file the execution continues to try other configured HAR files and
|
||||
* [Route] handlers. If `path` is a relative path, then it is resolved relative to the current working directory.
|
||||
* @param options
|
||||
*/
|
||||
routeFromHar(harPath: string, options?: {
|
||||
/**
|
||||
* If set to true any request not found in the HAR file will be aborted. If set to false missing requests will continue
|
||||
* normal flow and can be handled by other [Route] handlers or served from other HAR files configured with
|
||||
* [page.routeFromHar(harPath[, options])](https://playwright.dev/docs/api/class-page#page-route-from-har). Defaults to
|
||||
* false.
|
||||
*/
|
||||
strict?: boolean;
|
||||
|
||||
/**
|
||||
* A glob pattern or regular expression to match request URL while routing. Only requests with URL matching the pattern
|
||||
* will be surved from the HAR file. If not specified, all requests are served from the HAR file.
|
||||
*/
|
||||
url?: string|RegExp;
|
||||
}): Promise<void>;
|
||||
|
||||
/**
|
||||
* Returns the buffer with the captured screenshot.
|
||||
* @param options
|
||||
@ -3513,6 +3541,13 @@ export interface Page {
|
||||
*/
|
||||
unroute(url: string|RegExp|((url: URL) => boolean), handler?: ((route: Route, request: Request) => void)): Promise<void>;
|
||||
|
||||
/**
|
||||
* Removes HAR handler previously added with
|
||||
* [page.routeFromHar(harPath[, options])](https://playwright.dev/docs/api/class-page#page-route-from-har).
|
||||
* @param harPath Path to the HAR file which was passed to [page.routeFromHar(harPath[, options])](https://playwright.dev/docs/api/class-page#page-route-from-har).
|
||||
*/
|
||||
unrouteFromHar(harPath: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Shortcut for main frame's [frame.url()](https://playwright.dev/docs/api/class-frame#frame-url).
|
||||
*/
|
||||
@ -6806,6 +6841,35 @@ export interface BrowserContext {
|
||||
times?: number;
|
||||
}): Promise<void>;
|
||||
|
||||
/**
|
||||
* Provides the capability to serve network requests that are made in the context from prerecorded HAR file.
|
||||
*
|
||||
* > NOTE:
|
||||
* [browserContext.routeFromHar(harPath[, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-route-from-har)
|
||||
* will not intercept requests intercepted by Service Worker. See
|
||||
* [this](https://github.com/microsoft/playwright/issues/1090) issue. We recommend disabling Service Workers when using
|
||||
* request interception. Via `await context.addInitScript(() => delete window.navigator.serviceWorker);`
|
||||
* @param harPath Path to the HAR file with prerecorded network data. If HAR file contains an entry with the matching url and HTTP method, then the entry's headers, status and body will be used to fulfill. An entry resulting in a redirect will be followed
|
||||
* automatically. If there is no matching entry in the file the execution continues to try other configured HAR files and
|
||||
* [Route] handlers. If `path` is a relative path, then it is resolved relative to the current working directory.
|
||||
* @param options
|
||||
*/
|
||||
routeFromHar(harPath: string, options?: {
|
||||
/**
|
||||
* If set to true any request not found in the HAR file will be aborted. If set to false missing requests will continue
|
||||
* normal flow and can be handled by other [Route] handlers or served from other HAR files configured with
|
||||
* [browserContext.routeFromHar(harPath[, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-route-from-har).
|
||||
* Defaults to false.
|
||||
*/
|
||||
strict?: boolean;
|
||||
|
||||
/**
|
||||
* A glob pattern or regular expression to match request URL while routing. Only requests with URL matching the pattern
|
||||
* will be surved from the HAR file. If not specified, all requests are served from the HAR file.
|
||||
*/
|
||||
url?: string|RegExp;
|
||||
}): Promise<void>;
|
||||
|
||||
/**
|
||||
* > NOTE: Service workers are only supported on Chromium-based browsers.
|
||||
*
|
||||
@ -6956,6 +7020,13 @@ export interface BrowserContext {
|
||||
*/
|
||||
unroute(url: string|RegExp|((url: URL) => boolean), handler?: ((route: Route, request: Request) => void)): Promise<void>;
|
||||
|
||||
/**
|
||||
* Removes HAR handler previously added with
|
||||
* [browserContext.routeFromHar(harPath[, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-route-from-har).
|
||||
* @param harPath Path to the HAR file which was passed to [browserContext.routeFromHar(harPath[, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-route-from-har).
|
||||
*/
|
||||
unrouteFromHar(harPath: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* > NOTE: Only works with Chromium browser's persistent context.
|
||||
*
|
||||
@ -14950,27 +15021,6 @@ export interface Route {
|
||||
*/
|
||||
contentType?: string;
|
||||
|
||||
/**
|
||||
* HAR file to extract the response from. If HAR file contains an entry with the matching url and HTTP method, then the
|
||||
* entry's headers, status and body will be used to fulfill. An entry resulting in a redirect will be followed
|
||||
* automatically. Individual fields such as headers can be overridden using fulfill options. If `path` is a relative path,
|
||||
* then it is resolved relative to the current working directory.
|
||||
*/
|
||||
har?: {
|
||||
/**
|
||||
* Path to the HAR file.
|
||||
*/
|
||||
path: string;
|
||||
|
||||
/**
|
||||
* Behavior in the case where matching entry was not found in the HAR. Either
|
||||
* [route.abort([errorCode])](https://playwright.dev/docs/api/class-route#route-abort) the request,
|
||||
* [route.continue([options])](https://playwright.dev/docs/api/class-route#route-continue) it, or throw an error. Defaults
|
||||
* to "abort".
|
||||
*/
|
||||
fallback?: "abort"|"continue"|"throw";
|
||||
};
|
||||
|
||||
/**
|
||||
* Response headers. Header values will be converted to a string.
|
||||
*/
|
||||
|
||||
134
tests/library/browsercontext-route-from-har.spec.ts
Normal file
134
tests/library/browsercontext-route-from-har.spec.ts
Normal file
@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Copyright Microsoft Corporation. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { browserTest as it, expect } from '../config/browserTest';
|
||||
import fs from 'fs';
|
||||
|
||||
it('routeFromHar should fulfill from har, matching the method and following redirects', async ({ context, page, isAndroid, asset }) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = asset('har-fulfill.har');
|
||||
await context.routeFromHar(harPath);
|
||||
await page.goto('http://no.playwright/');
|
||||
// HAR contains a redirect for the script that should be followed automatically.
|
||||
expect(await page.evaluate('window.value')).toBe('foo');
|
||||
// HAR contains a POST for the css file that should not be used.
|
||||
await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(255, 0, 0)');
|
||||
});
|
||||
|
||||
it('routeFromHar strict:false should fallback when not found in har', async ({ context, page, server, isAndroid, asset }) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = asset('har-fulfill.har');
|
||||
let requestCount = 0;
|
||||
await context.route('**/*', route => {
|
||||
++requestCount;
|
||||
route.continue();
|
||||
});
|
||||
await context.routeFromHar(harPath, { strict: false });
|
||||
await page.goto(server.PREFIX + '/one-style.html');
|
||||
await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(255, 192, 203)');
|
||||
expect(requestCount).toBe(2);
|
||||
});
|
||||
|
||||
it('routeFromHar by default should abort requests not found in har', async ({ context, page, server, isAndroid, asset }) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = asset('har-fulfill.har');
|
||||
let requestCount = 0;
|
||||
await context.route('**/*', route => {
|
||||
++requestCount;
|
||||
route.continue();
|
||||
});
|
||||
await context.routeFromHar(harPath);
|
||||
const error = await page.goto(server.EMPTY_PAGE).catch(e => e);
|
||||
expect(error instanceof Error).toBe(true);
|
||||
expect(requestCount).toBe(0);
|
||||
});
|
||||
|
||||
it('routeFromHar strict:false should continue requests on bad har', async ({ context, page, server, isAndroid }, testInfo) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = testInfo.outputPath('test.har');
|
||||
fs.writeFileSync(harPath, JSON.stringify({ log: {} }), 'utf-8');
|
||||
let requestCount = 0;
|
||||
await context.route('**/*', route => {
|
||||
++requestCount;
|
||||
route.continue();
|
||||
});
|
||||
await context.routeFromHar(harPath, { strict: false });
|
||||
await page.goto(server.PREFIX + '/one-style.html');
|
||||
expect(requestCount).toBe(2);
|
||||
});
|
||||
|
||||
it('routeFromHar should only handle requests matching url filter', async ({ context, page, isAndroid, asset }) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = asset('har-fulfill.har');
|
||||
let fulfillCount = 0;
|
||||
let passthroughCount = 0;
|
||||
await context.route('**/*', async route => {
|
||||
++fulfillCount;
|
||||
expect(route.request().url()).toBe('http://no.playwright/');
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'text/html',
|
||||
body: '<script src="./script.js"></script><div>hello</div>',
|
||||
});
|
||||
});
|
||||
await context.routeFromHar(harPath, { url: '**/*.js' });
|
||||
await context.route('**/*', route => {
|
||||
++passthroughCount;
|
||||
route.fallback();
|
||||
});
|
||||
await page.goto('http://no.playwright/');
|
||||
// HAR contains a redirect for the script that should be followed automatically.
|
||||
expect(await page.evaluate('window.value')).toBe('foo');
|
||||
expect(fulfillCount).toBe(1);
|
||||
expect(passthroughCount).toBe(2);
|
||||
});
|
||||
|
||||
it('routeFromHar should support mutliple calls with same path', async ({ context, page, isAndroid, asset }) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = asset('har-fulfill.har');
|
||||
let abortCount = 0;
|
||||
await context.route('**/*', async route => {
|
||||
++abortCount;
|
||||
await route.abort();
|
||||
});
|
||||
await context.routeFromHar(harPath, { url: '**/*.js' });
|
||||
await context.routeFromHar(harPath, { url: '**/*.css' });
|
||||
await context.routeFromHar(harPath, { url: /.*no.playwright\/$/ });
|
||||
await page.goto('http://no.playwright/');
|
||||
expect(await page.evaluate('window.value')).toBe('foo');
|
||||
expect(abortCount).toBe(0);
|
||||
});
|
||||
|
||||
it('unrouteFromHar should remove har handler added with routeFromHar', async ({ context, page, server, isAndroid, asset }) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = asset('har-fulfill.har');
|
||||
let requestCount = 0;
|
||||
await context.route('**/*', route => {
|
||||
++requestCount;
|
||||
route.continue();
|
||||
});
|
||||
await context.routeFromHar(harPath, { strict: true });
|
||||
await context.unrouteFromHar(harPath);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
expect(requestCount).toBe(1);
|
||||
});
|
||||
@ -79,7 +79,7 @@ it('should fall back after exception', async ({ page, server }) => {
|
||||
});
|
||||
await page.route('**/empty.html', async route => {
|
||||
try {
|
||||
await route.fulfill({ har: { path: 'file' }, response: {} as any });
|
||||
await route.fulfill({ response: {} as any });
|
||||
} catch (e) {
|
||||
route.fallback();
|
||||
}
|
||||
|
||||
@ -323,103 +323,6 @@ it('headerValue should return set-cookie from intercepted response', async ({ pa
|
||||
expect(await response.headerValue('Set-Cookie')).toBe('a=b');
|
||||
});
|
||||
|
||||
it('should complain about har + response options', async ({ page, server, isAndroid, isElectron }) => {
|
||||
it.fixme(isElectron, 'error: Browser context management is not supported.');
|
||||
it.fixme(isAndroid);
|
||||
|
||||
let error;
|
||||
await page.route('**/*.css', async route => {
|
||||
const response = await page.request.fetch(route.request());
|
||||
error = await route.fulfill({ har: { path: 'har' }, response }).catch(e => e);
|
||||
await route.continue();
|
||||
});
|
||||
await page.goto(server.PREFIX + '/one-style.html');
|
||||
expect(error.message).toBe(`route.fulfill: At most one of "har" and "response" options should be present`);
|
||||
});
|
||||
|
||||
it('should complain about bad har.fallback', async ({ page, server, isAndroid }) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
let error;
|
||||
await page.route('**/*.css', async route => {
|
||||
error = await route.fulfill({ har: { path: 'har', fallback: 'foo' } as any }).catch(e => e);
|
||||
await route.continue();
|
||||
});
|
||||
await page.goto(server.PREFIX + '/one-style.html');
|
||||
expect(error.message).toBe(`route.fulfill: har.fallback: expected one of "abort", "continue" or "throw", received "foo"`);
|
||||
});
|
||||
|
||||
it('should fulfill from har, matching the method and following redirects', async ({ page, isAndroid, asset }) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = asset('har-fulfill.har');
|
||||
await page.route('**/*', route => route.fulfill({ har: { path: harPath } }));
|
||||
await page.goto('http://no.playwright/');
|
||||
// HAR contains a redirect for the script that should be followed automatically.
|
||||
expect(await page.evaluate('window.value')).toBe('foo');
|
||||
// HAR contains a POST for the css file that should not be used.
|
||||
await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(255, 0, 0)');
|
||||
});
|
||||
|
||||
it('should fallback to abort when not found in har', async ({ page, server, isAndroid, asset }) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = asset('har-fulfill.har');
|
||||
await page.route('**/*', route => route.fulfill({ har: { path: harPath } }));
|
||||
const error = await page.goto(server.EMPTY_PAGE).catch(e => e);
|
||||
expect(error instanceof Error).toBe(true);
|
||||
});
|
||||
|
||||
it('should support fallback:continue when not found in har', async ({ page, server, isAndroid, asset }) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = asset('har-fulfill.har');
|
||||
await page.route('**/*', route => route.fulfill({ har: { path: harPath, fallback: 'continue' } }));
|
||||
await page.goto(server.PREFIX + '/one-style.html');
|
||||
await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(255, 192, 203)');
|
||||
});
|
||||
|
||||
it('should support fallback:throw when not found in har', async ({ page, server, isAndroid, asset }) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = asset('har-fulfill.har');
|
||||
let error;
|
||||
await page.route('**/*.css', async route => {
|
||||
error = await route.fulfill({ har: { path: harPath, fallback: 'throw' } }).catch(e => e);
|
||||
await route.continue();
|
||||
});
|
||||
await page.goto(server.PREFIX + '/one-style.html');
|
||||
expect(error.message).toBe(`route.fulfill: Error reading HAR file ${harPath}: No entry matching ${server.PREFIX + '/one-style.css'}`);
|
||||
});
|
||||
|
||||
it('should complain about bad har with fallback:throw', async ({ page, server, isAndroid }, testInfo) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = testInfo.outputPath('test.har');
|
||||
fs.writeFileSync(harPath, JSON.stringify({ log: {} }), 'utf-8');
|
||||
let error;
|
||||
await page.route('**/*.css', async route => {
|
||||
error = await route.fulfill({ har: { path: harPath, fallback: 'throw' } }).catch(e => e);
|
||||
await route.continue();
|
||||
});
|
||||
await page.goto(server.PREFIX + '/one-style.html');
|
||||
expect(error.message).toContain(`route.fulfill: Error reading HAR file ${harPath}:`);
|
||||
});
|
||||
|
||||
it('should override status when fulfilling from har', async ({ page, isAndroid, asset }) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = asset('har-fulfill.har');
|
||||
await page.route('**/*', async route => {
|
||||
await route.fulfill({ har: { path: harPath }, status: route.request().url().endsWith('.css') ? 404 : undefined });
|
||||
});
|
||||
await page.goto('http://no.playwright/');
|
||||
// Script should work.
|
||||
expect(await page.evaluate('window.value')).toBe('foo');
|
||||
// 404 should fail the CSS and styles should not apply.
|
||||
await expect(page.locator('body')).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
|
||||
});
|
||||
|
||||
it('should fulfill with har response', async ({ page, isAndroid, asset }) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
@ -462,3 +365,120 @@ function findResponse(har: HARFile, url: string) {
|
||||
expect(entry, originalUrl).toBeTruthy();
|
||||
return entry?.response;
|
||||
}
|
||||
|
||||
it('routeFromHar should fulfill from har, matching the method and following redirects', async ({ page, isAndroid, asset }) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = asset('har-fulfill.har');
|
||||
await page.routeFromHar(harPath);
|
||||
await page.goto('http://no.playwright/');
|
||||
// HAR contains a redirect for the script that should be followed automatically.
|
||||
expect(await page.evaluate('window.value')).toBe('foo');
|
||||
// HAR contains a POST for the css file that should not be used.
|
||||
await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(255, 0, 0)');
|
||||
});
|
||||
|
||||
it('routeFromHar strict:false should fallback when not found in har', async ({ page, server, isAndroid, asset }) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = asset('har-fulfill.har');
|
||||
let requestCount = 0;
|
||||
await page.route('**/*', route => {
|
||||
++requestCount;
|
||||
route.continue();
|
||||
});
|
||||
await page.routeFromHar(harPath, { strict: false });
|
||||
await page.goto(server.PREFIX + '/one-style.html');
|
||||
await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(255, 192, 203)');
|
||||
expect(requestCount).toBe(2);
|
||||
});
|
||||
|
||||
it('routeFromHar by default should abort requests not found in har', async ({ page, server, isAndroid, asset }) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = asset('har-fulfill.har');
|
||||
let requestCount = 0;
|
||||
await page.route('**/*', route => {
|
||||
++requestCount;
|
||||
route.continue();
|
||||
});
|
||||
await page.routeFromHar(harPath);
|
||||
const error = await page.goto(server.EMPTY_PAGE).catch(e => e);
|
||||
expect(error instanceof Error).toBe(true);
|
||||
expect(requestCount).toBe(0);
|
||||
});
|
||||
|
||||
it('routeFromHar strict:false should continue requests on bad har', async ({ page, server, isAndroid }, testInfo) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = testInfo.outputPath('test.har');
|
||||
fs.writeFileSync(harPath, JSON.stringify({ log: {} }), 'utf-8');
|
||||
let requestCount = 0;
|
||||
await page.route('**/*', route => {
|
||||
++requestCount;
|
||||
route.continue();
|
||||
});
|
||||
await page.routeFromHar(harPath, { strict: false });
|
||||
await page.goto(server.PREFIX + '/one-style.html');
|
||||
expect(requestCount).toBe(2);
|
||||
});
|
||||
|
||||
it('routeFromHar should only handle requests matching url filter', async ({ page, server, isAndroid, asset }) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = asset('har-fulfill.har');
|
||||
let fulfillCount = 0;
|
||||
let passthroughCount = 0;
|
||||
await page.route('**/*', async route => {
|
||||
++fulfillCount;
|
||||
expect(route.request().url()).toBe('http://no.playwright/');
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'text/html',
|
||||
body: '<script src="./script.js"></script><div>hello</div>',
|
||||
});
|
||||
});
|
||||
await page.routeFromHar(harPath, { url: '**/*.js' });
|
||||
await page.route('**/*', route => {
|
||||
++passthroughCount;
|
||||
route.fallback();
|
||||
});
|
||||
await page.goto('http://no.playwright/');
|
||||
// HAR contains a redirect for the script that should be followed automatically.
|
||||
expect(await page.evaluate('window.value')).toBe('foo');
|
||||
expect(fulfillCount).toBe(1);
|
||||
expect(passthroughCount).toBe(2);
|
||||
});
|
||||
|
||||
it('routeFromHar should support mutliple calls with same path', async ({ page, server, isAndroid, asset }) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = asset('har-fulfill.har');
|
||||
let abortCount = 0;
|
||||
await page.route('**/*', async route => {
|
||||
++abortCount;
|
||||
await route.abort();
|
||||
});
|
||||
await page.routeFromHar(harPath, { url: '**/*.js' });
|
||||
await page.routeFromHar(harPath, { url: '**/*.css' });
|
||||
await page.routeFromHar(harPath, { url: /.*no.playwright\/$/ });
|
||||
await page.goto('http://no.playwright/');
|
||||
expect(await page.evaluate('window.value')).toBe('foo');
|
||||
expect(abortCount).toBe(0);
|
||||
});
|
||||
|
||||
it('unrouteFromHar should remove har handler added with routeFromHar', async ({ page, server, isAndroid, asset }) => {
|
||||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = asset('har-fulfill.har');
|
||||
let requestCount = 0;
|
||||
await page.route('**/*', route => {
|
||||
++requestCount;
|
||||
route.continue();
|
||||
});
|
||||
await page.routeFromHar(harPath, { strict: true });
|
||||
await page.unrouteFromHar(harPath);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
expect(requestCount).toBe(1);
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user