From 10428a86c595900e04550b8fed7eaa522bdc0977 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Thu, 16 Nov 2023 17:03:32 +0100 Subject: [PATCH] chore(roll): roll Playwright to 1.40.0-alpha-oct-18-2023 --- README.md | 2 +- playwright/_impl/_artifact.py | 4 +- playwright/_impl/_assertions.py | 13 ++---- playwright/_impl/_browser.py | 23 ++++------ playwright/_impl/_browser_context.py | 30 ++++++++---- playwright/_impl/_browser_type.py | 7 ++- playwright/_impl/_connection.py | 34 +++++++++----- playwright/_impl/_download.py | 2 +- .../_impl/{_api_types.py => _errors.py} | 9 ++++ playwright/_impl/_fetch.py | 18 ++++++-- playwright/_impl/_frame.py | 46 +++++++++---------- playwright/_impl/_helper.py | 15 ++---- playwright/_impl/_impl_to_api_mapping.py | 2 +- playwright/_impl/_network.py | 24 +++++----- playwright/_impl/_page.py | 38 +++++++++------ playwright/_impl/_selectors.py | 2 +- .../_impl/{_wait_helper.py => _waiter.py} | 10 ++-- playwright/async_api/__init__.py | 6 +-- playwright/async_api/_generated.py | 35 ++++++++++---- playwright/sync_api/__init__.py | 6 +-- playwright/sync_api/_context_manager.py | 2 +- playwright/sync_api/_generated.py | 37 +++++++++++---- scripts/documentation_provider.py | 8 +--- scripts/generate_api.py | 2 +- setup.py | 2 +- tests/async/test_asyncio.py | 6 +-- tests/async/test_browsercontext.py | 3 +- tests/async/test_cdp_session.py | 3 +- tests/async/test_defaultbrowsercontext.py | 3 +- tests/async/test_download.py | 3 +- tests/async/test_fetch_browser_context.py | 8 +--- tests/async/test_fetch_global.py | 8 ++-- tests/async/test_keyboard.py | 3 +- tests/async/test_launcher.py | 6 +-- tests/async/test_network.py | 2 +- tests/async/test_page.py | 7 +-- tests/async/test_page_request_intercept.py | 2 +- tests/async/test_worker.py | 5 +- tests/sync/test_cdp_session.py | 3 +- tests/sync/test_fetch_browser_context.py | 8 +--- tests/sync/test_fetch_global.py | 2 +- tests/sync/test_sync.py | 3 +- tests/utils.py | 3 ++ 43 files changed, 257 insertions(+), 198 deletions(-) rename playwright/_impl/{_api_types.py => _errors.py} (81%) rename playwright/_impl/{_wait_helper.py => _waiter.py} (95%) diff --git a/README.md b/README.md index 988c74e29a..6191d65358 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 119.0.6045.9 | ✅ | ✅ | ✅ | +| Chromium 119.0.6045.21 | ✅ | ✅ | ✅ | | WebKit 17.4 | ✅ | ✅ | ✅ | | Firefox 118.0.1 | ✅ | ✅ | ✅ | diff --git a/playwright/_impl/_artifact.py b/playwright/_impl/_artifact.py index 78985a7745..63833fe042 100644 --- a/playwright/_impl/_artifact.py +++ b/playwright/_impl/_artifact.py @@ -28,13 +28,13 @@ def __init__( super().__init__(parent, type, guid, initializer) self.absolute_path = initializer["absolutePath"] - async def path_after_finished(self) -> Optional[pathlib.Path]: + async def path_after_finished(self) -> pathlib.Path: if self._connection.is_remote: raise Error( "Path is not available when using browser_type.connect(). Use save_as() to save a local copy." ) path = await self._channel.send("pathAfterFinished") - return pathlib.Path(path) if path else None + return pathlib.Path(path) async def save_as(self, path: Union[str, Path]) -> None: stream = cast(Stream, from_channel(await self._channel.send("saveAsStream"))) diff --git a/playwright/_impl/_assertions.py b/playwright/_impl/_assertions.py index f54a672e1b..730e1e294e 100644 --- a/playwright/_impl/_assertions.py +++ b/playwright/_impl/_assertions.py @@ -16,6 +16,7 @@ from urllib.parse import urljoin from playwright._impl._api_structures import ExpectedTextValue, FrameExpectOptions +from playwright._impl._connection import format_call_log from playwright._impl._fetch import APIResponse from playwright._impl._helper import is_textual_mime_type from playwright._impl._locator import Locator @@ -56,9 +57,6 @@ async def _expect_impl( result = await self._actual_locator._expect(expression, expect_options) if result["matches"] == self._is_not: actual = result.get("received") - log = "\n".join(result.get("log", "")).strip() - if log: - log = "\nCall log:\n" + log if self._custom_message: out_message = self._custom_message if expected is not None: @@ -67,7 +65,9 @@ async def _expect_impl( out_message = ( f"{message} '{expected}'" if expected is not None else f"{message}" ) - raise AssertionError(f"{out_message}\nActual value: {actual} {log}") + raise AssertionError( + f"{out_message}\nActual value: {actual} {format_call_log(result.get('log'))}" + ) class PageAssertions(AssertionsBase): @@ -720,10 +720,7 @@ async def to_be_ok( if self._is_not: message = message.replace("expected to", "expected not to") out_message = self._custom_message or message - log_list = await self._actual._fetch_log() - log = "\n".join(log_list).strip() - if log: - out_message += f"\n Call log:\n{log}" + out_message += format_call_log(await self._actual._fetch_log()) content_type = self._actual.headers.get("content-type") is_text_encoding = content_type and is_textual_mime_type(content_type) diff --git a/playwright/_impl/_browser.py b/playwright/_impl/_browser.py index 2649d75ea1..5fbc841dbd 100644 --- a/playwright/_impl/_browser.py +++ b/playwright/_impl/_browser.py @@ -27,9 +27,9 @@ from playwright._impl._artifact import Artifact from playwright._impl._browser_context import BrowserContext from playwright._impl._cdp_session import CDPSession -from playwright._impl._connection import ChannelOwner, from_channel +from playwright._impl._connection import ChannelOwner, filter_none, from_channel +from playwright._impl._errors import is_target_closed_error from playwright._impl._helper import ( - BROWSER_CLOSED_ERROR, ColorScheme, ForcedColors, HarContentPolicy, @@ -37,7 +37,6 @@ ReducedMotion, ServiceWorkersPolicy, async_readfile, - is_safe_close_error, locals_to_params, make_dirs_for_file, prepare_record_har_options, @@ -60,12 +59,12 @@ def __init__( super().__init__(parent, type, guid, initializer) self._browser_type = parent self._is_connected = True - self._is_closed_or_closing = False self._should_close_connection_on_close = False self._cr_tracing_path: Optional[str] = None self._contexts: List[BrowserContext] = [] self._channel.on("close", lambda _: self._on_close()) + self._close_reason: Optional[str] = None def __repr__(self) -> str: return f"" @@ -73,7 +72,6 @@ def __repr__(self) -> str: def _on_close(self) -> None: self._is_connected = False self.emit(Browser.Events.Disconnected, self) - self._is_closed_or_closing = True @property def contexts(self) -> List[BrowserContext]: @@ -179,17 +177,16 @@ async def inner() -> Page: return await self._connection.wrap_api_call(inner) - async def close(self) -> None: - if self._is_closed_or_closing: - return - self._is_closed_or_closing = True + async def close(self, reason: str = None) -> None: + self._close_reason = reason try: - await self._channel.send("close") + if self._should_close_connection_on_close: + await self._connection.stop_async() + else: + await self._channel.send("close", filter_none({"reason": reason})) except Exception as e: - if not is_safe_close_error(e): + if not is_target_closed_error(e): raise e - if self._should_close_connection_on_close: - await self._connection.stop_async(BROWSER_CLOSED_ERROR) @property def version(self) -> str: diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py index bfcae8c293..60c4166e8f 100644 --- a/playwright/_impl/_browser_context.py +++ b/playwright/_impl/_browser_context.py @@ -36,16 +36,17 @@ SetCookieParam, StorageState, ) -from playwright._impl._api_types import Error from playwright._impl._artifact import Artifact from playwright._impl._cdp_session import CDPSession from playwright._impl._connection import ( ChannelOwner, + filter_none, from_channel, from_nullable_channel, ) from playwright._impl._console_message import ConsoleMessage from playwright._impl._dialog import Dialog +from playwright._impl._errors import Error, TargetClosedError from playwright._impl._event_context_manager import EventContextManagerImpl from playwright._impl._fetch import APIRequestContext from playwright._impl._frame import Frame @@ -70,7 +71,7 @@ from playwright._impl._network import Request, Response, Route, serialize_headers from playwright._impl._page import BindingCall, Page, Worker from playwright._impl._tracing import Tracing -from playwright._impl._wait_helper import WaitHelper +from playwright._impl._waiter import Waiter from playwright._impl._web_error import WebError if TYPE_CHECKING: # pragma: no cover @@ -194,6 +195,7 @@ def __init__( self.once( self.Events.Close, lambda context: self._closed_future.set_result(True) ) + self._close_reason: Optional[str] = None self._set_event_to_subscription_mapping( { BrowserContext.Events.Console: "console", @@ -433,16 +435,16 @@ def expect_event( ) -> EventContextManagerImpl: if timeout is None: timeout = self._timeout_settings.timeout() - wait_helper = WaitHelper(self, f"browser_context.expect_event({event})") - wait_helper.reject_on_timeout( + waiter = Waiter(self, f"browser_context.expect_event({event})") + waiter.reject_on_timeout( timeout, f'Timeout {timeout}ms exceeded while waiting for event "{event}"' ) if event != BrowserContext.Events.Close: - wait_helper.reject_on_event( - self, BrowserContext.Events.Close, Error("Context closed") + waiter.reject_on_event( + self, BrowserContext.Events.Close, lambda: TargetClosedError() ) - wait_helper.wait_for_event(self, event, predicate) - return EventContextManagerImpl(wait_helper.result()) + waiter.wait_for_event(self, event, predicate) + return EventContextManagerImpl(waiter.result()) def _on_close(self) -> None: if self._browser: @@ -450,9 +452,10 @@ def _on_close(self) -> None: self.emit(BrowserContext.Events.Close, self) - async def close(self) -> None: + async def close(self, reason: str = None) -> None: if self._close_was_called: return + self._close_reason = reason self._close_was_called = True async def _inner_close() -> None: @@ -479,7 +482,7 @@ async def _inner_close() -> None: await har.delete() await self._channel._connection.wrap_api_call(_inner_close, True) - await self._channel.send("close") + await self._channel.send("close", filter_none({"reason": reason})) await self._closed_future async def storage_state(self, path: Union[str, Path] = None) -> StorageState: @@ -488,6 +491,13 @@ async def storage_state(self, path: Union[str, Path] = None) -> StorageState: await async_writefile(path, json.dumps(result)) return result + def _effective_close_reason(self) -> Optional[str]: + if self._close_reason: + return self._close_reason + if self._browser: + return self._browser._close_reason + return None + async def wait_for_event( self, event: str, predicate: Callable = None, timeout: float = None ) -> Any: diff --git a/playwright/_impl/_browser_type.py b/playwright/_impl/_browser_type.py index 4d93a9b14b..4a916171ab 100644 --- a/playwright/_impl/_browser_type.py +++ b/playwright/_impl/_browser_type.py @@ -23,7 +23,6 @@ ProxySettings, ViewportSize, ) -from playwright._impl._api_types import Error from playwright._impl._browser import Browser, prepare_browser_context_params from playwright._impl._browser_context import BrowserContext from playwright._impl._connection import ( @@ -33,8 +32,8 @@ from_channel, from_nullable_channel, ) +from playwright._impl._errors import Error from playwright._impl._helper import ( - BROWSER_CLOSED_ERROR, ColorScheme, Env, ForcedColors, @@ -46,7 +45,7 @@ ) from playwright._impl._json_pipe import JsonPipeTransport from playwright._impl._network import serialize_headers -from playwright._impl._wait_helper import throw_on_timeout +from playwright._impl._waiter import throw_on_timeout if TYPE_CHECKING: from playwright._impl._playwright import Playwright @@ -249,7 +248,7 @@ def handle_transport_close() -> None: page._on_close() context._on_close() browser._on_close() - connection.cleanup(BROWSER_CLOSED_ERROR) + connection.cleanup() transport.once("close", handle_transport_close) diff --git a/playwright/_impl/_connection.py b/playwright/_impl/_connection.py index 7f58c214e9..479f908f0d 100644 --- a/playwright/_impl/_connection.py +++ b/playwright/_impl/_connection.py @@ -36,6 +36,7 @@ from pyee.asyncio import AsyncIOEventEmitter import playwright +from playwright._impl._errors import TargetClosedError from playwright._impl._helper import Error, ParsedMessagePayload, parse_error from playwright._impl._transport import Transport @@ -250,7 +251,7 @@ def __init__( ] = contextvars.ContextVar("ApiZone", default=None) self._local_utils: Optional["LocalUtils"] = local_utils self._tracing_count = 0 - self._closed_error_message: Optional[str] = None + self._closed_error: Optional[Exception] = None @property def local_utils(self) -> "LocalUtils": @@ -281,21 +282,21 @@ def stop_sync(self) -> None: self._loop.run_until_complete(self._transport.wait_until_stopped()) self.cleanup() - async def stop_async(self, error_message: str = None) -> None: + async def stop_async(self) -> None: self._transport.request_stop() await self._transport.wait_until_stopped() - self.cleanup(error_message) + self.cleanup() - def cleanup(self, error_message: str = None) -> None: - if not error_message: - error_message = "Connection closed" - self._closed_error_message = error_message + def cleanup(self, cause: Exception = None) -> None: + self._closed_error = ( + TargetClosedError(str(cause)) if cause else TargetClosedError() + ) if self._init_task and not self._init_task.done(): self._init_task.cancel() for ws_connection in self._child_ws_connections: ws_connection._transport.dispose() for callback in self._callbacks.values(): - callback.future.set_exception(Error(error_message)) + callback.future.set_exception(self._closed_error) self._callbacks.clear() self.emit("close") @@ -313,8 +314,8 @@ def set_in_tracing(self, is_tracing: bool) -> None: def _send_message_to_server( self, object: ChannelOwner, method: str, params: Dict, no_reply: bool = False ) -> ProtocolCallback: - if self._closed_error_message: - raise Error(self._closed_error_message) + if self._closed_error: + raise self._closed_error if object._was_collected: raise Error( "The object has been collected to prevent unbounded heap growth." @@ -361,7 +362,7 @@ def _send_message_to_server( return callback def dispatch(self, msg: ParsedMessagePayload) -> None: - if self._closed_error_message: + if self._closed_error: return id = msg.get("id") if id: @@ -373,11 +374,12 @@ def dispatch(self, msg: ParsedMessagePayload) -> None: if callback.no_reply: return error = msg.get("error") - if error: + if error and not msg.get("result"): parsed_error = parse_error(error["error"]) # type: ignore parsed_error._stack = "".join( traceback.format_list(callback.stack_trace)[-10:] ) + parsed_error._message += format_call_log(msg.get("log")) # type: ignore callback.future.set_exception(parsed_error) else: result = self._replace_guids_with_channels(msg.get("result")) @@ -565,3 +567,11 @@ def _extract_stack_trace_information_from_stack( def filter_none(d: Mapping) -> Dict: return {k: v for k, v in d.items() if v is not None} + + +def format_call_log(log: Optional[List[str]]) -> str: + if not log: + return "" + if len(list(filter(lambda x: x.strip(), log))) == 0: + return "" + return "\nCall log:\n" + "\n - ".join(log) + "\n" diff --git a/playwright/_impl/_download.py b/playwright/_impl/_download.py index 1b93850ba3..ffaf5cacd8 100644 --- a/playwright/_impl/_download.py +++ b/playwright/_impl/_download.py @@ -54,7 +54,7 @@ async def delete(self) -> None: async def failure(self) -> Optional[str]: return await self._artifact.failure() - async def path(self) -> Optional[pathlib.Path]: + async def path(self) -> pathlib.Path: return await self._artifact.path_after_finished() async def save_as(self, path: Union[str, Path]) -> None: diff --git a/playwright/_impl/_api_types.py b/playwright/_impl/_errors.py similarity index 81% rename from playwright/_impl/_api_types.py rename to playwright/_impl/_errors.py index e921e9867e..9bd6ab901d 100644 --- a/playwright/_impl/_api_types.py +++ b/playwright/_impl/_errors.py @@ -19,6 +19,10 @@ from typing import Optional +def is_target_closed_error(error: Exception) -> bool: + return isinstance(error, TargetClosedError) + + class Error(Exception): def __init__(self, message: str) -> None: self._message = message @@ -41,3 +45,8 @@ def stack(self) -> Optional[str]: class TimeoutError(Error): pass + + +class TargetClosedError(Error): + def __init__(self, message: str = None) -> None: + super().__init__(message or "Target page, context or browser has been closed") diff --git a/playwright/_impl/_fetch.py b/playwright/_impl/_fetch.py index 997133227a..27fab09c18 100644 --- a/playwright/_impl/_fetch.py +++ b/playwright/_impl/_fetch.py @@ -30,13 +30,13 @@ StorageState, ) from playwright._impl._connection import ChannelOwner, filter_none, from_channel +from playwright._impl._errors import is_target_closed_error from playwright._impl._helper import ( Error, NameValue, async_readfile, async_writefile, is_file_payload, - is_safe_close_error, locals_to_params, object_to_array, to_impl, @@ -330,13 +330,13 @@ async def _inner_fetch( if data: if isinstance(data, str): if is_json_content_type(serialized_headers): - json_data = data + json_data = data if is_json_parsable(data) else json.dumps(data) else: post_data_buffer = data.encode() elif isinstance(data, bytes): post_data_buffer = data elif isinstance(data, (dict, list, int, bool)): - json_data = data + json_data = json.dumps(data) else: raise Error(f"Unsupported 'data' type: {type(data)}") elif form: @@ -451,7 +451,7 @@ async def body(self) -> bytes: raise Error("Response has been disposed") return base64.b64decode(result["binary"]) except Error as exc: - if is_safe_close_error(exc): + if is_target_closed_error(exc): raise Error("Response has been disposed") raise exc @@ -491,3 +491,13 @@ def is_json_content_type(headers: network.HeadersArray = None) -> bool: if header["name"] == "Content-Type": return header["value"].startswith("application/json") return False + + +def is_json_parsable(value: Any) -> bool: + if not isinstance(value, str): + return False + try: + json.loads(value) + return True + except json.JSONDecodeError: + return False diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py index b004d3cbc0..d8836e3bb2 100644 --- a/playwright/_impl/_frame.py +++ b/playwright/_impl/_frame.py @@ -20,13 +20,13 @@ from pyee import EventEmitter from playwright._impl._api_structures import AriaRole, FilePayload, Position -from playwright._impl._api_types import Error from playwright._impl._connection import ( ChannelOwner, from_channel, from_nullable_channel, ) from playwright._impl._element_handle import ElementHandle, convert_select_option_values +from playwright._impl._errors import Error from playwright._impl._event_context_manager import EventContextManagerImpl from playwright._impl._helper import ( DocumentLoadState, @@ -59,7 +59,7 @@ ) from playwright._impl._network import Response from playwright._impl._set_input_files_helpers import convert_input_files -from playwright._impl._wait_helper import WaitHelper +from playwright._impl._waiter import Waiter if sys.version_info >= (3, 8): # pragma: no cover from typing import Literal @@ -139,18 +139,18 @@ async def goto( ), ) - def _setup_navigation_wait_helper( - self, wait_name: str, timeout: float = None - ) -> WaitHelper: + def _setup_navigation_waiter(self, wait_name: str, timeout: float = None) -> Waiter: assert self._page - wait_helper = WaitHelper(self._page, f"frame.{wait_name}") - wait_helper.reject_on_event( - self._page, "close", Error("Navigation failed because page was closed!") + waiter = Waiter(self._page, f"frame.{wait_name}") + waiter.reject_on_event( + self._page, + "close", + lambda: cast(Page, self._page)._close_error_with_reason(), ) - wait_helper.reject_on_event( + waiter.reject_on_event( self._page, "crash", Error("Navigation failed because page crashed!") ) - wait_helper.reject_on_event( + waiter.reject_on_event( self._page, "framedetached", Error("Navigating frame was detached!"), @@ -158,8 +158,8 @@ def _setup_navigation_wait_helper( ) if timeout is None: timeout = self._page._timeout_settings.navigation_timeout() - wait_helper.reject_on_timeout(timeout, f"Timeout {timeout}ms exceeded.") - return wait_helper + waiter.reject_on_timeout(timeout, f"Timeout {timeout}ms exceeded.") + return waiter def expect_navigation( self, @@ -174,10 +174,10 @@ def expect_navigation( if timeout is None: timeout = self._page._timeout_settings.navigation_timeout() deadline = monotonic_time() + timeout - wait_helper = self._setup_navigation_wait_helper("expect_navigation", timeout) + waiter = self._setup_navigation_waiter("expect_navigation", timeout) to_url = f' to "{url}"' if url else "" - wait_helper.log(f"waiting for navigation{to_url} until '{wait_until}'") + waiter.log(f"waiting for navigation{to_url} until '{wait_until}'") matcher = ( URLMatcher(self._page._browser_context._options.get("baseURL"), url) if url @@ -188,17 +188,17 @@ def predicate(event: Any) -> bool: # Any failed navigation results in a rejection. if event.get("error"): return True - wait_helper.log(f' navigated to "{event["url"]}"') + waiter.log(f' navigated to "{event["url"]}"') return not matcher or matcher.matches(event["url"]) - wait_helper.wait_for_event( + waiter.wait_for_event( self._event_emitter, "navigated", predicate=predicate, ) async def continuation() -> Optional[Response]: - event = await wait_helper.result() + event = await waiter.result() if "error" in event: raise Error(event["error"]) if wait_until not in self._load_states: @@ -244,24 +244,24 @@ async def _wait_for_load_state_impl( raise Error( "state: expected one of (load|domcontentloaded|networkidle|commit)" ) - wait_helper = self._setup_navigation_wait_helper("wait_for_load_state", timeout) + waiter = self._setup_navigation_waiter("wait_for_load_state", timeout) if state in self._load_states: - wait_helper.log(f' not waiting, "{state}" event already fired') + waiter.log(f' not waiting, "{state}" event already fired') # TODO: align with upstream - wait_helper._fulfill(None) + waiter._fulfill(None) else: def handle_load_state_event(actual_state: str) -> bool: - wait_helper.log(f'"{actual_state}" event fired') + waiter.log(f'"{actual_state}" event fired') return actual_state == state - wait_helper.wait_for_event( + waiter.wait_for_event( self._event_emitter, "loadstate", handle_load_state_event, ) - await wait_helper.result() + await waiter.result() async def frame_element(self) -> ElementHandle: return from_channel(await self._channel.send("frameElement")) diff --git a/playwright/_impl/_helper.py b/playwright/_impl/_helper.py index 5f80311271..1b49026130 100644 --- a/playwright/_impl/_helper.py +++ b/playwright/_impl/_helper.py @@ -40,7 +40,7 @@ from greenlet import greenlet from playwright._impl._api_structures import NameValue -from playwright._impl._api_types import Error, TimeoutError +from playwright._impl._errors import Error, TargetClosedError, TimeoutError from playwright._impl._str_utils import escape_regex_flags if sys.version_info >= (3, 8): # pragma: no cover @@ -221,6 +221,8 @@ def parse_error(error: ErrorPayload) -> Error: base_error_class = Error if error.get("name") == "TimeoutError": base_error_class = TimeoutError + if error.get("name") == "TargetClosedError": + base_error_class = TargetClosedError exc = base_error_class(cast(str, patch_error_message(error.get("message")))) exc._name = error["name"] exc._stack = error["stack"] @@ -322,17 +324,6 @@ def prepare_interception_patterns( return patterns -BROWSER_CLOSED_ERROR = "Browser has been closed" -BROWSER_OR_CONTEXT_CLOSED_ERROR = "Target page, context or browser has been closed" - - -def is_safe_close_error(error: Exception) -> bool: - message = str(error) - return message.endswith(BROWSER_CLOSED_ERROR) or message.endswith( - BROWSER_OR_CONTEXT_CLOSED_ERROR - ) - - to_snake_case_regex = re.compile("((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))") diff --git a/playwright/_impl/_impl_to_api_mapping.py b/playwright/_impl/_impl_to_api_mapping.py index 332d9a4d91..60a748fdc6 100644 --- a/playwright/_impl/_impl_to_api_mapping.py +++ b/playwright/_impl/_impl_to_api_mapping.py @@ -15,7 +15,7 @@ import inspect from typing import Any, Callable, Dict, List, Optional, Union -from playwright._impl._api_types import Error +from playwright._impl._errors import Error from playwright._impl._map import Map API_ATTR = "_pw_api_instance_" diff --git a/playwright/_impl/_network.py b/playwright/_impl/_network.py index 35234d2867..9eba30e0d4 100644 --- a/playwright/_impl/_network.py +++ b/playwright/_impl/_network.py @@ -49,16 +49,16 @@ ResourceTiming, SecurityDetails, ) -from playwright._impl._api_types import Error from playwright._impl._connection import ( ChannelOwner, filter_none, from_channel, from_nullable_channel, ) +from playwright._impl._errors import Error from playwright._impl._event_context_manager import EventContextManagerImpl from playwright._impl._helper import locals_to_params -from playwright._impl._wait_helper import WaitHelper +from playwright._impl._waiter import Waiter if TYPE_CHECKING: # pragma: no cover from playwright._impl._browser_context import BrowserContext @@ -641,22 +641,20 @@ def expect_event( ) -> EventContextManagerImpl: if timeout is None: timeout = cast(Any, self._parent)._timeout_settings.timeout() - wait_helper = WaitHelper(self, f"web_socket.expect_event({event})") - wait_helper.reject_on_timeout( + waiter = Waiter(self, f"web_socket.expect_event({event})") + waiter.reject_on_timeout( cast(float, timeout), f'Timeout {timeout}ms exceeded while waiting for event "{event}"', ) if event != WebSocket.Events.Close: - wait_helper.reject_on_event( - self, WebSocket.Events.Close, Error("Socket closed") - ) + waiter.reject_on_event(self, WebSocket.Events.Close, Error("Socket closed")) if event != WebSocket.Events.Error: - wait_helper.reject_on_event( - self, WebSocket.Events.Error, Error("Socket error") - ) - wait_helper.reject_on_event(self._page, "close", Error("Page closed")) - wait_helper.wait_for_event(self, event, predicate) - return EventContextManagerImpl(wait_helper.result()) + waiter.reject_on_event(self, WebSocket.Events.Error, Error("Socket error")) + waiter.reject_on_event( + self._page, "close", lambda: self._page._close_error_with_reason() + ) + waiter.wait_for_event(self, event, predicate) + return EventContextManagerImpl(waiter.result()) async def wait_for_event( self, event: str, predicate: Callable = None, timeout: float = None diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index 99944be413..8c9f4557a9 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -40,7 +40,6 @@ Position, ViewportSize, ) -from playwright._impl._api_types import Error from playwright._impl._artifact import Artifact from playwright._impl._connection import ( ChannelOwner, @@ -50,6 +49,7 @@ from playwright._impl._console_message import ConsoleMessage from playwright._impl._download import Download from playwright._impl._element_handle import ElementHandle +from playwright._impl._errors import Error, TargetClosedError, is_target_closed_error from playwright._impl._event_context_manager import EventContextManagerImpl from playwright._impl._file_chooser import FileChooser from playwright._impl._frame import Frame @@ -72,7 +72,6 @@ URLMatchResponse, async_readfile, async_writefile, - is_safe_close_error, locals_to_params, make_dirs_for_file, serialize_error, @@ -86,7 +85,7 @@ ) from playwright._impl._network import Request, Response, Route, serialize_headers from playwright._impl._video import Video -from playwright._impl._wait_helper import WaitHelper +from playwright._impl._waiter import Waiter if sys.version_info >= (3, 8): # pragma: no cover from typing import Literal @@ -151,6 +150,7 @@ def __init__( ) self._video: Optional[Video] = None self._opener = cast("Page", from_nullable_channel(initializer.get("opener"))) + self._close_reason: Optional[str] = None self._channel.on( "bindingCall", @@ -195,13 +195,15 @@ def __init__( self._closed_or_crashed_future: asyncio.Future = asyncio.Future() self.on( Page.Events.Close, - lambda _: self._closed_or_crashed_future.set_result(True) + lambda _: self._closed_or_crashed_future.set_result( + self._close_error_with_reason() + ) if not self._closed_or_crashed_future.done() else None, ) self.on( Page.Events.Crash, - lambda _: self._closed_or_crashed_future.set_result(True) + lambda _: self._closed_or_crashed_future.set_result(TargetClosedError()) if not self._closed_or_crashed_future.done() else None, ) @@ -662,13 +664,14 @@ async def screenshot( async def title(self) -> str: return await self._main_frame.title() - async def close(self, runBeforeUnload: bool = None) -> None: + async def close(self, runBeforeUnload: bool = None, reason: str = None) -> None: + self._close_reason = reason try: await self._channel.send("close", locals_to_params(locals())) if self._owned_context: await self._owned_context.close() except Exception as e: - if not is_safe_close_error(e) and not runBeforeUnload: + if not is_target_closed_error(e) and not runBeforeUnload: raise e def is_closed(self) -> bool: @@ -1006,6 +1009,11 @@ def video( self._video = Video(self) return self._video + def _close_error_with_reason(self) -> TargetClosedError: + return TargetClosedError( + self._close_reason or self._browser_context._effective_close_reason() + ) + def expect_event( self, event: str, @@ -1025,18 +1033,20 @@ def _expect_event( ) -> EventContextManagerImpl: if timeout is None: timeout = self._timeout_settings.timeout() - wait_helper = WaitHelper(self, f"page.expect_event({event})") - wait_helper.reject_on_timeout( + waiter = Waiter(self, f"page.expect_event({event})") + waiter.reject_on_timeout( timeout, f'Timeout {timeout}ms exceeded while waiting for event "{event}"' ) if log_line: - wait_helper.log(log_line) + waiter.log(log_line) if event != Page.Events.Crash: - wait_helper.reject_on_event(self, Page.Events.Crash, Error("Page crashed")) + waiter.reject_on_event(self, Page.Events.Crash, Error("Page crashed")) if event != Page.Events.Close: - wait_helper.reject_on_event(self, Page.Events.Close, Error("Page closed")) - wait_helper.wait_for_event(self, event, predicate) - return EventContextManagerImpl(wait_helper.result()) + waiter.reject_on_event( + self, Page.Events.Close, lambda: self._close_error_with_reason() + ) + waiter.wait_for_event(self, event, predicate) + return EventContextManagerImpl(waiter.result()) def expect_console_message( self, diff --git a/playwright/_impl/_selectors.py b/playwright/_impl/_selectors.py index 409b0921d4..729e172547 100644 --- a/playwright/_impl/_selectors.py +++ b/playwright/_impl/_selectors.py @@ -16,8 +16,8 @@ from pathlib import Path from typing import Any, Dict, List, Set, Union -from playwright._impl._api_types import Error from playwright._impl._connection import ChannelOwner +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 diff --git a/playwright/_impl/_wait_helper.py b/playwright/_impl/_waiter.py similarity index 95% rename from playwright/_impl/_wait_helper.py rename to playwright/_impl/_waiter.py index 783ac36893..7b0ad2cc6a 100644 --- a/playwright/_impl/_wait_helper.py +++ b/playwright/_impl/_waiter.py @@ -16,15 +16,15 @@ import math import uuid from asyncio.tasks import Task -from typing import Any, Callable, List, Tuple +from typing import Any, Callable, List, Tuple, Union from pyee import EventEmitter -from playwright._impl._api_types import Error, TimeoutError from playwright._impl._connection import ChannelOwner +from playwright._impl._errors import Error, TimeoutError -class WaitHelper: +class Waiter: def __init__(self, channel_owner: ChannelOwner, event: str) -> None: self._result: asyncio.Future = asyncio.Future() self._wait_id = uuid.uuid4().hex @@ -66,12 +66,12 @@ def reject_on_event( self, emitter: EventEmitter, event: str, - error: Error, + error: Union[Error, Callable[..., Error]], predicate: Callable = None, ) -> None: def listener(event_data: Any = None) -> None: if not predicate or predicate(event_data): - self._reject(error) + self._reject(error() if callable(error) else error) emitter.on(event, listener) self._registered_listeners.append((emitter, event, listener)) diff --git a/playwright/async_api/__init__.py b/playwright/async_api/__init__.py index e63e27b8db..554e83927b 100644 --- a/playwright/async_api/__init__.py +++ b/playwright/async_api/__init__.py @@ -21,7 +21,7 @@ from typing import Any, Optional, Union, overload import playwright._impl._api_structures -import playwright._impl._api_types +import playwright._impl._errors import playwright.async_api._generated from playwright._impl._assertions import ( APIResponseAssertions as APIResponseAssertionsImpl, @@ -79,8 +79,8 @@ StorageState = playwright._impl._api_structures.StorageState ViewportSize = playwright._impl._api_structures.ViewportSize -Error = playwright._impl._api_types.Error -TimeoutError = playwright._impl._api_types.TimeoutError +Error = playwright._impl._errors.Error +TimeoutError = playwright._impl._errors.TimeoutError def async_playwright() -> PlaywrightContextManager: diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index 0b08eb1022..426c248227 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -42,7 +42,6 @@ StorageState, ViewportSize, ) -from playwright._impl._api_types import Error from playwright._impl._assertions import ( APIResponseAssertions as APIResponseAssertionsImpl, ) @@ -62,6 +61,7 @@ from playwright._impl._dialog import Dialog as DialogImpl from playwright._impl._download import Download as DownloadImpl from playwright._impl._element_handle import ElementHandle as ElementHandleImpl +from playwright._impl._errors import Error from playwright._impl._fetch import APIRequest as APIRequestImpl from playwright._impl._fetch import APIRequestContext as APIRequestContextImpl from playwright._impl._fetch import APIResponse as APIResponseImpl @@ -7241,7 +7241,7 @@ async def failure(self) -> typing.Optional[str]: return mapping.from_maybe_impl(await self._impl_obj.failure()) - async def path(self) -> typing.Optional[pathlib.Path]: + async def path(self) -> pathlib.Path: """Download.path Returns path to the downloaded file in case of successful download. The method will wait for the download to finish @@ -7252,7 +7252,7 @@ async def path(self) -> typing.Optional[pathlib.Path]: Returns ------- - Union[pathlib.Path, None] + pathlib.Path """ return mapping.from_maybe_impl(await self._impl_obj.path()) @@ -9999,7 +9999,12 @@ async def title(self) -> str: return mapping.from_maybe_impl(await self._impl_obj.title()) - async def close(self, *, run_before_unload: typing.Optional[bool] = None) -> None: + async def close( + self, + *, + run_before_unload: typing.Optional[bool] = None, + reason: typing.Optional[str] = None + ) -> None: """Page.close If `runBeforeUnload` is `false`, does not run any unload handlers and waits for the page to be closed. If @@ -10015,10 +10020,12 @@ async def close(self, *, run_before_unload: typing.Optional[bool] = None) -> Non run_before_unload : Union[bool, None] Defaults to `false`. Whether to run the [before unload](https://developer.mozilla.org/en-US/docs/Web/Events/beforeunload) page handlers. + reason : Union[str, None] + The reason to be reported to the operations interrupted by the page closure. """ return mapping.from_maybe_impl( - await self._impl_obj.close(runBeforeUnload=run_before_unload) + await self._impl_obj.close(runBeforeUnload=run_before_unload, reason=reason) ) def is_closed(self) -> bool: @@ -13726,15 +13733,20 @@ def expect_event( ).future ) - async def close(self) -> None: + async def close(self, *, reason: typing.Optional[str] = None) -> None: """BrowserContext.close Closes the browser context. All the pages that belong to the browser context will be closed. **NOTE** The default browser context cannot be closed. + + Parameters + ---------- + reason : Union[str, None] + The reason to be reported to the operations interrupted by the context closure. """ - return mapping.from_maybe_impl(await self._impl_obj.close()) + return mapping.from_maybe_impl(await self._impl_obj.close(reason=reason)) async def storage_state( self, *, path: typing.Optional[typing.Union[str, pathlib.Path]] = None @@ -14462,7 +14474,7 @@ async def new_page( ) ) - async def close(self) -> None: + async def close(self, *, reason: typing.Optional[str] = None) -> None: """Browser.close In case this browser is obtained using `browser_type.launch()`, closes the browser and all of its pages (if @@ -14476,9 +14488,14 @@ async def close(self) -> None: `browser.close()`. The `Browser` object itself is considered to be disposed and cannot be used anymore. + + Parameters + ---------- + reason : Union[str, None] + The reason to be reported to the operations interrupted by the browser closure. """ - return mapping.from_maybe_impl(await self._impl_obj.close()) + return mapping.from_maybe_impl(await self._impl_obj.close(reason=reason)) async def new_browser_cdp_session(self) -> "CDPSession": """Browser.new_browser_cdp_session diff --git a/playwright/sync_api/__init__.py b/playwright/sync_api/__init__.py index 8553aaf2f2..e17c0e3059 100644 --- a/playwright/sync_api/__init__.py +++ b/playwright/sync_api/__init__.py @@ -21,7 +21,7 @@ from typing import Any, Optional, Union, overload import playwright._impl._api_structures -import playwright._impl._api_types +import playwright._impl._errors import playwright.sync_api._generated from playwright._impl._assertions import ( APIResponseAssertions as APIResponseAssertionsImpl, @@ -79,8 +79,8 @@ StorageState = playwright._impl._api_structures.StorageState ViewportSize = playwright._impl._api_structures.ViewportSize -Error = playwright._impl._api_types.Error -TimeoutError = playwright._impl._api_types.TimeoutError +Error = playwright._impl._errors.Error +TimeoutError = playwright._impl._errors.TimeoutError def sync_playwright() -> PlaywrightContextManager: diff --git a/playwright/sync_api/_context_manager.py b/playwright/sync_api/_context_manager.py index 9813f89205..3efc01e912 100644 --- a/playwright/sync_api/_context_manager.py +++ b/playwright/sync_api/_context_manager.py @@ -17,9 +17,9 @@ from greenlet import greenlet -from playwright._impl._api_types import Error from playwright._impl._connection import ChannelOwner, Connection from playwright._impl._driver import compute_driver_executable +from playwright._impl._errors import Error from playwright._impl._object_factory import create_remote_object from playwright._impl._playwright import Playwright from playwright._impl._transport import PipeTransport diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py index ce7df81099..182d31874c 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -42,7 +42,6 @@ StorageState, ViewportSize, ) -from playwright._impl._api_types import Error from playwright._impl._assertions import ( APIResponseAssertions as APIResponseAssertionsImpl, ) @@ -56,6 +55,7 @@ from playwright._impl._dialog import Dialog as DialogImpl from playwright._impl._download import Download as DownloadImpl from playwright._impl._element_handle import ElementHandle as ElementHandleImpl +from playwright._impl._errors import Error from playwright._impl._fetch import APIRequest as APIRequestImpl from playwright._impl._fetch import APIRequestContext as APIRequestContextImpl from playwright._impl._fetch import APIResponse as APIResponseImpl @@ -7361,7 +7361,7 @@ def failure(self) -> typing.Optional[str]: return mapping.from_maybe_impl(self._sync(self._impl_obj.failure())) - def path(self) -> typing.Optional[pathlib.Path]: + def path(self) -> pathlib.Path: """Download.path Returns path to the downloaded file in case of successful download. The method will wait for the download to finish @@ -7372,7 +7372,7 @@ def path(self) -> typing.Optional[pathlib.Path]: Returns ------- - Union[pathlib.Path, None] + pathlib.Path """ return mapping.from_maybe_impl(self._sync(self._impl_obj.path())) @@ -10069,7 +10069,12 @@ def title(self) -> str: return mapping.from_maybe_impl(self._sync(self._impl_obj.title())) - def close(self, *, run_before_unload: typing.Optional[bool] = None) -> None: + def close( + self, + *, + run_before_unload: typing.Optional[bool] = None, + reason: typing.Optional[str] = None + ) -> None: """Page.close If `runBeforeUnload` is `false`, does not run any unload handlers and waits for the page to be closed. If @@ -10085,10 +10090,14 @@ def close(self, *, run_before_unload: typing.Optional[bool] = None) -> None: run_before_unload : Union[bool, None] Defaults to `false`. Whether to run the [before unload](https://developer.mozilla.org/en-US/docs/Web/Events/beforeunload) page handlers. + reason : Union[str, None] + The reason to be reported to the operations interrupted by the page closure. """ return mapping.from_maybe_impl( - self._sync(self._impl_obj.close(runBeforeUnload=run_before_unload)) + self._sync( + self._impl_obj.close(runBeforeUnload=run_before_unload, reason=reason) + ) ) def is_closed(self) -> bool: @@ -13784,15 +13793,20 @@ def expect_event( ).future, ) - def close(self) -> None: + def close(self, *, reason: typing.Optional[str] = None) -> None: """BrowserContext.close Closes the browser context. All the pages that belong to the browser context will be closed. **NOTE** The default browser context cannot be closed. + + Parameters + ---------- + reason : Union[str, None] + The reason to be reported to the operations interrupted by the context closure. """ - return mapping.from_maybe_impl(self._sync(self._impl_obj.close())) + return mapping.from_maybe_impl(self._sync(self._impl_obj.close(reason=reason))) def storage_state( self, *, path: typing.Optional[typing.Union[str, pathlib.Path]] = None @@ -14524,7 +14538,7 @@ def new_page( ) ) - def close(self) -> None: + def close(self, *, reason: typing.Optional[str] = None) -> None: """Browser.close In case this browser is obtained using `browser_type.launch()`, closes the browser and all of its pages (if @@ -14538,9 +14552,14 @@ def close(self) -> None: `browser.close()`. The `Browser` object itself is considered to be disposed and cannot be used anymore. + + Parameters + ---------- + reason : Union[str, None] + The reason to be reported to the operations interrupted by the browser closure. """ - return mapping.from_maybe_impl(self._sync(self._impl_obj.close())) + return mapping.from_maybe_impl(self._sync(self._impl_obj.close(reason=reason))) def new_browser_cdp_session(self) -> "CDPSession": """Browser.new_browser_cdp_session diff --git a/scripts/documentation_provider.py b/scripts/documentation_provider.py index f625143ea6..506d522fb0 100644 --- a/scripts/documentation_provider.py +++ b/scripts/documentation_provider.py @@ -323,7 +323,7 @@ def serialize_python_type(self, value: Any) -> str: str_value = str(value) if isinstance(value, list): return f"[{', '.join(list(map(lambda a: self.serialize_python_type(a), value)))}]" - if str_value == "": + if str_value == "": return "Error" if str_value == "": return "None" @@ -337,11 +337,7 @@ def serialize_python_type(self, value: Any) -> str: if match: return "EventContextManager[" + match.group(1) + "]" match = re.match(r"^$", str_value) - if ( - match - and "_api_structures" not in str_value - and "_api_types" not in str_value - ): + if match and "_api_structures" not in str_value and "_errors" not in str_value: if match.group(1) == "EventContextManagerImpl": return "EventContextManager" return match.group(1) diff --git a/scripts/generate_api.py b/scripts/generate_api.py index da5cc8ed25..3045c1e613 100644 --- a/scripts/generate_api.py +++ b/scripts/generate_api.py @@ -246,7 +246,7 @@ def return_value(value: Any) -> List[str]: from playwright._impl._video import Video as VideoImpl from playwright._impl._tracing import Tracing as TracingImpl from playwright._impl._locator import Locator as LocatorImpl, FrameLocator as FrameLocatorImpl -from playwright._impl._api_types import Error +from playwright._impl._errors import Error from playwright._impl._fetch import APIRequest as APIRequestImpl, APIResponse as APIResponseImpl, APIRequestContext as APIRequestContextImpl from playwright._impl._assertions import PageAssertions as PageAssertionsImpl, LocatorAssertions as LocatorAssertionsImpl, APIResponseAssertions as APIResponseAssertionsImpl """ diff --git a/setup.py b/setup.py index e7a40794f2..4ae0a85e55 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ InWheel = None from wheel.bdist_wheel import bdist_wheel as BDistWheelCommand -driver_version = "1.39.0" +driver_version = "1.40.0-alpha-oct-18-2023" def extractall(zip: zipfile.ZipFile, path: str) -> None: diff --git a/tests/async/test_asyncio.py b/tests/async/test_asyncio.py index f82081276d..f1def84d38 100644 --- a/tests/async/test_asyncio.py +++ b/tests/async/test_asyncio.py @@ -18,8 +18,8 @@ import pytest from playwright.async_api import Page, async_playwright - -from ..server import Server +from tests.server import Server +from tests.utils import TARGET_CLOSED_ERROR_MESSAGE async def test_should_cancel_underlying_protocol_calls( @@ -66,7 +66,7 @@ async def test_cancel_pending_protocol_call_on_playwright_stop(server: Server) - await playwright.stop() with pytest.raises(Exception) as exc_info: await pending_task - assert "Connection closed" in str(exc_info.value) + assert TARGET_CLOSED_ERROR_MESSAGE in str(exc_info.value) async def test_should_collect_stale_handles(page: Page, server: Server) -> None: diff --git a/tests/async/test_browsercontext.py b/tests/async/test_browsercontext.py index ec3b7e230c..59670340a7 100644 --- a/tests/async/test_browsercontext.py +++ b/tests/async/test_browsercontext.py @@ -20,6 +20,7 @@ from playwright.async_api import Browser, Error from tests.server import Server +from tests.utils import TARGET_CLOSED_ERROR_MESSAGE async def test_page_event_should_create_new_context(browser): @@ -133,7 +134,7 @@ async def test_close_should_abort_wait_for_event(browser): with pytest.raises(Error) as exc_info: async with context.expect_page(): await context.close() - assert "Context closed" in exc_info.value.message + assert TARGET_CLOSED_ERROR_MESSAGE in exc_info.value.message async def test_close_should_be_callable_twice(browser): diff --git a/tests/async/test_cdp_session.py b/tests/async/test_cdp_session.py index 43a7900b85..af8f0715d9 100644 --- a/tests/async/test_cdp_session.py +++ b/tests/async/test_cdp_session.py @@ -15,6 +15,7 @@ import pytest from playwright.async_api import Browser, Error +from tests.utils import TARGET_CLOSED_ERROR_MESSAGE @pytest.mark.only_browser("chromium") @@ -56,7 +57,7 @@ async def test_should_be_able_to_detach_session(page): await client.send( "Runtime.evaluate", {"expression": "3 + 1", "returnByValue": True} ) - assert "Target page, context or browser has been closed" in exc_info.value.message + assert TARGET_CLOSED_ERROR_MESSAGE in exc_info.value.message @pytest.mark.only_browser("chromium") diff --git a/tests/async/test_defaultbrowsercontext.py b/tests/async/test_defaultbrowsercontext.py index 07c183a270..ea4b77eb7c 100644 --- a/tests/async/test_defaultbrowsercontext.py +++ b/tests/async/test_defaultbrowsercontext.py @@ -17,8 +17,7 @@ import pytest -from playwright._impl._api_types import Error -from playwright.async_api import expect +from playwright.async_api import Error, expect @pytest.fixture() diff --git a/tests/async/test_download.py b/tests/async/test_download.py index 29d1f766dc..a0f9f1ab58 100644 --- a/tests/async/test_download.py +++ b/tests/async/test_download.py @@ -19,6 +19,7 @@ import pytest from playwright.async_api import Browser, Error, Page +from tests.utils import TARGET_CLOSED_ERROR_MESSAGE def assert_file_content(path, content): @@ -185,7 +186,7 @@ async def test_should_error_when_saving_after_deletion(tmpdir, browser, server): await download.delete() with pytest.raises(Error) as exc: await download.save_as(user_path) - assert "Target page, context or browser has been closed" in exc.value.message + assert TARGET_CLOSED_ERROR_MESSAGE in exc.value.message await page.close() diff --git a/tests/async/test_fetch_browser_context.py b/tests/async/test_fetch_browser_context.py index 6022f55c34..fbd3130f3a 100644 --- a/tests/async/test_fetch_browser_context.py +++ b/tests/async/test_fetch_browser_context.py @@ -165,12 +165,8 @@ async def support_post_data(fetch_data, request_post_data): await support_post_data("My request", "My request".encode()) await support_post_data(b"My request", "My request".encode()) - await support_post_data( - ["my", "request"], json.dumps(["my", "request"], separators=(",", ":")).encode() - ) - await support_post_data( - {"my": "request"}, json.dumps({"my": "request"}, separators=(",", ":")).encode() - ) + await support_post_data(["my", "request"], json.dumps(["my", "request"]).encode()) + await support_post_data({"my": "request"}, json.dumps({"my": "request"}).encode()) with pytest.raises(Error, match="Unsupported 'data' type: "): await support_post_data(lambda: None, None) diff --git a/tests/async/test_fetch_global.py b/tests/async/test_fetch_global.py index d5eec7d9d9..430547df8f 100644 --- a/tests/async/test_fetch_global.py +++ b/tests/async/test_fetch_global.py @@ -300,7 +300,7 @@ async def test_should_json_stringify_body_when_content_type_is_application_json( ), ) body = req.post_body - assert body.decode() == json.dumps(serialization, separators=(",", ":")) + assert body.decode() == json.dumps(serialization) await request.dispose() @@ -309,7 +309,7 @@ async def test_should_not_double_stringify_body_when_content_type_is_application playwright: Playwright, server: Server, serialization: Any ): request = await playwright.request.new_context() - stringified_value = json.dumps(serialization, separators=(",", ":")) + stringified_value = json.dumps(serialization) [req, _] = await asyncio.gather( server.wait_for_request("/empty.html"), request.post( @@ -328,7 +328,7 @@ async def test_should_accept_already_serialized_data_as_bytes_when_content_type_ playwright: Playwright, server: Server ): request = await playwright.request.new_context() - stringified_value = json.dumps({"foo": "bar"}, separators=(",", ":")).encode() + stringified_value = json.dumps({"foo": "bar"}).encode() [req, _] = await asyncio.gather( server.wait_for_request("/empty.html"), request.post( @@ -410,5 +410,5 @@ async def test_should_serialize_null_values_in_json( server.set_route("/echo", lambda req: (req.write(req.post_body), req.finish())) response = await request.post(server.PREFIX + "/echo", data={"foo": None}) assert response.status == 200 - assert await response.text() == '{"foo":null}' + assert await response.text() == '{"foo": null}' await request.dispose() diff --git a/tests/async/test_keyboard.py b/tests/async/test_keyboard.py index 6aee344ebb..761fe977c5 100644 --- a/tests/async/test_keyboard.py +++ b/tests/async/test_keyboard.py @@ -13,8 +13,7 @@ # limitations under the License. import pytest -from playwright._impl._api_types import Error -from playwright.async_api import Page +from playwright.async_api import Error, Page async def captureLastKeydown(page): diff --git a/tests/async/test_launcher.py b/tests/async/test_launcher.py index a3c0b57210..a1f3f14801 100644 --- a/tests/async/test_launcher.py +++ b/tests/async/test_launcher.py @@ -18,6 +18,7 @@ import pytest from playwright.async_api import BrowserType, Error +from tests.utils import TARGET_CLOSED_ERROR_MESSAGE async def test_browser_type_launch_should_reject_all_promises_when_browser_is_closed( @@ -29,10 +30,7 @@ async def test_browser_type_launch_should_reject_all_promises_when_browser_is_cl await page.close() with pytest.raises(Error) as exc: await never_resolves - assert ( - "Target closed" in exc.value.message - or "Target page, context or browser has been closed" in exc.value.message - ) + assert TARGET_CLOSED_ERROR_MESSAGE in exc.value.message @pytest.mark.skip_browser("firefox") diff --git a/tests/async/test_network.py b/tests/async/test_network.py index f4072fff4b..5234d91d75 100644 --- a/tests/async/test_network.py +++ b/tests/async/test_network.py @@ -346,7 +346,7 @@ async def test_request_post_data_should_work(page, server): '() => fetch("./post", { method: "POST", body: JSON.stringify({foo: "bar"})})' ) assert len(requests) == 1 - assert requests[0].post_data == '{"foo":"bar"}' + assert requests[0].post_data == '{"foo": "bar"}' async def test_request_post_data__should_be_undefined_when_there_is_no_post_data( diff --git a/tests/async/test_page.py b/tests/async/test_page.py index b983b631e3..117c8009a5 100644 --- a/tests/async/test_page.py +++ b/tests/async/test_page.py @@ -20,6 +20,7 @@ from playwright.async_api import BrowserContext, Error, Page, Route, TimeoutError from tests.server import Server +from tests.utils import TARGET_CLOSED_ERROR_MESSAGE async def test_close_should_reject_all_promises(context): @@ -28,7 +29,7 @@ async def test_close_should_reject_all_promises(context): await asyncio.gather( new_page.evaluate("() => new Promise(r => {})"), new_page.close() ) - assert "Target closed" in exc_info.value.message + assert " closed" in exc_info.value.message async def test_closed_should_not_visible_in_context_pages(context): @@ -112,7 +113,7 @@ async def wait_for_response(): ) for i in range(2): error = results[i] - assert "Page closed" in error.message + assert TARGET_CLOSED_ERROR_MESSAGE in error.message assert "Timeout" not in error.message @@ -269,7 +270,7 @@ async def test_wait_for_event_should_fail_with_error_upon_disconnect(page): with pytest.raises(Error) as exc_info: async with page.expect_download(): await page.close() - assert "Page closed" in exc_info.value.message + assert TARGET_CLOSED_ERROR_MESSAGE in exc_info.value.message async def test_wait_for_response_should_work(page, server): diff --git a/tests/async/test_page_request_intercept.py b/tests/async/test_page_request_intercept.py index 2491645c60..39b07d4bc2 100644 --- a/tests/async/test_page_request_intercept.py +++ b/tests/async/test_page_request_intercept.py @@ -78,7 +78,7 @@ async def handle(route: Route): await page.route("**/*.html", lambda route: handle(route)) await page.goto(server.PREFIX + "/empty.html") request = await request_promise - assert request.post_body.decode("utf-8") == '{"foo":"bar"}' + assert request.post_body.decode("utf-8") == '{"foo": "bar"}' async def test_should_fulfill_popup_main_request_using_alias( diff --git a/tests/async/test_worker.py b/tests/async/test_worker.py index 08d6bd0cc3..8b2e56f3f3 100644 --- a/tests/async/test_worker.py +++ b/tests/async/test_worker.py @@ -19,6 +19,7 @@ from flaky import flaky from playwright.async_api import Error, Page, Worker +from tests.utils import TARGET_CLOSED_ERROR_MESSAGE async def test_workers_page_workers(page: Page, server): @@ -51,9 +52,7 @@ async def test_workers_should_emit_created_and_destroyed_events(page: Page): assert await worker_destroyed_promise == worker with pytest.raises(Error) as exc: await worker_this_obj.get_property("self") - assert ( - "Worker was closed" in exc.value.message or "Target closed" in exc.value.message - ) + assert TARGET_CLOSED_ERROR_MESSAGE in exc.value.message async def test_workers_should_report_console_logs(page): diff --git a/tests/sync/test_cdp_session.py b/tests/sync/test_cdp_session.py index c07dcb85fc..ad4e1f8f29 100644 --- a/tests/sync/test_cdp_session.py +++ b/tests/sync/test_cdp_session.py @@ -16,6 +16,7 @@ from playwright.sync_api import Browser, Error, Page from tests.server import Server +from tests.utils import TARGET_CLOSED_ERROR_MESSAGE @pytest.mark.only_browser("chromium") @@ -55,7 +56,7 @@ def test_should_be_able_to_detach_session(page: Page) -> None: client.detach() with pytest.raises(Error) as exc_info: client.send("Runtime.evaluate", {"expression": "3 + 1", "returnByValue": True}) - assert "Target page, context or browser has been closed" in exc_info.value.message + assert TARGET_CLOSED_ERROR_MESSAGE in exc_info.value.message @pytest.mark.only_browser("chromium") diff --git a/tests/sync/test_fetch_browser_context.py b/tests/sync/test_fetch_browser_context.py index 6f8276f7ea..edb00993b0 100644 --- a/tests/sync/test_fetch_browser_context.py +++ b/tests/sync/test_fetch_browser_context.py @@ -159,12 +159,8 @@ def support_post_data(fetch_data: Any, request_post_data: Any) -> None: support_post_data("My request", "My request".encode()) support_post_data(b"My request", "My request".encode()) - support_post_data( - ["my", "request"], json.dumps(["my", "request"], separators=(",", ":")).encode() - ) - support_post_data( - {"my": "request"}, json.dumps({"my": "request"}, separators=(",", ":")).encode() - ) + support_post_data(["my", "request"], json.dumps(["my", "request"]).encode()) + support_post_data({"my": "request"}, json.dumps({"my": "request"}).encode()) with pytest.raises(Error, match="Unsupported 'data' type: "): support_post_data(lambda: None, None) diff --git a/tests/sync/test_fetch_global.py b/tests/sync/test_fetch_global.py index 300465aec9..5c25d40593 100644 --- a/tests/sync/test_fetch_global.py +++ b/tests/sync/test_fetch_global.py @@ -321,5 +321,5 @@ def test_should_serialize_null_values_in_json( server.set_route("/echo", lambda req: (req.write(req.post_body), req.finish())) response = request.post(server.PREFIX + "/echo", data={"foo": None}) assert response.status == 200 - assert response.text() == '{"foo":null}' + assert response.text() == '{"foo": null}' request.dispose() diff --git a/tests/sync/test_sync.py b/tests/sync/test_sync.py index e2606dc8dc..3f27a41403 100644 --- a/tests/sync/test_sync.py +++ b/tests/sync/test_sync.py @@ -28,6 +28,7 @@ sync_playwright, ) from tests.server import Server +from tests.utils import TARGET_CLOSED_ERROR_MESSAGE def test_sync_query_selector(page: Page) -> None: @@ -272,7 +273,7 @@ def test_close_should_reject_all_promises(context: BrowserContext) -> None: lambda: new_page.evaluate("() => new Promise(r => {})"), lambda: new_page.close(), ) - assert "Target closed" in exc_info.value.message + assert TARGET_CLOSED_ERROR_MESSAGE in exc_info.value.message def test_expect_response_should_work(page: Page, server: Server) -> None: diff --git a/tests/utils.py b/tests/utils.py index 287900faaa..96886a3059 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -55,3 +55,6 @@ def get_trace_actions(events: List[Any]) -> List[str]: key=lambda e: e["startTime"], ) return [e["apiName"] for e in action_events] + + +TARGET_CLOSED_ERROR_MESSAGE = "Target page, context or browser has been closed"