From a78cea6248e767e12ff8127370460c7eaf6c5601 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Wed, 9 Jul 2025 08:48:05 -0700 Subject: [PATCH 01/12] Initial roll to 1.54.0 --- README.md | 6 ++-- playwright/_impl/_api_structures.py | 13 +++++++++ playwright/_impl/_browser_context.py | 6 ++-- playwright/_impl/_console_message.py | 23 ++++++++++++++-- playwright/async_api/__init__.py | 2 ++ playwright/async_api/_generated.py | 41 ++++++++++++++++++++-------- playwright/sync_api/__init__.py | 2 ++ playwright/sync_api/_generated.py | 41 ++++++++++++++++++++-------- scripts/generate_api.py | 2 +- setup.py | 2 +- 10 files changed, 107 insertions(+), 31 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..963f653bc 100644 --- a/playwright/_impl/_api_structures.py +++ b/playwright/_impl/_api_structures.py @@ -34,6 +34,18 @@ class Cookie(TypedDict, total=False): sameSite: Literal["Lax", "None", "Strict"] +class ContextCookie(TypedDict, total=False): + name: str + value: str + domain: str + path: str + expires: float + httpOnly: bool + secure: bool + sameSite: Literal["Lax", "None", "Strict"] + partitionKey: Optional[str] + + # TODO: We are waiting for PEP705 so SetCookieParam can be readonly and matches Cookie. class SetCookieParam(TypedDict, total=False): name: str @@ -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): diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py index 60b60c46e..9178f6587 100644 --- a/playwright/_impl/_browser_context.py +++ b/playwright/_impl/_browser_context.py @@ -32,7 +32,7 @@ ) from playwright._impl._api_structures import ( - Cookie, + ContextCookie, Geolocation, SetCookieParam, StorageState, @@ -332,7 +332,9 @@ async def new_page(self) -> Page: raise Error("Please use browser.new_context()") return from_channel(await self._channel.send("newPage", None)) - async def cookies(self, urls: Union[str, Sequence[str]] = None) -> List[Cookie]: + async def cookies( + self, urls: Union[str, Sequence[str]] = None + ) -> List[ContextCookie]: if urls is None: urls = [] if isinstance(urls, str): 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/async_api/__init__.py b/playwright/async_api/__init__.py index be918f53c..77109a629 100644 --- a/playwright/async_api/__init__.py +++ b/playwright/async_api/__init__.py @@ -68,6 +68,7 @@ ChromiumBrowserContext = BrowserContext +ContextCookie = playwright._impl._api_structures.ContextCookie Cookie = playwright._impl._api_structures.Cookie FilePayload = playwright._impl._api_structures.FilePayload FloatRect = playwright._impl._api_structures.FloatRect @@ -159,6 +160,7 @@ def __call__( "CDPSession", "ChromiumBrowserContext", "ConsoleMessage", + "ContextCookie", "Cookie", "Dialog", "Download", diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index 5f0af8bf0..be454661d 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -21,7 +21,7 @@ from playwright._impl._accessibility import Accessibility as AccessibilityImpl from playwright._impl._api_structures import ( ClientCertificate, - Cookie, + ContextCookie, FilePayload, FloatRect, Geolocation, @@ -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 ------- @@ -12776,7 +12794,7 @@ async def new_page(self) -> "Page": async def cookies( self, urls: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None - ) -> typing.List[Cookie]: + ) -> typing.List[ContextCookie]: """BrowserContext.cookies If no URLs are specified, this method returns all cookies. If URLs are specified, only cookies that affect those @@ -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..1d80f5c68 100644 --- a/playwright/sync_api/__init__.py +++ b/playwright/sync_api/__init__.py @@ -68,6 +68,7 @@ ChromiumBrowserContext = BrowserContext +ContextCookie = playwright._impl._api_structures.ContextCookie Cookie = playwright._impl._api_structures.Cookie FilePayload = playwright._impl._api_structures.FilePayload FloatRect = playwright._impl._api_structures.FloatRect @@ -158,6 +159,7 @@ def __call__( "CDPSession", "ChromiumBrowserContext", "ConsoleMessage", + "ContextCookie", "Cookie", "Dialog", "Download", diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py index 763df6de3..2f622f5c8 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -21,7 +21,7 @@ from playwright._impl._accessibility import Accessibility as AccessibilityImpl from playwright._impl._api_structures import ( ClientCertificate, - Cookie, + ContextCookie, FilePayload, FloatRect, Geolocation, @@ -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 ------- @@ -12798,7 +12816,7 @@ def new_page(self) -> "Page": def cookies( self, urls: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None - ) -> typing.List[Cookie]: + ) -> typing.List[ContextCookie]: """BrowserContext.cookies If no URLs are specified, this method returns all cookies. If URLs are specified, only cookies that affect those @@ -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/scripts/generate_api.py b/scripts/generate_api.py index 01f8f525a..8e8632fe3 100644 --- a/scripts/generate_api.py +++ b/scripts/generate_api.py @@ -225,7 +225,7 @@ def return_value(value: Any) -> List[str]: from playwright._impl._accessibility import Accessibility as AccessibilityImpl -from playwright._impl._api_structures import Cookie, SetCookieParam, FloatRect, FilePayload, Geolocation, HttpCredentials, PdfMargins, Position, ProxySettings, ResourceTiming, SourceLocation, StorageState, ClientCertificate, ViewportSize, RemoteAddr, SecurityDetails, RequestSizes, NameValue, TracingGroupLocation +from playwright._impl._api_structures import ContextCookie, SetCookieParam, FloatRect, FilePayload, Geolocation, HttpCredentials, PdfMargins, Position, ProxySettings, ResourceTiming, SourceLocation, StorageState, ClientCertificate, ViewportSize, RemoteAddr, SecurityDetails, RequestSizes, NameValue, TracingGroupLocation from playwright._impl._browser import Browser as BrowserImpl from playwright._impl._browser_context import BrowserContext as BrowserContextImpl from playwright._impl._browser_type import BrowserType as BrowserTypeImpl diff --git a/setup.py b/setup.py index fd590167f..d1017dcdf 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-alpha-2025-07-09" base_wheel_bundles = [ { From c475963a3cae0aafea0953c407152426bdb5815b Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Wed, 9 Jul 2025 11:24:50 -0700 Subject: [PATCH 02/12] Core roll changes --- playwright/_impl/_assertions.py | 26 ++++++++- playwright/_impl/_browser_context.py | 18 +++++-- playwright/_impl/_frame.py | 40 +++++++++++--- playwright/_impl/_helper.py | 10 ++++ playwright/_impl/_locator.py | 18 +------ playwright/_impl/_network.py | 11 ++-- playwright/_impl/_page.py | 31 +++++------ playwright/_impl/_selectors.py | 4 ++ tests/async/conftest.py | 2 +- tests/async/test_browsercontext.py | 4 +- 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_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 + 21 files changed, 190 insertions(+), 62 deletions(-) diff --git a/playwright/_impl/_assertions.py b/playwright/_impl/_assertions.py index 6e0161b7c..b0d390f04 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 9178f6587..f56315d42 100644 --- a/playwright/_impl/_browser_context.py +++ b/playwright/_impl/_browser_context.py @@ -119,6 +119,7 @@ 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._tracing = cast(Tracing, from_channel(initializer["tracing"])) self._har_recorders: Dict[str, HarRecordingMetadata] = {} self._request: APIRequestContext = from_channel(initializer["requestContext"]) @@ -302,6 +303,14 @@ def pages(self) -> List[Page]: def browser(self) -> Optional["Browser"]: return self._browser + @property + def base_url(self) -> Optional[str]: + return self._base_url + + @property + def videos_dir(self) -> Optional[str]: + return self._options.get("recordVideo") + async def _initialize_har_from_options( self, record_har_path: Optional[Union[Path, str]], @@ -426,7 +435,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, @@ -454,17 +463,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/_frame.py b/playwright/_impl/_frame.py index c0646b680..3a3f7d94c 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( @@ -801,7 +829,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..cbf875188 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, @@ -722,21 +722,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..1e42ae392 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/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_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_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 633b160587459caf923bab8c59215d7306010fb0 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Wed, 9 Jul 2025 12:21:09 -0700 Subject: [PATCH 03/12] Alignment fixes --- playwright/_impl/_browser_context.py | 9 +-------- playwright/_impl/_frame.py | 12 ++++++++++++ playwright/_impl/_locator.py | 3 ++- playwright/_impl/_page.py | 2 +- tests/async/test_chromium_tracing.py | 10 ++++++---- 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py index f56315d42..e1cdedb0e 100644 --- a/playwright/_impl/_browser_context.py +++ b/playwright/_impl/_browser_context.py @@ -120,6 +120,7 @@ def __init__( 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"]) @@ -303,14 +304,6 @@ def pages(self) -> List[Page]: def browser(self) -> Optional["Browser"]: return self._browser - @property - def base_url(self) -> Optional[str]: - return self._base_url - - @property - def videos_dir(self) -> Optional[str]: - return self._options.get("recordVideo") - async def _initialize_har_from_options( self, record_har_path: Optional[Union[Path, str]], diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py index 3a3f7d94c..fd107d314 100644 --- a/playwright/_impl/_frame.py +++ b/playwright/_impl/_frame.py @@ -586,6 +586,18 @@ async def fill( noWaitAfter: bool = None, strict: bool = None, force: bool = None, + ) -> None: + await self._fill(**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())) diff --git a/playwright/_impl/_locator.py b/playwright/_impl/_locator.py index cbf875188..a65b68266 100644 --- a/playwright/_impl/_locator.py +++ b/playwright/_impl/_locator.py @@ -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, diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index 1e42ae392..017df7f4a 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -1181,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.videos_dir: + if not self._browser_context._videos_dir: return None return self._force_video() diff --git a/tests/async/test_chromium_tracing.py b/tests/async/test_chromium_tracing.py index 23608e009..727a772e5 100644 --- a/tests/async/test_chromium_tracing.py +++ b/tests/async/test_chromium_tracing.py @@ -51,16 +51,18 @@ 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 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 ) From 2db296d6fd9e35362ec54dbe6136aaef82311521 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 10 Jul 2025 06:02:25 -0700 Subject: [PATCH 04/12] Fix Chromium tracing tests --- playwright/_impl/_assertions.py | 2 +- playwright/_impl/_frame.py | 4 ++-- playwright/_impl/_page.py | 10 +++++----- tests/async/test_chromium_tracing.py | 8 ++++++++ 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/playwright/_impl/_assertions.py b/playwright/_impl/_assertions.py index b0d390f04..3aadbf5fe 100644 --- a/playwright/_impl/_assertions.py +++ b/playwright/_impl/_assertions.py @@ -138,7 +138,7 @@ async def to_have_url( ignoreCase: bool = None, ) -> None: __tracebackhide__ = True - base_url = self._actual_page.context.base_url + 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) diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py index fd107d314..a8f9bdb75 100644 --- a/playwright/_impl/_frame.py +++ b/playwright/_impl/_frame.py @@ -224,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.base_url, + cast("Page", self._page)._browser_context._base_url, event["url"], url, ) @@ -257,7 +257,7 @@ async def wait_for_url( timeout: float = None, ) -> None: assert self._page - if url_matches(self._page._browser_context.base_url, 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( diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index 017df7f4a..a0fa4eec2 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -388,7 +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.base_url, frame.url, url): + if url and url_matches(self._browser_context._base_url, frame.url, url): return frame return None @@ -680,7 +680,7 @@ async def route( self._routes.insert( 0, RouteHandler( - self._browser_context.base_url, + self._browser_context._base_url, url, handler, True if self._dispatcher_fiber else False, @@ -722,7 +722,7 @@ async def route_web_socket( ) -> None: self._web_socket_routes.insert( 0, - WebSocketRouteHandler(self._browser_context.base_url, url, handler), + WebSocketRouteHandler(self._browser_context._base_url, url, handler), ) await self._update_web_socket_interception_patterns() @@ -1268,7 +1268,7 @@ def expect_request( def my_predicate(request: Request) -> bool: if not callable(urlOrPredicate): return url_matches( - self._browser_context.base_url, + self._browser_context._base_url, request.url, urlOrPredicate, ) @@ -1300,7 +1300,7 @@ def expect_response( def my_predicate(request: Response) -> bool: if not callable(urlOrPredicate): return url_matches( - self._browser_context.base_url, + self._browser_context._base_url, request.url, urlOrPredicate, ) diff --git a/tests/async/test_chromium_tracing.py b/tests/async/test_chromium_tracing.py index 727a772e5..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 @@ -54,6 +61,7 @@ async def test_should_run_with_custom_categories_if_provided( path=output_file, 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) From ab9fb96b2ab7af26d72af7866d797b510012e932 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 10 Jul 2025 06:18:46 -0700 Subject: [PATCH 05/12] Add missing test --- tests/async/test_selectors_misc.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/async/test_selectors_misc.py b/tests/async/test_selectors_misc.py index 5527d6ec8..c9f4b97cf 100644 --- a/tests/async/test_selectors_misc.py +++ b/tests/async/test_selectors_misc.py @@ -12,6 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +import pytest + +from playwright._impl._errors import Error +from playwright._impl._selectors import Selectors from playwright.async_api import Page @@ -52,3 +56,14 @@ 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, +) -> None: + await selectors.register("alreadyRegistered", "return []") + with pytest.raises( + Error, + match='Selectors.register: "alreadyRegistered" selector engine has been already registered', + ): + await selectors.register("alreadyRegistered", "return []") From 9ce446eddf92c617bfaaa4e43eae72fe6fb2fcec Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 10 Jul 2025 06:26:05 -0700 Subject: [PATCH 06/12] Fix fill() --- playwright/_impl/_frame.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py index a8f9bdb75..fe19a576d 100644 --- a/playwright/_impl/_frame.py +++ b/playwright/_impl/_frame.py @@ -587,7 +587,7 @@ async def fill( strict: bool = None, force: bool = None, ) -> None: - await self._fill(**locals()) + await self._fill(**locals_to_params(locals())) async def _fill( self, From 25aa857a0cd29f6eb2fbd4c9fd66ce39e1bf9574 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 10 Jul 2025 06:44:05 -0700 Subject: [PATCH 07/12] Decrease WebKit version number in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fa9e246a9..3b2eee14d 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | | Chromium 139.0.7258.5 | ✅ | ✅ | ✅ | -| WebKit 26.0 | ✅ | ✅ | ✅ | +| WebKit 18.5 | ✅ | ✅ | ✅ | | Firefox 140.0.2 | ✅ | ✅ | ✅ | ## Documentation From 1e1d182c7d55847a2dbc1b75f4fc263dc3c35464 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 10 Jul 2025 07:21:29 -0700 Subject: [PATCH 08/12] Proper selector.register callback --- tests/async/test_selectors_misc.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/async/test_selectors_misc.py b/tests/async/test_selectors_misc.py index c9f4b97cf..d7ed5eb14 100644 --- a/tests/async/test_selectors_misc.py +++ b/tests/async/test_selectors_misc.py @@ -61,9 +61,19 @@ async def test_should_work_with_internal_and(page: Page) -> None: async def test_should_throw_already_registered_error_when_registering( selectors: Selectors, ) -> None: - await selectors.register("alreadyRegistered", "return []") + create_tag_selector = """ + () => ({ + query(root, selector) { + return root.querySelector(selector); + }, + queryAll(root, selector) { + return Array.from(root.querySelectorAll(selector)); + } + }); + """ + await selectors.register("alreadyRegistered", create_tag_selector) with pytest.raises( Error, match='Selectors.register: "alreadyRegistered" selector engine has been already registered', ): - await selectors.register("alreadyRegistered", "return []") + await selectors.register("alreadyRegistered", create_tag_selector) From d03400af6e39930c7e6fa0714581bbf9754ca315 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 10 Jul 2025 08:15:41 -0700 Subject: [PATCH 09/12] Remove rogue semicolon --- tests/async/test_selectors_misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/async/test_selectors_misc.py b/tests/async/test_selectors_misc.py index d7ed5eb14..8cf755740 100644 --- a/tests/async/test_selectors_misc.py +++ b/tests/async/test_selectors_misc.py @@ -69,7 +69,7 @@ async def test_should_throw_already_registered_error_when_registering( queryAll(root, selector) { return Array.from(root.querySelectorAll(selector)); } - }); + }) """ await selectors.register("alreadyRegistered", create_tag_selector) with pytest.raises( From aa636841504886e2bb3acbed4a851c06ddc3f6bb Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 10 Jul 2025 09:36:00 -0700 Subject: [PATCH 10/12] Discriminate browsers for selector test --- tests/async/test_selectors_misc.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/async/test_selectors_misc.py b/tests/async/test_selectors_misc.py index 8cf755740..5ad6c3519 100644 --- a/tests/async/test_selectors_misc.py +++ b/tests/async/test_selectors_misc.py @@ -14,6 +14,7 @@ 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 @@ -60,6 +61,7 @@ async def test_should_work_with_internal_and(page: Page) -> None: async def test_should_throw_already_registered_error_when_registering( selectors: Selectors, + browser: Browser, ) -> None: create_tag_selector = """ () => ({ @@ -71,9 +73,10 @@ async def test_should_throw_already_registered_error_when_registering( } }) """ - await selectors.register("alreadyRegistered", create_tag_selector) + name = f"alreadyRegistered-{browser.browser_type.name}" + await selectors.register(name, create_tag_selector) with pytest.raises( Error, - match='Selectors.register: "alreadyRegistered" selector engine has been already registered', + match=f'Selectors.register: "{name}" selector engine has been already registered', ): - await selectors.register("alreadyRegistered", create_tag_selector) + await selectors.register(name, create_tag_selector) From 1b573595169bc696f6e0f123b7ffa5216356d140 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Fri, 11 Jul 2025 05:46:48 -0700 Subject: [PATCH 11/12] Update to 1.54.0 proper --- README.md | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3b2eee14d..fa9e246a9 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | | Chromium 139.0.7258.5 | ✅ | ✅ | ✅ | -| WebKit 18.5 | ✅ | ✅ | ✅ | +| WebKit 26.0 | ✅ | ✅ | ✅ | | Firefox 140.0.2 | ✅ | ✅ | ✅ | ## Documentation diff --git a/setup.py b/setup.py index d1017dcdf..c4a75870a 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ import zipfile from typing import Dict -driver_version = "1.54.0-alpha-2025-07-09" +driver_version = "1.54.0" base_wheel_bundles = [ { From 76d46a09c78a98bbc5297143a373985e6ba5c2fd Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Fri, 11 Jul 2025 05:51:31 -0700 Subject: [PATCH 12/12] Rename cookie types --- playwright/_impl/_api_structures.py | 6 +++--- playwright/_impl/_browser_context.py | 6 ++---- playwright/async_api/__init__.py | 4 ++-- playwright/async_api/_generated.py | 4 ++-- playwright/sync_api/__init__.py | 4 ++-- playwright/sync_api/_generated.py | 4 ++-- scripts/generate_api.py | 2 +- 7 files changed, 14 insertions(+), 16 deletions(-) diff --git a/playwright/_impl/_api_structures.py b/playwright/_impl/_api_structures.py index 963f653bc..0afa0d02e 100644 --- a/playwright/_impl/_api_structures.py +++ b/playwright/_impl/_api_structures.py @@ -32,9 +32,10 @@ class Cookie(TypedDict, total=False): httpOnly: bool secure: bool sameSite: Literal["Lax", "None", "Strict"] + partitionKey: Optional[str] -class ContextCookie(TypedDict, total=False): +class StorageStateCookie(TypedDict, total=False): name: str value: str domain: str @@ -43,7 +44,6 @@ class ContextCookie(TypedDict, total=False): httpOnly: bool secure: bool sameSite: Literal["Lax", "None", "Strict"] - partitionKey: Optional[str] # TODO: We are waiting for PEP705 so SetCookieParam can be readonly and matches Cookie. @@ -110,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/_browser_context.py b/playwright/_impl/_browser_context.py index e1cdedb0e..391e61ec6 100644 --- a/playwright/_impl/_browser_context.py +++ b/playwright/_impl/_browser_context.py @@ -32,7 +32,7 @@ ) from playwright._impl._api_structures import ( - ContextCookie, + Cookie, Geolocation, SetCookieParam, StorageState, @@ -334,9 +334,7 @@ async def new_page(self) -> Page: raise Error("Please use browser.new_context()") return from_channel(await self._channel.send("newPage", None)) - async def cookies( - self, urls: Union[str, Sequence[str]] = None - ) -> List[ContextCookie]: + async def cookies(self, urls: Union[str, Sequence[str]] = None) -> List[Cookie]: if urls is None: urls = [] if isinstance(urls, str): diff --git a/playwright/async_api/__init__.py b/playwright/async_api/__init__.py index 77109a629..257ac2022 100644 --- a/playwright/async_api/__init__.py +++ b/playwright/async_api/__init__.py @@ -68,7 +68,6 @@ ChromiumBrowserContext = BrowserContext -ContextCookie = playwright._impl._api_structures.ContextCookie Cookie = playwright._impl._api_structures.Cookie FilePayload = playwright._impl._api_structures.FilePayload FloatRect = playwright._impl._api_structures.FloatRect @@ -80,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 @@ -160,7 +160,6 @@ def __call__( "CDPSession", "ChromiumBrowserContext", "ConsoleMessage", - "ContextCookie", "Cookie", "Dialog", "Download", @@ -189,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 be454661d..bedf233de 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -21,7 +21,7 @@ from playwright._impl._accessibility import Accessibility as AccessibilityImpl from playwright._impl._api_structures import ( ClientCertificate, - ContextCookie, + Cookie, FilePayload, FloatRect, Geolocation, @@ -12794,7 +12794,7 @@ async def new_page(self) -> "Page": async def cookies( self, urls: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None - ) -> typing.List[ContextCookie]: + ) -> typing.List[Cookie]: """BrowserContext.cookies If no URLs are specified, this method returns all cookies. If URLs are specified, only cookies that affect those diff --git a/playwright/sync_api/__init__.py b/playwright/sync_api/__init__.py index 1d80f5c68..e901cadbf 100644 --- a/playwright/sync_api/__init__.py +++ b/playwright/sync_api/__init__.py @@ -68,7 +68,6 @@ ChromiumBrowserContext = BrowserContext -ContextCookie = playwright._impl._api_structures.ContextCookie Cookie = playwright._impl._api_structures.Cookie FilePayload = playwright._impl._api_structures.FilePayload FloatRect = playwright._impl._api_structures.FloatRect @@ -80,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 @@ -159,7 +159,6 @@ def __call__( "CDPSession", "ChromiumBrowserContext", "ConsoleMessage", - "ContextCookie", "Cookie", "Dialog", "Download", @@ -188,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 2f622f5c8..8f4b60764 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -21,7 +21,7 @@ from playwright._impl._accessibility import Accessibility as AccessibilityImpl from playwright._impl._api_structures import ( ClientCertificate, - ContextCookie, + Cookie, FilePayload, FloatRect, Geolocation, @@ -12816,7 +12816,7 @@ def new_page(self) -> "Page": def cookies( self, urls: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None - ) -> typing.List[ContextCookie]: + ) -> typing.List[Cookie]: """BrowserContext.cookies If no URLs are specified, this method returns all cookies. If URLs are specified, only cookies that affect those diff --git a/scripts/generate_api.py b/scripts/generate_api.py index 8e8632fe3..01f8f525a 100644 --- a/scripts/generate_api.py +++ b/scripts/generate_api.py @@ -225,7 +225,7 @@ def return_value(value: Any) -> List[str]: from playwright._impl._accessibility import Accessibility as AccessibilityImpl -from playwright._impl._api_structures import ContextCookie, SetCookieParam, FloatRect, FilePayload, Geolocation, HttpCredentials, PdfMargins, Position, ProxySettings, ResourceTiming, SourceLocation, StorageState, ClientCertificate, ViewportSize, RemoteAddr, SecurityDetails, RequestSizes, NameValue, TracingGroupLocation +from playwright._impl._api_structures import Cookie, SetCookieParam, FloatRect, FilePayload, Geolocation, HttpCredentials, PdfMargins, Position, ProxySettings, ResourceTiming, SourceLocation, StorageState, ClientCertificate, ViewportSize, RemoteAddr, SecurityDetails, RequestSizes, NameValue, TracingGroupLocation from playwright._impl._browser import Browser as BrowserImpl from playwright._impl._browser_context import BrowserContext as BrowserContextImpl from playwright._impl._browser_type import BrowserType as BrowserTypeImpl