From 48bf2191bf66d98c4e2d8ce2fbb9b6376220fc63 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 10 Mar 2025 17:35:17 -0700 Subject: [PATCH 1/2] chore: resolve glob to regex in local utils --- .../src/client/browserContext.ts | 17 +++++- packages/playwright-core/src/client/frame.ts | 9 ++- .../playwright-core/src/client/localUtils.ts | 5 ++ .../playwright-core/src/client/network.ts | 20 +++---- packages/playwright-core/src/client/page.ts | 37 ++++++++---- .../playwright-core/src/protocol/validator.ts | 8 +++ .../dispatchers/localUtilsDispatcher.ts | 6 ++ .../dispatchers/webSocketRouteDispatcher.ts | 2 +- .../src/utils/isomorphic/urlMatch.ts | 57 +++++++++++++------ packages/protocol/src/channels.d.ts | 13 +++++ packages/protocol/src/protocol.yml | 8 +++ 11 files changed, 136 insertions(+), 46 deletions(-) diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index accee952882a1..32e74ab1c5b89 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -45,7 +45,7 @@ import type { BrowserType } from './browserType'; import type { BrowserContextOptions, Headers, LaunchOptions, StorageState, WaitForEventOptions } from './types'; import type * as structs from '../../types/structs'; import type * as api from '../../types/types'; -import type { URLMatch } from '../utils/isomorphic/urlMatch'; +import type { URLMatch, URLMatchResolved } from '../utils/isomorphic/urlMatch'; import type { Platform } from './platform'; import type * as channels from '@protocol/channels'; @@ -338,13 +338,24 @@ export class BrowserContext extends ChannelOwner this._bindings.set(name, binding); } + async _resolveUrlMatcher(url: URLMatch, forWebSocket?: boolean): Promise { + if (!isString(url)) + return url; + const localUtils = this._connection.localUtils(); + if (!localUtils) + throw new Error('Route is not supported in thin clients'); + return await localUtils.globToRegex(this._options.baseURL, url, forWebSocket); + } + async route(url: URLMatch, handler: network.RouteHandlerCallback, options: { times?: number } = {}): Promise { - this._routes.unshift(new network.RouteHandler(this._platform, this._options.baseURL, url, handler, options.times)); + const resolved = await this._resolveUrlMatcher(url); + this._routes.unshift(new network.RouteHandler(this._platform, url, resolved, handler, options.times)); await this._updateInterceptionPatterns(); } async routeWebSocket(url: URLMatch, handler: network.WebSocketRouteHandlerCallback): Promise { - this._webSocketRoutes.unshift(new network.WebSocketRouteHandler(this._options.baseURL, url, handler)); + const resolved = await this._resolveUrlMatcher(url, true); + this._webSocketRoutes.unshift(new network.WebSocketRouteHandler(url, resolved, handler)); await this._updateWebSocketInterceptionPatterns(); } diff --git a/packages/playwright-core/src/client/frame.ts b/packages/playwright-core/src/client/frame.ts index dafd3f8b2aafc..b67703f119f50 100644 --- a/packages/playwright-core/src/client/frame.ts +++ b/packages/playwright-core/src/client/frame.ts @@ -27,7 +27,8 @@ import { kLifecycleEvents } from './types'; import { Waiter } from './waiter'; import { assert } from '../utils/isomorphic/assert'; import { getByAltTextSelector, getByLabelSelector, getByPlaceholderSelector, getByRoleSelector, getByTestIdSelector, getByTextSelector, getByTitleSelector } from '../utils/isomorphic/locatorUtils'; -import { urlMatches } from '../utils/isomorphic/urlMatch'; +import { urlMatchesResolved } from '../utils/isomorphic/urlMatch'; +import { isString } from '../utils/isomorphic/rtti'; import type { LocatorOptions } from './locator'; import type { Page } from './page'; @@ -122,12 +123,13 @@ export class Frame extends ChannelOwner implements api.Fr const toUrl = typeof options.url === 'string' ? ` to "${options.url}"` : ''; waiter.log(`waiting for navigation${toUrl} until "${waitUntil}"`); + const matcher = options.url ? await this._page!.context()._resolveUrlMatcher(options.url) : undefined; const navigatedEvent = await waiter.waitForEvent(this._eventEmitter, 'navigated', event => { // Any failed navigation results in a rejection. if (event.error) return true; waiter.log(` navigated to "${event.url}"`); - return urlMatches(this._page?.context()._options.baseURL, event.url, options.url); + return urlMatchesResolved(event.url, matcher); }); if (navigatedEvent.error) { const e = new Error(navigatedEvent.error); @@ -166,7 +168,8 @@ export class Frame extends ChannelOwner implements api.Fr } async waitForURL(url: URLMatch, options: { waitUntil?: LifecycleEvent, timeout?: number } = {}): Promise { - if (urlMatches(this._page?.context()._options.baseURL, this.url(), url)) + const matcher = await this._page!.context()._resolveUrlMatcher(url); + if (urlMatchesResolved(this.url(), matcher)) return await this.waitForLoadState(options.waitUntil, options); await this.waitForNavigation({ url, ...options }); diff --git a/packages/playwright-core/src/client/localUtils.ts b/packages/playwright-core/src/client/localUtils.ts index 5ba982251f4a3..4f743d1b39f9b 100644 --- a/packages/playwright-core/src/client/localUtils.ts +++ b/packages/playwright-core/src/client/localUtils.ts @@ -71,4 +71,9 @@ export class LocalUtils extends ChannelOwner { async addStackToTracingNoReply(params: channels.LocalUtilsAddStackToTracingNoReplyParams): Promise { return await this._channel.addStackToTracingNoReply(params); } + + async globToRegex(baseURL: string | undefined, glob: string, forWebSocket?: boolean): Promise { + const { regex } = await this._channel.globToRegex({ baseURL, glob, forWebSocket }); + return new RegExp(regex); + } } diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts index 01525b2939ff2..341c66c7eaea0 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -23,7 +23,7 @@ import { Waiter } from './waiter'; import { Worker } from './worker'; import { assert } from '../utils/isomorphic/assert'; import { headersObjectToArray } from '../utils/isomorphic/headers'; -import { urlMatches } from '../utils/isomorphic/urlMatch'; +import { urlMatchesResolved } from '../utils/isomorphic/urlMatch'; import { LongStandingScope, ManualPromise } from '../utils/isomorphic/manualPromise'; import { MultiMap } from '../utils/isomorphic/multimap'; import { isRegExp, isString } from '../utils/isomorphic/rtti'; @@ -36,7 +36,7 @@ import type { Headers, RemoteAddr, SecurityDetails, WaitForEventOptions } from ' import type { Serializable } from '../../types/structs'; import type * as api from '../../types/types'; import type { HeadersArray } from '../utils/isomorphic/types'; -import type { URLMatch } from '../utils/isomorphic/urlMatch'; +import type { URLMatch, URLMatchResolved } from '../utils/isomorphic/urlMatch'; import type * as channels from '@protocol/channels'; import type { Platform, Zone } from './platform'; @@ -576,13 +576,13 @@ export class WebSocketRoute extends ChannelOwner } export class WebSocketRouteHandler { - private readonly _baseURL: string | undefined; readonly url: URLMatch; + private _resolvedMatcher: URLMatchResolved; readonly handler: WebSocketRouteHandlerCallback; - constructor(baseURL: string | undefined, url: URLMatch, handler: WebSocketRouteHandlerCallback) { - this._baseURL = baseURL; + constructor(url: URLMatch, resolvedMatcher: URLMatchResolved, handler: WebSocketRouteHandlerCallback) { this.url = url; + this._resolvedMatcher = resolvedMatcher; this.handler = handler; } @@ -603,7 +603,7 @@ export class WebSocketRouteHandler { } public matches(wsURL: string): boolean { - return urlMatches(this._baseURL, wsURL, this.url); + return urlMatchesResolved(wsURL, this._resolvedMatcher); } public async handle(webSocketRoute: WebSocketRoute) { @@ -812,18 +812,18 @@ export function validateHeaders(headers: Headers) { export class RouteHandler { private handledCount = 0; - private readonly _baseURL: string | undefined; private readonly _times: number; readonly url: URLMatch; + private _resolvedMatcher: URLMatchResolved; readonly handler: RouteHandlerCallback; private _ignoreException: boolean = false; private _activeInvocations: Set<{ complete: Promise, route: Route }> = new Set(); private _savedZone: Zone; - constructor(platform: Platform, baseURL: string | undefined, url: URLMatch, handler: RouteHandlerCallback, times: number = Number.MAX_SAFE_INTEGER) { - this._baseURL = baseURL; + constructor(platform: Platform, url: URLMatch, resolvedMatcher: URLMatchResolved, handler: RouteHandlerCallback, times: number = Number.MAX_SAFE_INTEGER) { this._times = times; this.url = url; + this._resolvedMatcher = resolvedMatcher; this.handler = handler; this._savedZone = platform.zones.current().pop(); } @@ -845,7 +845,7 @@ export class RouteHandler { } public matches(requestURL: string): boolean { - return urlMatches(this._baseURL, requestURL, this.url); + return urlMatchesResolved(requestURL, this._resolvedMatcher); } public async handle(route: Route): Promise { diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index b91b0b53cd5ec..684c79833fd4d 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -38,7 +38,7 @@ import { assert } from '../utils/isomorphic/assert'; import { mkdirIfNeeded } from './fileUtils'; import { headersObjectToArray } from '../utils/isomorphic/headers'; import { trimStringWithEllipsis } from '../utils/isomorphic/stringUtils'; -import { urlMatches, urlMatchesEqual } from '../utils/isomorphic/urlMatch'; +import { urlMatchesEqual, urlMatchesResolved } from '../utils/isomorphic/urlMatch'; import { LongStandingScope } from '../utils/isomorphic/manualPromise'; import { isObject, isRegExp, isString } from '../utils/isomorphic/rtti'; @@ -52,7 +52,7 @@ import type { FilePayload, Headers, LifecycleEvent, SelectOption, SelectOptionOp import type * as structs from '../../types/structs'; import type * as api from '../../types/types'; import type { ByRoleOptions } from '../utils/isomorphic/locatorUtils'; -import type { URLMatch } from '../utils/isomorphic/urlMatch'; +import type { URLMatch, URLMatchResolved } from '../utils/isomorphic/urlMatch'; import type * as channels from '@protocol/channels'; type PDFOptions = Omit & { @@ -266,7 +266,9 @@ export class Page extends ChannelOwner implements api.Page return this.frames().find(f => { if (name) return f.name() === name; - return urlMatches(this._browserContext._options.baseURL, f.url(), url); + if (isString(url)) + return f.url() === url; + return urlMatchesResolved(f.url(), url); }) || null; } @@ -428,11 +430,21 @@ export class Page extends ChannelOwner implements api.Page return await this._mainFrame.waitForURL(url, options); } + private async _resolveUrlMatcher(urlOrPredicate: string | RegExp | ((r: T) => boolean | Promise)): Promise boolean | Promise)> { + if (!isString(urlOrPredicate)) + return urlOrPredicate; + const localUtils = this._connection.localUtils(); + if (!localUtils) + throw new Error('Route is not supported in thin clients'); + return await localUtils.globToRegex(this._browserContext._options.baseURL, urlOrPredicate); + } + async waitForRequest(urlOrPredicate: string | RegExp | ((r: Request) => boolean | Promise), options: { timeout?: number } = {}): Promise { + const resolved = await this._resolveUrlMatcher(urlOrPredicate); const predicate = async (request: Request) => { - if (isString(urlOrPredicate) || isRegExp(urlOrPredicate)) - return urlMatches(this._browserContext._options.baseURL, request.url(), urlOrPredicate); - return await urlOrPredicate(request); + if (isRegExp(resolved)) + return resolved.test(request.url()); + return await resolved(request); }; const trimmedUrl = trimUrl(urlOrPredicate); const logLine = trimmedUrl ? `waiting for request ${trimmedUrl}` : undefined; @@ -440,10 +452,11 @@ export class Page extends ChannelOwner implements api.Page } async waitForResponse(urlOrPredicate: string | RegExp | ((r: Response) => boolean | Promise), options: { timeout?: number } = {}): Promise { + const resolved = await this._resolveUrlMatcher(urlOrPredicate); const predicate = async (response: Response) => { - if (isString(urlOrPredicate) || isRegExp(urlOrPredicate)) - return urlMatches(this._browserContext._options.baseURL, response.url(), urlOrPredicate); - return await urlOrPredicate(response); + if (isRegExp(resolved)) + return resolved.test(response.url()); + return await resolved(response); }; const trimmedUrl = trimUrl(urlOrPredicate); const logLine = trimmedUrl ? `waiting for response ${trimmedUrl}` : undefined; @@ -520,7 +533,8 @@ export class Page extends ChannelOwner implements api.Page } async route(url: URLMatch, handler: RouteHandlerCallback, options: { times?: number } = {}): Promise { - this._routes.unshift(new RouteHandler(this._platform, this._browserContext._options.baseURL, url, handler, options.times)); + const resolved = await this._browserContext._resolveUrlMatcher(url); + this._routes.unshift(new RouteHandler(this._platform, url, resolved, handler, options.times)); await this._updateInterceptionPatterns(); } @@ -538,7 +552,8 @@ export class Page extends ChannelOwner implements api.Page } async routeWebSocket(url: URLMatch, handler: WebSocketRouteHandlerCallback): Promise { - this._webSocketRoutes.unshift(new WebSocketRouteHandler(this._browserContext._options.baseURL, url, handler)); + const resolved = await this._browserContext._resolveUrlMatcher(url, true); + this._webSocketRoutes.unshift(new WebSocketRouteHandler(url, resolved, handler)); await this._updateWebSocketInterceptionPatterns(); } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 109990bc88c49..bdbbd8540c049 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -344,6 +344,14 @@ scheme.LocalUtilsTraceDiscardedParams = tObject({ stacksId: tString, }); scheme.LocalUtilsTraceDiscardedResult = tOptional(tObject({})); +scheme.LocalUtilsGlobToRegexParams = tObject({ + glob: tString, + baseURL: tOptional(tString), + forWebSocket: tOptional(tBoolean), +}); +scheme.LocalUtilsGlobToRegexResult = tObject({ + regex: tString, +}); scheme.RootInitializer = tOptional(tObject({})); scheme.RootInitializeParams = tObject({ sdkLanguage: tEnum(['javascript', 'python', 'java', 'csharp']), diff --git a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts index 1c74b7258ca3f..368e84f7f216c 100644 --- a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts @@ -24,6 +24,7 @@ import { Progress, ProgressController } from '../progress'; import { SocksInterceptor } from '../socksInterceptor'; import { WebSocketTransport } from '../transport'; import { fetchData } from '../utils/network'; +import { globToRegexPattern } from '../../utils/isomorphic/urlMatch'; import type { HarBackend } from '../harBackend'; import type { CallMetadata } from '../instrumentation'; @@ -120,6 +121,11 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels. return { pipe, headers: transport.headers }; }, params.timeout || 0); } + + async globToRegex(params: channels.LocalUtilsGlobToRegexParams, metadata?: CallMetadata): Promise { + const regex = globToRegexPattern(params.baseURL, params.glob, params.forWebSocket); + return { regex }; + } } async function urlToWSEndpoint(progress: Progress | undefined, endpointURL: string): Promise { diff --git a/packages/playwright-core/src/server/dispatchers/webSocketRouteDispatcher.ts b/packages/playwright-core/src/server/dispatchers/webSocketRouteDispatcher.ts index f31bf7659d860..b85aae5b3f766 100644 --- a/packages/playwright-core/src/server/dispatchers/webSocketRouteDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/webSocketRouteDispatcher.ts @@ -148,7 +148,7 @@ export class WebSocketRouteDispatcher extends Dispatcher<{ guid: string }, chann function matchesPattern(dispatcher: PageDispatcher | BrowserContextDispatcher, baseURL: string | undefined, url: string) { for (const pattern of dispatcher._webSocketInterceptionPatterns || []) { const urlMatch = pattern.regexSource ? new RegExp(pattern.regexSource, pattern.regexFlags) : pattern.glob; - if (urlMatches(baseURL, url, urlMatch)) + if (urlMatches(baseURL, url, urlMatch, true)) return true; } return false; diff --git a/packages/playwright-core/src/utils/isomorphic/urlMatch.ts b/packages/playwright-core/src/utils/isomorphic/urlMatch.ts index a6afce68dde77..e702d2b29a7f1 100644 --- a/packages/playwright-core/src/utils/isomorphic/urlMatch.ts +++ b/packages/playwright-core/src/utils/isomorphic/urlMatch.ts @@ -19,7 +19,7 @@ import { isString } from './stringUtils'; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping const escapedChars = new Set(['$', '^', '+', '.', '*', '(', ')', '|', '\\', '?', '{', '}', '[', ']']); -export function globToRegex(glob: string): RegExp { +export function globToRegex(glob: string): string { const tokens = ['^']; let inGroup = false; for (let i = 0; i < glob.length; ++i) { @@ -70,14 +70,15 @@ export function globToRegex(glob: string): RegExp { } } tokens.push('$'); - return new RegExp(tokens.join('')); + return tokens.join(''); } function isRegExp(obj: any): obj is RegExp { return obj instanceof RegExp || Object.prototype.toString.call(obj) === '[object RegExp]'; } -export type URLMatch = string | RegExp | ((url: URL) => boolean); +export type URLMatchResolved = RegExp | ((url: URL) => boolean); +export type URLMatch = string | URLMatchResolved; export function urlMatchesEqual(match1: URLMatch, match2: URLMatch) { if (isRegExp(match1) && isRegExp(match2)) @@ -85,14 +86,43 @@ export function urlMatchesEqual(match1: URLMatch, match2: URLMatch) { return match1 === match2; } -export function urlMatches(baseURL: string | undefined, urlString: string, match: URLMatch | undefined): boolean { +export function urlMatches(baseURL: string | undefined, urlString: string, match: URLMatch | undefined, forWebSocket?: boolean): boolean { if (match === undefined || match === '') return true; - if (isString(match) && !match.startsWith('*')) { - // Allow http(s) baseURL to match ws(s) urls. - if (baseURL && /^https?:\/\//.test(baseURL) && /^wss?:\/\//.test(urlString)) - baseURL = baseURL.replace(/^http/, 'ws'); + if (isString(match)) + match = new RegExp(globToRegexPattern(baseURL, match, forWebSocket)); + if (isRegExp(match)) { + const r = match.test(urlString); + return r; + } + const url = parseURL(urlString); + if (!url) + return false; + if (typeof match !== 'function') + throw new Error('url parameter should be string, RegExp or function'); + return match(url); +} + +export function urlMatchesResolved(urlString: string, match: URLMatchResolved | undefined): boolean { + return urlMatches(undefined, urlString, match); +} +export function globToRegexPattern(baseURL: string | undefined, glob: string, forWebSocket?: boolean): string { + glob = resolveGlobBase(baseURL, glob, forWebSocket); + return globToRegex(glob); +} + +function toWebSocketBaseUrl(baseURL: string | undefined) { + // Allow http(s) baseURL to match ws(s) urls. + if (baseURL && /^https?:\/\//.test(baseURL)) + baseURL = baseURL.replace(/^http/, 'ws'); + return baseURL; +} + +function resolveGlobBase(baseURL: string | undefined, match: string, forWebSocket?: boolean): string { + if (!match.startsWith('*')) { + if (forWebSocket) + baseURL = toWebSocketBaseUrl(baseURL); const tokenMap = new Map(); function mapToken(original: string, replacement: string) { if (original.length === 0) @@ -123,16 +153,7 @@ export function urlMatches(baseURL: string | undefined, urlString: string, match resolved = resolved.replace(token, original); match = resolved; } - if (isString(match)) - match = globToRegex(match); - if (isRegExp(match)) - return match.test(urlString); - const url = parseURL(urlString); - if (!url) - return false; - if (typeof match !== 'function') - throw new Error('url parameter should be string, RegExp or function'); - return match(url); + return match; } function parseURL(url: string): URL | null { diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 582c61e721378..511ecd4c286a4 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -473,6 +473,7 @@ export interface LocalUtilsChannel extends LocalUtilsEventTarget, Channel { tracingStarted(params: LocalUtilsTracingStartedParams, metadata?: CallMetadata): Promise; addStackToTracingNoReply(params: LocalUtilsAddStackToTracingNoReplyParams, metadata?: CallMetadata): Promise; traceDiscarded(params: LocalUtilsTraceDiscardedParams, metadata?: CallMetadata): Promise; + globToRegex(params: LocalUtilsGlobToRegexParams, metadata?: CallMetadata): Promise; } export type LocalUtilsZipParams = { zipFile: string, @@ -572,6 +573,18 @@ export type LocalUtilsTraceDiscardedOptions = { }; export type LocalUtilsTraceDiscardedResult = void; +export type LocalUtilsGlobToRegexParams = { + glob: string, + baseURL?: string, + forWebSocket?: boolean, +}; +export type LocalUtilsGlobToRegexOptions = { + baseURL?: string, + forWebSocket?: boolean, +}; +export type LocalUtilsGlobToRegexResult = { + regex: string, +}; export interface LocalUtilsEvents { } diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index f385b3eb7ad40..b7a9279a4dace 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -705,6 +705,14 @@ LocalUtils: parameters: stacksId: string + globToRegex: + parameters: + glob: string + baseURL: string? + forWebSocket: boolean? + returns: + regex: string + Root: type: interface From 8ffe4253b6d9a0c6508a387223b4f1a35766c6e1 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 11 Mar 2025 13:49:46 -0700 Subject: [PATCH 2/2] local utils only --- .../src/client/browserContext.ts | 17 ++------- packages/playwright-core/src/client/frame.ts | 9 ++--- .../playwright-core/src/client/localUtils.ts | 5 --- .../playwright-core/src/client/network.ts | 20 +++++----- packages/playwright-core/src/client/page.ts | 37 ++++++------------- .../playwright-core/src/protocol/validator.ts | 2 +- .../dispatchers/localUtilsDispatcher.ts | 4 +- .../src/utils/isomorphic/urlMatch.ts | 25 +++++-------- packages/protocol/src/channels.d.ts | 4 +- packages/protocol/src/protocol.yml | 2 +- tests/page/interception.spec.ts | 5 ++- 11 files changed, 47 insertions(+), 83 deletions(-) diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 32e74ab1c5b89..accee952882a1 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -45,7 +45,7 @@ import type { BrowserType } from './browserType'; import type { BrowserContextOptions, Headers, LaunchOptions, StorageState, WaitForEventOptions } from './types'; import type * as structs from '../../types/structs'; import type * as api from '../../types/types'; -import type { URLMatch, URLMatchResolved } from '../utils/isomorphic/urlMatch'; +import type { URLMatch } from '../utils/isomorphic/urlMatch'; import type { Platform } from './platform'; import type * as channels from '@protocol/channels'; @@ -338,24 +338,13 @@ export class BrowserContext extends ChannelOwner this._bindings.set(name, binding); } - async _resolveUrlMatcher(url: URLMatch, forWebSocket?: boolean): Promise { - if (!isString(url)) - return url; - const localUtils = this._connection.localUtils(); - if (!localUtils) - throw new Error('Route is not supported in thin clients'); - return await localUtils.globToRegex(this._options.baseURL, url, forWebSocket); - } - async route(url: URLMatch, handler: network.RouteHandlerCallback, options: { times?: number } = {}): Promise { - const resolved = await this._resolveUrlMatcher(url); - this._routes.unshift(new network.RouteHandler(this._platform, url, resolved, handler, options.times)); + this._routes.unshift(new network.RouteHandler(this._platform, this._options.baseURL, url, handler, options.times)); await this._updateInterceptionPatterns(); } async routeWebSocket(url: URLMatch, handler: network.WebSocketRouteHandlerCallback): Promise { - const resolved = await this._resolveUrlMatcher(url, true); - this._webSocketRoutes.unshift(new network.WebSocketRouteHandler(url, resolved, handler)); + this._webSocketRoutes.unshift(new network.WebSocketRouteHandler(this._options.baseURL, url, handler)); await this._updateWebSocketInterceptionPatterns(); } diff --git a/packages/playwright-core/src/client/frame.ts b/packages/playwright-core/src/client/frame.ts index b67703f119f50..dafd3f8b2aafc 100644 --- a/packages/playwright-core/src/client/frame.ts +++ b/packages/playwright-core/src/client/frame.ts @@ -27,8 +27,7 @@ import { kLifecycleEvents } from './types'; import { Waiter } from './waiter'; import { assert } from '../utils/isomorphic/assert'; import { getByAltTextSelector, getByLabelSelector, getByPlaceholderSelector, getByRoleSelector, getByTestIdSelector, getByTextSelector, getByTitleSelector } from '../utils/isomorphic/locatorUtils'; -import { urlMatchesResolved } from '../utils/isomorphic/urlMatch'; -import { isString } from '../utils/isomorphic/rtti'; +import { urlMatches } from '../utils/isomorphic/urlMatch'; import type { LocatorOptions } from './locator'; import type { Page } from './page'; @@ -123,13 +122,12 @@ export class Frame extends ChannelOwner implements api.Fr const toUrl = typeof options.url === 'string' ? ` to "${options.url}"` : ''; waiter.log(`waiting for navigation${toUrl} until "${waitUntil}"`); - const matcher = options.url ? await this._page!.context()._resolveUrlMatcher(options.url) : undefined; const navigatedEvent = await waiter.waitForEvent(this._eventEmitter, 'navigated', event => { // Any failed navigation results in a rejection. if (event.error) return true; waiter.log(` navigated to "${event.url}"`); - return urlMatchesResolved(event.url, matcher); + return urlMatches(this._page?.context()._options.baseURL, event.url, options.url); }); if (navigatedEvent.error) { const e = new Error(navigatedEvent.error); @@ -168,8 +166,7 @@ export class Frame extends ChannelOwner implements api.Fr } async waitForURL(url: URLMatch, options: { waitUntil?: LifecycleEvent, timeout?: number } = {}): Promise { - const matcher = await this._page!.context()._resolveUrlMatcher(url); - if (urlMatchesResolved(this.url(), matcher)) + if (urlMatches(this._page?.context()._options.baseURL, this.url(), url)) return await this.waitForLoadState(options.waitUntil, options); await this.waitForNavigation({ url, ...options }); diff --git a/packages/playwright-core/src/client/localUtils.ts b/packages/playwright-core/src/client/localUtils.ts index 4f743d1b39f9b..5ba982251f4a3 100644 --- a/packages/playwright-core/src/client/localUtils.ts +++ b/packages/playwright-core/src/client/localUtils.ts @@ -71,9 +71,4 @@ export class LocalUtils extends ChannelOwner { async addStackToTracingNoReply(params: channels.LocalUtilsAddStackToTracingNoReplyParams): Promise { return await this._channel.addStackToTracingNoReply(params); } - - async globToRegex(baseURL: string | undefined, glob: string, forWebSocket?: boolean): Promise { - const { regex } = await this._channel.globToRegex({ baseURL, glob, forWebSocket }); - return new RegExp(regex); - } } diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts index 341c66c7eaea0..4670f6d059135 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -23,7 +23,7 @@ import { Waiter } from './waiter'; import { Worker } from './worker'; import { assert } from '../utils/isomorphic/assert'; import { headersObjectToArray } from '../utils/isomorphic/headers'; -import { urlMatchesResolved } from '../utils/isomorphic/urlMatch'; +import { urlMatches } from '../utils/isomorphic/urlMatch'; import { LongStandingScope, ManualPromise } from '../utils/isomorphic/manualPromise'; import { MultiMap } from '../utils/isomorphic/multimap'; import { isRegExp, isString } from '../utils/isomorphic/rtti'; @@ -36,7 +36,7 @@ import type { Headers, RemoteAddr, SecurityDetails, WaitForEventOptions } from ' import type { Serializable } from '../../types/structs'; import type * as api from '../../types/types'; import type { HeadersArray } from '../utils/isomorphic/types'; -import type { URLMatch, URLMatchResolved } from '../utils/isomorphic/urlMatch'; +import type { URLMatch } from '../utils/isomorphic/urlMatch'; import type * as channels from '@protocol/channels'; import type { Platform, Zone } from './platform'; @@ -576,13 +576,13 @@ export class WebSocketRoute extends ChannelOwner } export class WebSocketRouteHandler { + private readonly _baseURL: string | undefined; readonly url: URLMatch; - private _resolvedMatcher: URLMatchResolved; readonly handler: WebSocketRouteHandlerCallback; - constructor(url: URLMatch, resolvedMatcher: URLMatchResolved, handler: WebSocketRouteHandlerCallback) { + constructor(baseURL: string | undefined, url: URLMatch, handler: WebSocketRouteHandlerCallback) { + this._baseURL = baseURL; this.url = url; - this._resolvedMatcher = resolvedMatcher; this.handler = handler; } @@ -603,7 +603,7 @@ export class WebSocketRouteHandler { } public matches(wsURL: string): boolean { - return urlMatchesResolved(wsURL, this._resolvedMatcher); + return urlMatches(this._baseURL, wsURL, this.url, true); } public async handle(webSocketRoute: WebSocketRoute) { @@ -812,18 +812,18 @@ export function validateHeaders(headers: Headers) { export class RouteHandler { private handledCount = 0; + private readonly _baseURL: string | undefined; private readonly _times: number; readonly url: URLMatch; - private _resolvedMatcher: URLMatchResolved; readonly handler: RouteHandlerCallback; private _ignoreException: boolean = false; private _activeInvocations: Set<{ complete: Promise, route: Route }> = new Set(); private _savedZone: Zone; - constructor(platform: Platform, url: URLMatch, resolvedMatcher: URLMatchResolved, handler: RouteHandlerCallback, times: number = Number.MAX_SAFE_INTEGER) { + constructor(platform: Platform, baseURL: string | undefined, url: URLMatch, handler: RouteHandlerCallback, times: number = Number.MAX_SAFE_INTEGER) { + this._baseURL = baseURL; this._times = times; this.url = url; - this._resolvedMatcher = resolvedMatcher; this.handler = handler; this._savedZone = platform.zones.current().pop(); } @@ -845,7 +845,7 @@ export class RouteHandler { } public matches(requestURL: string): boolean { - return urlMatchesResolved(requestURL, this._resolvedMatcher); + return urlMatches(this._baseURL, requestURL, this.url); } public async handle(route: Route): Promise { diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 684c79833fd4d..b91b0b53cd5ec 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -38,7 +38,7 @@ import { assert } from '../utils/isomorphic/assert'; import { mkdirIfNeeded } from './fileUtils'; import { headersObjectToArray } from '../utils/isomorphic/headers'; import { trimStringWithEllipsis } from '../utils/isomorphic/stringUtils'; -import { urlMatchesEqual, urlMatchesResolved } from '../utils/isomorphic/urlMatch'; +import { urlMatches, urlMatchesEqual } from '../utils/isomorphic/urlMatch'; import { LongStandingScope } from '../utils/isomorphic/manualPromise'; import { isObject, isRegExp, isString } from '../utils/isomorphic/rtti'; @@ -52,7 +52,7 @@ import type { FilePayload, Headers, LifecycleEvent, SelectOption, SelectOptionOp import type * as structs from '../../types/structs'; import type * as api from '../../types/types'; import type { ByRoleOptions } from '../utils/isomorphic/locatorUtils'; -import type { URLMatch, URLMatchResolved } from '../utils/isomorphic/urlMatch'; +import type { URLMatch } from '../utils/isomorphic/urlMatch'; import type * as channels from '@protocol/channels'; type PDFOptions = Omit & { @@ -266,9 +266,7 @@ export class Page extends ChannelOwner implements api.Page return this.frames().find(f => { if (name) return f.name() === name; - if (isString(url)) - return f.url() === url; - return urlMatchesResolved(f.url(), url); + return urlMatches(this._browserContext._options.baseURL, f.url(), url); }) || null; } @@ -430,21 +428,11 @@ export class Page extends ChannelOwner implements api.Page return await this._mainFrame.waitForURL(url, options); } - private async _resolveUrlMatcher(urlOrPredicate: string | RegExp | ((r: T) => boolean | Promise)): Promise boolean | Promise)> { - if (!isString(urlOrPredicate)) - return urlOrPredicate; - const localUtils = this._connection.localUtils(); - if (!localUtils) - throw new Error('Route is not supported in thin clients'); - return await localUtils.globToRegex(this._browserContext._options.baseURL, urlOrPredicate); - } - async waitForRequest(urlOrPredicate: string | RegExp | ((r: Request) => boolean | Promise), options: { timeout?: number } = {}): Promise { - const resolved = await this._resolveUrlMatcher(urlOrPredicate); const predicate = async (request: Request) => { - if (isRegExp(resolved)) - return resolved.test(request.url()); - return await resolved(request); + if (isString(urlOrPredicate) || isRegExp(urlOrPredicate)) + return urlMatches(this._browserContext._options.baseURL, request.url(), urlOrPredicate); + return await urlOrPredicate(request); }; const trimmedUrl = trimUrl(urlOrPredicate); const logLine = trimmedUrl ? `waiting for request ${trimmedUrl}` : undefined; @@ -452,11 +440,10 @@ export class Page extends ChannelOwner implements api.Page } async waitForResponse(urlOrPredicate: string | RegExp | ((r: Response) => boolean | Promise), options: { timeout?: number } = {}): Promise { - const resolved = await this._resolveUrlMatcher(urlOrPredicate); const predicate = async (response: Response) => { - if (isRegExp(resolved)) - return resolved.test(response.url()); - return await resolved(response); + if (isString(urlOrPredicate) || isRegExp(urlOrPredicate)) + return urlMatches(this._browserContext._options.baseURL, response.url(), urlOrPredicate); + return await urlOrPredicate(response); }; const trimmedUrl = trimUrl(urlOrPredicate); const logLine = trimmedUrl ? `waiting for response ${trimmedUrl}` : undefined; @@ -533,8 +520,7 @@ export class Page extends ChannelOwner implements api.Page } async route(url: URLMatch, handler: RouteHandlerCallback, options: { times?: number } = {}): Promise { - const resolved = await this._browserContext._resolveUrlMatcher(url); - this._routes.unshift(new RouteHandler(this._platform, url, resolved, handler, options.times)); + this._routes.unshift(new RouteHandler(this._platform, this._browserContext._options.baseURL, url, handler, options.times)); await this._updateInterceptionPatterns(); } @@ -552,8 +538,7 @@ export class Page extends ChannelOwner implements api.Page } async routeWebSocket(url: URLMatch, handler: WebSocketRouteHandlerCallback): Promise { - const resolved = await this._browserContext._resolveUrlMatcher(url, true); - this._webSocketRoutes.unshift(new WebSocketRouteHandler(url, resolved, handler)); + this._webSocketRoutes.unshift(new WebSocketRouteHandler(this._browserContext._options.baseURL, url, handler)); await this._updateWebSocketInterceptionPatterns(); } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index bdbbd8540c049..da56445699f0a 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -347,7 +347,7 @@ scheme.LocalUtilsTraceDiscardedResult = tOptional(tObject({})); scheme.LocalUtilsGlobToRegexParams = tObject({ glob: tString, baseURL: tOptional(tString), - forWebSocket: tOptional(tBoolean), + webSocketUrl: tOptional(tBoolean), }); scheme.LocalUtilsGlobToRegexResult = tObject({ regex: tString, diff --git a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts index 368e84f7f216c..a681b349d325f 100644 --- a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts @@ -24,7 +24,7 @@ import { Progress, ProgressController } from '../progress'; import { SocksInterceptor } from '../socksInterceptor'; import { WebSocketTransport } from '../transport'; import { fetchData } from '../utils/network'; -import { globToRegexPattern } from '../../utils/isomorphic/urlMatch'; +import { resolveGlobToRegexPattern } from '../../utils/isomorphic/urlMatch'; import type { HarBackend } from '../harBackend'; import type { CallMetadata } from '../instrumentation'; @@ -123,7 +123,7 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels. } async globToRegex(params: channels.LocalUtilsGlobToRegexParams, metadata?: CallMetadata): Promise { - const regex = globToRegexPattern(params.baseURL, params.glob, params.forWebSocket); + const regex = resolveGlobToRegexPattern(params.baseURL, params.glob, params.webSocketUrl); return { regex }; } } diff --git a/packages/playwright-core/src/utils/isomorphic/urlMatch.ts b/packages/playwright-core/src/utils/isomorphic/urlMatch.ts index e702d2b29a7f1..35965191c9ddd 100644 --- a/packages/playwright-core/src/utils/isomorphic/urlMatch.ts +++ b/packages/playwright-core/src/utils/isomorphic/urlMatch.ts @@ -19,7 +19,7 @@ import { isString } from './stringUtils'; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping const escapedChars = new Set(['$', '^', '+', '.', '*', '(', ')', '|', '\\', '?', '{', '}', '[', ']']); -export function globToRegex(glob: string): string { +export function globToRegexPattern(glob: string): string { const tokens = ['^']; let inGroup = false; for (let i = 0; i < glob.length; ++i) { @@ -77,8 +77,7 @@ function isRegExp(obj: any): obj is RegExp { return obj instanceof RegExp || Object.prototype.toString.call(obj) === '[object RegExp]'; } -export type URLMatchResolved = RegExp | ((url: URL) => boolean); -export type URLMatch = string | URLMatchResolved; +export type URLMatch = string | RegExp | ((url: URL) => boolean); export function urlMatchesEqual(match1: URLMatch, match2: URLMatch) { if (isRegExp(match1) && isRegExp(match2)) @@ -86,11 +85,11 @@ export function urlMatchesEqual(match1: URLMatch, match2: URLMatch) { return match1 === match2; } -export function urlMatches(baseURL: string | undefined, urlString: string, match: URLMatch | undefined, forWebSocket?: boolean): boolean { +export function urlMatches(baseURL: string | undefined, urlString: string, match: URLMatch | undefined, webSocketUrl?: boolean): boolean { if (match === undefined || match === '') return true; if (isString(match)) - match = new RegExp(globToRegexPattern(baseURL, match, forWebSocket)); + match = new RegExp(resolveGlobToRegexPattern(baseURL, match, webSocketUrl)); if (isRegExp(match)) { const r = match.test(urlString); return r; @@ -103,13 +102,11 @@ export function urlMatches(baseURL: string | undefined, urlString: string, match return match(url); } -export function urlMatchesResolved(urlString: string, match: URLMatchResolved | undefined): boolean { - return urlMatches(undefined, urlString, match); -} - -export function globToRegexPattern(baseURL: string | undefined, glob: string, forWebSocket?: boolean): string { - glob = resolveGlobBase(baseURL, glob, forWebSocket); - return globToRegex(glob); +export function resolveGlobToRegexPattern(baseURL: string | undefined, glob: string, webSocketUrl?: boolean): string { + if (webSocketUrl) + baseURL = toWebSocketBaseUrl(baseURL); + glob = resolveGlobBase(baseURL, glob); + return globToRegexPattern(glob); } function toWebSocketBaseUrl(baseURL: string | undefined) { @@ -119,10 +116,8 @@ function toWebSocketBaseUrl(baseURL: string | undefined) { return baseURL; } -function resolveGlobBase(baseURL: string | undefined, match: string, forWebSocket?: boolean): string { +function resolveGlobBase(baseURL: string | undefined, match: string): string { if (!match.startsWith('*')) { - if (forWebSocket) - baseURL = toWebSocketBaseUrl(baseURL); const tokenMap = new Map(); function mapToken(original: string, replacement: string) { if (original.length === 0) diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 511ecd4c286a4..80aca67578622 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -576,11 +576,11 @@ export type LocalUtilsTraceDiscardedResult = void; export type LocalUtilsGlobToRegexParams = { glob: string, baseURL?: string, - forWebSocket?: boolean, + webSocketUrl?: boolean, }; export type LocalUtilsGlobToRegexOptions = { baseURL?: string, - forWebSocket?: boolean, + webSocketUrl?: boolean, }; export type LocalUtilsGlobToRegexResult = { regex: string, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index b7a9279a4dace..3c2366f166558 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -709,7 +709,7 @@ LocalUtils: parameters: glob: string baseURL: string? - forWebSocket: boolean? + webSocketUrl: boolean? returns: regex: string diff --git a/tests/page/interception.spec.ts b/tests/page/interception.spec.ts index 1c2a8f5f4665a..80c94d0854034 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, urlMatches } from '../../packages/playwright-core/lib/utils/isomorphic/urlMatch'; +import { globToRegexPattern, urlMatches } from '../../packages/playwright-core/lib/utils/isomorphic/urlMatch'; import vm from 'vm'; it('should work with navigation @smoke', async ({ page, server }) => { @@ -71,6 +71,9 @@ it('should intercept after a service worker', async ({ page, server, browserName }); it('should work with glob', async () => { + function globToRegex(glob: string): RegExp { + return new RegExp(globToRegexPattern(glob)); + } expect(globToRegex('**/*.js').test('https://localhost:8080/foo.js')).toBeTruthy(); expect(globToRegex('**/*.css').test('https://localhost:8080/foo.js')).toBeFalsy(); expect(globToRegex('*.js').test('https://localhost:8080/foo.js')).toBeFalsy();