From f791b880a790cf126ba735d137bfa7c0cfb8264e Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Thu, 23 Mar 2023 00:21:04 +0100 Subject: [PATCH] chore(roll): roll Playwright to 1.32.0-beta-1679448099000 (#1826) --- README.md | 4 +- playwright/_impl/_browser.py | 10 +--- playwright/_impl/_browser_context.py | 58 +++++++++++++------ playwright/_impl/_browser_type.py | 22 ++++--- playwright/_impl/_connection.py | 35 +++++++---- playwright/_impl/_fetch.py | 5 +- playwright/_impl/_local_utils.py | 22 ++++++- playwright/_impl/_locator.py | 3 +- playwright/_impl/_page.py | 13 +++-- playwright/_impl/_tracing.py | 77 +++++++++++++++---------- playwright/async_api/_generated.py | 76 ++++++++++++++++-------- playwright/sync_api/_generated.py | 74 ++++++++++++++++-------- setup.py | 2 +- tests/async/test_browsertype_connect.py | 2 + tests/async/test_har.py | 4 +- tests/async/test_input.py | 2 + tests/async/test_locators.py | 22 +++++-- tests/async/test_tracing.py | 74 +++++++++++++++++++++++- tests/sync/test_har.py | 4 +- tests/sync/test_locators.py | 22 +++++-- tests/sync/test_tracing.py | 74 +++++++++++++++++++++++- 21 files changed, 448 insertions(+), 157 deletions(-) diff --git a/README.md b/README.md index c7b9f0639..eb56e67c9 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 112.0.5615.20 | ✅ | ✅ | ✅ | +| Chromium 112.0.5615.29 | ✅ | ✅ | ✅ | | WebKit 16.4 | ✅ | ✅ | ✅ | -| Firefox 110.0.1 | ✅ | ✅ | ✅ | +| Firefox 111.0 | ✅ | ✅ | ✅ | ## Documentation diff --git a/playwright/_impl/_browser.py b/playwright/_impl/_browser.py index e9d0dde1b..24b07adc3 100644 --- a/playwright/_impl/_browser.py +++ b/playwright/_impl/_browser.py @@ -125,10 +125,7 @@ async def new_context( channel = await self._channel.send("newContext", params) context = cast(BrowserContext, from_channel(channel)) - self._contexts.append(context) - context._browser = self - context._options = params - context._set_browser_type(self._browser_type) + self._browser_type._did_create_context(context, params, {}) return context async def new_page( @@ -175,11 +172,6 @@ async def new_page( context._owner_page = page return page - def _set_browser_type(self, browser_type: "BrowserType") -> None: - self._browser_type = browser_type - for context in self._contexts: - context._set_browser_type(browser_type) - async def close(self) -> None: if self._is_closed_or_closing: return diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py index 42d59b7ee..f2787f862 100644 --- a/playwright/_impl/_browser_context.py +++ b/playwright/_impl/_browser_context.py @@ -14,6 +14,7 @@ import asyncio import json +import sys from pathlib import Path from types import SimpleNamespace from typing import ( @@ -59,7 +60,6 @@ URLMatcher, async_readfile, async_writefile, - is_safe_close_error, locals_to_params, prepare_record_har_options, to_impl, @@ -71,7 +71,11 @@ if TYPE_CHECKING: # pragma: no cover from playwright._impl._browser import Browser - from playwright._impl._browser_type import BrowserType + +if sys.version_info >= (3, 8): # pragma: no cover + from typing import Literal +else: # pragma: no cover + from typing_extensions import Literal class BrowserContext(ChannelOwner): @@ -90,11 +94,15 @@ def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) + # 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._bindings: Dict[str, Any] = {} self._timeout_settings = TimeoutSettings(None) - self._browser: Optional["Browser"] = None self._owner_page: Optional[Page] = None self._options: Dict[str, Any] = {} self._background_pages: Set[Page] = set() @@ -172,6 +180,7 @@ def __init__( BrowserContext.Events.RequestFailed: "requestFailed", } ) + self._close_was_called = False def __repr__(self) -> str: return f"" @@ -226,13 +235,14 @@ def pages(self) -> List[Page]: def browser(self) -> Optional["Browser"]: return self._browser - def _set_browser_type(self, browser_type: "BrowserType") -> None: - self._browser_type = browser_type + 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 new_page(self) -> Page: if self._owner_page: @@ -328,15 +338,15 @@ async def _record_into_har( har: Union[Path, str], page: Optional[Page] = None, url: Union[Pattern[str], str] = None, - content: HarContentPolicy = None, - mode: HarMode = None, + update_content: HarContentPolicy = None, + update_mode: HarMode = None, ) -> None: params: Dict[str, Any] = { "options": prepare_record_har_options( { "recordHarPath": har, - "recordHarContent": content or "attach", - "recordHarMode": mode or "minimal", + "recordHarContent": update_content or "attach", + "recordHarMode": update_mode or "minimal", "recordHarUrlFilter": url, } ) @@ -344,7 +354,10 @@ async def _record_into_har( if page: params["page"] = page._channel har_id = await self._channel.send("harStart", params) - self._har_recorders[har_id] = {"path": str(har), "content": content or "attach"} + self._har_recorders[har_id] = { + "path": str(har), + "content": update_content or "attach", + } async def route_from_har( self, @@ -352,12 +365,16 @@ async def route_from_har( url: Union[Pattern[str], str] = None, not_found: RouteFromHarNotFoundPolicy = None, update: bool = None, - content: HarContentPolicy = None, - mode: HarMode = None, + update_content: Literal["attach", "embed"] = None, + update_mode: HarMode = None, ) -> None: if update: await self._record_into_har( - har=har, page=None, url=url, content=content, mode=mode + har=har, + page=None, + url=url, + update_content=update_content, + update_mode=update_mode, ) return router = await HarRouter.create( @@ -400,7 +417,11 @@ def _on_close(self) -> None: self.emit(BrowserContext.Events.Close, self) async def close(self) -> None: - try: + if self._close_was_called: + return + self._close_was_called = True + + async def _inner_close() -> None: for har_id, params in self._har_recorders.items(): har = cast( Artifact, @@ -422,11 +443,10 @@ async def close(self) -> None: else: await har.save_as(params["path"]) await har.delete() - await self._channel.send("close") - await self._closed_future - except Exception as e: - if not is_safe_close_error(e): - raise e + + await self._channel._connection.wrap_api_call(_inner_close, True) + await self._channel.send("close") + await self._closed_future async def _pause(self) -> None: await self._channel.send("pause") diff --git a/playwright/_impl/_browser_type.py b/playwright/_impl/_browser_type.py index afe69980d..07287d609 100644 --- a/playwright/_impl/_browser_type.py +++ b/playwright/_impl/_browser_type.py @@ -92,7 +92,7 @@ async def launch( browser = cast( Browser, from_channel(await self._channel.send("launch", params)) ) - browser._set_browser_type(self) + self._did_launch_browser(browser) return browser async def launch_persistent_context( @@ -154,8 +154,7 @@ async def launch_persistent_context( BrowserContext, from_channel(await self._channel.send("launchPersistentContext", params)), ) - context._options = params - context._set_browser_type(self) + self._did_create_context(context, params, params) return context async def connect_over_cdp( @@ -168,15 +167,14 @@ async def connect_over_cdp( params = locals_to_params(locals()) response = await self._channel.send_return_as_dict("connectOverCDP", params) browser = cast(Browser, from_channel(response["browser"])) + self._did_launch_browser(browser) default_context = cast( Optional[BrowserContext], from_nullable_channel(response.get("defaultContext")), ) if default_context: - browser._contexts.append(default_context) - default_context._browser = browser - browser._set_browser_type(self) + self._did_create_context(default_context, {}, {}) return browser async def connect( @@ -231,6 +229,7 @@ async def connect( 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 def handle_transport_close() -> None: @@ -243,9 +242,16 @@ def handle_transport_close() -> None: transport.once("close", handle_transport_close) - browser._set_browser_type(self) return browser + def _did_create_context( + self, context: BrowserContext, context_options: Dict, browser_options: Dict + ) -> None: + context._set_options(context_options, browser_options) + + def _did_launch_browser(self, browser: Browser) -> None: + browser._browser_type = self + def normalize_launch_params(params: Dict) -> None: if "env" in params: @@ -261,3 +267,5 @@ def normalize_launch_params(params: Dict) -> None: params["executablePath"] = str(Path(params["executablePath"])) if "downloadsPath" in params: params["downloadsPath"] = str(Path(params["downloadsPath"])) + if "tracesDir" in params: + params["tracesDir"] = str(Path(params["tracesDir"])) diff --git a/playwright/_impl/_connection.py b/playwright/_impl/_connection.py index 88bf1b5a0..aa57f2157 100644 --- a/playwright/_impl/_connection.py +++ b/playwright/_impl/_connection.py @@ -19,7 +19,17 @@ import sys import traceback from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union, cast +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + List, + Mapping, + Optional, + Union, + cast, +) from greenlet import greenlet from pyee import EventEmitter @@ -231,7 +241,7 @@ def __init__( Optional[ParsedStackTrace] ] = contextvars.ContextVar("ApiZone", default=None) self._local_utils: Optional["LocalUtils"] = local_utils - self._stack_collector: List[List[Dict[str, Any]]] = [] + self._tracing_count = 0 @property def local_utils(self) -> "LocalUtils": @@ -279,12 +289,11 @@ def call_on_object_with_known_name( ) -> None: self._waiting_for_object[guid] = callback - def start_collecting_call_metadata(self, collector: Any) -> None: - if collector not in self._stack_collector: - self._stack_collector.append(collector) - - def stop_collecting_call_metadata(self, collector: Any) -> None: - self._stack_collector.remove(collector) + def set_in_tracing(self, is_tracing: bool) -> None: + if is_tracing: + self._tracing_count += 1 + else: + self._tracing_count -= 1 def _send_message_to_server( self, guid: str, method: str, params: Dict @@ -299,8 +308,6 @@ def _send_message_to_server( ) self._callbacks[id] = callback stack_trace_information = cast(ParsedStackTrace, self._api_zone.get()) - for collector in self._stack_collector: - collector.append({"stack": stack_trace_information["frames"], "id": id}) frames = stack_trace_information.get("frames", []) location = ( { @@ -325,6 +332,10 @@ def _send_message_to_server( } self._transport.send(message) self._callbacks[id] = callback + + if self._tracing_count > 0 and frames and guid != "localUtils": + self.local_utils.add_stack_to_tracing_no_reply(id, frames) + return callback def dispatch(self, msg: ParsedMessagePayload) -> None: @@ -521,3 +532,7 @@ def _extract_stack_trace_information_from_stack( "frames": parsed_frames, "apiName": "" if is_internal else api_name, } + + +def filter_none(d: Mapping) -> Dict: + return {k: v for k, v in d.items() if v is not None} diff --git a/playwright/_impl/_fetch.py b/playwright/_impl/_fetch.py index 1d351c124..997133227 100644 --- a/playwright/_impl/_fetch.py +++ b/playwright/_impl/_fetch.py @@ -29,7 +29,7 @@ ServerFilePayload, StorageState, ) -from playwright._impl._connection import ChannelOwner, from_channel +from playwright._impl._connection import ChannelOwner, filter_none, from_channel from playwright._impl._helper import ( Error, NameValue, @@ -366,9 +366,6 @@ async def _inner_fetch( base64.b64encode(post_data_buffer).decode() if post_data_buffer else None ) - def filter_none(input: Dict) -> Dict: - return {k: v for k, v in input.items() if v is not None} - response = await self._channel.send( "fetch", filter_none( diff --git a/playwright/_impl/_local_utils.py b/playwright/_impl/_local_utils.py index 10303008d..6b97386c4 100644 --- a/playwright/_impl/_local_utils.py +++ b/playwright/_impl/_local_utils.py @@ -13,10 +13,10 @@ # limitations under the License. import base64 -from typing import Dict, Optional, cast +from typing import Dict, List, Optional, cast from playwright._impl._api_structures import HeadersArray -from playwright._impl._connection import ChannelOwner +from playwright._impl._connection import ChannelOwner, StackFrame from playwright._impl._helper import HarLookupResult, locals_to_params @@ -57,3 +57,21 @@ async def har_close(self, harId: str) -> None: async def har_unzip(self, zipFile: str, harFile: str) -> None: params = locals_to_params(locals()) await self._channel.send("harUnzip", params) + + async def tracing_started(self, tracesDir: Optional[str], traceName: str) -> str: + params = locals_to_params(locals()) + return await self._channel.send("tracingStarted", params) + + async def trace_discarded(self, stacks_id: str) -> None: + return await self._channel.send("traceDiscarded", {"stacks_id": stacks_id}) + + def add_stack_to_tracing_no_reply(self, id: int, frames: List[StackFrame]) -> None: + self._channel.send_no_reply( + "addStackToTracingNoReply", + { + "callData": { + "stack": frames, + "id": id, + } + }, + ) diff --git a/playwright/_impl/_locator.py b/playwright/_impl/_locator.py index 990d055fd..7b288170b 100644 --- a/playwright/_impl/_locator.py +++ b/playwright/_impl/_locator.py @@ -37,6 +37,7 @@ FrameExpectResult, Position, ) +from playwright._impl._connection import filter_none from playwright._impl._element_handle import ElementHandle from playwright._impl._helper import ( Error, @@ -654,7 +655,7 @@ async def _expect( { "selector": self._selector, "expression": expression, - **({k: v for k, v in options.items() if v is not None}), + **(filter_none(options)), }, ) if result.get("received"): diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index 897cfbc14..fdd7571ad 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -59,7 +59,6 @@ ColorScheme, DocumentLoadState, ForcedColors, - HarContentPolicy, HarMode, KeyboardModifier, MouseButton, @@ -619,12 +618,16 @@ async def route_from_har( url: Union[Pattern[str], str] = None, not_found: RouteFromHarNotFoundPolicy = None, update: bool = None, - content: HarContentPolicy = None, - mode: HarMode = None, + update_content: Literal["attach", "embed"] = None, + update_mode: HarMode = None, ) -> None: if update: await self._browser_context._record_into_har( - har=har, page=self, url=url, content=content, mode=mode + har=har, + page=self, + url=url, + update_content=update_content, + update_mode=update_mode, ) return router = await HarRouter.create( @@ -686,7 +689,7 @@ async def close(self, runBeforeUnload: bool = None) -> None: if self._owned_context: await self._owned_context.close() except Exception as e: - if not is_safe_close_error(e): + if not is_safe_close_error(e) and not runBeforeUnload: raise e def is_closed(self) -> bool: diff --git a/playwright/_impl/_tracing.py b/playwright/_impl/_tracing.py index 3d117938a..509bbe336 100644 --- a/playwright/_impl/_tracing.py +++ b/playwright/_impl/_tracing.py @@ -13,10 +13,14 @@ # limitations under the License. import pathlib -from typing import Any, Dict, List, Optional, Union, cast +from typing import Dict, Optional, Union, cast from playwright._impl._artifact import Artifact -from playwright._impl._connection import ChannelOwner, from_nullable_channel +from playwright._impl._connection import ( + ChannelOwner, + filter_none, + from_nullable_channel, +) from playwright._impl._helper import locals_to_params @@ -26,7 +30,9 @@ def __init__( ) -> None: super().__init__(parent, type, guid, initializer) self._include_sources: bool = False - self._metadata_collector: List[Dict[str, Any]] = [] + self._stacks_id: Optional[str] = None + self._is_tracing: bool = False + self._traces_dir: Optional[str] = None async def start( self, @@ -38,18 +44,28 @@ async def start( ) -> None: params = locals_to_params(locals()) self._include_sources = bool(sources) - await self._channel.send("tracingStart", params) - await self._channel.send( - "tracingStartChunk", {"title": title} if title else None - ) - self._metadata_collector = [] - self._connection.start_collecting_call_metadata(self._metadata_collector) - async def start_chunk(self, title: str = None) -> None: + async def _inner_start() -> str: + await self._channel.send("tracingStart", params) + return await self._channel.send( + "tracingStartChunk", filter_none({"title": title, "name": name}) + ) + + trace_name = await self._connection.wrap_api_call(_inner_start) + await self._start_collecting_stacks(trace_name) + + async def start_chunk(self, title: str = None, name: str = None) -> None: params = locals_to_params(locals()) - await self._channel.send("tracingStartChunk", params) - self._metadata_collector = [] - self._connection.start_collecting_call_metadata(self._metadata_collector) + trace_name = await self._channel.send("tracingStartChunk", params) + await self._start_collecting_stacks(trace_name) + + async def _start_collecting_stacks(self, trace_name: str) -> None: + if not self._is_tracing: + self._is_tracing = True + self._connection.set_in_tracing(True) + self._stacks_id = await self._connection.local_utils.tracing_started( + self._traces_dir, trace_name + ) async def stop_chunk(self, path: Union[pathlib.Path, str] = None) -> None: await self._do_stop_chunk(path) @@ -59,14 +75,15 @@ async def stop(self, path: Union[pathlib.Path, str] = None) -> None: await self._channel.send("tracingStop") async def _do_stop_chunk(self, file_path: Union[pathlib.Path, str] = None) -> None: - if self._metadata_collector: - self._connection.stop_collecting_call_metadata(self._metadata_collector) - metadata = self._metadata_collector - self._metadata_collector = [] + if self._is_tracing: + self._is_tracing = False + self._connection.set_in_tracing(False) if not file_path: - await self._channel.send("tracingStopChunk", {"mode": "discard"}) # Not interested in any artifacts + await self._channel.send("tracingStopChunk", {"mode": "discard"}) + if self._stacks_id: + await self._connection.local_utils.trace_discarded(self._stacks_id) return is_local = not self._connection.is_remote @@ -79,7 +96,7 @@ async def _do_stop_chunk(self, file_path: Union[pathlib.Path, str] = None) -> No { "zipFile": str(file_path), "entries": result["entries"], - "metadata": metadata, + "stacksId": self._stacks_id, "mode": "write", "includeSources": self._include_sources, } @@ -100,20 +117,20 @@ async def _do_stop_chunk(self, file_path: Union[pathlib.Path, str] = None) -> No # The artifact may be missing if the browser closed while stopping tracing. if not artifact: + if self._stacks_id: + await self._connection.local_utils.trace_discarded(self._stacks_id) return # Save trace to the final local file. await artifact.save_as(file_path) await artifact.delete() - # Add local sources to the remote trace if necessary. - if len(metadata) > 0: - await self._connection.local_utils.zip( - { - "zipFile": str(file_path), - "entries": [], - "metadata": metadata, - "mode": "append", - "includeSources": self._include_sources, - } - ) + await self._connection.local_utils.zip( + { + "zipFile": str(file_path), + "entries": [], + "stacksId": self._stacks_id, + "mode": "append", + "includeSources": self._include_sources, + } + ) diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index 7dfcf336d..50631e826 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -4748,22 +4748,26 @@ def get_by_label( ) -> "Locator": """Frame.get_by_label - Allows locating input elements by the text of the associated label. + Allows locating input elements by the text of the associated `