diff --git a/README.md b/README.md index 3bd3ec79d..c7b9f0639 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 111.0.5563.19 | ✅ | ✅ | ✅ | +| Chromium 112.0.5615.20 | ✅ | ✅ | ✅ | | WebKit 16.4 | ✅ | ✅ | ✅ | -| Firefox 109.0 | ✅ | ✅ | ✅ | +| Firefox 110.0.1 | ✅ | ✅ | ✅ | ## Documentation diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py index 4c1262e5f..42d59b7ee 100644 --- a/playwright/_impl/_browser_context.py +++ b/playwright/_impl/_browser_context.py @@ -48,6 +48,8 @@ from playwright._impl._frame import Frame from playwright._impl._har_router import HarRouter from playwright._impl._helper import ( + HarContentPolicy, + HarMode, HarRecordingMetadata, RouteFromHarNotFoundPolicy, RouteHandler, @@ -326,13 +328,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, ) -> None: params: Dict[str, Any] = { "options": prepare_record_har_options( { "recordHarPath": har, - "recordHarContent": "attach", - "recordHarMode": "minimal", + "recordHarContent": content or "attach", + "recordHarMode": mode or "minimal", "recordHarUrlFilter": url, } ) @@ -340,7 +344,7 @@ 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": "attach"} + self._har_recorders[har_id] = {"path": str(har), "content": content or "attach"} async def route_from_har( self, @@ -348,9 +352,13 @@ async def route_from_har( url: Union[Pattern[str], str] = None, not_found: RouteFromHarNotFoundPolicy = None, update: bool = None, + content: HarContentPolicy = None, + mode: HarMode = None, ) -> None: if update: - await self._record_into_har(har=har, page=None, url=url) + await self._record_into_har( + har=har, page=None, url=url, content=content, mode=mode + ) return router = await HarRouter.create( local_utils=self._connection.local_utils, diff --git a/playwright/_impl/_connection.py b/playwright/_impl/_connection.py index 08652e3e4..88bf1b5a0 100644 --- a/playwright/_impl/_connection.py +++ b/playwright/_impl/_connection.py @@ -14,6 +14,7 @@ import asyncio import contextvars +import datetime import inspect import sys import traceback @@ -33,6 +34,12 @@ from playwright._impl._playwright import Playwright +if sys.version_info >= (3, 8): # pragma: no cover + from typing import TypedDict +else: # pragma: no cover + from typing_extensions import TypedDict + + class Channel(AsyncIOEventEmitter): def __init__(self, connection: "Connection", guid: str) -> None: super().__init__() @@ -220,10 +227,11 @@ def __init__( self._error: Optional[BaseException] = None self.is_remote = False self._init_task: Optional[asyncio.Task] = None - self._api_zone: contextvars.ContextVar[Optional[Dict]] = contextvars.ContextVar( - "ApiZone", default=None - ) + self._api_zone: contextvars.ContextVar[ + Optional[ParsedStackTrace] + ] = contextvars.ContextVar("ApiZone", default=None) self._local_utils: Optional["LocalUtils"] = local_utils + self._stack_collector: List[List[Dict[str, Any]]] = [] @property def local_utils(self) -> "LocalUtils": @@ -271,6 +279,13 @@ 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 _send_message_to_server( self, guid: str, method: str, params: Dict ) -> ProtocolCallback: @@ -283,12 +298,30 @@ def _send_message_to_server( getattr(task, "__pw_stack_trace__", traceback.extract_stack()), ) 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 = ( + { + "file": frames[0]["file"], + "line": frames[0]["line"], + "column": frames[0]["column"], + } + if len(frames) > 0 + else None + ) message = { "id": id, "guid": guid, "method": method, "params": self._replace_channels_with_guids(params), - "metadata": self._api_zone.get(), + "metadata": { + "wallTime": int(datetime.datetime.now().timestamp() * 1000), + "apiName": stack_trace_information["apiName"], + "location": location, + "internal": not stack_trace_information["apiName"], + }, } self._transport.send(message) self._callbacks[id] = callback @@ -412,9 +445,7 @@ async def wrap_api_call( return await cb() task = asyncio.current_task(self._loop) st: List[inspect.FrameInfo] = getattr(task, "__pw_stack__", inspect.stack()) - metadata = _extract_metadata_from_stack(st, is_internal) - if metadata: - self._api_zone.set(metadata) + self._api_zone.set(_extract_stack_trace_information_from_stack(st, is_internal)) try: return await cb() finally: @@ -427,9 +458,7 @@ def wrap_api_call_sync( return cb() task = asyncio.current_task(self._loop) st: List[inspect.FrameInfo] = getattr(task, "__pw_stack__", inspect.stack()) - metadata = _extract_metadata_from_stack(st, is_internal) - if metadata: - self._api_zone.set(metadata) + self._api_zone.set(_extract_stack_trace_information_from_stack(st, is_internal)) try: return cb() finally: @@ -444,19 +473,25 @@ def from_nullable_channel(channel: Optional[Channel]) -> Optional[Any]: return channel._object if channel else None -def _extract_metadata_from_stack( +class StackFrame(TypedDict): + file: str + line: int + column: int + function: Optional[str] + + +class ParsedStackTrace(TypedDict): + frames: List[StackFrame] + apiName: Optional[str] + + +def _extract_stack_trace_information_from_stack( st: List[inspect.FrameInfo], is_internal: bool -) -> Optional[Dict]: - if is_internal: - return { - "apiName": "", - "stack": [], - "internal": True, - } +) -> Optional[ParsedStackTrace]: playwright_module_path = str(Path(playwright.__file__).parents[0]) last_internal_api_name = "" api_name = "" - stack: List[Dict] = [] + parsed_frames: List[StackFrame] = [] for frame in st: is_playwright_internal = frame.filename.startswith(playwright_module_path) @@ -466,10 +501,11 @@ def _extract_metadata_from_stack( method_name += frame[0].f_code.co_name if not is_playwright_internal: - stack.append( + parsed_frames.append( { "file": frame.filename, "line": frame.lineno, + "column": 0, "function": method_name, } ) @@ -480,9 +516,8 @@ def _extract_metadata_from_stack( last_internal_api_name = "" if not api_name: api_name = last_internal_api_name - if api_name: - return { - "apiName": api_name, - "stack": stack, - } - return None + + return { + "frames": parsed_frames, + "apiName": "" if is_internal else api_name, + } diff --git a/playwright/_impl/_local_utils.py b/playwright/_impl/_local_utils.py index 73870ae64..10303008d 100644 --- a/playwright/_impl/_local_utils.py +++ b/playwright/_impl/_local_utils.py @@ -13,9 +13,9 @@ # limitations under the License. import base64 -from typing import Dict, List, Optional, cast +from typing import Dict, Optional, cast -from playwright._impl._api_structures import HeadersArray, NameValue +from playwright._impl._api_structures import HeadersArray from playwright._impl._connection import ChannelOwner from playwright._impl._helper import HarLookupResult, locals_to_params @@ -26,8 +26,8 @@ def __init__( ) -> None: super().__init__(parent, type, guid, initializer) - async def zip(self, zip_file: str, entries: List[NameValue]) -> None: - await self._channel.send("zip", {"zipFile": zip_file, "entries": entries}) + async def zip(self, params: Dict) -> None: + await self._channel.send("zip", params) async def har_open(self, file: str) -> None: params = locals_to_params(locals()) diff --git a/playwright/_impl/_locator.py b/playwright/_impl/_locator.py index c70691c07..55b1df75a 100644 --- a/playwright/_impl/_locator.py +++ b/playwright/_impl/_locator.py @@ -44,6 +44,7 @@ MouseButton, locals_to_params, monotonic_time, + to_impl, ) from playwright._impl._js_handle import Serializable, parse_value, serialize_argument from playwright._impl._str_utils import ( @@ -207,13 +208,23 @@ async def clear( def locator( self, - selector: str, + selector_or_locator: Union[str, "Locator"], has_text: Union[str, Pattern[str]] = None, has: "Locator" = None, ) -> "Locator": + if isinstance(selector_or_locator, str): + return Locator( + self._frame, + f"{self._selector} >> {selector_or_locator}", + has_text=has_text, + has=has, + ) + selector_or_locator = to_impl(selector_or_locator) + if selector_or_locator._frame != self._frame: + raise Error("Locators must belong to the same frame.") return Locator( self._frame, - f"{self._selector} >> {selector}", + f"{self._selector} >> {selector_or_locator._selector}", has_text=has_text, has=has, ) @@ -663,13 +674,23 @@ def __init__(self, frame: "Frame", frame_selector: str) -> None: def locator( self, - selector: str, + selector_or_locator: Union["Locator", str], has_text: Union[str, Pattern[str]] = None, has: "Locator" = None, ) -> Locator: + if isinstance(selector_or_locator, str): + return Locator( + self._frame, + f"{self._frame_selector} >> internal:control=enter-frame >> {selector_or_locator}", + has_text=has_text, + has=has, + ) + selector_or_locator = to_impl(selector_or_locator) + if selector_or_locator._frame != self._frame: + raise ValueError("Locators must belong to the same frame.") return Locator( self._frame, - f"{self._frame_selector} >> internal:control=enter-frame >> {selector}", + f"{self._frame_selector} >> internal:control=enter-frame >> {selector_or_locator._selector}", has_text=has_text, has=has, ) diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index 22a8f72c6..897cfbc14 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -59,6 +59,8 @@ ColorScheme, DocumentLoadState, ForcedColors, + HarContentPolicy, + HarMode, KeyboardModifier, MouseButton, ReducedMotion, @@ -617,9 +619,13 @@ async def route_from_har( url: Union[Pattern[str], str] = None, not_found: RouteFromHarNotFoundPolicy = None, update: bool = None, + content: HarContentPolicy = None, + mode: HarMode = None, ) -> None: if update: - await self._browser_context._record_into_har(har=har, page=self, url=url) + await self._browser_context._record_into_har( + har=har, page=self, url=url, content=content, mode=mode + ) return router = await HarRouter.create( local_utils=self._connection.local_utils, diff --git a/playwright/_impl/_tracing.py b/playwright/_impl/_tracing.py index b6b0025ed..3d117938a 100644 --- a/playwright/_impl/_tracing.py +++ b/playwright/_impl/_tracing.py @@ -13,7 +13,7 @@ # limitations under the License. import pathlib -from typing import Dict, Optional, Union, cast +from typing import Any, Dict, List, Optional, Union, cast from playwright._impl._artifact import Artifact from playwright._impl._connection import ChannelOwner, from_nullable_channel @@ -25,6 +25,8 @@ def __init__( self, parent: ChannelOwner, type: str, guid: str, initializer: Dict ) -> None: super().__init__(parent, type, guid, initializer) + self._include_sources: bool = False + self._metadata_collector: List[Dict[str, Any]] = [] async def start( self, @@ -35,12 +37,19 @@ async def start( sources: bool = None, ) -> None: params = locals_to_params(locals()) + self._include_sources = bool(sources) await self._channel.send("tracingStart", params) - await self.start_chunk(title) + 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: params = locals_to_params(locals()) await self._channel.send("tracingStartChunk", params) + self._metadata_collector = [] + self._connection.start_collecting_call_metadata(self._metadata_collector) async def stop_chunk(self, path: Union[pathlib.Path, str] = None) -> None: await self._do_stop_chunk(path) @@ -50,32 +59,47 @@ 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 not file_path: + await self._channel.send("tracingStopChunk", {"mode": "discard"}) + # Not interested in any artifacts + return + is_local = not self._connection.is_remote - mode = "doNotSave" - if file_path: - if is_local: - mode = "compressTraceAndSources" - else: - mode = "compressTrace" + if is_local: + result = await self._channel.send_return_as_dict( + "tracingStopChunk", {"mode": "entries"} + ) + await self._connection.local_utils.zip( + { + "zipFile": str(file_path), + "entries": result["entries"], + "metadata": metadata, + "mode": "write", + "includeSources": self._include_sources, + } + ) + return result = await self._channel.send_return_as_dict( "tracingStopChunk", { - "mode": mode, + "mode": "archive", }, ) - if not file_path: - # Not interested in artifacts. - return artifact = cast( Optional[Artifact], from_nullable_channel(result.get("artifact")), ) + # The artifact may be missing if the browser closed while stopping tracing. if not artifact: - # The artifact may be missing if the browser closed while stopping tracing. return # Save trace to the final local file. @@ -83,7 +107,13 @@ async def _do_stop_chunk(self, file_path: Union[pathlib.Path, str] = None) -> No await artifact.delete() # Add local sources to the remote trace if necessary. - if result.get("sourceEntries", []): + if len(metadata) > 0: await self._connection.local_utils.zip( - str(file_path), result["sourceEntries"] + { + "zipFile": str(file_path), + "entries": [], + "metadata": metadata, + "mode": "append", + "includeSources": self._include_sources, + } ) diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index e8b8757ee..7dfcf336d 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -1943,8 +1943,8 @@ async def scroll_into_view_if_needed( Parameters ---------- timeout : Union[float, None] - Maximum time in milliseconds, defaults to 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. + 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. """ return mapping.from_maybe_impl( @@ -1985,8 +1985,8 @@ async def hover( A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of the element. timeout : Union[float, None] - Maximum time in milliseconds, defaults to 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. + 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. no_wait_after : Union[bool, None] Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as @@ -2052,8 +2052,8 @@ async def click( click_count : Union[int, None] defaults to 1. See [UIEvent.detail]. timeout : Union[float, None] - Maximum time in milliseconds, defaults to 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. + 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. force : Union[bool, None] Whether to bypass the [actionability](../actionability.md) checks. Defaults to `false`. no_wait_after : Union[bool, None] @@ -2122,8 +2122,8 @@ async def dblclick( button : Union["left", "middle", "right", None] Defaults to `left`. timeout : Union[float, None] - Maximum time in milliseconds, defaults to 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. + 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. force : Union[bool, None] Whether to bypass the [actionability](../actionability.md) checks. Defaults to `false`. no_wait_after : Union[bool, None] @@ -2208,8 +2208,8 @@ async def select_option( element : Union[ElementHandle, List[ElementHandle], None] Option elements to select. Optional. timeout : Union[float, None] - Maximum time in milliseconds, defaults to 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. + 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. force : Union[bool, None] Whether to bypass the [actionability](../actionability.md) checks. Defaults to `false`. no_wait_after : Union[bool, None] @@ -2270,8 +2270,8 @@ async def tap( A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of the element. timeout : Union[float, None] - Maximum time in milliseconds, defaults to 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. + 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. force : Union[bool, None] Whether to bypass the [actionability](../actionability.md) checks. Defaults to `false`. no_wait_after : Union[bool, None] @@ -2319,8 +2319,8 @@ async def fill( value : str Value to set for the ``, `