From 8587bc6986fc3bccad66683b6ecce78caa808489 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Jun 2025 00:43:56 +0200 Subject: [PATCH 01/79] build(deps): bump greenlet from 3.2.2 to 3.2.3 (#2880) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 28863d0dd..a6b31fd67 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # This file was autogenerated by uv via the following command: # uv pip compile pyproject.toml -o requirements.txt -greenlet==3.2.2 +greenlet==3.2.3 # via playwright (pyproject.toml) pyee==13.0.0 # via playwright (pyproject.toml) From 0ff7fc983ef39ed6121ffbac4eaf15e9b02a6aba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Jun 2025 00:44:04 +0200 Subject: [PATCH 02/79] build(deps): bump requests from 2.32.3 to 2.32.4 (#2882) 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 56b7edd22..99eaf42a5 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.6.1 -requests==2.32.3 +requests==2.32.4 service_identity==24.2.0 twisted==24.11.0 types-pyOpenSSL==24.1.0.20240722 From 767288e7e6fa1d708c22d9896a98bc25384c5318 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 21 Jun 2025 19:26:25 +0200 Subject: [PATCH 03/79] build(deps): bump pytest-xdist from 3.6.1 to 3.7.0 (#2881) 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 99eaf42a5..da3b04258 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -14,7 +14,7 @@ pytest-cov==6.1.1 pytest-repeat==0.9.4 pytest-rerunfailures==15.1 pytest-timeout==2.4.0 -pytest-xdist==3.6.1 +pytest-xdist==3.7.0 requests==2.32.4 service_identity==24.2.0 twisted==24.11.0 From f4a9b58b78236d36054dfee7be59431dc67c8fcf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 21 Jun 2025 19:26:50 +0200 Subject: [PATCH 04/79] build(deps): bump twisted from 24.11.0 to 25.5.0 (#2889) 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 da3b04258..567b81818 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -17,6 +17,6 @@ pytest-timeout==2.4.0 pytest-xdist==3.7.0 requests==2.32.4 service_identity==24.2.0 -twisted==24.11.0 +twisted==25.5.0 types-pyOpenSSL==24.1.0.20240722 types-requests==2.32.0.20250602 From 114d7cf3490fa6cf8876359f6636a44c4fcf1ec8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 21 Jun 2025 20:03:43 +0200 Subject: [PATCH 05/79] build(deps): bump types-requests from 2.32.0.20250602 to 2.32.4.20250611 (#2890) 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 567b81818..12c2da3ef 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -19,4 +19,4 @@ requests==2.32.4 service_identity==24.2.0 twisted==25.5.0 types-pyOpenSSL==24.1.0.20240722 -types-requests==2.32.0.20250602 +types-requests==2.32.4.20250611 From 6e3b718246dff3945edc715ec7e1682f39546529 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 21 Jun 2025 20:03:52 +0200 Subject: [PATCH 06/79] build(deps): bump pytest-cov from 6.1.1 to 6.2.1 (#2888) 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 12c2da3ef..afe7e4bb8 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.0 pytest-asyncio==1.0.0 -pytest-cov==6.1.1 +pytest-cov==6.2.1 pytest-repeat==0.9.4 pytest-rerunfailures==15.1 pytest-timeout==2.4.0 From b0c0ee0f0954dc78b1ac0e08715edf069fba4c90 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 24 Jun 2025 18:01:28 +0200 Subject: [PATCH 07/79] devops: use windows-2019 for conda builds (#2899) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a6d8fcd5..c18a04bc6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -160,7 +160,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-22.04, macos-13, windows-2019] + os: [ubuntu-22.04, macos-13, windows-2022] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 From fdca00ccdc399f85298d3265a30280270d07e55c Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Tue, 24 Jun 2025 10:52:34 -0700 Subject: [PATCH 08/79] chore: roll to 1.53.1 (#2892) --- README.md | 6 +- playwright/_impl/_assertions.py | 35 +++- playwright/_impl/_browser.py | 108 ++++++------ playwright/_impl/_browser_context.py | 89 ++++++---- playwright/_impl/_browser_type.py | 116 +++++++++---- playwright/_impl/_connection.py | 71 ++++++-- playwright/_impl/_element_handle.py | 4 +- playwright/_impl/_fetch.py | 5 +- playwright/_impl/_frame.py | 46 +++-- playwright/_impl/_helper.py | 24 ++- playwright/_impl/_input.py | 19 ++- playwright/_impl/_local_utils.py | 1 - playwright/_impl/_locator.py | 16 +- playwright/_impl/_network.py | 6 +- playwright/_impl/_object_factory.py | 3 - playwright/_impl/_page.py | 30 +++- playwright/_impl/_playwright.py | 10 +- playwright/_impl/_selectors.py | 49 ++---- playwright/_impl/_tracing.py | 1 - playwright/async_api/_generated.py | 191 ++++++++++++--------- playwright/sync_api/_generated.py | 197 ++++++++++++---------- setup.py | 2 +- tests/async/conftest.py | 4 +- tests/async/test_assertions.py | 37 +++- tests/async/test_browsertype_connect.py | 8 +- tests/async/test_defaultbrowsercontext.py | 3 +- tests/async/test_page_aria_snapshot.py | 11 -- tests/async/test_tracing.py | 99 ++++++----- tests/sync/conftest.py | 2 +- tests/sync/test_assertions.py | 10 +- tests/sync/test_har.py | 7 + tests/sync/test_page_aria_snapshot.py | 11 -- tests/sync/test_route_web_socket.py | 1 + tests/sync/test_tracing.py | 99 ++++++----- 34 files changed, 782 insertions(+), 539 deletions(-) diff --git a/README.md b/README.md index b450b87f2..9577b82e8 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 136.0.7103.25 | ✅ | ✅ | ✅ | -| WebKit 18.4 | ✅ | ✅ | ✅ | -| Firefox 137.0 | ✅ | ✅ | ✅ | +| Chromium 138.0.7204.23 | ✅ | ✅ | ✅ | +| WebKit 18.5 | ✅ | ✅ | ✅ | +| Firefox 139.0 | ✅ | ✅ | ✅ | ## Documentation diff --git a/playwright/_impl/_assertions.py b/playwright/_impl/_assertions.py index 2a3beb756..6e0161b7c 100644 --- a/playwright/_impl/_assertions.py +++ b/playwright/_impl/_assertions.py @@ -51,6 +51,7 @@ async def _expect_impl( expect_options: FrameExpectOptions, expected: Any, message: str, + title: str = None, ) -> None: __tracebackhide__ = True expect_options["isNot"] = self._is_not @@ -60,7 +61,7 @@ async def _expect_impl( message = message.replace("expected to", "expected not to") if "useInnerText" in expect_options and expect_options["useInnerText"] is None: del expect_options["useInnerText"] - result = await self._actual_locator._expect(expression, expect_options) + result = await self._actual_locator._expect(expression, expect_options, title) if result["matches"] == self._is_not: actual = result.get("received") if self._custom_message: @@ -105,6 +106,7 @@ async def to_have_title( FrameExpectOptions(expectedText=expected_values, timeout=timeout), titleOrRegExp, "Page title expected to be", + 'Expect "to_have_title"', ) async def not_to_have_title( @@ -129,6 +131,7 @@ async def to_have_url( FrameExpectOptions(expectedText=expected_text, timeout=timeout), urlOrRegExp, "Page URL expected to be", + 'Expect "to_have_url"', ) async def not_to_have_url( @@ -190,6 +193,7 @@ async def to_contain_text( ), expected, "Locator expected to contain text", + 'Expect "to_contain_text"', ) else: expected_text = to_expected_text_values( @@ -207,6 +211,7 @@ async def to_contain_text( ), expected, "Locator expected to contain text", + 'Expect "to_contain_text"', ) async def not_to_contain_text( @@ -241,6 +246,7 @@ async def to_have_attribute( ), value, "Locator expected to have attribute", + 'Expect "to_have_attribute"', ) async def not_to_have_attribute( @@ -276,6 +282,7 @@ async def to_have_class( FrameExpectOptions(expectedText=expected_text, timeout=timeout), expected, "Locator expected to have class", + 'Expect "to_have_class"', ) else: expected_text = to_expected_text_values([expected]) @@ -284,6 +291,7 @@ async def to_have_class( FrameExpectOptions(expectedText=expected_text, timeout=timeout), expected, "Locator expected to have class", + 'Expect "to_have_class"', ) async def not_to_have_class( @@ -318,6 +326,7 @@ async def to_contain_class( FrameExpectOptions(expectedText=expected_text, timeout=timeout), expected, "Locator expected to contain class names", + 'Expect "to_contain_class"', ) else: expected_text = to_expected_text_values([expected]) @@ -326,6 +335,7 @@ async def to_contain_class( FrameExpectOptions(expectedText=expected_text, timeout=timeout), expected, "Locator expected to contain class", + 'Expect "to_contain_class"', ) async def not_to_contain_class( @@ -350,6 +360,7 @@ async def to_have_count( FrameExpectOptions(expectedNumber=count, timeout=timeout), count, "Locator expected to have count", + 'Expect "to_have_count"', ) async def not_to_have_count( @@ -375,6 +386,7 @@ async def to_have_css( ), value, "Locator expected to have CSS", + 'Expect "to_have_css"', ) async def not_to_have_css( @@ -398,6 +410,7 @@ async def to_have_id( FrameExpectOptions(expectedText=expected_text, timeout=timeout), id, "Locator expected to have ID", + 'Expect "to_have_id"', ) async def not_to_have_id( @@ -422,6 +435,7 @@ async def to_have_js_property( ), value, "Locator expected to have JS Property", + 'Expect "to_have_property"', ) async def not_to_have_js_property( @@ -445,6 +459,7 @@ async def to_have_value( FrameExpectOptions(expectedText=expected_text, timeout=timeout), value, "Locator expected to have Value", + 'Expect "to_have_value"', ) async def not_to_have_value( @@ -469,6 +484,7 @@ async def to_have_values( FrameExpectOptions(expectedText=expected_text, timeout=timeout), values, "Locator expected to have Values", + 'Expect "to_have_values"', ) async def not_to_have_values( @@ -512,6 +528,7 @@ async def to_have_text( ), expected, "Locator expected to have text", + 'Expect "to_have_text"', ) else: expected_text = to_expected_text_values( @@ -526,6 +543,7 @@ async def to_have_text( ), expected, "Locator expected to have text", + 'Expect "to_have_text"', ) async def not_to_have_text( @@ -558,6 +576,7 @@ async def to_be_attached( FrameExpectOptions(timeout=timeout), None, f"Locator expected to be {attached_string}", + 'Expect "to_be_attached"', ) async def to_be_checked( @@ -582,6 +601,7 @@ async def to_be_checked( FrameExpectOptions(timeout=timeout, expectedValue=expected_value), None, f"Locator expected to be {checked_string}", + 'Expect "to_be_checked"', ) async def not_to_be_attached( @@ -609,6 +629,7 @@ async def to_be_disabled( FrameExpectOptions(timeout=timeout), None, "Locator expected to be disabled", + 'Expect "to_be_disabled"', ) async def not_to_be_disabled( @@ -632,6 +653,7 @@ async def to_be_editable( FrameExpectOptions(timeout=timeout), None, f"Locator expected to be {editable_string}", + 'Expect "to_be_editable"', ) async def not_to_be_editable( @@ -652,6 +674,7 @@ async def to_be_empty( FrameExpectOptions(timeout=timeout), None, "Locator expected to be empty", + 'Expect "to_be_empty"', ) async def not_to_be_empty( @@ -675,6 +698,7 @@ async def to_be_enabled( FrameExpectOptions(timeout=timeout), None, f"Locator expected to be {enabled_string}", + 'Expect "to_be_enabled"', ) async def not_to_be_enabled( @@ -695,6 +719,7 @@ async def to_be_hidden( FrameExpectOptions(timeout=timeout), None, "Locator expected to be hidden", + 'Expect "to_be_hidden"', ) async def not_to_be_hidden( @@ -718,6 +743,7 @@ async def to_be_visible( FrameExpectOptions(timeout=timeout), None, f"Locator expected to be {visible_string}", + 'Expect "to_be_visible"', ) async def not_to_be_visible( @@ -738,6 +764,7 @@ async def to_be_focused( FrameExpectOptions(timeout=timeout), None, "Locator expected to be focused", + 'Expect "to_be_focused"', ) async def not_to_be_focused( @@ -758,6 +785,7 @@ async def to_be_in_viewport( FrameExpectOptions(timeout=timeout, expectedNumber=ratio), None, "Locator expected to be in viewport", + 'Expect "to_be_in_viewport"', ) async def not_to_be_in_viewport( @@ -781,6 +809,7 @@ async def to_have_accessible_description( FrameExpectOptions(expectedText=expected_values, timeout=timeout), None, "Locator expected to have accessible description", + 'Expect "to_have_accessible_description"', ) async def not_to_have_accessible_description( @@ -807,6 +836,7 @@ async def to_have_accessible_name( FrameExpectOptions(expectedText=expected_values, timeout=timeout), None, "Locator expected to have accessible name", + 'Expect "to_have_accessible_name"', ) async def not_to_have_accessible_name( @@ -828,6 +858,7 @@ async def to_have_role(self, role: AriaRole, timeout: float = None) -> None: FrameExpectOptions(expectedText=expected_values, timeout=timeout), None, "Locator expected to have accessible role", + 'Expect "to_have_role"', ) async def to_have_accessible_error_message( @@ -845,6 +876,7 @@ async def to_have_accessible_error_message( FrameExpectOptions(expectedText=expected_values, timeout=timeout), None, "Locator expected to have accessible error message", + 'Expect "to_have_accessible_error_message"', ) async def not_to_have_accessible_error_message( @@ -871,6 +903,7 @@ async def to_match_aria_snapshot( FrameExpectOptions(expectedValue=expected, timeout=timeout), expected, "Locator expected to match Aria snapshot", + 'Expect "to_match_aria_snapshot"', ) async def not_to_match_aria_snapshot( diff --git a/playwright/_impl/_browser.py b/playwright/_impl/_browser.py index aa56d8244..9b3c1cacc 100644 --- a/playwright/_impl/_browser.py +++ b/playwright/_impl/_browser.py @@ -12,10 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json from pathlib import Path from types import SimpleNamespace -from typing import TYPE_CHECKING, Dict, List, Optional, Pattern, Sequence, Union, cast +from typing import ( + TYPE_CHECKING, + Dict, + List, + Optional, + Pattern, + Sequence, + Set, + Union, + cast, +) from playwright._impl._api_structures import ( ClientCertificate, @@ -38,12 +47,9 @@ HarMode, ReducedMotion, ServiceWorkersPolicy, - async_readfile, locals_to_params, make_dirs_for_file, - prepare_record_har_options, ) -from playwright._impl._network import serialize_headers, to_client_certificates_protocol from playwright._impl._page import Page if TYPE_CHECKING: # pragma: no cover @@ -59,28 +65,61 @@ def __init__( self, parent: "BrowserType", type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) - self._browser_type = parent + self._browser_type: Optional["BrowserType"] = None self._is_connected = True self._should_close_connection_on_close = False self._cr_tracing_path: Optional[str] = None - self._contexts: List[BrowserContext] = [] + self._contexts: Set[BrowserContext] = set() + self._traces_dir: Optional[str] = None + self._channel.on( + "context", + lambda params: self._did_create_context( + cast(BrowserContext, from_channel(params["context"])) + ), + ) self._channel.on("close", lambda _: self._on_close()) self._close_reason: Optional[str] = None def __repr__(self) -> str: return f"" + def _connect_to_browser_type( + self, + browser_type: "BrowserType", + traces_dir: Optional[str] = None, + ) -> None: + # Note: when using connect(), `browserType` is different from `this.parent`. + # This is why browser type is not wired up in the constructor, and instead this separate method is called later on. + self._browser_type = browser_type + self._traces_dir = traces_dir + for context in self._contexts: + self._setup_browser_context(context) + + def _did_create_context(self, context: BrowserContext) -> None: + context._browser = self + self._contexts.add(context) + # Note: when connecting to a browser, initial contexts arrive before `_browserType` is set, + # and will be configured later in `ConnectToBrowserType`. + if self._browser_type: + self._setup_browser_context(context) + + def _setup_browser_context(self, context: BrowserContext) -> None: + context._tracing._traces_dir = self._traces_dir + assert self._browser_type is not None + self._browser_type._playwright.selectors._contexts_for_selectors.add(context) + def _on_close(self) -> None: self._is_connected = False self.emit(Browser.Events.Disconnected, self) @property def contexts(self) -> List[BrowserContext]: - return self._contexts.copy() + return list(self._contexts) @property def browser_type(self) -> "BrowserType": + assert self._browser_type is not None return self._browser_type def is_connected(self) -> bool: @@ -126,11 +165,18 @@ async def new_context( clientCertificates: List[ClientCertificate] = None, ) -> BrowserContext: params = locals_to_params(locals()) - await prepare_browser_context_params(params) + assert self._browser_type is not None + await self._browser_type._prepare_browser_context_params(params) channel = await self._channel.send("newContext", params) context = cast(BrowserContext, from_channel(channel)) - self._browser_type._did_create_context(context, params, {}) + await context._initialize_har_from_options( + record_har_content=recordHarContent, + record_har_mode=recordHarMode, + record_har_omit_content=recordHarOmitContent, + record_har_path=recordHarPath, + record_har_url_filter=recordHarUrlFilter, + ) return context async def new_page( @@ -181,7 +227,7 @@ async def inner() -> Page: context._owner_page = page return page - return await self._connection.wrap_api_call(inner) + return await self._connection.wrap_api_call(inner, title="Create page") async def close(self, reason: str = None) -> None: self._close_reason = reason @@ -226,43 +272,3 @@ async def stop_tracing(self) -> bytes: f.write(buffer) self._cr_tracing_path = None return buffer - - -async def prepare_browser_context_params(params: Dict) -> None: - if params.get("noViewport"): - del params["noViewport"] - params["noDefaultViewport"] = True - if "defaultBrowserType" in params: - del params["defaultBrowserType"] - if "extraHTTPHeaders" in params: - params["extraHTTPHeaders"] = serialize_headers(params["extraHTTPHeaders"]) - if "recordHarPath" in params: - params["recordHar"] = prepare_record_har_options(params) - del params["recordHarPath"] - if "recordVideoDir" in params: - params["recordVideo"] = {"dir": Path(params["recordVideoDir"]).absolute()} - if "recordVideoSize" in params: - params["recordVideo"]["size"] = params["recordVideoSize"] - del params["recordVideoSize"] - del params["recordVideoDir"] - if "storageState" in params: - storageState = params["storageState"] - if not isinstance(storageState, dict): - params["storageState"] = json.loads( - (await async_readfile(storageState)).decode() - ) - if params.get("colorScheme", None) == "null": - params["colorScheme"] = "no-override" - if params.get("reducedMotion", None) == "null": - params["reducedMotion"] = "no-override" - if params.get("forcedColors", None) == "null": - params["forcedColors"] = "no-override" - if params.get("contrast", None) == "null": - params["contrast"] = "no-override" - if "acceptDownloads" in params: - params["acceptDownloads"] = "accept" if params["acceptDownloads"] else "deny" - - if "clientCertificates" in params: - params["clientCertificates"] = await to_client_certificates_protocol( - params["clientCertificates"] - ) diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py index 22da4375d..1264d3f8b 100644 --- a/playwright/_impl/_browser_context.py +++ b/playwright/_impl/_browser_context.py @@ -66,7 +66,6 @@ async_writefile, locals_to_params, parse_error, - prepare_record_har_options, to_impl, ) from playwright._impl._network import ( @@ -106,18 +105,18 @@ def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) + # Browser is null for browser contexts created outside of normal browser, e.g. android or electron. # circular import workaround: self._browser: Optional["Browser"] = None if parent.__class__.__name__ == "Browser": self._browser = cast("Browser", parent) - self._browser._contexts.append(self) self._pages: List[Page] = [] self._routes: List[RouteHandler] = [] self._web_socket_routes: List[WebSocketRouteHandler] = [] self._bindings: Dict[str, Any] = {} self._timeout_settings = TimeoutSettings(None) self._owner_page: Optional[Page] = None - self._options: Dict[str, Any] = {} + self._options: Dict[str, Any] = initializer["options"] self._background_pages: Set[Page] = set() self._service_workers: Set[Worker] = set() self._tracing = cast(Tracing, from_channel(initializer["tracing"])) @@ -220,7 +219,7 @@ def __init__( BrowserContext.Events.RequestFailed: "requestFailed", } ) - self._close_was_called = False + self._closing_or_closed = False def __repr__(self) -> str: return f"" @@ -237,7 +236,7 @@ async def _on_route(self, route: Route) -> None: route_handlers = self._routes.copy() for route_handler in route_handlers: # If the page or the context was closed we stall all requests right away. - if (page and page._close_was_called) or self._close_was_called: + if (page and page._close_was_called) or self._closing_or_closed: return if not route_handler.matches(route.request.url): continue @@ -288,19 +287,12 @@ def set_default_navigation_timeout(self, timeout: float) -> None: def _set_default_navigation_timeout_impl(self, timeout: Optional[float]) -> None: self._timeout_settings.set_default_navigation_timeout(timeout) - self._channel.send_no_reply( - "setDefaultNavigationTimeoutNoReply", - {} if timeout is None else {"timeout": timeout}, - ) def set_default_timeout(self, timeout: float) -> None: return self._set_default_timeout_impl(timeout) def _set_default_timeout_impl(self, timeout: Optional[float]) -> None: self._timeout_settings.set_default_timeout(timeout) - self._channel.send_no_reply( - "setDefaultTimeoutNoReply", {} if timeout is None else {"timeout": timeout} - ) @property def pages(self) -> List[Page]: @@ -310,14 +302,30 @@ def pages(self) -> List[Page]: def browser(self) -> Optional["Browser"]: return self._browser - def _set_options(self, context_options: Dict, browser_options: Dict) -> None: - self._options = context_options - if self._options.get("recordHar"): - self._har_recorders[""] = { - "path": self._options["recordHar"]["path"], - "content": self._options["recordHar"].get("content"), - } - self._tracing._traces_dir = browser_options.get("tracesDir") + async def _initialize_har_from_options( + self, + record_har_path: Optional[Union[Path, str]], + record_har_content: Optional[HarContentPolicy], + record_har_omit_content: Optional[bool], + record_har_url_filter: Optional[Union[Pattern[str], str]], + record_har_mode: Optional[HarMode], + ) -> None: + if not record_har_path: + return + record_har_path = str(record_har_path) + default_policy: HarContentPolicy = ( + "attach" if record_har_path.endswith(".zip") else "embed" + ) + content_policy: HarContentPolicy = record_har_content or ( + "omit" if record_har_omit_content is True else default_policy + ) + await self._record_into_har( + har=record_har_path, + page=None, + url=record_har_url_filter, + update_content=content_policy, + update_mode=(record_har_mode or "full"), + ) async def new_page(self) -> Page: if self._owner_page: @@ -476,22 +484,25 @@ async def _record_into_har( update_content: HarContentPolicy = None, update_mode: HarMode = None, ) -> None: + update_content = update_content or "attach" params: Dict[str, Any] = { - "options": prepare_record_har_options( - { - "recordHarPath": har, - "recordHarContent": update_content or "attach", - "recordHarMode": update_mode or "minimal", - "recordHarUrlFilter": url, - } - ) + "options": { + "zip": str(har).endswith(".zip"), + "content": update_content, + "urlGlob": url if isinstance(url, str) else None, + "urlRegexSource": url.pattern if isinstance(url, Pattern) else None, + "urlRegexFlags": ( + escape_regex_flags(url) if isinstance(url, Pattern) else None + ), + "mode": update_mode or "minimal", + } } if page: params["page"] = page._channel har_id = await self._channel.send("harStart", params) self._har_recorders[har_id] = { "path": str(har), - "content": update_content or "attach", + "content": update_content, } async def route_from_har( @@ -555,22 +566,30 @@ def expect_event( return EventContextManagerImpl(waiter.result()) def _on_close(self) -> None: + self._closing_or_closed = True if self._browser: - self._browser._contexts.remove(self) + if self in self._browser._contexts: + self._browser._contexts.remove(self) + assert self._browser._browser_type is not None + if ( + self + in self._browser._browser_type._playwright.selectors._contexts_for_selectors + ): + self._browser._browser_type._playwright.selectors._contexts_for_selectors.remove( + self + ) self._dispose_har_routers() self._tracing._reset_stack_counter() self.emit(BrowserContext.Events.Close, self) async def close(self, reason: str = None) -> None: - if self._close_was_called: + if self._closing_or_closed: return self._close_reason = reason - self._close_was_called = True + self._closing_or_closed = True - await self._channel._connection.wrap_api_call( - lambda: self.request.dispose(reason=reason), True - ) + await self.request.dispose(reason=reason) async def _inner_close() -> None: for har_id, params in self._har_recorders.items(): diff --git a/playwright/_impl/_browser_type.py b/playwright/_impl/_browser_type.py index bedc5ea73..ab8c00e97 100644 --- a/playwright/_impl/_browser_type.py +++ b/playwright/_impl/_browser_type.py @@ -13,6 +13,7 @@ # limitations under the License. import asyncio +import json import pathlib import sys from pathlib import Path @@ -25,16 +26,12 @@ ProxySettings, ViewportSize, ) -from playwright._impl._browser import Browser, prepare_browser_context_params +from playwright._impl._browser import Browser from playwright._impl._browser_context import BrowserContext -from playwright._impl._connection import ( - ChannelOwner, - Connection, - from_channel, - from_nullable_channel, -) +from playwright._impl._connection import ChannelOwner, Connection, from_channel from playwright._impl._errors import Error from playwright._impl._helper import ( + PLAYWRIGHT_MAX_DEADLINE, ColorScheme, Contrast, Env, @@ -43,10 +40,12 @@ HarMode, ReducedMotion, ServiceWorkersPolicy, + TimeoutSettings, + async_readfile, locals_to_params, ) from playwright._impl._json_pipe import JsonPipeTransport -from playwright._impl._network import serialize_headers +from playwright._impl._network import serialize_headers, to_client_certificates_protocol from playwright._impl._waiter import throw_on_timeout if TYPE_CHECKING: @@ -96,7 +95,9 @@ async def launch( browser = cast( Browser, from_channel(await self._channel.send("launch", params)) ) - self._did_launch_browser(browser) + browser._connect_to_browser_type( + self, str(tracesDir) if tracesDir is not None else None + ) return browser async def launch_persistent_context( @@ -155,13 +156,26 @@ async def launch_persistent_context( ) -> BrowserContext: userDataDir = self._user_data_dir(userDataDir) params = locals_to_params(locals()) - await prepare_browser_context_params(params) + await self._prepare_browser_context_params(params) normalize_launch_params(params) - context = cast( - BrowserContext, - from_channel(await self._channel.send("launchPersistentContext", params)), + result = await self._channel.send_return_as_dict( + "launchPersistentContext", params + ) + browser = cast( + Browser, + from_channel(result["browser"]), + ) + browser._connect_to_browser_type( + self, str(tracesDir) if tracesDir is not None else None + ) + context = cast(BrowserContext, from_channel(result["context"])) + await context._initialize_har_from_options( + record_har_content=recordHarContent, + record_har_mode=recordHarMode, + record_har_omit_content=recordHarOmitContent, + record_har_path=recordHarPath, + record_har_url_filter=recordHarUrlFilter, ) - self._did_create_context(context, params, params) return context def _user_data_dir(self, userDataDir: Optional[Union[str, Path]]) -> str: @@ -183,18 +197,13 @@ async def connect_over_cdp( headers: Dict[str, str] = None, ) -> Browser: params = locals_to_params(locals()) + params["timeout"] = TimeoutSettings.launch_timeout(timeout) if params.get("headers"): params["headers"] = serialize_headers(params["headers"]) response = await self._channel.send_return_as_dict("connectOverCDP", params) browser = cast(Browser, from_channel(response["browser"])) - self._did_launch_browser(browser) + browser._connect_to_browser_type(self, None) - default_context = cast( - Optional[BrowserContext], - from_nullable_channel(response.get("defaultContext")), - ) - if default_context: - self._did_create_context(default_context, {}, {}) return browser async def connect( @@ -205,8 +214,6 @@ async def connect( headers: Dict[str, str] = None, exposeNetwork: str = None, ) -> Browser: - if timeout is None: - timeout = 30000 if slowMo is None: slowMo = 0 @@ -219,7 +226,7 @@ async def connect( "wsEndpoint": wsEndpoint, "headers": headers, "slowMo": slowMo, - "timeout": timeout, + "timeout": timeout if timeout is not None else 0, "exposeNetwork": exposeNetwork, }, ) @@ -259,7 +266,10 @@ def handle_transport_close(reason: Optional[str]) -> None: connection._loop.create_task(connection.run()) playwright_future = connection.playwright_future - timeout_future = throw_on_timeout(timeout, Error("Connection timed out")) + timeout_future = throw_on_timeout( + timeout if timeout is not None else PLAYWRIGHT_MAX_DEADLINE, + Error("Connection timed out"), + ) done, pending = await asyncio.wait( {transport.on_error_future, playwright_future, timeout_future}, return_when=asyncio.FIRST_COMPLETED, @@ -274,18 +284,59 @@ def handle_transport_close(reason: Optional[str]) -> None: pre_launched_browser = playwright._initializer.get("preLaunchedBrowser") assert pre_launched_browser browser = cast(Browser, from_channel(pre_launched_browser)) - self._did_launch_browser(browser) browser._should_close_connection_on_close = True + browser._connect_to_browser_type(self, None) return browser - def _did_create_context( - self, context: BrowserContext, context_options: Dict, browser_options: Dict - ) -> None: - context._set_options(context_options, browser_options) + async def _prepare_browser_context_params(self, params: Dict) -> None: + if params.get("noViewport"): + del params["noViewport"] + params["noDefaultViewport"] = True + if "defaultBrowserType" in params: + del params["defaultBrowserType"] + if "extraHTTPHeaders" in params: + params["extraHTTPHeaders"] = serialize_headers(params["extraHTTPHeaders"]) + if "recordVideoDir" in params: + params["recordVideo"] = {"dir": Path(params["recordVideoDir"]).absolute()} + if "recordVideoSize" in params: + params["recordVideo"]["size"] = params["recordVideoSize"] + del params["recordVideoSize"] + del params["recordVideoDir"] + if "storageState" in params: + storageState = params["storageState"] + if not isinstance(storageState, dict): + params["storageState"] = json.loads( + (await async_readfile(storageState)).decode() + ) + if params.get("colorScheme", None) == "null": + params["colorScheme"] = "no-override" + if params.get("reducedMotion", None) == "null": + params["reducedMotion"] = "no-override" + if params.get("forcedColors", None) == "null": + params["forcedColors"] = "no-override" + if params.get("contrast", None) == "null": + params["contrast"] = "no-override" + if "acceptDownloads" in params: + params["acceptDownloads"] = ( + "accept" if params["acceptDownloads"] else "deny" + ) + + if "clientCertificates" in params: + params["clientCertificates"] = await to_client_certificates_protocol( + params["clientCertificates"] + ) + params["selectorEngines"] = self._playwright.selectors._selector_engines + params["testIdAttributeName"] = ( + self._playwright.selectors._test_id_attribute_name + ) - def _did_launch_browser(self, browser: Browser) -> None: - browser._browser_type = self + # Remove HAR options + params.pop("recordHarPath", None) + params.pop("recordHarOmitContent", None) + params.pop("recordHarUrlFilter", None) + params.pop("recordHarMode", None) + params.pop("recordHarContent", None) def normalize_launch_params(params: Dict) -> None: @@ -304,3 +355,4 @@ def normalize_launch_params(params: Dict) -> None: params["downloadsPath"] = str(Path(params["downloadsPath"])) if "tracesDir" in params: params["tracesDir"] = str(Path(params["tracesDir"])) + params["timeout"] = TimeoutSettings.launch_timeout(params.get("timeout")) diff --git a/playwright/_impl/_connection.py b/playwright/_impl/_connection.py index 1328e7c97..3519aeebd 100644 --- a/playwright/_impl/_connection.py +++ b/playwright/_impl/_connection.py @@ -55,26 +55,48 @@ def __init__(self, connection: "Connection", object: "ChannelOwner") -> None: self._guid = object._guid self._object = object self.on("error", lambda exc: self._connection._on_event_listener_error(exc)) - self._is_internal_type = False + self._timeout_calculator: Optional[Callable[[Optional[float]], float]] = None - async def send(self, method: str, params: Dict = None) -> Any: + async def send( + self, + method: str, + params: Dict = None, + is_internal: bool = False, + title: str = None, + ) -> Any: return await self._connection.wrap_api_call( lambda: self._inner_send(method, params, False), - self._is_internal_type, + is_internal, + title, ) - async def send_return_as_dict(self, method: str, params: Dict = None) -> Any: + async def send_return_as_dict( + self, + method: str, + params: Dict = None, + is_internal: bool = False, + title: str = None, + ) -> Any: return await self._connection.wrap_api_call( lambda: self._inner_send(method, params, True), - self._is_internal_type, + is_internal, + title, ) - def send_no_reply(self, method: str, params: Dict = None) -> None: + def send_no_reply( + self, + method: str, + params: Dict = None, + is_internal: bool = False, + title: str = None, + ) -> None: # No reply messages are used to e.g. waitForEventInfo(after). self._connection.wrap_api_call_sync( lambda: self._connection._send_message_to_server( self._object, method, {} if params is None else params, True - ) + ), + is_internal, + title, ) async def _inner_send( @@ -82,6 +104,8 @@ async def _inner_send( ) -> Any: if params is None: params = {} + if self._timeout_calculator is not None: + params["timeout"] = self._timeout_calculator(params.get("timeout")) if self._connection._error: error = self._connection._error self._connection._error = None @@ -112,8 +136,10 @@ async def _inner_send( key = next(iter(result)) return result[key] - def mark_as_internal_type(self) -> None: - self._is_internal_type = True + def _set_timeout_calculator( + self, timeout_calculator: Callable[[Optional[float]], float] + ) -> None: + self._timeout_calculator = timeout_calculator class ChannelOwner(AsyncIOEventEmitter): @@ -355,6 +381,9 @@ def _send_message_to_server( } if location: metadata["location"] = location # type: ignore + title = stack_trace_information["title"] + if title: + metadata["title"] = title message = { "id": id, "guid": object._guid, @@ -507,7 +536,7 @@ def _replace_guids_with_channels(self, payload: Any) -> Any: return payload async def wrap_api_call( - self, cb: Callable[[], Any], is_internal: bool = False + self, cb: Callable[[], Any], is_internal: bool = False, title: str = None ) -> Any: if self._api_zone.get(): return await cb() @@ -516,7 +545,7 @@ async def wrap_api_call( task, "__pw_stack__", None ) or inspect.stack(0) - parsed_st = _extract_stack_trace_information_from_stack(st, is_internal) + parsed_st = _extract_stack_trace_information_from_stack(st, is_internal, title) self._api_zone.set(parsed_st) try: return await cb() @@ -526,7 +555,7 @@ async def wrap_api_call( self._api_zone.set(None) def wrap_api_call_sync( - self, cb: Callable[[], Any], is_internal: bool = False + self, cb: Callable[[], Any], is_internal: bool = False, title: str = None ) -> Any: if self._api_zone.get(): return cb() @@ -534,7 +563,7 @@ def wrap_api_call_sync( st: List[inspect.FrameInfo] = getattr( task, "__pw_stack__", None ) or inspect.stack(0) - parsed_st = _extract_stack_trace_information_from_stack(st, is_internal) + parsed_st = _extract_stack_trace_information_from_stack(st, is_internal, title) self._api_zone.set(parsed_st) try: return cb() @@ -562,10 +591,11 @@ class StackFrame(TypedDict): class ParsedStackTrace(TypedDict): frames: List[StackFrame] apiName: Optional[str] + title: Optional[str] def _extract_stack_trace_information_from_stack( - st: List[inspect.FrameInfo], is_internal: bool + st: List[inspect.FrameInfo], is_internal: bool, title: str = None ) -> ParsedStackTrace: playwright_module_path = str(Path(playwright.__file__).parents[0]) last_internal_api_name = "" @@ -605,11 +635,22 @@ def _extract_stack_trace_information_from_stack( return { "frames": parsed_frames, "apiName": "" if is_internal else api_name, + "title": title, } def _filter_none(d: Mapping) -> Dict: - return {k: v for k, v in d.items() if v is not None} + result = {} + for k, v in d.items(): + if v is None: + continue + elif isinstance(v, dict): + filtered_v = _filter_none(v) + if filtered_v: + result[k] = filtered_v + else: + result[k] = v + return result def format_call_log(log: Optional[List[str]]) -> str: diff --git a/playwright/_impl/_element_handle.py b/playwright/_impl/_element_handle.py index cb3d672d4..fd99c0b00 100644 --- a/playwright/_impl/_element_handle.py +++ b/playwright/_impl/_element_handle.py @@ -55,6 +55,8 @@ def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) + self._frame = cast("Frame", parent) + self._channel._set_timeout_calculator(self._frame._timeout) async def _createSelectorForTest(self, name: str) -> Optional[str]: return await self._channel.send("createSelectorForTest", dict(name=name)) @@ -204,7 +206,7 @@ async def set_input_files( await self._channel.send( "setInputFiles", { - "timeout": timeout, + "timeout": self._frame._timeout(timeout), **converted, }, ) diff --git a/playwright/_impl/_fetch.py b/playwright/_impl/_fetch.py index 88f5810ee..a0120e0cd 100644 --- a/playwright/_impl/_fetch.py +++ b/playwright/_impl/_fetch.py @@ -36,6 +36,7 @@ Error, NameValue, TargetClosedError, + TimeoutSettings, async_readfile, async_writefile, is_file_payload, @@ -92,6 +93,7 @@ async def new_context( APIRequestContext, from_channel(await self.playwright._channel.send("newRequest", params)), ) + context._timeout_settings.set_default_timeout(timeout) return context @@ -102,6 +104,8 @@ def __init__( super().__init__(parent, type, guid, initializer) self._tracing: Tracing = from_channel(initializer["tracing"]) self._close_reason: Optional[str] = None + self._timeout_settings = TimeoutSettings(None) + self._channel._set_timeout_calculator(self._timeout_settings.timeout) async def dispose(self, reason: str = None) -> None: self._close_reason = reason @@ -414,7 +418,6 @@ async def _inner_fetch( "jsonData": json_data, "formData": form_data, "multipartData": multipart_data, - "timeout": timeout, "failOnStatusCode": failOnStatusCode, "ignoreHTTPSErrors": ignoreHTTPSErrors, "maxRedirects": maxRedirects, diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py index d616046e6..fc3c4a54d 100644 --- a/playwright/_impl/_frame.py +++ b/playwright/_impl/_frame.py @@ -19,6 +19,7 @@ Any, Dict, List, + Literal, Optional, Pattern, Sequence, @@ -42,8 +43,8 @@ DocumentLoadState, FrameNavigatedEvent, KeyboardModifier, - Literal, MouseButton, + TimeoutSettings, URLMatch, async_readfile, locals_to_params, @@ -100,6 +101,7 @@ def __init__( "navigated", lambda params: self._on_frame_navigated(params), ) + self._channel._set_timeout_calculator(self._timeout) def __repr__(self) -> str: return f"" @@ -142,7 +144,9 @@ async def goto( return cast( Optional[Response], from_nullable_channel( - await self._channel.send("goto", locals_to_params(locals())) + await self._channel.send( + "goto", self._locals_to_params_with_navigation_timeout(locals()) + ) ), ) @@ -163,8 +167,7 @@ def _setup_navigation_waiter(self, wait_name: str, timeout: float = None) -> Wai Error("Navigating frame was detached!"), lambda frame: frame == self, ) - if timeout is None: - timeout = self._page._timeout_settings.navigation_timeout() + timeout = self._page._timeout_settings.navigation_timeout(timeout) waiter.reject_on_timeout(timeout, f"Timeout {timeout}ms exceeded.") return waiter @@ -270,6 +273,18 @@ def handle_load_state_event(actual_state: str) -> bool: ) await waiter.result() + def _timeout(self, timeout: Optional[float]) -> float: + timeout_settings = ( + self._page._timeout_settings if self._page else TimeoutSettings(None) + ) + return timeout_settings.timeout(timeout) + + def _navigation_timeout(self, timeout: Optional[float]) -> float: + timeout_settings = ( + self._page._timeout_settings if self._page else TimeoutSettings(None) + ) + return timeout_settings.navigation_timeout(timeout) + async def frame_element(self) -> ElementHandle: return from_channel(await self._channel.send("frameElement")) @@ -343,14 +358,10 @@ async def is_enabled( ) -> bool: return await self._channel.send("isEnabled", locals_to_params(locals())) - async def is_hidden( - self, selector: str, strict: bool = None, timeout: float = None - ) -> bool: + async def is_hidden(self, selector: str, strict: bool = None) -> bool: return await self._channel.send("isHidden", locals_to_params(locals())) - async def is_visible( - self, selector: str, strict: bool = None, timeout: float = None - ) -> bool: + async def is_visible(self, selector: str, strict: bool = None) -> bool: return await self._channel.send("isVisible", locals_to_params(locals())) async def dispatch_event( @@ -421,7 +432,9 @@ async def set_content( timeout: float = None, waitUntil: DocumentLoadState = None, ) -> None: - await self._channel.send("setContent", locals_to_params(locals())) + await self._channel.send( + "setContent", self._locals_to_params_with_navigation_timeout(locals()) + ) @property def name(self) -> str: @@ -500,7 +513,9 @@ async def dblclick( strict: bool = None, trial: bool = None, ) -> None: - await self._channel.send("dblclick", locals_to_params(locals())) + await self._channel.send( + "dblclick", locals_to_params(locals()), title="Double click" + ) async def tap( self, @@ -701,7 +716,7 @@ async def set_input_files( { "selector": selector, "strict": strict, - "timeout": timeout, + "timeout": self._timeout(timeout), **converted, }, ) @@ -805,3 +820,8 @@ async def set_checked( async def _highlight(self, selector: str) -> None: await self._channel.send("highlight", {"selector": selector}) + + def _locals_to_params_with_navigation_timeout(self, args: Dict) -> Dict: + params = locals_to_params(args) + params["timeout"] = self._navigation_timeout(params.get("timeout")) + return params diff --git a/playwright/_impl/_helper.py b/playwright/_impl/_helper.py index 96acb8857..67a096dc5 100644 --- a/playwright/_impl/_helper.py +++ b/playwright/_impl/_helper.py @@ -250,7 +250,21 @@ class HarLookupResult(TypedDict, total=False): body: Optional[str] +DEFAULT_PLAYWRIGHT_TIMEOUT_IN_MILLISECONDS = 30000 +DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT_IN_MILLISECONDS = 180000 +PLAYWRIGHT_MAX_DEADLINE = 2147483647 # 2^31-1 + + class TimeoutSettings: + + @staticmethod + def launch_timeout(timeout: Optional[float] = None) -> float: + return ( + timeout + if timeout is not None + else DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT_IN_MILLISECONDS + ) + def __init__(self, parent: Optional["TimeoutSettings"]) -> None: self._parent = parent self._default_timeout: Optional[float] = None @@ -266,7 +280,7 @@ def timeout(self, timeout: float = None) -> float: return self._default_timeout if self._parent: return self._parent.timeout() - return 30000 + return DEFAULT_PLAYWRIGHT_TIMEOUT_IN_MILLISECONDS def set_default_navigation_timeout( self, navigation_timeout: Optional[float] @@ -279,12 +293,16 @@ def default_navigation_timeout(self) -> Optional[float]: def default_timeout(self) -> Optional[float]: return self._default_timeout - def navigation_timeout(self) -> float: + def navigation_timeout(self, timeout: float = None) -> float: + if timeout is not None: + return timeout if self._default_navigation_timeout is not None: return self._default_navigation_timeout + if self._default_timeout is not None: + return self._default_timeout if self._parent: return self._parent.navigation_timeout() - return 30000 + return DEFAULT_PLAYWRIGHT_TIMEOUT_IN_MILLISECONDS def serialize_error(ex: Exception, tb: Optional[TracebackType]) -> ErrorPayload: diff --git a/playwright/_impl/_input.py b/playwright/_impl/_input.py index a97ba5d11..0e986ae8c 100644 --- a/playwright/_impl/_input.py +++ b/playwright/_impl/_input.py @@ -61,6 +61,17 @@ async def up( ) -> None: await self._channel.send("mouseUp", locals_to_params(locals())) + async def _click( + self, + x: float, + y: float, + delay: float = None, + button: MouseButton = None, + clickCount: int = None, + title: str = None, + ) -> None: + await self._channel.send("mouseClick", locals_to_params(locals()), title=title) + async def click( self, x: float, @@ -69,7 +80,9 @@ async def click( button: MouseButton = None, clickCount: int = None, ) -> None: - await self._channel.send("mouseClick", locals_to_params(locals())) + params = locals() + del params["self"] + await self._click(**params) async def dblclick( self, @@ -78,7 +91,9 @@ async def dblclick( delay: float = None, button: MouseButton = None, ) -> None: - await self.click(x, y, delay=delay, button=button, clickCount=2) + await self._click( + x, y, delay=delay, button=button, clickCount=2, title="Double click" + ) async def wheel(self, deltaX: float, deltaY: float) -> None: await self._channel.send("mouseWheel", locals_to_params(locals())) diff --git a/playwright/_impl/_local_utils.py b/playwright/_impl/_local_utils.py index 5ea8b644d..7172ee58a 100644 --- a/playwright/_impl/_local_utils.py +++ b/playwright/_impl/_local_utils.py @@ -25,7 +25,6 @@ def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) - self._channel.mark_as_internal_type() self.devices = { device["name"]: parse_device_descriptor(device["descriptor"]) for device in initializer["deviceDescriptors"] diff --git a/playwright/_impl/_locator.py b/playwright/_impl/_locator.py index 189485f47..9d190c453 100644 --- a/playwright/_impl/_locator.py +++ b/playwright/_impl/_locator.py @@ -107,7 +107,7 @@ async def _with_element( task: Callable[[ElementHandle, float], Awaitable[T]], timeout: float = None, ) -> T: - timeout = self._frame.page._timeout_settings.timeout(timeout) + timeout = self._frame._timeout(timeout) deadline = (monotonic_time() + timeout) if timeout else 0 handle = await self.element_handle(timeout=timeout) if not handle: @@ -336,6 +336,12 @@ def nth(self, index: int) -> "Locator": def content_frame(self) -> "FrameLocator": return FrameLocator(self._frame, self._selector) + def describe(self, description: str) -> "Locator": + return Locator( + self._frame, + f"{self._selector} >> internal:describe={json.dumps(description)}", + ) + def filter( self, hasText: Union[str, Pattern[str]] = None, @@ -540,7 +546,7 @@ async def screenshot( ), ) - async def aria_snapshot(self, timeout: float = None, ref: bool = None) -> str: + async def aria_snapshot(self, timeout: float = None) -> str: return await self._frame._channel.send( "ariaSnapshot", { @@ -711,7 +717,10 @@ async def set_checked( ) async def _expect( - self, expression: str, options: FrameExpectOptions + self, + expression: str, + options: FrameExpectOptions, + title: str = None, ) -> FrameExpectResult: if "expectedValue" in options: options["expectedValue"] = serialize_argument(options["expectedValue"]) @@ -722,6 +731,7 @@ async def _expect( "expression": expression, **options, }, + title=title, ) if result.get("received"): result["received"] = parse_value(result["received"]) diff --git a/playwright/_impl/_network.py b/playwright/_impl/_network.py index 768c22f0c..748967dd8 100644 --- a/playwright/_impl/_network.py +++ b/playwright/_impl/_network.py @@ -131,7 +131,6 @@ def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) - self._channel.mark_as_internal_type() self._redirected_from: Optional["Request"] = from_nullable_channel( initializer.get("redirectedFrom") ) @@ -292,7 +291,7 @@ async def _actual_headers(self) -> "RawHeaders": return RawHeaders(serialize_headers(override)) if not self._all_headers_future: self._all_headers_future = asyncio.Future() - headers = await self._channel.send("rawRequestHeaders") + headers = await self._channel.send("rawRequestHeaders", is_internal=True) self._all_headers_future.set_result(RawHeaders(headers)) return await self._all_headers_future @@ -319,7 +318,6 @@ def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) - self._channel.mark_as_internal_type() self._handling_future: Optional[asyncio.Future["bool"]] = None self._context: "BrowserContext" = cast("BrowserContext", None) self._did_throw = False @@ -610,7 +608,6 @@ def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) - self._channel.mark_as_internal_type() self._on_page_message: Optional[Callable[[Union[str, bytes]], Any]] = None self._on_page_close: Optional[Callable[[Optional[int], Optional[str]], Any]] = ( None @@ -768,7 +765,6 @@ def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) - self._channel.mark_as_internal_type() self._request: Request = from_channel(self._initializer["request"]) timing = self._initializer["timing"] self._request._timing["startTime"] = timing["startTime"] diff --git a/playwright/_impl/_object_factory.py b/playwright/_impl/_object_factory.py index 5f38b781b..b44009bc3 100644 --- a/playwright/_impl/_object_factory.py +++ b/playwright/_impl/_object_factory.py @@ -35,7 +35,6 @@ ) from playwright._impl._page import BindingCall, Page, Worker from playwright._impl._playwright import Playwright -from playwright._impl._selectors import SelectorsOwner from playwright._impl._stream import Stream from playwright._impl._tracing import Tracing from playwright._impl._writable_stream import WritableStream @@ -100,6 +99,4 @@ def create_remote_object( return Worker(parent, type, guid, initializer) if type == "WritableStream": return WritableStream(parent, type, guid, initializer) - if type == "Selectors": - return SelectorsOwner(parent, type, guid, initializer) return DummyObject(parent, type, guid, initializer) diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index 6327cce70..82b43a231 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -227,6 +227,7 @@ def __init__( ), ) self._channel.on("video", lambda params: self._on_video(params)) + self._channel.on("viewportSizeChanged", self._on_viewport_size_changed) self._channel.on( "webSocket", lambda params: self.emit( @@ -236,6 +237,7 @@ def __init__( self._channel.on( "worker", lambda params: self._on_worker(from_channel(params["worker"])) ) + self._channel._set_timeout_calculator(self._timeout_settings.timeout) self._closed_or_crashed_future: asyncio.Future = asyncio.Future() self.on( Page.Events.Close, @@ -286,7 +288,7 @@ async def _on_route(self, route: Route) -> None: route_handlers = self._routes.copy() for route_handler in route_handlers: # If the page was closed we stall all requests right away. - if self._close_was_called or self.context._close_was_called: + if self._close_was_called or self.context._closing_or_closed: return if not route_handler.matches(route.request.url): continue @@ -363,6 +365,9 @@ def _on_video(self, params: Any) -> None: artifact = from_channel(params["artifact"]) self._force_video()._artifact_ready(artifact) + def _on_viewport_size_changed(self, params: Any) -> None: + self._viewport_size = params["viewportSize"] + @property def context(self) -> "BrowserContext": return self._browser_context @@ -397,13 +402,9 @@ def frames(self) -> List[Frame]: def set_default_navigation_timeout(self, timeout: float) -> None: self._timeout_settings.set_default_navigation_timeout(timeout) - self._channel.send_no_reply( - "setDefaultNavigationTimeoutNoReply", dict(timeout=timeout) - ) def set_default_timeout(self, timeout: float) -> None: self._timeout_settings.set_default_timeout(timeout) - self._channel.send_no_reply("setDefaultTimeoutNoReply", dict(timeout=timeout)) async def query_selector( self, @@ -557,7 +558,9 @@ async def reload( waitUntil: DocumentLoadState = None, ) -> Optional[Response]: return from_nullable_channel( - await self._channel.send("reload", locals_to_params(locals())) + await self._channel.send( + "reload", self._locals_to_params_with_navigation_timeout(locals()) + ) ) async def wait_for_load_state( @@ -588,7 +591,9 @@ async def go_back( waitUntil: DocumentLoadState = None, ) -> Optional[Response]: return from_nullable_channel( - await self._channel.send("goBack", locals_to_params(locals())) + await self._channel.send( + "goBack", self._locals_to_params_with_navigation_timeout(locals()) + ) ) async def go_forward( @@ -597,7 +602,9 @@ async def go_forward( waitUntil: DocumentLoadState = None, ) -> Optional[Response]: return from_nullable_channel( - await self._channel.send("goForward", locals_to_params(locals())) + await self._channel.send( + "goForward", self._locals_to_params_with_navigation_timeout(locals()) + ) ) async def request_gc(self) -> None: @@ -1400,6 +1407,13 @@ async def remove_locator_handler(self, locator: "Locator") -> None: del self._locator_handlers[uid] self._channel.send_no_reply("unregisterLocatorHandler", {"uid": uid}) + def _locals_to_params_with_navigation_timeout(self, args: Dict) -> Dict: + params = locals_to_params(args) + params["timeout"] = self._timeout_settings.navigation_timeout( + params.get("timeout") + ) + return params + class Worker(ChannelOwner): Events = SimpleNamespace(Close="close") diff --git a/playwright/_impl/_playwright.py b/playwright/_impl/_playwright.py index c02e73316..5c0151158 100644 --- a/playwright/_impl/_playwright.py +++ b/playwright/_impl/_playwright.py @@ -17,7 +17,7 @@ from playwright._impl._browser_type import BrowserType from playwright._impl._connection import ChannelOwner, from_channel from playwright._impl._fetch import APIRequest -from playwright._impl._selectors import Selectors, SelectorsOwner +from playwright._impl._selectors import Selectors class Playwright(ChannelOwner): @@ -41,12 +41,7 @@ def __init__( self.webkit._playwright = self self.selectors = Selectors(self._loop, self._dispatcher_fiber) - selectors_owner: SelectorsOwner = from_channel(initializer["selectors"]) - self.selectors._add_channel(selectors_owner) - self._connection.on( - "close", lambda: self.selectors._remove_channel(selectors_owner) - ) self.devices = self._connection.local_utils.devices def __getitem__(self, value: str) -> "BrowserType": @@ -59,10 +54,7 @@ def __getitem__(self, value: str) -> "BrowserType": raise ValueError("Invalid browser " + value) def _set_selectors(self, selectors: Selectors) -> None: - selectors_owner = from_channel(self._initializer["selectors"]) - self.selectors._remove_channel(selectors_owner) self.selectors = selectors - self.selectors._add_channel(selectors_owner) async def stop(self) -> None: pass diff --git a/playwright/_impl/_selectors.py b/playwright/_impl/_selectors.py index cf8af8c06..b0cf5afbb 100644 --- a/playwright/_impl/_selectors.py +++ b/playwright/_impl/_selectors.py @@ -14,20 +14,21 @@ import asyncio from pathlib import Path -from typing import Any, Dict, List, Set, Union +from typing import Any, Dict, List, Optional, Set, Union -from playwright._impl._connection import ChannelOwner +from playwright._impl._browser_context import BrowserContext from playwright._impl._errors import Error from playwright._impl._helper import async_readfile -from playwright._impl._locator import set_test_id_attribute_name, test_id_attribute_name +from playwright._impl._locator import set_test_id_attribute_name class Selectors: def __init__(self, loop: asyncio.AbstractEventLoop, dispatcher_fiber: Any) -> None: self._loop = loop - self._channels: Set[SelectorsOwner] = set() - self._registrations: List[Dict] = [] + self._contexts_for_selectors: Set[BrowserContext] = set() + self._selector_engines: List[Dict] = [] self._dispatcher_fiber = dispatcher_fiber + self._test_id_attribute_name: Optional[str] = None async def register( self, @@ -40,37 +41,19 @@ async def register( raise Error("Either source or path should be specified") if path: script = (await async_readfile(path)).decode() - params: Dict[str, Any] = dict(name=name, source=script) + engine: Dict[str, Any] = dict(name=name, source=script) if contentScript: - params["contentScript"] = True - for channel in self._channels: - await channel._channel.send("register", params) - self._registrations.append(params) + engine["contentScript"] = contentScript + for context in self._contexts_for_selectors: + await context._channel.send( + "registerSelectorEngine", {"selectorEngine": engine} + ) + self._selector_engines.append(engine) def set_test_id_attribute(self, attributeName: str) -> None: set_test_id_attribute_name(attributeName) - for channel in self._channels: - channel._channel.send_no_reply( + self._test_id_attribute_name = attributeName + for context in self._contexts_for_selectors: + context._channel.send_no_reply( "setTestIdAttributeName", {"testIdAttributeName": attributeName} ) - - def _add_channel(self, channel: "SelectorsOwner") -> None: - self._channels.add(channel) - for params in self._registrations: - # This should not fail except for connection closure, but just in case we catch. - channel._channel.send_no_reply("register", params) - channel._channel.send_no_reply( - "setTestIdAttributeName", - {"testIdAttributeName": test_id_attribute_name()}, - ) - - def _remove_channel(self, channel: "SelectorsOwner") -> None: - if channel in self._channels: - self._channels.remove(channel) - - -class SelectorsOwner(ChannelOwner): - def __init__( - self, parent: ChannelOwner, type: str, guid: str, initializer: Dict - ) -> None: - super().__init__(parent, type, guid, initializer) diff --git a/playwright/_impl/_tracing.py b/playwright/_impl/_tracing.py index a68b53bf7..e984bcbad 100644 --- a/playwright/_impl/_tracing.py +++ b/playwright/_impl/_tracing.py @@ -26,7 +26,6 @@ def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) - self._channel.mark_as_internal_type() self._include_sources: bool = False self._stacks_id: Optional[str] = None self._is_tracing: bool = False diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index b622ab858..5f0af8bf0 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -674,7 +674,7 @@ async def fulfill( headers: typing.Optional[typing.Dict[str, str]] = None, body: typing.Optional[typing.Union[str, bytes]] = None, json: typing.Optional[typing.Any] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, content_type: typing.Optional[str] = None, response: typing.Optional["APIResponse"] = None, ) -> None: @@ -2770,7 +2770,7 @@ async def screenshot( *, timeout: typing.Optional[float] = None, type: typing.Optional[Literal["jpeg", "png"]] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, quality: typing.Optional[int] = None, omit_background: typing.Optional[bool] = None, animations: typing.Optional[Literal["allow", "disabled"]] = None, @@ -3918,11 +3918,7 @@ async def is_enabled( ) async def is_hidden( - self, - selector: str, - *, - strict: typing.Optional[bool] = None, - timeout: typing.Optional[float] = None, + self, selector: str, *, strict: typing.Optional[bool] = None ) -> bool: """Frame.is_hidden @@ -3937,8 +3933,6 @@ async def is_hidden( strict : Union[bool, None] When true, the call requires selector to resolve to a single element. If given selector resolves to more than one element, the call throws an exception. - timeout : Union[float, None] - Deprecated: This option is ignored. `frame.is_hidden()` does not wait for the element to become hidden and returns immediately. Returns ------- @@ -3946,17 +3940,11 @@ async def is_hidden( """ return mapping.from_maybe_impl( - await self._impl_obj.is_hidden( - selector=selector, strict=strict, timeout=timeout - ) + await self._impl_obj.is_hidden(selector=selector, strict=strict) ) async def is_visible( - self, - selector: str, - *, - strict: typing.Optional[bool] = None, - timeout: typing.Optional[float] = None, + self, selector: str, *, strict: typing.Optional[bool] = None ) -> bool: """Frame.is_visible @@ -3971,8 +3959,6 @@ async def is_visible( strict : Union[bool, None] When true, the call requires selector to resolve to a single element. If given selector resolves to more than one element, the call throws an exception. - timeout : Union[float, None] - Deprecated: This option is ignored. `frame.is_visible()` does not wait for the element to become visible and returns immediately. Returns ------- @@ -3980,9 +3966,7 @@ async def is_visible( """ return mapping.from_maybe_impl( - await self._impl_obj.is_visible( - selector=selector, strict=strict, timeout=timeout - ) + await self._impl_obj.is_visible(selector=selector, strict=strict) ) async def dispatch_event( @@ -4216,7 +4200,7 @@ async def add_script_tag( self, *, url: typing.Optional[str] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, content: typing.Optional[str] = None, type: typing.Optional[str] = None, ) -> "ElementHandle": @@ -4254,7 +4238,7 @@ async def add_style_tag( self, *, url: typing.Optional[str] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, content: typing.Optional[str] = None, ) -> "ElementHandle": """Frame.add_style_tag @@ -4576,8 +4560,8 @@ def locator( self, selector: str, *, - has_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - has_not_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + has_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + has_not_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, has: typing.Optional["Locator"] = None, has_not: typing.Optional["Locator"] = None, ) -> "Locator": @@ -4844,7 +4828,7 @@ def get_by_role( expanded: typing.Optional[bool] = None, include_hidden: typing.Optional[bool] = None, level: typing.Optional[int] = None, - name: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + name: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, pressed: typing.Optional[bool] = None, selected: typing.Optional[bool] = None, exact: typing.Optional[bool] = None, @@ -6061,8 +6045,8 @@ def locator( self, selector_or_locator: typing.Union["Locator", str], *, - has_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - has_not_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + has_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + has_not_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, has: typing.Optional["Locator"] = None, has_not: typing.Optional["Locator"] = None, ) -> "Locator": @@ -6326,7 +6310,7 @@ def get_by_role( expanded: typing.Optional[bool] = None, include_hidden: typing.Optional[bool] = None, level: typing.Optional[int] = None, - name: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + name: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, pressed: typing.Optional[bool] = None, selected: typing.Optional[bool] = None, exact: typing.Optional[bool] = None, @@ -6722,7 +6706,7 @@ async def register( name: str, script: typing.Optional[str] = None, *, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, content_script: typing.Optional[bool] = None, ) -> None: """Selectors.register @@ -8662,7 +8646,7 @@ async def add_script_tag( self, *, url: typing.Optional[str] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, content: typing.Optional[str] = None, type: typing.Optional[str] = None, ) -> "ElementHandle": @@ -8699,7 +8683,7 @@ async def add_style_tag( self, *, url: typing.Optional[str] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, content: typing.Optional[str] = None, ) -> "ElementHandle": """Page.add_style_tag @@ -9385,7 +9369,7 @@ async def add_init_script( self, script: typing.Optional[str] = None, *, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, ) -> None: """Page.add_init_script @@ -9607,7 +9591,7 @@ async def route_from_har( self, har: typing.Union[pathlib.Path, str], *, - url: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + url: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, not_found: typing.Optional[Literal["abort", "fallback"]] = None, update: typing.Optional[bool] = None, update_content: typing.Optional[Literal["attach", "embed"]] = None, @@ -9663,7 +9647,7 @@ async def screenshot( *, timeout: typing.Optional[float] = None, type: typing.Optional[Literal["jpeg", "png"]] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, quality: typing.Optional[int] = None, omit_background: typing.Optional[bool] = None, full_page: typing.Optional[bool] = None, @@ -10097,8 +10081,8 @@ def locator( self, selector: str, *, - has_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - has_not_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + has_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + has_not_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, has: typing.Optional["Locator"] = None, has_not: typing.Optional["Locator"] = None, ) -> "Locator": @@ -10363,7 +10347,7 @@ def get_by_role( expanded: typing.Optional[bool] = None, include_hidden: typing.Optional[bool] = None, level: typing.Optional[int] = None, - name: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + name: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, pressed: typing.Optional[bool] = None, selected: typing.Optional[bool] = None, exact: typing.Optional[bool] = None, @@ -11512,7 +11496,7 @@ async def pdf( height: typing.Optional[typing.Union[str, float]] = None, prefer_css_page_size: typing.Optional[bool] = None, margin: typing.Optional[PdfMargins] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, outline: typing.Optional[bool] = None, tagged: typing.Optional[bool] = None, ) -> bytes: @@ -12836,9 +12820,9 @@ async def add_cookies(self, cookies: typing.Sequence[SetCookieParam]) -> None: async def clear_cookies( self, *, - name: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - domain: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - path: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + name: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + domain: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + path: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, ) -> None: """BrowserContext.clear_cookies @@ -12991,7 +12975,7 @@ async def add_init_script( self, script: typing.Optional[str] = None, *, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, ) -> None: """BrowserContext.add_init_script @@ -13340,7 +13324,7 @@ async def route_from_har( self, har: typing.Union[pathlib.Path, str], *, - url: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + url: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, not_found: typing.Optional[Literal["abort", "fallback"]] = None, update: typing.Optional[bool] = None, update_content: typing.Optional[Literal["attach", "embed"]] = None, @@ -13450,7 +13434,7 @@ async def close(self, *, reason: typing.Optional[str] = None) -> None: async def storage_state( self, *, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, indexed_db: typing.Optional[bool] = None, ) -> StorageState: """BrowserContext.storage_state @@ -13752,9 +13736,9 @@ async def new_context( accept_downloads: typing.Optional[bool] = None, default_browser_type: typing.Optional[str] = None, proxy: typing.Optional[ProxySettings] = None, - record_har_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + record_har_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, record_har_omit_content: typing.Optional[bool] = None, - record_video_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, + record_video_dir: typing.Optional[typing.Union[pathlib.Path, str]] = None, record_video_size: typing.Optional[ViewportSize] = None, storage_state: typing.Optional[ typing.Union[StorageState, str, pathlib.Path] @@ -13763,7 +13747,7 @@ async def new_context( strict_selectors: typing.Optional[bool] = None, service_workers: typing.Optional[Literal["allow", "block"]] = None, record_har_url_filter: typing.Optional[ - typing.Union[str, typing.Pattern[str]] + typing.Union[typing.Pattern[str], str] ] = None, record_har_mode: typing.Optional[Literal["full", "minimal"]] = None, record_har_content: typing.Optional[Literal["attach", "embed", "omit"]] = None, @@ -13999,9 +13983,9 @@ async def new_page( accept_downloads: typing.Optional[bool] = None, default_browser_type: typing.Optional[str] = None, proxy: typing.Optional[ProxySettings] = None, - record_har_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + record_har_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, record_har_omit_content: typing.Optional[bool] = None, - record_video_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, + record_video_dir: typing.Optional[typing.Union[pathlib.Path, str]] = None, record_video_size: typing.Optional[ViewportSize] = None, storage_state: typing.Optional[ typing.Union[StorageState, str, pathlib.Path] @@ -14010,7 +13994,7 @@ async def new_page( strict_selectors: typing.Optional[bool] = None, service_workers: typing.Optional[Literal["allow", "block"]] = None, record_har_url_filter: typing.Optional[ - typing.Union[str, typing.Pattern[str]] + typing.Union[typing.Pattern[str], str] ] = None, record_har_mode: typing.Optional[Literal["full", "minimal"]] = None, record_har_content: typing.Optional[Literal["attach", "embed", "omit"]] = None, @@ -14240,7 +14224,7 @@ async def start_tracing( self, *, page: typing.Optional["Page"] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, screenshots: typing.Optional[bool] = None, categories: typing.Optional[typing.Sequence[str]] = None, ) -> None: @@ -14333,7 +14317,7 @@ def executable_path(self) -> str: async def launch( self, *, - executable_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + executable_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, channel: typing.Optional[str] = None, args: typing.Optional[typing.Sequence[str]] = None, ignore_default_args: typing.Optional[ @@ -14347,9 +14331,9 @@ async def launch( headless: typing.Optional[bool] = None, devtools: typing.Optional[bool] = None, proxy: typing.Optional[ProxySettings] = None, - downloads_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + downloads_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, slow_mo: typing.Optional[float] = None, - traces_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, + traces_dir: typing.Optional[typing.Union[pathlib.Path, str]] = None, chromium_sandbox: typing.Optional[bool] = None, firefox_user_prefs: typing.Optional[ typing.Dict[str, typing.Union[str, float, bool]] @@ -14442,6 +14426,9 @@ async def launch( Firefox user preferences. Learn more about the Firefox user preferences at [`about:config`](https://support.mozilla.org/en-US/kb/about-config-editor-firefox). + You can also provide a path to a custom [`policies.json` file](https://mozilla.github.io/policy-templates/) via + `PLAYWRIGHT_FIREFOX_POLICIES_JSON` environment variable. + Returns ------- Browser @@ -14474,7 +14461,7 @@ async def launch_persistent_context( user_data_dir: typing.Union[str, pathlib.Path], *, channel: typing.Optional[str] = None, - executable_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + executable_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, args: typing.Optional[typing.Sequence[str]] = None, ignore_default_args: typing.Optional[ typing.Union[bool, typing.Sequence[str]] @@ -14487,7 +14474,7 @@ async def launch_persistent_context( headless: typing.Optional[bool] = None, devtools: typing.Optional[bool] = None, proxy: typing.Optional[ProxySettings] = None, - downloads_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + downloads_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, slow_mo: typing.Optional[float] = None, viewport: typing.Optional[ViewportSize] = None, screen: typing.Optional[ViewportSize] = None, @@ -14515,20 +14502,20 @@ async def launch_persistent_context( forced_colors: typing.Optional[Literal["active", "none", "null"]] = None, contrast: typing.Optional[Literal["more", "no-preference", "null"]] = None, accept_downloads: typing.Optional[bool] = None, - traces_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, + traces_dir: typing.Optional[typing.Union[pathlib.Path, str]] = None, chromium_sandbox: typing.Optional[bool] = None, firefox_user_prefs: typing.Optional[ typing.Dict[str, typing.Union[str, float, bool]] ] = None, - record_har_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + record_har_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, record_har_omit_content: typing.Optional[bool] = None, - record_video_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, + record_video_dir: typing.Optional[typing.Union[pathlib.Path, str]] = None, record_video_size: typing.Optional[ViewportSize] = None, base_url: typing.Optional[str] = None, strict_selectors: typing.Optional[bool] = None, service_workers: typing.Optional[Literal["allow", "block"]] = None, record_har_url_filter: typing.Optional[ - typing.Union[str, typing.Pattern[str]] + typing.Union[typing.Pattern[str], str] ] = None, record_har_mode: typing.Optional[Literal["full", "minimal"]] = None, record_har_content: typing.Optional[Literal["attach", "embed", "omit"]] = None, @@ -14673,6 +14660,9 @@ async def launch_persistent_context( firefox_user_prefs : Union[Dict[str, Union[bool, float, str]], None] Firefox user preferences. Learn more about the Firefox user preferences at [`about:config`](https://support.mozilla.org/en-US/kb/about-config-editor-firefox). + + You can also provide a path to a custom [`policies.json` file](https://mozilla.github.io/policy-templates/) via + `PLAYWRIGHT_FIREFOX_POLICIES_JSON` environment variable. record_har_path : Union[pathlib.Path, str, None] Enables [HAR](http://www.softwareishard.com/blog/har-12-spec) recording for all pages into the specified HAR file on the filesystem. If not specified, the HAR is not recorded. Make sure to call `browser_context.close()` @@ -15047,6 +15037,15 @@ async def start( Start tracing. + **NOTE** You probably want to + [enable tracing in your config file](https://playwright.dev/docs/api/class-testoptions#test-options-trace) instead + of using `Tracing.start`. + + The `context.tracing` API captures browser operations and network activity, but it doesn't record test assertions + (like `expect` calls). We recommend + [enabling tracing through Playwright Test configuration](https://playwright.dev/docs/api/class-testoptions#test-options-trace), + which includes those assertions and provides a more complete trace for debugging test failures. + **Usage** ```py @@ -15126,7 +15125,7 @@ async def start_chunk( ) async def stop_chunk( - self, *, path: typing.Optional[typing.Union[str, pathlib.Path]] = None + self, *, path: typing.Optional[typing.Union[pathlib.Path, str]] = None ) -> None: """Tracing.stop_chunk @@ -15141,7 +15140,7 @@ async def stop_chunk( return mapping.from_maybe_impl(await self._impl_obj.stop_chunk(path=path)) async def stop( - self, *, path: typing.Optional[typing.Union[str, pathlib.Path]] = None + self, *, path: typing.Optional[typing.Union[pathlib.Path, str]] = None ) -> None: """Tracing.stop @@ -15627,6 +15626,13 @@ async def evaluate( **Usage** + Passing argument to `expression`: + + ```py + result = await page.get_by_testid(\"myId\").evaluate(\"(element, [x, y]) => element.textContent + ' ' + x * y\", [7, 8]) + print(result) # prints \"myId text 56\" + ``` + Parameters ---------- expression : str @@ -15838,8 +15844,8 @@ def locator( self, selector_or_locator: typing.Union[str, "Locator"], *, - has_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - has_not_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + has_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + has_not_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, has: typing.Optional["Locator"] = None, has_not: typing.Optional["Locator"] = None, ) -> "Locator": @@ -16103,7 +16109,7 @@ def get_by_role( expanded: typing.Optional[bool] = None, include_hidden: typing.Optional[bool] = None, level: typing.Optional[int] = None, - name: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + name: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, pressed: typing.Optional[bool] = None, selected: typing.Optional[bool] = None, exact: typing.Optional[bool] = None, @@ -16433,11 +16439,36 @@ def nth(self, index: int) -> "Locator": return mapping.from_impl(self._impl_obj.nth(index=index)) + def describe(self, description: str) -> "Locator": + """Locator.describe + + Describes the locator, description is used in the trace viewer and reports. Returns the locator pointing to the + same element. + + **Usage** + + ```py + button = page.get_by_test_id(\"btn-sub\").describe(\"Subscribe button\") + await button.click() + ``` + + Parameters + ---------- + description : str + Locator description. + + Returns + ------- + Locator + """ + + return mapping.from_impl(self._impl_obj.describe(description=description)) + def filter( self, *, - has_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - has_not_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + has_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + has_not_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, has: typing.Optional["Locator"] = None, has_not: typing.Optional["Locator"] = None, visible: typing.Optional[bool] = None, @@ -17117,7 +17148,7 @@ async def screenshot( *, timeout: typing.Optional[float] = None, type: typing.Optional[Literal["jpeg", "png"]] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, quality: typing.Optional[int] = None, omit_background: typing.Optional[bool] = None, animations: typing.Optional[Literal["allow", "disabled"]] = None, @@ -17220,12 +17251,7 @@ async def screenshot( ) ) - async def aria_snapshot( - self, - *, - timeout: typing.Optional[float] = None, - ref: typing.Optional[bool] = None, - ) -> str: + async def aria_snapshot(self, *, timeout: typing.Optional[float] = None) -> str: """Locator.aria_snapshot Captures the aria snapshot of the given element. Read more about [aria snapshots](https://playwright.dev/python/docs/aria-snapshots) and @@ -17270,9 +17296,6 @@ async def aria_snapshot( timeout : Union[float, None] Maximum time in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can be changed by using the `browser_context.set_default_timeout()` or `page.set_default_timeout()` methods. - ref : Union[bool, None] - Generate symbolic reference for each element. One can use `aria-ref=` locator immediately after capturing the - snapshot to perform actions on the element. Returns ------- @@ -17280,7 +17303,7 @@ async def aria_snapshot( """ return mapping.from_maybe_impl( - await self._impl_obj.aria_snapshot(timeout=timeout, ref=ref) + await self._impl_obj.aria_snapshot(timeout=timeout) ) async def scroll_into_view_if_needed( @@ -18667,7 +18690,7 @@ async def fetch( async def storage_state( self, *, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, indexed_db: typing.Optional[bool] = None, ) -> StorageState: """APIRequestContext.storage_state @@ -19175,7 +19198,7 @@ async def to_have_class( ```py from playwright.async_api import expect - locator = page.locator(\"list > .component\") + locator = page.locator(\".list > .component\") await expect(locator).to_have_class([\"component\", \"component selected\", \"component\"]) ``` @@ -19256,7 +19279,7 @@ async def to_contain_class( expected class lists. Each element's class attribute is matched against the corresponding class in the array: ```html -
+
@@ -19266,7 +19289,7 @@ async def to_contain_class( ```py from playwright.async_api import expect - locator = page.locator(\"list > .component\") + locator = page.locator(\".list > .component\") await expect(locator).to_contain_class([\"inactive\", \"active\", \"inactive\"]) ``` diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py index 828636efe..763df6de3 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -682,7 +682,7 @@ def fulfill( headers: typing.Optional[typing.Dict[str, str]] = None, body: typing.Optional[typing.Union[str, bytes]] = None, json: typing.Optional[typing.Any] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, content_type: typing.Optional[str] = None, response: typing.Optional["APIResponse"] = None, ) -> None: @@ -2804,7 +2804,7 @@ def screenshot( *, timeout: typing.Optional[float] = None, type: typing.Optional[Literal["jpeg", "png"]] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, quality: typing.Optional[int] = None, omit_background: typing.Optional[bool] = None, animations: typing.Optional[Literal["allow", "disabled"]] = None, @@ -3980,13 +3980,7 @@ def is_enabled( ) ) - def is_hidden( - self, - selector: str, - *, - strict: typing.Optional[bool] = None, - timeout: typing.Optional[float] = None, - ) -> bool: + def is_hidden(self, selector: str, *, strict: typing.Optional[bool] = None) -> bool: """Frame.is_hidden Returns whether the element is hidden, the opposite of [visible](https://playwright.dev/python/docs/actionability#visible). `selector` that @@ -4000,8 +3994,6 @@ def is_hidden( strict : Union[bool, None] When true, the call requires selector to resolve to a single element. If given selector resolves to more than one element, the call throws an exception. - timeout : Union[float, None] - Deprecated: This option is ignored. `frame.is_hidden()` does not wait for the element to become hidden and returns immediately. Returns ------- @@ -4009,19 +4001,11 @@ def is_hidden( """ return mapping.from_maybe_impl( - self._sync( - self._impl_obj.is_hidden( - selector=selector, strict=strict, timeout=timeout - ) - ) + self._sync(self._impl_obj.is_hidden(selector=selector, strict=strict)) ) def is_visible( - self, - selector: str, - *, - strict: typing.Optional[bool] = None, - timeout: typing.Optional[float] = None, + self, selector: str, *, strict: typing.Optional[bool] = None ) -> bool: """Frame.is_visible @@ -4036,8 +4020,6 @@ def is_visible( strict : Union[bool, None] When true, the call requires selector to resolve to a single element. If given selector resolves to more than one element, the call throws an exception. - timeout : Union[float, None] - Deprecated: This option is ignored. `frame.is_visible()` does not wait for the element to become visible and returns immediately. Returns ------- @@ -4045,11 +4027,7 @@ def is_visible( """ return mapping.from_maybe_impl( - self._sync( - self._impl_obj.is_visible( - selector=selector, strict=strict, timeout=timeout - ) - ) + self._sync(self._impl_obj.is_visible(selector=selector, strict=strict)) ) def dispatch_event( @@ -4291,7 +4269,7 @@ def add_script_tag( self, *, url: typing.Optional[str] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, content: typing.Optional[str] = None, type: typing.Optional[str] = None, ) -> "ElementHandle": @@ -4331,7 +4309,7 @@ def add_style_tag( self, *, url: typing.Optional[str] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, content: typing.Optional[str] = None, ) -> "ElementHandle": """Frame.add_style_tag @@ -4663,8 +4641,8 @@ def locator( self, selector: str, *, - has_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - has_not_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + has_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + has_not_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, has: typing.Optional["Locator"] = None, has_not: typing.Optional["Locator"] = None, ) -> "Locator": @@ -4931,7 +4909,7 @@ def get_by_role( expanded: typing.Optional[bool] = None, include_hidden: typing.Optional[bool] = None, level: typing.Optional[int] = None, - name: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + name: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, pressed: typing.Optional[bool] = None, selected: typing.Optional[bool] = None, exact: typing.Optional[bool] = None, @@ -6175,8 +6153,8 @@ def locator( self, selector_or_locator: typing.Union["Locator", str], *, - has_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - has_not_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + has_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + has_not_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, has: typing.Optional["Locator"] = None, has_not: typing.Optional["Locator"] = None, ) -> "Locator": @@ -6440,7 +6418,7 @@ def get_by_role( expanded: typing.Optional[bool] = None, include_hidden: typing.Optional[bool] = None, level: typing.Optional[int] = None, - name: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + name: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, pressed: typing.Optional[bool] = None, selected: typing.Optional[bool] = None, exact: typing.Optional[bool] = None, @@ -6834,7 +6812,7 @@ def register( name: str, script: typing.Optional[str] = None, *, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, content_script: typing.Optional[bool] = None, ) -> None: """Selectors.register @@ -8691,7 +8669,7 @@ def add_script_tag( self, *, url: typing.Optional[str] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, content: typing.Optional[str] = None, type: typing.Optional[str] = None, ) -> "ElementHandle": @@ -8730,7 +8708,7 @@ def add_style_tag( self, *, url: typing.Optional[str] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, content: typing.Optional[str] = None, ) -> "ElementHandle": """Page.add_style_tag @@ -9428,7 +9406,7 @@ def add_init_script( self, script: typing.Optional[str] = None, *, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, ) -> None: """Page.add_init_script @@ -9656,7 +9634,7 @@ def route_from_har( self, har: typing.Union[pathlib.Path, str], *, - url: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + url: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, not_found: typing.Optional[Literal["abort", "fallback"]] = None, update: typing.Optional[bool] = None, update_content: typing.Optional[Literal["attach", "embed"]] = None, @@ -9714,7 +9692,7 @@ def screenshot( *, timeout: typing.Optional[float] = None, type: typing.Optional[Literal["jpeg", "png"]] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, quality: typing.Optional[int] = None, omit_background: typing.Optional[bool] = None, full_page: typing.Optional[bool] = None, @@ -10160,8 +10138,8 @@ def locator( self, selector: str, *, - has_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - has_not_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + has_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + has_not_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, has: typing.Optional["Locator"] = None, has_not: typing.Optional["Locator"] = None, ) -> "Locator": @@ -10426,7 +10404,7 @@ def get_by_role( expanded: typing.Optional[bool] = None, include_hidden: typing.Optional[bool] = None, level: typing.Optional[int] = None, - name: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + name: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, pressed: typing.Optional[bool] = None, selected: typing.Optional[bool] = None, exact: typing.Optional[bool] = None, @@ -11600,7 +11578,7 @@ def pdf( height: typing.Optional[typing.Union[str, float]] = None, prefer_css_page_size: typing.Optional[bool] = None, margin: typing.Optional[PdfMargins] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, outline: typing.Optional[bool] = None, tagged: typing.Optional[bool] = None, ) -> bytes: @@ -12864,9 +12842,9 @@ def add_cookies(self, cookies: typing.Sequence[SetCookieParam]) -> None: def clear_cookies( self, *, - name: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - domain: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - path: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + name: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + domain: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + path: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, ) -> None: """BrowserContext.clear_cookies @@ -13021,7 +12999,7 @@ def add_init_script( self, script: typing.Optional[str] = None, *, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, ) -> None: """BrowserContext.add_init_script @@ -13375,7 +13353,7 @@ def route_from_har( self, har: typing.Union[pathlib.Path, str], *, - url: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + url: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, not_found: typing.Optional[Literal["abort", "fallback"]] = None, update: typing.Optional[bool] = None, update_content: typing.Optional[Literal["attach", "embed"]] = None, @@ -13487,7 +13465,7 @@ def close(self, *, reason: typing.Optional[str] = None) -> None: def storage_state( self, *, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, indexed_db: typing.Optional[bool] = None, ) -> StorageState: """BrowserContext.storage_state @@ -13789,9 +13767,9 @@ def new_context( accept_downloads: typing.Optional[bool] = None, default_browser_type: typing.Optional[str] = None, proxy: typing.Optional[ProxySettings] = None, - record_har_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + record_har_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, record_har_omit_content: typing.Optional[bool] = None, - record_video_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, + record_video_dir: typing.Optional[typing.Union[pathlib.Path, str]] = None, record_video_size: typing.Optional[ViewportSize] = None, storage_state: typing.Optional[ typing.Union[StorageState, str, pathlib.Path] @@ -13800,7 +13778,7 @@ def new_context( strict_selectors: typing.Optional[bool] = None, service_workers: typing.Optional[Literal["allow", "block"]] = None, record_har_url_filter: typing.Optional[ - typing.Union[str, typing.Pattern[str]] + typing.Union[typing.Pattern[str], str] ] = None, record_har_mode: typing.Optional[Literal["full", "minimal"]] = None, record_har_content: typing.Optional[Literal["attach", "embed", "omit"]] = None, @@ -14038,9 +14016,9 @@ def new_page( accept_downloads: typing.Optional[bool] = None, default_browser_type: typing.Optional[str] = None, proxy: typing.Optional[ProxySettings] = None, - record_har_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + record_har_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, record_har_omit_content: typing.Optional[bool] = None, - record_video_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, + record_video_dir: typing.Optional[typing.Union[pathlib.Path, str]] = None, record_video_size: typing.Optional[ViewportSize] = None, storage_state: typing.Optional[ typing.Union[StorageState, str, pathlib.Path] @@ -14049,7 +14027,7 @@ def new_page( strict_selectors: typing.Optional[bool] = None, service_workers: typing.Optional[Literal["allow", "block"]] = None, record_har_url_filter: typing.Optional[ - typing.Union[str, typing.Pattern[str]] + typing.Union[typing.Pattern[str], str] ] = None, record_har_mode: typing.Optional[Literal["full", "minimal"]] = None, record_har_content: typing.Optional[Literal["attach", "embed", "omit"]] = None, @@ -14281,7 +14259,7 @@ def start_tracing( self, *, page: typing.Optional["Page"] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, screenshots: typing.Optional[bool] = None, categories: typing.Optional[typing.Sequence[str]] = None, ) -> None: @@ -14376,7 +14354,7 @@ def executable_path(self) -> str: def launch( self, *, - executable_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + executable_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, channel: typing.Optional[str] = None, args: typing.Optional[typing.Sequence[str]] = None, ignore_default_args: typing.Optional[ @@ -14390,9 +14368,9 @@ def launch( headless: typing.Optional[bool] = None, devtools: typing.Optional[bool] = None, proxy: typing.Optional[ProxySettings] = None, - downloads_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + downloads_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, slow_mo: typing.Optional[float] = None, - traces_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, + traces_dir: typing.Optional[typing.Union[pathlib.Path, str]] = None, chromium_sandbox: typing.Optional[bool] = None, firefox_user_prefs: typing.Optional[ typing.Dict[str, typing.Union[str, float, bool]] @@ -14485,6 +14463,9 @@ def launch( Firefox user preferences. Learn more about the Firefox user preferences at [`about:config`](https://support.mozilla.org/en-US/kb/about-config-editor-firefox). + You can also provide a path to a custom [`policies.json` file](https://mozilla.github.io/policy-templates/) via + `PLAYWRIGHT_FIREFOX_POLICIES_JSON` environment variable. + Returns ------- Browser @@ -14519,7 +14500,7 @@ def launch_persistent_context( user_data_dir: typing.Union[str, pathlib.Path], *, channel: typing.Optional[str] = None, - executable_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + executable_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, args: typing.Optional[typing.Sequence[str]] = None, ignore_default_args: typing.Optional[ typing.Union[bool, typing.Sequence[str]] @@ -14532,7 +14513,7 @@ def launch_persistent_context( headless: typing.Optional[bool] = None, devtools: typing.Optional[bool] = None, proxy: typing.Optional[ProxySettings] = None, - downloads_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + downloads_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, slow_mo: typing.Optional[float] = None, viewport: typing.Optional[ViewportSize] = None, screen: typing.Optional[ViewportSize] = None, @@ -14560,20 +14541,20 @@ def launch_persistent_context( forced_colors: typing.Optional[Literal["active", "none", "null"]] = None, contrast: typing.Optional[Literal["more", "no-preference", "null"]] = None, accept_downloads: typing.Optional[bool] = None, - traces_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, + traces_dir: typing.Optional[typing.Union[pathlib.Path, str]] = None, chromium_sandbox: typing.Optional[bool] = None, firefox_user_prefs: typing.Optional[ typing.Dict[str, typing.Union[str, float, bool]] ] = None, - record_har_path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + record_har_path: typing.Optional[typing.Union[pathlib.Path, str]] = None, record_har_omit_content: typing.Optional[bool] = None, - record_video_dir: typing.Optional[typing.Union[str, pathlib.Path]] = None, + record_video_dir: typing.Optional[typing.Union[pathlib.Path, str]] = None, record_video_size: typing.Optional[ViewportSize] = None, base_url: typing.Optional[str] = None, strict_selectors: typing.Optional[bool] = None, service_workers: typing.Optional[Literal["allow", "block"]] = None, record_har_url_filter: typing.Optional[ - typing.Union[str, typing.Pattern[str]] + typing.Union[typing.Pattern[str], str] ] = None, record_har_mode: typing.Optional[Literal["full", "minimal"]] = None, record_har_content: typing.Optional[Literal["attach", "embed", "omit"]] = None, @@ -14718,6 +14699,9 @@ def launch_persistent_context( firefox_user_prefs : Union[Dict[str, Union[bool, float, str]], None] Firefox user preferences. Learn more about the Firefox user preferences at [`about:config`](https://support.mozilla.org/en-US/kb/about-config-editor-firefox). + + You can also provide a path to a custom [`policies.json` file](https://mozilla.github.io/policy-templates/) via + `PLAYWRIGHT_FIREFOX_POLICIES_JSON` environment variable. record_har_path : Union[pathlib.Path, str, None] Enables [HAR](http://www.softwareishard.com/blog/har-12-spec) recording for all pages into the specified HAR file on the filesystem. If not specified, the HAR is not recorded. Make sure to call `browser_context.close()` @@ -15095,6 +15079,15 @@ def start( Start tracing. + **NOTE** You probably want to + [enable tracing in your config file](https://playwright.dev/docs/api/class-testoptions#test-options-trace) instead + of using `Tracing.start`. + + The `context.tracing` API captures browser operations and network activity, but it doesn't record test assertions + (like `expect` calls). We recommend + [enabling tracing through Playwright Test configuration](https://playwright.dev/docs/api/class-testoptions#test-options-trace), + which includes those assertions and provides a more complete trace for debugging test failures. + **Usage** ```py @@ -15176,7 +15169,7 @@ def start_chunk( ) def stop_chunk( - self, *, path: typing.Optional[typing.Union[str, pathlib.Path]] = None + self, *, path: typing.Optional[typing.Union[pathlib.Path, str]] = None ) -> None: """Tracing.stop_chunk @@ -15191,7 +15184,7 @@ def stop_chunk( return mapping.from_maybe_impl(self._sync(self._impl_obj.stop_chunk(path=path))) def stop( - self, *, path: typing.Optional[typing.Union[str, pathlib.Path]] = None + self, *, path: typing.Optional[typing.Union[pathlib.Path, str]] = None ) -> None: """Tracing.stop @@ -15685,6 +15678,13 @@ def evaluate( **Usage** + Passing argument to `expression`: + + ```py + result = page.get_by_testid(\"myId\").evaluate(\"(element, [x, y]) => element.textContent + ' ' + x * y\", [7, 8]) + print(result) # prints \"myId text 56\" + ``` + Parameters ---------- expression : str @@ -15906,8 +15906,8 @@ def locator( self, selector_or_locator: typing.Union[str, "Locator"], *, - has_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - has_not_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + has_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + has_not_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, has: typing.Optional["Locator"] = None, has_not: typing.Optional["Locator"] = None, ) -> "Locator": @@ -16171,7 +16171,7 @@ def get_by_role( expanded: typing.Optional[bool] = None, include_hidden: typing.Optional[bool] = None, level: typing.Optional[int] = None, - name: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + name: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, pressed: typing.Optional[bool] = None, selected: typing.Optional[bool] = None, exact: typing.Optional[bool] = None, @@ -16503,11 +16503,36 @@ def nth(self, index: int) -> "Locator": return mapping.from_impl(self._impl_obj.nth(index=index)) + def describe(self, description: str) -> "Locator": + """Locator.describe + + Describes the locator, description is used in the trace viewer and reports. Returns the locator pointing to the + same element. + + **Usage** + + ```py + button = page.get_by_test_id(\"btn-sub\").describe(\"Subscribe button\") + button.click() + ``` + + Parameters + ---------- + description : str + Locator description. + + Returns + ------- + Locator + """ + + return mapping.from_impl(self._impl_obj.describe(description=description)) + def filter( self, *, - has_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, - has_not_text: typing.Optional[typing.Union[str, typing.Pattern[str]]] = None, + has_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, + has_not_text: typing.Optional[typing.Union[typing.Pattern[str], str]] = None, has: typing.Optional["Locator"] = None, has_not: typing.Optional["Locator"] = None, visible: typing.Optional[bool] = None, @@ -17206,7 +17231,7 @@ def screenshot( *, timeout: typing.Optional[float] = None, type: typing.Optional[Literal["jpeg", "png"]] = None, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, quality: typing.Optional[int] = None, omit_background: typing.Optional[bool] = None, animations: typing.Optional[Literal["allow", "disabled"]] = None, @@ -17311,12 +17336,7 @@ def screenshot( ) ) - def aria_snapshot( - self, - *, - timeout: typing.Optional[float] = None, - ref: typing.Optional[bool] = None, - ) -> str: + def aria_snapshot(self, *, timeout: typing.Optional[float] = None) -> str: """Locator.aria_snapshot Captures the aria snapshot of the given element. Read more about [aria snapshots](https://playwright.dev/python/docs/aria-snapshots) and @@ -17361,9 +17381,6 @@ def aria_snapshot( timeout : Union[float, None] Maximum time in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can be changed by using the `browser_context.set_default_timeout()` or `page.set_default_timeout()` methods. - ref : Union[bool, None] - Generate symbolic reference for each element. One can use `aria-ref=` locator immediately after capturing the - snapshot to perform actions on the element. Returns ------- @@ -17371,7 +17388,7 @@ def aria_snapshot( """ return mapping.from_maybe_impl( - self._sync(self._impl_obj.aria_snapshot(timeout=timeout, ref=ref)) + self._sync(self._impl_obj.aria_snapshot(timeout=timeout)) ) def scroll_into_view_if_needed( @@ -18794,7 +18811,7 @@ def fetch( def storage_state( self, *, - path: typing.Optional[typing.Union[str, pathlib.Path]] = None, + path: typing.Optional[typing.Union[pathlib.Path, str]] = None, indexed_db: typing.Optional[bool] = None, ) -> StorageState: """APIRequestContext.storage_state @@ -19320,7 +19337,7 @@ def to_have_class( ```py from playwright.sync_api import expect - locator = page.locator(\"list > .component\") + locator = page.locator(\".list > .component\") expect(locator).to_have_class([\"component\", \"component selected\", \"component\"]) ``` @@ -19405,7 +19422,7 @@ def to_contain_class( expected class lists. Each element's class attribute is matched against the corresponding class in the array: ```html -
+
@@ -19415,7 +19432,7 @@ def to_contain_class( ```py from playwright.sync_api import expect - locator = page.locator(\"list > .component\") + locator = page.locator(\".list > .component\") await expect(locator).to_contain_class([\"inactive\", \"active\", \"inactive\"]) ``` diff --git a/setup.py b/setup.py index abe2fd6e2..fd590167f 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ import zipfile from typing import Dict -driver_version = "1.52.0" +driver_version = "1.53.1" base_wheel_bundles = [ { diff --git a/tests/async/conftest.py b/tests/async/conftest.py index a007d55ac..f2e06d56e 100644 --- a/tests/async/conftest.py +++ b/tests/async/conftest.py @@ -156,9 +156,7 @@ def stack_frames(self) -> Locator: return self.page.get_by_test_id("stack-trace-list").locator(".list-view-entry") async def select_action(self, title: str, ordinal: int = 0) -> None: - await self.page.locator(f'.action-title:has-text("{title}")').nth( - ordinal - ).click() + await self.page.locator(".action-title", has_text=title).nth(ordinal).click() async def select_snapshot(self, name: str) -> None: await self.page.click( diff --git a/tests/async/test_assertions.py b/tests/async/test_assertions.py index 58f4ea5f5..3213e5523 100644 --- a/tests/async/test_assertions.py +++ b/tests/async/test_assertions.py @@ -161,7 +161,7 @@ async def test_assertions_locator_to_contain_class(page: Page, server: Server) - assert excinfo.match("Locator expected to contain class 'does-not-exist'") assert excinfo.match("Actual value: foo bar baz") - assert excinfo.match("LocatorAssertions.to_contain_class with timeout 100ms") + assert excinfo.match('Expect "to_contain_class" with timeout 100ms') await page.set_content( '
' @@ -552,6 +552,35 @@ async def test_assertions_locator_to_be_checked(page: Page, server: Server) -> N await expect(my_checkbox).to_be_checked() +async def test_assertions_boolean_checked_with_intermediate_true(page: Page) -> None: + await page.set_content("") + await page.locator("input").evaluate("e => e.indeterminate = true") + await expect(page.locator("input")).to_be_checked(indeterminate=True) + + +async def test_assertions_boolean_checked_with_intermediate_true_and_checked( + page: Page, +) -> None: + 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" + ): + await expect(page.locator("input")).to_be_checked( + checked=False, indeterminate=True + ) + + +async def test_assertions_boolean_fail_with_indeterminate_true(page: Page) -> None: + await page.set_content("") + with pytest.raises( + AssertionError, match='Expect "to_be_checked" with timeout 1000ms' + ): + await expect(page.locator("input")).to_be_checked( + indeterminate=True, timeout=1000 + ) + + async def test_assertions_locator_to_be_disabled_enabled( page: Page, server: Server ) -> None: @@ -991,7 +1020,7 @@ async def test_should_be_attached_over_navigation(page: Page, server: Server) -> async def test_should_be_able_to_set_custom_timeout(page: Page) -> None: with pytest.raises(AssertionError) as exc_info: await expect(page.locator("#a1")).to_be_visible(timeout=111) - assert "LocatorAssertions.to_be_visible with timeout 111ms" in str(exc_info.value) + assert 'Expect "to_be_visible" with timeout 111ms' in str(exc_info.value) async def test_should_be_able_to_set_custom_global_timeout(page: Page) -> None: @@ -999,9 +1028,7 @@ async def test_should_be_able_to_set_custom_global_timeout(page: Page) -> None: expect.set_options(timeout=111) with pytest.raises(AssertionError) as exc_info: await expect(page.locator("#a1")).to_be_visible() - assert "LocatorAssertions.to_be_visible with timeout 111ms" in str( - exc_info.value - ) + assert 'Expect "to_be_visible" with timeout 111ms' in str(exc_info.value) finally: expect.set_options(timeout=None) diff --git a/tests/async/test_browsertype_connect.py b/tests/async/test_browsertype_connect.py index d2eca4628..ccb112ab9 100644 --- a/tests/async/test_browsertype_connect.py +++ b/tests/async/test_browsertype_connect.py @@ -347,9 +347,9 @@ async def test_should_record_trace_with_source( async with show_trace_viewer(path) as trace_viewer: await expect(trace_viewer.action_titles).to_have_text( [ - re.compile("Page.goto"), - re.compile("Page.set_content"), - re.compile("Page.click"), + re.compile(r'Navigate to "/empty\.html"'), + re.compile(r"Set content"), + re.compile(r"Click"), ] ) await trace_viewer.show_source_tab() @@ -358,7 +358,7 @@ async def test_should_record_trace_with_source( re.compile(r"test_should_record_trace_with_source"), ] ) - await trace_viewer.select_action("Page.set_content") + await trace_viewer.select_action("Set content") # Check that the source file is shown await expect( trace_viewer.page.locator(".source-tab-file-name") diff --git a/tests/async/test_defaultbrowsercontext.py b/tests/async/test_defaultbrowsercontext.py index 67de51702..25ef0c3f8 100644 --- a/tests/async/test_defaultbrowsercontext.py +++ b/tests/async/test_defaultbrowsercontext.py @@ -24,6 +24,7 @@ List, Literal, Optional, + Sequence, Tuple, ) @@ -124,7 +125,7 @@ async def test_context_add_cookies_should_work( ] -def _filter_cookies(cookies: List[Cookie]) -> List[Cookie]: +def _filter_cookies(cookies: Sequence[Cookie]) -> List[Cookie]: return list( filter(lambda cookie: cookie["domain"] != "copilot.microsoft.com", cookies) ) diff --git a/tests/async/test_page_aria_snapshot.py b/tests/async/test_page_aria_snapshot.py index 007d1f56c..30a9c9661 100644 --- a/tests/async/test_page_aria_snapshot.py +++ b/tests/async/test_page_aria_snapshot.py @@ -96,17 +96,6 @@ async def test_should_snapshot_complex(page: Page) -> None: ) -async def test_should_snapshot_with_ref(page: Page) -> None: - await page.set_content('') - expected = """ - - list [ref=s1e3]: - - listitem [ref=s1e4]: - - link "link" [ref=s1e5]: - - /url: about:blank - """ - assert await page.locator("body").aria_snapshot(ref=True) == _unshift(expected) - - async def test_should_snapshot_with_unexpected_children_equal(page: Page) -> None: await page.set_content( """ diff --git a/tests/async/test_tracing.py b/tests/async/test_tracing.py index 270bbfb80..e735c96a8 100644 --- a/tests/async/test_tracing.py +++ b/tests/async/test_tracing.py @@ -98,9 +98,9 @@ async def my_method_inner() -> None: async with show_trace_viewer(path) as trace_viewer: await expect(trace_viewer.action_titles).to_have_text( [ - re.compile(r"Page.goto"), - re.compile(r"Page.set_content"), - re.compile(r"Locator.click"), + re.compile(r'Navigate to "/empty\.html"'), + re.compile(r"Set content"), + re.compile(r"Click"), ] ) await trace_viewer.show_source_tab() @@ -113,7 +113,7 @@ async def my_method_inner() -> None: ] ) - await trace_viewer.select_action("Page.set_content") + await trace_viewer.select_action("Set content") # Check that the source file is shown await expect( trace_viewer.page.locator(".source-tab-file-name") @@ -138,13 +138,9 @@ async def test_should_collect_trace_with_resources_but_no_js( await page.mouse.dblclick(30, 30) await page.keyboard.insert_text("abc") await page.wait_for_timeout(2000) # Give it some time to produce screenshots. - await page.route( - "**/empty.html", lambda route: route.continue_() - ) # should produce a route.continue_ entry. + await page.route("**/empty.html", lambda route: route.continue_()) await page.goto(server.EMPTY_PAGE) - await page.goto( - server.PREFIX + "/one-style.html" - ) # should not produce a route.continue_ entry since we continue all routes if no match. + await page.goto(server.PREFIX + "/one-style.html") await page.close() trace_file_path = tmp_path / "trace.zip" await context.tracing.stop(path=trace_file_path) @@ -152,25 +148,24 @@ async def test_should_collect_trace_with_resources_but_no_js( async with show_trace_viewer(trace_file_path) as trace_viewer: await expect(trace_viewer.action_titles).to_have_text( [ - re.compile("Page.goto"), - re.compile("Page.set_content"), - re.compile("Page.click"), - re.compile("Mouse.move"), - re.compile("Mouse.dblclick"), - re.compile("Keyboard.insert_text"), - re.compile("Page.wait_for_timeout"), - re.compile("Page.route"), - re.compile("Page.goto"), - re.compile("Page.goto"), - re.compile("Page.close"), + re.compile(r'Navigate to "/frames/frame\.html"'), + re.compile(r"Set content"), + re.compile(r"Click"), + re.compile(r"Mouse move"), + re.compile(r"Double click"), + re.compile(r'Insert "abc"'), + re.compile(r"Wait for timeout"), + re.compile(r'Navigate to "/empty\.html"'), + re.compile(r'Navigate to "/one-style\.html"'), + re.compile(r"Close"), ] ) - await trace_viewer.select_action("Page.set_content") + await trace_viewer.select_action("Set content") await expect( trace_viewer.page.locator(".browser-frame-address-bar") ).to_have_text(server.PREFIX + "/frames/frame.html") - frame = await trace_viewer.snapshot_frame("Page.set_content", 0, False) + frame = await trace_viewer.snapshot_frame("Set content", 0, False) await expect(frame.locator("button")).to_have_text("Click") @@ -200,8 +195,8 @@ async def _handle_response(response: Response) -> None: async with show_trace_viewer(trace_file_path) as trace_viewer: await expect(trace_viewer.action_titles).to_have_text( [ - re.compile(r"Page.goto"), - re.compile(r"Page.close"), + re.compile(r'Navigate to "/grid\.html"'), + re.compile(r"Close"), ] ) @@ -229,17 +224,17 @@ async def test_should_collect_two_traces( async with show_trace_viewer(tracing1_path) as trace_viewer: await expect(trace_viewer.action_titles).to_have_text( [ - re.compile("Page.goto"), - re.compile("Page.set_content"), - re.compile("Page.click"), + re.compile(r'Navigate to "/empty\.html"'), + re.compile(r"Set content"), + re.compile(r"Click"), ] ) async with show_trace_viewer(tracing2_path) as trace_viewer: await expect(trace_viewer.action_titles).to_have_text( [ - re.compile(r"Page.dblclick"), - re.compile(r"Page.close"), + re.compile(r"Double click"), + re.compile(r"Close"), ] ) @@ -267,13 +262,13 @@ async def test_should_work_with_playwright_context_managers( async with show_trace_viewer(trace_file_path) as trace_viewer: await expect(trace_viewer.action_titles).to_have_text( [ - re.compile("Page.goto"), - re.compile("Page.set_content"), - re.compile("Page.expect_console_message"), - re.compile("Page.evaluate"), - re.compile("Page.click"), - re.compile("Page.expect_popup"), - re.compile("Page.evaluate"), + re.compile(r'Navigate to "/empty\.html"'), + re.compile(r"Set content"), + re.compile(r'Wait for event "page\.expect_event\(console\)"'), + re.compile(r"Evaluate"), + re.compile(r"Click"), + re.compile(r'Wait for event "page\.expect_event\(popup\)"'), + re.compile(r"Evaluate"), ] ) @@ -297,9 +292,9 @@ async def test_should_display_wait_for_load_state_even_if_did_not_wait_for_it( async with show_trace_viewer(trace_file_path) as trace_viewer: await expect(trace_viewer.action_titles).to_have_text( [ - re.compile(r"Page.goto"), - re.compile(r"Page.wait_for_load_state"), - re.compile(r"Page.wait_for_load_state"), + re.compile(r'Navigate to "/empty\.html"'), + re.compile(r'Wait for event "frame\.wait_for_load_state"'), + re.compile(r'Wait for event "frame\.wait_for_load_state"'), ] ) @@ -333,10 +328,12 @@ async def test_should_respect_traces_dir_and_name( async with show_trace_viewer(tmp_path / "trace1.zip") as trace_viewer: await expect(trace_viewer.action_titles).to_have_text( [ - re.compile(r"Page.goto"), + re.compile('Navigate to "/one-style\\.html"'), ] ) - frame = await trace_viewer.snapshot_frame("Page.goto", 0, False) + frame = await trace_viewer.snapshot_frame( + 'Navigate to "/one-style.html"', 0, False + ) await expect(frame.locator("body")).to_have_css( "background-color", "rgb(255, 192, 203)" ) @@ -345,10 +342,10 @@ async def test_should_respect_traces_dir_and_name( async with show_trace_viewer(tmp_path / "trace2.zip") as trace_viewer: await expect(trace_viewer.action_titles).to_have_text( [ - re.compile(r"Page.goto"), + re.compile(r'Navigate to "/har\.html"'), ] ) - frame = await trace_viewer.snapshot_frame("Page.goto", 0, False) + frame = await trace_viewer.snapshot_frame('Navigate to "/har.html"', 0, False) await expect(frame.locator("body")).to_have_css( "background-color", "rgb(255, 192, 203)" ) @@ -380,12 +377,12 @@ async def test_should_show_tracing_group_in_action_list( await trace_viewer.expand_action("inner group 1") await expect(trace_viewer.action_titles).to_have_text( [ - "BrowserContext.new_page", - "outer group", - re.compile("Page.goto"), - "inner group 1", - re.compile("Locator.click"), - "inner group 2", - re.compile("Locator.is_visible"), + re.compile(r"Create page"), + re.compile(r"outer group"), + re.compile(r"Navigate to \"data:\""), + 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/conftest.py b/tests/sync/conftest.py index 46bf86239..3d7ae9116 100644 --- a/tests/sync/conftest.py +++ b/tests/sync/conftest.py @@ -146,7 +146,7 @@ def stack_frames(self) -> Locator: return self.page.get_by_test_id("stack-trace-list").locator(".list-view-entry") def select_action(self, title: str, ordinal: int = 0) -> None: - self.page.locator(f'.action-title:has-text("{title}")').nth(ordinal).click() + self.page.locator(".action-title", has_text=title).nth(ordinal).click() def select_snapshot(self, name: str) -> None: self.page.click(f'.snapshot-tab .tabbed-pane-tab-label:has-text("{name}")') diff --git a/tests/sync/test_assertions.py b/tests/sync/test_assertions.py index 0dce717d3..740e6e750 100644 --- a/tests/sync/test_assertions.py +++ b/tests/sync/test_assertions.py @@ -140,7 +140,7 @@ def test_assertions_locator_to_contain_class(page: Page, server: Server) -> None assert excinfo.match("Locator expected to contain class 'does-not-exist'") assert excinfo.match("Actual value: foo bar baz") - assert excinfo.match("LocatorAssertions.to_contain_class with timeout 100ms") + assert excinfo.match('Expect "to_contain_class" with timeout 100ms') page.set_content( '
' @@ -548,7 +548,7 @@ def test_assertions_boolean_checked_with_intermediate_true_and_checked( def test_assertions_boolean_fail_with_indeterminate_true(page: Page) -> None: page.set_content("") with pytest.raises( - AssertionError, match="LocatorAssertions.to_be_checked with timeout 1000ms" + AssertionError, match='Expect "to_be_checked" with timeout 1000ms' ): expect(page.locator("input")).to_be_checked(indeterminate=True, timeout=1000) @@ -957,7 +957,7 @@ def test_should_be_attached_with_impossible_timeout_not(page: Page) -> None: def test_should_be_able_to_set_custom_timeout(page: Page) -> None: with pytest.raises(AssertionError) as exc_info: expect(page.locator("#a1")).to_be_visible(timeout=111) - assert "LocatorAssertions.to_be_visible with timeout 111ms" in str(exc_info.value) + assert 'Expect "to_be_visible" with timeout 111ms' in str(exc_info.value) def test_should_be_able_to_set_custom_global_timeout(page: Page) -> None: @@ -965,9 +965,7 @@ def test_should_be_able_to_set_custom_global_timeout(page: Page) -> None: expect.set_options(timeout=111) with pytest.raises(AssertionError) as exc_info: expect(page.locator("#a1")).to_be_visible() - assert "LocatorAssertions.to_be_visible with timeout 111ms" in str( - exc_info.value - ) + assert 'Expect "to_be_visible" with timeout 111ms' in str(exc_info.value) finally: expect.set_options(timeout=5_000) diff --git a/tests/sync/test_har.py b/tests/sync/test_har.py index 990b1d382..6ac848b8a 100644 --- a/tests/sync/test_har.py +++ b/tests/sync/test_har.py @@ -443,6 +443,7 @@ def test_should_round_trip_har_zip( page_2.goto(server.PREFIX + "/one-style.html") assert "hello, world!" in page_2.content() expect(page_2.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)") + context_2.close() def test_should_round_trip_har_with_post_data( @@ -476,6 +477,7 @@ def test_should_round_trip_har_with_post_data( assert page_2.evaluate(fetch_function, "3") == "3" with pytest.raises(Exception): page_2.evaluate(fetch_function, "4") + context_2.close() def test_should_disambiguate_by_header( @@ -517,6 +519,7 @@ def test_should_disambiguate_by_header( assert page_2.evaluate(fetch_function, "baz2") == "baz2" assert page_2.evaluate(fetch_function, "baz3") == "baz3" assert page_2.evaluate(fetch_function, "baz4") == "baz1" + context_2.close() def test_should_produce_extracted_zip( @@ -542,6 +545,7 @@ def test_should_produce_extracted_zip( page_2.goto(server.PREFIX + "/one-style.html") assert "hello, world!" in page_2.content() expect(page_2.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)") + context_2.close() def test_should_update_har_zip_for_context( @@ -562,6 +566,7 @@ def test_should_update_har_zip_for_context( page_2.goto(server.PREFIX + "/one-style.html") assert "hello, world!" in page_2.content() expect(page_2.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)") + context_2.close() def test_should_update_har_zip_for_page( @@ -582,6 +587,7 @@ def test_should_update_har_zip_for_page( page_2.goto(server.PREFIX + "/one-style.html") assert "hello, world!" in page_2.content() expect(page_2.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)") + context_2.close() def test_should_update_har_zip_for_page_with_different_options( @@ -627,3 +633,4 @@ def test_should_update_extracted_har_zip_for_page( page_2.goto(server.PREFIX + "/one-style.html") assert "hello, world!" in page_2.content() expect(page_2.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)") + context_2.close() diff --git a/tests/sync/test_page_aria_snapshot.py b/tests/sync/test_page_aria_snapshot.py index ca1c48393..e892bb371 100644 --- a/tests/sync/test_page_aria_snapshot.py +++ b/tests/sync/test_page_aria_snapshot.py @@ -96,17 +96,6 @@ def test_should_snapshot_complex(page: Page) -> None: ) -def test_should_snapshot_with_ref(page: Page) -> None: - page.set_content('') - expected = """ - - list [ref=s1e3]: - - listitem [ref=s1e4]: - - link "link" [ref=s1e5]: - - /url: about:blank - """ - assert page.locator("body").aria_snapshot(ref=True) == _unshift(expected) - - def test_should_snapshot_with_unexpected_children_equal(page: Page) -> None: page.set_content( """ diff --git a/tests/sync/test_route_web_socket.py b/tests/sync/test_route_web_socket.py index 2e97ebd8d..0cc8eda5d 100644 --- a/tests/sync/test_route_web_socket.py +++ b/tests/sync/test_route_web_socket.py @@ -340,6 +340,7 @@ def _handle_ws(ws: WebSocketRoute) -> None: f"message: data=echo origin=ws://localhost:{server.PORT} lastEventId=", ], ) + context.close() def test_should_work_with_no_trailing_slash(page: Page, server: Server) -> None: diff --git a/tests/sync/test_tracing.py b/tests/sync/test_tracing.py index 43e875b16..1a42aab9b 100644 --- a/tests/sync/test_tracing.py +++ b/tests/sync/test_tracing.py @@ -39,6 +39,7 @@ def test_browser_context_output_trace( page.goto(server.PREFIX + "/grid.html") context.tracing.stop(path=tmp_path / "trace.zip") assert Path(tmp_path / "trace.zip").exists() + context.close() def test_start_stop(browser: Browser) -> None: @@ -72,6 +73,7 @@ def test_browser_context_output_trace_chunk( button.click() context.tracing.stop_chunk(path=tmp_path / "trace2.zip") assert Path(tmp_path / "trace2.zip").exists() + context.close() def test_should_collect_sources( @@ -98,9 +100,9 @@ def my_method_inner() -> None: with show_trace_viewer(path) as trace_viewer: expect(trace_viewer.action_titles).to_have_text( [ - re.compile(r"Page.goto"), - re.compile(r"Page.set_content"), - re.compile(r"Locator.click"), + re.compile(r'Navigate to "/empty\.html"'), + re.compile(r"Set content"), + re.compile(r"Click"), ] ) trace_viewer.show_source_tab() @@ -113,7 +115,7 @@ def my_method_inner() -> None: ] ) - trace_viewer.select_action("Page.set_content") + trace_viewer.select_action("Set content") # Check that the source file is shown expect(trace_viewer.page.locator(".source-tab-file-name")).to_have_attribute( "title", re.compile(r".*test_.*\.py") @@ -138,13 +140,9 @@ def test_should_collect_trace_with_resources_but_no_js( page.mouse.dblclick(30, 30) page.keyboard.insert_text("abc") page.wait_for_timeout(2000) # Give it some time to produce screenshots. - page.route( - "**/empty.html", lambda route: route.continue_() - ) # should produce a route.continue_ entry. + page.route("**/empty.html", lambda route: route.continue_()) page.goto(server.EMPTY_PAGE) - page.goto( - server.PREFIX + "/one-style.html" - ) # should not produce a route.continue_ entry since we continue all routes if no match. + page.goto(server.PREFIX + "/one-style.html") page.close() trace_file_path = tmp_path / "trace.zip" context.tracing.stop(path=trace_file_path) @@ -152,25 +150,24 @@ def test_should_collect_trace_with_resources_but_no_js( with show_trace_viewer(trace_file_path) as trace_viewer: expect(trace_viewer.action_titles).to_have_text( [ - re.compile("Page.goto"), - re.compile("Page.set_content"), - re.compile("Page.click"), - re.compile("Mouse.move"), - re.compile("Mouse.dblclick"), - re.compile("Keyboard.insert_text"), - re.compile("Page.wait_for_timeout"), - re.compile("Page.route"), - re.compile("Page.goto"), - re.compile("Page.goto"), - re.compile("Page.close"), + re.compile(r'Navigate to "/frames/frame\.html"'), + re.compile(r"Set content"), + re.compile(r"Click"), + re.compile(r"Mouse move"), + re.compile(r"Double click"), + re.compile(r'Insert "abc"'), + re.compile(r"Wait for timeout"), + re.compile(r'Navigate to "/empty\.html"'), + re.compile(r'Navigate to "/one-style\.html"'), + re.compile(r"Close"), ] ) - trace_viewer.select_action("Page.set_content") + trace_viewer.select_action("Set content") expect(trace_viewer.page.locator(".browser-frame-address-bar")).to_have_text( server.PREFIX + "/frames/frame.html" ) - frame = trace_viewer.snapshot_frame("Page.set_content", 0, False) + frame = trace_viewer.snapshot_frame("Set content", 0, False) expect(frame.locator("button")).to_have_text("Click") @@ -201,8 +198,8 @@ def _handle_response(response: Response) -> None: with show_trace_viewer(trace_file_path) as trace_viewer: expect(trace_viewer.action_titles).to_have_text( [ - re.compile(r"Page.goto"), - re.compile(r"Page.close"), + re.compile(r'Navigate to "/grid\.html"'), + re.compile(r"Close"), ] ) @@ -230,17 +227,17 @@ def test_should_collect_two_traces( with show_trace_viewer(tracing1_path) as trace_viewer: expect(trace_viewer.action_titles).to_have_text( [ - re.compile("Page.goto"), - re.compile("Page.set_content"), - re.compile("Page.click"), + re.compile(r'Navigate to "/empty\.html"'), + re.compile(r"Set content"), + re.compile(r"Click"), ] ) with show_trace_viewer(tracing2_path) as trace_viewer: expect(trace_viewer.action_titles).to_have_text( [ - re.compile(r"Page.dblclick"), - re.compile(r"Page.close"), + re.compile(r"Double click"), + re.compile(r"Close"), ] ) @@ -268,13 +265,13 @@ def test_should_work_with_playwright_context_managers( with show_trace_viewer(trace_file_path) as trace_viewer: expect(trace_viewer.action_titles).to_have_text( [ - re.compile("Page.goto"), - re.compile("Page.set_content"), - re.compile("Page.expect_console_message"), - re.compile("Page.evaluate"), - re.compile("Page.click"), - re.compile("Page.expect_popup"), - re.compile("Page.evaluate"), + re.compile(r'Navigate to "/empty\.html"'), + re.compile(r"Set content"), + re.compile(r'Wait for event "page\.expect_event\(console\)"'), + re.compile(r"Evaluate"), + re.compile(r"Click"), + re.compile(r'Wait for event "page\.expect_event\(popup\)"'), + re.compile(r"Evaluate"), ] ) @@ -298,9 +295,9 @@ def test_should_display_wait_for_load_state_even_if_did_not_wait_for_it( with show_trace_viewer(trace_file_path) as trace_viewer: expect(trace_viewer.action_titles).to_have_text( [ - re.compile(r"Page.goto"), - re.compile(r"Page.wait_for_load_state"), - re.compile(r"Page.wait_for_load_state"), + re.compile(r'Navigate to "/empty\.html"'), + re.compile(r'Wait for event "frame\.wait_for_load_state"'), + re.compile(r'Wait for event "frame\.wait_for_load_state"'), ] ) @@ -334,10 +331,10 @@ def test_should_respect_traces_dir_and_name( with show_trace_viewer(tmp_path / "trace1.zip") as trace_viewer: expect(trace_viewer.action_titles).to_have_text( [ - re.compile(r"Page.goto"), + re.compile(r'Navigate to "/one-style\.html"'), ] ) - frame = trace_viewer.snapshot_frame("Page.goto", 0, False) + frame = trace_viewer.snapshot_frame('Navigate to "/one-style.html"', 0, False) expect(frame.locator("body")).to_have_css( "background-color", "rgb(255, 192, 203)" ) @@ -346,10 +343,10 @@ def test_should_respect_traces_dir_and_name( with show_trace_viewer(tmp_path / "trace2.zip") as trace_viewer: expect(trace_viewer.action_titles).to_have_text( [ - re.compile(r"Page.goto"), + re.compile(r'Navigate to "/har\.html"'), ] ) - frame = trace_viewer.snapshot_frame("Page.goto", 0, False) + frame = trace_viewer.snapshot_frame('Navigate to "/har.html"', 0, False) expect(frame.locator("body")).to_have_css( "background-color", "rgb(255, 192, 203)" ) @@ -381,12 +378,12 @@ def test_should_show_tracing_group_in_action_list( trace_viewer.expand_action("inner group 1") expect(trace_viewer.action_titles).to_have_text( [ - "BrowserContext.new_page", - "outer group", - re.compile("Page.goto"), - "inner group 1", - re.compile("Locator.click"), - "inner group 2", - re.compile("Locator.is_visible"), + re.compile(r"Create page"), + re.compile(r"outer group"), + re.compile(r"Navigate to \"data:\""), + re.compile(r"inner group 1"), + re.compile(r"Click"), + re.compile(r"inner group 2"), + re.compile(r"Is visible"), ] ) From 3713a322d85482d9ff7a591272e1938b31b10848 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Wed, 25 Jun 2025 09:15:21 -0700 Subject: [PATCH 09/79] chore: explicit timeout config for send (#2900) --- playwright/_impl/_accessibility.py | 2 +- playwright/_impl/_artifact.py | 40 ++++++- playwright/_impl/_browser.py | 12 +- playwright/_impl/_browser_context.py | 35 +++--- playwright/_impl/_browser_type.py | 16 ++- playwright/_impl/_cdp_session.py | 7 +- playwright/_impl/_clock.py | 32 +++-- playwright/_impl/_connection.py | 55 +++++---- playwright/_impl/_dialog.py | 7 +- playwright/_impl/_element_handle.py | 110 ++++++++++++------ playwright/_impl/_fetch.py | 13 ++- playwright/_impl/_frame.py | 116 ++++++++++++------- playwright/_impl/_har_router.py | 4 +- playwright/_impl/_input.py | 24 ++-- playwright/_impl/_js_handle.py | 21 +++- playwright/_impl/_json_pipe.py | 4 +- playwright/_impl/_local_utils.py | 15 +-- playwright/_impl/_locator.py | 3 + playwright/_impl/_network.py | 74 +++++++++--- playwright/_impl/_page.py | 81 ++++++++----- playwright/_impl/_selectors.py | 8 +- playwright/_impl/_set_input_files_helpers.py | 1 + playwright/_impl/_stream.py | 4 +- playwright/_impl/_tracing.py | 23 ++-- playwright/_impl/_waiter.py | 3 + playwright/_impl/_writable_stream.py | 4 +- 26 files changed, 478 insertions(+), 236 deletions(-) diff --git a/playwright/_impl/_accessibility.py b/playwright/_impl/_accessibility.py index 010b4e8c5..fe6909c21 100644 --- a/playwright/_impl/_accessibility.py +++ b/playwright/_impl/_accessibility.py @@ -65,5 +65,5 @@ async def snapshot( params = locals_to_params(locals()) if root: params["root"] = root._channel - result = await self._channel.send("accessibilitySnapshot", params) + result = await self._channel.send("accessibilitySnapshot", None, params) return _ax_node_from_protocol(result) if result else None diff --git a/playwright/_impl/_artifact.py b/playwright/_impl/_artifact.py index a5af44573..a08294cbe 100644 --- a/playwright/_impl/_artifact.py +++ b/playwright/_impl/_artifact.py @@ -33,27 +33,55 @@ async def path_after_finished(self) -> pathlib.Path: raise Error( "Path is not available when using browser_type.connect(). Use save_as() to save a local copy." ) - path = await self._channel.send("pathAfterFinished") + path = await self._channel.send( + "pathAfterFinished", + None, + ) return pathlib.Path(path) async def save_as(self, path: Union[str, Path]) -> None: - stream = cast(Stream, from_channel(await self._channel.send("saveAsStream"))) + stream = cast( + Stream, + from_channel( + await self._channel.send( + "saveAsStream", + None, + ) + ), + ) make_dirs_for_file(path) await stream.save_as(path) async def failure(self) -> Optional[str]: - reason = await self._channel.send("failure") + reason = await self._channel.send( + "failure", + None, + ) if reason is None: return None return patch_error_message(reason) async def delete(self) -> None: - await self._channel.send("delete") + await self._channel.send( + "delete", + None, + ) async def read_info_buffer(self) -> bytes: - stream = cast(Stream, from_channel(await self._channel.send("stream"))) + stream = cast( + Stream, + from_channel( + await self._channel.send( + "stream", + None, + ) + ), + ) buffer = await stream.read_all() return buffer async def cancel(self) -> None: # pyright: ignore[reportIncompatibleMethodOverride] - await self._channel.send("cancel") + await self._channel.send( + "cancel", + None, + ) diff --git a/playwright/_impl/_browser.py b/playwright/_impl/_browser.py index 9b3c1cacc..5a9a87450 100644 --- a/playwright/_impl/_browser.py +++ b/playwright/_impl/_browser.py @@ -168,7 +168,7 @@ async def new_context( assert self._browser_type is not None await self._browser_type._prepare_browser_context_params(params) - channel = await self._channel.send("newContext", params) + channel = await self._channel.send("newContext", None, params) context = cast(BrowserContext, from_channel(channel)) await context._initialize_har_from_options( record_har_content=recordHarContent, @@ -235,7 +235,7 @@ async def close(self, reason: str = None) -> None: if self._should_close_connection_on_close: await self._connection.stop_async() else: - await self._channel.send("close", {"reason": reason}) + await self._channel.send("close", None, {"reason": reason}) except Exception as e: if not is_target_closed_error(e): raise e @@ -245,7 +245,7 @@ def version(self) -> str: return self._initializer["version"] async def new_browser_cdp_session(self) -> CDPSession: - return from_channel(await self._channel.send("newBrowserCDPSession")) + return from_channel(await self._channel.send("newBrowserCDPSession", None)) async def start_tracing( self, @@ -260,10 +260,12 @@ async def start_tracing( if path: self._cr_tracing_path = str(path) params["path"] = str(path) - await self._channel.send("startTracing", params) + await self._channel.send("startTracing", None, params) async def stop_tracing(self) -> bytes: - artifact = cast(Artifact, from_channel(await self._channel.send("stopTracing"))) + artifact = cast( + Artifact, from_channel(await self._channel.send("stopTracing", None)) + ) buffer = await artifact.read_info_buffer() await artifact.delete() if self._cr_tracing_path: diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py index 1264d3f8b..60b60c46e 100644 --- a/playwright/_impl/_browser_context.py +++ b/playwright/_impl/_browser_context.py @@ -330,17 +330,17 @@ async def _initialize_har_from_options( async def new_page(self) -> Page: if self._owner_page: raise Error("Please use browser.new_context()") - return from_channel(await self._channel.send("newPage")) + return from_channel(await self._channel.send("newPage", None)) async def cookies(self, urls: Union[str, Sequence[str]] = None) -> List[Cookie]: if urls is None: urls = [] if isinstance(urls, str): urls = [urls] - return await self._channel.send("cookies", dict(urls=urls)) + return await self._channel.send("cookies", None, dict(urls=urls)) async def add_cookies(self, cookies: Sequence[SetCookieParam]) -> None: - await self._channel.send("addCookies", dict(cookies=cookies)) + await self._channel.send("addCookies", None, dict(cookies=cookies)) async def clear_cookies( self, @@ -350,6 +350,7 @@ async def clear_cookies( ) -> None: await self._channel.send( "clearCookies", + None, { "name": name if isinstance(name, str) else None, "nameRegexSource": name.pattern if isinstance(name, Pattern) else None, @@ -374,21 +375,21 @@ async def clear_cookies( async def grant_permissions( self, permissions: Sequence[str], origin: str = None ) -> None: - await self._channel.send("grantPermissions", locals_to_params(locals())) + await self._channel.send("grantPermissions", None, locals_to_params(locals())) async def clear_permissions(self) -> None: - await self._channel.send("clearPermissions") + await self._channel.send("clearPermissions", None) async def set_geolocation(self, geolocation: Geolocation = None) -> None: - await self._channel.send("setGeolocation", locals_to_params(locals())) + await self._channel.send("setGeolocation", None, locals_to_params(locals())) async def set_extra_http_headers(self, headers: Dict[str, str]) -> None: await self._channel.send( - "setExtraHTTPHeaders", dict(headers=serialize_headers(headers)) + "setExtraHTTPHeaders", None, dict(headers=serialize_headers(headers)) ) async def set_offline(self, offline: bool) -> None: - await self._channel.send("setOffline", dict(offline=offline)) + await self._channel.send("setOffline", None, dict(offline=offline)) async def add_init_script( self, script: str = None, path: Union[str, Path] = None @@ -397,7 +398,7 @@ async def add_init_script( script = (await async_readfile(path)).decode() if not isinstance(script, str): raise Error("Either path or script parameter must be specified") - await self._channel.send("addInitScript", dict(source=script)) + await self._channel.send("addInitScript", None, dict(source=script)) async def expose_binding( self, name: str, callback: Callable, handle: bool = None @@ -411,7 +412,7 @@ async def expose_binding( raise Error(f'Function "{name}" has been already registered') self._bindings[name] = callback await self._channel.send( - "exposeBinding", dict(name=name, needsHandle=handle or False) + "exposeBinding", None, dict(name=name, needsHandle=handle or False) ) async def expose_function(self, name: str, callback: Callable) -> None: @@ -499,7 +500,7 @@ async def _record_into_har( } if page: params["page"] = page._channel - har_id = await self._channel.send("harStart", params) + har_id = await self._channel.send("harStart", None, params) self._har_recorders[har_id] = { "path": str(har), "content": update_content, @@ -535,7 +536,7 @@ async def route_from_har( async def _update_interception_patterns(self) -> None: patterns = RouteHandler.prepare_interception_patterns(self._routes) await self._channel.send( - "setNetworkInterceptionPatterns", {"patterns": patterns} + "setNetworkInterceptionPatterns", None, {"patterns": patterns} ) async def _update_web_socket_interception_patterns(self) -> None: @@ -543,7 +544,7 @@ async def _update_web_socket_interception_patterns(self) -> None: self._web_socket_routes ) await self._channel.send( - "setWebSocketInterceptionPatterns", {"patterns": patterns} + "setWebSocketInterceptionPatterns", None, {"patterns": patterns} ) def expect_event( @@ -596,7 +597,7 @@ async def _inner_close() -> None: har = cast( Artifact, from_channel( - await self._channel.send("harExport", {"harId": har_id}) + await self._channel.send("harExport", None, {"harId": har_id}) ), ) # Server side will compress artifact if content is attach or if file is .zip. @@ -615,14 +616,14 @@ async def _inner_close() -> None: await har.delete() await self._channel._connection.wrap_api_call(_inner_close, True) - await self._channel.send("close", {"reason": reason}) + await self._channel.send("close", None, {"reason": reason}) await self._closed_future async def storage_state( self, path: Union[str, Path] = None, indexedDB: bool = None ) -> StorageState: result = await self._channel.send_return_as_dict( - "storageState", {"indexedDB": indexedDB} + "storageState", None, {"indexedDB": indexedDB} ) if path: await async_writefile(path, json.dumps(result)) @@ -749,7 +750,7 @@ async def new_cdp_session(self, page: Union[Page, Frame]) -> CDPSession: params["frame"] = page._channel else: raise Error("page: expected Page or Frame") - return from_channel(await self._channel.send("newCDPSession", params)) + return from_channel(await self._channel.send("newCDPSession", None, params)) @property def tracing(self) -> Tracing: diff --git a/playwright/_impl/_browser_type.py b/playwright/_impl/_browser_type.py index ab8c00e97..93173160c 100644 --- a/playwright/_impl/_browser_type.py +++ b/playwright/_impl/_browser_type.py @@ -93,7 +93,12 @@ async def launch( params = locals_to_params(locals()) normalize_launch_params(params) browser = cast( - Browser, from_channel(await self._channel.send("launch", params)) + Browser, + from_channel( + await self._channel.send( + "launch", TimeoutSettings.launch_timeout, params + ) + ), ) browser._connect_to_browser_type( self, str(tracesDir) if tracesDir is not None else None @@ -159,7 +164,7 @@ async def launch_persistent_context( await self._prepare_browser_context_params(params) normalize_launch_params(params) result = await self._channel.send_return_as_dict( - "launchPersistentContext", params + "launchPersistentContext", TimeoutSettings.launch_timeout, params ) browser = cast( Browser, @@ -197,10 +202,11 @@ async def connect_over_cdp( headers: Dict[str, str] = None, ) -> Browser: params = locals_to_params(locals()) - params["timeout"] = TimeoutSettings.launch_timeout(timeout) if params.get("headers"): params["headers"] = serialize_headers(params["headers"]) - response = await self._channel.send_return_as_dict("connectOverCDP", params) + response = await self._channel.send_return_as_dict( + "connectOverCDP", TimeoutSettings.launch_timeout, params + ) browser = cast(Browser, from_channel(response["browser"])) browser._connect_to_browser_type(self, None) @@ -222,6 +228,7 @@ async def connect( pipe_channel = ( await local_utils._channel.send_return_as_dict( "connect", + None, { "wsEndpoint": wsEndpoint, "headers": headers, @@ -355,4 +362,3 @@ def normalize_launch_params(params: Dict) -> None: params["downloadsPath"] = str(Path(params["downloadsPath"])) if "tracesDir" in params: params["tracesDir"] = str(Path(params["tracesDir"])) - params["timeout"] = TimeoutSettings.launch_timeout(params.get("timeout")) diff --git a/playwright/_impl/_cdp_session.py b/playwright/_impl/_cdp_session.py index b6e383ff2..95e65c57a 100644 --- a/playwright/_impl/_cdp_session.py +++ b/playwright/_impl/_cdp_session.py @@ -29,7 +29,10 @@ def _on_event(self, params: Any) -> None: self.emit(params["method"], params.get("params")) async def send(self, method: str, params: Dict = None) -> Dict: - return await self._channel.send("send", locals_to_params(locals())) + return await self._channel.send("send", None, locals_to_params(locals())) async def detach(self) -> None: - await self._channel.send("detach") + await self._channel.send( + "detach", + None, + ) diff --git a/playwright/_impl/_clock.py b/playwright/_impl/_clock.py index d8bb58718..928536019 100644 --- a/playwright/_impl/_clock.py +++ b/playwright/_impl/_clock.py @@ -27,7 +27,9 @@ def __init__(self, browser_context: "BrowserContext") -> None: async def install(self, time: Union[float, str, datetime.datetime] = None) -> None: await self._browser_context._channel.send( - "clockInstall", parse_time(time) if time is not None else {} + "clockInstall", + None, + parse_time(time) if time is not None else {}, ) async def fast_forward( @@ -35,38 +37,54 @@ async def fast_forward( ticks: Union[int, str], ) -> None: await self._browser_context._channel.send( - "clockFastForward", parse_ticks(ticks) + "clockFastForward", + None, + parse_ticks(ticks), ) async def pause_at( self, time: Union[float, str, datetime.datetime], ) -> None: - await self._browser_context._channel.send("clockPauseAt", parse_time(time)) + await self._browser_context._channel.send( + "clockPauseAt", + None, + parse_time(time), + ) async def resume( self, ) -> None: - await self._browser_context._channel.send("clockResume") + await self._browser_context._channel.send("clockResume", None) async def run_for( self, ticks: Union[int, str], ) -> None: - await self._browser_context._channel.send("clockRunFor", parse_ticks(ticks)) + await self._browser_context._channel.send( + "clockRunFor", + None, + parse_ticks(ticks), + ) async def set_fixed_time( self, time: Union[float, str, datetime.datetime], ) -> None: - await self._browser_context._channel.send("clockSetFixedTime", parse_time(time)) + await self._browser_context._channel.send( + "clockSetFixedTime", + None, + parse_time(time), + ) async def set_system_time( self, time: Union[float, str, datetime.datetime], ) -> None: await self._browser_context._channel.send( - "clockSetSystemTime", parse_time(time) + "clockSetSystemTime", + None, + parse_time(time), ) diff --git a/playwright/_impl/_connection.py b/playwright/_impl/_connection.py index 3519aeebd..a837500b1 100644 --- a/playwright/_impl/_connection.py +++ b/playwright/_impl/_connection.py @@ -47,6 +47,8 @@ from playwright._impl._local_utils import LocalUtils from playwright._impl._playwright import Playwright +TimeoutCalculator = Optional[Callable[[Optional[float]], float]] + class Channel(AsyncIOEventEmitter): def __init__(self, connection: "Connection", object: "ChannelOwner") -> None: @@ -55,17 +57,17 @@ def __init__(self, connection: "Connection", object: "ChannelOwner") -> None: self._guid = object._guid self._object = object self.on("error", lambda exc: self._connection._on_event_listener_error(exc)) - self._timeout_calculator: Optional[Callable[[Optional[float]], float]] = None async def send( self, method: str, + timeout_calculator: TimeoutCalculator, params: Dict = None, is_internal: bool = False, title: str = None, ) -> Any: return await self._connection.wrap_api_call( - lambda: self._inner_send(method, params, False), + lambda: self._inner_send(method, timeout_calculator, params, False), is_internal, title, ) @@ -73,12 +75,13 @@ async def send( async def send_return_as_dict( self, method: str, + timeout_calculator: TimeoutCalculator, params: Dict = None, is_internal: bool = False, title: str = None, ) -> Any: return await self._connection.wrap_api_call( - lambda: self._inner_send(method, params, True), + lambda: self._inner_send(method, timeout_calculator, params, True), is_internal, title, ) @@ -86,6 +89,7 @@ async def send_return_as_dict( def send_no_reply( self, method: str, + timeout_calculator: TimeoutCalculator, params: Dict = None, is_internal: bool = False, title: str = None, @@ -93,25 +97,28 @@ def send_no_reply( # No reply messages are used to e.g. waitForEventInfo(after). self._connection.wrap_api_call_sync( lambda: self._connection._send_message_to_server( - self._object, method, {} if params is None else params, True + self._object, + method, + _augment_params(params, timeout_calculator), + True, ), is_internal, title, ) async def _inner_send( - self, method: str, params: Optional[Dict], return_as_dict: bool + self, + method: str, + timeout_calculator: TimeoutCalculator, + params: Optional[Dict], + return_as_dict: bool, ) -> Any: - if params is None: - params = {} - if self._timeout_calculator is not None: - params["timeout"] = self._timeout_calculator(params.get("timeout")) if self._connection._error: error = self._connection._error self._connection._error = None raise error callback = self._connection._send_message_to_server( - self._object, method, _filter_none(params) + self._object, method, _augment_params(params, timeout_calculator) ) done, _ = await asyncio.wait( { @@ -136,11 +143,6 @@ async def _inner_send( key = next(iter(result)) return result[key] - def _set_timeout_calculator( - self, timeout_calculator: Callable[[Optional[float]], float] - ) -> None: - self._timeout_calculator = timeout_calculator - class ChannelOwner(AsyncIOEventEmitter): def __init__( @@ -197,7 +199,9 @@ def _update_subscription(self, event: str, enabled: bool) -> None: if protocol_event: self._connection.wrap_api_call_sync( lambda: self._channel.send_no_reply( - "updateSubscription", {"event": protocol_event, "enabled": enabled} + "updateSubscription", + None, + {"event": protocol_event, "enabled": enabled}, ), True, ) @@ -244,6 +248,7 @@ async def initialize(self) -> "Playwright": return from_channel( await self._channel.send( "initialize", + None, { "sdkLanguage": "python", }, @@ -639,17 +644,23 @@ def _extract_stack_trace_information_from_stack( } +def _augment_params( + params: Optional[Dict], + timeout_calculator: Optional[Callable[[Optional[float]], float]], +) -> Dict: + if params is None: + params = {} + if timeout_calculator: + params["timeout"] = timeout_calculator(params.get("timeout")) + return _filter_none(params) + + def _filter_none(d: Mapping) -> Dict: result = {} for k, v in d.items(): if v is None: continue - elif isinstance(v, dict): - filtered_v = _filter_none(v) - if filtered_v: - result[k] = filtered_v - else: - result[k] = v + result[k] = _filter_none(v) if isinstance(v, dict) else v return result diff --git a/playwright/_impl/_dialog.py b/playwright/_impl/_dialog.py index a0c6ca77f..226e703b9 100644 --- a/playwright/_impl/_dialog.py +++ b/playwright/_impl/_dialog.py @@ -48,7 +48,10 @@ def page(self) -> Optional["Page"]: return self._page async def accept(self, promptText: str = None) -> None: - await self._channel.send("accept", locals_to_params(locals())) + await self._channel.send("accept", None, locals_to_params(locals())) async def dismiss(self) -> None: - await self._channel.send("dismiss") + await self._channel.send( + "dismiss", + None, + ) diff --git a/playwright/_impl/_element_handle.py b/playwright/_impl/_element_handle.py index fd99c0b00..88f1a7358 100644 --- a/playwright/_impl/_element_handle.py +++ b/playwright/_impl/_element_handle.py @@ -56,57 +56,62 @@ def __init__( ) -> None: super().__init__(parent, type, guid, initializer) self._frame = cast("Frame", parent) - self._channel._set_timeout_calculator(self._frame._timeout) async def _createSelectorForTest(self, name: str) -> Optional[str]: - return await self._channel.send("createSelectorForTest", dict(name=name)) + return await self._channel.send( + "createSelectorForTest", self._frame._timeout, dict(name=name) + ) def as_element(self) -> Optional["ElementHandle"]: return self async def owner_frame(self) -> Optional["Frame"]: - return from_nullable_channel(await self._channel.send("ownerFrame")) + return from_nullable_channel(await self._channel.send("ownerFrame", None)) async def content_frame(self) -> Optional["Frame"]: - return from_nullable_channel(await self._channel.send("contentFrame")) + return from_nullable_channel(await self._channel.send("contentFrame", None)) async def get_attribute(self, name: str) -> Optional[str]: - return await self._channel.send("getAttribute", dict(name=name)) + return await self._channel.send("getAttribute", None, dict(name=name)) async def text_content(self) -> Optional[str]: - return await self._channel.send("textContent") + return await self._channel.send("textContent", None) async def inner_text(self) -> str: - return await self._channel.send("innerText") + return await self._channel.send("innerText", None) async def inner_html(self) -> str: - return await self._channel.send("innerHTML") + return await self._channel.send("innerHTML", None) async def is_checked(self) -> bool: - return await self._channel.send("isChecked") + return await self._channel.send("isChecked", None) async def is_disabled(self) -> bool: - return await self._channel.send("isDisabled") + return await self._channel.send("isDisabled", None) async def is_editable(self) -> bool: - return await self._channel.send("isEditable") + return await self._channel.send("isEditable", None) async def is_enabled(self) -> bool: - return await self._channel.send("isEnabled") + return await self._channel.send("isEnabled", None) async def is_hidden(self) -> bool: - return await self._channel.send("isHidden") + return await self._channel.send("isHidden", None) async def is_visible(self) -> bool: - return await self._channel.send("isVisible") + return await self._channel.send("isVisible", None) async def dispatch_event(self, type: str, eventInit: Dict = None) -> None: await self._channel.send( - "dispatchEvent", dict(type=type, eventInit=serialize_argument(eventInit)) + "dispatchEvent", + None, + dict(type=type, eventInit=serialize_argument(eventInit)), ) async def scroll_into_view_if_needed(self, timeout: float = None) -> None: - await self._channel.send("scrollIntoViewIfNeeded", locals_to_params(locals())) + await self._channel.send( + "scrollIntoViewIfNeeded", self._frame._timeout, locals_to_params(locals()) + ) async def hover( self, @@ -117,7 +122,9 @@ async def hover( force: bool = None, trial: bool = None, ) -> None: - await self._channel.send("hover", locals_to_params(locals())) + await self._channel.send( + "hover", self._frame._timeout, locals_to_params(locals()) + ) async def click( self, @@ -131,7 +138,9 @@ async def click( noWaitAfter: bool = None, trial: bool = None, ) -> None: - await self._channel.send("click", locals_to_params(locals())) + await self._channel.send( + "click", self._frame._timeout, locals_to_params(locals()) + ) async def dblclick( self, @@ -144,7 +153,9 @@ async def dblclick( noWaitAfter: bool = None, trial: bool = None, ) -> None: - await self._channel.send("dblclick", locals_to_params(locals())) + await self._channel.send( + "dblclick", self._frame._timeout, locals_to_params(locals()) + ) async def select_option( self, @@ -163,7 +174,7 @@ async def select_option( **convert_select_option_values(value, index, label, element), ) ) - return await self._channel.send("selectOption", params) + return await self._channel.send("selectOption", self._frame._timeout, params) async def tap( self, @@ -174,7 +185,9 @@ async def tap( noWaitAfter: bool = None, trial: bool = None, ) -> None: - await self._channel.send("tap", locals_to_params(locals())) + await self._channel.send( + "tap", self._frame._timeout, locals_to_params(locals()) + ) async def fill( self, @@ -183,13 +196,19 @@ async def fill( noWaitAfter: bool = None, force: bool = None, ) -> None: - await self._channel.send("fill", locals_to_params(locals())) + await self._channel.send( + "fill", self._frame._timeout, locals_to_params(locals()) + ) async def select_text(self, force: bool = None, timeout: float = None) -> None: - await self._channel.send("selectText", locals_to_params(locals())) + await self._channel.send( + "selectText", self._frame._timeout, locals_to_params(locals()) + ) async def input_value(self, timeout: float = None) -> str: - return await self._channel.send("inputValue", locals_to_params(locals())) + return await self._channel.send( + "inputValue", self._frame._timeout, locals_to_params(locals()) + ) async def set_input_files( self, @@ -205,14 +224,15 @@ async def set_input_files( converted = await convert_input_files(files, frame.page.context) await self._channel.send( "setInputFiles", + self._frame._timeout, { - "timeout": self._frame._timeout(timeout), + "timeout": timeout, **converted, }, ) async def focus(self) -> None: - await self._channel.send("focus") + await self._channel.send("focus", None) async def type( self, @@ -221,7 +241,9 @@ async def type( timeout: float = None, noWaitAfter: bool = None, ) -> None: - await self._channel.send("type", locals_to_params(locals())) + await self._channel.send( + "type", self._frame._timeout, locals_to_params(locals()) + ) async def press( self, @@ -230,7 +252,9 @@ async def press( timeout: float = None, noWaitAfter: bool = None, ) -> None: - await self._channel.send("press", locals_to_params(locals())) + await self._channel.send( + "press", self._frame._timeout, locals_to_params(locals()) + ) async def set_checked( self, @@ -264,7 +288,9 @@ async def check( noWaitAfter: bool = None, trial: bool = None, ) -> None: - await self._channel.send("check", locals_to_params(locals())) + await self._channel.send( + "check", self._frame._timeout, locals_to_params(locals()) + ) async def uncheck( self, @@ -274,10 +300,12 @@ async def uncheck( noWaitAfter: bool = None, trial: bool = None, ) -> None: - await self._channel.send("uncheck", locals_to_params(locals())) + await self._channel.send( + "uncheck", self._frame._timeout, locals_to_params(locals()) + ) async def bounding_box(self) -> Optional[FloatRect]: - return await self._channel.send("boundingBox") + return await self._channel.send("boundingBox", None) async def screenshot( self, @@ -308,7 +336,9 @@ async def screenshot( params["mask"], ) ) - encoded_binary = await self._channel.send("screenshot", params) + encoded_binary = await self._channel.send( + "screenshot", self._frame._timeout, params + ) decoded_binary = base64.b64decode(encoded_binary) if path: make_dirs_for_file(path) @@ -317,14 +347,16 @@ async def screenshot( async def query_selector(self, selector: str) -> Optional["ElementHandle"]: return from_nullable_channel( - await self._channel.send("querySelector", dict(selector=selector)) + await self._channel.send("querySelector", None, dict(selector=selector)) ) async def query_selector_all(self, selector: str) -> List["ElementHandle"]: return list( map( cast(Callable[[Any], Any], from_nullable_channel), - await self._channel.send("querySelectorAll", dict(selector=selector)), + await self._channel.send( + "querySelectorAll", None, dict(selector=selector) + ), ) ) @@ -337,6 +369,7 @@ async def eval_on_selector( return parse_result( await self._channel.send( "evalOnSelector", + None, dict( selector=selector, expression=expression, @@ -354,6 +387,7 @@ async def eval_on_selector_all( return parse_result( await self._channel.send( "evalOnSelectorAll", + None, dict( selector=selector, expression=expression, @@ -369,7 +403,9 @@ async def wait_for_element_state( ], timeout: float = None, ) -> None: - await self._channel.send("waitForElementState", locals_to_params(locals())) + await self._channel.send( + "waitForElementState", self._frame._timeout, locals_to_params(locals()) + ) async def wait_for_selector( self, @@ -379,7 +415,9 @@ async def wait_for_selector( strict: bool = None, ) -> Optional["ElementHandle"]: return from_nullable_channel( - await self._channel.send("waitForSelector", locals_to_params(locals())) + await self._channel.send( + "waitForSelector", self._frame._timeout, locals_to_params(locals()) + ) ) diff --git a/playwright/_impl/_fetch.py b/playwright/_impl/_fetch.py index a0120e0cd..e4174ea27 100644 --- a/playwright/_impl/_fetch.py +++ b/playwright/_impl/_fetch.py @@ -91,7 +91,9 @@ async def new_context( ) context = cast( APIRequestContext, - from_channel(await self.playwright._channel.send("newRequest", params)), + from_channel( + await self.playwright._channel.send("newRequest", None, params) + ), ) context._timeout_settings.set_default_timeout(timeout) return context @@ -105,12 +107,11 @@ def __init__( self._tracing: Tracing = from_channel(initializer["tracing"]) self._close_reason: Optional[str] = None self._timeout_settings = TimeoutSettings(None) - self._channel._set_timeout_calculator(self._timeout_settings.timeout) async def dispose(self, reason: str = None) -> None: self._close_reason = reason try: - await self._channel.send("dispose", {"reason": reason}) + await self._channel.send("dispose", None, {"reason": reason}) except Error as e: if is_target_closed_error(e): return @@ -408,6 +409,7 @@ async def _inner_fetch( response = await self._channel.send( "fetch", + self._timeout_settings.timeout, { "url": url, "params": object_to_array(params) if isinstance(params, dict) else None, @@ -432,7 +434,7 @@ async def storage_state( indexedDB: bool = None, ) -> StorageState: result = await self._channel.send_return_as_dict( - "storageState", {"indexedDB": indexedDB} + "storageState", None, {"indexedDB": indexedDB} ) if path: await async_writefile(path, json.dumps(result)) @@ -487,6 +489,7 @@ async def body(self) -> bytes: result = await self._request._connection.wrap_api_call( lambda: self._request._channel.send_return_as_dict( "fetchResponseBody", + None, { "fetchUid": self._fetch_uid, }, @@ -512,6 +515,7 @@ async def json(self) -> Any: async def dispose(self) -> None: await self._request._channel.send( "disposeAPIResponse", + None, { "fetchUid": self._fetch_uid, }, @@ -524,6 +528,7 @@ def _fetch_uid(self) -> str: async def _fetch_log(self) -> List[str]: return await self._request._channel.send( "fetchLog", + None, { "fetchUid": self._fetch_uid, }, diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py index fc3c4a54d..c0646b680 100644 --- a/playwright/_impl/_frame.py +++ b/playwright/_impl/_frame.py @@ -101,7 +101,6 @@ def __init__( "navigated", lambda params: self._on_frame_navigated(params), ) - self._channel._set_timeout_calculator(self._timeout) def __repr__(self) -> str: return f"" @@ -127,7 +126,7 @@ def _on_frame_navigated(self, event: FrameNavigatedEvent) -> None: self._page.emit("framenavigated", self) async def _query_count(self, selector: str) -> int: - return await self._channel.send("queryCount", {"selector": selector}) + return await self._channel.send("queryCount", None, {"selector": selector}) @property def page(self) -> "Page": @@ -145,7 +144,7 @@ async def goto( Optional[Response], from_nullable_channel( await self._channel.send( - "goto", self._locals_to_params_with_navigation_timeout(locals()) + "goto", self._navigation_timeout, locals_to_params(locals()) ) ), ) @@ -286,12 +285,13 @@ def _navigation_timeout(self, timeout: Optional[float]) -> float: return timeout_settings.navigation_timeout(timeout) async def frame_element(self) -> ElementHandle: - return from_channel(await self._channel.send("frameElement")) + return from_channel(await self._channel.send("frameElement", None)) async def evaluate(self, expression: str, arg: Serializable = None) -> Any: return parse_result( await self._channel.send( "evaluateExpression", + None, dict( expression=expression, arg=serialize_argument(arg), @@ -305,6 +305,7 @@ async def evaluate_handle( return from_channel( await self._channel.send( "evaluateExpressionHandle", + None, dict( expression=expression, arg=serialize_argument(arg), @@ -316,14 +317,16 @@ async def query_selector( self, selector: str, strict: bool = None ) -> Optional[ElementHandle]: return from_nullable_channel( - await self._channel.send("querySelector", locals_to_params(locals())) + await self._channel.send("querySelector", None, locals_to_params(locals())) ) async def query_selector_all(self, selector: str) -> List[ElementHandle]: return list( map( from_channel, - await self._channel.send("querySelectorAll", dict(selector=selector)), + await self._channel.send( + "querySelectorAll", None, dict(selector=selector) + ), ) ) @@ -335,34 +338,48 @@ async def wait_for_selector( state: Literal["attached", "detached", "hidden", "visible"] = None, ) -> Optional[ElementHandle]: return from_nullable_channel( - await self._channel.send("waitForSelector", locals_to_params(locals())) + await self._channel.send( + "waitForSelector", self._timeout, locals_to_params(locals()) + ) ) async def is_checked( self, selector: str, strict: bool = None, timeout: float = None ) -> bool: - return await self._channel.send("isChecked", locals_to_params(locals())) + return await self._channel.send( + "isChecked", self._timeout, locals_to_params(locals()) + ) async def is_disabled( self, selector: str, strict: bool = None, timeout: float = None ) -> bool: - return await self._channel.send("isDisabled", locals_to_params(locals())) + return await self._channel.send( + "isDisabled", self._timeout, locals_to_params(locals()) + ) async def is_editable( self, selector: str, strict: bool = None, timeout: float = None ) -> bool: - return await self._channel.send("isEditable", locals_to_params(locals())) + return await self._channel.send( + "isEditable", self._timeout, locals_to_params(locals()) + ) async def is_enabled( self, selector: str, strict: bool = None, timeout: float = None ) -> bool: - return await self._channel.send("isEnabled", locals_to_params(locals())) + return await self._channel.send( + "isEnabled", self._timeout, locals_to_params(locals()) + ) async def is_hidden(self, selector: str, strict: bool = None) -> bool: - return await self._channel.send("isHidden", locals_to_params(locals())) + return await self._channel.send( + "isHidden", self._timeout, locals_to_params(locals()) + ) async def is_visible(self, selector: str, strict: bool = None) -> bool: - return await self._channel.send("isVisible", locals_to_params(locals())) + return await self._channel.send( + "isVisible", self._timeout, locals_to_params(locals()) + ) async def dispatch_event( self, @@ -374,6 +391,7 @@ async def dispatch_event( ) -> None: await self._channel.send( "dispatchEvent", + self._timeout, locals_to_params( dict( selector=selector, @@ -395,6 +413,7 @@ async def eval_on_selector( return parse_result( await self._channel.send( "evalOnSelector", + None, locals_to_params( dict( selector=selector, @@ -415,6 +434,7 @@ async def eval_on_selector_all( return parse_result( await self._channel.send( "evalOnSelectorAll", + None, dict( selector=selector, expression=expression, @@ -424,7 +444,7 @@ async def eval_on_selector_all( ) async def content(self) -> str: - return await self._channel.send("content") + return await self._channel.send("content", None) async def set_content( self, @@ -433,7 +453,7 @@ async def set_content( waitUntil: DocumentLoadState = None, ) -> None: await self._channel.send( - "setContent", self._locals_to_params_with_navigation_timeout(locals()) + "setContent", self._navigation_timeout, locals_to_params(locals()) ) @property @@ -468,7 +488,7 @@ async def add_script_tag( (await async_readfile(path)).decode(), path ) del params["path"] - return from_channel(await self._channel.send("addScriptTag", params)) + return from_channel(await self._channel.send("addScriptTag", None, params)) async def add_style_tag( self, url: str = None, path: Union[str, Path] = None, content: str = None @@ -482,7 +502,7 @@ async def add_style_tag( + "*/" ) del params["path"] - return from_channel(await self._channel.send("addStyleTag", params)) + return from_channel(await self._channel.send("addStyleTag", None, params)) async def click( self, @@ -498,7 +518,7 @@ async def click( strict: bool = None, trial: bool = None, ) -> None: - await self._channel.send("click", locals_to_params(locals())) + await self._channel.send("click", self._timeout, locals_to_params(locals())) async def dblclick( self, @@ -514,7 +534,7 @@ async def dblclick( trial: bool = None, ) -> None: await self._channel.send( - "dblclick", locals_to_params(locals()), title="Double click" + "dblclick", self._timeout, locals_to_params(locals()), title="Double click" ) async def tap( @@ -528,7 +548,7 @@ async def tap( strict: bool = None, trial: bool = None, ) -> None: - await self._channel.send("tap", locals_to_params(locals())) + await self._channel.send("tap", self._timeout, locals_to_params(locals())) async def fill( self, @@ -539,7 +559,7 @@ async def fill( strict: bool = None, force: bool = None, ) -> None: - await self._channel.send("fill", locals_to_params(locals())) + await self._channel.send("fill", self._timeout, locals_to_params(locals())) def locator( self, @@ -620,27 +640,35 @@ def frame_locator(self, selector: str) -> FrameLocator: async def focus( self, selector: str, strict: bool = None, timeout: float = None ) -> None: - await self._channel.send("focus", locals_to_params(locals())) + await self._channel.send("focus", self._timeout, locals_to_params(locals())) async def text_content( self, selector: str, strict: bool = None, timeout: float = None ) -> Optional[str]: - return await self._channel.send("textContent", locals_to_params(locals())) + return await self._channel.send( + "textContent", self._timeout, locals_to_params(locals()) + ) async def inner_text( self, selector: str, strict: bool = None, timeout: float = None ) -> str: - return await self._channel.send("innerText", locals_to_params(locals())) + return await self._channel.send( + "innerText", self._timeout, locals_to_params(locals()) + ) async def inner_html( self, selector: str, strict: bool = None, timeout: float = None ) -> str: - return await self._channel.send("innerHTML", locals_to_params(locals())) + return await self._channel.send( + "innerHTML", self._timeout, locals_to_params(locals()) + ) async def get_attribute( self, selector: str, name: str, strict: bool = None, timeout: float = None ) -> Optional[str]: - return await self._channel.send("getAttribute", locals_to_params(locals())) + return await self._channel.send( + "getAttribute", self._timeout, locals_to_params(locals()) + ) async def hover( self, @@ -653,7 +681,7 @@ async def hover( strict: bool = None, trial: bool = None, ) -> None: - await self._channel.send("hover", locals_to_params(locals())) + await self._channel.send("hover", self._timeout, locals_to_params(locals())) async def drag_and_drop( self, @@ -667,7 +695,9 @@ async def drag_and_drop( timeout: float = None, trial: bool = None, ) -> None: - await self._channel.send("dragAndDrop", locals_to_params(locals())) + await self._channel.send( + "dragAndDrop", self._timeout, locals_to_params(locals()) + ) async def select_option( self, @@ -690,7 +720,7 @@ async def select_option( **convert_select_option_values(value, index, label, element), ) ) - return await self._channel.send("selectOption", params) + return await self._channel.send("selectOption", self._timeout, params) async def input_value( self, @@ -698,7 +728,9 @@ async def input_value( strict: bool = None, timeout: float = None, ) -> str: - return await self._channel.send("inputValue", locals_to_params(locals())) + return await self._channel.send( + "inputValue", self._timeout, locals_to_params(locals()) + ) async def set_input_files( self, @@ -713,6 +745,7 @@ async def set_input_files( converted = await convert_input_files(files, self.page.context) await self._channel.send( "setInputFiles", + self._timeout, { "selector": selector, "strict": strict, @@ -730,7 +763,7 @@ async def type( timeout: float = None, noWaitAfter: bool = None, ) -> None: - await self._channel.send("type", locals_to_params(locals())) + await self._channel.send("type", self._timeout, locals_to_params(locals())) async def press( self, @@ -741,7 +774,7 @@ async def press( timeout: float = None, noWaitAfter: bool = None, ) -> None: - await self._channel.send("press", locals_to_params(locals())) + await self._channel.send("press", self._timeout, locals_to_params(locals())) async def check( self, @@ -753,7 +786,7 @@ async def check( strict: bool = None, trial: bool = None, ) -> None: - await self._channel.send("check", locals_to_params(locals())) + await self._channel.send("check", self._timeout, locals_to_params(locals())) async def uncheck( self, @@ -765,10 +798,10 @@ async def uncheck( strict: bool = None, trial: bool = None, ) -> None: - await self._channel.send("uncheck", locals_to_params(locals())) + await self._channel.send("uncheck", self._timeout, locals_to_params(locals())) async def wait_for_timeout(self, timeout: float) -> None: - await self._channel.send("waitForTimeout", locals_to_params(locals())) + await self._channel.send("waitForTimeout", None, locals_to_params(locals())) async def wait_for_function( self, @@ -783,10 +816,12 @@ async def wait_for_function( params["arg"] = serialize_argument(arg) if polling is not None and polling != "raf": params["pollingInterval"] = polling - return from_channel(await self._channel.send("waitForFunction", params)) + return from_channel( + await self._channel.send("waitForFunction", self._timeout, params) + ) async def title(self) -> str: - return await self._channel.send("title") + return await self._channel.send("title", None) async def set_checked( self, @@ -819,9 +854,4 @@ async def set_checked( ) async def _highlight(self, selector: str) -> None: - await self._channel.send("highlight", {"selector": selector}) - - def _locals_to_params_with_navigation_timeout(self, args: Dict) -> Dict: - params = locals_to_params(args) - params["timeout"] = self._navigation_timeout(params.get("timeout")) - return params + await self._channel.send("highlight", None, {"selector": selector}) diff --git a/playwright/_impl/_har_router.py b/playwright/_impl/_har_router.py index 33ff37871..1fa1b0433 100644 --- a/playwright/_impl/_har_router.py +++ b/playwright/_impl/_har_router.py @@ -49,7 +49,7 @@ async def create( not_found_action: RouteFromHarNotFoundPolicy, url_matcher: Optional[URLMatch] = None, ) -> "HarRouter": - har_id = await local_utils._channel.send("harOpen", {"file": file}) + har_id = await local_utils._channel.send("harOpen", None, {"file": file}) return HarRouter( local_utils=local_utils, har_id=har_id, @@ -118,5 +118,5 @@ async def add_page_route(self, page: "Page") -> None: def dispose(self) -> None: asyncio.create_task( - self._local_utils._channel.send("harClose", {"harId": self._har_id}) + self._local_utils._channel.send("harClose", None, {"harId": self._har_id}) ) diff --git a/playwright/_impl/_input.py b/playwright/_impl/_input.py index 0e986ae8c..8a39242ee 100644 --- a/playwright/_impl/_input.py +++ b/playwright/_impl/_input.py @@ -23,19 +23,19 @@ def __init__(self, channel: Channel) -> None: self._dispatcher_fiber = channel._connection._dispatcher_fiber async def down(self, key: str) -> None: - await self._channel.send("keyboardDown", locals_to_params(locals())) + await self._channel.send("keyboardDown", None, locals_to_params(locals())) async def up(self, key: str) -> None: - await self._channel.send("keyboardUp", locals_to_params(locals())) + await self._channel.send("keyboardUp", None, locals_to_params(locals())) async def insert_text(self, text: str) -> None: - await self._channel.send("keyboardInsertText", locals_to_params(locals())) + await self._channel.send("keyboardInsertText", None, locals_to_params(locals())) async def type(self, text: str, delay: float = None) -> None: - await self._channel.send("keyboardType", locals_to_params(locals())) + await self._channel.send("keyboardType", None, locals_to_params(locals())) async def press(self, key: str, delay: float = None) -> None: - await self._channel.send("keyboardPress", locals_to_params(locals())) + await self._channel.send("keyboardPress", None, locals_to_params(locals())) class Mouse: @@ -45,21 +45,21 @@ def __init__(self, channel: Channel) -> None: self._dispatcher_fiber = channel._connection._dispatcher_fiber async def move(self, x: float, y: float, steps: int = None) -> None: - await self._channel.send("mouseMove", locals_to_params(locals())) + await self._channel.send("mouseMove", None, locals_to_params(locals())) async def down( self, button: MouseButton = None, clickCount: int = None, ) -> None: - await self._channel.send("mouseDown", locals_to_params(locals())) + await self._channel.send("mouseDown", None, locals_to_params(locals())) async def up( self, button: MouseButton = None, clickCount: int = None, ) -> None: - await self._channel.send("mouseUp", locals_to_params(locals())) + await self._channel.send("mouseUp", None, locals_to_params(locals())) async def _click( self, @@ -70,7 +70,9 @@ async def _click( clickCount: int = None, title: str = None, ) -> None: - await self._channel.send("mouseClick", locals_to_params(locals()), title=title) + await self._channel.send( + "mouseClick", None, locals_to_params(locals()), title=title + ) async def click( self, @@ -96,7 +98,7 @@ async def dblclick( ) async def wheel(self, deltaX: float, deltaY: float) -> None: - await self._channel.send("mouseWheel", locals_to_params(locals())) + await self._channel.send("mouseWheel", None, locals_to_params(locals())) class Touchscreen: @@ -106,4 +108,4 @@ def __init__(self, channel: Channel) -> None: self._dispatcher_fiber = channel._connection._dispatcher_fiber async def tap(self, x: float, y: float) -> None: - await self._channel.send("touchscreenTap", locals_to_params(locals())) + await self._channel.send("touchscreenTap", None, locals_to_params(locals())) diff --git a/playwright/_impl/_js_handle.py b/playwright/_impl/_js_handle.py index 0d0d7e2ef..84ef40d18 100644 --- a/playwright/_impl/_js_handle.py +++ b/playwright/_impl/_js_handle.py @@ -71,6 +71,7 @@ async def evaluate(self, expression: str, arg: Serializable = None) -> Any: return parse_result( await self._channel.send( "evaluateExpression", + None, dict( expression=expression, arg=serialize_argument(arg), @@ -84,6 +85,7 @@ async def evaluate_handle( return from_channel( await self._channel.send( "evaluateExpressionHandle", + None, dict( expression=expression, arg=serialize_argument(arg), @@ -93,13 +95,16 @@ async def evaluate_handle( async def get_property(self, propertyName: str) -> "JSHandle": return from_channel( - await self._channel.send("getProperty", dict(name=propertyName)) + await self._channel.send("getProperty", None, dict(name=propertyName)) ) async def get_properties(self) -> Dict[str, "JSHandle"]: return { prop["name"]: from_channel(prop["value"]) - for prop in await self._channel.send("getPropertyList") + for prop in await self._channel.send( + "getPropertyList", + None, + ) } def as_element(self) -> Optional["ElementHandle"]: @@ -107,13 +112,21 @@ def as_element(self) -> Optional["ElementHandle"]: async def dispose(self) -> None: try: - await self._channel.send("dispose") + await self._channel.send( + "dispose", + None, + ) except Exception as e: if not is_target_closed_error(e): raise e async def json_value(self) -> Any: - return parse_result(await self._channel.send("jsonValue")) + return parse_result( + await self._channel.send( + "jsonValue", + None, + ) + ) def serialize_value( diff --git a/playwright/_impl/_json_pipe.py b/playwright/_impl/_json_pipe.py index 3a6973baf..41973b8c7 100644 --- a/playwright/_impl/_json_pipe.py +++ b/playwright/_impl/_json_pipe.py @@ -36,7 +36,7 @@ def __init__( def request_stop(self) -> None: self._stop_requested = True - self._pipe_channel.send_no_reply("close", {}) + self._pipe_channel.send_no_reply("close", None, {}) def dispose(self) -> None: self.on_error_future.cancel() @@ -74,4 +74,4 @@ async def run(self) -> None: def send(self, message: Dict) -> None: if self._stop_requested: raise Error("Playwright connection closed") - self._pipe_channel.send_no_reply("send", {"message": message}) + self._pipe_channel.send_no_reply("send", None, {"message": message}) diff --git a/playwright/_impl/_local_utils.py b/playwright/_impl/_local_utils.py index 7172ee58a..c2d2d3fca 100644 --- a/playwright/_impl/_local_utils.py +++ b/playwright/_impl/_local_utils.py @@ -31,11 +31,11 @@ def __init__( } async def zip(self, params: Dict) -> None: - await self._channel.send("zip", params) + await self._channel.send("zip", None, params) async def har_open(self, file: str) -> None: params = locals_to_params(locals()) - await self._channel.send("harOpen", params) + await self._channel.send("harOpen", None, params) async def har_lookup( self, @@ -51,27 +51,28 @@ async def har_lookup( params["postData"] = base64.b64encode(params["postData"]).decode() return cast( HarLookupResult, - await self._channel.send_return_as_dict("harLookup", params), + await self._channel.send_return_as_dict("harLookup", None, params), ) async def har_close(self, harId: str) -> None: params = locals_to_params(locals()) - await self._channel.send("harClose", params) + await self._channel.send("harClose", None, params) async def har_unzip(self, zipFile: str, harFile: str) -> None: params = locals_to_params(locals()) - await self._channel.send("harUnzip", params) + await self._channel.send("harUnzip", None, params) async def tracing_started(self, tracesDir: Optional[str], traceName: str) -> str: params = locals_to_params(locals()) - return await self._channel.send("tracingStarted", params) + return await self._channel.send("tracingStarted", None, params) async def trace_discarded(self, stacks_id: str) -> None: - return await self._channel.send("traceDiscarded", {"stacksId": stacks_id}) + return await self._channel.send("traceDiscarded", None, {"stacksId": stacks_id}) def add_stack_to_tracing_no_reply(self, id: int, frames: List[StackFrame]) -> None: self._channel.send_no_reply( "addStackToTracingNoReply", + None, { "callData": { "stack": frames, diff --git a/playwright/_impl/_locator.py b/playwright/_impl/_locator.py index 9d190c453..6fe075130 100644 --- a/playwright/_impl/_locator.py +++ b/playwright/_impl/_locator.py @@ -383,6 +383,7 @@ async def focus(self, timeout: float = None) -> None: async def blur(self, timeout: float = None) -> None: await self._frame._channel.send( "blur", + self._frame._timeout, { "selector": self._selector, "strict": True, @@ -549,6 +550,7 @@ async def screenshot( async def aria_snapshot(self, timeout: float = None) -> str: return await self._frame._channel.send( "ariaSnapshot", + self._frame._timeout, { "selector": self._selector, **locals_to_params(locals()), @@ -726,6 +728,7 @@ async def _expect( options["expectedValue"] = serialize_argument(options["expectedValue"]) result = await self._frame._channel.send_return_as_dict( "expect", + self._frame._timeout, { "selector": self._selector, "expression": expression, diff --git a/playwright/_impl/_network.py b/playwright/_impl/_network.py index 748967dd8..616c75ec9 100644 --- a/playwright/_impl/_network.py +++ b/playwright/_impl/_network.py @@ -192,7 +192,10 @@ async def sizes(self) -> RequestSizes: response = await self.response() if not response: raise Error("Unable to fetch sizes for failed request") - return await response._channel.send("sizes") + return await response._channel.send( + "sizes", + None, + ) @property def post_data(self) -> Optional[str]: @@ -226,7 +229,12 @@ def post_data_buffer(self) -> Optional[bytes]: return None async def response(self) -> Optional["Response"]: - return from_nullable_channel(await self._channel.send("response")) + return from_nullable_channel( + await self._channel.send( + "response", + None, + ) + ) @property def frame(self) -> "Frame": @@ -291,7 +299,9 @@ async def _actual_headers(self) -> "RawHeaders": return RawHeaders(serialize_headers(override)) if not self._all_headers_future: self._all_headers_future = asyncio.Future() - headers = await self._channel.send("rawRequestHeaders", is_internal=True) + headers = await self._channel.send( + "rawRequestHeaders", None, is_internal=True + ) self._all_headers_future.set_result(RawHeaders(headers)) return await self._all_headers_future @@ -348,6 +358,7 @@ async def abort(self, errorCode: str = None) -> None: lambda: self._race_with_page_close( self._channel.send( "abort", + None, { "errorCode": errorCode, }, @@ -433,7 +444,7 @@ async def _inner_fulfill( headers["content-length"] = str(length) params["headers"] = serialize_headers(headers) - await self._race_with_page_close(self._channel.send("fulfill", params)) + await self._race_with_page_close(self._channel.send("fulfill", None, params)) async def _handle_route(self, callback: Callable) -> None: self._check_not_handled() @@ -499,6 +510,7 @@ async def _inner_continue(self, is_fallback: bool = False) -> None: await self._race_with_page_close( self._channel.send( "continue", + None, { "url": options.url, "method": options.method, @@ -518,7 +530,7 @@ async def _inner_continue(self, is_fallback: bool = False) -> None: async def _redirected_navigation_request(self, url: str) -> None: await self._handle_route( lambda: self._race_with_page_close( - self._channel.send("redirectNavigationRequest", {"url": url}) + self._channel.send("redirectNavigationRequest", None, {"url": url}) ) ) @@ -577,6 +589,7 @@ def close(self, code: int = None, reason: str = None) -> None: self._ws._loop, self._ws._channel.send( "closeServer", + None, { "code": code, "reason": reason, @@ -590,7 +603,7 @@ def send(self, message: Union[str, bytes]) -> None: _create_task_and_ignore_exception( self._ws._loop, self._ws._channel.send( - "sendToServer", {"message": message, "isBase64": False} + "sendToServer", None, {"message": message, "isBase64": False} ), ) else: @@ -598,6 +611,7 @@ def send(self, message: Union[str, bytes]) -> None: self._ws._loop, self._ws._channel.send( "sendToServer", + None, {"message": base64.b64encode(message).decode(), "isBase64": True}, ), ) @@ -633,7 +647,7 @@ def _channel_message_from_page(self, event: Dict) -> None: ) elif self._connected: _create_task_and_ignore_exception( - self._loop, self._channel.send("sendToServer", event) + self._loop, self._channel.send("sendToServer", None, event) ) def _channel_message_from_server(self, event: Dict) -> None: @@ -645,7 +659,7 @@ def _channel_message_from_server(self, event: Dict) -> None: ) else: _create_task_and_ignore_exception( - self._loop, self._channel.send("sendToPage", event) + self._loop, self._channel.send("sendToPage", None, event) ) def _channel_close_page(self, event: Dict) -> None: @@ -653,7 +667,7 @@ def _channel_close_page(self, event: Dict) -> None: self._on_page_close(event["code"], event["reason"]) else: _create_task_and_ignore_exception( - self._loop, self._channel.send("closeServer", event) + self._loop, self._channel.send("closeServer", None, event) ) def _channel_close_server(self, event: Dict) -> None: @@ -661,7 +675,7 @@ def _channel_close_server(self, event: Dict) -> None: self._on_server_close(event["code"], event["reason"]) else: _create_task_and_ignore_exception( - self._loop, self._channel.send("closePage", event) + self._loop, self._channel.send("closePage", None, event) ) @property @@ -671,7 +685,7 @@ def url(self) -> str: async def close(self, code: int = None, reason: str = None) -> None: try: await self._channel.send( - "closePage", {"code": code, "reason": reason, "wasClean": True} + "closePage", None, {"code": code, "reason": reason, "wasClean": True} ) except Exception: pass @@ -680,7 +694,12 @@ def connect_to_server(self) -> "WebSocketRoute": if self._connected: raise Error("Already connected to the server") self._connected = True - asyncio.create_task(self._channel.send("connect")) + asyncio.create_task( + self._channel.send( + "connect", + None, + ) + ) return cast("WebSocketRoute", self._server) def send(self, message: Union[str, bytes]) -> None: @@ -688,7 +707,7 @@ def send(self, message: Union[str, bytes]) -> None: _create_task_and_ignore_exception( self._loop, self._channel.send( - "sendToPage", {"message": message, "isBase64": False} + "sendToPage", None, {"message": message, "isBase64": False} ), ) else: @@ -696,6 +715,7 @@ def send(self, message: Union[str, bytes]) -> None: self._loop, self._channel.send( "sendToPage", + None, { "message": base64.b64encode(message).decode(), "isBase64": True, @@ -713,7 +733,10 @@ async def _after_handle(self) -> None: if self._connected: return # Ensure that websocket is "open" and can send messages without an actual server connection. - await self._channel.send("ensureOpened") + await self._channel.send( + "ensureOpened", + None, + ) class WebSocketRouteHandler: @@ -826,15 +849,27 @@ async def header_values(self, name: str) -> List[str]: async def _actual_headers(self) -> "RawHeaders": if not self._raw_headers_future: self._raw_headers_future = asyncio.Future() - headers = cast(HeadersArray, await self._channel.send("rawResponseHeaders")) + headers = cast( + HeadersArray, + await self._channel.send( + "rawResponseHeaders", + None, + ), + ) self._raw_headers_future.set_result(RawHeaders(headers)) return await self._raw_headers_future async def server_addr(self) -> Optional[RemoteAddr]: - return await self._channel.send("serverAddr") + return await self._channel.send( + "serverAddr", + None, + ) async def security_details(self) -> Optional[SecurityDetails]: - return await self._channel.send("securityDetails") + return await self._channel.send( + "securityDetails", + None, + ) async def finished(self) -> None: async def on_finished() -> None: @@ -853,7 +888,10 @@ async def on_finished() -> None: await on_finished_task async def body(self) -> bytes: - binary = await self._channel.send("body") + binary = await self._channel.send( + "body", + None, + ) return base64.b64decode(binary) async def text(self) -> str: diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index 82b43a231..20bba35db 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -237,7 +237,6 @@ def __init__( self._channel.on( "worker", lambda params: self._on_worker(from_channel(params["worker"])) ) - self._channel._set_timeout_calculator(self._timeout_settings.timeout) self._closed_or_crashed_future: asyncio.Future = asyncio.Future() self.on( Page.Events.Close, @@ -520,12 +519,16 @@ async def expose_binding( ) self._bindings[name] = callback await self._channel.send( - "exposeBinding", dict(name=name, needsHandle=handle or False) + "exposeBinding", + None, + dict(name=name, needsHandle=handle or False), ) async def set_extra_http_headers(self, headers: Dict[str, str]) -> None: await self._channel.send( - "setExtraHTTPHeaders", dict(headers=serialize_headers(headers)) + "setExtraHTTPHeaders", + None, + dict(headers=serialize_headers(headers)), ) @property @@ -559,7 +562,9 @@ async def reload( ) -> Optional[Response]: return from_nullable_channel( await self._channel.send( - "reload", self._locals_to_params_with_navigation_timeout(locals()) + "reload", + self._timeout_settings.navigation_timeout, + locals_to_params(locals()), ) ) @@ -592,7 +597,9 @@ async def go_back( ) -> Optional[Response]: return from_nullable_channel( await self._channel.send( - "goBack", self._locals_to_params_with_navigation_timeout(locals()) + "goBack", + self._timeout_settings.navigation_timeout, + locals_to_params(locals()), ) ) @@ -603,12 +610,14 @@ async def go_forward( ) -> Optional[Response]: return from_nullable_channel( await self._channel.send( - "goForward", self._locals_to_params_with_navigation_timeout(locals()) + "goForward", + self._timeout_settings.navigation_timeout, + locals_to_params(locals()), ) ) async def request_gc(self) -> None: - await self._channel.send("requestGC") + await self._channel.send("requestGC", None) async def emulate_media( self, @@ -637,18 +646,22 @@ async def emulate_media( params["contrast"] = ( "no-override" if params["contrast"] == "null" else contrast ) - await self._channel.send("emulateMedia", params) + await self._channel.send("emulateMedia", None, params) async def set_viewport_size(self, viewportSize: ViewportSize) -> None: self._viewport_size = viewportSize - await self._channel.send("setViewportSize", locals_to_params(locals())) + await self._channel.send( + "setViewportSize", + None, + locals_to_params(locals()), + ) @property def viewport_size(self) -> Optional[ViewportSize]: return self._viewport_size async def bring_to_front(self) -> None: - await self._channel.send("bringToFront") + await self._channel.send("bringToFront", None) async def add_init_script( self, script: str = None, path: Union[str, Path] = None @@ -659,7 +672,7 @@ async def add_init_script( ) if not isinstance(script, str): raise Error("Either path or script parameter must be specified") - await self._channel.send("addInitScript", dict(source=script)) + await self._channel.send("addInitScript", None, dict(source=script)) async def route( self, url: URLMatch, handler: RouteHandlerCallback, times: int = None @@ -757,7 +770,9 @@ async def route_from_har( async def _update_interception_patterns(self) -> None: patterns = RouteHandler.prepare_interception_patterns(self._routes) await self._channel.send( - "setNetworkInterceptionPatterns", {"patterns": patterns} + "setNetworkInterceptionPatterns", + None, + {"patterns": patterns}, ) async def _update_web_socket_interception_patterns(self) -> None: @@ -765,7 +780,9 @@ async def _update_web_socket_interception_patterns(self) -> None: self._web_socket_routes ) await self._channel.send( - "setWebSocketInterceptionPatterns", {"patterns": patterns} + "setWebSocketInterceptionPatterns", + None, + {"patterns": patterns}, ) async def screenshot( @@ -799,7 +816,9 @@ async def screenshot( params["mask"], ) ) - encoded_binary = await self._channel.send("screenshot", params) + encoded_binary = await self._channel.send( + "screenshot", self._timeout_settings.timeout, params + ) decoded_binary = base64.b64decode(encoded_binary) if path: make_dirs_for_file(path) @@ -813,7 +832,7 @@ async def close(self, runBeforeUnload: bool = None, reason: str = None) -> None: self._close_reason = reason self._close_was_called = True try: - await self._channel.send("close", locals_to_params(locals())) + await self._channel.send("close", None, locals_to_params(locals())) if self._owned_context: await self._owned_context.close() except Exception as e: @@ -1112,7 +1131,9 @@ async def pause(self) -> None: try: await asyncio.wait( [ - asyncio.create_task(self._browser_context._channel.send("pause")), + asyncio.create_task( + self._browser_context._channel.send("pause", None) + ), self._closed_or_crashed_future, ], return_when=asyncio.FIRST_COMPLETED, @@ -1144,7 +1165,7 @@ async def pdf( params = locals_to_params(locals()) if "path" in params: del params["path"] - encoded_binary = await self._channel.send("pdf", params) + encoded_binary = await self._channel.send("pdf", None, params) decoded_binary = base64.b64decode(encoded_binary) if path: make_dirs_for_file(path) @@ -1354,6 +1375,7 @@ async def add_locator_handler( return uid = await self._channel.send( "registerLocatorHandler", + None, { "selector": locator._selector, "noWaitAfter": noWaitAfter, @@ -1394,7 +1416,9 @@ def _handler() -> None: try: await self._connection.wrap_api_call( lambda: self._channel.send( - "resolveLocatorHandlerNoReply", {"uid": uid, "remove": remove} + "resolveLocatorHandlerNoReply", + None, + {"uid": uid, "remove": remove}, ), is_internal=True, ) @@ -1405,14 +1429,11 @@ async def remove_locator_handler(self, locator: "Locator") -> None: for uid, data in self._locator_handlers.copy().items(): if data.locator._equals(locator): del self._locator_handlers[uid] - self._channel.send_no_reply("unregisterLocatorHandler", {"uid": uid}) - - def _locals_to_params_with_navigation_timeout(self, args: Dict) -> Dict: - params = locals_to_params(args) - params["timeout"] = self._timeout_settings.navigation_timeout( - params.get("timeout") - ) - return params + self._channel.send_no_reply( + "unregisterLocatorHandler", + None, + {"uid": uid}, + ) class Worker(ChannelOwner): @@ -1444,6 +1465,7 @@ async def evaluate(self, expression: str, arg: Serializable = None) -> Any: return parse_result( await self._channel.send( "evaluateExpression", + None, dict( expression=expression, arg=serialize_argument(arg), @@ -1457,6 +1479,7 @@ async def evaluate_handle( return from_channel( await self._channel.send( "evaluateExpressionHandle", + None, dict( expression=expression, arg=serialize_argument(arg), @@ -1482,12 +1505,14 @@ async def call(self, func: Callable) -> None: result = func(source, *func_args) if inspect.iscoroutine(result): result = await result - await self._channel.send("resolve", dict(result=serialize_argument(result))) + await self._channel.send( + "resolve", None, dict(result=serialize_argument(result)) + ) except Exception as e: tb = sys.exc_info()[2] asyncio.create_task( self._channel.send( - "reject", dict(error=dict(error=serialize_error(e, tb))) + "reject", None, dict(error=dict(error=serialize_error(e, tb))) ) ) diff --git a/playwright/_impl/_selectors.py b/playwright/_impl/_selectors.py index b0cf5afbb..2a2e70974 100644 --- a/playwright/_impl/_selectors.py +++ b/playwright/_impl/_selectors.py @@ -46,7 +46,9 @@ async def register( engine["contentScript"] = contentScript for context in self._contexts_for_selectors: await context._channel.send( - "registerSelectorEngine", {"selectorEngine": engine} + "registerSelectorEngine", + None, + {"selectorEngine": engine}, ) self._selector_engines.append(engine) @@ -55,5 +57,7 @@ def set_test_id_attribute(self, attributeName: str) -> None: self._test_id_attribute_name = attributeName for context in self._contexts_for_selectors: context._channel.send_no_reply( - "setTestIdAttributeName", {"testIdAttributeName": attributeName} + "setTestIdAttributeName", + None, + {"testIdAttributeName": attributeName}, ) diff --git a/playwright/_impl/_set_input_files_helpers.py b/playwright/_impl/_set_input_files_helpers.py index ababf5fab..f868886a3 100644 --- a/playwright/_impl/_set_input_files_helpers.py +++ b/playwright/_impl/_set_input_files_helpers.py @@ -84,6 +84,7 @@ async def convert_input_files( result = await context._connection.wrap_api_call( lambda: context._channel.send_return_as_dict( "createTempFiles", + None, { "rootDirName": ( os.path.basename(local_directory) diff --git a/playwright/_impl/_stream.py b/playwright/_impl/_stream.py index d27427589..04afa48e1 100644 --- a/playwright/_impl/_stream.py +++ b/playwright/_impl/_stream.py @@ -28,7 +28,7 @@ def __init__( async def save_as(self, path: Union[str, Path]) -> None: file = await self._loop.run_in_executor(None, lambda: open(path, "wb")) while True: - binary = await self._channel.send("read", {"size": 1024 * 1024}) + binary = await self._channel.send("read", None, {"size": 1024 * 1024}) if not binary: break await self._loop.run_in_executor( @@ -39,7 +39,7 @@ async def save_as(self, path: Union[str, Path]) -> None: async def read_all(self) -> bytes: binary = b"" while True: - chunk = await self._channel.send("read", {"size": 1024 * 1024}) + chunk = await self._channel.send("read", None, {"size": 1024 * 1024}) if not chunk: break binary += base64.b64decode(chunk) diff --git a/playwright/_impl/_tracing.py b/playwright/_impl/_tracing.py index e984bcbad..bbc6ec35e 100644 --- a/playwright/_impl/_tracing.py +++ b/playwright/_impl/_tracing.py @@ -42,15 +42,15 @@ async def start( params = locals_to_params(locals()) self._include_sources = bool(sources) - await self._channel.send("tracingStart", params) + await self._channel.send("tracingStart", None, params) trace_name = await self._channel.send( - "tracingStartChunk", {"title": title, "name": name} + "tracingStartChunk", None, {"title": title, "name": name} ) await self._start_collecting_stacks(trace_name) async def start_chunk(self, title: str = None, name: str = None) -> None: params = locals_to_params(locals()) - trace_name = await self._channel.send("tracingStartChunk", params) + trace_name = await self._channel.send("tracingStartChunk", None, params) await self._start_collecting_stacks(trace_name) async def _start_collecting_stacks(self, trace_name: str) -> None: @@ -66,14 +66,17 @@ async def stop_chunk(self, path: Union[pathlib.Path, str] = None) -> None: async def stop(self, path: Union[pathlib.Path, str] = None) -> None: await self._do_stop_chunk(path) - await self._channel.send("tracingStop") + await self._channel.send( + "tracingStop", + None, + ) async def _do_stop_chunk(self, file_path: Union[pathlib.Path, str] = None) -> None: self._reset_stack_counter() if not file_path: # Not interested in any artifacts - await self._channel.send("tracingStopChunk", {"mode": "discard"}) + await self._channel.send("tracingStopChunk", None, {"mode": "discard"}) if self._stacks_id: await self._connection.local_utils.trace_discarded(self._stacks_id) return @@ -82,7 +85,7 @@ async def _do_stop_chunk(self, file_path: Union[pathlib.Path, str] = None) -> No if is_local: result = await self._channel.send_return_as_dict( - "tracingStopChunk", {"mode": "entries"} + "tracingStopChunk", None, {"mode": "entries"} ) await self._connection.local_utils.zip( { @@ -97,6 +100,7 @@ async def _do_stop_chunk(self, file_path: Union[pathlib.Path, str] = None) -> No result = await self._channel.send_return_as_dict( "tracingStopChunk", + None, { "mode": "archive", }, @@ -133,7 +137,10 @@ def _reset_stack_counter(self) -> None: self._connection.set_is_tracing(False) async def group(self, name: str, location: TracingGroupLocation = None) -> None: - await self._channel.send("tracingGroup", locals_to_params(locals())) + await self._channel.send("tracingGroup", None, locals_to_params(locals())) async def group_end(self) -> None: - await self._channel.send("tracingGroupEnd") + await self._channel.send( + "tracingGroupEnd", + None, + ) diff --git a/playwright/_impl/_waiter.py b/playwright/_impl/_waiter.py index 7b0ad2cc6..f7ff4b6c1 100644 --- a/playwright/_impl/_waiter.py +++ b/playwright/_impl/_waiter.py @@ -38,6 +38,7 @@ def __init__(self, channel_owner: ChannelOwner, event: str) -> None: def _wait_for_event_info_before(self, wait_id: str, event: str) -> None: self._channel.send_no_reply( "waitForEventInfo", + None, { "info": { "waitId": wait_id, @@ -51,6 +52,7 @@ def _wait_for_event_info_after(self, wait_id: str, error: Exception = None) -> N self._channel._connection.wrap_api_call_sync( lambda: self._channel.send_no_reply( "waitForEventInfo", + None, { "info": { "waitId": wait_id, @@ -130,6 +132,7 @@ def log(self, message: str) -> None: self._channel._connection.wrap_api_call_sync( lambda: self._channel.send_no_reply( "waitForEventInfo", + None, { "info": { "waitId": self._wait_id, diff --git a/playwright/_impl/_writable_stream.py b/playwright/_impl/_writable_stream.py index 702adf153..7d5b7704b 100644 --- a/playwright/_impl/_writable_stream.py +++ b/playwright/_impl/_writable_stream.py @@ -37,6 +37,6 @@ async def copy(self, path: Union[str, Path]) -> None: if not data: break await self._channel.send( - "write", {"binary": base64.b64encode(data).decode()} + "write", None, {"binary": base64.b64encode(data).decode()} ) - await self._channel.send("close") + await self._channel.send("close", None) From b7882d99feae7d5b31bb83c814c55abf2090461d Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 26 Jun 2025 06:56:03 -0700 Subject: [PATCH 10/79] fix(api): ignore deprecated timeout arg provided to is_visible/hidden (#2905) --- playwright/_impl/_locator.py | 6 ++---- playwright/_impl/_page.py | 6 ++++-- tests/async/test_locators.py | 9 +++++++++ tests/async/test_page.py | 8 ++++++++ tests/sync/test_locators.py | 9 +++++++++ tests/sync/test_page.py | 8 ++++++++ 6 files changed, 40 insertions(+), 6 deletions(-) diff --git a/playwright/_impl/_locator.py b/playwright/_impl/_locator.py index 6fe075130..a1ea180ed 100644 --- a/playwright/_impl/_locator.py +++ b/playwright/_impl/_locator.py @@ -501,19 +501,17 @@ async def is_enabled(self, timeout: float = None) -> bool: ) async def is_hidden(self, timeout: float = None) -> bool: - params = locals_to_params(locals()) + # timeout is deprecated and does nothing return await self._frame.is_hidden( self._selector, strict=True, - **params, ) async def is_visible(self, timeout: float = None) -> bool: - params = locals_to_params(locals()) + # timeout is deprecated and does nothing return await self._frame.is_visible( self._selector, strict=True, - **params, ) async def press( diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index 20bba35db..55ee44df2 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -447,12 +447,14 @@ async def is_enabled( async def is_hidden( self, selector: str, strict: bool = None, timeout: float = None ) -> bool: - return await self._main_frame.is_hidden(**locals_to_params(locals())) + # timeout is deprecated and does nothing + return await self._main_frame.is_hidden(selector=selector, strict=strict) async def is_visible( self, selector: str, strict: bool = None, timeout: float = None ) -> bool: - return await self._main_frame.is_visible(**locals_to_params(locals())) + # timeout is deprecated and does nothing + return await self._main_frame.is_visible(selector=selector, strict=strict) async def dispatch_event( self, diff --git a/tests/async/test_locators.py b/tests/async/test_locators.py index a5891f558..980de041f 100644 --- a/tests/async/test_locators.py +++ b/tests/async/test_locators.py @@ -1143,3 +1143,12 @@ async def test_locator_click_timeout_error_should_contain_call_log(page: Page) - "During handling of the above exception, another exception occurred" not in formatted_exception ) + + +async def test_locator_should_ignore_deprecated_is_hidden_and_visible_timeout( + page: Page, +) -> None: + await page.set_content("
foo
") + div = page.locator("div") + assert await div.is_hidden(timeout=10) is False + assert await div.is_visible(timeout=10) is True diff --git a/tests/async/test_page.py b/tests/async/test_page.py index 962a11e59..03907c4b9 100644 --- a/tests/async/test_page.py +++ b/tests/async/test_page.py @@ -1451,3 +1451,11 @@ async def test_page_pause_should_reset_custom_timeouts( server.set_route("/empty.html", lambda route: None) with pytest.raises(Error, match="Timeout 456ms exceeded."): await page.goto(server.EMPTY_PAGE) + + +async def test_page_should_ignore_deprecated_is_hidden_and_visible_timeout( + page: Page, +) -> None: + await page.set_content("
foo
") + assert await page.is_hidden("div", timeout=10) is False + assert await page.is_visible("div", timeout=10) is True diff --git a/tests/sync/test_locators.py b/tests/sync/test_locators.py index 31d7b174b..b554f0544 100644 --- a/tests/sync/test_locators.py +++ b/tests/sync/test_locators.py @@ -997,3 +997,12 @@ def test_locator_click_timeout_error_should_contain_call_log(page: Page) -> None "During handling of the above exception, another exception occurred" not in formatted_exception ) + + +def test_locator_should_ignore_deprecated_is_hidden_and_visible_timeout( + page: Page, +) -> None: + page.set_content("
foo
") + div = page.locator("div") + assert div.is_hidden(timeout=10) is False + assert div.is_visible(timeout=10) is True diff --git a/tests/sync/test_page.py b/tests/sync/test_page.py index 7550a80d1..e29c7cabc 100644 --- a/tests/sync/test_page.py +++ b/tests/sync/test_page.py @@ -114,3 +114,11 @@ def test_page_pause_should_reset_custom_timeouts( server.set_route("/empty.html", lambda route: None) with pytest.raises(Error, match="Timeout 456ms exceeded."): page.goto(server.EMPTY_PAGE) + + +def test_page_should_ignore_deprecated_is_hidden_and_visible_timeout( + page: Page, +) -> None: + page.set_content("
foo
") + assert page.is_hidden("div", timeout=10) is False + assert page.is_visible("div", timeout=10) is True From 6157d89ed543719a7e149f1debf0d0fd1de73a1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 11:06:59 +0200 Subject: [PATCH 11/79] build(deps): bump typing-extensions from 4.13.2 to 4.14.1 (#2912) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a6b31fd67..14dc0500a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,5 @@ greenlet==3.2.3 # via playwright (pyproject.toml) pyee==13.0.0 # via playwright (pyproject.toml) -typing-extensions==4.13.2 +typing-extensions==4.14.1 # via pyee From 8cd92566bc6724d54216c4aa333546c3906c81b4 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Thu, 10 Jul 2025 13:11:08 +0200 Subject: [PATCH 12/79] fix: Make context manager __exit__/__aexit__ signatures compatible with typing protocols (#2915) --- playwright/_impl/_async_base.py | 6 +++--- playwright/_impl/_sync_base.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/playwright/_impl/_async_base.py b/playwright/_impl/_async_base.py index b06994a65..db7e5d005 100644 --- a/playwright/_impl/_async_base.py +++ b/playwright/_impl/_async_base.py @@ -96,9 +96,9 @@ async def __aenter__(self: Self) -> Self: async def __aexit__( self, - exc_type: Type[BaseException], - exc_val: BaseException, - traceback: TracebackType, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + traceback: Optional[TracebackType], ) -> None: await self.close() diff --git a/playwright/_impl/_sync_base.py b/playwright/_impl/_sync_base.py index e6fac9750..3fef433b5 100644 --- a/playwright/_impl/_sync_base.py +++ b/playwright/_impl/_sync_base.py @@ -142,9 +142,9 @@ def __enter__(self: Self) -> Self: def __exit__( self, - exc_type: Type[BaseException], - exc_val: BaseException, - _traceback: TracebackType, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + _traceback: Optional[TracebackType], ) -> None: self.close() From 3f957528af49f6c5314b0fdb5317e15eeb87bb5e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Jul 2025 15:07:35 +0200 Subject: [PATCH 13/79] build(deps): bump pytest from 8.4.0 to 8.4.1 (#2897) 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 afe7e4bb8..e19488956 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -8,7 +8,7 @@ Pillow==11.2.1 pixelmatch==0.3.0 pre-commit==3.5.0 pyOpenSSL==25.1.0 -pytest==8.4.0 +pytest==8.4.1 pytest-asyncio==1.0.0 pytest-cov==6.2.1 pytest-repeat==0.9.4 From 6af4fb2ac5951576b8a47ce553aacf84f2ef5561 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Fri, 11 Jul 2025 08:06:15 -0700 Subject: [PATCH 14/79] chore: roll to 1.54.0 (#2913) --- README.md | 6 +-- playwright/_impl/_api_structures.py | 15 +++++- playwright/_impl/_assertions.py | 26 ++++++++- playwright/_impl/_browser_context.py | 11 ++-- playwright/_impl/_console_message.py | 23 +++++++- playwright/_impl/_frame.py | 52 +++++++++++++++--- playwright/_impl/_helper.py | 10 ++++ playwright/_impl/_locator.py | 21 ++------ playwright/_impl/_network.py | 11 ++-- playwright/_impl/_page.py | 31 +++++------ playwright/_impl/_selectors.py | 4 ++ playwright/async_api/__init__.py | 2 + playwright/async_api/_generated.py | 37 +++++++++---- playwright/sync_api/__init__.py | 2 + playwright/sync_api/_generated.py | 37 +++++++++---- setup.py | 2 +- tests/async/conftest.py | 2 +- tests/async/test_browsercontext.py | 4 +- tests/async/test_chromium_tracing.py | 18 +++++-- tests/async/test_fetch_global.py | 2 +- tests/async/test_geolocation.py | 4 +- tests/async/test_page_add_locator_handler.py | 2 +- tests/async/test_page_request_intercept.py | 2 +- tests/async/test_page_route.py | 12 +++++ tests/async/test_selectors_misc.py | 28 ++++++++++ tests/async/test_tracing.py | 2 + tests/async/test_unroute_behavior.py | 56 ++++++++++++++++++++ tests/sync/conftest.py | 2 +- tests/sync/test_fetch_global.py | 2 +- tests/sync/test_page_add_locator_handler.py | 2 +- tests/sync/test_tracing.py | 2 + 31 files changed, 338 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index 9577b82e8..fa9e246a9 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 138.0.7204.23 | ✅ | ✅ | ✅ | -| WebKit 18.5 | ✅ | ✅ | ✅ | -| Firefox 139.0 | ✅ | ✅ | ✅ | +| Chromium 139.0.7258.5 | ✅ | ✅ | ✅ | +| WebKit 26.0 | ✅ | ✅ | ✅ | +| Firefox 140.0.2 | ✅ | ✅ | ✅ | ## Documentation diff --git a/playwright/_impl/_api_structures.py b/playwright/_impl/_api_structures.py index 3b639486a..0afa0d02e 100644 --- a/playwright/_impl/_api_structures.py +++ b/playwright/_impl/_api_structures.py @@ -32,6 +32,18 @@ class Cookie(TypedDict, total=False): httpOnly: bool secure: bool sameSite: Literal["Lax", "None", "Strict"] + partitionKey: Optional[str] + + +class StorageStateCookie(TypedDict, total=False): + name: str + value: str + domain: str + path: str + expires: float + httpOnly: bool + secure: bool + sameSite: Literal["Lax", "None", "Strict"] # TODO: We are waiting for PEP705 so SetCookieParam can be readonly and matches Cookie. @@ -45,6 +57,7 @@ class SetCookieParam(TypedDict, total=False): httpOnly: Optional[bool] secure: Optional[bool] sameSite: Optional[Literal["Lax", "None", "Strict"]] + partitionKey: Optional[str] class FloatRect(TypedDict): @@ -97,7 +110,7 @@ class ProxySettings(TypedDict, total=False): class StorageState(TypedDict, total=False): - cookies: List[Cookie] + cookies: List[StorageStateCookie] origins: List[OriginState] diff --git a/playwright/_impl/_assertions.py b/playwright/_impl/_assertions.py index 6e0161b7c..3aadbf5fe 100644 --- a/playwright/_impl/_assertions.py +++ b/playwright/_impl/_assertions.py @@ -20,6 +20,7 @@ AriaRole, ExpectedTextValue, FrameExpectOptions, + FrameExpectResult, ) from playwright._impl._connection import format_call_log from playwright._impl._errors import Error @@ -45,6 +46,13 @@ def __init__( self._is_not = is_not self._custom_message = message + async def _call_expect( + self, expression: str, expect_options: FrameExpectOptions, title: Optional[str] + ) -> FrameExpectResult: + raise NotImplementedError( + "_call_expect must be implemented in a derived class." + ) + async def _expect_impl( self, expression: str, @@ -61,7 +69,7 @@ async def _expect_impl( message = message.replace("expected to", "expected not to") if "useInnerText" in expect_options and expect_options["useInnerText"] is None: del expect_options["useInnerText"] - result = await self._actual_locator._expect(expression, expect_options, title) + result = await self._call_expect(expression, expect_options, title) if result["matches"] == self._is_not: actual = result.get("received") if self._custom_message: @@ -88,6 +96,14 @@ def __init__( super().__init__(page.locator(":root"), timeout, is_not, message) self._actual_page = page + async def _call_expect( + self, expression: str, expect_options: FrameExpectOptions, title: Optional[str] + ) -> FrameExpectResult: + __tracebackhide__ = True + return await self._actual_page.main_frame._expect( + None, expression, expect_options, title + ) + @property def _not(self) -> "PageAssertions": return PageAssertions( @@ -122,7 +138,7 @@ async def to_have_url( ignoreCase: bool = None, ) -> None: __tracebackhide__ = True - base_url = self._actual_page.context._options.get("baseURL") + base_url = self._actual_page.context._base_url if isinstance(urlOrRegExp, str) and base_url: urlOrRegExp = urljoin(base_url, urlOrRegExp) expected_text = to_expected_text_values([urlOrRegExp], ignoreCase=ignoreCase) @@ -155,6 +171,12 @@ def __init__( super().__init__(locator, timeout, is_not, message) self._actual_locator = locator + async def _call_expect( + self, expression: str, expect_options: FrameExpectOptions, title: Optional[str] + ) -> FrameExpectResult: + __tracebackhide__ = True + return await self._actual_locator._expect(expression, expect_options, title) + @property def _not(self) -> "LocatorAssertions": return LocatorAssertions( diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py index 60b60c46e..391e61ec6 100644 --- a/playwright/_impl/_browser_context.py +++ b/playwright/_impl/_browser_context.py @@ -119,6 +119,8 @@ def __init__( 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") self._tracing = cast(Tracing, from_channel(initializer["tracing"])) self._har_recorders: Dict[str, HarRecordingMetadata] = {} self._request: APIRequestContext = from_channel(initializer["requestContext"]) @@ -424,7 +426,7 @@ async def route( self._routes.insert( 0, RouteHandler( - self._options.get("baseURL"), + self._base_url, url, handler, True if self._dispatcher_fiber else False, @@ -452,17 +454,16 @@ async def _unroute_internal( behavior: Literal["default", "ignoreErrors", "wait"] = None, ) -> None: self._routes = remaining + if behavior is not None and behavior != "default": + await asyncio.gather(*map(lambda router: router.stop(behavior), removed)) # type: ignore await self._update_interception_patterns() - if behavior is None or behavior == "default": - return - await asyncio.gather(*map(lambda router: router.stop(behavior), removed)) # type: ignore async def route_web_socket( self, url: URLMatch, handler: WebSocketRouteHandlerCallback ) -> None: self._web_socket_routes.insert( 0, - WebSocketRouteHandler(self._options.get("baseURL"), url, handler), + WebSocketRouteHandler(self._base_url, url, handler), ) await self._update_web_socket_interception_patterns() diff --git a/playwright/_impl/_console_message.py b/playwright/_impl/_console_message.py index ba8fc0a38..53c0dee95 100644 --- a/playwright/_impl/_console_message.py +++ b/playwright/_impl/_console_message.py @@ -13,7 +13,7 @@ # limitations under the License. from asyncio import AbstractEventLoop -from typing import TYPE_CHECKING, Any, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union from playwright._impl._api_structures import SourceLocation from playwright._impl._connection import from_channel, from_nullable_channel @@ -39,7 +39,26 @@ def __str__(self) -> str: return self.text @property - def type(self) -> str: + def type(self) -> Union[ + Literal["assert"], + Literal["clear"], + Literal["count"], + Literal["debug"], + Literal["dir"], + Literal["dirxml"], + Literal["endGroup"], + Literal["error"], + Literal["info"], + Literal["log"], + Literal["profile"], + Literal["profileEnd"], + Literal["startGroup"], + Literal["startGroupCollapsed"], + Literal["table"], + Literal["timeEnd"], + Literal["trace"], + Literal["warning"], + ]: return self._event["type"] @property diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py index c0646b680..fe19a576d 100644 --- a/playwright/_impl/_frame.py +++ b/playwright/_impl/_frame.py @@ -30,7 +30,13 @@ from pyee import EventEmitter -from playwright._impl._api_structures import AriaRole, FilePayload, Position +from playwright._impl._api_structures import ( + AriaRole, + FilePayload, + FrameExpectOptions, + FrameExpectResult, + Position, +) from playwright._impl._connection import ( ChannelOwner, from_channel, @@ -56,6 +62,7 @@ Serializable, add_source_url_to_script, parse_result, + parse_value, serialize_argument, ) from playwright._impl._locator import ( @@ -170,6 +177,29 @@ def _setup_navigation_waiter(self, wait_name: str, timeout: float = None) -> Wai waiter.reject_on_timeout(timeout, f"Timeout {timeout}ms exceeded.") return waiter + async def _expect( + self, + selector: Optional[str], + expression: str, + options: FrameExpectOptions, + title: str = None, + ) -> FrameExpectResult: + if "expectedValue" in options: + options["expectedValue"] = serialize_argument(options["expectedValue"]) + result = await self._channel.send_return_as_dict( + "expect", + self._timeout, + { + "selector": selector, + "expression": expression, + **options, + }, + title=title, + ) + if result.get("received"): + result["received"] = parse_value(result["received"]) + return result + def expect_navigation( self, url: URLMatch = None, @@ -194,7 +224,7 @@ def predicate(event: Any) -> bool: return True waiter.log(f' navigated to "{event["url"]}"') return url_matches( - cast("Page", self._page)._browser_context._options.get("baseURL"), + cast("Page", self._page)._browser_context._base_url, event["url"], url, ) @@ -227,9 +257,7 @@ async def wait_for_url( timeout: float = None, ) -> None: assert self._page - if url_matches( - self._page._browser_context._options.get("baseURL"), self.url, url - ): + if url_matches(self._page._browser_context._base_url, self.url, url): await self._wait_for_load_state_impl(state=waitUntil, timeout=timeout) return async with self.expect_navigation( @@ -558,6 +586,18 @@ async def fill( noWaitAfter: bool = None, strict: bool = None, force: bool = None, + ) -> None: + await self._fill(**locals_to_params(locals())) + + async def _fill( + self, + selector: str, + value: str, + timeout: float = None, + noWaitAfter: bool = None, + strict: bool = None, + force: bool = None, + title: str = None, ) -> None: await self._channel.send("fill", self._timeout, locals_to_params(locals())) @@ -801,7 +841,7 @@ async def uncheck( await self._channel.send("uncheck", self._timeout, locals_to_params(locals())) async def wait_for_timeout(self, timeout: float) -> None: - await self._channel.send("waitForTimeout", None, locals_to_params(locals())) + await self._channel.send("waitForTimeout", None, {"waitTimeout": timeout}) async def wait_for_function( self, diff --git a/playwright/_impl/_helper.py b/playwright/_impl/_helper.py index 67a096dc5..66e59c65f 100644 --- a/playwright/_impl/_helper.py +++ b/playwright/_impl/_helper.py @@ -189,6 +189,16 @@ def map_token(original: str, replacement: str) -> str: # Escaped `\\?` behaves the same as `?` in our glob patterns. match = match.replace(r"\\?", "?") + # Special case about: URLs as they are not relative to base_url + if ( + match.startswith("about:") + or match.startswith("data:") + or match.startswith("chrome:") + or match.startswith("edge:") + or match.startswith("file:") + ): + # about: and data: URLs are not relative to base_url, so we return them as is. + return match # Glob symbols may be escaped in the URL and some of them such as ? affect resolution, # so we replace them with safe components first. processed_parts = [] diff --git a/playwright/_impl/_locator.py b/playwright/_impl/_locator.py index a1ea180ed..a65b68266 100644 --- a/playwright/_impl/_locator.py +++ b/playwright/_impl/_locator.py @@ -47,7 +47,7 @@ monotonic_time, to_impl, ) -from playwright._impl._js_handle import Serializable, parse_value, serialize_argument +from playwright._impl._js_handle import Serializable from playwright._impl._str_utils import ( escape_for_attribute_selector, escape_for_text_selector, @@ -217,7 +217,8 @@ async def clear( noWaitAfter: bool = None, force: bool = None, ) -> None: - await self.fill("", timeout=timeout, force=force) + params = locals_to_params(locals()) + await self._frame._fill(self._selector, value="", title="Clear", **params) def locator( self, @@ -722,21 +723,7 @@ async def _expect( options: FrameExpectOptions, title: str = None, ) -> FrameExpectResult: - if "expectedValue" in options: - options["expectedValue"] = serialize_argument(options["expectedValue"]) - result = await self._frame._channel.send_return_as_dict( - "expect", - self._frame._timeout, - { - "selector": self._selector, - "expression": expression, - **options, - }, - title=title, - ) - if result.get("received"): - result["received"] = parse_value(result["received"]) - return result + return await self._frame._expect(self._selector, expression, options, title) async def highlight(self) -> None: await self._frame._highlight(self._selector) diff --git a/playwright/_impl/_network.py b/playwright/_impl/_network.py index 616c75ec9..a999ce73c 100644 --- a/playwright/_impl/_network.py +++ b/playwright/_impl/_network.py @@ -733,10 +733,13 @@ async def _after_handle(self) -> None: if self._connected: return # Ensure that websocket is "open" and can send messages without an actual server connection. - await self._channel.send( - "ensureOpened", - None, - ) + try: + await self._channel.send( + "ensureOpened", + None, + ) + except Exception: + pass class WebSocketRouteHandler: diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index 55ee44df2..a0fa4eec2 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -388,9 +388,7 @@ def frame(self, name: str = None, url: URLMatch = None) -> Optional[Frame]: for frame in self._frames: if name and frame.name == name: return frame - if url and url_matches( - self._browser_context._options.get("baseURL"), frame.url, url - ): + if url and url_matches(self._browser_context._base_url, frame.url, url): return frame return None @@ -682,7 +680,7 @@ async def route( self._routes.insert( 0, RouteHandler( - self._browser_context._options.get("baseURL"), + self._browser_context._base_url, url, handler, True if self._dispatcher_fiber else False, @@ -710,24 +708,21 @@ async def _unroute_internal( behavior: Literal["default", "ignoreErrors", "wait"] = None, ) -> None: self._routes = remaining - await self._update_interception_patterns() - if behavior is None or behavior == "default": - return - await asyncio.gather( - *map( - lambda route: route.stop(behavior), # type: ignore - removed, + if behavior is not None and behavior != "default": + await asyncio.gather( + *map( + lambda route: route.stop(behavior), # type: ignore + removed, + ) ) - ) + await self._update_interception_patterns() async def route_web_socket( self, url: URLMatch, handler: WebSocketRouteHandlerCallback ) -> None: self._web_socket_routes.insert( 0, - WebSocketRouteHandler( - self._browser_context._options.get("baseURL"), url, handler - ), + WebSocketRouteHandler(self._browser_context._base_url, url, handler), ) await self._update_web_socket_interception_patterns() @@ -1186,7 +1181,7 @@ def video( # Note: we are creating Video object lazily, because we do not know # BrowserContextOptions when constructing the page - it is assigned # too late during launchPersistentContext. - if not self._browser_context._options.get("recordVideo"): + if not self._browser_context._videos_dir: return None return self._force_video() @@ -1273,7 +1268,7 @@ def expect_request( def my_predicate(request: Request) -> bool: if not callable(urlOrPredicate): return url_matches( - self._browser_context._options.get("baseURL"), + self._browser_context._base_url, request.url, urlOrPredicate, ) @@ -1305,7 +1300,7 @@ def expect_response( def my_predicate(request: Response) -> bool: if not callable(urlOrPredicate): return url_matches( - self._browser_context._options.get("baseURL"), + self._browser_context._base_url, request.url, urlOrPredicate, ) diff --git a/playwright/_impl/_selectors.py b/playwright/_impl/_selectors.py index 2a2e70974..c3bac78e5 100644 --- a/playwright/_impl/_selectors.py +++ b/playwright/_impl/_selectors.py @@ -37,6 +37,10 @@ async def register( path: Union[str, Path] = None, contentScript: bool = None, ) -> None: + if any(engine for engine in self._selector_engines if engine["name"] == name): + raise Error( + f'Selectors.register: "{name}" selector engine has been already registered' + ) if not script and not path: raise Error("Either source or path should be specified") if path: diff --git a/playwright/async_api/__init__.py b/playwright/async_api/__init__.py index be918f53c..257ac2022 100644 --- a/playwright/async_api/__init__.py +++ b/playwright/async_api/__init__.py @@ -79,6 +79,7 @@ ResourceTiming = playwright._impl._api_structures.ResourceTiming SourceLocation = playwright._impl._api_structures.SourceLocation StorageState = playwright._impl._api_structures.StorageState +StorageStateCookie = playwright._impl._api_structures.StorageStateCookie ViewportSize = playwright._impl._api_structures.ViewportSize Error = playwright._impl._errors.Error @@ -187,6 +188,7 @@ def __call__( "Selectors", "SourceLocation", "StorageState", + "StorageStateCookie", "TimeoutError", "Touchscreen", "Video", diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index 5f0af8bf0..bedf233de 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -6975,16 +6975,33 @@ async def set_system_time( class ConsoleMessage(AsyncBase): @property - def type(self) -> str: + def type( + self, + ) -> typing.Union[ + Literal["assert"], + Literal["clear"], + Literal["count"], + Literal["debug"], + Literal["dir"], + Literal["dirxml"], + Literal["endGroup"], + Literal["error"], + Literal["info"], + Literal["log"], + Literal["profile"], + Literal["profileEnd"], + Literal["startGroup"], + Literal["startGroupCollapsed"], + Literal["table"], + Literal["timeEnd"], + Literal["trace"], + Literal["warning"], + ]: """ConsoleMessage.type - One of the following values: `'log'`, `'debug'`, `'info'`, `'error'`, `'warning'`, `'dir'`, `'dirxml'`, `'table'`, - `'trace'`, `'clear'`, `'startGroup'`, `'startGroupCollapsed'`, `'endGroup'`, `'assert'`, `'profile'`, - `'profileEnd'`, `'count'`, `'timeEnd'`. - Returns ------- - str + Union["assert", "clear", "count", "debug", "dir", "dirxml", "endGroup", "error", "info", "log", "profile", "profileEnd", "startGroup", "startGroupCollapsed", "table", "timeEnd", "trace", "warning"] """ return mapping.from_maybe_impl(self._impl_obj.type) @@ -12649,7 +12666,8 @@ def pages(self) -> typing.List["Page"]: def browser(self) -> typing.Optional["Browser"]: """BrowserContext.browser - Returns the browser instance of the context. If it was launched as a persistent context null gets returned. + Gets the browser instance that owns the context. Returns `null` if the context is created outside of normal + browser, e.g. Android or Electron. Returns ------- @@ -12789,7 +12807,7 @@ async def cookies( Returns ------- - List[{name: str, value: str, domain: str, path: str, expires: float, httpOnly: bool, secure: bool, sameSite: Union["Lax", "None", "Strict"]}] + List[{name: str, value: str, domain: str, path: str, expires: float, httpOnly: bool, secure: bool, sameSite: Union["Lax", "None", "Strict"], partitionKey: Union[str, None]}] """ return mapping.from_impl_list( @@ -12810,7 +12828,7 @@ async def add_cookies(self, cookies: typing.Sequence[SetCookieParam]) -> None: Parameters ---------- - cookies : Sequence[{name: str, value: str, url: Union[str, None], domain: Union[str, None], path: Union[str, None], expires: Union[float, None], httpOnly: Union[bool, None], secure: Union[bool, None], sameSite: Union["Lax", "None", "Strict", None]}] + cookies : Sequence[{name: str, value: str, url: Union[str, None], domain: Union[str, None], path: Union[str, None], expires: Union[float, None], httpOnly: Union[bool, None], secure: Union[bool, None], sameSite: Union["Lax", "None", "Strict", None], partitionKey: Union[str, None]}] """ return mapping.from_maybe_impl( @@ -12884,6 +12902,7 @@ async def grant_permissions( - `'notifications'` - `'payment-handler'` - `'storage-access'` + - `'local-fonts'` origin : Union[str, None] The [origin] to grant permissions to, e.g. "https://example.com". """ diff --git a/playwright/sync_api/__init__.py b/playwright/sync_api/__init__.py index 136433982..e901cadbf 100644 --- a/playwright/sync_api/__init__.py +++ b/playwright/sync_api/__init__.py @@ -79,6 +79,7 @@ ResourceTiming = playwright._impl._api_structures.ResourceTiming SourceLocation = playwright._impl._api_structures.SourceLocation StorageState = playwright._impl._api_structures.StorageState +StorageStateCookie = playwright._impl._api_structures.StorageStateCookie ViewportSize = playwright._impl._api_structures.ViewportSize Error = playwright._impl._errors.Error @@ -186,6 +187,7 @@ def __call__( "Selectors", "SourceLocation", "StorageState", + "StorageStateCookie", "sync_playwright", "TimeoutError", "Touchscreen", diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py index 763df6de3..8f4b60764 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -7083,16 +7083,33 @@ def set_system_time( class ConsoleMessage(SyncBase): @property - def type(self) -> str: + def type( + self, + ) -> typing.Union[ + Literal["assert"], + Literal["clear"], + Literal["count"], + Literal["debug"], + Literal["dir"], + Literal["dirxml"], + Literal["endGroup"], + Literal["error"], + Literal["info"], + Literal["log"], + Literal["profile"], + Literal["profileEnd"], + Literal["startGroup"], + Literal["startGroupCollapsed"], + Literal["table"], + Literal["timeEnd"], + Literal["trace"], + Literal["warning"], + ]: """ConsoleMessage.type - One of the following values: `'log'`, `'debug'`, `'info'`, `'error'`, `'warning'`, `'dir'`, `'dirxml'`, `'table'`, - `'trace'`, `'clear'`, `'startGroup'`, `'startGroupCollapsed'`, `'endGroup'`, `'assert'`, `'profile'`, - `'profileEnd'`, `'count'`, `'timeEnd'`. - Returns ------- - str + Union["assert", "clear", "count", "debug", "dir", "dirxml", "endGroup", "error", "info", "log", "profile", "profileEnd", "startGroup", "startGroupCollapsed", "table", "timeEnd", "trace", "warning"] """ return mapping.from_maybe_impl(self._impl_obj.type) @@ -12671,7 +12688,8 @@ def pages(self) -> typing.List["Page"]: def browser(self) -> typing.Optional["Browser"]: """BrowserContext.browser - Returns the browser instance of the context. If it was launched as a persistent context null gets returned. + Gets the browser instance that owns the context. Returns `null` if the context is created outside of normal + browser, e.g. Android or Electron. Returns ------- @@ -12811,7 +12829,7 @@ def cookies( Returns ------- - List[{name: str, value: str, domain: str, path: str, expires: float, httpOnly: bool, secure: bool, sameSite: Union["Lax", "None", "Strict"]}] + List[{name: str, value: str, domain: str, path: str, expires: float, httpOnly: bool, secure: bool, sameSite: Union["Lax", "None", "Strict"], partitionKey: Union[str, None]}] """ return mapping.from_impl_list( @@ -12832,7 +12850,7 @@ def add_cookies(self, cookies: typing.Sequence[SetCookieParam]) -> None: Parameters ---------- - cookies : Sequence[{name: str, value: str, url: Union[str, None], domain: Union[str, None], path: Union[str, None], expires: Union[float, None], httpOnly: Union[bool, None], secure: Union[bool, None], sameSite: Union["Lax", "None", "Strict", None]}] + cookies : Sequence[{name: str, value: str, url: Union[str, None], domain: Union[str, None], path: Union[str, None], expires: Union[float, None], httpOnly: Union[bool, None], secure: Union[bool, None], sameSite: Union["Lax", "None", "Strict", None], partitionKey: Union[str, None]}] """ return mapping.from_maybe_impl( @@ -12908,6 +12926,7 @@ def grant_permissions( - `'notifications'` - `'payment-handler'` - `'storage-access'` + - `'local-fonts'` origin : Union[str, None] The [origin] to grant permissions to, e.g. "https://example.com". """ diff --git a/setup.py b/setup.py index fd590167f..c4a75870a 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ import zipfile from typing import Dict -driver_version = "1.53.1" +driver_version = "1.54.0" base_wheel_bundles = [ { diff --git a/tests/async/conftest.py b/tests/async/conftest.py index f2e06d56e..5dff9794f 100644 --- a/tests/async/conftest.py +++ b/tests/async/conftest.py @@ -153,7 +153,7 @@ def action_titles(self) -> Locator: @property def stack_frames(self) -> Locator: - return self.page.get_by_test_id("stack-trace-list").locator(".list-view-entry") + return self.page.get_by_role("list", name="Stack Trace").get_by_role("listitem") async def select_action(self, title: str, ordinal: int = 0) -> None: await self.page.locator(".action-title", has_text=title).nth(ordinal).click() diff --git a/tests/async/test_browsercontext.py b/tests/async/test_browsercontext.py index 37c812f57..ba53b6f95 100644 --- a/tests/async/test_browsercontext.py +++ b/tests/async/test_browsercontext.py @@ -118,9 +118,9 @@ async def test_page_event_should_propagate_default_viewport_to_the_page( async def test_page_event_should_respect_device_scale_factor(browser: Browser) -> None: - context = await browser.new_context(device_scale_factor=3) + context = await browser.new_context(device_scale_factor=3.5) page = await context.new_page() - assert await page.evaluate("window.devicePixelRatio") == 3 + assert await page.evaluate("window.devicePixelRatio") == 3.5 await context.close() diff --git a/tests/async/test_chromium_tracing.py b/tests/async/test_chromium_tracing.py index 23608e009..fd065efde 100644 --- a/tests/async/test_chromium_tracing.py +++ b/tests/async/test_chromium_tracing.py @@ -44,6 +44,13 @@ async def test_should_create_directories_as_needed( assert os.path.getsize(output_file) > 0 +async def rafraf(target: Page, count: int = 1) -> None: + for _ in range(count): + await target.evaluate( + "async () => await new Promise(f => window.requestAnimationFrame(() => window.requestAnimationFrame(f)));" + ) + + @pytest.mark.only_browser("chromium") async def test_should_run_with_custom_categories_if_provided( browser: Browser, page: Page, tmp_path: Path @@ -51,16 +58,19 @@ async def test_should_run_with_custom_categories_if_provided( output_file = tmp_path / "trace.json" await browser.start_tracing( page=page, - screenshots=True, path=output_file, - categories=["disabled-by-default-v8.cpu_profiler.hires"], + categories=["disabled-by-default-cc.debug"], ) + await rafraf(page) await browser.stop_tracing() with open(output_file, mode="r") as of: trace_json = json.load(of) + trace_config = trace_json["metadata"].get("trace-config") + trace_events = trace_json["traceEvents"] assert ( - "disabled-by-default-v8.cpu_profiler.hires" - in trace_json["metadata"]["trace-config"] + trace_config is not None and "disabled-by-default-cc.debug" in trace_config + ) or any( + event.get("cat") == "disabled-by-default-cc.debug" for event in trace_events ) diff --git a/tests/async/test_fetch_global.py b/tests/async/test_fetch_global.py index 6b74208e2..e2a7678c5 100644 --- a/tests/async/test_fetch_global.py +++ b/tests/async/test_fetch_global.py @@ -85,7 +85,7 @@ async def test_should_support_global_timeout_option( ) -> None: request = await playwright.request.new_context(timeout=100) server.set_route("/empty.html", lambda req: None) - with pytest.raises(Error, match="Request timed out after 100ms"): + with pytest.raises(Error, match="Timeout 100ms exceeded"): await request.get(server.EMPTY_PAGE) diff --git a/tests/async/test_geolocation.py b/tests/async/test_geolocation.py index 5791b5984..12b00a4fa 100644 --- a/tests/async/test_geolocation.py +++ b/tests/async/test_geolocation.py @@ -48,7 +48,7 @@ async def test_should_isolate_contexts( await page.goto(server.EMPTY_PAGE) context2 = await browser.new_context( - permissions=["geolocation"], geolocation={"latitude": 20, "longitude": 20} + permissions=["geolocation"], geolocation={"latitude": 10.5, "longitude": 10.5} ) page2 = await context2.new_page() @@ -66,7 +66,7 @@ async def test_should_isolate_contexts( resolve({latitude: position.coords.latitude, longitude: position.coords.longitude}) }))""" ) - assert geolocation2 == {"latitude": 20, "longitude": 20} + assert geolocation2 == {"latitude": 10.5, "longitude": 10.5} await context2.close() diff --git a/tests/async/test_page_add_locator_handler.py b/tests/async/test_page_add_locator_handler.py index 4492037a7..4a5a44323 100644 --- a/tests/async/test_page_add_locator_handler.py +++ b/tests/async/test_page_add_locator_handler.py @@ -312,7 +312,7 @@ def _handler() -> None: with pytest.raises(Error) as exc_info: await page.locator("#target").click(timeout=3000) assert await page.evaluate("window.clicked") == 0 - await expect(page.locator("#interstitial")).to_be_visible() + assert await page.locator("#interstitial").is_visible() assert called == 1 assert ( 'locator handler has finished, waiting for get_by_role("button", name="close") to be hidden' diff --git a/tests/async/test_page_request_intercept.py b/tests/async/test_page_request_intercept.py index 934aed8a0..dc8f7416a 100644 --- a/tests/async/test_page_request_intercept.py +++ b/tests/async/test_page_request_intercept.py @@ -34,7 +34,7 @@ def _handler(request: TestServerRequest) -> None: async def handle(route: Route) -> None: with pytest.raises(Error) as error: await route.fetch(timeout=1000) - assert "Request timed out after 1000ms" in error.value.message + assert "Timeout 1000ms exceeded" in error.value.message await page.route("**/*", lambda route: handle(route)) with pytest.raises(Error) as error: diff --git a/tests/async/test_page_route.py b/tests/async/test_page_route.py index b04f96145..fecafdfba 100644 --- a/tests/async/test_page_route.py +++ b/tests/async/test_page_route.py @@ -1155,6 +1155,18 @@ def glob_to_regex(pattern: str) -> re.Pattern: assert url_matches("http://first.host/", "http://second.host/foo", "**/foo") assert url_matches("http://playwright.dev/", "http://localhost/", "*//localhost/") + custom_prefixes = ["about", "data", "chrome", "edge", "file"] + for prefix in custom_prefixes: + assert url_matches( + "http://playwright.dev/", f"{prefix}:blank", f"{prefix}:blank" + ) + assert not url_matches( + "http://playwright.dev/", f"{prefix}:blank", "http://playwright.dev/" + ) + assert url_matches(None, f"{prefix}:blank", f"{prefix}:blank") + assert url_matches(None, f"{prefix}:blank", f"{prefix}:*") + assert not url_matches(None, f"not{prefix}:blank", f"{prefix}:*") + # Added for Python implementation assert url_matches( None, diff --git a/tests/async/test_selectors_misc.py b/tests/async/test_selectors_misc.py index 5527d6ec8..5ad6c3519 100644 --- a/tests/async/test_selectors_misc.py +++ b/tests/async/test_selectors_misc.py @@ -12,6 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +import pytest + +from playwright._impl._browser import Browser +from playwright._impl._errors import Error +from playwright._impl._selectors import Selectors from playwright.async_api import Page @@ -52,3 +57,26 @@ async def test_should_work_with_internal_and(page: Page) -> None: '.bar >> internal:and="span"', "els => els.map(e => e.textContent)" ) ) == ["world2"] + + +async def test_should_throw_already_registered_error_when_registering( + selectors: Selectors, + browser: Browser, +) -> None: + create_tag_selector = """ + () => ({ + query(root, selector) { + return root.querySelector(selector); + }, + queryAll(root, selector) { + return Array.from(root.querySelectorAll(selector)); + } + }) + """ + name = f"alreadyRegistered-{browser.browser_type.name}" + await selectors.register(name, create_tag_selector) + with pytest.raises( + Error, + match=f'Selectors.register: "{name}" selector engine has been already registered', + ): + await selectors.register(name, create_tag_selector) diff --git a/tests/async/test_tracing.py b/tests/async/test_tracing.py index e735c96a8..e902eafbd 100644 --- a/tests/async/test_tracing.py +++ b/tests/async/test_tracing.py @@ -136,6 +136,7 @@ async def test_should_collect_trace_with_resources_but_no_js( await page.click('"Click"') await page.mouse.move(20, 20) await page.mouse.dblclick(30, 30) + await page.request.get(server.EMPTY_PAGE) await page.keyboard.insert_text("abc") await page.wait_for_timeout(2000) # Give it some time to produce screenshots. await page.route("**/empty.html", lambda route: route.continue_()) @@ -153,6 +154,7 @@ async def test_should_collect_trace_with_resources_but_no_js( re.compile(r"Click"), re.compile(r"Mouse move"), re.compile(r"Double click"), + re.compile(r"GET \"/empty\.html\""), re.compile(r'Insert "abc"'), re.compile(r"Wait for timeout"), re.compile(r'Navigate to "/empty\.html"'), diff --git a/tests/async/test_unroute_behavior.py b/tests/async/test_unroute_behavior.py index 036423cdc..ff737eaab 100644 --- a/tests/async/test_unroute_behavior.py +++ b/tests/async/test_unroute_behavior.py @@ -451,3 +451,59 @@ async def _goto_ignore_exceptions() -> None: await page.close() # Should not throw. await route.fulfill() + + +async def test_should_not_continue_requests_in_flight_page( + page: Page, server: Server +) -> None: + await page.goto(server.EMPTY_PAGE) + + route_future: "asyncio.Future[Route]" = asyncio.Future() + + async def handle(route: Route) -> None: + route_future.set_result(route) + await asyncio.sleep(3) + await route.fulfill(status=200) + + async def _evaluate_ignore_exceptions() -> None: + try: + await page.evaluate("() => fetch('/')") + except Error: + pass + + await page.route( + "**/*", + handle, + ) + + asyncio.create_task(_evaluate_ignore_exceptions()) + await route_future + await page.unroute_all(behavior="wait") + + +async def test_should_not_continue_requests_in_flight_context( + page: Page, context: BrowserContext, server: Server +) -> None: + await page.goto(server.EMPTY_PAGE) + + route_future: "asyncio.Future[Route]" = asyncio.Future() + + async def handle(route: Route) -> None: + route_future.set_result(route) + await asyncio.sleep(3) + await route.fulfill(status=200) + + async def _evaluate_ignore_exceptions() -> None: + try: + await page.evaluate("() => fetch('/')") + except Error: + pass + + await context.route( + "**/*", + handle, + ) + + asyncio.create_task(_evaluate_ignore_exceptions()) + await route_future + await context.unroute_all(behavior="wait") diff --git a/tests/sync/conftest.py b/tests/sync/conftest.py index 3d7ae9116..58193c0de 100644 --- a/tests/sync/conftest.py +++ b/tests/sync/conftest.py @@ -143,7 +143,7 @@ def action_titles(self) -> Locator: @property def stack_frames(self) -> Locator: - return self.page.get_by_test_id("stack-trace-list").locator(".list-view-entry") + return self.page.get_by_role("list", name="Stack trace").get_by_role("listitem") def select_action(self, title: str, ordinal: int = 0) -> None: self.page.locator(".action-title", has_text=title).nth(ordinal).click() diff --git a/tests/sync/test_fetch_global.py b/tests/sync/test_fetch_global.py index 7305834a9..bf3970c21 100644 --- a/tests/sync/test_fetch_global.py +++ b/tests/sync/test_fetch_global.py @@ -67,7 +67,7 @@ def test_should_support_global_timeout_option( ) -> None: request = playwright.request.new_context(timeout=100) server.set_route("/empty.html", lambda req: None) - with pytest.raises(Error, match="Request timed out after 100ms"): + with pytest.raises(Error, match="Timeout 100ms exceeded"): request.get(server.EMPTY_PAGE) diff --git a/tests/sync/test_page_add_locator_handler.py b/tests/sync/test_page_add_locator_handler.py index b069520ec..b2d037f07 100644 --- a/tests/sync/test_page_add_locator_handler.py +++ b/tests/sync/test_page_add_locator_handler.py @@ -310,7 +310,7 @@ def _handler() -> None: with pytest.raises(Error) as exc_info: page.locator("#target").click(timeout=3000) assert page.evaluate("window.clicked") == 0 - expect(page.locator("#interstitial")).to_be_visible() + assert page.locator("#interstitial").is_visible() assert called == 1 assert ( 'locator handler has finished, waiting for get_by_role("button", name="close") to be hidden' diff --git a/tests/sync/test_tracing.py b/tests/sync/test_tracing.py index 1a42aab9b..8d0eaa191 100644 --- a/tests/sync/test_tracing.py +++ b/tests/sync/test_tracing.py @@ -138,6 +138,7 @@ def test_should_collect_trace_with_resources_but_no_js( page.click('"Click"') page.mouse.move(20, 20) page.mouse.dblclick(30, 30) + page.request.get(server.EMPTY_PAGE) page.keyboard.insert_text("abc") page.wait_for_timeout(2000) # Give it some time to produce screenshots. page.route("**/empty.html", lambda route: route.continue_()) @@ -155,6 +156,7 @@ def test_should_collect_trace_with_resources_but_no_js( re.compile(r"Click"), re.compile(r"Mouse move"), re.compile(r"Double click"), + re.compile(r"GET \"/empty\.html\""), re.compile(r'Insert "abc"'), re.compile(r"Wait for timeout"), re.compile(r'Navigate to "/empty\.html"'), From bd5b0331e67ed22bc75417092e87a456dd73c832 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Wed, 16 Jul 2025 11:49:13 -0700 Subject: [PATCH 15/79] chore: roll to 1.54.1 (#2919) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c4a75870a..5c2911865 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ import zipfile from typing import Dict -driver_version = "1.54.0" +driver_version = "1.54.1" base_wheel_bundles = [ { From cc27d2512920d9bc7208722d3293815f3f2e6139 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 22 Jul 2025 14:42:42 +0200 Subject: [PATCH 16/79] test: update client certificate test certs (#2923) --- tests/assets/client-certificates/README.md | 57 +--------- .../client/localhost/localhost.csr | 27 +++++ .../client/localhost/localhost.ext | 1 + .../client/localhost/localhost.key | 52 +++++++++ .../client/localhost/localhost.pem | 30 ++++++ .../client/self-signed/cert.pem | 50 ++++----- .../client/self-signed/csr.pem | 46 ++++---- .../client/self-signed/key.pem | 100 +++++++++--------- .../client/trusted/cert-legacy.pfx | Bin 0 -> 4045 bytes .../client/trusted/cert.pem | 52 ++++----- .../client/trusted/cert.pfx | Bin 0 -> 4195 bytes .../client/trusted/csr.pem | 46 ++++---- .../client/trusted/key.pem | 100 +++++++++--------- tests/assets/client-certificates/generate.sh | 88 +++++++++++++++ .../server/server_cert.pem | 54 +++++----- .../client-certificates/server/server_key.pem | 100 +++++++++--------- 16 files changed, 474 insertions(+), 329 deletions(-) create mode 100644 tests/assets/client-certificates/client/localhost/localhost.csr create mode 100644 tests/assets/client-certificates/client/localhost/localhost.ext create mode 100644 tests/assets/client-certificates/client/localhost/localhost.key create mode 100644 tests/assets/client-certificates/client/localhost/localhost.pem create mode 100644 tests/assets/client-certificates/client/trusted/cert-legacy.pfx create mode 100644 tests/assets/client-certificates/client/trusted/cert.pfx create mode 100755 tests/assets/client-certificates/generate.sh diff --git a/tests/assets/client-certificates/README.md b/tests/assets/client-certificates/README.md index b0ee78e70..2c73bd92f 100644 --- a/tests/assets/client-certificates/README.md +++ b/tests/assets/client-certificates/README.md @@ -1,60 +1,7 @@ # Client Certificate test-certificates -## Server +Regenerate all certificates by running: -```bash -openssl req \ - -x509 \ - -newkey rsa:4096 \ - -keyout server/server_key.pem \ - -out server/server_cert.pem \ - -nodes \ - -days 365 \ - -subj "/CN=localhost/O=Client\ Certificate\ Demo" \ - -addext "subjectAltName=DNS:localhost,DNS:local.playwright" ``` - -## Trusted client-certificate (server signed/valid) - -``` -mkdir -p client/trusted -# generate server-signed (valid) certifcate -openssl req \ - -newkey rsa:4096 \ - -keyout client/trusted/key.pem \ - -out client/trusted/csr.pem \ - -nodes \ - -days 365 \ - -subj "/CN=Alice" - -# sign with server_cert.pem -openssl x509 \ - -req \ - -in client/trusted/csr.pem \ - -CA server/server_cert.pem \ - -CAkey server/server_key.pem \ - -out client/trusted/cert.pem \ - -set_serial 01 \ - -days 365 -``` - -## Self-signed certificate (invalid) - -``` -mkdir -p client/self-signed -openssl req \ - -newkey rsa:4096 \ - -keyout client/self-signed/key.pem \ - -out client/self-signed/csr.pem \ - -nodes \ - -days 365 \ - -subj "/CN=Bob" - -# sign with self-signed/key.pem -openssl x509 \ - -req \ - -in client/self-signed/csr.pem \ - -signkey client/self-signed/key.pem \ - -out client/self-signed/cert.pem \ - -days 365 +bash generate.sh ``` diff --git a/tests/assets/client-certificates/client/localhost/localhost.csr b/tests/assets/client-certificates/client/localhost/localhost.csr new file mode 100644 index 000000000..cbf60ba43 --- /dev/null +++ b/tests/assets/client-certificates/client/localhost/localhost.csr @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIEizCCAnMCAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAuxTIfOTWJzsLSYT/RXu6eUYe95oFA0gMM6pdQAws +U08ufU8S68UzBIAb6zdwDqM6LD5FqLrwaTsUW73dKefb003EgA19L3sZTHSwnSv9 +g9+rnKw/8EA+1thFo7h3EfdWkO/nuxnPfEnHu5oxMk/mCu+hdjfgh01mxc00PIO/ +wioxZtuhzKNIUu+qQAj70Dwbkzy3Zb1QAnj+02AUYnrVTYhusmP4pjrfYgAWU1x4 +WN+XTSI27LnQHE8t8OxuICFwqEEPcJg4pCFNQgmO0gcRDbd4U7JgQ0xMXZ9na9k0 +f1GOgtjKsz5IIDrAwLnrIOEXiIYlNaQ/v28BrlWytaOHGFKmDxAbGXHz1WuftmR1 +vYQivC5fkXU3+QKWoGPZaLIhA7/ZOMSVgoo+Rv+mS6Mze/6LphatUMcI1oYX4ZBY +FrsVsAeAO/FPZDcwVct0bdPCRRFBQpgl6DxMyItHMexNHwURi4YHwhYiBBe+Njc1 +tTh6dniA11MUVC5WmKME7CxgXxHVXE4MYcf/w1x4sOnxHneb1bCa8jOPELRc6byR +8mk23fbBP42EPkrgpCtLhbHgskLKobLFav43xaIiYFn744U7rbINI+5RfJ8BH5Kw +CB5RlAsymPxwDNcPXyyyeHEEUAQmtnJPrGPSGMN/QODgCWRXEvY556NGx0D5riu7 +MXkCAwEAAaAyMDAGCSqGSIb3DQEJDjEjMCEwHwYDVR0RBBgwFoIJbG9jYWxob3N0 +ggkxMjcuMC4wLjEwDQYJKoZIhvcNAQELBQADggIBAEYqpx28FFRHqXfyumTkkFb/ +hrlUD9qKUYSdjpsPLFRUYiLofNlYhCtEFEuHyLkNHqLO5Z6E7yOpm/HvdXY0eBQy +tK0verggKXy8MnpLVtsFOf4DgenTwFRG3NbNk2/KynwiAwJ0iwOdDfAmwrzYFLeR +uXysPE0lGJu277akuo/PDg3QEY44iDJgI4rqtY25sR8J23M9Roa5qN4bdaXl6l10 +UJhdugTG90gRFmmUVLLRa21A3j4m7pvWHu7zYJN7ylS/NQ6ZEnpNno7Ie6MLzrm6 +h8MOSoxtmrXi0e3aYf7PC3u3YLAjyUzHicgMdXk8SjtUXi6l3U5hU+6WBeOKKmwa +vi+FUpRfnutkWpinNqikmMw0Sqy+bpXwbwgm2oD6driiMneBCT37gW/IyUPWVxzg +dVgeQIt3i8SMwnGUrzfVo/gqjq4qFBpzS3h9jPPrMYQN2LQdmyo13R9UH26cLGE1 +cgVmqUKa11kX8353wfA36JuW5yGv2yaK4V1kwFISznMvk5AGdOHONdZOrN4sLIIY +7Npqp5AO1KdoYcNVptCA4n6mN/30fTmA/W1ajsKcfIn5RlmvdgpooKoEgaU/X9RL +d2ydIpijWbQqm+nF9skpO4jIFPfuQj6mMXCPWtsvbpHCoo0iMHHWfVFcpvsXGYN2 +6ji5z3opVapp1m5uTmcA +-----END CERTIFICATE REQUEST----- diff --git a/tests/assets/client-certificates/client/localhost/localhost.ext b/tests/assets/client-certificates/client/localhost/localhost.ext new file mode 100644 index 000000000..d4d022726 --- /dev/null +++ b/tests/assets/client-certificates/client/localhost/localhost.ext @@ -0,0 +1 @@ +subjectAltName=DNS:localhost,DNS:127.0.0.1 diff --git a/tests/assets/client-certificates/client/localhost/localhost.key b/tests/assets/client-certificates/client/localhost/localhost.key new file mode 100644 index 000000000..7f6adba53 --- /dev/null +++ b/tests/assets/client-certificates/client/localhost/localhost.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQC7FMh85NYnOwtJ +hP9Fe7p5Rh73mgUDSAwzql1ADCxTTy59TxLrxTMEgBvrN3AOozosPkWouvBpOxRb +vd0p59vTTcSADX0vexlMdLCdK/2D36ucrD/wQD7W2EWjuHcR91aQ7+e7Gc98Sce7 +mjEyT+YK76F2N+CHTWbFzTQ8g7/CKjFm26HMo0hS76pACPvQPBuTPLdlvVACeP7T +YBRietVNiG6yY/imOt9iABZTXHhY35dNIjbsudAcTy3w7G4gIXCoQQ9wmDikIU1C +CY7SBxENt3hTsmBDTExdn2dr2TR/UY6C2MqzPkggOsDAuesg4ReIhiU1pD+/bwGu +VbK1o4cYUqYPEBsZcfPVa5+2ZHW9hCK8Ll+RdTf5ApagY9losiEDv9k4xJWCij5G +/6ZLozN7/oumFq1QxwjWhhfhkFgWuxWwB4A78U9kNzBVy3Rt08JFEUFCmCXoPEzI +i0cx7E0fBRGLhgfCFiIEF742NzW1OHp2eIDXUxRULlaYowTsLGBfEdVcTgxhx//D +XHiw6fEed5vVsJryM48QtFzpvJHyaTbd9sE/jYQ+SuCkK0uFseCyQsqhssVq/jfF +oiJgWfvjhTutsg0j7lF8nwEfkrAIHlGUCzKY/HAM1w9fLLJ4cQRQBCa2ck+sY9IY +w39A4OAJZFcS9jnno0bHQPmuK7sxeQIDAQABAoICAA5XaYcpg8E+JX9dUrRg58qk +NXuFsxytSUIsrTlbtYotZ8LzbN/mHiMaLwm5Fj4JBUye+XgV3Jg0jzr5MxsjSxbH +v2iRoCcjqKzTxTZHSQfy/ZTlH4GrayXNLol+eqJF87zopzsQn3dHsKgRCfRxa5Er +DZWicvPsWxSOxpJdBzY7Rc48yAqH+eNhvAtspOExumtvHCAQgzGtVNufYfCque9X +piTGxSj5GmbI2u1JCXDGszKWjN9Y3ztMVplBhq+v4JMFacmX4b+zTdjiIrC3GfeT +OQYxhm+iSbhjn+oEnKGl/ubI98EF5UGTP3OGzR+YIdW1cuTJ0pk6SUa0Cx8hihmR +rnUgxbXjR/sB383ES4kYVT+tm/a8PIvPd1gu4j28sNRXGgVZ8GU76Y2ZITm3eoDr +7PzVWsTe+vbxBXenlcmzO9rj91yK0eDBaJnwAIPxHGFxTkQkx3T2nnWStkHrvT6h +15fHITgKWE+K2FMCIJQj5gIT3qGnZxZDLIgmyowVV9ZXuI2XchehD+L7nVsMhsby +yIITglz1miYgy7zVL0oolIXuE0eQ4xAZlu1D5wjAQy/VzyB7OYY6bTErC0EmXjNS +3QAYsI+Kn7YgjNJJNuJ5cC2S8nHNJ2uTJ54KWeA5s1Ry8B5kJrMGZxCT6Uk3POcG +Ry5BxOczToKIgXeqm4IBAoIBAQDcvNBNXANexHn4r3yXgmL9IH4QXHAJ3h7tO6Ng +zZh4K5VmoxSJ82CWQkJrs827F1kE2T9Mq6YPvXrh5VlKZjR+II2ZG7KqT00SULDy +kSgG8OU/2+DWhyoQ6nt1RhXKDz0s4SjHwkeKwBFexTHf5ed+TILPfnPJIUX89gxV +4EKL+EsqOsT6CMS7gruUuaA29VGVDneBw9KxKD9IdNXDrbot0DhidIYW1QkCed9g +tnb2QvJrQmTkupUDI+HdWqOfpcBHwf9FIEI1q6cJtpVYSuaZfZX7hbvSHsTt9rwt +b6gkiuPBy6qoXP1LWHc093tSzXEk1CWy0F4cCPi4r7UWqmWtAoIBAQDY95LGRUTn +RKRSl2C5uZnsME5G/6XkqVcGhP5TOnT3/li/NLESZDaF+4iEhEh4M1oWVqShq4ps +S98baYebriZDMmBm8L1YM63N+fhiI3q1+TF26f7zk6XPFQr88WIli58JmrK02EXG +4XG/GBtzqfsnueBnwBGz5iE8phXqWyPs1/3qGyK+BHnUqnr3BAZpg8aw2PEjMzoC +ChfLMzSJZRGDNp7sslkMJ9H9CXoMZsG5YJkQp7aSBD1SI5rJ2q7JC3PmOR913CgU +0DDhxyJhD77aTEwrZOByaHq8F8KxZ8cl+YRl/tnWu7Gwa2pfjnC9DCk1ARCuDcSg +OW7JTtsd3zx9AoIBAH17uM7BaAkPmGcPG7zlmnBbcE7MvcReSSaDqLT3K53k6OGY +A60Idff1YtznMiUReMGQ3rMvQQ/hn2Gbh88Lmvu4dcZ8QG0g96dZx72dVyva9ff/ +fyl1XSyQn+5jES/0ycohlZU5lIID/dvqLhgiEh9yT0q1kAzepXLQTOLkwe/gDprL +Hf8lzPDruMcrXzDe9KnPt5BFShj70D3YbUz4DcbNf8A4jaGdKaoGrj3EfIwyMq1W +6RQ+HUfTtiqnxCyVhWFFn2Aknn70Pdj/upaevciz4/dAZy1j4H+GrCMIPoXHjwI0 +Tae4dSXH/LxXk/vWXmOZVnT4jwdQ8lPLTx67b2ECggEAK5t20IrTknflXwQ12J5J +JYN/+B0hxpeSeij4xNmW8NEaHTQF8uBZZQxtH9VGi4ImtR6s8CF+LM4DBYtsSgny +fsb9QTNZmwSoBiIbnf3rh++R1YiqSWJ/jON51eTeCRXK3S9Og7KEM7jUF8hMnC6p +4A4n4DJmXHYAcCQhe3zd95hh3E+f5/kWU3wAQu14LHTj1l+D98MwAYDtz1V3VbYO +kwTDZGdkJmFKf0UMVrnAbfXQTdyngSmA+aVWUwO05Yt7u+X3QMUC+UvuxzIy4rc7 +cLytAnu/8L63DF7qLqXhDOzdg3J5bgNDb2Xnd1U1q4lqLtEL/S+fOWTRs3w55gMc +MQKCAQA0tO3yakIOlizE7qlJye2n2Y/q0nSx1iFlKqEe1ja0O7/t+UzfklBiaDhA +NAqYOiUsDsL/vf76KQ0uE/v0No/36SKvBuQxozdNEGlZHf7CnsrIg1w9llrJgkYA +1FoSHgHCbC84+dUoonjYg9vx3Lsszw5O1bmwqTBaGSSglIr3f+a3zxtQUquxNEPS +advAKAdvXsNECj2cMkv3mxSokGQtd/s/rpvxJ8zi5nq5ig80Gfe/6GJpwJnC+sLr ++tn38FNfk5pKXk8jBXjyLtMNDb2iJbiTa1iX1P76Ae2jtexSd2gELNu7oWbotb/O +DjCwHSmcSUmAmx7IXfoyRQuKLeRw +-----END PRIVATE KEY----- diff --git a/tests/assets/client-certificates/client/localhost/localhost.pem b/tests/assets/client-certificates/client/localhost/localhost.pem new file mode 100644 index 000000000..5c1a67505 --- /dev/null +++ b/tests/assets/client-certificates/client/localhost/localhost.pem @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFKDCCAxCgAwIBAgIBATANBgkqhkiG9w0BAQsFADA2MRIwEAYDVQQDDAlsb2Nh +bGhvc3QxIDAeBgNVBAoMF0NsaWVudCBDZXJ0aWZpY2F0ZSBEZW1vMB4XDTI1MDcy +MTE1NTYxOVoXDTM1MDcxOTE1NTYxOVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIC +IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuxTIfOTWJzsLSYT/RXu6eUYe +95oFA0gMM6pdQAwsU08ufU8S68UzBIAb6zdwDqM6LD5FqLrwaTsUW73dKefb003E +gA19L3sZTHSwnSv9g9+rnKw/8EA+1thFo7h3EfdWkO/nuxnPfEnHu5oxMk/mCu+h +djfgh01mxc00PIO/wioxZtuhzKNIUu+qQAj70Dwbkzy3Zb1QAnj+02AUYnrVTYhu +smP4pjrfYgAWU1x4WN+XTSI27LnQHE8t8OxuICFwqEEPcJg4pCFNQgmO0gcRDbd4 +U7JgQ0xMXZ9na9k0f1GOgtjKsz5IIDrAwLnrIOEXiIYlNaQ/v28BrlWytaOHGFKm +DxAbGXHz1WuftmR1vYQivC5fkXU3+QKWoGPZaLIhA7/ZOMSVgoo+Rv+mS6Mze/6L +phatUMcI1oYX4ZBYFrsVsAeAO/FPZDcwVct0bdPCRRFBQpgl6DxMyItHMexNHwUR +i4YHwhYiBBe+Njc1tTh6dniA11MUVC5WmKME7CxgXxHVXE4MYcf/w1x4sOnxHneb +1bCa8jOPELRc6byR8mk23fbBP42EPkrgpCtLhbHgskLKobLFav43xaIiYFn744U7 +rbINI+5RfJ8BH5KwCB5RlAsymPxwDNcPXyyyeHEEUAQmtnJPrGPSGMN/QODgCWRX +EvY556NGx0D5riu7MXkCAwEAAaNjMGEwHwYDVR0RBBgwFoIJbG9jYWxob3N0ggkx +MjcuMC4wLjEwHQYDVR0OBBYEFIE6kbIR0PlzaJuZg52JqXuFFQHBMB8GA1UdIwQY +MBaAFPHaEfjJoIftSkHTb8mwme27LtifMA0GCSqGSIb3DQEBCwUAA4ICAQB7E57H +19qUD4DjLxJGAVpDphD8mZg9aR7bUd6laXZ12VQnn3+OrF+6JjJ2TIr8ssRkkzc6 +rMhRqob+DxeB94JqlFQDmwP37wJXnuTtuGj71NHQal15b5ZB28fFdwWgUlYECWyg +sYeMK5HMnNDziniRnjPoKU6f8urmUW2G8N2SOufnRar/StY/8u24IFh/vDTVJKWB +a22pVk/PIx6JcErfMx0OJdoIUZd/C9DT/a7t+Pt+t3EPVNM2fcjs9aMcg6tlU6Tp +YwHMzkFQYmcrp2QEhwy23z2E+wnLbeHTULDsHVOLudUGDcSgkW48bPNTOtZTysQ+ +P4mJf+Ju2HC/EwuVrOa14uuay9c+hC1Q1gsGbLZB8NE6jOX5QYN2a7iiUEdCdufo +wWIXEC+2dJiEji3ehwJp0q4jK15X0IOIfxLH4PholORH659DdCfpm7YAjoaPE9nr +28MMd9Lj6zWmfyeb5p0IUDpkreL2huxjXHJkmSm4dcxDxLqsP6KSPlhrPvjAVUqw +5M/jUarDAHaPLNlwqcqWODb891FIyRmc6YHtSRGujMyfJ8LRS2f9rHeyM3m2jMHf +8EahOgyQ8nvSs/a42VS97IHkGl6qufK7ZUq/VMhQRb+KbaIDN5TLtNwEQ1I40Bm9 +UOoSX/uP1PstOZvf9gnYto8sf75B+PxO692uYw== +-----END CERTIFICATE----- diff --git a/tests/assets/client-certificates/client/self-signed/cert.pem b/tests/assets/client-certificates/client/self-signed/cert.pem index 3c0771794..d14ac95b2 100644 --- a/tests/assets/client-certificates/client/self-signed/cert.pem +++ b/tests/assets/client-certificates/client/self-signed/cert.pem @@ -1,28 +1,28 @@ -----BEGIN CERTIFICATE----- -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 -----END CERTIFICATE----- diff --git a/tests/assets/client-certificates/client/self-signed/csr.pem b/tests/assets/client-certificates/client/self-signed/csr.pem index 4c99e1349..283dd45a6 100644 --- a/tests/assets/client-certificates/client/self-signed/csr.pem +++ b/tests/assets/client-certificates/client/self-signed/csr.pem @@ -1,26 +1,26 @@ -----BEGIN CERTIFICATE REQUEST----- MIIEUzCCAjsCAQAwDjEMMAoGA1UEAwwDQm9iMIICIjANBgkqhkiG9w0BAQEFAAOC -Ag8AMIICCgKCAgEA179eTsqcc1c3AOQHzCZEyYLPta2CCAscUFqcEZ9vWvjW0uzO -v9TDlB33Unovjch4CElZOBhzTadVsbmnYKpxwyVU89WCuQKvedz4k1vu7S1YryfN -bmS8PWbnQ4ds9NB7SgJNHZILvx9DXuWeFEyzRIo1984z4HheBzrkf791LqpYKaKz -iANUo8h8t0dmTX/boOz8cEnQNwtTC0ZX3aD0obG/UAhr/22ZGPo/E659fh4ptyYX -2LrIUHGy+EuxnJ9Y4cTqa88Ee6K6AkDiT/AoNQNxE4X++jqLuie8j/ZYpI1Oll38 -GwKVOyy1msRLtoGmISNwkMIQDGABrJlxgpP4QQAQ+08v9srzXOlkdxdr7OCP81r+ -ccBXiSQEe7BAkdJ8l98l5dprJ++GJ+SZcV4+/iGR0dKU2IdAG5HiKZIFn6ch9Ux+ -UMqeGaYCpkHrTiietHwcXgtVBlE0jFmB/HspmI/O0abK+grMmueaH7XtTI8YHnw0 -mUpL8+yp7mfA7zFusgFgyiBPXeD/NQgg8vja67k++d1VGoXm2xr+5WPQCSbgQoMk -kOBMLHWJTefd6F4Z5M+oI0VwYbf6eQW246wJgpCHSPR0Vdijd6MAGRWKUuLfDsA9 -+12iGbKvwJ2enJlStft2V2LZcjBfdIMbigW1aSVNN5w6m6YVrQPry3WPkWcCAwEA -AaAAMA0GCSqGSIb3DQEBCwUAA4ICAQCb07d2IjUy1PeHCj/2k/z9FrZSo6K3c8y6 -b/u/MZ0AXPKLPDSo7UYpOJ8Z2cBiJ8jQapjTSEL8POUYqcvCmP55R6u68KmvINHo -+Ly7pP+xPrbA4Q0WmPnz37hQn+I1he0GuEQyjZZqUln9zwp67TsWNKxKtCH+1j8M -Ltzx6kuHCdPtDUtv291yhVRqvbjiDs+gzdQYNJtAkUbHwHFxu8oZhg8QZGyXYMN8 -TGoQ1LTezFZXJtX69K7WnrDGrjsgB6EMvwkqAFSYNH0LFvI0xo13OOgXr9mrwohA -76uZtjXL9B15EqrMce6mdUZi46QJuQ2avTi57Lz+fqvsBYdQO89VcFSmqu2nfspN -QZDrooyjHrlls8MpoBd8fde9oT4uA4/d9SJtuHUnjgGN7Qr7eTruWXL8wVMwFnvL -igWE4detO9y2gpRLq6uEqzWYMGtN9PXJCGU8C8m9E2EBUKMrT/bpNbboatLcgRrW -acj0BRVqoVzk1sRq7Sa6ejywqgARvIhTehg6DqdMdcENCPQ7rxDRu5PSDM8/mwIj -0KYl8d2PlECB4ofRyLcy17BZzjP6hSnkGzcFk0/bChZOSIRnwvKbvfXnB45hhPk8 -XwT/6UNSwC2STP3gtOmLqrWj+OE0gy0AkDMvP3UnQVGMUvgfYg+N4ROCVtlqzxe9 -W65c05Mm1g== +Ag8AMIICCgKCAgEAzQMXYOZz3ILrbF9qpDng2pw2wJf1UFopehwaYyu7riWJ9+AD +rhSnCSFBM3ScMBc/8dIR6etWwci8QwJ/MtvIU0yx4llq+53G+19Bc1teC6q/b4QC +RDIcTGxOZoR+jfYZVjPODEyJ5y5MZIo34ZP4bu+JnpT4W7+uojm3jOoyNPqXMcc7 +0uAhfSKG+JfrwZKteA4T5VKFytVcWgh4v03z8zTYeW3kD4lCsongBz6yu2dn3D6X +MROnxiPwi+IRQqX1pwnJ0UA3CTeOHEw1jA3koxWIIg44PWaPaCj9Udrhf4ew00XL +PWVZP8T5rVf8yUfecWQCR7FueJFqoLhPMMFi17rYmGZUvw3/YkXBjay4Q9e+G2WS +3Xk8u+I1sCuVBJNBRv9DqtMC9D/N9NI8GkLrXwZmk82SXG+cQ0TSkNUHYI/03YKo +qsn5H8PsG7Tc+Y2Ca6TaCWims7lvOg7U0E6lu2h5NGcdWHFPJ9qfe+xho/yfYYwG +qEKanGAu1kd5SbIaX6/YiM5/Pp/96MeRNrB9kLzDnZTNuGtdCawVFgbkWmfX2Z6/ +a0d6SvZGDzBxxTVZRB0my01E1FP53MS8YRH98HUjGEwNRVq2e1W+aXldKppgZ85G +ZD3l2YTeuI0iPJCDUzQiaWzFtWc8s13YQ1HLCOyOXF7QqMyNCiLGb4xQ56kCAwEA +AaAAMA0GCSqGSIb3DQEBCwUAA4ICAQAn/ZI7IkBUEfhZHefwtF+QHCyxSEKvqwHq +fSqKVdarBPz8Ik8m3icj8R/DcS3y5jgzx3x8bXQoDpgsAQgeb825NRv2wAQAGoH1 +8vh204lTyjqzrgtK7eQeQDc7fjeigIkxQsAK9zk4BaFUWp0wEC0RLVAgvlQTl7vu +n1jSSrhK8tvGy62/cIxZfwD0bAMHlW4m1A4fUuSGWQX2KldgA8tnmT6wx0If/nKb +VB68AMbyMHUeb32v9wEvx2nHlwMjqNFeg7vYyJXOfBdDILUl+OTBoQY1X+jSx5iM +txTzmA8Hcgx0Fq+BnbQuZCLqFpNWEfenAtQtaAFuJwMiKCf6kgbqkDVShJkmt+vC +j3dcsVMZDsdMk4qRpiJhaTQOYmsMGCj4uoDpFGjwPoUwlDkjYgHAAsm9uCkshc+m +WZO7I6Z3Tbi3XskJvAMc3dTWjtc6nApEtr/mn8LcETfOp7RRSfjllj6ijWUrVwUy +BpzU9C/zLTkhFX0DVDCIV+jEefF8JPfzSKLgXyRbInTz1/6/sKXtswXW0NjzqLMI +C9ggMBhOiDv9KJn3G/mY4CqIfo9KMzF+++4t+wdXTir8DWNlMUAn1vlBwxZAgKCM +GonVExBU0VIGCpyTRLkesEHnPMgybP6gLzP3++54x288OS5JwuPPtkDcsBHUjTq8 +HxTJvUul/Q== -----END CERTIFICATE REQUEST----- diff --git a/tests/assets/client-certificates/client/self-signed/key.pem b/tests/assets/client-certificates/client/self-signed/key.pem index 70d5e3dd0..c3ddd1a91 100644 --- a/tests/assets/client-certificates/client/self-signed/key.pem +++ b/tests/assets/client-certificates/client/self-signed/key.pem @@ -1,52 +1,52 @@ -----BEGIN PRIVATE KEY----- -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 -dg6m3MAyZ1IXjHlogOdmGvnq07/w +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDNAxdg5nPcguts +X2qkOeDanDbAl/VQWil6HBpjK7uuJYn34AOuFKcJIUEzdJwwFz/x0hHp61bByLxD +An8y28hTTLHiWWr7ncb7X0FzW14Lqr9vhAJEMhxMbE5mhH6N9hlWM84MTInnLkxk +ijfhk/hu74melPhbv66iObeM6jI0+pcxxzvS4CF9Iob4l+vBkq14DhPlUoXK1Vxa +CHi/TfPzNNh5beQPiUKyieAHPrK7Z2fcPpcxE6fGI/CL4hFCpfWnCcnRQDcJN44c +TDWMDeSjFYgiDjg9Zo9oKP1R2uF/h7DTRcs9ZVk/xPmtV/zJR95xZAJHsW54kWqg +uE8wwWLXutiYZlS/Df9iRcGNrLhD174bZZLdeTy74jWwK5UEk0FG/0Oq0wL0P830 +0jwaQutfBmaTzZJcb5xDRNKQ1Qdgj/Tdgqiqyfkfw+wbtNz5jYJrpNoJaKazuW86 +DtTQTqW7aHk0Zx1YcU8n2p977GGj/J9hjAaoQpqcYC7WR3lJshpfr9iIzn8+n/3o +x5E2sH2QvMOdlM24a10JrBUWBuRaZ9fZnr9rR3pK9kYPMHHFNVlEHSbLTUTUU/nc +xLxhEf3wdSMYTA1FWrZ7Vb5peV0qmmBnzkZkPeXZhN64jSI8kINTNCJpbMW1Zzyz +XdhDUcsI7I5cXtCozI0KIsZvjFDnqQIDAQABAoICAAN6aFLBqijNNFEM/95MKJVQ +5eln0pbDxtUeZbC1yNv8IU56J5nUGh7gqG5m7bDvrgssXxcuwdStEwuYft+2JJyM +Li7qyTK+YyY34CCExfBQ++k4jkDJsFr4Ee7xk8OVD6o7nATvpf3M9mkUwryyIdqA ++B7fhGSrGHuCWuu6O/KT502GBazu1kadF7jfO/XXZxfEtl/zQdeWfdf9sY2+VPOU ++5C41XARijcE+Y7p6IafKx8MlUxU+ulUygOXiOcucV/dfcXt7tkaTxAKF3T6Nd0x +8/Ku9tOM2kVAP8b8HYwIOW7mLdvrbKOVNA61sdFY5axbD+JXP2pufiZ+pgJL36FF +SDQIW5M3aH7CSa1i3i4MP49jWomhTNwseVrXsDuGCKVqgIR5LZwpS4VOHLAILkCh +cIEDnoMS9YPuQdENIIxKyZGGHaeJ+LRb4w+szvtmu55Kp+N7AubPfoypPrx7a8LN +8/0/w731DS6nTICYXXzzGoB3cefb3nsBNaH1+edffPTZOYlFZ9ElFjIs/xvWCSy4 +qYwQ1cW4DslIiVD62wm8Df2yr/5J6znfU01RXQ4GWfmDNFBdYsQO/8JEy6UZEvCy +tFZ1gseD9K69O4XZSEKRKIvv8+1Y/CwD0ppIOYCIycTKn87GXFmsjbuj8tghmHp4 +TUi3EUvrw8mQMi5QBa5BAoIBAQD8JzdNy4ietoT8UUWqBT+ZSURPWeQN1esbncYU +b9viBIznnjjFFr5JdYa5k3rxM+bTRq47NRt+r0HOvyJWUFJcI6tbgmGtW+rmM2kB +hq6ekTJleLz+/cjNSjWD14avNORPz/ozZMlcz52NEl31pdniuDbueeNgyh+CkJtH +BS8s8mMVZ+3NtafZ1ilGn/RP+n21C1J8Kcxd/1srtfcpybzAQiSYul2DlKVPGvqO +XpLyt42/cyc7a3MyXtms2XhJ62fDK24Qptp6KJNTzqdtY+KP/iOW0SUgxf+JpC87 +W2NJW7tqyyaebn7KO1lGs9y03KzwaLZvy2RaBfjQnxS8uXzpAoIBAQDQI8QHNtr5 +nHYSzLZJMhJkP641wQWo4ODfkWxtEMqTyOXrVw3L8HMdA03Nmf9jnCv5awYYA3j0 +PmSL3PdM36d3VsAyyxMBN4HrH2Z94oTnoKmDfXjB3prhPYgO6aosSvE8rw1N055o +757p9vAA5w9apBBLNdcm3cjUm2ZKeocL4wnFjOW63CtcFEouE4R7C32rauEu9Bdg +dAXciBmOrHtihJUQrpMfyfN2fVSLbO4SoFy7ZHq5YKFk4MzNIp/cENwRIqdcLvOz +o++RSbwptRtkd/HZCFSh/4gEPRLe/k5gGErS9ZqpeSMfV++IdBMqC0Sx3UpyXuue +FOhIvnLpJlzBAoIBAGjcDh2mBLyr/oXHbocUA6zFUUkGgtZWHZ2wcQ1Sr0hAyDAS +Fl2v5ZY677oA4OGpydYW0KICpdp7G4zU43ytjnKOytYVVHV5gigVPRfLYJbEnwaf +vUj1VSo6MCMR4ArAnimqvcvdn/eex1BBUR20yPWF0iI+QhagN5ZeeJSCTWoNqrLe +M4CWiKUIcMXUAw+3hctiV/0WjMySQuHcnFqecIYre3igF/9+M3jAKW5HWijhuGrj +gm8tcgyCcVd2YJWs9cuuJel62eRvN0Vk7S+KmE91SmuPsjb84BXnV1UB3jpFkZ0J +upesL8H+CFRku+Xi13Bqu2OmW6csUJrBbShGovECggEBAKGk9SupJXyvT1+gTn0f +/vqOHiyvAEc8hkf6t5sobDtDzZPs4tEcpznEBBuF2rqwYdJtlKj3oWsGPa4FaKXy +GCvtWozX+6V5R1Oj6kQftJnyw1NUEYF28Q+2asEyJTAK77jyNkHX9HGIjwEi/xek +Wt9JBUJzyOjtW3gKS/HRoKnRpBghKZTqQl5bf5SzIbMxpGKJOeLuPG1zDc5MgJS2 +TYigcOgovCf2/jZqdUtmyKn8kqgSC+GGMzGWCFfT6RTOnypLoHBOIoPD8F0ER7aY +aXKoWFH2T0wUmLy59brrA1FL7GhTx86QPn+sGmH9y5hecfY0ZwnVv+TgVdmQ1stN +OMECggEBAOXDX319Dmo0ydAPYyngK4/slOetGaJmz8looU8a2R2+Ko/VMTZDmJhf +P0vS74g3U7sukRjmYzUY1mPj27CDURvk1ENam9KPOQ59ws/TaHaJ7tobjUXVQ93/ +OREkrlCuqbEqJJzQ01mCWIbmDnGvJwD87rW6YwmI9Rs+kjZxNLj+IW4CqpY36q9A +HwaUZLXc2q0W1CqLmFYF5HotvSFIAYHWuClEmM2NI9+0VItarBz6AwCnVXwKbJLC +irXlllX+63uloTDR5W1ymy2hTUrhE1jgh9DR4106QSVDiEWqme/BAWmUoyuN/zms +v3/WVVAXEcIowL3T4jzJ0RLdf1qE8Bc= -----END PRIVATE KEY----- diff --git a/tests/assets/client-certificates/client/trusted/cert-legacy.pfx b/tests/assets/client-certificates/client/trusted/cert-legacy.pfx new file mode 100644 index 0000000000000000000000000000000000000000..9f06aa35c82f05be583a0cf8de295a68fdb9ccbf GIT binary patch literal 4045 zcmY+GXD}R$w})AEwM9hly<2r7T6C5uQKLjBT6EFET46(2y|>j8ge8dRqD7BhqVvY; zz4x1U=HC0i_rsYp^Ze$VPiLMvU^u8BfQH-l+v5I{V1pU)iT-;72{t~M43|(%MP~8G)#e}oK!^o{<5?eS zE^b1E6zBh}dJKHXp8t5yQP>tfz@jCWP9~0;T3EKEsr8vUX6wq{a`e0R055^sE6cO! zJGs^yC6la~u=NcIOF#L{(n!z+N6jvg5cYnT_HL@+(asUxO{SAAyY~24(`rc#eEm^T z`qu%F_7-YFLT^$)%yP&BHMZQR0S~D_>$Ym<5!cTqi7gPHp)MC=`*XcEpkgpFIfk ze4Vww6k(Ko#)Y>#?TqPB?>K^|R0yPO)6SEZ;%;@sm^*;Kln7%MNCv^!*%}F;0#+v; zX6@i=mtiWwH5l$x$vM|(H*P>%cxcUg7`mv{YH%_+eZ4}!ineM>3Pj7)L^Dd40)c_q z7M*EMLJz~}n(#%{Qjb1UeU}-Dw6Kc)vnWl9cNKU6{MohpHubs?8YTd+^M)ESR-nD_ z=k95BFJ-f}&-vjQlP>&;#Jw6u3{hErc()S83Tu5g4zWbTcAx=D`to%)6PskY`yciM z@fVWkkcn&lA^%UIJQ8H>BUn@K1y^449dOqZ@7!dLg}%xsWA3m(#c%Wm5#amdJxPTk}hO+ihOtJ$eU$>x5@6mug5|2 z&~^}~l=U`!q540)ll&HS&Zwg0Bq~Q5(SgCgcQazJ8M}S;w8XS>vmnZrm%>H&vX%My z9a!kZy*A)SiGx$O^65z6j~aK==YNDyF>G_HX1X0=pq#E$V=0G=uG_X^KGh< z2!HDlmQHWA5yq|8=<4=uXxnqYi?K*&Fus4hHSdy(Ey_Fu)Ng@UwAC9lhG^8CsID@J zH^8vJ^LErq!Tdn2JI=FjnEdO|WikfH;rPJwT-_E(+jmOM!@IeqGYiOD8F?UyL`nX$ zw!Luc@+j%C?%QZLMAH6B)E}k>I=93zc2viluLw=(PUVhes8&2{aNzdtimr_*vcHUb zd8g<=J9E=T^c?`d1K)^|4K>TTwKpRI;vSx;ZImDCAgG$zD?rkApafZdj1Irrb{+S# zV|dY_8C6zlI&>hNOabcA;2OdIR3q9;5zo#>9T4;;KF!9iKXus7fZUIA4Fq5N>^!CJ ztHS2IJIK^hA1x&@?vPRZcFCdF=E3eO!1l32U5T*mK2IjITAncDk}Yu~``xsm89|DC z#dV9xN`ENv?d03|FQ)O2H-zcDsx)G0y(ve?P&R=GB=f2)=LBmG3Fl}kj-p31Y8wxG zM2sZhk!QOvMT<36OZqRo*C1Aw#_0KstjDV9;+Y@pXJ;Y)WSow7UT9dv)MT6|{U5@- znsx$i zWd7j5W6sN)lVQk?_TvWk5ZRKAE)#2JXU3RhuxH7GT5)hsN7b45@VsH_1@s^UUXx&y zI4zkmIA28CsK)n|fFmm|0diG?W4PVN!WR1ia-dZtOHsX1j&aYs_;dD&KrD)em@jmz z>hj~6ytOj)cal8b+p`Bu_y7lM*xp4;vN-KS7Nhx8v$P@%}o*Hww0I% za`?*QjcJ@3i1o!Nu5G$Q%Iepm5rsPY7Y?81VmJab42#UEnB$nHPQYt_nms)z+Yt@g zI*FigUO_d#+i{B1mehww_K{78U^uY<{|Ffc2R7ltf%SiJ&EG=f6aSB%iLe2GL;t_f z`~O)R=U;0(-(fdc*(0$2wf5g}y=MlEG)sz?^@$Exj>MX@xp8H#=hSRU{#3c6#n-!6 z^Dl70%g)#AiW5`&nF-%uqWMoQT{gij6js|W?0oxZv8VZ|1U)7bXNRPq928EFw4CGS zNUIv|=z-%Jc#D5_$xEZK>jg0~!a9#~7u)-zXsx9d{%yxl3_fBQF*1=SUJfwYKCo4y zu&P2&X5l@2b*8bB9H~7ly1Gl#8tKQA9VvnHDSaK}9t}nszI348u@(zCR_;Lhdr zlEL4BJU8M-sPO2A9!XDhzmsb4Nt^aZ7iDlv-B1@6F&rVNjn*90$;Iqa_2j!0u zW@o@V1qnBcE8eBlKy@#XE*^3RL~9dG&!b!9r1mvy&J%c8pZ$;$)E`*u;h(x}{Ydbq zxT;r$t9s}pL9P7M*^2z2=Q`BApjnBn3ZH=RkUg;op4Xt3H&pHZoN)m-$n-3?6s^m! zHz#@gOfWnNw2mQSj%s84BfpgCt1O26-0t(67=^dzQsB!IHvK_D(DiK4F6|K%4PFCS zN|jr)H<(f<4$Xg~r2D=(x68m9=*!Y5#aFO9O1wJ0z<%gtZC)OXhb+KGxmHUxw@x2# z>?)RpHYGjIezNjor1sEpiFDSI?Pm61VNH_uQ|{5Gp)yFyc;&+P zBn`)+~ z$zrRCHM9`StP&r=VWR`RA?#*DWwb)KR|n2y7r%N>w@Dj0P?Qo=2ZYbpnp8iTz{_I} zmWqqIbh?gYY&3iEUDx4Lv(qU?(YdYD4Ljy6#~>CkWi=qs;;X_Xy@Bz7#asPneMMhI?r$5=y! z&`BP5!6>5_Bi~MnE zVzM^&Y@XaLpKccc-qLcL<_x(Nh$%DKJ?^MhasAa%kYA)}0?Ph%_KIgS~u{B?5%l56asAQiqB6@(wktV>9R3)SLH5Sa*=_sae(Nw0xoNVGSh^Gp~ zr8=t6bF{?)J6JT0$dM~>Umg7tP{t&PMeUiL5MRbY4(xWVmc#KHePy#wm>nypHrtXI zBGWZCL!f`jE?fe(&e1H71X2fg)aiU^PIXC9E$Q4FlzDAb%NBw{H|QJ%;934MqF$*g zE{U=p?!lmAVsj3QRfaU@xBxvhnPRGgKbnd48t-y#%gyX6=RJ-wFK?AdHh}UKfhTI> zzx}^UHpoEVhc#yF;B>~gtlrV=3n>*@m-w|yds5Igbs_jv&oBzk7*8Mnv=MSS`*cA= zdad@R{TVI38(mY?JNp2U4v6BhJ^P)qF~&l(qDPR$BmwKqSI(aYZ@jaeBj*MFk-81a zb>g!!x)c$-(7y>D0_z7x-4TQ)(VI&jyGwe@xfSPBT^2V^6j0`vzN6F_l8Ct|B~g@_ zCX%H-{RALu>@a``4RKr^#Gh>(=6M`YE*R&rryl(fNNfu887fm#Qe1PJjZ!iXBb_gx zFLK+LS~|*~x5$CcUSR64F`}l6zFRC`szHnT<47Ko?s<0_8Rz?RT_#3Lw^n4F$f(|P z&P`+fuwsik=4N|F3-?Zzpb8I-Q}>($41>VJn|wj&1K7=&7yGvd;crRve=041Nf*6t zOc=|>6I+W~eFX0&lMK&w$gN5Un}8QSns}LQF@@g`cwklvbt-XnvokgmNO7GPp2sS@qr?7)tZ->WjvY3R?3$G< za11P@HJ4?yevSA;?)5*-E(5H&bI$oQ~0(!QB?#)6= zW?~))4qV`M#?1wX9w9%SSPE@-n2`NT{f!k^|KP;5mX4b3pk9j_yI2;pPKFJxeC^#+ zEv|F9+f#GsZm|m03zkvaR1xKbWpGnOZ|u>+g|>csvZs;B8iegAb;-IL9P={wXTY6p zoQYC+SEZ}Z`u!(|K~UpCw4T~h9hakq>BRm&^(n!X4#tNT*>t|*)$kVfG+9z$%2TC| ztF+{df6~NOWPa|VPj$YxtGcdXsl4t96tK=dVrDpw9@mOl^oh<{qrBcb}m)bR!-2=At)8z@=jfi}1u7w8w=h@qm8$0<=L*oo`IkKy+3! zK4PYFxfLwFYWws2AtX)t8No+}vnDK~s+T{%O|6`NMLZXwzLN)gXb`avUsprclx2><(AmRI@nEQ!ypee@7r zmHZASkAAMQ$IYsWa)CMi_e%pl7Ny3e;(sEY$)YH*6Nr^elHe>IuiPr+2Nnghfr0pV v0%SMo1u{oL4l#A9lB!#kyZu-1VlnQq&o(Y9C8o_ zX+#7Bu6Ov3JQdy zXs+NW;_ZJWl0p>m!e43z2q5~~CjM0baO-~nF%{eZF7_{^g!6!*Mw1N=MQ=OvIqQf; zAae12Kp+_roRWm>|E2;V#6)lo5{ON-8o&_*1_*$mfD*Ggm{%nX$3EWpN$~?uIEvT? z>b9;n8(k(8(AJpfS!_5o>ol3}NtNYGo593tDP2S^6*>Nk1k7T@TW3i$Zf0M|3|L`8&a)D{=#p!$Y z!k{*-s-XRjHM@oK1(%a5D@ULzl@;U18aoXlNM{qXja;rz$%u1^=Hu5lN!^+DZs7;E zIOn7F#%?UE3aZy13Ja#`#C{XH_fY9=1gIent?;!^wdUST(jtSf<vc0XCi30w<7)Kn=dcVm^u#DU8&z}r!4x}NP-4e#?Q+@E(n z2eHpy^{mnR%(s=O!*JX94PZ30ZXCm4rW2`YTiz*oEJ%OgZVdM_=W*lY1pQX0v zcseI1`B|2S2x)Q6LEX|u;wv1;wqta=Lf!XO1Ok5WYjf0?;ix&KIkpb6epA!sUwZ13 zdX29%>cV`sSp)L{$1(^cENC->yYVUT3mRWV?_ON^)m_HN#a^lBb~tp=l52c@{qyE>8T7kQ8S-S!Iz!>03ce^0q#C>^9z};XsuV;pWaaaUghEE+FW%}p9eg4 zl(jVmic;LU0uSh$wu6qOiK#l<1dm(M=_#RxH@`Y>i1-;T{YluCD9E5NKdV`$c9mt- zLyUf;V)Kg@qr88sRFQMc$!Wiaa1cFaC12}zLB`A?jn2$Zw$2T;ttk58YVnbA4~BKP zIwvJ9y;Hx4lnYYl|uVnvF!V2M79>6urEG3n{8ZQ*!F+IguL@tL8fW8T;8XRT3Bt(ToiSQq2 zZOEE;vwl`eS4Q02xt)6$@GgH4`R*M2@nL2$VGg2QT5{= z7O}?a3hOPhvy8eQ?J_$c{b4CBKV^R;WGYC%`n0atsEy+~Q(e>QjP`DT;RLM~u0>la z(VSXWCmkJ2<`VWP3(xr3Sf6cyC7txxf7d*ua9yeI&r7i?<||Kc-Pfp$vbm~Tep{AD zV(ycft>Y2IksXQeYHEnzEMpSVN3}HEKD%CD)3Pv(6S@7fYovs_pk8F2^XDCw%%~^aa>107_#8YiWzwYy9GuX zmcL*Pi08fC2_R%whR877(|~}qxj;C@c!a+f%{v;3QW~p6y1Doil`-A-P7z1B)d5Ha z5EZSt>B+SBv<2&Mne}oibB&5+MyyR~0n?PT8Bd}n6%jd|b1^|(grJ$6Dq&I0|y|w+=}t{{Kc$ zk^_K$#iqYx{r@ulhFC<-Cm}wnVg_VLA_I}<|1^G|I~T}{ZT~2K-oE2c4YLA&H_!`M zd8sS>-0{G~-MpNhJ7KN(i)7>+D^upT zYPcd^A!=(NpA(d5(!2tzIpUB$-~4XXkaSQ(jR=2D%w<1}9=KHKw(}u*ddk4fbgW}a zqE8$%ppXv+Kb53FdO6nXMp|26#+Vi5Sez%nn%_Ze+0wPiC`J8&YIpkNU7dV#5cGZ4 zd~Vh3>RrNZYgSQ#xSK0duM(w)ozRu}pi@W2z(X1RoM8nzmRX{IjK^Qq^r;PeFLAUs z4n8bZta7Fq(4_7b;a5PZ;yw^Mf3}kmgQfNYj5pg~*pGp6fs>e7alU%Z;=;uq;t+B&7yBcIm=ALKpAmE8D}*FC})% zTJkh!;Dk*(|C#W57YH9mN<-b&=bz%%sXe$Tvr@A^~qL$@5gx8euypS z-DkyZiFuXJ!v2gd3`5}f#N;>(!-)&Mk&J8gs0@pvZVXt{kS2F1;dX07Qp=UBwK5U! z*6OSglYeAUuI8)HJ2BOXGMI!=f5DU-?la=^Bt=p5_QT_C1xaFy_IRsUT@&o>l=*f8 z1XTdX^fCH=Oe$#R0NG}Y{+6!_%cEDNzV2egzvnykYBgO8yjw%vt$%-An6Sv2R^A*d zCeFPy^0aQEDNGz(+Q)7fHL>FxPt+YsJk8jhHS}APp&a~OD)|GySc%MuBz}E50A!U` z$#|UWvp^>YtX2!IxLBs4uzLYT7J8Cu-Z9Sc9t+Q<*?m1+&{w{`044Q|(s%rc*$c>A zK?ZY16p+~~>~N{jImfWdUpIgIHRCF4W@ovkTwrL+hA*PJTc$L(IB|P_8^|3z+@%h) zlCYPcrskc|rb8&TIVGiVKxWZT2^dT^&yUwsZ)Qm1rvom8eYf5Gm0c*r)n#WG?kW8~ z#?ZZo6)h?dT%{LDr`k-eow?34q9wE-9A#49gQ|@HLyCT_V-idnPeR7>vPgo}YXjge z?&`Ts5)mp#`@l^Ku$R!b@aXoTRfjHqw&6x$F#0{@nLA5;cu@=JwNqSd&-3> zI9gU7_1NB3)hUH+ggrt4oN$xZ=y!xZ5`U9Bx1HOPln%3L@ErP zezb(kiv)j+F=U4~y$waEtW!<(BM(vD7HmwfIE|u`W{>@bdH127NoS6UhJ^SrJUz}^ zCOX^w$~pDWW*#f;pZ6nL*yjYS=|ZIRm%NX3aRhEJA~0wB5&*o z`%}>RBGw>c)X+-g51+;;S#7nXmq1Gt-Lws!cIqCgGUJ^JHio{mM84g98pS0v#Jn`q z?`mm>l9}zgYeG;&0+?F71Vp83^B75*tVX6qMa3%`@`B%_W|mRuf2n&X${FmvPI>y% zCvCDP3u&y@%M0oWe{AqPFd~hvwf(o*Vc?0cz8rQjs(Ox=9;A|*9-!TWSfeYL&Adko z*;H26+=X?7lSiLziZ&#>cM_Gaeq73UTOh82E~TEjCofmLe3JO_cB}i|7_vhrxK|IW z;&c=8{`aGnQ}el0PNlajZ)gbx(kd|v8@Sv^?*qR~3ADeKUE-@9+|rr*`<0ovUXqRv zDPl@jjr;n%YO#-LmX1AB+||{ne@qZ>T>CB)bmB*&%x_jA-XDYMB5&;W3MluVO;MaVLR+2ek*}Tr#W$JTW?5469~0!r=X+K+ z<=tlcYByaH@(LA}Cx86xr}+~7;8?0bmPeDVnCSe`y>T=wjIPQ#0I_O4IXH>D!q5Nq^>kKanAH>Z^%zdbcz%b-5rxE23$n4D$YUcrNYSgLac zbaYZZ>emQ*KXG&sh&faC{t3OA-ogOOTq%^P&{%ZUXqnJDN;B;;On2Wjz^=u4Uz%YC z)7L2$CpewquqlV#>N;Bl(=&-OKI{!U&3DIC79-LRtX{a4OPtX0a&sz^NBd&oMBoL&qTZz(LvLuszA;OZ0Af^(^aIv%^c@d$rlmCCohZ9qAJ zR%$a~3J{jbufd$kT3Bf7y&O(r7a;nYD>qg~JFluNM3@%~1(tGoxbz6!Z}a#?se4~) z&X!ZPu5OZ(a7nYizY}WN4JsK~Vs8&c>ZpeSUpmT3RdJQS>1elHf8a4L7gXp;|7*dt zmQ}d6tW?R=2U+Yp;P(bVD{~Y@WS(y(#P_U@7hBZGg_(HyFhea%QH&SP{ZG(?Fp4pO z8NMNPpRZ#h=yOVHhYmKTvHG=Isc;3j7@X>#w;Tu{CIa6gtIwDvZmp7uG{A8*nC7u^ gS(8*}6{v#btuKpsyio37vPl1#YXpev^1o5<{9 literal 0 HcmV?d00001 diff --git a/tests/assets/client-certificates/client/trusted/csr.pem b/tests/assets/client-certificates/client/trusted/csr.pem index 8ead6da3d..fc930ea7c 100644 --- a/tests/assets/client-certificates/client/trusted/csr.pem +++ b/tests/assets/client-certificates/client/trusted/csr.pem @@ -1,26 +1,26 @@ -----BEGIN CERTIFICATE REQUEST----- MIIEVTCCAj0CAQAwEDEOMAwGA1UEAwwFQWxpY2UwggIiMA0GCSqGSIb3DQEBAQUA -A4ICDwAwggIKAoICAQCac3+4rNmH4/N1s4HqR2X168tgS/aA6sHW5at8mWRnq54N -m11RvnK55jHQYVAdBgJy5M07w0wakp8inxzlY95wqxBimYG63Un/1p7mX9FkB4LN -ISCc6j/s/Ufv85MXPbn0S5rm9UcQO9cINJb1RP1YgDDLN5cxMz6X4nyofN8H6Lhv -h4JDdBw4DfDEFERkVfF+bkZ7YW4XHEChgzm3RxCF0eeGzIXGrkkK9AsSdJAhOvTl -HPFCQKXTYZhsL5+3Ma4RnWnDWvLTHx6KzoU+twTM2mYhhQuQgQpnmDHxGge8kGeH -GtfdgAjtVJTE57xF/shP0JU+tuIV8NNhQ/vEmhL0Wa093/EvpTVp0EUEuDh9ORRH -5K5M4bKJyU4XX5noiht6yOn00uaoJcWduUAWsU+cDSvDTMw81opWWm0QIAV3G2yu -RSkumHAKqvQLeyeyiKz+OEhyEiZ7EZNExPD0TSpApSTU6aCTUAvPYGQ59VjsMHTu -J9r4wKIYaDvfL+t72vg2vTQma5cTOBJfIdxH9blFTjEnToH3LX8t0XndQ2RkiRnI -ze2p2jUShxo/lWCjCw+2Iaw0A0fNUK1BbOrFRPq1u7AnEuMJt7HF50MloItM97R9 -vofDwgDIzlX/PzlVRcn1WCo8Fr/0EXxPPreX0YDIp1ANQ8fSv7bKb2vQIxWuCQID -AQABoAAwDQYJKoZIhvcNAQELBQADggIBAGgf3EC8WL3RGmuGA+d/4wd1jNfrfU6n -xjnDwdEEX0TQZGGPjh5xvoCK76yZPkO6+z0IYSepEmWBS27HJKl7nuoOvS7MjQyJ -C+3Bdk3ToCeQjmNBlRBKsUw5ftTU902oMl5BptHGj1KGjYBLAkPdXb44wXSVKJ8q -ihFhWlovsva6GDoUorksU3vOwijdlGzTANQHJGFncgrRud9ATavpGS3KVxR73R3A -aBbu3Qw+QIfu8Qx5eBJp8CbMrpAmjfuq17STvqr5bC10Fnn4NegrnHOQG9JcK02+ -5Bn3+9X/n1mue7aohIdErLEiDMSqMOwFfrJeaH6YM1G4QkWyqGugtmHsWOUf0nlU -nkH1krvfw9rb6b+03c4A6GSeHnbX5ufFDSf5gaR6Wy7c0jBnoxVbtBLH2zXlrd0k -iRQG7C6XZzGMS7hb7GL7+bkRy9kWjmDL7z7Fp+EgzKhNmzuWII3E9X9va33HoQ/Q -UdK3JVToxRQg6XRKOxL9+U/+8i6U8lxObLWkWh2cypZqbz5qJxa+2u5JYO/KEoHZ -G963UX7XWezR98vZuTc1XHGZtBDMrjjDd7Kmb4/i/xBPeWwseeGtzFy9z2pnEnkL -uKE4C8wUNpzUUlsn4LneZXObIoErE7FqAAlVFujVe7iaJBmXoUXZR36drbfiaODK -vwAGyrYHaOlR +A4ICDwAwggIKAoICAQC4ggn74iAHLgOWOiOvB2CPe+Hr7W6STYJLZOoqPdh7mv7Q +Gm8cYxfD+26p13aEaW4/qn45losWdEPPy2ZiVIF+kcOP0R4AqsB0w9UHT4WSzCWt +Dqs8ywDMJ6tHge0++8S1bTpdutn/m8DPnKtkD9RQUzLFmGDO+mB08Xu+egTzJvUR +bHXRJ27E+CXUXLEHbAJd8EKjJiQYXhcj4lzUXOUg+xkpPAGe1dgQ6BDkv3xq/81S +9NTIT5YriHEm6egi9AFJLZbbZtCpRQm1MDMq7zfD7oL6ECLGrJ/aaxLEM49gMdw2 +SscYHotVxX2OMAKgN/ytB18L/mIQ4pOWp3HJQ+nks9Zu1V8cg7Pz5pWwjqoncbyU +aBi3vGsitAot3cyLXbN9hH8zQB8QqGyNLvqQnCJOBlYxOPA8M8NQGDThWKspcix0 +LhkmnYsWWt3+KEGEpFRcJCEthm/DAd1GImP7/dg1/btT/zOsIIkIoCyBwznuR4M4 +XoNTbIBsiTnO/6PjOdTrjLVX7vMueTjdbCgK3VeOuwetbZ7aZ/hvMJCjJ4wuM3l2 +ZyfEQlP4gJLJS3Fw2PRsySZJl9JyEzwTqYVosDF5SF8uw0HIcFaliZCqBbgYfOiQ +zjIX/GiHZdor1ZrhAbO/MpSxuxGzxF7Em4n+4p816NnC9S2JbHRetkM9P1adHwID +AQABoAAwDQYJKoZIhvcNAQELBQADggIBACpScaAoLAs9DuTcI5Y4dbHf7LwF4Zxx +UgPNzE1HB1Pr6NaHRiOWrlCtDEB5UwHrr0oZuRTqSEGg3Pe5Z2QtPG/sdFgfm4BP +29o6qo0CXiEVBwU7/K0/lL2/0a7uSbD0Tkw9d6Bgik7/Z5rzXZIi2vtUcrOtHskP +C+Z9w3vH9a+RnUeo52mCnRi4SaiSEnD5jvhmgaI9iF+k5pYBiMrKRj5W/F1QCkf4 +7OuSK97xN0eG/I4Oxgzi/qt51ySCYZbqoh7dIpwi/a4UsK8kdzDDI1M3J7bU07cO +CJRfr0EETqCQw/gAKoag3tRFNvWQB6Z9G3Ev5jeaCLpcc32NlpN5xH3VW8A0Zb81 +dn5BXkPSxjwJaD0a3cLFkfgrasoe7ZMmHrVpQDw+9USuGCYXMPzNZLEeTFbrRkUn +sqi30e28E1H69zVWj+OKzCWEH/azVlfaoVbwM+njUJDe5V09KvFtI7aZYmvLxbUX +4ifoRUVoKedyKnueVmoIG57lF2VzeEhX5YjCngxIg+YuE99HkMQAZSlS6uJcVM92 +tsC/+pYECBk8ukenbxmKXROl3u4p2M1iCSL/8EOVROuyjnuzCXJZOpNptdpX4ZgL +kHP1erq7/U8ZU8HviUsfMoisagx8dA8uj/4fk0jfNxOJqlZL9eJhpgBfYHqmAz3h +m+PQVw96eeoK -----END CERTIFICATE REQUEST----- diff --git a/tests/assets/client-certificates/client/trusted/key.pem b/tests/assets/client-certificates/client/trusted/key.pem index d60201e5a..a11cdc8a7 100644 --- a/tests/assets/client-certificates/client/trusted/key.pem +++ b/tests/assets/client-certificates/client/trusted/key.pem @@ -1,52 +1,52 @@ -----BEGIN PRIVATE KEY----- -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 -Y+eu85cike5J4vnz8uG04yt7rXjIrUav +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 +21eLmxOuln98Q3na7ftpAWQdyjcE6Q== -----END PRIVATE KEY----- diff --git a/tests/assets/client-certificates/generate.sh b/tests/assets/client-certificates/generate.sh new file mode 100755 index 000000000..f835f5f32 --- /dev/null +++ b/tests/assets/client-certificates/generate.sh @@ -0,0 +1,88 @@ +#!/bin/bash + +# Client Certificate test-certificates + +cd "$(dirname "$0")" + +## Server + +openssl req \ + -x509 \ + -newkey rsa:4096 \ + -keyout server/server_key.pem \ + -out server/server_cert.pem \ + -nodes \ + -days 3650 \ + -subj "/CN=localhost/O=Client\ Certificate\ Demo" \ + -addext "subjectAltName=DNS:localhost,DNS:local.playwright" + +## Trusted client-certificate (server signed/valid) + +mkdir -p client/trusted +# generate server-signed (valid) certificate +openssl req \ + -newkey rsa:4096 \ + -keyout client/trusted/key.pem \ + -out client/trusted/csr.pem \ + -nodes \ + -days 3650 \ + -subj "/CN=Alice" + +# sign with server_cert.pem +openssl x509 \ + -req \ + -in client/trusted/csr.pem \ + -CA server/server_cert.pem \ + -CAkey server/server_key.pem \ + -out client/trusted/cert.pem \ + -set_serial 01 \ + -days 3650 +# create pfx +openssl pkcs12 -export -out client/trusted/cert.pfx -inkey client/trusted/key.pem -in client/trusted/cert.pem -passout pass:secure + +## Trusted certificate for localhost (server signed/valid) + +mkdir -p client/localhost + +# generate server-signed (valid) certificate +openssl req \ + -newkey rsa:4096 \ + -keyout client/localhost/localhost.key \ + -out client/localhost/localhost.csr \ + -nodes \ + -days 3650 \ + -subj "/CN=localhost" \ + -addext "subjectAltName=DNS:localhost,DNS:127.0.0.1" + +# put extensions +echo "subjectAltName=DNS:localhost,DNS:127.0.0.1" > client/localhost/localhost.ext + +# sign with server_cert.pem +openssl x509 \ + -req \ + -in client/localhost/localhost.csr \ + -CA server/server_cert.pem \ + -CAkey server/server_key.pem \ + -set_serial 01 \ + -out client/localhost/localhost.pem \ + -days 3650 \ + -extfile client/localhost/localhost.ext + +## Self-signed certificate (invalid) + +mkdir -p client/self-signed +openssl req \ + -newkey rsa:4096 \ + -keyout client/self-signed/key.pem \ + -out client/self-signed/csr.pem \ + -nodes \ + -days 3650 \ + -subj "/CN=Bob" + +# sign with self-signed/key.pem +openssl x509 \ + -req \ + -in client/self-signed/csr.pem \ + -signkey client/self-signed/key.pem \ + -out client/self-signed/cert.pem \ + -days 3650 diff --git a/tests/assets/client-certificates/server/server_cert.pem b/tests/assets/client-certificates/server/server_cert.pem index 52d8f5314..18ee3d805 100644 --- a/tests/assets/client-certificates/server/server_cert.pem +++ b/tests/assets/client-certificates/server/server_cert.pem @@ -1,32 +1,32 @@ -----BEGIN CERTIFICATE----- -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 -2Ct10prMWB/3 +cmlnaHQwDQYJKoZIhvcNAQELBQADggIBAH1/6Sp4dW96yvwi9ptgVRYfRSRWfYYy +2nU6kJ1DPOW7hTPf2wLf6Z2KqiJXn8tECHfM4pnPSgDhtZHDDHAu9l7Diqzk/NIl +AblRs++4vSKdnCx1uh3EFLjIMmHa/cRUzfuR+oGfri3v7jgTBV6UJJQ0pMqIDk/P +VEOWWukllru40M8Rwy4F6LmpwIWMtNDxmhtXSbQJ6TZn+isNSoaWgHjQmayrVyXZ +OcNYuR8M/ECvnufuIOW53+hhwG1X0jEoBdXqqXbTKcGjA+yLHp008AAB5DNQblYm +FJ4N0v6eYLhOO0u3n0b/ZsqUkZx2h38tlZoAdJNqy+d4TE3WbGRNgNJrTjAOzpPL +cQ5RNr3dDsUDFPnCoK3LUf9BWerARoDt+bbrM/VqvWH/0uqaU0/vo8IchSyQ9wGv +SwrWLJU32HQUJ2VQP/m4ADf3X2Ozj+e8bBf7XPPD8KDyL4aR0KuYzrsOnhFlBkHD +PtNjc7SgiE7AZe1WOEmhBQcr+vI6S8qFTurTntCBkvrFgYBAIIhK2XwFN5sFtByr +GCB67NQMp37g2qFNYi8EmKQvPLpAcC3Je5PvHFSj2y9Z14FclGoxjU50SlH9/A5I +iHGcIVjCv9NhNDFAwakF/sTnrpT/FIS7tm9GwoJGWCdYg4pl72WYCW6hJBG+XY7G +t3jt9TP8623D -----END CERTIFICATE----- diff --git a/tests/assets/client-certificates/server/server_key.pem b/tests/assets/client-certificates/server/server_key.pem index ff6a3fc11..3a762bde7 100644 --- a/tests/assets/client-certificates/server/server_key.pem +++ b/tests/assets/client-certificates/server/server_key.pem @@ -1,52 +1,52 @@ -----BEGIN PRIVATE KEY----- -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 -8ynLaTs3oV3BjQya9pAUyzIX5L67 +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 +dvQgA6dBjtYg5cQMHNbnfT3j3WzyQg== -----END PRIVATE KEY----- From 6dd5aa23c2cff964652e534532e93098119ff678 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 22 Jul 2025 16:54:59 +0200 Subject: [PATCH 17/79] chore: update pre-commit / linters (#2925) --- .pre-commit-config.yaml | 12 ++++++------ local-requirements.txt | 2 +- playwright/_impl/_clock.py | 2 +- playwright/_impl/_set_input_files_helpers.py | 2 +- tests/async/test_page.py | 3 +-- tests/async/test_page_add_locator_handler.py | 1 - tests/async/test_worker.py | 2 +- tests/common/test_signals.py | 1 - tests/sync/test_page_add_locator_handler.py | 1 - 9 files changed, 11 insertions(+), 15 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5c8c8f1db..57fdca816 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,20 +15,20 @@ repos: - id: check-executables-have-shebangs - id: check-merge-conflict - repo: https://github.com/psf/black - rev: 24.8.0 + rev: 25.1.0 hooks: - id: black - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.11.2 + rev: v1.17.0 hooks: - id: mypy - additional_dependencies: [types-pyOpenSSL==24.1.0.20240722, types-requests==2.32.0.20240914] + additional_dependencies: [types-pyOpenSSL==24.1.0.20240722, types-requests==2.32.4.20250611] - repo: https://github.com/pycqa/flake8 - rev: 7.1.1 + rev: 7.3.0 hooks: - id: flake8 - repo: https://github.com/pycqa/isort - rev: 5.13.2 + rev: 6.0.1 hooks: - id: isort - repo: local @@ -39,7 +39,7 @@ repos: language: node pass_filenames: false types: [python] - additional_dependencies: ["pyright@1.1.384"] + additional_dependencies: ["pyright@1.1.403"] - repo: local hooks: - id: check-license-header diff --git a/local-requirements.txt b/local-requirements.txt index e19488956..28f60ba65 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -2,7 +2,7 @@ autobahn==23.1.2 black==25.1.0 build==1.2.2.post1 flake8==7.2.0 -mypy==1.16.0 +mypy==1.17.0 objgraph==3.6.2 Pillow==11.2.1 pixelmatch==0.3.0 diff --git a/playwright/_impl/_clock.py b/playwright/_impl/_clock.py index 928536019..f6eb7c42d 100644 --- a/playwright/_impl/_clock.py +++ b/playwright/_impl/_clock.py @@ -89,7 +89,7 @@ async def set_system_time( def parse_time( - time: Union[float, str, datetime.datetime] + time: Union[float, str, datetime.datetime], ) -> Dict[str, Union[int, str]]: if isinstance(time, (float, int)): return {"timeNumber": int(time * 1_000)} diff --git a/playwright/_impl/_set_input_files_helpers.py b/playwright/_impl/_set_input_files_helpers.py index f868886a3..0f40d5b99 100644 --- a/playwright/_impl/_set_input_files_helpers.py +++ b/playwright/_impl/_set_input_files_helpers.py @@ -139,7 +139,7 @@ async def convert_input_files( def resolve_paths_and_directory_for_input_files( - items: Sequence[Union[str, Path]] + items: Sequence[Union[str, Path]], ) -> Tuple[Optional[List[str]], Optional[str]]: local_paths: Optional[List[str]] = None local_directory: Optional[str] = None diff --git a/tests/async/test_page.py b/tests/async/test_page.py index 03907c4b9..607c86fb3 100644 --- a/tests/async/test_page.py +++ b/tests/async/test_page.py @@ -662,7 +662,7 @@ async def load() -> None: async def test_set_content_should_work_with_tricky_content(page: Page) -> None: - await page.set_content("
hello world
" + "\x7F") + await page.set_content("
hello world
" + "\x7f") assert await page.eval_on_selector("div", "div => div.textContent") == "hello world" @@ -1403,7 +1403,6 @@ async def test_should_not_throw_when_continuing_after_page_is_closed( async def handle_route(route: Route) -> None: await page.close() await route.continue_() - nonlocal done done.set_result(True) await page.route("**/*", handle_route) diff --git a/tests/async/test_page_add_locator_handler.py b/tests/async/test_page_add_locator_handler.py index 4a5a44323..5f44170f8 100644 --- a/tests/async/test_page_add_locator_handler.py +++ b/tests/async/test_page_add_locator_handler.py @@ -30,7 +30,6 @@ async def test_should_work(page: Page, server: Server) -> None: original_locator = page.get_by_text("This interstitial covers the button") async def handler(locator: Locator) -> None: - nonlocal original_locator assert locator == original_locator nonlocal before_count nonlocal after_count diff --git a/tests/async/test_worker.py b/tests/async/test_worker.py index de1a858e8..bba22fc0d 100644 --- a/tests/async/test_worker.py +++ b/tests/async/test_worker.py @@ -199,5 +199,5 @@ async def test_workers_should_format_number_using_context_locale( "() => new Worker(URL.createObjectURL(new Blob(['console.log(1)'], {type: 'application/javascript'})))" ) worker = await worker_info.value - assert await worker.evaluate("() => (10000.20).toLocaleString()") == "10\u00A0000,2" + assert await worker.evaluate("() => (10000.20).toLocaleString()") == "10\u00a0000,2" await context.close() diff --git a/tests/common/test_signals.py b/tests/common/test_signals.py index 174eaf6f2..ba600b44d 100644 --- a/tests/common/test_signals.py +++ b/tests/common/test_signals.py @@ -50,7 +50,6 @@ async def main() -> None: page = await context.new_page() notified = False try: - nonlocal sigint_received while not sigint_received: if not notified: wait_queue.put("ready") diff --git a/tests/sync/test_page_add_locator_handler.py b/tests/sync/test_page_add_locator_handler.py index b2d037f07..7a2b6a438 100644 --- a/tests/sync/test_page_add_locator_handler.py +++ b/tests/sync/test_page_add_locator_handler.py @@ -29,7 +29,6 @@ def test_should_work(page: Page, server: Server) -> None: original_locator = page.get_by_text("This interstitial covers the button") def handler(locator: Locator) -> None: - nonlocal original_locator assert locator == original_locator nonlocal before_count nonlocal after_count From 2b75be585dac7f0ed762a37e48df3b8296527f9e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 23:04:07 +0200 Subject: [PATCH 18/79] build(deps): bump pytest-asyncio from 1.0.0 to 1.1.0 (#2932) 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 28f60ba65..64544740f 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -9,7 +9,7 @@ pixelmatch==0.3.0 pre-commit==3.5.0 pyOpenSSL==25.1.0 pytest==8.4.1 -pytest-asyncio==1.0.0 +pytest-asyncio==1.1.0 pytest-cov==6.2.1 pytest-repeat==0.9.4 pytest-rerunfailures==15.1 From a74efb8e1ce5a60480a48f63a8a6d76dd3d55fb7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Aug 2025 14:04:39 +0200 Subject: [PATCH 19/79] build(deps): bump pytest-xdist from 3.7.0 to 3.8.0 (#2916) 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 64544740f..de20c0daa 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -14,7 +14,7 @@ pytest-cov==6.2.1 pytest-repeat==0.9.4 pytest-rerunfailures==15.1 pytest-timeout==2.4.0 -pytest-xdist==3.7.0 +pytest-xdist==3.8.0 requests==2.32.4 service_identity==24.2.0 twisted==25.5.0 From 8ad65d414aee14f5445ad1b7023f92aedc322700 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Aug 2025 09:44:39 +0200 Subject: [PATCH 20/79] build(deps): bump mypy from 1.17.0 to 1.17.1 (#2933) 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 de20c0daa..d9f834c3f 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -2,7 +2,7 @@ autobahn==23.1.2 black==25.1.0 build==1.2.2.post1 flake8==7.2.0 -mypy==1.17.0 +mypy==1.17.1 objgraph==3.6.2 Pillow==11.2.1 pixelmatch==0.3.0 From 7dc8b32b9c34c43620e0c7f19cc08b71410aff22 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 5 Aug 2025 10:46:40 +0200 Subject: [PATCH 21/79] test: unflake dialog inline script test (#2934) --- tests/sync/test_browsercontext_events.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/sync/test_browsercontext_events.py b/tests/sync/test_browsercontext_events.py index 6e44b76d5..a100fb04a 100644 --- a/tests/sync/test_browsercontext_events.py +++ b/tests/sync/test_browsercontext_events.py @@ -172,23 +172,25 @@ def test_dialog_event_should_work_with_inline_script_tag( ) -> None: def handle_route(request: TestServerRequest) -> None: request.setHeader("content-type", "text/html") - request.write(b"""""") + request.write(b"") request.finish() server.set_route("/popup.html", handle_route) page.goto(server.EMPTY_PAGE) page.set_content("Click me") - def handle_dialog(dialog: Dialog) -> None: - assert dialog.message == "hey?" - assert dialog.page == popup - dialog.accept("hello") + with ( + page.context.expect_event("dialog") as dialog_info, + page.expect_popup() as popup_info, + ): + page.click("a") - page.context.on("dialog", handle_dialog) + dialog: Dialog = dialog_info.value + popup: Page = popup_info.value - with page.expect_popup() as popup_info: - page.click("a") - popup = popup_info.value + assert dialog.message == "hey?" + assert dialog.page == popup + dialog.accept("hello") assert popup.evaluate("window.result") == "hello" From 3fea01e81a33f1ef31e271bdda78d398f18e87be Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 5 Aug 2025 11:32:51 +0200 Subject: [PATCH 22/79] test: unflake test_context_add_cookies_should_work (#2935) --- tests/async/test_defaultbrowsercontext.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/async/test_defaultbrowsercontext.py b/tests/async/test_defaultbrowsercontext.py index 25ef0c3f8..f5d2a606d 100644 --- a/tests/async/test_defaultbrowsercontext.py +++ b/tests/async/test_defaultbrowsercontext.py @@ -111,7 +111,7 @@ async def test_context_add_cookies_should_work( ] ) assert await page.evaluate("() => document.cookie") == "username=John Doe" - assert await page.context.cookies() == [ + assert _filter_cookies(await page.context.cookies()) == [ { "name": "username", "value": "John Doe", @@ -127,7 +127,7 @@ async def test_context_add_cookies_should_work( def _filter_cookies(cookies: Sequence[Cookie]) -> List[Cookie]: return list( - filter(lambda cookie: cookie["domain"] != "copilot.microsoft.com", cookies) + filter(lambda cookie: not cookie["domain"].endswith("microsoft.com"), cookies) ) From 8963460fffe065bfeb58a162f6e35fc8e3e65d4d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Aug 2025 13:37:31 +0200 Subject: [PATCH 23/79] build(deps): bump build from 1.2.2.post1 to 1.3.0 (#2937) 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 d9f834c3f..8c214a014 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -1,6 +1,6 @@ autobahn==23.1.2 black==25.1.0 -build==1.2.2.post1 +build==1.3.0 flake8==7.2.0 mypy==1.17.1 objgraph==3.6.2 From 5cc395e9214b5ebb32ca8629d6905e53824978b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 10:35:18 +0200 Subject: [PATCH 24/79] build(deps): bump actions/checkout from 4 to 5 in the actions group (#2940) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 10 +++++----- .github/workflows/publish.yml | 2 +- .github/workflows/publish_docker.yml | 2 +- .github/workflows/test_docker.yml | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c18a04bc6..200b2a65a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: name: Lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python uses: actions/setup-python@v5 with: @@ -80,7 +80,7 @@ jobs: browser: chromium runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python uses: actions/setup-python@v5 with: @@ -127,7 +127,7 @@ jobs: browser-channel: msedge runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python uses: actions/setup-python@v5 with: @@ -163,7 +163,7 @@ jobs: os: [ubuntu-22.04, macos-13, windows-2022] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Get conda @@ -184,7 +184,7 @@ jobs: run: working-directory: examples/todomvc/ steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b682372fd..c6e71028a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -25,7 +25,7 @@ jobs: # Required for conda-incubator/setup-miniconda@v3 shell: bash -el {0} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Get conda diff --git a/.github/workflows/publish_docker.yml b/.github/workflows/publish_docker.yml index 7d83136bc..7c2b73e13 100644 --- a/.github/workflows/publish_docker.yml +++ b/.github/workflows/publish_docker.yml @@ -15,7 +15,7 @@ jobs: contents: read # This is required for actions/checkout to succeed environment: Docker steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Azure login uses: azure/login@v2 with: diff --git a/.github/workflows/test_docker.yml b/.github/workflows/test_docker.yml index c1f2be3de..e5252e389 100644 --- a/.github/workflows/test_docker.yml +++ b/.github/workflows/test_docker.yml @@ -30,7 +30,7 @@ jobs: - ubuntu-24.04 - ubuntu-24.04-arm steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python uses: actions/setup-python@v5 with: From d33a807492c0a944186bc37dd5d71d57e1c36901 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 10:35:43 +0200 Subject: [PATCH 25/79] build(deps): bump greenlet from 3.2.3 to 3.2.4 (#2939) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 14dc0500a..75d362fe8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # This file was autogenerated by uv via the following command: # uv pip compile pyproject.toml -o requirements.txt -greenlet==3.2.3 +greenlet==3.2.4 # via playwright (pyproject.toml) pyee==13.0.0 # via playwright (pyproject.toml) From e49496e7a803b2fd05e5a7e814db4c0af8962743 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 14:03:42 +0200 Subject: [PATCH 26/79] build(deps): bump types-requests from 2.32.4.20250611 to 2.32.4.20250809 (#2941) 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 8c214a014..2f4f0d488 100644 --- a/local-requirements.txt +++ b/local-requirements.txt @@ -19,4 +19,4 @@ requests==2.32.4 service_identity==24.2.0 twisted==25.5.0 types-pyOpenSSL==24.1.0.20240722 -types-requests==2.32.4.20250611 +types-requests==2.32.4.20250809 From 6e9ff7a80f02db4e0ccdb87efadbf543a0c3ceb9 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 12 Aug 2025 15:34:31 +0200 Subject: [PATCH 27/79] test: unflake cr tracing test (#2942) --- tests/async/test_chromium_tracing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/async/test_chromium_tracing.py b/tests/async/test_chromium_tracing.py index fd065efde..4cd796e0d 100644 --- a/tests/async/test_chromium_tracing.py +++ b/tests/async/test_chromium_tracing.py @@ -114,5 +114,6 @@ async def test_should_support_a_buffer_without_a_path( ) -> None: await browser.start_tracing(page=page, screenshots=True) await page.goto(server.PREFIX + "/grid.html") + await rafraf(page) trace = await browser.stop_tracing() assert "screenshot" in trace.decode() From 1be34f206e1b57cf94cbb295158f75860f744e9c Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Thu, 14 Aug 2025 15:25:41 +0200 Subject: [PATCH 28/79] fix: timeout option support with fetch api (#2947) --- playwright/_impl/_fetch.py | 1 + tests/async/test_fetch_global.py | 9 +++++++++ tests/async/test_page_request_intercept.py | 2 +- tests/sync/test_fetch_global.py | 9 +++++++++ tests/sync/test_page_request_intercept.py | 2 +- 5 files changed, 21 insertions(+), 2 deletions(-) diff --git a/playwright/_impl/_fetch.py b/playwright/_impl/_fetch.py index e4174ea27..50bf4ad4a 100644 --- a/playwright/_impl/_fetch.py +++ b/playwright/_impl/_fetch.py @@ -412,6 +412,7 @@ async def _inner_fetch( self._timeout_settings.timeout, { "url": url, + "timeout": timeout, "params": object_to_array(params) if isinstance(params, dict) else None, "encodedParams": params if isinstance(params, str) else None, "method": method, diff --git a/tests/async/test_fetch_global.py b/tests/async/test_fetch_global.py index e2a7678c5..1f7593194 100644 --- a/tests/async/test_fetch_global.py +++ b/tests/async/test_fetch_global.py @@ -89,6 +89,15 @@ async def test_should_support_global_timeout_option( await request.get(server.EMPTY_PAGE) +async def test_should_support_timeout_option_in_get_method( + playwright: Playwright, server: Server +) -> None: + request = await playwright.request.new_context() + server.set_route("/empty.html", lambda req: None) + with pytest.raises(Error, match="APIRequestContext.get: Timeout 123ms exceeded."): + await request.get(server.EMPTY_PAGE, timeout=123) + + async def test_should_propagate_extra_http_headers_with_redirects( playwright: Playwright, server: Server ) -> None: diff --git a/tests/async/test_page_request_intercept.py b/tests/async/test_page_request_intercept.py index dc8f7416a..a9859e87b 100644 --- a/tests/async/test_page_request_intercept.py +++ b/tests/async/test_page_request_intercept.py @@ -34,7 +34,7 @@ def _handler(request: TestServerRequest) -> None: async def handle(route: Route) -> None: with pytest.raises(Error) as error: await route.fetch(timeout=1000) - assert "Timeout 1000ms exceeded" in error.value.message + assert "Route.fetch: Timeout 1000ms exceeded." in error.value.message await page.route("**/*", lambda route: handle(route)) with pytest.raises(Error) as error: diff --git a/tests/sync/test_fetch_global.py b/tests/sync/test_fetch_global.py index bf3970c21..5cb3abc5b 100644 --- a/tests/sync/test_fetch_global.py +++ b/tests/sync/test_fetch_global.py @@ -71,6 +71,15 @@ def test_should_support_global_timeout_option( request.get(server.EMPTY_PAGE) +def test_should_support_timeout_option_in_get_method( + playwright: Playwright, server: Server +) -> None: + request = playwright.request.new_context() + server.set_route("/empty.html", lambda req: None) + with pytest.raises(Error, match="APIRequestContext.get: Timeout 123ms exceeded."): + request.get(server.EMPTY_PAGE, timeout=123) + + def test_should_propagate_extra_http_headers_with_redirects( playwright: Playwright, server: Server ) -> None: diff --git a/tests/sync/test_page_request_intercept.py b/tests/sync/test_page_request_intercept.py index 86cf21b63..1d3dd0f46 100644 --- a/tests/sync/test_page_request_intercept.py +++ b/tests/sync/test_page_request_intercept.py @@ -34,7 +34,7 @@ def _handle(request: TestServerRequest) -> None: def handle(route: Route) -> None: with pytest.raises(Error) as error: route.fetch(timeout=1000) - assert "Request timed out after 1000ms" in error.value.message + assert "Route.fetch: Timeout 1000ms exceeded." in error.value.message page.route("**/*", lambda route: handle(route)) with pytest.raises(Error) as error: From 254aabddf3a68c9385cc9e363d2b61fa4bba777d Mon Sep 17 00:00:00 2001 From: Xianghu Zhao Date: Tue, 19 Aug 2025 21:17:08 +0800 Subject: [PATCH 29/79] fix: screenshot type inferred from path file extension (#2951) --- playwright/_impl/_element_handle.py | 12 ++++++++++ playwright/_impl/_page.py | 4 +++- tests/async/test_screenshot.py | 34 +++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/playwright/_impl/_element_handle.py b/playwright/_impl/_element_handle.py index 88f1a7358..1561e19fc 100644 --- a/playwright/_impl/_element_handle.py +++ b/playwright/_impl/_element_handle.py @@ -13,6 +13,7 @@ # limitations under the License. import base64 +import mimetypes from pathlib import Path from typing import ( TYPE_CHECKING, @@ -323,6 +324,8 @@ async def screenshot( ) -> bytes: params = locals_to_params(locals()) if "path" in params: + if "type" not in params: + params["type"] = determine_screenshot_type(params["path"]) del params["path"] if "mask" in params: params["mask"] = list( @@ -450,3 +453,12 @@ def convert_select_option_values( elements = list(map(lambda e: e._channel, element)) return dict(options=options, elements=elements) + + +def determine_screenshot_type(path: Union[str, Path]) -> Literal["jpeg", "png"]: + mime_type, _ = mimetypes.guess_type(path) + if mime_type == "image/png": + return "png" + if mime_type == "image/jpeg": + return "jpeg" + raise Error(f'Unsupported screenshot mime type for path "{path}": {mime_type}') diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index a0fa4eec2..1019b2f6e 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -51,7 +51,7 @@ ) from playwright._impl._console_message import ConsoleMessage from playwright._impl._download import Download -from playwright._impl._element_handle import ElementHandle +from playwright._impl._element_handle import ElementHandle, determine_screenshot_type from playwright._impl._errors import Error, TargetClosedError, is_target_closed_error from playwright._impl._event_context_manager import EventContextManagerImpl from playwright._impl._file_chooser import FileChooser @@ -800,6 +800,8 @@ async def screenshot( ) -> bytes: params = locals_to_params(locals()) if "path" in params: + if "type" not in params: + params["type"] = determine_screenshot_type(params["path"]) del params["path"] if "mask" in params: params["mask"] = list( diff --git a/tests/async/test_screenshot.py b/tests/async/test_screenshot.py index 3cd536f96..36149225f 100644 --- a/tests/async/test_screenshot.py +++ b/tests/async/test_screenshot.py @@ -12,13 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. +from pathlib import Path from typing import Callable +from PIL import Image + from playwright.async_api import Page from tests.server import Server from tests.utils import must +def assert_image_file_format(path: Path, image_format: str) -> None: + with Image.open(path) as img: + assert img.format == image_format + + async def test_should_screenshot_with_mask( page: Page, server: Server, assert_to_be_golden: Callable[[bytes, str], None] ) -> None: @@ -43,3 +51,29 @@ async def test_should_screenshot_with_mask( ), "mask-should-work-with-element-handle.png", ) + + +async def test_should_infer_screenshot_type_from_path( + page: Page, tmp_path: Path +) -> None: + output_png_file = tmp_path / "foo.png" + await page.screenshot(path=output_png_file) + assert_image_file_format(output_png_file, "PNG") + + output_jpeg_file = tmp_path / "bar.jpeg" + await page.screenshot(path=output_jpeg_file) + assert_image_file_format(output_jpeg_file, "JPEG") + + output_jpg_file = tmp_path / "bar.jpg" + await page.screenshot(path=output_jpg_file) + assert_image_file_format(output_jpg_file, "JPEG") + + +async def test_should_screenshot_with_type_argument(page: Page, tmp_path: Path) -> None: + output_jpeg_with_png_extension = tmp_path / "foo_jpeg.png" + await page.screenshot(path=output_jpeg_with_png_extension, type="jpeg") + assert_image_file_format(output_jpeg_with_png_extension, "JPEG") + + output_png_with_jpeg_extension = tmp_path / "bar_png.jpeg" + await page.screenshot(path=output_png_with_jpeg_extension, type="png") + assert_image_file_format(output_png_with_jpeg_extension, "PNG") From 3cbe13e58a4a20b4b3aaa1afbdc69747a7c37933 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Thu, 21 Aug 2025 17:08:58 +0200 Subject: [PATCH 30/79] 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 31/79] 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 32/79] 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 33/79] 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 34/79] 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 35/79] 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 36/79] 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 37/79] 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 ,