From 3cbe13e58a4a20b4b3aaa1afbdc69747a7c37933 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Thu, 21 Aug 2025 17:08:58 +0200 Subject: [PATCH 01/19] chore: roll to 1.55.0 (#2956) --- README.md | 4 +- playwright/_impl/_helper.py | 26 +++- playwright/async_api/_generated.py | 27 +++- playwright/sync_api/_generated.py | 35 ++++- setup.py | 2 +- .../content-script.js | 0 .../index.js | 2 +- .../manifest.json | 4 +- .../extension-mv3-with-logging/background.js | 5 + .../extension-mv3-with-logging/content.js | 1 + .../extension-mv3-with-logging/manifest.json | 17 +++ tests/async/test_extension.py | 125 ++++++++++++++++++ tests/async/test_launcher.py | 38 +----- tests/async/test_page_route.py | 17 +++ tests/async/test_tracing.py | 1 - tests/sync/test_extension.py | 101 ++++++++++++++ tests/sync/test_launcher.py | 38 +----- tests/sync/test_tracing.py | 1 - 18 files changed, 352 insertions(+), 92 deletions(-) rename tests/assets/{simple-extension => extension-mv3-simple}/content-script.js (100%) rename tests/assets/{simple-extension => extension-mv3-simple}/index.js (63%) rename tests/assets/{simple-extension => extension-mv3-simple}/manifest.json (80%) create mode 100644 tests/assets/extension-mv3-with-logging/background.js create mode 100644 tests/assets/extension-mv3-with-logging/content.js create mode 100644 tests/assets/extension-mv3-with-logging/manifest.json create mode 100644 tests/async/test_extension.py create mode 100644 tests/sync/test_extension.py diff --git a/README.md b/README.md index fa9e246a9..cf85c6116 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 139.0.7258.5 | ✅ | ✅ | ✅ | +| Chromium 140.0.7339.16 | ✅ | ✅ | ✅ | | WebKit 26.0 | ✅ | ✅ | ✅ | -| Firefox 140.0.2 | ✅ | ✅ | ✅ | +| Firefox 141.0 | ✅ | ✅ | ✅ | ## Documentation diff --git a/playwright/_impl/_helper.py b/playwright/_impl/_helper.py index 66e59c65f..8f1ca8594 100644 --- a/playwright/_impl/_helper.py +++ b/playwright/_impl/_helper.py @@ -29,6 +29,7 @@ Optional, Pattern, Set, + Tuple, TypedDict, TypeVar, Union, @@ -221,14 +222,35 @@ def map_token(original: str, replacement: str) -> str: processed_parts.append(new_prefix + new_suffix) relative_path = "/".join(processed_parts) - resolved_url = urljoin(base_url if base_url is not None else "", relative_path) + resolved_url, case_insensitive_part = resolve_base_url(base_url, relative_path) for replacement, original in token_map.items(): - resolved_url = resolved_url.replace(replacement, original, 1) + normalize = case_insensitive_part and replacement in case_insensitive_part + resolved_url = resolved_url.replace( + replacement, original.lower() if normalize else original, 1 + ) return ensure_trailing_slash(resolved_url) +def resolve_base_url( + base_url: Optional[str], given_url: str +) -> Tuple[str, Optional[str]]: + try: + resolved = urljoin(base_url if base_url is not None else "", given_url) + parsed = urlparse(resolved) + # Schema and domain are case-insensitive. + hostname_port = ( + parsed.hostname or "" + ) # can't use parsed.netloc because it includes userinfo (username:password) + if parsed.port: + hostname_port += f":{parsed.port}" + case_insensitive_prefix = f"{parsed.scheme}://{hostname_port}" + return resolved, case_insensitive_prefix + except Exception: + return given_url, None + + # In Node.js, new URL('http://localhost') returns 'http://localhost/'. # To ensure the same url matching behavior, do the same. def ensure_trailing_slash(url: str) -> str: diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index bedf233de..bdda2b2b0 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -11487,8 +11487,8 @@ async def main(): async def pause(self) -> None: """Page.pause - Pauses script execution. Playwright will stop executing the script and wait for the user to either press 'Resume' - button in the page overlay or to call `playwright.resume()` in the DevTools console. + Pauses script execution. Playwright will stop executing the script and wait for the user to either press the + 'Resume' button in the page overlay or to call `playwright.resume()` in the DevTools console. User can inspect selectors or perform manual steps while paused. Resume will continue running the original script from the place it was paused. @@ -13921,6 +13921,10 @@ async def new_context( `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided with an exact match to the request origin that the certificate is valid for. + Client certificate authentication is only active when at least one client certificate is provided. If you want to + reject all client certificates sent by the server, you need to provide a client certificate with an `origin` that + does not match any of the domains you plan to visit. + **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it work by replacing `localhost` with `local.playwright`. @@ -14152,6 +14156,10 @@ async def new_page( `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided with an exact match to the request origin that the certificate is valid for. + Client certificate authentication is only active when at least one client certificate is provided. If you want to + reject all client certificates sent by the server, you need to provide a client certificate with an `origin` that + does not match any of the domains you plan to visit. + **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it work by replacing `localhost` with `local.playwright`. @@ -14559,6 +14567,13 @@ async def launch_persistent_context( **parent** directory of the "Profile Path" seen at `chrome://version`. Note that browsers do not allow launching multiple instances with the same User Data Directory. + + **NOTE** Chromium/Chrome: Due to recent Chrome policy changes, automating the default Chrome user profile is not + supported. Pointing `userDataDir` to Chrome's main "User Data" directory (the profile used for your regular + browsing) may result in pages not loading or the browser exiting. Create and use a separate directory (for example, + an empty folder) as your automation profile instead. See https://developer.chrome.com/blog/remote-debugging-port + for details. + channel : Union[str, None] Browser distribution channel. @@ -14733,6 +14748,10 @@ async def launch_persistent_context( `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided with an exact match to the request origin that the certificate is valid for. + Client certificate authentication is only active when at least one client certificate is provided. If you want to + reject all client certificates sent by the server, you need to provide a client certificate with an `origin` that + does not match any of the domains you plan to visit. + **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it work by replacing `localhost` with `local.playwright`. @@ -18801,6 +18820,10 @@ async def new_context( `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided with an exact match to the request origin that the certificate is valid for. + Client certificate authentication is only active when at least one client certificate is provided. If you want to + reject all client certificates sent by the server, you need to provide a client certificate with an `origin` that + does not match any of the domains you plan to visit. + **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it work by replacing `localhost` with `local.playwright`. diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py index 8f4b60764..83fedfbe9 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -11569,8 +11569,8 @@ def run(playwright: Playwright): def pause(self) -> None: """Page.pause - Pauses script execution. Playwright will stop executing the script and wait for the user to either press 'Resume' - button in the page overlay or to call `playwright.resume()` in the DevTools console. + Pauses script execution. Playwright will stop executing the script and wait for the user to either press the + 'Resume' button in the page overlay or to call `playwright.resume()` in the DevTools console. User can inspect selectors or perform manual steps while paused. Resume will continue running the original script from the place it was paused. @@ -12255,7 +12255,7 @@ def add_locator_handler( ```py # Setup the handler. - def handler(): + async def handler(): await page.get_by_role(\"button\", name=\"No thanks\").click() await page.add_locator_handler(page.get_by_text(\"Sign up to the newsletter\"), handler) @@ -12268,7 +12268,7 @@ def handler(): ```py # Setup the handler. - def handler(): + async def handler(): await page.get_by_role(\"button\", name=\"Remind me later\").click() await page.add_locator_handler(page.get_by_text(\"Confirm your security details\"), handler) @@ -12283,7 +12283,7 @@ def handler(): ```py # Setup the handler. - def handler(): + async def handler(): await page.evaluate(\"window.removeObstructionsForTestIfNeeded()\") await page.add_locator_handler(page.locator(\"body\"), handler, no_wait_after=True) @@ -12296,7 +12296,7 @@ def handler(): invocations by setting `times`: ```py - def handler(locator): + async def handler(locator): await locator.click() await page.add_locator_handler(page.get_by_label(\"Close\"), handler, times=1) ``` @@ -13952,6 +13952,10 @@ def new_context( `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided with an exact match to the request origin that the certificate is valid for. + Client certificate authentication is only active when at least one client certificate is provided. If you want to + reject all client certificates sent by the server, you need to provide a client certificate with an `origin` that + does not match any of the domains you plan to visit. + **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it work by replacing `localhost` with `local.playwright`. @@ -14185,6 +14189,10 @@ def new_page( `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided with an exact match to the request origin that the certificate is valid for. + Client certificate authentication is only active when at least one client certificate is provided. If you want to + reject all client certificates sent by the server, you need to provide a client certificate with an `origin` that + does not match any of the domains you plan to visit. + **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it work by replacing `localhost` with `local.playwright`. @@ -14598,6 +14606,13 @@ def launch_persistent_context( **parent** directory of the "Profile Path" seen at `chrome://version`. Note that browsers do not allow launching multiple instances with the same User Data Directory. + + **NOTE** Chromium/Chrome: Due to recent Chrome policy changes, automating the default Chrome user profile is not + supported. Pointing `userDataDir` to Chrome's main "User Data" directory (the profile used for your regular + browsing) may result in pages not loading or the browser exiting. Create and use a separate directory (for example, + an empty folder) as your automation profile instead. See https://developer.chrome.com/blog/remote-debugging-port + for details. + channel : Union[str, None] Browser distribution channel. @@ -14772,6 +14787,10 @@ def launch_persistent_context( `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided with an exact match to the request origin that the certificate is valid for. + Client certificate authentication is only active when at least one client certificate is provided. If you want to + reject all client certificates sent by the server, you need to provide a client certificate with an `origin` that + does not match any of the domains you plan to visit. + **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it work by replacing `localhost` with `local.playwright`. @@ -18922,6 +18941,10 @@ def new_context( `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided with an exact match to the request origin that the certificate is valid for. + Client certificate authentication is only active when at least one client certificate is provided. If you want to + reject all client certificates sent by the server, you need to provide a client certificate with an `origin` that + does not match any of the domains you plan to visit. + **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it work by replacing `localhost` with `local.playwright`. diff --git a/setup.py b/setup.py index 5c2911865..d147f3be7 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ import zipfile from typing import Dict -driver_version = "1.54.1" +driver_version = "1.55.0" base_wheel_bundles = [ { diff --git a/tests/assets/simple-extension/content-script.js b/tests/assets/extension-mv3-simple/content-script.js similarity index 100% rename from tests/assets/simple-extension/content-script.js rename to tests/assets/extension-mv3-simple/content-script.js diff --git a/tests/assets/simple-extension/index.js b/tests/assets/extension-mv3-simple/index.js similarity index 63% rename from tests/assets/simple-extension/index.js rename to tests/assets/extension-mv3-simple/index.js index a0bb3f4ea..1523a8364 100644 --- a/tests/assets/simple-extension/index.js +++ b/tests/assets/extension-mv3-simple/index.js @@ -1,2 +1,2 @@ // Mock script for background extension -window.MAGIC = 42; +globalThis.MAGIC = 42; diff --git a/tests/assets/simple-extension/manifest.json b/tests/assets/extension-mv3-simple/manifest.json similarity index 80% rename from tests/assets/simple-extension/manifest.json rename to tests/assets/extension-mv3-simple/manifest.json index da2cd082e..39b77fc21 100644 --- a/tests/assets/simple-extension/manifest.json +++ b/tests/assets/extension-mv3-simple/manifest.json @@ -2,7 +2,7 @@ "name": "Simple extension", "version": "0.1", "background": { - "scripts": ["index.js"] + "service_worker": "index.js" }, "content_scripts": [{ "matches": [""], @@ -10,5 +10,5 @@ "js": ["content-script.js"] }], "permissions": ["background", "activeTab"], - "manifest_version": 2 + "manifest_version": 3 } diff --git a/tests/assets/extension-mv3-with-logging/background.js b/tests/assets/extension-mv3-with-logging/background.js new file mode 100644 index 000000000..3b1a406fb --- /dev/null +++ b/tests/assets/extension-mv3-with-logging/background.js @@ -0,0 +1,5 @@ +console.log("Service worker script loaded"); + +chrome.runtime.onInstalled.addListener(() => { + console.log("Extension installed"); +}); diff --git a/tests/assets/extension-mv3-with-logging/content.js b/tests/assets/extension-mv3-with-logging/content.js new file mode 100644 index 000000000..e718206c2 --- /dev/null +++ b/tests/assets/extension-mv3-with-logging/content.js @@ -0,0 +1 @@ +console.log("Test console log from a third-party execution context"); diff --git a/tests/assets/extension-mv3-with-logging/manifest.json b/tests/assets/extension-mv3-with-logging/manifest.json new file mode 100644 index 000000000..5ad1fde38 --- /dev/null +++ b/tests/assets/extension-mv3-with-logging/manifest.json @@ -0,0 +1,17 @@ +{ + "manifest_version": 3, + "name": "Console Log Extension", + "version": "1.0", + "background": { + "service_worker": "background.js" + }, + "permissions": [ + "tabs" + ], + "content_scripts": [ + { + "matches": [""], + "js": ["content.js"] + } + ] + } diff --git a/tests/async/test_extension.py b/tests/async/test_extension.py new file mode 100644 index 000000000..853afd8a5 --- /dev/null +++ b/tests/async/test_extension.py @@ -0,0 +1,125 @@ +# 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 asyncio +from pathlib import Path +from typing import Any, AsyncGenerator, Awaitable, Callable, Dict, List, Optional + +import pytest + +from playwright.async_api import BrowserContext, BrowserType + +from ..server import Server + + +@pytest.fixture() +async def launch_persistent_context( + browser_type: BrowserType, + browser_channel: Optional[str], + tmp_path: Path, + launch_arguments: Dict[str, Any], + is_headless_shell: bool, +) -> AsyncGenerator[Callable[..., Awaitable[BrowserContext]], None]: + if browser_channel and browser_channel.startswith("chrome"): + pytest.skip( + "--load-extension is not supported in Chrome anymore. https://groups.google.com/a/chromium.org/g/chromium-extensions/c/1-g8EFx2BBY/m/S0ET5wPjCAAJ" + ) + if is_headless_shell: + pytest.skip("Headless Shell has no support for extensions") + + contexts: List[BrowserContext] = [] + + async def launch(extension_path: str, **kwargs: Any) -> BrowserContext: + context = await browser_type.launch_persistent_context( + str(tmp_path), + **launch_arguments, + **kwargs, + args=[ + f"--disable-extensions-except={extension_path}", + f"--load-extension={extension_path}", + ], + ) + contexts.append(context) + return context + + yield launch + + for context in contexts: + await context.close() + + +@pytest.mark.only_browser("chromium") +async def test_should_give_access_to_the_service_worker( + launch_persistent_context: Callable[..., Awaitable[BrowserContext]], + assetdir: Path, +) -> None: + extension_path = str(assetdir / "extension-mv3-simple") + context = await launch_persistent_context(extension_path) + service_workers = context.service_workers + service_worker = ( + service_workers[0] + if len(service_workers) + else await context.wait_for_event("serviceworker") + ) + assert service_worker + assert service_worker in context.service_workers + while not await service_worker.evaluate("globalThis.MAGIC") == 42: + await context.pages[0].wait_for_timeout(100) + await context.close() + assert len(context.background_pages) == 0 + + +@pytest.mark.only_browser("chromium") +async def test_should_give_access_to_the_service_worker_when_recording_video( + launch_persistent_context: Callable[..., Awaitable[BrowserContext]], + tmp_path: Path, + assetdir: Path, +) -> None: + extension_path = str(assetdir / "extension-mv3-simple") + context = await launch_persistent_context( + extension_path, record_video_dir=(tmp_path / "videos") + ) + service_workers = context.service_workers + service_worker = ( + service_workers[0] + if len(service_workers) + else await context.wait_for_event("serviceworker") + ) + assert service_worker + assert service_worker in context.service_workers + while not await service_worker.evaluate("globalThis.MAGIC") == 42: + await context.pages[0].wait_for_timeout(100) + await context.close() + assert len(context.background_pages) == 0 + + +# https://github.com/microsoft/playwright/issues/32762 +@pytest.mark.only_browser("chromium") +async def test_should_report_console_messages_from_content_script( + launch_persistent_context: Callable[..., Awaitable[BrowserContext]], + assetdir: Path, + server: Server, +) -> None: + extension_path = str(assetdir / "extension-mv3-with-logging") + context = await launch_persistent_context(extension_path) + page = await context.new_page() + [message, _] = await asyncio.gather( + page.context.wait_for_event( + "console", + lambda e: "Test console log from a third-party execution context" in e.text, + ), + page.goto(server.EMPTY_PAGE), + ) + assert "Test console log from a third-party execution context" in message.text + await context.close() diff --git a/tests/async/test_launcher.py b/tests/async/test_launcher.py index 1b974725b..bd5dd82de 100644 --- a/tests/async/test_launcher.py +++ b/tests/async/test_launcher.py @@ -15,7 +15,7 @@ import asyncio import os from pathlib import Path -from typing import Dict, Optional +from typing import Dict import pytest @@ -107,39 +107,3 @@ async def test_browser_close_should_be_callable_twice( browser.close(), ) await browser.close() - - -@pytest.mark.only_browser("chromium") -async def test_browser_launch_should_return_background_pages( - browser_type: BrowserType, - tmp_path: Path, - browser_channel: Optional[str], - assetdir: Path, - launch_arguments: Dict, -) -> None: - if browser_channel: - pytest.skip() - - extension_path = str(assetdir / "simple-extension") - context = await browser_type.launch_persistent_context( - str(tmp_path), - **{ - **launch_arguments, - "headless": False, - "args": [ - f"--disable-extensions-except={extension_path}", - f"--load-extension={extension_path}", - ], - }, - ) - background_page = None - if len(context.background_pages): - background_page = context.background_pages[0] - else: - background_page = await context.wait_for_event("backgroundpage") - assert background_page - assert background_page in context.background_pages - assert background_page not in context.pages - await context.close() - assert len(context.background_pages) == 0 - assert len(context.pages) == 0 diff --git a/tests/async/test_page_route.py b/tests/async/test_page_route.py index fecafdfba..b561af0a2 100644 --- a/tests/async/test_page_route.py +++ b/tests/async/test_page_route.py @@ -1128,6 +1128,23 @@ def glob_to_regex(pattern: str) -> re.Pattern: "http://playwright.dev/foo/", "http://playwright.dev/foo/bar?x=y", "./bar?x=y" ) + # Case insensitive matching + assert url_matches( + None, "https://playwright.dev/fooBAR", "HtTpS://pLaYwRiGhT.dEv/fooBAR" + ) + assert url_matches( + "http://ignored", + "https://playwright.dev/fooBAR", + "HtTpS://pLaYwRiGhT.dEv/fooBAR", + ) + # Path and search query are case-sensitive + assert not url_matches( + None, "https://playwright.dev/foobar", "https://playwright.dev/fooBAR" + ) + assert not url_matches( + None, "https://playwright.dev/foobar?a=b", "https://playwright.dev/foobar?A=B" + ) + # This is not supported, we treat ? as a query separator. assert not url_matches( None, diff --git a/tests/async/test_tracing.py b/tests/async/test_tracing.py index e902eafbd..cbd282820 100644 --- a/tests/async/test_tracing.py +++ b/tests/async/test_tracing.py @@ -385,6 +385,5 @@ async def test_should_show_tracing_group_in_action_list( re.compile(r"inner group 1"), re.compile(r"Click"), re.compile(r"inner group 2"), - re.compile(r"Is visible"), ] ) diff --git a/tests/sync/test_extension.py b/tests/sync/test_extension.py new file mode 100644 index 000000000..2cb8aee77 --- /dev/null +++ b/tests/sync/test_extension.py @@ -0,0 +1,101 @@ +# 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. + +from pathlib import Path +from typing import Any, Callable, Dict, Generator, List, Optional + +import pytest + +from playwright.sync_api import BrowserContext, BrowserType + + +@pytest.fixture() +def launch_persistent_context( + browser_type: BrowserType, + browser_channel: Optional[str], + tmp_path: Path, + launch_arguments: Dict[str, Any], + is_headless_shell: bool, +) -> Generator[Callable[..., BrowserContext], None, None]: + if browser_channel and browser_channel.startswith("chrome"): + pytest.skip( + "--load-extension is not supported in Chrome anymore. https://groups.google.com/a/chromium.org/g/chromium-extensions/c/1-g8EFx2BBY/m/S0ET5wPjCAAJ" + ) + if is_headless_shell: + pytest.skip("Headless Shell has no support for extensions") + + contexts: List[BrowserContext] = [] + + def launch(extension_path: str, **kwargs: Any) -> BrowserContext: + context = browser_type.launch_persistent_context( + str(tmp_path), + **launch_arguments, + **kwargs, + args=[ + f"--disable-extensions-except={extension_path}", + f"--load-extension={extension_path}", + ], + ) + contexts.append(context) + return context + + yield launch + + for context in contexts: + context.close() + + +@pytest.mark.only_browser("chromium") +def test_should_give_access_to_the_service_worker( + launch_persistent_context: Callable[..., BrowserContext], + assetdir: Path, +) -> None: + extension_path = str(assetdir / "extension-mv3-simple") + context = launch_persistent_context(extension_path) + service_workers = context.service_workers + service_worker = ( + service_workers[0] + if len(service_workers) + else context.wait_for_event("serviceworker") + ) + assert service_worker + assert service_worker in context.service_workers + while not service_worker.evaluate("globalThis.MAGIC") == 42: + context.pages[0].wait_for_timeout(100) + context.close() + assert len(context.background_pages) == 0 + + +@pytest.mark.only_browser("chromium") +def test_should_give_access_to_the_service_worker_when_recording_video( + launch_persistent_context: Callable[..., BrowserContext], + tmp_path: Path, + assetdir: Path, +) -> None: + extension_path = str(assetdir / "extension-mv3-simple") + context = launch_persistent_context( + extension_path, record_video_dir=(tmp_path / "videos") + ) + service_workers = context.service_workers + service_worker = ( + service_workers[0] + if len(service_workers) + else context.wait_for_event("serviceworker") + ) + assert service_worker + assert service_worker in context.service_workers + while not service_worker.evaluate("globalThis.MAGIC") == 42: + context.pages[0].wait_for_timeout(100) + context.close() + assert len(context.background_pages) == 0 diff --git a/tests/sync/test_launcher.py b/tests/sync/test_launcher.py index 52deeb827..2e5ec1573 100644 --- a/tests/sync/test_launcher.py +++ b/tests/sync/test_launcher.py @@ -14,7 +14,7 @@ import os from pathlib import Path -from typing import Dict, Optional +from typing import Dict import pytest @@ -88,39 +88,3 @@ def test_browser_close_should_be_callable_twice( browser = browser_type.launch(**launch_arguments) browser.close() browser.close() - - -@pytest.mark.only_browser("chromium") -def test_browser_launch_should_return_background_pages( - browser_type: BrowserType, - tmp_path: Path, - browser_channel: Optional[str], - assetdir: Path, - launch_arguments: Dict, -) -> None: - if browser_channel: - pytest.skip() - - extension_path = str(assetdir / "simple-extension") - context = browser_type.launch_persistent_context( - str(tmp_path), - **{ - **launch_arguments, - "headless": False, - "args": [ - f"--disable-extensions-except={extension_path}", - f"--load-extension={extension_path}", - ], - }, - ) - background_page = None - if len(context.background_pages): - background_page = context.background_pages[0] - else: - background_page = context.wait_for_event("backgroundpage") - assert background_page - assert background_page in context.background_pages - assert background_page not in context.pages - context.close() - assert len(context.background_pages) == 0 - assert len(context.pages) == 0 diff --git a/tests/sync/test_tracing.py b/tests/sync/test_tracing.py index 8d0eaa191..ce26600e5 100644 --- a/tests/sync/test_tracing.py +++ b/tests/sync/test_tracing.py @@ -386,6 +386,5 @@ def test_should_show_tracing_group_in_action_list( re.compile(r"inner group 1"), re.compile(r"Click"), re.compile(r"inner group 2"), - re.compile(r"Is visible"), ] ) From 4a03d717fa82f1daed6d5f1dab505e8cd06aa8f2 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Thu, 28 Aug 2025 11:27:29 +0200 Subject: [PATCH 02/19] chore(roll): roll Playwright to 1.55.0-beta-1756314050000 (#2960) --- ROLLING.md | 10 ++++++---- setup.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/ROLLING.md b/ROLLING.md index f5f500a3f..811d7fcb3 100644 --- a/ROLLING.md +++ b/ROLLING.md @@ -5,10 +5,12 @@ * create virtual environment, if don't have one: `python -m venv env` * activate venv: `source env/bin/activate` * install all deps: - - `python -m pip install --upgrade pip` - - `pip install -r local-requirements.txt` - - `pre-commit install` - - `pip install -e .` +``` +python -m pip install --upgrade pip +pip install -r local-requirements.txt +pre-commit install +pip install -e . +``` * change driver version in `setup.py` * download new driver: `python -m build --wheel` * generate API: `./scripts/update_api.sh` diff --git a/setup.py b/setup.py index d147f3be7..543395520 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ import zipfile from typing import Dict -driver_version = "1.55.0" +driver_version = "1.55.0-beta-1756314050000" base_wheel_bundles = [ { From e93c23a33ab2b3b8c7168031986702ae39473d89 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 17:52:38 +0200 Subject: [PATCH 03/19] build(deps): bump requests from 2.32.4 to 2.32.5 (#2962) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- local-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-requirements.txt b/local-requirements.txt index 2f4f0d488..ac7d4a464 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -15,7 +15,7 @@ pytest-repeat==0.9.4 pytest-rerunfailures==15.1 pytest-timeout==2.4.0 pytest-xdist==3.8.0 -requests==2.32.4 +requests==2.32.5 service_identity==24.2.0 twisted==25.5.0 types-pyOpenSSL==24.1.0.20240722 From 3763d818a09ad2f47f8be9a8e8c2c404ce2a3a2a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 23:16:55 +0200 Subject: [PATCH 04/19] build(deps): bump actions/setup-python from 5 to 6 in the actions group (#2969) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/publish_docker.yml | 2 +- .github/workflows/test_docker.yml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 200b2a65a..65ba1f433 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.10" - name: Install dependencies & browsers @@ -82,7 +82,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install dependencies & browsers @@ -129,7 +129,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.10" - name: Install dependencies & browsers @@ -186,7 +186,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.10' - name: Install dependencies & browsers diff --git a/.github/workflows/publish_docker.yml b/.github/workflows/publish_docker.yml index 7c2b73e13..7494f1abc 100644 --- a/.github/workflows/publish_docker.yml +++ b/.github/workflows/publish_docker.yml @@ -25,7 +25,7 @@ jobs: - name: Login to ACR via OIDC run: az acr login --name playwright - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.10" - name: Set up Docker QEMU for arm64 docker builds diff --git a/.github/workflows/test_docker.yml b/.github/workflows/test_docker.yml index e5252e389..464eb3b46 100644 --- a/.github/workflows/test_docker.yml +++ b/.github/workflows/test_docker.yml @@ -32,7 +32,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.10" - name: Install dependencies From e43199cd3d108073ed90266f082fbc1749e0219a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 10:50:46 +0200 Subject: [PATCH 05/19] build(deps): bump pillow from 11.2.1 to 11.3.0 in the pip group (#2908) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- local-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-requirements.txt b/local-requirements.txt index ac7d4a464..1dc49533b 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -4,7 +4,7 @@ build==1.3.0 flake8==7.2.0 mypy==1.17.1 objgraph==3.6.2 -Pillow==11.2.1 +Pillow==11.3.0 pixelmatch==0.3.0 pre-commit==3.5.0 pyOpenSSL==25.1.0 From 8ab311b6a692f5ff311f028bca5851cd5ae0ad3c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 10:51:22 +0200 Subject: [PATCH 06/19] build(deps): bump pytest-cov from 6.2.1 to 6.3.0 (#2968) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- local-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/local-requirements.txt b/local-requirements.txt index 1dc49533b..8a72b5745 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -10,7 +10,7 @@ pre-commit==3.5.0 pyOpenSSL==25.1.0 pytest==8.4.1 pytest-asyncio==1.1.0 -pytest-cov==6.2.1 +pytest-cov==6.3.0 pytest-repeat==0.9.4 pytest-rerunfailures==15.1 pytest-timeout==2.4.0 From 6fa9500cf051f9abb2b21af7725fa7d1e951a8bb Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 9 Sep 2025 11:51:24 +0200 Subject: [PATCH 07/19] chore: fix api generation under Python 3.13 (#2970) --- scripts/documentation_provider.py | 3 +++ scripts/generate_api.py | 1 + 2 files changed, 4 insertions(+) diff --git a/scripts/documentation_provider.py b/scripts/documentation_provider.py index 6ea931fac..a842a1aad 100644 --- a/scripts/documentation_provider.py +++ b/scripts/documentation_provider.py @@ -13,6 +13,7 @@ # limitations under the License. import json +import pathlib import re import subprocess from sys import stderr @@ -359,6 +360,8 @@ def serialize_python_type(self, value: Any, direction: str) -> str: match = re.match(r"^$", str_value) if match: return match.group(1) + if str_value == str(pathlib.Path): + return "pathlib.Path" match = re.match( r"playwright._impl._event_context_manager.EventContextManagerImpl\[playwright._impl.[^.]+.(.*)\]", str_value, diff --git a/scripts/generate_api.py b/scripts/generate_api.py index 01f8f525a..b0e7e2a32 100644 --- a/scripts/generate_api.py +++ b/scripts/generate_api.py @@ -57,6 +57,7 @@ def process_type(value: Any, param: bool = False) -> str: value = str(value) + value = re.sub("pathlib._local.Path", "pathlib.Path", value) value = re.sub(r"", r"\1", value) value = re.sub(r"NoneType", "None", value) value = re.sub(r"playwright\._impl\._api_structures.([\w]+)", r"\1", value) From db390c6e826029c0863a1dce5ca2cda44c54fede Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Mon, 6 Oct 2025 06:19:07 -0700 Subject: [PATCH 08/19] chore: roll to 1.56.0 (#2986) --- README.md | 4 +- playwright/_impl/_api_structures.py | 1 + playwright/_impl/_assertions.py | 4 +- playwright/_impl/_browser_context.py | 12 +---- playwright/_impl/_glob.py | 12 +---- playwright/_impl/_page.py | 20 +++++++- playwright/async_api/_generated.py | 65 +++++++++++++++++------- playwright/sync_api/_generated.py | 65 +++++++++++++++++------- setup.py | 2 +- tests/async/test_assertions.py | 8 +-- tests/async/test_console.py | 2 +- tests/async/test_page_aria_snapshot.py | 10 ++++ tests/async/test_page_event_console.py | 35 +++++++++++++ tests/async/test_page_event_pageerror.py | 36 +++++++++++++ tests/async/test_page_event_request.py | 62 ++++++++++++++++++++++ tests/async/test_page_route.py | 12 +++-- tests/sync/test_assertions.py | 8 +-- tests/sync/test_console.py | 5 +- 18 files changed, 286 insertions(+), 77 deletions(-) create mode 100644 tests/async/test_page_event_console.py create mode 100644 tests/async/test_page_event_pageerror.py create mode 100644 tests/async/test_page_event_request.py diff --git a/README.md b/README.md index cf85c6116..b54d5a364 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 140.0.7339.16 | ✅ | ✅ | ✅ | +| Chromium 141.0.7390.37 | ✅ | ✅ | ✅ | | WebKit 26.0 | ✅ | ✅ | ✅ | -| Firefox 141.0 | ✅ | ✅ | ✅ | +| Firefox 142.0.1 | ✅ | ✅ | ✅ | ## Documentation diff --git a/playwright/_impl/_api_structures.py b/playwright/_impl/_api_structures.py index 0afa0d02e..c0d0ee442 100644 --- a/playwright/_impl/_api_structures.py +++ b/playwright/_impl/_api_structures.py @@ -218,6 +218,7 @@ class FrameExpectResult(TypedDict): matches: bool received: Any log: List[str] + errorMessage: Optional[str] AriaRole = Literal[ diff --git a/playwright/_impl/_assertions.py b/playwright/_impl/_assertions.py index 3aadbf5fe..aea37d35c 100644 --- a/playwright/_impl/_assertions.py +++ b/playwright/_impl/_assertions.py @@ -80,8 +80,10 @@ async def _expect_impl( out_message = ( f"{message} '{expected}'" if expected is not None else f"{message}" ) + error_message = result.get("errorMessage") + error_message = f"\n{error_message}" if error_message else "" raise AssertionError( - f"{out_message}\nActual value: {actual} {format_call_log(result.get('log'))}" + f"{out_message}\nActual value: {actual}{error_message} {format_call_log(result.get('log'))}" ) diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py index 391e61ec6..bab7d1bf1 100644 --- a/playwright/_impl/_browser_context.py +++ b/playwright/_impl/_browser_context.py @@ -88,6 +88,7 @@ class BrowserContext(ChannelOwner): Events = SimpleNamespace( + # Deprecated in v1.56, never emitted anymore. BackgroundPage="backgroundpage", Close="close", Console="console", @@ -117,7 +118,6 @@ def __init__( self._timeout_settings = TimeoutSettings(None) self._owner_page: Optional[Page] = None self._options: Dict[str, Any] = initializer["options"] - self._background_pages: Set[Page] = set() self._service_workers: Set[Worker] = set() self._base_url: Optional[str] = self._options.get("baseURL") self._videos_dir: Optional[str] = self._options.get("recordVideo") @@ -149,10 +149,6 @@ def __init__( ) ), ) - self._channel.on( - "backgroundPage", - lambda params: self._on_background_page(from_channel(params["page"])), - ) self._channel.on( "serviceWorker", @@ -658,10 +654,6 @@ def expect_page( ) -> EventContextManagerImpl[Page]: return self.expect_event(BrowserContext.Events.Page, predicate, timeout) - def _on_background_page(self, page: Page) -> None: - self._background_pages.add(page) - self.emit(BrowserContext.Events.BackgroundPage, page) - def _on_service_worker(self, worker: Worker) -> None: worker._context = self self._service_workers.add(worker) @@ -736,7 +728,7 @@ def _on_response(self, response: Response, page: Optional[Page]) -> None: @property def background_pages(self) -> List[Page]: - return list(self._background_pages) + return [] @property def service_workers(self) -> List[Worker]: diff --git a/playwright/_impl/_glob.py b/playwright/_impl/_glob.py index 08b7ce466..a0e6dcd4b 100644 --- a/playwright/_impl/_glob.py +++ b/playwright/_impl/_glob.py @@ -28,20 +28,12 @@ def glob_to_regex_pattern(glob: str) -> str: tokens.append("\\" + char if char in escaped_chars else char) i += 1 elif c == "*": - before_deep = glob[i - 1] if i > 0 else None star_count = 1 while i + 1 < len(glob) and glob[i + 1] == "*": star_count += 1 i += 1 - after_deep = glob[i + 1] if i + 1 < len(glob) else None - is_deep = ( - star_count > 1 - and (before_deep == "/" or before_deep is None) - and (after_deep == "/" or after_deep is None) - ) - if is_deep: - tokens.append("((?:[^/]*(?:/|$))*)") - i += 1 + if star_count > 1: + tokens.append("(.*)") else: tokens.append("([^/]*)") else: diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index 1019b2f6e..29a583a7c 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -79,6 +79,7 @@ async_writefile, locals_to_params, make_dirs_for_file, + parse_error, serialize_error, url_matches, ) @@ -344,8 +345,6 @@ def _on_close(self) -> None: self._is_closed = True if self in self._browser_context._pages: self._browser_context._pages.remove(self) - if self in self._browser_context._background_pages: - self._browser_context._background_pages.remove(self) self._dispose_har_routers() self.emit(Page.Events.Close, self) @@ -1434,6 +1433,23 @@ async def remove_locator_handler(self, locator: "Locator") -> None: {"uid": uid}, ) + async def requests(self) -> List[Request]: + request_objects = await self._channel.send("requests", None) + return [from_channel(r) for r in request_objects] + + async def console_messages(self) -> List[ConsoleMessage]: + message_dicts = await self._channel.send("consoleMessages", None) + return [ + ConsoleMessage( + {**event, "page": self._channel}, self._loop, self._dispatcher_fiber + ) + for event in message_dicts + ] + + async def page_errors(self) -> List[Error]: + error_objects = await self._channel.send("pageErrors", None) + return [parse_error(error["error"]) for error in error_objects] + class Worker(ChannelOwner): Events = SimpleNamespace(Close="close") diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index bdda2b2b0..71f5aff82 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -12254,6 +12254,49 @@ async def remove_locator_handler(self, locator: "Locator") -> None: await self._impl_obj.remove_locator_handler(locator=locator._impl_obj) ) + async def requests(self) -> typing.List["Request"]: + """Page.requests + + Returns up to (currently) 100 last network request from this page. See `page.on('request')` for more details. + + Returned requests should be accessed immediately, otherwise they might be collected to prevent unbounded memory + growth as new requests come in. Once collected, retrieving most information about the request is impossible. + + Note that requests reported through the `page.on('request')` request are not collected, so there is a trade off + between efficient memory usage with `page.requests()` and the amount of available information reported + through `page.on('request')`. + + Returns + ------- + List[Request] + """ + + return mapping.from_impl_list(await self._impl_obj.requests()) + + async def console_messages(self) -> typing.List["ConsoleMessage"]: + """Page.console_messages + + Returns up to (currently) 200 last console messages from this page. See `page.on('console')` for more details. + + Returns + ------- + List[ConsoleMessage] + """ + + return mapping.from_impl_list(await self._impl_obj.console_messages()) + + async def page_errors(self) -> typing.List["Error"]: + """Page.page_errors + + Returns up to (currently) 200 last page errors from this page. See `page.on('page_error')` for more details. + + Returns + ------- + List[Error] + """ + + return mapping.from_impl_list(await self._impl_obj.page_errors()) + mapping.register(PageImpl, Page) @@ -12297,13 +12340,7 @@ def on( f: typing.Callable[["Page"], "typing.Union[typing.Awaitable[None], None]"], ) -> None: """ - **NOTE** Only works with Chromium browser's persistent context. - - Emitted when new background page is created in the context. - - ```py - background_page = await context.wait_for_event(\"backgroundpage\") - ```""" + This event is not emitted.""" @typing.overload def on( @@ -12477,13 +12514,7 @@ def once( f: typing.Callable[["Page"], "typing.Union[typing.Awaitable[None], None]"], ) -> None: """ - **NOTE** Only works with Chromium browser's persistent context. - - Emitted when new background page is created in the context. - - ```py - background_page = await context.wait_for_event(\"backgroundpage\") - ```""" + This event is not emitted.""" @typing.overload def once( @@ -12679,9 +12710,7 @@ def browser(self) -> typing.Optional["Browser"]: def background_pages(self) -> typing.List["Page"]: """BrowserContext.background_pages - **NOTE** Background pages are only supported on Chromium-based browsers. - - All existing background pages in the context. + Returns an empty list. Returns ------- @@ -16617,7 +16646,7 @@ def and_(self, locator: "Locator") -> "Locator": The following example finds a button with a specific title. ```py - button = page.get_by_role(\"button\").and_(page.getByTitle(\"Subscribe\")) + button = page.get_by_role(\"button\").and_(page.get_by_title(\"Subscribe\")) ``` Parameters diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py index 83fedfbe9..024014c51 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -12342,6 +12342,49 @@ def remove_locator_handler(self, locator: "Locator") -> None: self._sync(self._impl_obj.remove_locator_handler(locator=locator._impl_obj)) ) + def requests(self) -> typing.List["Request"]: + """Page.requests + + Returns up to (currently) 100 last network request from this page. See `page.on('request')` for more details. + + Returned requests should be accessed immediately, otherwise they might be collected to prevent unbounded memory + growth as new requests come in. Once collected, retrieving most information about the request is impossible. + + Note that requests reported through the `page.on('request')` request are not collected, so there is a trade off + between efficient memory usage with `page.requests()` and the amount of available information reported + through `page.on('request')`. + + Returns + ------- + List[Request] + """ + + return mapping.from_impl_list(self._sync(self._impl_obj.requests())) + + def console_messages(self) -> typing.List["ConsoleMessage"]: + """Page.console_messages + + Returns up to (currently) 200 last console messages from this page. See `page.on('console')` for more details. + + Returns + ------- + List[ConsoleMessage] + """ + + return mapping.from_impl_list(self._sync(self._impl_obj.console_messages())) + + def page_errors(self) -> typing.List["Error"]: + """Page.page_errors + + Returns up to (currently) 200 last page errors from this page. See `page.on('page_error')` for more details. + + Returns + ------- + List[Error] + """ + + return mapping.from_impl_list(self._sync(self._impl_obj.page_errors())) + mapping.register(PageImpl, Page) @@ -12383,13 +12426,7 @@ def on( self, event: Literal["backgroundpage"], f: typing.Callable[["Page"], "None"] ) -> None: """ - **NOTE** Only works with Chromium browser's persistent context. - - Emitted when new background page is created in the context. - - ```py - background_page = context.wait_for_event(\"backgroundpage\") - ```""" + This event is not emitted.""" @typing.overload def on( @@ -12529,13 +12566,7 @@ def once( self, event: Literal["backgroundpage"], f: typing.Callable[["Page"], "None"] ) -> None: """ - **NOTE** Only works with Chromium browser's persistent context. - - Emitted when new background page is created in the context. - - ```py - background_page = context.wait_for_event(\"backgroundpage\") - ```""" + This event is not emitted.""" @typing.overload def once( @@ -12701,9 +12732,7 @@ def browser(self) -> typing.Optional["Browser"]: def background_pages(self) -> typing.List["Page"]: """BrowserContext.background_pages - **NOTE** Background pages are only supported on Chromium-based browsers. - - All existing background pages in the context. + Returns an empty list. Returns ------- @@ -16680,7 +16709,7 @@ def and_(self, locator: "Locator") -> "Locator": The following example finds a button with a specific title. ```py - button = page.get_by_role(\"button\").and_(page.getByTitle(\"Subscribe\")) + button = page.get_by_role(\"button\").and_(page.get_by_title(\"Subscribe\")) ``` Parameters diff --git a/setup.py b/setup.py index 543395520..c2a56354b 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ import zipfile from typing import Dict -driver_version = "1.55.0-beta-1756314050000" +driver_version = "1.56.0-beta-1759412259000" base_wheel_bundles = [ { diff --git a/tests/async/test_assertions.py b/tests/async/test_assertions.py index 3213e5523..49e3c3e7f 100644 --- a/tests/async/test_assertions.py +++ b/tests/async/test_assertions.py @@ -516,7 +516,7 @@ async def test_to_have_values_fails_when_multiple_not_specified( ) locator = page.locator("select") await locator.select_option(["B"]) - with pytest.raises(Error) as excinfo: + with pytest.raises(AssertionError) as excinfo: await expect(locator).to_have_values(["R", "G"], timeout=500) assert "Error: Not a select element with a multiple attribute" in str(excinfo.value) @@ -530,7 +530,7 @@ async def test_to_have_values_fails_when_not_a_select_element( """ ) locator = page.locator("input") - with pytest.raises(Error) as excinfo: + with pytest.raises(AssertionError) as excinfo: await expect(locator).to_have_values(["R", "G"], timeout=500) assert "Error: Not a select element with a multiple attribute" in str(excinfo.value) @@ -564,7 +564,7 @@ async def test_assertions_boolean_checked_with_intermediate_true_and_checked( await page.set_content("") await page.locator("input").evaluate("e => e.indeterminate = true") with pytest.raises( - Error, match="Can't assert indeterminate and checked at the same time" + AssertionError, match="Can't assert indeterminate and checked at the same time" ): await expect(page.locator("input")).to_be_checked( checked=False, indeterminate=True @@ -658,7 +658,7 @@ async def test_assertions_locator_to_be_editable_throws( await page.goto(server.EMPTY_PAGE) await page.set_content("") with pytest.raises( - Error, + AssertionError, match=r"Element is not an ,