diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 966be27211..3aa5d9bc94 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -562,6 +562,7 @@ Logger sink for Playwright logging. - `omitContent` ?<[boolean]> Optional setting to control whether to omit request content from the HAR. Defaults to `false`. - `path` <[path]> Path on the filesystem to write the HAR file to. + - `urlFilter` ?<[string]|[RegExp]> A glob or regex pattern to filter requests that are stored in the HAR. When a [`option: baseURL`] via the context options was provided and the passed URL is a path, it gets merged via the [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. Enables [HAR](http://www.softwareishard.com/blog/har-12-spec) recording for all pages into `recordHar.path` file. If not specified, the HAR is not recorded. Make sure to await [`method: BrowserContext.close`] for the HAR to be diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 5cad0e77e1..275f9a0304 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -28,7 +28,7 @@ import { Events } from './events'; import { TimeoutSettings } from '../common/timeoutSettings'; import { Waiter } from './waiter'; import type { URLMatch, Headers, WaitForEventOptions, BrowserContextOptions, StorageState, LaunchOptions } from './types'; -import { headersObjectToArray } from '../utils'; +import { headersObjectToArray, isRegExp, isString } from '../utils'; import { mkdirIfNeeded } from '../utils/fileUtils'; import { isSafeCloseError } from '../common/errors'; import type * as api from '../../types/types'; @@ -390,6 +390,18 @@ async function prepareStorageState(options: BrowserContextOptions): Promise { if (options.videoSize && !options.videosPath) throw new Error(`"videoSize" option requires "videosPath" to be specified`); @@ -401,6 +413,7 @@ export async function prepareBrowserContextParams(options: BrowserContextOptions noDefaultViewport: options.viewport === null, extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined, storageState: await prepareStorageState(options), + recordHar: prepareRecordHarOptions(options.recordHar), }; if (!contextParams.recordVideo && options.videosPath) { contextParams.recordVideo = { diff --git a/packages/playwright-core/src/client/clientHelper.ts b/packages/playwright-core/src/client/clientHelper.ts index 3b28794b81..f2a617ea09 100644 --- a/packages/playwright-core/src/client/clientHelper.ts +++ b/packages/playwright-core/src/client/clientHelper.ts @@ -17,7 +17,7 @@ import type * as types from './types'; import fs from 'fs'; -import { isString, isRegExp, constructURLBasedOnBaseURL } from '../utils'; +import { isString } from '../utils'; export function envObjectToArray(env: types.Env): { name: string, value: string }[] { const result: { name: string, value: string }[] = []; @@ -48,90 +48,3 @@ export async function evaluationScript(fun: Function | string | { path?: string, } throw new Error('Either path or content property must be present'); } - -export function parsedURL(url: string): URL | null { - try { - return new URL(url); - } catch (e) { - return null; - } -} - -export function urlMatches(baseURL: string | undefined, urlString: string, match: types.URLMatch | undefined): boolean { - if (match === undefined || match === '') - return true; - if (isString(match) && !match.startsWith('*')) - match = constructURLBasedOnBaseURL(baseURL, match); - if (isString(match)) - match = globToRegex(match); - if (isRegExp(match)) - return match.test(urlString); - if (typeof match === 'string' && match === urlString) - return true; - const url = parsedURL(urlString); - if (!url) - return false; - if (typeof match === 'string') - return url.pathname === match; - if (typeof match !== 'function') - throw new Error('url parameter should be string, RegExp or function'); - return match(url); -} - -const escapeGlobChars = new Set(['/', '$', '^', '+', '.', '(', ')', '=', '!', '|']); - -export function globToRegex(glob: string): RegExp { - const tokens = ['^']; - let inGroup; - for (let i = 0; i < glob.length; ++i) { - const c = glob[i]; - if (escapeGlobChars.has(c)) { - tokens.push('\\' + c); - continue; - } - if (c === '*') { - const beforeDeep = glob[i - 1]; - let starCount = 1; - while (glob[i + 1] === '*') { - starCount++; - i++; - } - const afterDeep = glob[i + 1]; - const isDeep = starCount > 1 && - (beforeDeep === '/' || beforeDeep === undefined) && - (afterDeep === '/' || afterDeep === undefined); - if (isDeep) { - tokens.push('((?:[^/]*(?:\/|$))*)'); - i++; - } else { - tokens.push('([^/]*)'); - } - continue; - } - - switch (c) { - case '?': - tokens.push('.'); - break; - case '{': - inGroup = true; - tokens.push('('); - break; - case '}': - inGroup = false; - tokens.push(')'); - break; - case ',': - if (inGroup) { - tokens.push('|'); - break; - } - tokens.push('\\' + c); - break; - default: - tokens.push(c); - } - } - tokens.push('$'); - return new RegExp(tokens.join('')); -} diff --git a/packages/playwright-core/src/client/frame.ts b/packages/playwright-core/src/client/frame.ts index 2745325fbb..bde414d258 100644 --- a/packages/playwright-core/src/client/frame.ts +++ b/packages/playwright-core/src/client/frame.ts @@ -29,7 +29,7 @@ import { Waiter } from './waiter'; import { Events } from './events'; import type { LifecycleEvent, URLMatch, SelectOption, SelectOptionOptions, FilePayload, WaitForFunctionOptions, StrictOptions } from './types'; import { kLifecycleEvents } from './types'; -import { urlMatches } from './clientHelper'; +import { urlMatches } from '../common/netUtils'; import type * as api from '../../types/types'; import type * as structs from '../../types/structs'; import { debugLogger } from '../common/debugLogger'; diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts index c107210a0b..429efd1e4a 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -28,7 +28,7 @@ import type { Page } from './page'; import { Waiter } from './waiter'; import type * as api from '../../types/types'; import type { HeadersArray, URLMatch } from '../common/types'; -import { urlMatches } from './clientHelper'; +import { urlMatches } from '../common/netUtils'; import { MultiMap } from '../utils/multimap'; import { APIResponse } from './fetch'; diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 2297273102..ca841c2e83 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -45,13 +45,14 @@ 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, urlMatches } from './clientHelper'; +import { evaluationScript } from './clientHelper'; import { isString, isRegExp, isObject, headersObjectToArray } from '../utils'; import { mkdirIfNeeded } from '../utils/fileUtils'; import { isSafeCloseError } from '../common/errors'; import { Video } from './video'; import { Artifact } from './artifact'; import type { APIRequestContext } from './fetch'; +import { urlMatches } from '../common/netUtils'; type PDFOptions = Omit & { width?: string | number, diff --git a/packages/playwright-core/src/client/types.ts b/packages/playwright-core/src/client/types.ts index 2b5d249f3e..00e8d3203f 100644 --- a/packages/playwright-core/src/client/types.ts +++ b/packages/playwright-core/src/client/types.ts @@ -47,13 +47,18 @@ export type SetStorageState = { export type LifecycleEvent = channels.LifecycleEvent; export const kLifecycleEvents: Set = new Set(['load', 'domcontentloaded', 'networkidle', 'commit']); -export type BrowserContextOptions = Omit & { +export type BrowserContextOptions = Omit & { viewport?: Size | null, extraHTTPHeaders?: Headers, logger?: Logger, videosPath?: string, videoSize?: Size, storageState?: string | SetStorageState, + recordHar?: { + path: string, + omitContent?: boolean, + urlFilter?: string | RegExp, + }, }; type LaunchOverrides = { diff --git a/packages/playwright-core/src/common/netUtils.ts b/packages/playwright-core/src/common/netUtils.ts index 53c999bb1a..2b33e304c5 100644 --- a/packages/playwright-core/src/common/netUtils.ts +++ b/packages/playwright-core/src/common/netUtils.ts @@ -20,6 +20,8 @@ import net from 'net'; import { getProxyForUrl } from '../utilsBundle'; import { HttpsProxyAgent } from '../utilsBundle'; import * as URL from 'url'; +import type { URLMatch } from './types'; +import { isString, constructURLBasedOnBaseURL, isRegExp } from '../utils'; export async function createSocket(host: string, port: number): Promise { return new Promise((resolve, reject) => { @@ -101,3 +103,91 @@ export function fetchData(params: HTTPRequestParams, onError?: (params: HTTPRequ }, reject); }); } + +export function urlMatches(baseURL: string | undefined, urlString: string, match: URLMatch | undefined): boolean { + if (match === undefined || match === '') + return true; + if (isString(match) && !match.startsWith('*')) + match = constructURLBasedOnBaseURL(baseURL, match); + if (isString(match)) + match = globToRegex(match); + if (isRegExp(match)) + return match.test(urlString); + if (typeof match === 'string' && match === urlString) + return true; + const url = parsedURL(urlString); + if (!url) + return false; + if (typeof match === 'string') + return url.pathname === match; + if (typeof match !== 'function') + throw new Error('url parameter should be string, RegExp or function'); + return match(url); +} + +function parsedURL(url: string): URL | null { + try { + return new URL.URL(url); + } catch (e) { + return null; + } +} + +const escapeGlobChars = new Set(['/', '$', '^', '+', '.', '(', ')', '=', '!', '|']); + +// Note: this function is exported so it can be unit-tested. +export function globToRegex(glob: string): RegExp { + const tokens = ['^']; + let inGroup; + for (let i = 0; i < glob.length; ++i) { + const c = glob[i]; + if (escapeGlobChars.has(c)) { + tokens.push('\\' + c); + continue; + } + if (c === '*') { + const beforeDeep = glob[i - 1]; + let starCount = 1; + while (glob[i + 1] === '*') { + starCount++; + i++; + } + const afterDeep = glob[i + 1]; + const isDeep = starCount > 1 && + (beforeDeep === '/' || beforeDeep === undefined) && + (afterDeep === '/' || afterDeep === undefined); + if (isDeep) { + tokens.push('((?:[^/]*(?:\/|$))*)'); + i++; + } else { + tokens.push('([^/]*)'); + } + continue; + } + + switch (c) { + case '?': + tokens.push('.'); + break; + case '{': + inGroup = true; + tokens.push('('); + break; + case '}': + inGroup = false; + tokens.push(')'); + break; + case ',': + if (inGroup) { + tokens.push('|'); + break; + } + tokens.push('\\' + c); + break; + default: + tokens.push(c); + } + } + tokens.push('$'); + return new RegExp(tokens.join('')); +} diff --git a/packages/playwright-core/src/protocol/channels.ts b/packages/playwright-core/src/protocol/channels.ts index 16e250f78e..e1cb131191 100644 --- a/packages/playwright-core/src/protocol/channels.ts +++ b/packages/playwright-core/src/protocol/channels.ts @@ -263,6 +263,14 @@ export type SerializedError = { value?: SerializedValue, }; +export type RecordHarOptions = { + omitContent?: boolean, + path: string, + urlGlob?: string, + urlRegexSource?: string, + urlRegexFlags?: string, +}; + export type FormField = { name: string, value?: string, @@ -737,10 +745,7 @@ export type BrowserTypeLaunchPersistentContextParams = { height: number, }, }, - recordHar?: { - omitContent?: boolean, - path: string, - }, + recordHar?: RecordHarOptions, strictSelectors?: boolean, userDataDir: string, slowMo?: number, @@ -809,10 +814,7 @@ export type BrowserTypeLaunchPersistentContextOptions = { height: number, }, }, - recordHar?: { - omitContent?: boolean, - path: string, - }, + recordHar?: RecordHarOptions, strictSelectors?: boolean, slowMo?: number, }; @@ -905,10 +907,7 @@ export type BrowserNewContextParams = { height: number, }, }, - recordHar?: { - omitContent?: boolean, - path: string, - }, + recordHar?: RecordHarOptions, strictSelectors?: boolean, proxy?: { server: string, @@ -964,10 +963,7 @@ export type BrowserNewContextOptions = { height: number, }, }, - recordHar?: { - omitContent?: boolean, - path: string, - }, + recordHar?: RecordHarOptions, strictSelectors?: boolean, proxy?: { server: string, @@ -3573,10 +3569,7 @@ export type ElectronLaunchParams = { ignoreHTTPSErrors?: boolean, locale?: string, offline?: boolean, - recordHar?: { - omitContent?: boolean, - path: string, - }, + recordHar?: RecordHarOptions, recordVideo?: { dir: string, size?: { @@ -3609,10 +3602,7 @@ export type ElectronLaunchOptions = { ignoreHTTPSErrors?: boolean, locale?: string, offline?: boolean, - recordHar?: { - omitContent?: boolean, - path: string, - }, + recordHar?: RecordHarOptions, recordVideo?: { dir: string, size?: { @@ -3991,10 +3981,7 @@ export type AndroidDeviceLaunchBrowserParams = { height: number, }, }, - recordHar?: { - omitContent?: boolean, - path: string, - }, + recordHar?: RecordHarOptions, strictSelectors?: boolean, pkg?: string, proxy?: { @@ -4047,10 +4034,7 @@ export type AndroidDeviceLaunchBrowserOptions = { height: number, }, }, - recordHar?: { - omitContent?: boolean, - path: string, - }, + recordHar?: RecordHarOptions, strictSelectors?: boolean, pkg?: string, proxy?: { diff --git a/packages/playwright-core/src/protocol/protocol.yml b/packages/playwright-core/src/protocol/protocol.yml index 1d37848e7f..493e83676f 100644 --- a/packages/playwright-core/src/protocol/protocol.yml +++ b/packages/playwright-core/src/protocol/protocol.yml @@ -220,6 +220,17 @@ SerializedError: stack: string? value: SerializedValue? + +RecordHarOptions: + type: object + properties: + omitContent: boolean? + path: string + urlGlob: string? + urlRegexSource: string? + urlRegexFlags: string? + + FormField: type: object properties: @@ -442,11 +453,7 @@ ContextOptions: properties: width: number height: number - recordHar: - type: object? - properties: - omitContent: boolean? - path: string + recordHar: RecordHarOptions? strictSelectors: boolean? LocalUtils: @@ -2786,11 +2793,7 @@ Electron: ignoreHTTPSErrors: boolean? locale: string? offline: boolean? - recordHar: - type: object? - properties: - omitContent: boolean? - path: string + recordHar: RecordHarOptions? recordVideo: type: object? properties: diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index dcffd61ee5..e7e0db8b84 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -153,6 +153,13 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { })), value: tOptional(tType('SerializedValue')), }); + scheme.RecordHarOptions = tObject({ + omitContent: tOptional(tBoolean), + path: tString, + urlGlob: tOptional(tString), + urlRegexSource: tOptional(tString), + urlRegexFlags: tOptional(tString), + }); scheme.FormField = tObject({ name: tString, value: tOptional(tString), @@ -345,10 +352,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { height: tNumber, })), })), - recordHar: tOptional(tObject({ - omitContent: tOptional(tBoolean), - path: tString, - })), + recordHar: tOptional(tType('RecordHarOptions')), strictSelectors: tOptional(tBoolean), userDataDir: tString, slowMo: tOptional(tNumber), @@ -404,10 +408,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { height: tNumber, })), })), - recordHar: tOptional(tObject({ - omitContent: tOptional(tBoolean), - path: tString, - })), + recordHar: tOptional(tType('RecordHarOptions')), strictSelectors: tOptional(tBoolean), proxy: tOptional(tObject({ server: tString, @@ -1279,10 +1280,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { ignoreHTTPSErrors: tOptional(tBoolean), locale: tOptional(tString), offline: tOptional(tBoolean), - recordHar: tOptional(tObject({ - omitContent: tOptional(tBoolean), - path: tString, - })), + recordHar: tOptional(tType('RecordHarOptions')), recordVideo: tOptional(tObject({ dir: tString, size: tOptional(tObject({ @@ -1441,10 +1439,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { height: tNumber, })), })), - recordHar: tOptional(tObject({ - omitContent: tOptional(tBoolean), - path: tString, - })), + recordHar: tOptional(tType('RecordHarOptions')), strictSelectors: tOptional(tBoolean), pkg: tOptional(tString), proxy: tOptional(tObject({ diff --git a/packages/playwright-core/src/server/har/harRecorder.ts b/packages/playwright-core/src/server/har/harRecorder.ts index 528c62fa5d..6538e59c68 100644 --- a/packages/playwright-core/src/server/har/harRecorder.ts +++ b/packages/playwright-core/src/server/har/harRecorder.ts @@ -20,11 +20,7 @@ import { Artifact } from '../artifact'; import type { BrowserContext } from '../browserContext'; import type * as har from './har'; import { HarTracer } from './harTracer'; - -type HarOptions = { - path: string; - omitContent?: boolean; -}; +import type { HarOptions } from '../types'; export class HarRecorder { private _artifact: Artifact; @@ -36,10 +32,12 @@ export class HarRecorder { constructor(context: BrowserContext | APIRequestContext, options: HarOptions) { this._artifact = new Artifact(context, options.path); this._options = options; + const urlFilterRe = options.urlRegexSource !== undefined && options.urlRegexFlags !== undefined ? new RegExp(options.urlRegexSource, options.urlRegexFlags) : undefined; this._tracer = new HarTracer(context, this, { content: options.omitContent ? 'omit' : 'embedded', waitForContentOnStop: true, skipScripts: false, + urlFilter: urlFilterRe ?? options.urlGlob, }); this._tracer.start(); } diff --git a/packages/playwright-core/src/server/har/harTracer.ts b/packages/playwright-core/src/server/har/harTracer.ts index bd906299a4..8c4de82ff1 100644 --- a/packages/playwright-core/src/server/har/harTracer.ts +++ b/packages/playwright-core/src/server/har/harTracer.ts @@ -27,6 +27,7 @@ import { eventsHelper } from '../../utils/eventsHelper'; import { mime } from '../../utilsBundle'; import { ManualPromise } from '../../utils/manualPromise'; import { getPlaywrightVersion } from '../../common/userAgent'; +import { urlMatches } from '../../common/netUtils'; const FALLBACK_HTTP_VERSION = 'HTTP/1.1'; @@ -40,6 +41,7 @@ type HarTracerOptions = { content: 'omit' | 'sha1' | 'embedded'; skipScripts: boolean; waitForContentOnStop: boolean; + urlFilter?: string | RegExp; }; export class HarTracer { @@ -51,12 +53,14 @@ export class HarTracer { private _eventListeners: RegisteredListener[] = []; private _started = false; private _entrySymbol: symbol; + private _baseURL: string | undefined; constructor(context: BrowserContext | APIRequestContext, delegate: HarTracerDelegate, options: HarTracerOptions) { this._context = context; this._delegate = delegate; this._options = options; this._entrySymbol = Symbol('requestHarEntry'); + this._baseURL = context instanceof APIRequestContext ? context._defaultOptions().baseURL : context._options.baseURL; } start() { @@ -76,7 +80,10 @@ export class HarTracer { eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFailed, request => this._onRequestFailed(request)), eventsHelper.addEventListener(this._context, BrowserContext.Events.Response, (response: network.Response) => this._onResponse(response))); } + } + private _shouldIncludeEntryWithUrl(urlString: string) { + return !this._options.urlFilter || urlMatches(this._baseURL, urlString, this._options.urlFilter); } private _entryForRequest(request: network.Request | APIRequestEvent): har.Entry | undefined { @@ -146,6 +153,8 @@ export class HarTracer { } private _onAPIRequest(event: APIRequestEvent) { + if (!this._shouldIncludeEntryWithUrl(event.url.toString())) + return; const harEntry = createHarEntry(event.method, event.url, '', ''); harEntry.request.cookies = event.cookies; harEntry.request.headers = Object.entries(event.headers).map(([name, value]) => ({ name, value })); @@ -189,6 +198,8 @@ export class HarTracer { } private _onRequest(request: network.Request) { + if (!this._shouldIncludeEntryWithUrl(request.url())) + return; const page = request.frame()._page; const url = network.parsedURL(request.url()); if (!url) diff --git a/packages/playwright-core/src/server/types.ts b/packages/playwright-core/src/server/types.ts index 6ae1d5e83a..1caa9eb47f 100644 --- a/packages/playwright-core/src/server/types.ts +++ b/packages/playwright-core/src/server/types.ts @@ -227,6 +227,14 @@ export type SetNetworkCookieParam = { export type EmulatedSize = { viewport: Size, screen: Size }; +export type HarOptions = { + omitContent?: boolean, + path: string, + urlGlob?: string, + urlRegexSource?: string, + urlRegexFlags?: string, +}; + export type BrowserContextOptions = { viewport?: Size, screen?: Size, @@ -253,10 +261,7 @@ export type BrowserContextOptions = { dir: string, size?: Size, }, - recordHar?: { - omitContent?: boolean, - path: string - }, + recordHar?: HarOptions, storageState?: SetStorageState, strictSelectors?: boolean, proxy?: ProxySettings, diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 64052e025b..8396ba264a 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -10310,6 +10310,13 @@ export interface BrowserType { * Path on the filesystem to write the HAR file to. */ path: string; + + /** + * A glob or regex pattern to filter requests that are stored in the HAR. When a `baseURL` via the context options was + * provided and the passed URL is a path, it gets merged via the + * [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. + */ + urlFilter?: string|RegExp; }; /** @@ -11459,6 +11466,13 @@ export interface AndroidDevice { * Path on the filesystem to write the HAR file to. */ path: string; + + /** + * A glob or regex pattern to filter requests that are stored in the HAR. When a `baseURL` via the context options was + * provided and the passed URL is a path, it gets merged via the + * [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. + */ + urlFilter?: string|RegExp; }; /** @@ -12985,6 +12999,13 @@ export interface Browser extends EventEmitter { * Path on the filesystem to write the HAR file to. */ path: string; + + /** + * A glob or regex pattern to filter requests that are stored in the HAR. When a `baseURL` via the context options was + * provided and the passed URL is a path, it gets merged via the + * [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. + */ + urlFilter?: string|RegExp; }; /** @@ -13744,6 +13765,13 @@ export interface Electron { * Path on the filesystem to write the HAR file to. */ path: string; + + /** + * A glob or regex pattern to filter requests that are stored in the HAR. When a `baseURL` via the context options was + * provided and the passed URL is a path, it gets merged via the + * [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. + */ + urlFilter?: string|RegExp; }; /** @@ -15426,6 +15454,13 @@ export interface BrowserContextOptions { * Path on the filesystem to write the HAR file to. */ path: string; + + /** + * A glob or regex pattern to filter requests that are stored in the HAR. When a `baseURL` via the context options was + * provided and the passed URL is a path, it gets merged via the + * [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. + */ + urlFilter?: string|RegExp; }; /** diff --git a/tests/library/har.spec.ts b/tests/library/har.spec.ts index be4a456fff..bb97d405b9 100644 --- a/tests/library/har.spec.ts +++ b/tests/library/har.spec.ts @@ -265,6 +265,28 @@ it('should include content @smoke', async ({ contextFactory, server }, testInfo) expect(log.entries[1].response.content.compression).toBe(0); }); +it('should filter by glob', async ({ contextFactory, server }, testInfo) => { + const harPath = testInfo.outputPath('test.har'); + const context = await contextFactory({ baseURL: server.PREFIX, recordHar: { path: harPath, urlFilter: '/*.css' }, ignoreHTTPSErrors: true }); + const page = await context.newPage(); + await page.goto('/har.html'); + await context.close(); + const log = JSON.parse(fs.readFileSync(harPath).toString())['log'] as Log; + expect(log.entries.length).toBe(1); + expect(log.entries[0].request.url.endsWith('one-style.css')).toBe(true); +}); + +it('should filter by regexp', async ({ contextFactory, server }, testInfo) => { + const harPath = testInfo.outputPath('test.har'); + const context = await contextFactory({ recordHar: { path: harPath, urlFilter: /HAR.X?HTML/i }, ignoreHTTPSErrors: true }); + const page = await context.newPage(); + await page.goto(server.PREFIX + '/har.html'); + await context.close(); + const log = JSON.parse(fs.readFileSync(harPath).toString())['log'] as Log; + expect(log.entries.length).toBe(1); + expect(log.entries[0].request.url.endsWith('har.html')).toBe(true); +}); + it('should include sizes', async ({ contextFactory, server, asset }, testInfo) => { const { page, getLog } = await pageWithHar(contextFactory, testInfo); await page.goto(server.PREFIX + '/har.html'); diff --git a/tests/page/interception.spec.ts b/tests/page/interception.spec.ts index 19f114e990..3b743f1700 100644 --- a/tests/page/interception.spec.ts +++ b/tests/page/interception.spec.ts @@ -16,7 +16,7 @@ */ import { test as it, expect } from './pageTest'; -import { globToRegex } from '../../packages/playwright-core/lib/client/clientHelper'; +import { globToRegex } from '../../packages/playwright-core/lib/common/netUtils'; import vm from 'vm'; it('should work with navigation @smoke', async ({ page, server }) => {