feat: add browser.newContext({ baseUrl }) (#7409)

This commit is contained in:
Max Schmitt 2021-07-06 21:16:37 +02:00 committed by GitHub
parent b846ddda04
commit 371aa3dab2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 205 additions and 18 deletions

View File

@ -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

View File

@ -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]>

View File

@ -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-%%

View File

@ -127,7 +127,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
_onRoute(route: network.Route, request: network.Request) {
for (const {url, handler} of this._routes) {
if (urlMatches(request.url(), url)) {
if (urlMatches(this._options.baseURL, request.url(), url)) {
handler(route, request);
return;
}

View File

@ -17,7 +17,7 @@
import * as types from './types';
import fs from 'fs';
import { isString, isRegExp } from '../utils/utils';
import { isString, isRegExp, constructURLBasedOnBaseURL } from '../utils/utils';
const deprecatedHits = new Set();
export function deprecate(methodName: string, message: string) {
@ -65,9 +65,11 @@ export function parsedURL(url: string): URL | null {
}
}
export function urlMatches(urlString: string, match: types.URLMatch | undefined): boolean {
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))

View File

@ -118,7 +118,7 @@ export class Frame extends ChannelOwner<channels.FrameChannel, channels.FrameIni
if (event.error)
return true;
waiter.log(` navigated to "${event.url}"`);
return urlMatches(event.url, options.url);
return urlMatches(this._page?.context()._options.baseURL, event.url, options.url);
});
if (navigatedEvent.error) {
const e = new Error(navigatedEvent.error);
@ -155,7 +155,7 @@ export class Frame extends ChannelOwner<channels.FrameChannel, channels.FrameIni
}
async waitForURL(url: URLMatch, options: { waitUntil?: LifecycleEvent, timeout?: number } = {}): Promise<void> {
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 });
}

View File

@ -161,7 +161,7 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
private _onRoute(route: Route, request: Request) {
for (const {url, handler} of this._routes) {
if (urlMatches(request.url(), url)) {
if (urlMatches(this._browserContext._options.baseURL, request.url(), url)) {
handler(route, request);
return;
}
@ -216,7 +216,7 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
return this.frames().find(f => {
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<channels.PageChannel, channels.PageInitia
return this._wrapApiCall(async (channel: channels.PageChannel) => {
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<channels.PageChannel, channels.PageInitia
return this._wrapApiCall(async (channel: channels.PageChannel) => {
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);

View File

@ -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,

View File

@ -307,6 +307,7 @@ ContextOptions:
- reduce
- no-preference
acceptDownloads: boolean?
baseURL: string?
_debugName: string?
recordVideo:
type: object?

View File

@ -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,

View File

@ -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<network.Response | null> {
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<network.Response | null> {

View File

@ -263,6 +263,7 @@ export type BrowserContextOptions = {
path: string
},
proxy?: ProxySettings,
baseURL?: string,
_debugName?: string,
};

View File

@ -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;
}
}

View File

@ -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();
});

66
types/types.d.ts vendored
View File

@ -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<void>;
@ -3166,7 +3168,7 @@ export interface Page {
}): Promise<null|Response>;
/**
* 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<boolean>), 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<void>;
@ -7014,6 +7018,19 @@ export interface BrowserType<Unused = {}> {
*/
args?: Array<string>;
/**
* 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.
*/