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"), ] )