From e41b21dc7b706672d1a0674bf005a4abcb716f60 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 11 May 2023 09:56:48 -0700 Subject: [PATCH] chore: update navigation and timers docs (#22941) --- docs/src/api/class-frame.md | 2 + docs/src/api/class-page.md | 6 + docs/src/navigations.md | 535 ++++++---------------- docs/src/test-timeouts-js.md | 84 ++-- packages/playwright-core/types/types.d.ts | 9 + 5 files changed, 200 insertions(+), 436 deletions(-) diff --git a/docs/src/api/class-frame.md b/docs/src/api/class-frame.md index 33f8a2df5e..097c6cc0fd 100644 --- a/docs/src/api/class-frame.md +++ b/docs/src/api/class-frame.md @@ -2243,6 +2243,8 @@ class FrameExamples ## async method: Frame.waitForTimeout * since: v1.8 +* discouraged: Never wait for timeout in production. Tests that wait for time are + inherently flaky. Use [Locator] actions and web assertions that wait automatically. Waits for the given [`param: timeout`] in milliseconds. diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index cffd8be27a..b35458eac0 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -584,6 +584,12 @@ Math.random = () => 42; await page.addInitScript({ path: './preload.js' }); ``` +```js +await page.addInitScript(mock => { + window.mock = mock; +}, mock); +``` + ```java // In your playwright script, assuming the preload.js file is in same directory page.addInitScript(Paths.get("./preload.js")); diff --git a/docs/src/navigations.md b/docs/src/navigations.md index 1f941f42a4..a833d5bd68 100644 --- a/docs/src/navigations.md +++ b/docs/src/navigations.md @@ -3,9 +3,152 @@ id: navigations title: "Navigations" --- -Playwright can navigate to URLs and handle navigations caused by page interactions. This guide covers common scenarios to wait for page navigations and loading to complete. +Playwright can navigate to URLs and handle navigations caused by the page interactions. -## Navigation lifecycle +## Basic navigation + +Simplest form of a navigation is opening a URL: + +```js +// Navigate the page +await page.goto('https://example.com'); +``` + +```java +// Navigate the page +page.navigate("https://example.com"); +``` + +```python async +# Navigate the page +await page.goto("https://example.com") +``` + +```python sync +# Navigate the page +page.goto("https://example.com") +``` + +```csharp +// Navigate the page +await page.GotoAsync("https://example.com"); +``` + +The code above loads the page and waits for the web page to fire the +[load](https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event) event. +The load event is fired when the whole page has loaded, including all dependent +resources such as stylesheets, scripts, iframes, and images. + +:::note +If the page does a client-side redirect before `load`, [`method: Page.goto`] will +wait for the redirected page to fire the `load` event. +::: + +## When is the page loaded? + +Modern pages perform numerous activities after the `load` event was fired. They +fetch data lazily, populate UI, load expensive resources, scripts and styles after +the `load` event was fired. There is no way to tell that the page is `loaded`, +it depends on the page, framework, etc. So when can you start interacting with +it? + +In Playwright you can interact with the page at any moment. It will automatically +wait for the target elements to become [actionable](./actionability.md). + +```js +// Navigate and click element +// Click will auto-wait for the element +await page.goto('https://example.com'); +await page.getByText('Example Domain').click(); +``` + +```java +// Navigate and click element +// Click will auto-wait for the element +page.navigate("https://example.com"); +page.getByText("Example Domain").click(); +``` + +```python async +# Navigate and click element +# Click will auto-wait for the element +await page.goto("https://example.com") +await page.get_by_text("example domain").click() +``` + +```python sync +# Navigate and click element +# Click will auto-wait for the element +page.goto("https://example.com") +page.get_by_text("example domain").click() +``` + +```csharp +// Navigate and click element +// Click will auto-wait for the element +await page.GotoAsync("https://example.com"); +await page.GetByText("Example Domain").ClickAsync(); +``` + +For the scenario above, Playwright will wait for the text to become visible, +will wait for the rest of the actionability checks to pass for that element, +and will click it. + +Playwright operates as a very fast user - the moment it sees the button, it +clicks it. In the general case, you don't need to worry about whether all the +resources loaded, etc. + +## Hydration + +At some point in time, you'll stumble upon a use case where Playwright performs +an action, but nothing seemingly happens. Or you enter some text into the input +field and will disappear. The most probable reason behind that is a poor page +[hydration](https://en.wikipedia.org/wiki/Hydration_(web_development)). + +When page is hydrated, first, a static version of the page is sent to the browser. +Then the dynamic part is sent and the page becomes "live". As a very fast user, +Playwright will start interacting with the page the moment it sees it. And if +the button on a page is enabled, but the listeners have not yet been added, +Playwright will do its job, but the click won't have any effect. + +A simple way to verify if your page suffers from a poor hydration is to open Chrome +DevTools, pick "Slow 3G" network emulation in the Network panel and reload the page. +Once you see the element of interest, interact with it. You'll see that the button +clicks will be ignored and the entered text will be reset by the subsequent page +load code. The right fix for this issue is to make sure that all the interactive +controls are disabled until after the hydration, when the page is fully functional. + +## Waiting for navigation + +Clicking an element could trigger multiple navigations. In these cases, it is +recommended to explicitly [`method: Page.waitForURL`] to a specific url. + +```js +await page.getByText('Click me').click(); +await page.waitForURL('**/login'); +``` + +```java +page.getByText("Click me").click(); +page.waitForURL("**/login"); +``` + +```python async +await page.get_by_text("Click me").click() +await page.wait_for_url("**/login") +``` + +```python sync +page.get_by_text("Click me").click() +page.wait_for_url("**/login") +``` + +```csharp +await page.GetByText("Click me").ClickAsync(); +await page.WaitForURL("**/login"); +``` + +## Navigation events Playwright splits the process of showing a new document in a page into **navigation** and **loading**. @@ -23,391 +166,3 @@ events: - page executes some scripts and loads resources like stylesheets and images - [`event: Page.load`] event is fired - page executes dynamically loaded scripts - -## Scenarios initiated by browser UI - -Navigations can be initiated by changing the URL bar, reloading the page or going back or forward in session history. - -### Auto-wait - -Navigating to a URL auto-waits for the page to fire the `load` event. If the page does a client-side redirect before -`load`, [`method: Page.goto`] will auto-wait for the redirected page to fire the `load` event. - -```js -// Navigate the page -await page.goto('https://example.com'); -``` - -```java -// Navigate the page -page.navigate("https://example.com"); -``` - -```python async -# Navigate the page -await page.goto("https://example.com") -``` - -```python sync -# Navigate the page -page.goto("https://example.com") -``` - -```csharp -// Navigate the page -await page.GotoAsync("https://example.com"); -``` - -### Wait for element - -In lazy-loaded pages, it can be useful to wait until an element is visible with [`method: Locator.waitFor`]. -Alternatively, page interactions like [`method: Page.click`] auto-wait for elements. - -```js -// Navigate and wait for element -await page.goto('https://example.com'); -await page.getByText('Example Domain').waitFor(); - -// Navigate and click element -// Click will auto-wait for the element -await page.goto('https://example.com'); -await page.getByText('Example Domain').click(); -``` - -```java -// Navigate and wait for element -page.navigate("https://example.com"); -page.getByText("Example Domain").waitFor(); - -// Navigate and click element -// Click will auto-wait for the element -page.navigate("https://example.com"); -page.getByText("Example Domain").click(); -``` - -```python async -# Navigate and wait for element -await page.goto("https://example.com") -await page.get_by_text("example domain").wait_for() - -# Navigate and click element -# Click will auto-wait for the element -await page.goto("https://example.com") -await page.get_by_text("example domain").click() -``` - -```python sync -# Navigate and wait for element -page.goto("https://example.com") -page.get_by_text("example domain").wait_for() - -# Navigate and click element -# Click will auto-wait for the element -page.goto("https://example.com") -page.get_by_text("example domain").click() -``` - -```csharp -// Navigate and wait for element -await page.GotoAsync("https://example.com"); -await page.GetByText("Example Domain").WaitForAsync(); - -// Navigate and click element -// Click will auto-wait for the element -await page.GotoAsync("https://example.com"); -await page.GetByText("Example Domain").ClickAsync(); -``` - -## Scenarios initiated by page interaction - -In the scenarios below, [`method: Locator.click`] initiates a navigation and then waits for the navigation to complete. - -### Auto-wait - -By default, [`method: Locator.click`] will wait for the navigation step to complete. This can be combined with a page interaction on the navigated page which would auto-wait for an element. - -```js -// Click will auto-wait for navigation to complete -await page.getByText('Login').click(); - -// Fill will auto-wait for element on navigated page -await page.getByLabel('User Name').fill('John Doe'); -``` - -```java -// Click will auto-wait for navigation to complete -page.getByText("Login").click(); - -// Fill will auto-wait for element on navigated page -page.getByLabel("User Name").fill("John Doe"); -``` - -```python async -# Click will auto-wait for navigation to complete -await page.get_by_text("Login").click() - -# Fill will auto-wait for element on navigated page -await page.get_by_label("User Name").fill("John Doe") -``` - -```python sync -# Click will auto-wait for navigation to complete -page.get_by_text("Login").click() - -# Fill will auto-wait for element on navigated page -page.get_by_label("User Name").fill("John Doe") -``` - -```csharp -// Click will auto-wait for navigation to complete -await page.GetByText("Login").ClickAsync(); - -// Fill will auto-wait for element on navigated page -await page.GetByLabel("User Name").FillAsync("John Doe"); -``` - -### Wait for element - -In lazy-loaded pages, it can be useful to wait until an element is visible with [`method: Locator.waitFor`]. -Alternatively, page interactions like [`method: Locator.click`] auto-wait for elements. - -```js -// Click will auto-wait for the element and trigger navigation -await page.getByText('Login').click(); -// Wait for the element -await page.getByLabel('User Name').waitFor(); - -// Click triggers navigation -await page.getByText('Login').click(); -// Fill will auto-wait for element -await page.getByLabel('User Name').fill('John Doe'); -``` - -```java -// Click will auto-wait for the element and trigger navigation -page.getByText("Login").click(); -// Wait for the element -page.getByLabel("User Name").waitFor(); - -// Click triggers navigation -page.getByText("Login").click(); -// Fill will auto-wait for element -page.getByLabel("User Name").fill("John Doe"); -``` - -```python async -# Click will auto-wait for the element and trigger navigation -await page.get_by_text("Login").click() -# Wait for the element -await page.get_by_label("User Name").wait_for() - -# Click triggers navigation -await page.get_by_text("Login").click() -# Fill will auto-wait for element -await page.get_by_label("User Name").fill("John Doe") -``` - -```python sync -# Click triggers navigation -page.get_by_text("Login").click() -# Click will auto-wait for the element -page.get_by_label("User Name").wait_for() - -# Click triggers navigation -page.get_by_text("Login").click() -# Fill will auto-wait for element -page.get_by_label("User Name").fill("John Doe") -``` - -```csharp -// Click will auto-wait for the element and trigger navigation -await page.GetByText("Login").ClickAsync(); -// Wait for the element -await page.GetByLabel("User Name").WaitForAsync(); - -// Click triggers navigation -await page.GetByText("Login").ClickAsync(); -// Fill will auto-wait for element -await page.GetByLabel("User Name").FillAsync("John Doe"); -``` - -### Asynchronous navigation - -Clicking an element could trigger asynchronous processing before initiating the navigation. In these cases, it is -recommended to explicitly call [`method: Page.waitForNavigation`]. For example: -* Navigation is triggered from a `setTimeout` -* Page waits for network requests before navigation - -```js -// Start waiting for navigation before clicking. Note no await. -const navigationPromise = page.waitForNavigation(); -await page.getByText('Navigate after timeout').click(); -await navigationPromise; -``` - -```java -// Using waitForNavigation with a callback prevents a race condition -// between clicking and waiting for a navigation. -page.waitForNavigation(() -> { // Waits for the next navigation - page.getByText("Navigate after timeout").click(); // Triggers a navigation after a timeout -}); -``` - -```python async -# Waits for the next navigation. Using Python context manager -# prevents a race condition between clicking and waiting for a navigation. -async with page.expect_navigation(): - # Triggers a navigation after a timeout - await page.get_by_text("Navigate after timeout").click() -``` - -```python sync -# Waits for the next navigation. Using Python context manager -# prevents a race condition between clicking and waiting for a navigation. -with page.expect_navigation(): - # Triggers a navigation after a timeout - page.get_by_text("Navigate after timeout").click() -``` - -```csharp -// Using waitForNavigation with a callback prevents a race condition -// between clicking and waiting for a navigation. -await page.RunAndWaitForNavigationAsync(async () => -{ - // Triggers a navigation after a timeout - await page.GetByText("Navigate after timeout").ClickAsync(); -}); -``` - -### Multiple navigations - -Clicking an element could trigger multiple navigations. In these cases, it is recommended to explicitly -[`method: Page.waitForNavigation`] to a specific url. For example: -* Client-side redirects issued after the `load` event -* Multiple pushes to history state - -```js -// Start waiting for navigation before clicking. Note no await. -const navigationPromise = page.waitForNavigation({ url: '**/login' }); -// This action triggers the navigation with a script redirect. -await page.getByText('Click me').click(); -await navigationPromise; -``` - -```java -// Running action in the callback of waitForNavigation prevents a race -// condition between clicking and waiting for a navigation. -page.waitForNavigation(new Page.WaitForNavigationOptions().setUrl("**/login"), () -> { - page.getByText("Click me").click(); // Triggers a navigation with a script redirect -}); -``` - -```python async -# Using Python context manager prevents a race condition -# between clicking and waiting for a navigation. -async with page.expect_navigation(url="**/login"): - # Triggers a navigation with a script redirect - await page.get_by_text("Click me").click() -``` - -```python sync -# Using Python context manager prevents a race condition -# between clicking and waiting for a navigation. -with page.expect_navigation(url="**/login"): - # Triggers a navigation with a script redirect - page.get_by_text("Click me").click() -``` - -```csharp -// Running action in the callback of waitForNavigation prevents a race -// condition between clicking and waiting for a navigation. -await page.RunAndWaitForNavigationAsync(async () => -{ - // Triggers a navigation with a script redirect. - await page.GetByText("Click me").ClickAsync(); -}, new() -{ - UrlString = "**/login" -}); -``` - -### Loading a popup - -When popup is opened, explicitly calling [`method: Page.waitForLoadState`] ensures that popup is loaded to the desired state. - -```js -// Start waiting for popup before clicking. Note no await. -const popupPromise = page.waitForEvent('popup'); -await page.getByText('Open popup').click(); -const popup = await popupPromise; -// Wait for the popup to load. -await popup.waitForLoadState('load'); -``` - -```java -Page popup = page.waitForPopup(() -> { - page.getByText("Open popup").click(); // Opens popup -}); -popup.waitForLoadState(LoadState.LOAD); -``` - -```python async -async with page.expect_popup() as popup_info: - await page.get_by_text("Open popup").click() # Opens popup -popup = await popup_info.value -await popup.wait_for_load_state("load") -``` - -```python sync -with page.expect_popup() as popup_info: - page.get_by_text("Open popup").click() # Opens popup -popup = popup_info.value -popup.wait_for_load_state("load") -``` - -```csharp -var popup = await page.RunAndWaitForPopupAsync(async () => -{ - await page.GetByText("Open popup").ClickAsync(); // Opens popup -}); -popup.WaitForLoadStateAsync(LoadState.Load); -``` - -## Advanced patterns - -For pages that have complicated loading patterns, [`method: Page.waitForFunction`] is a powerful and extensible approach to define a custom wait criteria. - -```js -await page.goto('http://example.com'); -await page.waitForFunction(() => window.amILoadedYet()); -// Ready to take a screenshot, according to the page itself. -await page.screenshot(); -``` - -```java -page.navigate("http://example.com"); -page.waitForFunction("() => window.amILoadedYet()"); -// Ready to take a screenshot, according to the page itself. -page.screenshot(); -``` - -```python async -await page.goto("http://example.com") -await page.wait_for_function("() => window.amILoadedYet()") -# Ready to take a screenshot, according to the page itself. -await page.screenshot() -``` - -```python sync -page.goto("http://example.com") -page.wait_for_function("() => window.amILoadedYet()") -# Ready to take a screenshot, according to the page itself. -page.screenshot() -``` - -```csharp -await page.GotoAsync("http://example.com"); -await page.WaitForFunctionAsync("() => window.amILoadedYet()"); -// Ready to take a screenshot, according to the page itself. -await page.ScreenshotAsync(); -``` diff --git a/docs/src/test-timeouts-js.md b/docs/src/test-timeouts-js.md index 4e8682c071..88f71bbb4e 100644 --- a/docs/src/test-timeouts-js.md +++ b/docs/src/test-timeouts-js.md @@ -9,11 +9,6 @@ Playwright Test has multiple configurable timeouts for various tasks. |:----------|:----------------|:--------------------------------| |Test timeout|30000 ms|Timeout for each test, includes test, hooks and fixtures:
Set default
{`config = { timeout: 60000 }`}
Override
`test.setTimeout(120000)` | |Expect timeout|5000 ms|Timeout for each assertion:
Set default
{`config = { expect: { timeout: 10000 } }`}
Override
`expect(locator).toBeVisible({ timeout: 10000 })` | -|Action timeout| no timeout |Timeout for each action:
Set default
{`config = { use: { actionTimeout: 10000 } }`}
Override
`locator.click({ timeout: 10000 })` | -|Navigation timeout| no timeout |Timeout for each navigation action:
Set default
{`config = { use: { navigationTimeout: 30000 } }`}
Override
`page.goto('/', { timeout: 30000 })` | -|Global timeout|no timeout |Global timeout for the whole test run:
Set in config
`config = { globalTimeout: 60*60*1000 }`
| -|`beforeAll`/`afterAll` timeout|30000 ms|Timeout for the hook:
Set in hook
`test.setTimeout(60000)`
| -|Fixture timeout|no timeout |Timeout for an individual fixture:
Set in fixture
`{ scope: 'test', timeout: 30000 }`
| ## Test timeout @@ -115,7 +110,44 @@ export default defineConfig({ }); ``` -API reference: [`property: TestConfig.expect`]. +## Global timeout + +Playwright Test supports a timeout for the whole test run. This prevents excess resource usage when everything went wrong. There is no default global timeout, but you can set a reasonable one in the config, for example one hour. Global timeout produces the following error: + +``` +Running 1000 tests using 10 workers + + 514 skipped + 486 passed + Timed out waiting 3600s for the entire test run +``` + +You can set global timeout in the config. + +```js +// playwright.config.ts +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + globalTimeout: 60 * 60 * 1000, +}); +``` + +API reference: [`property: TestConfig.globalTimeout`]. + +## Advanced: low level timeouts + +These are the low-level timeouts that are pre-configured by the test runner, you should not need to change these. +If you happen to be in this section because your test are flaky, it is very likely that you should be looking for the solution elsewhere. + +|Timeout |Default |Description | +|:----------|:----------------|:--------------------------------| +|Action timeout| no timeout |Timeout for each action:
Set default
{`config = { use: { actionTimeout: 10000 } }`}
Override
`locator.click({ timeout: 10000 })` | +|Navigation timeout| no timeout |Timeout for each navigation action:
Set default
{`config = { use: { navigationTimeout: 30000 } }`}
Override
`page.goto('/', { timeout: 30000 })` | +|Global timeout|no timeout |Global timeout for the whole test run:
Set in config
`config = { globalTimeout: 60*60*1000 }`
| +|`beforeAll`/`afterAll` timeout|30000 ms|Timeout for the hook:
Set in hook
`test.setTimeout(60000)`
| +|Fixture timeout|no timeout |Timeout for an individual fixture:
Set in fixture
`{ scope: 'test', timeout: 30000 }`
| + ### Set timeout for a single assertion @@ -126,22 +158,6 @@ test('basic test', async ({ page }) => { await expect(page.getByRole('button')).toHaveText('Sign in', { timeout: 10000 }); }); ``` - -## Action and navigation timeouts - -Test usually performs some actions by calling Playwright APIs, for example `locator.click()`. These actions do not have a timeout by default, but you can set one. Action that timed out produces the following error: - -``` -example.spec.ts:3:1 › basic test =========================== - -locator.click: Timeout 1000ms exceeded. -=========================== logs =========================== -waiting for "locator('button')" -============================================================ -``` - -Playwright also allows to set a separate timeout for navigation actions like `page.goto()` because loading a page is usually slower. - ### Set action and navigation timeouts in the config ```js title="playwright.config.ts" @@ -168,30 +184,6 @@ test('basic test', async ({ page }) => { }); ``` -## Global timeout - -Playwright Test supports a timeout for the whole test run. This prevents excess resource usage when everything went wrong. There is no default global timeout, but you can set a reasonable one in the config, for example one hour. Global timeout produces the following error: - -``` -Running 1000 tests using 10 workers - - 514 skipped - 486 passed - Timed out waiting 3600s for the entire test run -``` - -You can set global timeout in the config. - -```js title="playwright.config.ts" -import { defineConfig } from '@playwright/test'; - -export default defineConfig({ - globalTimeout: 60 * 60 * 1000, -}); -``` - -API reference: [`property: TestConfig.globalTimeout`]. - ## Fixture timeout By default, [fixture](./test-fixtures) shares timeout with the test. However, for slow fixtures, especially [worker-scoped](./test-fixtures#worker-scoped-fixtures) ones, it is convenient to have a separate timeout. This way you can keep the overall test timeout small, and give the slow fixture more time. diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 7db8d1bdb1..2a7622eb96 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -276,6 +276,12 @@ export interface Page { * await page.addInitScript({ path: './preload.js' }); * ``` * + * ```js + * await page.addInitScript(mock => { + * window.mock = mock; + * }, mock); + * ``` + * * **NOTE** The order of evaluation of multiple scripts installed via * [browserContext.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script) * and [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) is not @@ -7289,6 +7295,9 @@ export interface Frame { }): Promise; /** + * **NOTE** Never wait for timeout in production. Tests that wait for time are inherently flaky. Use [Locator] actions and web + * assertions that wait automatically. + * * Waits for the given `timeout` in milliseconds. * * Note that `frame.waitForTimeout()` should only be used for debugging. Tests using the timer in production are going