URLs and navigation
Introduction
Section titled “Introduction”Playwright resolves a relative page.goto("/login") against the project’s
baseURL, and waits for a URL with
page.waitForURL(). POMWright gives every page object
its own base, the baseUrl, and its own path, the urlPath, so a suite that spans several sites needs no shared
setting, and builds a navigation helper from them: goto to go there, expectThisPage and expectAnotherPage to wait
until the browser is there or has left. Each is one Playwright call, with Playwright’s waits and timeouts.
baseUrl and urlPath
Section titled “baseUrl and urlPath”A string baseUrl is an origin: scheme, host and an optional port, with or without a trailing slash, and nothing
after it (https://shop.example, http://localhost:9000). A string urlPath is "" for the origin itself, or starts
with exactly one / (/login, /orders?tab=open). The constructor checks both, so a value from the environment is
checked like a literal:
LoginPage: baseUrl must be an origin such as "https://example.com" or "http://localhost:9000" (scheme, host, optional port, no path, query or hash); received "shop.example".LoginPage: urlPath must be "" or start with exactly one "/"; received "login".LoginPage: baseUrl must be a string or a RegExp; received undefined.localhost:9000 without a scheme and a base with a path are rejected: Playwright could not resolve paths against the
first, and would drop the path of the second for every path that starts with /. A path that starts with // would
name another host.
fullUrl
Section titled “fullUrl”For two strings, fullUrl is new URL(urlPath, baseUrl).href, the URL in the form the browser reports it:
baseUrl |
urlPath |
fullUrl |
|---|---|---|
"https://shop.example" |
"" |
https://shop.example/ |
"https://shop.example/" |
"/login" |
https://shop.example/login |
"https://shop.example" |
"/søk" |
https://shop.example/s%C3%B8k |
"http://localhost:9000" |
"/orders?tab=1" |
http://localhost:9000/orders?tab=1 |
Navigating
Section titled “Navigating”Every page object has a navigation helper:
| method | does |
|---|---|
goto(options?) |
page.goto(fullUrl), then the page object’s actions after navigation |
goto(target, options?) |
page.goto to target, resolved against baseUrl exactly as Playwright resolves against baseURL (goto("/account"), goto("?tab=2")), and an absolute URL as it is, on any domain (goto("https://help.example/faq")); never runs the actions |
expectThisPage(options?) |
one page.waitForURL for this page’s URL, then the actions |
expectAnotherPage(options?) |
one page.waitForURL for any other URL, then a check that the browser did not come straight back |
import { test } from "./fixtures";
test("the account link", async ({ loginPage }) => { await loginPage.navigation.goto(); await loginPage.logIn("ada@shop.example", "correct horse"); await loginPage.navigation.expectAnotherPage(); await loginPage.navigation.goto("/account");});As with page.goto, a complete URL is used as it is, so goto(target) also leaves the page object’s origin:
loginPage.navigation.goto("https://help.example/faq") goes from https://shop.example to the help site.
A page’s identity is its exact URL: /orders?tab=1 is not /orders for expectThisPage, and counts as another page
for expectAnotherPage. When the URL is already right, page.waitForURL returns at once, after the requested load
state, so calling expectThisPage() after a navigation that has already landed is fine. A goto(target) whose target
happens to equal fullUrl is ordinary navigation and does not run the actions.
Each call is a step of the report (LoginPage: Navigate to this Page, LoginPage: Expect this Page, …), shown at the
line that called it.
Options and timeouts
Section titled “Options and timeouts”type NavigationOptions = { waitUntil?: "load" | "domcontentloaded" | "networkidle" | "commit"; timeout?: number;};goto() and expectThisPage() also take runPostNavigationActions: false. Each option comes from the call, then
from the page object’s navOptions, then from Playwright: waitUntil defaults to "load", and timeout to the
project’s navigationTimeout.
POMWright sets no timeout or load state of its own, and every method makes one waiting call, so its limit is one
timeout:
import type { Page } from "@playwright/test";import { PageObject } from "pomwright";
export class OrdersPage extends PageObject<"main"> { constructor(page: Page) { super(page, "https://shop.example", "/orders", { navOptions: { waitUntil: "domcontentloaded", timeout: 15_000 }, }); }
protected defineLocators(): void { this.add("main").locator("main"); }}A wait that times out names the page object, what it expected and the URL found, with Playwright’s error as cause:
OrdersPage: expected URL "https://shop.example/orders"; found "https://shop.example/orders?tab=1" (page.waitForURL: Timeout 15000ms exceeded.)OrdersPage: expected to have left "https://shop.example/orders"; found "https://shop.example/orders" (page.waitForURL: Timeout 15000ms exceeded.)OrdersPage: left "https://shop.example/orders" but returned to it; found "https://shop.example/orders"Pages whose URL varies
Section titled “Pages whose URL varies”A baseUrl or urlPath may be a RegExp: an account page at /account/42, or a login page on an identity provider
whose host changes between environments. Declare which in the page object’s second type argument:
import type { Page } from "@playwright/test";import { PageObject } from "pomwright";
export class AccountPage extends PageObject<"main", { urlPathType: RegExp }> { constructor(page: Page) { super(page, "https://shop.example", /^\/account\/\d+$/); }
protected defineLocators(): void { this.add("main").locator("main"); }}fullUrl is then a UrlMatcher, which tests the two parts of a URL separately, each part as written, with its own
flags; a string part must match exactly:
flowchart LR url["https://login.identity-provider.example/authorize?client=shop"] --> origin["the origin<br/>https://login.identity-provider.example"] url --> rest["the rest: path, query, hash<br/>/authorize?client=shop"] origin -->|tested by| base["baseUrl"] rest -->|tested by| path["urlPath"]
baseUrl + urlPath |
matches https://login.identity-provider.example/authorize?client=shop |
|---|---|
/identity-provider/ + /\/authorize/ |
yes |
/identity-provider/ + "/authorize" |
no: a string path is exact, and the query follows |
/\.example$/ + /^\/authorize\?client=\w+$/ |
yes |
/authorize/ + /\/authorize/ |
no: the base only sees the origin |
"https://login.identity-provider.example/" + /\/authorize/ |
yes |
So ^ and $ on a base refer to the origin, a path prefix belongs in urlPath, and $ on a base includes the port
(/example(?::\d+)?$/ allows one). A UrlMatcher works wherever Playwright takes a URL predicate:
page.waitForURL(fullUrl), expect(page).toHaveURL(fullUrl), page.route(fullUrl, …). It also has test(url),
base, path, and a readable toString() (origin "https://shop.example" + path /^\/account\/\d+$/).
Such a page has no single URL to go to, so goto() without a target is not available (a compile error, and from
JavaScript: AccountPage: goto() without a URL needs a string fullUrl; this page object has a RegExp URL. Pass an absolute URL instead.). goto(target) still works; on a RegExp baseUrl the target must be absolute.
A page whose query varies, /orders?tab=2 and /orders?tab=3 alike, is such a page too:
urlPath: /^\/orders\?tab=\d+$/. A page with one fixed query is a string: urlPath: "/orders?tab=2".
Reference
Section titled “Reference”- Navigation:
NavigationFor,NavigationOptions,ThisPageOptions,UrlMatcher PageObjectconstructor- Playwright:
baseURL,page.goto(),page.waitForURL(),toHaveURL()