From 371aa3dab22d17569b4fa2f8f33355f9bc462b14 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 6 Jul 2021 21:16:37 +0200 Subject: [PATCH] feat: add browser.newContext({ baseUrl }) (#7409) --- docs/src/api/class-browsercontext.md | 2 + docs/src/api/class-page.md | 13 +++- docs/src/api/params.md | 8 +++ src/client/browserContext.ts | 2 +- src/client/clientHelper.ts | 6 +- src/client/frame.ts | 4 +- src/client/page.ts | 8 +-- src/protocol/channels.ts | 4 ++ src/protocol/protocol.yml | 1 + src/protocol/validator.ts | 2 + src/server/frames.ts | 5 +- src/server/types.ts | 1 + src/utils/utils.ts | 8 +++ tests/browsercontext-base-url.spec.ts | 93 +++++++++++++++++++++++++++ types/types.d.ts | 66 +++++++++++++++++-- 15 files changed, 205 insertions(+), 18 deletions(-) create mode 100644 tests/browsercontext-base-url.spec.ts diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 34c897af02..960d527374 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -1001,6 +1001,8 @@ Enabling routing disables http cache. - `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]> A glob pattern, regex pattern or predicate receiving [URL] to match while routing. +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. ### param: BrowserContext.route.handler * langs: js, python diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index aff6b9d585..6cd459a569 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -1903,6 +1903,8 @@ Shortcut for main frame's [`method: Frame.goto`] - `url` <[string]> URL to navigate page to. The url should include scheme, e.g. `https://`. +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. ### option: Page.goto.waitUntil = %%-navigation-wait-until-%% @@ -2510,7 +2512,8 @@ Enabling routing disables http cache. - `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]> A glob pattern, regex pattern or predicate receiving [URL] to match while routing. - +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. ### param: Page.route.handler * langs: js, python - `handler` <[function]\([Route], [Request]\)> @@ -3326,7 +3329,7 @@ Receives the [Page] object and resolves to truthy value when the waiting should * alias-csharp: RunAndWaitForRequest - returns: <[Request]> -Waits for the matching request and returns it. See [waiting for event](./events.md#waiting-for-event) for more details about events. +Waits for the matching request and returns it. See [waiting for event](./events.md#waiting-for-event) for more details about events. ```js // Note that Promise.all prevents a race condition @@ -3403,6 +3406,8 @@ await page.RunAndWaitForRequestAsync(async () => - `urlOrPredicate` <[string]|[RegExp]|[function]\([Request]\):[boolean]> Request URL string, regex or predicate receiving [Request] object. +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. ### param: Page.waitForRequest.urlOrPredicate * langs: js @@ -3522,12 +3527,16 @@ await page.RunAndWaitForResponseAsync(async () => - `urlOrPredicate` <[string]|[RegExp]|[function]\([Response]\):[boolean]> Request URL string, regex or predicate receiving [Response] object. +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. ### param: Page.waitForResponse.urlOrPredicate * langs: js - `urlOrPredicate` <[string]|[RegExp]|[function]\([Response]\):[boolean]|[Promise]<[boolean]>> Request URL string, regex or predicate receiving [Response] object. +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. ### option: Page.waitForResponse.timeout - `timeout` <[float]> diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 3b88a716d1..131efabe46 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -213,6 +213,13 @@ Whether to ignore HTTPS errors during navigation. Defaults to `false`. Toggles bypassing page's Content-Security-Policy. +## context-option-baseURL +- `baseURL` <[string]> + +When using [`method: Page.goto`], [`method: Page.route`], [`method: Page.waitForURL`], [`method: Page.waitForRequest`], or [`method: Page.waitForResponse`] it takes the base URL in consideration by using the [`URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor for building the corresponding URL. Examples: +* baseURL: `http://localhost:3000` and navigating to `/bar.html` results in `http://localhost:3000/bar.html` +* baseURL: `http://localhost:3000/foo/` and navigating to `./bar.html` results in `http://localhost:3000/foo/bar.html` + ## context-option-viewport * langs: js, java - alias-java: viewportSize @@ -566,6 +573,7 @@ using the [`method: AndroidDevice.setDefaultTimeout`] method. - %%-context-option-acceptdownloads-%% - %%-context-option-ignorehttpserrors-%% - %%-context-option-bypasscsp-%% +- %%-context-option-baseURL-%% - %%-context-option-viewport-%% - %%-csharp-context-option-viewport-%% - %%-python-context-option-viewport-%% diff --git a/src/client/browserContext.ts b/src/client/browserContext.ts index c0ff8e42a2..e2b781fd30 100644 --- a/src/client/browserContext.ts +++ b/src/client/browserContext.ts @@ -127,7 +127,7 @@ export class BrowserContext extends ChannelOwner { - if (urlMatches(this.url(), url)) + 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/src/client/page.ts b/src/client/page.ts index 929f5a6fc3..8c60853311 100644 --- a/src/client/page.ts +++ b/src/client/page.ts @@ -161,7 +161,7 @@ export class Page extends ChannelOwner { if (name) return f.name() === name; - return urlMatches(f.url(), url); + return urlMatches(this._browserContext._options.baseURL, f.url(), url); }) || null; } @@ -351,7 +351,7 @@ export class Page extends ChannelOwner { const predicate = (request: Request) => { if (isString(urlOrPredicate) || isRegExp(urlOrPredicate)) - return urlMatches(request.url(), urlOrPredicate); + return urlMatches(this._browserContext._options.baseURL, request.url(), urlOrPredicate); return urlOrPredicate(request); }; const trimmedUrl = trimUrl(urlOrPredicate); @@ -364,7 +364,7 @@ export class Page extends ChannelOwner { const predicate = (response: Response) => { if (isString(urlOrPredicate) || isRegExp(urlOrPredicate)) - return urlMatches(response.url(), urlOrPredicate); + return urlMatches(this._browserContext._options.baseURL, response.url(), urlOrPredicate); return urlOrPredicate(response); }; const trimmedUrl = trimUrl(urlOrPredicate); diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index cd53d5dfd8..74367858e3 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -329,6 +329,7 @@ export type BrowserTypeLaunchPersistentContextParams = { colorScheme?: 'dark' | 'light' | 'no-preference', reducedMotion?: 'reduce' | 'no-preference', acceptDownloads?: boolean, + baseURL?: string, _debugName?: string, recordVideo?: { dir: string, @@ -399,6 +400,7 @@ export type BrowserTypeLaunchPersistentContextOptions = { colorScheme?: 'dark' | 'light' | 'no-preference', reducedMotion?: 'reduce' | 'no-preference', acceptDownloads?: boolean, + baseURL?: string, _debugName?: string, recordVideo?: { dir: string, @@ -489,6 +491,7 @@ export type BrowserNewContextParams = { colorScheme?: 'dark' | 'light' | 'no-preference', reducedMotion?: 'reduce' | 'no-preference', acceptDownloads?: boolean, + baseURL?: string, _debugName?: string, recordVideo?: { dir: string, @@ -546,6 +549,7 @@ export type BrowserNewContextOptions = { colorScheme?: 'dark' | 'light' | 'no-preference', reducedMotion?: 'reduce' | 'no-preference', acceptDownloads?: boolean, + baseURL?: string, _debugName?: string, recordVideo?: { dir: string, diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index 535257c220..7d8e2e3af2 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -307,6 +307,7 @@ ContextOptions: - reduce - no-preference acceptDownloads: boolean? + baseURL: string? _debugName: string? recordVideo: type: object? diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index 603dd56568..77b78c521a 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -238,6 +238,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { colorScheme: tOptional(tEnum(['dark', 'light', 'no-preference'])), reducedMotion: tOptional(tEnum(['reduce', 'no-preference'])), acceptDownloads: tOptional(tBoolean), + baseURL: tOptional(tString), _debugName: tOptional(tString), recordVideo: tOptional(tObject({ dir: tString, @@ -297,6 +298,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { colorScheme: tOptional(tEnum(['dark', 'light', 'no-preference'])), reducedMotion: tOptional(tEnum(['reduce', 'no-preference'])), acceptDownloads: tOptional(tBoolean), + baseURL: tOptional(tString), _debugName: tOptional(tString), recordVideo: tOptional(tObject({ dir: tString, diff --git a/src/server/frames.ts b/src/server/frames.ts index 356e744f93..5b3e32e5d3 100644 --- a/src/server/frames.ts +++ b/src/server/frames.ts @@ -25,7 +25,7 @@ import { Page } from './page'; import * as types from './types'; import { BrowserContext } from './browserContext'; import { Progress, ProgressController } from './progress'; -import { assert, makeWaitForNextTask } from '../utils/utils'; +import { assert, constructURLBasedOnBaseURL, makeWaitForNextTask } from '../utils/utils'; import { debugLogger } from '../utils/debugLogger'; import { CallMetadata, internalCallMetadata, SdkObject } from './instrumentation'; import { ElementStateWithoutStable } from './injected/injectedScript'; @@ -556,8 +556,9 @@ export class Frame extends SdkObject { } async goto(metadata: CallMetadata, url: string, options: types.GotoOptions = {}): Promise { + const constructedNavigationURL = constructURLBasedOnBaseURL(this._page._browserContext._options.baseURL, url); const controller = new ProgressController(metadata, this); - return controller.run(progress => this._goto(progress, url, options), this._page._timeoutSettings.navigationTimeout(options)); + return controller.run(progress => this._goto(progress, constructedNavigationURL, options), this._page._timeoutSettings.navigationTimeout(options)); } private async _goto(progress: Progress, url: string, options: types.GotoOptions): Promise { diff --git a/src/server/types.ts b/src/server/types.ts index fdcba484a9..f222ed5b95 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -263,6 +263,7 @@ export type BrowserContextOptions = { path: string }, proxy?: ProxySettings, + baseURL?: string, _debugName?: string, }; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 03b1e7028f..ae0eb6c1f1 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -313,3 +313,11 @@ export function getUserAgent() { const packageJson = require('./../../package.json'); return `Playwright/${packageJson.version} (${os.arch()}/${os.platform()}/${os.release()})`; } + +export function constructURLBasedOnBaseURL(baseURL: string | undefined, givenURL: string): string { + try { + return (new URL.URL(givenURL, baseURL)).toString(); + } catch (e) { + return givenURL; + } +} diff --git a/tests/browsercontext-base-url.spec.ts b/tests/browsercontext-base-url.spec.ts new file mode 100644 index 0000000000..db2fdc6fbc --- /dev/null +++ b/tests/browsercontext-base-url.spec.ts @@ -0,0 +1,93 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * Modifications copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { browserTest as it, expect } from './config/browserTest'; + +it('should construct a new URL when a baseURL in browser.newContext is passed to page.goto', async function({browser, server}) { + const context = await browser.newContext({ + baseURL: server.PREFIX, + }); + const page = await context.newPage(); + expect((await page.goto('/empty.html')).url()).toBe(server.EMPTY_PAGE); + await context.close(); +}); + +it('should construct a new URL when a baseURL in browser.newPage is passed to page.goto', async function({browser, server}) { + const page = await browser.newPage({ + baseURL: server.PREFIX, + }); + expect((await page.goto('/empty.html')).url()).toBe(server.EMPTY_PAGE); + await page.close(); +}); + +it('should construct the URLs correctly when a baseURL without a trailing slash in browser.newPage is passed to page.goto', async function({browser, server}) { + const page = await browser.newPage({ + baseURL: server.PREFIX + '/url-construction', + }); + expect((await page.goto('mypage.html')).url()).toBe(server.PREFIX + '/mypage.html'); + expect((await page.goto('./mypage.html')).url()).toBe(server.PREFIX + '/mypage.html'); + expect((await page.goto('/mypage.html')).url()).toBe(server.PREFIX + '/mypage.html'); + await page.close(); +}); + +it('should construct the URLs correctly when a baseURL with a trailing slash in browser.newPage is passed to page.goto', async function({browser, server}) { + const page = await browser.newPage({ + baseURL: server.PREFIX + '/url-construction/', + }); + expect((await page.goto('mypage.html')).url()).toBe(server.PREFIX + '/url-construction/mypage.html'); + expect((await page.goto('./mypage.html')).url()).toBe(server.PREFIX + '/url-construction/mypage.html'); + expect((await page.goto('/mypage.html')).url()).toBe(server.PREFIX + '/mypage.html'); + expect((await page.goto('.')).url()).toBe(server.PREFIX + '/url-construction/'); + expect((await page.goto('/')).url()).toBe(server.PREFIX + '/'); + await page.close(); +}); + +it('should not construct a new URL when valid URLs are passed', async function({browser, server}) { + const page = await browser.newPage({ + baseURL: 'http://microsoft.com', + }); + expect((await page.goto(server.EMPTY_PAGE)).url()).toBe(server.EMPTY_PAGE); + + await page.goto('data:text/html,Hello world'); + expect(await page.evaluate(() => window.location.href)).toBe('data:text/html,Hello world'); + + await page.goto('about:blank'); + expect(await page.evaluate(() => window.location.href)).toBe('about:blank'); + await page.close(); +}); + +it('should be able to match a URL relative to its given URL with urlMatcher', async function({browser, server}) { + const page = await browser.newPage({ + baseURL: server.PREFIX + '/foobar/', + }); + await page.goto('/kek/index.html'); + await page.waitForURL('/kek/index.html'); + expect(page.url()).toBe(server.PREFIX + '/kek/index.html'); + + await page.route('./kek/index.html', route => route.fulfill({ + body: 'base-url-matched-route', + })); + const [request, response] = await Promise.all([ + page.waitForRequest('./kek/index.html'), + page.waitForResponse('./kek/index.html'), + page.goto('./kek/index.html'), + ]); + expect(request.url()).toBe(server.PREFIX + '/foobar/kek/index.html'); + expect(response.url()).toBe(server.PREFIX + '/foobar/kek/index.html'); + expect((await response.body()).toString()).toBe('base-url-matched-route'); + await page.close(); +}); diff --git a/types/types.d.ts b/types/types.d.ts index 99505cabec..9391053fef 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -1945,7 +1945,8 @@ export interface Page { * [upstream issue](https://bugs.chromium.org/p/chromium/issues/detail?id=761295). * * Shortcut for main frame's [frame.goto(url[, options])](https://playwright.dev/docs/api/class-frame#frame-goto) - * @param url URL to navigate page to. The url should include scheme, e.g. `https://`. + * @param url URL to navigate page to. The url should include scheme, e.g. `https://`. 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. * @param options */ goto(url: string, options?: { @@ -2437,7 +2438,8 @@ export interface Page { * [page.unroute(url[, handler])](https://playwright.dev/docs/api/class-page#page-unroute). * * > NOTE: Enabling routing disables http cache. - * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. + * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. 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. * @param handler handler function to route the request. */ route(url: string|RegExp|((url: URL) => boolean), handler: ((route: Route, request: Request) => void)): Promise; @@ -3166,7 +3168,7 @@ export interface Page { }): Promise; /** - * Waits for the matching request and returns it. See [waiting for event](https://playwright.dev/docs/events#waiting-for-event) for more details + * Waits for the matching request and returns it. See [waiting for event](https://playwright.dev/docs/events#waiting-for-event) for more details * about events. * * ```js @@ -3222,7 +3224,8 @@ export interface Page { * ]); * ``` * - * @param urlOrPredicate Request URL string, regex or predicate receiving [Response] object. + * @param urlOrPredicate Request URL string, regex or predicate receiving [Response] object. 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. * @param options */ waitForResponse(urlOrPredicate: string|RegExp|((response: Response) => boolean|Promise), options?: { @@ -5481,7 +5484,8 @@ export interface BrowserContext { * [browserContext.unroute(url[, handler])](https://playwright.dev/docs/api/class-browsercontext#browser-context-unroute). * * > NOTE: Enabling routing disables http cache. - * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. + * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. 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. * @param handler handler function to route the request. */ route(url: string|RegExp|((url: URL) => boolean), handler: ((route: Route, request: Request) => void)): Promise; @@ -7014,6 +7018,19 @@ export interface BrowserType { */ args?: Array; + /** + * When using [page.goto(url[, options])](https://playwright.dev/docs/api/class-page#page-goto), + * [page.route(url, handler)](https://playwright.dev/docs/api/class-page#page-route), + * [page.waitForURL(url[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-url), + * [page.waitForRequest(urlOrPredicate[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-request), or + * [page.waitForResponse(urlOrPredicate[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-response) it + * takes the base URL in consideration by using the [`URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) + * constructor for building the corresponding URL. Examples: + * - baseURL: `http://localhost:3000` and navigating to `/bar.html` results in `http://localhost:3000/bar.html` + * - baseURL: `http://localhost:3000/foo/` and navigating to `./bar.html` results in `http://localhost:3000/foo/bar.html` + */ + baseURL?: string; + /** * Toggles bypassing page's Content-Security-Policy. */ @@ -8138,6 +8155,19 @@ export interface AndroidDevice { */ acceptDownloads?: boolean; + /** + * When using [page.goto(url[, options])](https://playwright.dev/docs/api/class-page#page-goto), + * [page.route(url, handler)](https://playwright.dev/docs/api/class-page#page-route), + * [page.waitForURL(url[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-url), + * [page.waitForRequest(urlOrPredicate[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-request), or + * [page.waitForResponse(urlOrPredicate[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-response) it + * takes the base URL in consideration by using the [`URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) + * constructor for building the corresponding URL. Examples: + * - baseURL: `http://localhost:3000` and navigating to `/bar.html` results in `http://localhost:3000/bar.html` + * - baseURL: `http://localhost:3000/foo/` and navigating to `./bar.html` results in `http://localhost:3000/foo/bar.html` + */ + baseURL?: string; + /** * Toggles bypassing page's Content-Security-Policy. */ @@ -8896,6 +8926,19 @@ export interface Browser extends EventEmitter { */ acceptDownloads?: boolean; + /** + * When using [page.goto(url[, options])](https://playwright.dev/docs/api/class-page#page-goto), + * [page.route(url, handler)](https://playwright.dev/docs/api/class-page#page-route), + * [page.waitForURL(url[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-url), + * [page.waitForRequest(urlOrPredicate[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-request), or + * [page.waitForResponse(urlOrPredicate[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-response) it + * takes the base URL in consideration by using the [`URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) + * constructor for building the corresponding URL. Examples: + * - baseURL: `http://localhost:3000` and navigating to `/bar.html` results in `http://localhost:3000/bar.html` + * - baseURL: `http://localhost:3000/foo/` and navigating to `./bar.html` results in `http://localhost:3000/foo/bar.html` + */ + baseURL?: string; + /** * Toggles bypassing page's Content-Security-Policy. */ @@ -11013,6 +11056,19 @@ export interface BrowserContextOptions { */ acceptDownloads?: boolean; + /** + * When using [page.goto(url[, options])](https://playwright.dev/docs/api/class-page#page-goto), + * [page.route(url, handler)](https://playwright.dev/docs/api/class-page#page-route), + * [page.waitForURL(url[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-url), + * [page.waitForRequest(urlOrPredicate[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-request), or + * [page.waitForResponse(urlOrPredicate[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-response) it + * takes the base URL in consideration by using the [`URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) + * constructor for building the corresponding URL. Examples: + * - baseURL: `http://localhost:3000` and navigating to `/bar.html` results in `http://localhost:3000/bar.html` + * - baseURL: `http://localhost:3000/foo/` and navigating to `./bar.html` results in `http://localhost:3000/foo/bar.html` + */ + baseURL?: string; + /** * Toggles bypassing page's Content-Security-Policy. */