From e85f27886970d7ff940589e834688e43b20a994a Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 14 Jan 2021 15:01:39 -0800 Subject: [PATCH] docs: more python docs and snippets (#5021) --- docs/src/api/class-accessibility.md | 10 +- docs/src/api/class-browser.md | 2 +- docs/src/api/class-browsercontext.md | 6 +- docs/src/api/class-cdpsession.md | 2 +- docs/src/api/class-page.md | 2 +- docs/src/api/python.md | 27 +++- docs/src/assertions.md | 2 +- docs/src/emulation.md | 55 ++++--- docs/src/extensibility.md | 68 ++++++++- docs/src/input.md | 10 +- docs/src/multi-pages.md | 174 ++++++++++++++++++--- docs/src/navigations.md | 216 ++++++++++++++++++++++++--- docs/src/network.md | 209 ++++++++++++++++++++++++-- docs/src/pom.md | 72 +++++++-- docs/src/selectors.md | 188 +++++++++++++++++++++++ docs/src/verification.md | 150 ++++++++++++++++++- types/types.d.ts | 18 +-- 17 files changed, 1079 insertions(+), 132 deletions(-) diff --git a/docs/src/api/class-accessibility.md b/docs/src/api/class-accessibility.md index ff6b0958b8..8359c5f13c 100644 --- a/docs/src/api/class-accessibility.md +++ b/docs/src/api/class-accessibility.md @@ -7,10 +7,10 @@ assistive technology such as [screen readers](https://en.wikipedia.org/wiki/Scre Accessibility is a very platform-specific thing. On different platforms, there are different screen readers that might have wildly different output. -Blink - Chromium's rendering engine - has a concept of "accessibility tree", which is then translated into different -platform-specific APIs. Accessibility namespace gives users access to the Blink Accessibility Tree. +Rendering engines of Chromium, Firefox and Webkit have a concept of "accessibility tree", which is then translated into different +platform-specific APIs. Accessibility namespace gives access to this Accessibility Tree. -Most of the accessibility tree gets filtered out when converting from Blink AX Tree to Platform-specific AX-Tree or by +Most of the accessibility tree gets filtered out when converting from internal browser AX Tree to Platform-specific AX-Tree or by assistive technologies themselves. By default, Playwright tries to approximate this filtering, exposing only the "interesting" nodes of the tree. @@ -98,7 +98,7 @@ def find_focused_node(node): snapshot = await page.accessibility.snapshot() node = find_focused_node(snapshot) if node: - print(node["name"]) + print(node["name"]) ``` ```python sync @@ -113,7 +113,7 @@ def find_focused_node(node): snapshot = page.accessibility.snapshot() node = find_focused_node(snapshot) if node: - print(node["name"]) + print(node["name"]) ``` ### option: Accessibility.snapshot.interestingOnly diff --git a/docs/src/api/class-browser.md b/docs/src/api/class-browser.md index 7a4772d101..8f5ebf5a3f 100644 --- a/docs/src/api/class-browser.md +++ b/docs/src/api/class-browser.md @@ -1,5 +1,5 @@ # class: Browser -* extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter) +* extends: [EventEmitter] A Browser is created via [`method: BrowserType.launch`]. An example of using a [Browser] to create a [Page]: diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 0ae739a63f..293bf2bf73 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -1,5 +1,5 @@ # class: BrowserContext -* extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter) +* extends: [EventEmitter] BrowserContexts provide a way to operate multiple independent browser sessions. @@ -567,7 +567,7 @@ await browser.close(); ```python async context = await browser.new_context() page = await context.new_page() -await context.route(r"(\.png$)|(\.jpg$)", lambda route => route.abort()) +await context.route(r"(\.png$)|(\.jpg$)", lambda route: route.abort()) page = await context.new_page() await page.goto("https://example.com") await browser.close() @@ -576,7 +576,7 @@ await browser.close() ```python sync context = browser.new_context() page = context.new_page() -context.route(r"(\.png$)|(\.jpg$)", lambda route => route.abort()) +context.route(r"(\.png$)|(\.jpg$)", lambda route: route.abort()) page = await context.new_page() page = context.new_page() page.goto("https://example.com") diff --git a/docs/src/api/class-cdpsession.md b/docs/src/api/class-cdpsession.md index 658c4f4b07..84b1a323e8 100644 --- a/docs/src/api/class-cdpsession.md +++ b/docs/src/api/class-cdpsession.md @@ -1,5 +1,5 @@ # class: CDPSession -* extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter) +* extends: [EventEmitter] The `CDPSession` instances are used to talk raw Chrome Devtools Protocol: * protocol methods can be called with `session.send` method. diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 459612f106..3ec3e0772c 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -1,5 +1,5 @@ # class: Page -* extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter) +* extends: [EventEmitter] Page provides methods to interact with a single tab in a [Browser], or an [extension background page](https://developer.chrome.com/extensions/background_pages) in Chromium. One [Browser] diff --git a/docs/src/api/python.md b/docs/src/api/python.md index 1557735ef1..164bcb54b5 100644 --- a/docs/src/api/python.md +++ b/docs/src/api/python.md @@ -1,3 +1,22 @@ +## async method: Playwright.start +* langs: python + +Starts a new instance of Playwright without using the Python context manager. This is useful in REPL applications. Requires [`method: Playwright.stop`] to be called to cleanup resources. + +```py +>>> from playwright.sync_api import sync_playwright + +>>> playwright = sync_playwright().start() + +>>> browser = playwright.chromium.launch() +>>> page = browser.newPage() +>>> page.goto("http://whatsmyuseragent.org/") +>>> page.screenshot(path="example.png") +>>> browser.close() + +>>> playwright.stop() +``` + ## async method: Playwright.stop * langs: python @@ -202,13 +221,13 @@ from a `setTimeout`. Consider this example: ```python async async with page.expect_navigation(): - await page.click("a.delayed-navigation") # Clicking the link will indirectly cause a navigation + await page.click("a.delayed-navigation") # Clicking the link will indirectly cause a navigation # Context manager waited for the navigation to happen. ``` ```python sync with page.expect_navigation(): - page.click("a.delayed-navigation") # Clicking the link will indirectly cause a navigation + page.click("a.delayed-navigation") # Clicking the link will indirectly cause a navigation # Context manager waited for the navigation to happen. ``` @@ -236,13 +255,13 @@ from a `setTimeout`. Consider this example: ```python async async with frame.expect_navigation(): - await frame.click("a.delayed-navigation") # Clicking the link will indirectly cause a navigation + await frame.click("a.delayed-navigation") # Clicking the link will indirectly cause a navigation # Context manager waited for the navigation to happen. ``` ```python sync with frame.expect_navigation(): - frame.click("a.delayed-navigation") # Clicking the link will indirectly cause a navigation + frame.click("a.delayed-navigation") # Clicking the link will indirectly cause a navigation # Context manager waited for the navigation to happen. ``` diff --git a/docs/src/assertions.md b/docs/src/assertions.md index bd88e6e558..7393879850 100644 --- a/docs/src/assertions.md +++ b/docs/src/assertions.md @@ -4,7 +4,7 @@ title: "Assertions" --- The Playwright API can be used to read element contents and properties for test assertions. These values are fetched from the browser page and asserted in -Node.js. +your script. diff --git a/docs/src/emulation.md b/docs/src/emulation.md index 1216c2ab79..3e9ebf1dc9 100644 --- a/docs/src/emulation.md +++ b/docs/src/emulation.md @@ -4,12 +4,13 @@ title: "Device and environment emulation" --- Playwright allows overriding various parameters of the device where the browser is running: - - viewport size, device scale factor, touch support - - locale, timezone - - color scheme - - geolocation +- viewport size, device scale factor, touch support +- locale, timezone +- color scheme +- geolocation -Most of these parameters are configured during the browser context construction, but some of them such as viewport size can be changed for individual pages. +Most of these parameters are configured during the browser context construction, but some of them such as viewport size +can be changed for individual pages. @@ -17,7 +18,8 @@ Most of these parameters are configured during the browser context construction, ## Devices -Playwright comes with a registry of device parameters for selected mobile devices. It can be used to simulate browser behavior on a mobile device: +Playwright comes with a registry of device parameters for selected mobile devices. It can be used to simulate browser +behavior on a mobile device: ```js const { chromium, devices } = require('playwright'); @@ -33,32 +35,36 @@ const context = await browser.newContext({ import asyncio from playwright.async_api import async_playwright -async def main(): - async with async_playwright() as p: - pixel_2 = p.devices['Pixel 2'] - browser = await p.webkit.launch(headless=False) - context = await browser.new_context( - **pixel_2, - ) +async def run(playwright): + pixel_2 = playwright.devices['Pixel 2'] + browser = await playwright.webkit.launch(headless=False) + context = await browser.new_context( + **pixel_2, + ) -asyncio.get_event_loop().run_until_complete(main()) +async def main(): + async with async_playwright() as playwright: + await run(playwright) +asyncio.run(main()) ``` ```python sync from playwright.sync_api import sync_playwright -with sync_playwright() as p: - pixel_2 = p.devices['Pixel 2'] - browser = p.webkit.launch(headless=False) +def run(playwright): + pixel_2 = playwright.devices['Pixel 2'] + browser = playwright.webkit.launch(headless=False) context = browser.new_context( **pixel_2, ) + +with sync_playwright() as playwright: + run(playwright) ``` All pages created in the context above will share the same device parameters. #### API reference - - [`property: Playwright.devices`] - [`method: Browser.newContext`] @@ -87,7 +93,6 @@ context = browser.new_context( ``` #### API reference - - [`method: Browser.newContext`]
@@ -141,11 +146,9 @@ page.set_viewport_size(width=1600, height=1200) context = browser.new_context( viewport={ 'width': 2560, 'height': 1440 }, device_scale_factor=2, - ``` #### API reference - - [`method: Browser.newContext`] - [`method: Page.setViewportSize`] @@ -178,7 +181,6 @@ context = browser.new_context( ``` #### API reference - - [`method: Browser.newContext`]
@@ -248,7 +250,6 @@ context.clear_permissions() ``` #### API reference - - [`method: Browser.newContext`] - [`method: BrowserContext.grantPermissions`] - [`method: BrowserContext.clearPermissions`] @@ -256,6 +257,7 @@ context.clear_permissions()
## Geolocation + Create a context with `"geolocation"` permissions granted: ```js @@ -296,7 +298,6 @@ context.set_geolocation({"longitude": 29.979097, "latitude": 31.134256}) **Note** you can only change geolocation for all pages in the context. #### API reference - - [`method: Browser.newContext`] - [`method: BrowserContext.setGeolocation`] @@ -304,8 +305,7 @@ context.set_geolocation({"longitude": 29.979097, "latitude": 31.134256}) ## Color scheme and media -Create a context with dark or light mode. Pages created in this context will -follow this color scheme preference. +Create a context with dark or light mode. Pages created in this context will follow this color scheme preference. ```js // Create context with dark mode @@ -362,6 +362,5 @@ page.emulate_media(media='print') ``` #### API reference - - [`method: Browser.newContext`] -- [`method: Page.emulateMedia`] +- [`method: Page.emulateMedia`] \ No newline at end of file diff --git a/docs/src/extensibility.md b/docs/src/extensibility.md index 5372921381..1963d17ed6 100644 --- a/docs/src/extensibility.md +++ b/docs/src/extensibility.md @@ -10,14 +10,20 @@ title: "Extensibility" Playwright supports custom selector engines, registered with [`method: Selectors.register`]. Selector engine should have the following properties: - -- `create` function to create a relative selector from `root` (root is either a `Document`, `ShadowRoot` or `Element`) to a `target` element. +- `create` function to create a relative selector from `root` (root is either a `Document`, `ShadowRoot` or `Element`) + to a `target` element. - `query` function to query first element matching `selector` relative to the `root`. - `queryAll` function to query all elements matching `selector` relative to the `root`. -By default the engine is run directly in the frame's JavaScript context and, for example, can call an application-defined function. To isolate the engine from any JavaScript in the frame, but leave access to the DOM, register the engine with `{contentScript: true}` option. Content script engine is safer because it is protected from any tampering with the global objects, for example altering `Node.prototype` methods. All built-in selector engines run as content scripts. Note that running as a content script is not guaranteed when the engine is used together with other custom engines. +By default the engine is run directly in the frame's JavaScript context and, for example, can call an +application-defined function. To isolate the engine from any JavaScript in the frame, but leave access to the DOM, +register the engine with `{contentScript: true}` option. Content script engine is safer because it is protected from any +tampering with the global objects, for example altering `Node.prototype` methods. All built-in selector engines run as +content scripts. Note that running as a content script is not guaranteed when the engine is used together with other +custom engines. An example of registering selector engine that queries elements based on a tag name: + ```js // Must be a function that evaluates to a selector engine instance. const createTagNameEngine = () => ({ @@ -44,3 +50,59 @@ await page.click('tag=div >> span >> "Click me"'); // We can use it in any methods supporting selectors. const buttonCount = await page.$$eval('tag=button', buttons => buttons.length); ``` + +```python async +tag_selector = """ + // Must evaluate to a selector engine instance. + { + // Returns the first element matching given selector in the root's subtree. + query(root, selector) { + return root.querySelector(selector); + }, + + // Returns all elements matching given selector in the root's subtree. + queryAll(root, selector) { + return Array.from(root.querySelectorAll(selector)); + } + }""" + +# register the engine. selectors will be prefixed with "tag=". +await playwright.selectors.register("tag", tag_selector) + +# now we can use "tag=" selectors. +button = await page.query_selector("tag=button") + +# we can combine it with other selector engines using `>>` combinator. +await page.click("tag=div >> span >> "click me"") + +# we can use it in any methods supporting selectors. +button_count = await page.eval_on_selector_all("tag=button", buttons => buttons.length) +``` + +```python sync +tag_selector = """ + // Must evaluate to a selector engine instance. + { + // Returns the first element matching given selector in the root's subtree. + query(root, selector) { + return root.querySelector(selector); + }, + + // Returns all elements matching given selector in the root's subtree. + queryAll(root, selector) { + return Array.from(root.querySelectorAll(selector)); + } + }""" + +# register the engine. selectors will be prefixed with "tag=". +playwright.selectors.register("tag", tag_selector) + +# now we can use "tag=" selectors. +button = page.query_selector("tag=button") + +# we can combine it with other selector engines using `>>` combinator. +page.click("tag=div >> span >> "click me"") + +# we can use it in any methods supporting selectors. +button_count = page.eval_on_selector_all("tag=button", buttons => buttons.length) +``` diff --git a/docs/src/input.md b/docs/src/input.md index e81ca6cc0f..2128aa8a54 100644 --- a/docs/src/input.md +++ b/docs/src/input.md @@ -439,7 +439,6 @@ await page.setInputFiles('input#upload', { ``` ```python async -from playwright.async_api import FilePayload # Select one file await page.set_input_files('input#upload', 'myfile.pdf') @@ -452,12 +451,13 @@ await page.set_input_files('input#upload', []) # Upload buffer from memory await page.set_input_files( "input#upload", - files=[FilePayload("test.txt", "text/plain", b"this is a test")], + files=[ + {"name": "test.txt", "mimeType": "text/plain", "buffer": b"this is a test"} + ], ) ``` ```python sync -from playwright.sync_api import FilePayload # Select one file page.set_input_files('input#upload', 'myfile.pdf') @@ -470,7 +470,9 @@ page.set_input_files('input#upload', []) # Upload buffer from memory page.set_input_files( "input#upload", - files=[FilePayload("test.txt", "text/plain", b"this is a test")], + files=[ + {"name": "test.txt", "mimeType": "text/plain", "buffer": b"this is a test"} + ], ) ``` diff --git a/docs/src/multi-pages.md b/docs/src/multi-pages.md index ac4347dba2..7a0a462c02 100644 --- a/docs/src/multi-pages.md +++ b/docs/src/multi-pages.md @@ -3,16 +3,14 @@ id: multi-pages title: "Multi-page scenarios" --- -Playwright can automate scenarios that span multiple browser contexts or multiple -tabs in a browser window. +Playwright can automate scenarios that span multiple browser contexts or multiple tabs in a browser window. ## Multiple contexts -[Browser contexts](./core-concepts.md#browser-contexts) are isolated environments -on a single browser instance. Playwright can create multiple browser contexts -within a single scenario. This is useful when you want to test for +[Browser contexts](./core-concepts.md#browser-contexts) are isolated environments on a single browser instance. +Playwright can create multiple browser contexts within a single scenario. This is useful when you want to test for multi-user functionality, like chat. ```js @@ -30,8 +28,50 @@ await userContext.addCookies(userCookies); await adminContext.addCookies(adminCookies); ``` -#### API reference +```python async +import asyncio +from playwright.async_api import async_playwright +async def run(playwright): + # create a chromium browser instance + chromium = playwright.chromium + browser = await chromium.launch() + + # create two isolated browser contexts + user_context = await browser.new_context() + admin_context = await browser.new_context() + + # load user and admin cookies + await user_context.add_cookies(user_cookies) + await admin_context.add_cookies(admin_cookies) + +async def main(): + async with async_playwright() as playwright: + await run(playwright) +asyncio.run(main()) +``` + +```python sync +from playwright.sync_api import sync_playwright + +def run(playwright): + # create a chromium browser instance + chromium = playwright.chromium + browser = chromium.launch() + + # create two isolated browser contexts + user_context = browser.new_context() + admin_context = browser.new_context() + + # load user and admin cookies + user_context.add_cookies(user_cookies) + admin_context.add_cookies(admin_cookies) + +with sync_playwright() as playwright: + run(playwright) +``` + +#### API reference - [BrowserContext] - [`method: Browser.newContext`] - [`method: BrowserContext.addCookies`] @@ -39,11 +79,9 @@ await adminContext.addCookies(adminCookies); ## Multiple pages Each browser context can host multiple pages (tabs). - -* Each page behaves like a focused, active page. Bringing the page to front - is not required. -* Pages inside a context respect context-level emulation, like viewport sizes, - custom network routes or browser locale. +* Each page behaves like a focused, active page. Bringing the page to front is not required. +* Pages inside a context respect context-level emulation, like viewport sizes, custom network routes or browser + locale. ```js // Create two pages @@ -54,17 +92,33 @@ const pageTwo = await context.newPage(); const allPages = context.pages(); ``` -#### API reference +```python async +# create two pages +page_one = await context.new_page() +page_two = await context.new_page() +# get pages of a brower context +all_pages = context.pages() +``` + +```python sync +# create two pages +page_one = context.new_page() +page_two = context.new_page() + +# get pages of a brower context +all_pages = context.pages() +``` + +#### API reference - [Page] - [`method: BrowserContext.newPage`] - [`method: BrowserContext.pages`] ## Handling new pages -The `page` event on browser contexts can be used to get new pages that are -created in the context. This can be used to handle new pages opened by -`target="_blank"` links. +The `page` event on browser contexts can be used to get new pages that are created in the context. This can be used to +handle new pages opened by `target="_blank"` links. ```js // Get page after a specific action (e.g. clicking a link) @@ -76,27 +130,62 @@ await newPage.waitForLoadState(); console.log(await newPage.title()); ``` +```python async +# Get page after a specific action (e.g. clicking a link) +async with context.expect_page() as new_page_info: + await page.click('a[target="_blank"]') # Opens a new tab +new_page = await new_page_info.value + +await new_page.wait_for_load_state() +print(await new_page.title()) +``` + +```python sync +# Get page after a specific action (e.g. clicking a link) +with context.expect_page() as new_page_info: + page.click('a[target="_blank"]') # Opens a new tab +new_page = new_page_info.value + +new_page.wait_for_load_state() +print(new_page.title()) +``` + If the action that triggers the new page is unknown, the following pattern can be used. ```js // Get all new pages (including popups) in the context context.on('page', async page => { await page.waitForLoadState(); - await page.title(); + console.log(await page.title()); }) ``` -#### API reference +```python async +# Get all new pages (including popups) in the context +async def handle_page(page): + await page.wait_for_load_state() + print(await page.title()) +context.on("page", handle_page) +``` + +```python sync +# Get all new pages (including popups) in the context +def handle_page(page): + page.wait_for_load_state() + print(page.title()) + +context.on("page", handle_page) +``` + +#### API reference - [`event: BrowserContext.page`] ## Handling popups -If the page opens a pop-up, you can get a reference to it by listening to the -`popup` event on the page. +If the page opens a pop-up, you can get a reference to it by listening to the `popup` event on the page. -This event is emitted in addition to the `browserContext.on('page')` event, but -only for popups relevant to this page. +This event is emitted in addition to the `browserContext.on('page')` event, but only for popups relevant to this page. ```js // Get popup after a specific action (e.g., click) @@ -105,7 +194,27 @@ const [popup] = await Promise.all([ page.click('#open') ]); await popup.waitForLoadState(); -await popup.title(); +console.log(await popup.title()); +``` + +```python async +# Get popup after a specific action (e.g., click) +async with page.expect_popup() as popup_info: + await page.click("#open") +popup = await popup_info.value + +await popup.wait_for_load_state() +print(await popup.title()) +``` + +```python sync +# Get popup after a specific action (e.g., click) +with page.expect_popup() as popup_info: + page.click("#open") +popup = popup_info.value + +popup.wait_for_load_state() +print(popup.title()) ``` If the action that triggers the popup is unknown, the following pattern can be used. @@ -118,6 +227,23 @@ page.on('popup', async popup => { }) ``` -#### API reference +```python async +# Get all popups when they open +async def handle_popup(popup): + await popup.wait_for_load_state() + print(await popup.title()) -- [`event: Page.popup`] +page.on("popup", handle_popup) +``` + +```python sync +# Get all popups when they open +def handle_popup(popup): + popup.wait_for_load_state() + print(popup.title()) + +page.on("popup", handle_popup) +``` + +#### API reference +- [`event: Page.popup`] \ No newline at end of file diff --git a/docs/src/navigations.md b/docs/src/navigations.md index 86f40b6b7d..9fc233e844 100644 --- a/docs/src/navigations.md +++ b/docs/src/navigations.md @@ -3,17 +3,22 @@ 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 page interactions. This guide covers common scenarios +to wait for page navigations and loading to complete. ## Navigation lifecycle + Playwright splits the process of showing a new document in a page into **navigation** and **loading**. -**Navigations** can be initiated by changing the page URL or by interacting with the page (e.g., clicking a link). Navigation ends when response headers have been parsed and session history is updated. The navigation intent may be canceled, for example, on hitting an unresolved DNS address or transformed into a file download. Only after the navigation succeeds, page starts **loading** the document. - -**Loading** covers getting the remaining response body over the network, parsing, executing the scripts and firing load events: +**Navigations** can be initiated by changing the page URL or by interacting with the page (e.g., clicking a link). +Navigation ends when response headers have been parsed and session history is updated. The navigation intent may be +canceled, for example, on hitting an unresolved DNS address or transformed into a file download. Only after the +navigation succeeds, page starts **loading** the document. +**Loading** covers getting the remaining response body over the network, parsing, executing the scripts and firing load +events: - [`method: Page.url`] is set to the new url - document content is loaded over network and parsed - [`event: Page.domcontentloaded`] event is fired @@ -23,17 +28,31 @@ Playwright splits the process of showing a new document in a page into **navigat - `networkidle` is fired when no new network requests are made for 500 ms ## 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`, `page.goto` will auto-wait for the redirected page to fire the `load` event. + +Navigating to a URL auto-waits for the page to fire the `load` event. If the page does a client-side redirect before +`load`, `page.goto` will auto-wait for the redirected page to fire the `load` event. ```js // Navigate the page await page.goto('https://example.com'); ``` +```python async +# Navigate the page +await page.goto("https://example.com") +``` + +```python sync +# Navigate the page +page.goto("https://example.com") +``` + ### Custom wait + Override the default behavior to wait until a specific event, like `networkidle`. ```js @@ -41,8 +60,20 @@ Override the default behavior to wait until a specific event, like `networkidle` await page.goto('https://example.com', { waitUntil: 'networkidle' }); ``` +```python async +# Navigate and wait until network is idle +await page.goto("https://example.com", wait_until="networkidle") +``` + +```python sync +# Navigate and wait until network is idle +page.goto("https://example.com", wait_until="networkidle") +``` + ### Wait for element -In lazy-loaded pages, it can be useful to wait until an element is visible with [`method: Page.waitForSelector`]. Alternatively, page interactions like [`method: Page.click`] auto-wait for elements. + +In lazy-loaded pages, it can be useful to wait until an element is visible with [`method: Page.waitForSelector`]. +Alternatively, page interactions like [`method: Page.click`] auto-wait for elements. ```js // Navigate and wait for element @@ -55,6 +86,28 @@ await page.goto('https://example.com'); await page.click('text=Example Domain'); ``` +```python async +# Navigate and wait for element +await page.goto("https://example.com") +await page.wait_for_selector("text=example domain") + +# Navigate and click element +# Click will auto-wait for the element +await page.goto("https://example.com") +await page.click("text=example domain") +``` + +```python sync +# Navigate and wait for element +page.goto("https://example.com") +page.wait_for_selector("text=example domain") + +# Navigate and click element +# Click will auto-wait for the element +page.goto("https://example.com") +page.click("text=example domain") +``` + #### API reference - [`method: Page.goto`] - [`method: Page.reload`] @@ -62,10 +115,13 @@ await page.click('text=Example Domain'); - [`method: Page.goForward`] ## Scenarios initiated by page interaction -In the scenarios below, `page.click` initiates a navigation and then waits for the navigation to complete. + +In the scenarios below, [`method: Page.click`] initiates a navigation and then waits for the navigation to complete. ### Auto-wait -By default, `page.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. + +By default, [`method: Page.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 @@ -74,7 +130,24 @@ await page.click('text=Login'); await page.fill('#username', 'John Doe'); ``` +```python async +# Click will auto-wait for navigation to complete +await page.click("text=Login") + +# Fill will auto-wait for element on navigated page +await page.fill("#username", "John Doe") +``` + +```python sync +# Click will auto-wait for navigation to complete +page.click("text=Login") + +# Fill will auto-wait for element on navigated page +page.fill("#username", "John Doe") +``` + ### Custom wait + `page.click` can be combined with [`method: Page.waitForLoadState`] to wait for a loading event. ```js @@ -82,51 +155,125 @@ await page.click('button'); // Click triggers navigation await page.waitForLoadState('networkidle'); // This resolves after 'networkidle' ``` +```python async +await page.click("button"); # Click triggers navigation +await page.wait_for_load_state("networkidle"); # This waits for the "networkidle" +``` + +```python sync +page.click("button"); # Click triggers navigation +page.wait_for_load_state("networkidle"); # This waits for the "networkidle" +``` + ### Wait for element -In lazy-loaded pages, it can be useful to wait until an element is visible with [`method: Page.waitForSelector`]. Alternatively, page interactions like [`method: Page.click`] auto-wait for elements. + +In lazy-loaded pages, it can be useful to wait until an element is visible with [`method: Page.waitForSelector`]. +Alternatively, page interactions like [`method: Page.click`] auto-wait for elements. ```js // Click triggers navigation await page.click('text=Login'); - // Click will auto-wait for the element +// Click will auto-wait for the element await page.waitForSelector('#username', 'John Doe'); // Click triggers navigation await page.click('text=Login'); - // Fill will auto-wait for element +// Fill will auto-wait for element await page.fill('#username', 'John Doe'); ``` +```python async +# Click triggers navigation +await page.click("text=Login") +# Click will auto-wait for the element +await page.wait_for_selector("#username", "John Doe") + +# Click triggers navigation +await page.click("text=Login") +# Fill will auto-wait for element +await page.fill("#username", "John Doe") +``` + +```python sync +# Click triggers navigation +page.click("text=Login") +# Click will auto-wait for the element +page.wait_for_selector("#username", "John Doe") + +# Click triggers navigation +page.click("text=Login") +# Fill will auto-wait for element +page.fill("#username", "John Doe") +``` + ### Asynchronous navigation -Clicking an element could trigger asychronous processing before initiating the navigation. In these cases, it is recommended to explicitly call [`method: Page.waitForNavigation`]. For example: + +Clicking an element could trigger asychronous 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 +// Note that Promise.all prevents a race condition +// between clicking and waiting for a navigation. await Promise.all([ - page.click('a'), // Triggers a navigation after a timeout page.waitForNavigation(), // Waits for the next navigation + page.click('a'), // Triggers a navigation after a timeout ]); ``` -The `Promise.all` pattern prevents a race condition between `page.click` and `page.waitForNavigation` when navigation happens quickly. +```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.click("a") +``` + +```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.click("a") +``` ### 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: + +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 +// Note that Promise.all prevents a race condition +// between clicking and waiting for a navigation. await Promise.all([ page.waitForNavigation({ url: '**/login' }), page.click('a'), // Triggers a navigation with a script redirect ]); ``` -The `Promise.all` pattern prevents a race condition between `page.click` and `page.waitForNavigation` when navigation happens quickly. +```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.click("a") +``` + +```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.click("a") +``` ### Loading a popup -When popup is opened, explicitly calling [`method: Page.waitForLoadState`] ensures that popup is loaded to the desired state. + +When popup is opened, explicitly calling [`method: Page.waitForLoadState`] ensures that popup is loaded to the desired +state. ```js const [ popup ] = await Promise.all([ @@ -136,6 +283,20 @@ const [ popup ] = await Promise.all([ await popup.waitForLoadState('load'); ``` +```python async +async with page.expect_popup() as popup_info: + await page.click('a[target="_blank"]') # Opens popup +popup = await popup_info.value +await popup.wait_for_load_state("load") +``` + +```python sync +with page.expect_popup() as popup_info: + page.click('a[target="_blank"]') # Opens popup +popup = popup_info.value +popup.wait_for_load_state("load") +``` + #### API reference - [`method: Page.click`] - [`method: Page.waitForLoadState`] @@ -144,7 +305,9 @@ await popup.waitForLoadState('load'); - [`method: Page.waitForFunction`] ## Advanced patterns -For pages that have complicated loading patterns, [`method: Page.waitForFunction`] is a powerful and extensible approach to define a custom wait criteria. + +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'); @@ -153,5 +316,20 @@ await page.waitForFunction(() => window.amILoadedYet()); await 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 +# FIXME +page.goto("http://example.com") +page.wait_for_function("() => window.amILoadedYet()") +# Ready to take a screenshot, according to the page itself. +page.screenshot() +``` + #### API reference -- [`method: Page.waitForFunction`] +- [`method: Page.waitForFunction`] \ No newline at end of file diff --git a/docs/src/network.md b/docs/src/network.md index be0d3b1790..6f6a82d22c 100644 --- a/docs/src/network.md +++ b/docs/src/network.md @@ -3,8 +3,8 @@ id: network title: "Network" --- -Playwright provides APIs to **monitor** and **modify** network traffic, both HTTP and HTTPS. -Any requests that page does, including [XHRs](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) and +Playwright provides APIs to **monitor** and **modify** network traffic, both HTTP and HTTPS. Any requests that page +does, including [XHRs](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) and [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) requests, can be tracked, modified and handled. @@ -24,8 +24,23 @@ const page = await context.newPage(); await page.goto('https://example.com'); ``` -#### API reference +```python async +context = await browser.new_context( + http_credentials={"username": "bill", "password": "pa55w0rd"} +) +page = await context.new_page() +await page.goto("https://example.com") +``` +```python sync +context = browser.new_context( + http_credentials={"username": "bill", "password": "pa55w0rd"} +) +page = context.new_page() +page.goto("https://example.com") +``` + +#### API reference - [`method: Browser.newContext`]
@@ -35,12 +50,32 @@ await page.goto('https://example.com'); ```js const [ download ] = await Promise.all([ page.waitForEvent('download'), // <-- start waiting for the download - page.click('button#delayed-download') // <-- perform the action that directly or indirectly initiates it. + page.click('button#delayed-download') // <-- perform the action that directly or indirectly initiates it ]); const path = await download.path(); ``` -For every attachment downloaded by the page, [`event: Page.download`] event is emitted. If you create a browser context with the [`option: acceptDownloads`] set, all these attachments are going to be downloaded into a temporary folder. You can obtain the download url, file system path and payload stream using the [Download] object from the event. +```python async +# Start waiting for the download +async with page.expect_download() as download_info: + # Perform the action that directly or indirectly initiates it + await page.click("button#delayed-download") +download = await download_info.value +path = await download.path() +``` + +```python sync +# Start waiting for the download +with page.expect_download() as download_info: + # Perform the action that directly or indirectly initiates it + page.click("button#delayed-download") +download = download_info.value +path = download.path() +``` + +For every attachment downloaded by the page, [`event: Page.download`] event is emitted. If you create a browser context +with the [`option: acceptDownloads`] set, all these attachments are going to be downloaded into a temporary folder. You +can obtain the download url, file system path and payload stream using the [Download] object from the event. #### Variations @@ -50,10 +85,20 @@ If you have no idea what initiates the download, you can still handle the event: page.on('download', download => download.path().then(console.log)); ``` -Note that handling the event forks the control flow and makes script harder to follow. Your scenario might end while you are downloading a file since your main control flow is not awaiting for this operation to resolve. +```python async +async def handle_download(download): + print(await download.path()) +page.on("download", handle_download) +``` + +```python sync +page.on("download", lambda download: print(download.path())) +``` + +Note that handling the event forks the control flow and makes script harder to follow. Your scenario might end while you +are downloading a file since your main control flow is not awaiting for this operation to resolve. #### API reference - - [Download] - [`event: Page.download`] - [`method: Page.waitForEvent`] @@ -82,6 +127,43 @@ const { chromium, webkit, firefox } = require('playwright'); })(); ``` +```python async +import asyncio +from playwright.async_api import async_playwright + +async def run(playwright): + chromium = playwright.chromium + browser = await chromium.launch() + page = await browser.new_page() + # Subscribe to "request" and "response" events. + page.on("request", lambda request: print(">>", request.method, request.url)) + page.on("response", lambda response: print("<<", response.status, response.url)) + await page.goto("https://example.com") + await browser.close() + +async def main(): + async with async_playwright() as playwright: + await run(playwright) +asyncio.run(main()) +``` + +```python sync +from playwright.sync_api import sync_playwright + +def run(playwright): + chromium = playwright.chromium + browser = chromium.launch() + page = browser.new_page() + # Subscribe to "request" and "response" events. + page.on("request", lambda request: print(">>", request.method, request.url)) + page.on("response", lambda response: print("<<", response.status, response.url)) + page.goto("https://example.com") + browser.close() + +with sync_playwright() as playwright: + run(playwright) +``` + Or wait for a network response after the button click: ```js @@ -92,6 +174,20 @@ const [response] = await Promise.all([ ]); ``` +```python async +# Use a glob url pattern +async with page.expect_response("**/api/fetch_data") as response_info: + await page.click("button#update") +response = await response_info.value +``` + +```python sync +# Use a glob url pattern +with page.expect_response("**/api/fetch_data") as response_info: + page.click("button#update") +response = response_info.value +``` + #### Variations ```js @@ -108,8 +204,31 @@ const [response] = await Promise.all([ ]); ``` -#### API reference +```python async +# Use a regular expresison +async with page.expect_response(re.compile(r"\.jpeg$")) as response_info: + await page.click("button#update") +response = await response_info.value +# Use a predicate taking a response object +async with page.expect_response(lambda response: token in response.url) as response_info: + await page.click("button#update") +response = await response_info.value +``` + +```python sync +# Use a regular expresison +with page.expect_response(re.compile(r"\.jpeg$")) as response_info: + page.click("button#update") +response = response_info.value + +# Use a predicate taking a response object +with page.expect_response(lambda response: token in response.url) as response_info: + page.click("button#update") +response = response_info.value +``` + +#### API reference - [Request] - [Response] - [`event: Page.request`] @@ -129,6 +248,20 @@ await page.route('**/api/fetch_data', route => route.fulfill({ await page.goto('https://example.com'); ``` +```python async +await page.route( + "**/api/fetch_data", + lambda route: route.fulfill(status=200, body=test_data)) +await page.goto("https://example.com") +``` + +```python sync +page.route( + "**/api/fetch_data", + lambda route: route.fulfill(status=200, body=test_data)) +page.goto("https://example.com") +``` + You can mock API endpoints via handling the network quests in your Playwright script. #### Variations @@ -144,8 +277,25 @@ await browserContext.route('**/api/login', route => route.fulfill({ await page.goto('https://example.com'); ``` -#### API reference +```python async +# Set up route on the entire browser context. +# It will apply to popup windows and opened links. +await context.route( + "**/api/login", + lambda route: route.fulfill(status=200, body="accept")) +await page.goto("https://example.com") +``` +```python sync +# Set up route on the entire browser context. +# It will apply to popup windows and opened links. +context.route( + "**/api/login", + lambda route: route.fulfill(status=200, body="accept")) +page.goto("https://example.com") +``` + +#### API reference - [`method: BrowserContext.route`] - [`method: BrowserContext.unroute`] - [`method: Page.route`] @@ -168,6 +318,30 @@ await page.route('**/*', route => { await page.route('**/*', route => route.continue({method: 'POST'})); ``` +```python async +# Delete header +async def handle_route(route): + headers = route.request.headers + del headers["x-secret"] + route.continue_(headers=headers) +await page.route("**/*", handle_route) + +# Continue requests as POST. +await page.route("**/*", lambda route: route.continue_(method="POST")) +``` + +```python sync +# Delete header +def handle_route(route): + headers = route.request.headers + del headers["x-secret"] + route.continue_(headers=headers) +page.route("**/*", handle_route) + +# Continue requests as POST. +page.route("**/*", lambda route: route.continue_(method="POST")) +``` + You can continue requests with modifications. Example above removes an HTTP header from the outgoing requests. ## Abort requests @@ -182,10 +356,23 @@ await page.route('**/*', route => { }); ``` -#### API reference +```python async +await page.route("**/*.{png,jpg,jpeg}", lambda route: route.abort()) +# Abort based on the request type +await page.route("**/*", lambda route: route.abort() if route.request.resource_type == "image" else route.continue_()) +``` + +```python sync +page.route("**/*.{png,jpg,jpeg}", lambda route: route.abort()) + +# Abort based on the request type +page.route("**/*", lambda route: route.abort() if route.request.resource_type == "image" else route.continue_()) +``` + +#### API reference - [`method: Page.route`] - [`method: BrowserContext.route`] - [`method: Route.abort`] -
+
\ No newline at end of file diff --git a/docs/src/pom.md b/docs/src/pom.md index ce294f60ac..2707e5f738 100644 --- a/docs/src/pom.md +++ b/docs/src/pom.md @@ -3,23 +3,23 @@ id: pom title: "Page Object Models" --- -Large test suites can be structured to optimize ease of authoring and maintenance. -Page object models are one such approach to structure your test suite. +Large test suites can be structured to optimize ease of authoring and maintenance. Page object models are one such +approach to structure your test suite. ## Introduction -A page object represents a part of your web application. An e-commerce web -application might have a home page, a listings page and a checkout page. Each of -them can be represented by page object models. -Page objects **simplify authoring**. They create a higher-level API which suits -your application. +A page object represents a part of your web application. An e-commerce web application might have a home page, a +listings page and a checkout page. Each of them can be represented by page object models. -Page objects **simplify maintenance**. They capture element selectors in one place -and create reusable code to avoid repetition. +Page objects **simplify authoring**. They create a higher-level API which suits your application. + +Page objects **simplify maintenance**. They capture element selectors in one place and create reusable code to avoid +repetition. ## Implementation + Page object models wrap over a Playwright [Page]. ```js @@ -33,12 +33,40 @@ class SearchPage { } async search(text) { await this.page.fill('[aria-label="Enter your search term"]', text); - await this.page.keyboard.press('Enter'); + await this.page.press('[aria-label="Enter your search term"]', 'Enter'); } } module.exports = { SearchPage }; ``` +```python async +# models/search.py +class SearchPage: + def __init__(self, page): + self.page = page + + async def navigate(self): + await self.page.goto("https://bing.com") + + async def search(self, text): + await self.page.fill('[aria-label="Enter your search term"]', text) + await self.page.press('[aria-label="Enter your search term"]', "Enter") +``` + +```python sync +# models/search.py +class SearchPage: + def __init__(self, page): + self.page = page + + def navigate(self): + self.page.goto("https://bing.com") + + def search(self, text): + self.page.fill('[aria-label="Enter your search term"]', text) + self.page.press('[aria-label="Enter your search term"]', "Enter") +``` + Page objects can then be used inside a test. ```js @@ -52,5 +80,27 @@ await searchPage.navigate(); await searchPage.search('search query'); ``` +```python async +# test_search.py +from models.search import SearchPage + +# in the test +page = await browser.new_page() +search_page = SearchPage(page) +await search_page.navigate() +await search_page.search("search query") +``` + +```python sync +# test_search.py +from models.search import SearchPage + +# in the test +page = browser.new_page() +search_page = SearchPage(page) +search_page.navigate() +search_page.search("search query") +``` + ### API reference -- [Page] +- [Page] \ No newline at end of file diff --git a/docs/src/selectors.md b/docs/src/selectors.md index e9b93ad467..68f5a79251 100644 --- a/docs/src/selectors.md +++ b/docs/src/selectors.md @@ -59,6 +59,64 @@ const handle = await page.$('div >> ../span'); const handle = await divHandle.$('css=span'); ``` +```python async +# queries 'div' css selector +handle = await page.query_selector('css=div') + +# queries '//html/body/div' xpath selector +handle = await page.query_selector('xpath=//html/body/div') + +# queries '"foo"' text selector +handle = await page.query_selector('text="foo"') + +# queries 'span' css selector inside the result of '//html/body/div' xpath selector +handle = await page.query_selector('xpath=//html/body/div >> css=span') + +# converted to 'css=div' +handle = await page.query_selector('div') + +# converted to 'xpath=//html/body/div' +handle = await page.query_selector('//html/body/div') + +# converted to 'text="foo"' +handle = await page.query_selector('"foo"') + +# queries '../span' xpath selector starting with the result of 'div' css selector +handle = await page.query_selector('div >> ../span') + +# queries 'span' css selector inside the div handle +handle = await div_handle.query_selector('css=span') +``` + +```python sync +# queries 'div' css selector +handle = page.query_selector('css=div') + +# queries '//html/body/div' xpath selector +handle = page.query_selector('xpath=//html/body/div') + +# queries '"foo"' text selector +handle = page.query_selector('text="foo"') + +# queries 'span' css selector inside the result of '//html/body/div' xpath selector +handle = page.query_selector('xpath=//html/body/div >> css=span') + +# converted to 'css=div' +handle = page.query_selector('div') + +# converted to 'xpath=//html/body/div' +handle = page.query_selector('//html/body/div') + +# converted to 'text="foo"' +handle = page.query_selector('"foo"') + +# queries '../span' xpath selector starting with the result of 'div' css selector +handle = page.query_selector('div >> ../span') + +# queries 'span' css selector inside the div handle +handle = div_handle.query_selector('css=span') +``` + ## Syntax Selectors are defined by selector engine name and selector body, `engine=body`. @@ -127,6 +185,40 @@ await page.click('[aria-label="Close"]'); // short-form await page.click('css=nav >> text=Login'); ``` +```python async +# queries "Login" text selector +await page.click('text="Login"') +await page.click('"Login"') # short-form + +# queries "Search GitHub" placeholder attribute +await page.fill('css=[placeholder="Search GitHub"]') +await page.fill('[placeholder="Search GitHub"]') # short-form + +# queries "Close" accessibility label +await page.click('css=[aria-label="Close"]') +await page.click('[aria-label="Close"]') # short-form + +# combine role and text queries +await page.click('css=nav >> text=Login') +``` + +```python sync +# queries "Login" text selector +page.click('text="Login"') +page.click('"Login"') # short-form + +# queries "Search GitHub" placeholder attribute +page.fill('css=[placeholder="Search GitHub"]') +page.fill('[placeholder="Search GitHub"]') # short-form + +# queries "Close" accessibility label +page.click('css=[aria-label="Close"]') +page.click('[aria-label="Close"]') # short-form + +# combine role and text queries +page.click('css=nav >> text=Login') +``` + ### Define explicit contract When user-facing attributes change frequently, it is recommended to use explicit test ids, like `data-test-id`. These `data-*` attributes are supported by the [css] and [id selectors][id]. @@ -144,6 +236,24 @@ await page.click('[data-test-id=directions]'); // short-form await page.click('data-test-id=directions'); ``` +```python async +# queries data-test-id attribute with css +await page.click('css=[data-test-id=directions]') +await page.click('[data-test-id=directions]') # short-form + +# queries data-test-id with id +await page.click('data-test-id=directions') +``` + +```python sync +# queries data-test-id attribute with css +page.click('css=[data-test-id=directions]') +page.click('[data-test-id=directions]') # short-form + +# queries data-test-id with id +page.click('data-test-id=directions') +``` + ### Avoid selectors tied to implementation [xpath] and [css] can be tied to the DOM structure or implementation. These selectors can break when the DOM structure changes. @@ -153,6 +263,18 @@ await page.click('#tsf > div:nth-child(2) > div.A8SBwf > div.RNNXgb > div > div. await page.click('//*[@id="tsf"]/div[2]/div[1]/div[1]/div/div[2]/input'); ``` +```python async +# avoid long css or xpath chains +await page.click('#tsf > div:nth-child(2) > div.A8SBwf > div.RNNXgb > div > div.a4bIc > input') +await page.click('//*[@id="tsf"]/div[2]/div[1]/div[1]/div/div[2]/input') +``` + +```python sync +# avoid long css or xpath chains +page.click('#tsf > div:nth-child(2) > div.A8SBwf > div.RNNXgb > div > div.a4bIc > input') +page.click('//*[@id="tsf"]/div[2]/div[1]/div[1]/div/div[2]/input') +``` + ## CSS selector engine `css` is a default engine - any malformed selector not starting with `//` nor starting and ending with a quote is assumed to be a css selector. For example, Playwright converts `'span > button'` to `'css=span > button'`. @@ -212,12 +334,24 @@ Consider a page with two buttons, first invisible and second visible. ```js await page.click('button'); ``` + ```python async + await page.click("button") + ``` + ```python sync + page.click("button") + ``` * This will find a second button, because it is visible, and then click it. ```js await page.click('button:visible'); ``` + ```python async + await page.click("button:visible") + ``` + ```python sync + page.click("button:visible") + ``` Use `:visible` with caution, because it has two major drawbacks: * When elements change their visibility dynamically, `:visible` will give upredictable results based on the timing. @@ -238,6 +372,16 @@ The `:text` pseudo-class matches elements that have a text node child with speci await page.click('button:text("Sign in")'); ``` +```python async +# Click a button with text "Sign in". +await page.click('button:text("Sign in")') +``` + +```python sync +# Click a button with text "Sign in". +page.click('button:text("Sign in")') +``` + ### CSS extension: has The `:has()` pseudo-class is an [experimental CSS pseudo-class](https://developer.mozilla.org/en-US/docs/Web/CSS/:has) that is supported by Playwright. @@ -247,6 +391,16 @@ The `:has()` pseudo-class is an [experimental CSS pseudo-class](https://develope await page.textContent('article:has(div.promo)'); ``` +```python async +# Returns text content of an
element that has a
inside. +await page.textContent("article:has(div.promo)") +``` + +```python sync +# Returns text content of an
element that has a
inside. +page.textContent("article:has(div.promo)") +``` + ### CSS extension: is The `:is()` pseudo-class is an [experimental CSS pseudo-class](https://developer.mozilla.org/en-US/docs/Web/CSS/:is) that is supported by Playwright. @@ -256,6 +410,16 @@ The `:is()` pseudo-class is an [experimental CSS pseudo-class](https://developer await page.click('button:is(:text("Log in"), :text("Sign in"))'); ``` +```python async +# Clicks a