From 0e92fbc2cceba0551305f2e36f75f9b71f5d5b6e Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Tue, 5 Sep 2023 19:07:46 +0200 Subject: [PATCH] feat(roll): roll to Playwright 1.38.0-alpha-sep-4-2023 (#2058) --- README.md | 2 +- playwright/_impl/_api_types.py | 18 +- playwright/_impl/_browser.py | 2 + playwright/_impl/_browser_context.py | 16 ++ playwright/_impl/_connection.py | 2 +- playwright/_impl/_frame.py | 22 +-- playwright/_impl/_helper.py | 4 +- playwright/_impl/_locator.py | 9 + playwright/_impl/_network.py | 46 +++-- playwright/_impl/_page.py | 8 +- playwright/_impl/_page_error.py | 36 ++++ playwright/async_api/_generated.py | 192 ++++++++++++++------- playwright/sync_api/_generated.py | 190 +++++++++++++------- scripts/documentation_provider.py | 4 +- scripts/expected_api_mismatch.txt | 5 - scripts/generate_api.py | 11 +- scripts/generate_async_api.py | 4 +- scripts/generate_sync_api.py | 4 +- setup.py | 2 +- tests/async/test_browsercontext_events.py | 8 + tests/async/test_locators.py | 6 + tests/async/test_network.py | 5 +- tests/async/test_page_network_request.py | 43 +++++ tests/async/test_page_request_intercept.py | 17 +- tests/async/test_popup.py | 1 + tests/async/test_request_fulfill.py | 10 +- tests/sync/test_request_fulfill.py | 6 +- 27 files changed, 486 insertions(+), 187 deletions(-) create mode 100644 playwright/_impl/_page_error.py create mode 100644 tests/async/test_page_network_request.py diff --git a/README.md b/README.md index 77a30804d..39d199a75 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 116.0.5845.82 | ✅ | ✅ | ✅ | +| Chromium 117.0.5938.35 | ✅ | ✅ | ✅ | | WebKit 17.0 | ✅ | ✅ | ✅ | | Firefox 115.0 | ✅ | ✅ | ✅ | diff --git a/playwright/_impl/_api_types.py b/playwright/_impl/_api_types.py index f4627ae7f..e921e9867 100644 --- a/playwright/_impl/_api_types.py +++ b/playwright/_impl/_api_types.py @@ -21,11 +21,23 @@ class Error(Exception): def __init__(self, message: str) -> None: - self.message = message - self.name: Optional[str] = None - self.stack: Optional[str] = None + self._message = message + self._name: Optional[str] = None + self._stack: Optional[str] = None super().__init__(message) + @property + def message(self) -> str: + return self._message + + @property + def name(self) -> Optional[str]: + return self._name + + @property + def stack(self) -> Optional[str]: + return self._stack + class TimeoutError(Error): pass diff --git a/playwright/_impl/_browser.py b/playwright/_impl/_browser.py index b58782614..79ed408bd 100644 --- a/playwright/_impl/_browser.py +++ b/playwright/_impl/_browser.py @@ -254,3 +254,5 @@ async def prepare_browser_context_params(params: Dict) -> None: params["reducedMotion"] = "no-override" if params.get("forcedColors", None) == "null": params["forcedColors"] = "no-override" + if "acceptDownloads" in params: + params["acceptDownloads"] = "accept" if params["acceptDownloads"] else "deny" diff --git a/playwright/_impl/_browser_context.py b/playwright/_impl/_browser_context.py index 270232b35..03920bcde 100644 --- a/playwright/_impl/_browser_context.py +++ b/playwright/_impl/_browser_context.py @@ -63,11 +63,13 @@ async_readfile, async_writefile, locals_to_params, + parse_error, prepare_record_har_options, to_impl, ) from playwright._impl._network import Request, Response, Route, serialize_headers from playwright._impl._page import BindingCall, Page, Worker +from playwright._impl._page_error import PageError from playwright._impl._tracing import Tracing from playwright._impl._wait_helper import WaitHelper @@ -87,6 +89,7 @@ class BrowserContext(ChannelOwner): Console="console", Dialog="dialog", Page="page", + PageError="pageerror", ServiceWorker="serviceworker", Request="request", Response="response", @@ -148,6 +151,13 @@ def __init__( self._channel.on( "dialog", lambda params: self._on_dialog(from_channel(params["dialog"])) ) + self._channel.on( + "pageError", + lambda params: self._on_page_error( + parse_error(params["error"]["error"]), + from_nullable_channel(params["page"]), + ), + ) self._channel.on( "request", lambda params: self._on_request( @@ -206,6 +216,7 @@ def _on_page(self, page: Page) -> None: page._opener.emit(Page.Events.Popup, page) async def _on_route(self, route: Route) -> None: + route._context = self route_handlers = self._routes.copy() for route_handler in route_handlers: if not route_handler.matches(route.request.url): @@ -555,6 +566,11 @@ def _on_dialog(self, dialog: Dialog) -> None: else: asyncio.create_task(dialog.dismiss()) + async def _on_page_error(self, error: Error, page: Optional[Page]) -> None: + self.emit(BrowserContext.Events.PageError, PageError(self._loop, page, error)) + if page: + page.emit(Page.Events.PageError, error) + def _on_request(self, request: Request, page: Optional[Page]) -> None: self.emit(BrowserContext.Events.Request, request) if page: diff --git a/playwright/_impl/_connection.py b/playwright/_impl/_connection.py index 5f906c47e..45e80b003 100644 --- a/playwright/_impl/_connection.py +++ b/playwright/_impl/_connection.py @@ -368,7 +368,7 @@ def dispatch(self, msg: ParsedMessagePayload) -> None: error = msg.get("error") if error: parsed_error = parse_error(error["error"]) # type: ignore - parsed_error.stack = "".join( + parsed_error._stack = "".join( traceback.format_list(callback.stack_trace)[-10:] ) callback.future.set_exception(parsed_error) diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py index 9cd12a1d2..b004d3cbc 100644 --- a/playwright/_impl/_frame.py +++ b/playwright/_impl/_frame.py @@ -82,7 +82,7 @@ def __init__( self._url = initializer["url"] self._detached = False self._child_frames: List[Frame] = [] - self._page: "Page" + self._page: Optional[Page] = None self._load_states: Set[str] = set(initializer["loadStates"]) self._event_emitter = EventEmitter() self._channel.on( @@ -105,26 +105,16 @@ def _on_load_state( self._event_emitter.emit("loadstate", add) elif remove and remove in self._load_states: self._load_states.remove(remove) - if ( - not self._parent_frame - and add == "load" - and hasattr(self, "_page") - and self._page - ): + if not self._parent_frame and add == "load" and self._page: self._page.emit("load", self._page) - if ( - not self._parent_frame - and add == "domcontentloaded" - and hasattr(self, "_page") - and self._page - ): + if not self._parent_frame and add == "domcontentloaded" and self._page: self._page.emit("domcontentloaded", self._page) def _on_frame_navigated(self, event: FrameNavigatedEvent) -> None: self._url = event["url"] self._name = event["name"] self._event_emitter.emit("navigated", event) - if "error" not in event and hasattr(self, "_page") and self._page: + if "error" not in event and self._page: self._page.emit("framenavigated", self) async def _query_count(self, selector: str) -> int: @@ -132,6 +122,7 @@ async def _query_count(self, selector: str) -> int: @property def page(self) -> "Page": + assert self._page return self._page async def goto( @@ -151,6 +142,7 @@ async def goto( def _setup_navigation_wait_helper( self, wait_name: str, timeout: float = None ) -> WaitHelper: + 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!") @@ -175,6 +167,7 @@ def expect_navigation( wait_until: DocumentLoadState = None, timeout: float = None, ) -> EventContextManagerImpl[Response]: + assert self._page if not wait_until: wait_until = "load" @@ -225,6 +218,7 @@ async def wait_for_url( wait_until: DocumentLoadState = None, timeout: float = None, ) -> None: + assert self._page matcher = URLMatcher(self._page._browser_context._options.get("baseURL"), url) if matcher.matches(self.url): await self._wait_for_load_state_impl(state=wait_until, timeout=timeout) diff --git a/playwright/_impl/_helper.py b/playwright/_impl/_helper.py index 0af327d11..5f8031127 100644 --- a/playwright/_impl/_helper.py +++ b/playwright/_impl/_helper.py @@ -222,8 +222,8 @@ def parse_error(error: ErrorPayload) -> Error: if error.get("name") == "TimeoutError": base_error_class = TimeoutError exc = base_error_class(cast(str, patch_error_message(error.get("message")))) - exc.name = error["name"] - exc.stack = error["stack"] + exc._name = error["name"] + exc._stack = error["stack"] return exc diff --git a/playwright/_impl/_locator.py b/playwright/_impl/_locator.py index 409489558..8c9a18f03 100644 --- a/playwright/_impl/_locator.py +++ b/playwright/_impl/_locator.py @@ -625,6 +625,15 @@ async def type( **params, ) + async def press_sequentially( + self, + text: str, + delay: float = None, + timeout: float = None, + noWaitAfter: bool = None, + ) -> None: + await self.type(text, delay=delay, timeout=timeout, noWaitAfter=noWaitAfter) + async def uncheck( self, position: Position = None, diff --git a/playwright/_impl/_network.py b/playwright/_impl/_network.py index 419d5793e..35234d286 100644 --- a/playwright/_impl/_network.py +++ b/playwright/_impl/_network.py @@ -61,6 +61,7 @@ from playwright._impl._wait_helper import WaitHelper if TYPE_CHECKING: # pragma: no cover + from playwright._impl._browser_context import BrowserContext from playwright._impl._fetch import APIResponse from playwright._impl._frame import Frame from playwright._impl._page import Page @@ -191,7 +192,20 @@ async def response(self) -> Optional["Response"]: @property def frame(self) -> "Frame": - return from_channel(self._initializer["frame"]) + if not self._initializer.get("frame"): + raise Error("Service Worker requests do not have an associated frame.") + frame = cast("Frame", from_channel(self._initializer["frame"])) + if not frame._page: + raise Error( + "\n".join( + [ + "Frame for this navigation request is not available, because the request", + "was issued before the frame is created. You can check whether the request", + "is a navigation request by calling isNavigationRequest() method.", + ] + ) + ) + return frame def is_navigation_request(self) -> bool: return self._initializer["isNavigationRequest"] @@ -244,9 +258,15 @@ async def _actual_headers(self) -> "RawHeaders": return await self._all_headers_future def _target_closed_future(self) -> asyncio.Future: - if not hasattr(self.frame, "_page"): + frame = cast( + Optional["Frame"], from_nullable_channel(self._initializer.get("frame")) + ) + if not frame: return asyncio.Future() - return self.frame._page._closed_or_crashed_future + page = frame._page + if not page: + return asyncio.Future() + return page._closed_or_crashed_future class Route(ChannelOwner): @@ -255,6 +275,7 @@ def __init__( ) -> None: super().__init__(parent, type, guid, initializer) self._handling_future: Optional[asyncio.Future["bool"]] = None + self._context: "BrowserContext" = cast("BrowserContext", None) def _start_handling(self) -> "asyncio.Future[bool]": self._handling_future = asyncio.Future() @@ -368,15 +389,16 @@ async def fetch( maxRedirects: int = None, timeout: float = None, ) -> "APIResponse": - page = self.request.frame._page - return await page.context.request._inner_fetch( - self.request, - url, - method, - headers, - postData, - maxRedirects=maxRedirects, - timeout=timeout, + return await self._connection.wrap_api_call( + lambda: self._context.request._inner_fetch( + self.request, + url, + method, + headers, + postData, + maxRedirects=maxRedirects, + timeout=timeout, + ) ) async def fallback( diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index 902f3b5f1..be2538689 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -75,7 +75,6 @@ is_safe_close_error, locals_to_params, make_dirs_for_file, - parse_error, serialize_error, ) from playwright._impl._input import Keyboard, Mouse, Touchscreen @@ -177,12 +176,6 @@ def __init__( "frameDetached", lambda params: self._on_frame_detached(from_channel(params["frame"])), ) - self._channel.on( - "pageError", - lambda params: self.emit( - Page.Events.PageError, parse_error(params["error"]["error"]) - ), - ) self._channel.on( "route", lambda params: asyncio.create_task( @@ -239,6 +232,7 @@ def _on_frame_detached(self, frame: Frame) -> None: self.emit(Page.Events.FrameDetached, frame) async def _on_route(self, route: Route) -> None: + route._context = self.context route_handlers = self._routes.copy() for route_handler in route_handlers: if not route_handler.matches(route.request.url): diff --git a/playwright/_impl/_page_error.py b/playwright/_impl/_page_error.py new file mode 100644 index 000000000..d57bbf9e2 --- /dev/null +++ b/playwright/_impl/_page_error.py @@ -0,0 +1,36 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from asyncio import AbstractEventLoop +from typing import Optional + +from playwright._impl._helper import Error +from playwright._impl._page import Page + + +class PageError: + def __init__( + self, loop: AbstractEventLoop, page: Optional[Page], error: Error + ) -> None: + self._loop = loop + self._page = page + self._error = error + + @property + def page(self) -> Optional[Page]: + return self._page + + @property + def error(self) -> Error: + return self._error diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index 1393ca13c..65c2d0f2c 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -79,6 +79,7 @@ from playwright._impl._network import WebSocket as WebSocketImpl from playwright._impl._page import Page as PageImpl from playwright._impl._page import Worker as WorkerImpl +from playwright._impl._page_error import PageError as PageErrorImpl from playwright._impl._playwright import Playwright as PlaywrightImpl from playwright._impl._selectors import Selectors as SelectorsImpl from playwright._impl._tracing import Tracing as TracingImpl @@ -169,6 +170,21 @@ def frame(self) -> "Frame": Returns the `Frame` that initiated this request. + **Usage** + + ```py + frame_url = request.frame.url + ``` + + **Details** + + Note that in some cases the frame is not available, and this method will throw. + - When request originates in the Service Worker. You can use `request.serviceWorker()` to check that. + - When navigation request is issued before the corresponding frame is created. You can use + `request.is_navigation_request()` to check that. + + Here is an example that handles all the cases: + Returns ------- Frame @@ -330,6 +346,9 @@ def is_navigation_request(self) -> bool: Whether this request is driving frame's navigation. + Some navigation requests are issued before the corresponding frame is created, and therefore do not have + `request.frame()` available. + Returns ------- bool @@ -2316,7 +2335,7 @@ async def fill( [control](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control), the control will be filled instead. - To send fine-grained keyboard events, use `element_handle.type()`. + To send fine-grained keyboard events, use `keyboard.type()`. Parameters ---------- @@ -2457,30 +2476,6 @@ async def type( **Usage** - ```py - await element_handle.type(\"hello\") # types instantly - await element_handle.type(\"world\", delay=100) # types slower, like a user - ``` - - ```py - element_handle.type(\"hello\") # types instantly - element_handle.type(\"world\", delay=100) # types slower, like a user - ``` - - An example of typing into a text field and then submitting the form: - - ```py - element_handle = await page.query_selector(\"input\") - await element_handle.type(\"some text\") - await element_handle.press(\"Enter\") - ``` - - ```py - element_handle = page.query_selector(\"input\") - element_handle.type(\"some text\") - element_handle.press(\"Enter\") - ``` - Parameters ---------- text : str @@ -5770,16 +5765,6 @@ async def type( **Usage** - ```py - await frame.type(\"#mytextarea\", \"hello\") # types instantly - await frame.type(\"#mytextarea\", \"world\", delay=100) # types slower, like a user - ``` - - ```py - frame.type(\"#mytextarea\", \"hello\") # types instantly - frame.type(\"#mytextarea\", \"world\", delay=100) # types slower, like a user - ``` - Parameters ---------- selector : str @@ -7273,6 +7258,16 @@ async def save_as(self, path: typing.Union[str, pathlib.Path]) -> None: Copy the download to a user-specified path. It is safe to call this method while the download is still in progress. Will wait for the download to finish if necessary. + **Usage** + + ```py + await download.save_as(\"/path/to/save/at/\" + download.suggested_filename) + ``` + + ```py + download.save_as(\"/path/to/save/at/\" + download.suggested_filename) + ``` + Parameters ---------- path : Union[pathlib.Path, str] @@ -11441,16 +11436,6 @@ async def type( **Usage** - ```py - await page.type(\"#mytextarea\", \"hello\") # types instantly - await page.type(\"#mytextarea\", \"world\", delay=100) # types slower, like a user - ``` - - ```py - page.type(\"#mytextarea\", \"hello\") # types instantly - page.type(\"#mytextarea\", \"world\", delay=100) # types slower, like a user - ``` - Parameters ---------- selector : str @@ -12499,6 +12484,35 @@ async def set_checked( mapping.register(PageImpl, Page) +class PageError(AsyncBase): + @property + def page(self) -> typing.Optional["Page"]: + """PageError.page + + The page that produced this unhandled exception, if any. + + Returns + ------- + Union[Page, None] + """ + return mapping.from_impl_nullable(self._impl_obj.page) + + @property + def error(self) -> "Error": + """PageError.error + + Unhandled error that was thrown. + + Returns + ------- + Error + """ + return mapping.from_impl(self._impl_obj.error) + + +mapping.register(PageErrorImpl, PageError) + + class BrowserContext(AsyncContextManager): @typing.overload def on( @@ -12622,6 +12636,16 @@ def on( **NOTE** Use `page.wait_for_load_state()` to wait until the page gets to a particular state (you should not need it in most cases).""" + @typing.overload + def on( + self, + event: Literal["pageerror"], + f: typing.Callable[["PageError"], "typing.Union[typing.Awaitable[None], None]"], + ) -> None: + """ + Emitted when unhandled exceptions occur on any pages created through this context. To only listen for `pageError` + events from a particular page, use `page.on('page_error')`.""" + @typing.overload def on( self, @@ -12811,6 +12835,16 @@ def once( **NOTE** Use `page.wait_for_load_state()` to wait until the page gets to a particular state (you should not need it in most cases).""" + @typing.overload + def once( + self, + event: Literal["pageerror"], + f: typing.Callable[["PageError"], "typing.Union[typing.Awaitable[None], None]"], + ) -> None: + """ + Emitted when unhandled exceptions occur on any pages created through this context. To only listen for `pageError` + events from a particular page, use `page.on('page_error')`.""" + @typing.overload def once( self, @@ -15968,7 +16002,7 @@ async def fill( [control](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control), the control will be filled instead. - To send fine-grained keyboard events, use `locator.type()`. + To send fine-grained keyboard events, use `locator.press_sequentially()`. Parameters ---------- @@ -17826,8 +17860,46 @@ async def type( ) -> None: """Locator.type - **NOTE** In most cases, you should use `locator.fill()` instead. You only need to type characters if there - is special keyboard handling on the page. + Focuses the element, and then sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the + text. + + To press a special key, like `Control` or `ArrowDown`, use `locator.press()`. + + **Usage** + + Parameters + ---------- + text : str + A text to type into a focused element. + delay : Union[float, None] + Time to wait between key presses in milliseconds. Defaults to 0. + timeout : Union[float, None] + Maximum time in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can + be changed by using the `browser_context.set_default_timeout()` or `page.set_default_timeout()` methods. + 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 + navigating to inaccessible pages. Defaults to `false`. + """ + + return mapping.from_maybe_impl( + await self._impl_obj.type( + text=text, delay=delay, timeout=timeout, noWaitAfter=no_wait_after + ) + ) + + async def press_sequentially( + self, + text: str, + *, + delay: typing.Optional[float] = None, + timeout: typing.Optional[float] = None, + no_wait_after: typing.Optional[bool] = None + ) -> None: + """Locator.press_sequentially + + **NOTE** In most cases, you should use `locator.fill()` instead. You only need to press keys one by one if + there is special keyboard handling on the page. Focuses the element, and then sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text. @@ -17837,33 +17909,33 @@ async def type( **Usage** ```py - await element.type(\"hello\") # types instantly - await element.type(\"world\", delay=100) # types slower, like a user + await locator.press_sequentially(\"hello\") # types instantly + await locator.press_sequentially(\"world\", delay=100) # types slower, like a user ``` ```py - element.type(\"hello\") # types instantly - element.type(\"world\", delay=100) # types slower, like a user + locator.press_sequentially(\"hello\") # types instantly + locator.press_sequentially(\"world\", delay=100) # types slower, like a user ``` An example of typing into a text field and then submitting the form: ```py - element = page.get_by_label(\"Password\") - await element.type(\"my password\") - await element.press(\"Enter\") + locator = page.get_by_label(\"Password\") + await locator.press_sequentially(\"my password\") + await locator.press(\"Enter\") ``` ```py - element = page.get_by_label(\"Password\") - element.type(\"my password\") - element.press(\"Enter\") + locator = page.get_by_label(\"Password\") + locator.press_sequentially(\"my password\") + locator.press(\"Enter\") ``` Parameters ---------- text : str - A text to type into a focused element. + String of characters to sequentially press into a focused element. delay : Union[float, None] Time to wait between key presses in milliseconds. Defaults to 0. timeout : Union[float, None] @@ -17876,7 +17948,7 @@ async def type( """ return mapping.from_maybe_impl( - await self._impl_obj.type( + await self._impl_obj.press_sequentially( text=text, delay=delay, timeout=timeout, noWaitAfter=no_wait_after ) ) diff --git a/playwright/sync_api/_generated.py b/playwright/sync_api/_generated.py index 66b989217..775bc81b3 100644 --- a/playwright/sync_api/_generated.py +++ b/playwright/sync_api/_generated.py @@ -73,6 +73,7 @@ from playwright._impl._network import WebSocket as WebSocketImpl from playwright._impl._page import Page as PageImpl from playwright._impl._page import Worker as WorkerImpl +from playwright._impl._page_error import PageError as PageErrorImpl from playwright._impl._playwright import Playwright as PlaywrightImpl from playwright._impl._selectors import Selectors as SelectorsImpl from playwright._impl._sync_base import ( @@ -169,6 +170,21 @@ def frame(self) -> "Frame": Returns the `Frame` that initiated this request. + **Usage** + + ```py + frame_url = request.frame.url + ``` + + **Details** + + Note that in some cases the frame is not available, and this method will throw. + - When request originates in the Service Worker. You can use `request.serviceWorker()` to check that. + - When navigation request is issued before the corresponding frame is created. You can use + `request.is_navigation_request()` to check that. + + Here is an example that handles all the cases: + Returns ------- Frame @@ -330,6 +346,9 @@ def is_navigation_request(self) -> bool: Whether this request is driving frame's navigation. + Some navigation requests are issued before the corresponding frame is created, and therefore do not have + `request.frame()` available. + Returns ------- bool @@ -2334,7 +2353,7 @@ def fill( [control](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control), the control will be filled instead. - To send fine-grained keyboard events, use `element_handle.type()`. + To send fine-grained keyboard events, use `keyboard.type()`. Parameters ---------- @@ -2481,30 +2500,6 @@ def type( **Usage** - ```py - await element_handle.type(\"hello\") # types instantly - await element_handle.type(\"world\", delay=100) # types slower, like a user - ``` - - ```py - element_handle.type(\"hello\") # types instantly - element_handle.type(\"world\", delay=100) # types slower, like a user - ``` - - An example of typing into a text field and then submitting the form: - - ```py - element_handle = await page.query_selector(\"input\") - await element_handle.type(\"some text\") - await element_handle.press(\"Enter\") - ``` - - ```py - element_handle = page.query_selector(\"input\") - element_handle.type(\"some text\") - element_handle.press(\"Enter\") - ``` - Parameters ---------- text : str @@ -5878,16 +5873,6 @@ def type( **Usage** - ```py - await frame.type(\"#mytextarea\", \"hello\") # types instantly - await frame.type(\"#mytextarea\", \"world\", delay=100) # types slower, like a user - ``` - - ```py - frame.type(\"#mytextarea\", \"hello\") # types instantly - frame.type(\"#mytextarea\", \"world\", delay=100) # types slower, like a user - ``` - Parameters ---------- selector : str @@ -7393,6 +7378,16 @@ def save_as(self, path: typing.Union[str, pathlib.Path]) -> None: Copy the download to a user-specified path. It is safe to call this method while the download is still in progress. Will wait for the download to finish if necessary. + **Usage** + + ```py + await download.save_as(\"/path/to/save/at/\" + download.suggested_filename) + ``` + + ```py + download.save_as(\"/path/to/save/at/\" + download.suggested_filename) + ``` + Parameters ---------- path : Union[pathlib.Path, str] @@ -11537,16 +11532,6 @@ def type( **Usage** - ```py - await page.type(\"#mytextarea\", \"hello\") # types instantly - await page.type(\"#mytextarea\", \"world\", delay=100) # types slower, like a user - ``` - - ```py - page.type(\"#mytextarea\", \"hello\") # types instantly - page.type(\"#mytextarea\", \"world\", delay=100) # types slower, like a user - ``` - Parameters ---------- selector : str @@ -12609,6 +12594,35 @@ def set_checked( mapping.register(PageImpl, Page) +class PageError(SyncBase): + @property + def page(self) -> typing.Optional["Page"]: + """PageError.page + + The page that produced this unhandled exception, if any. + + Returns + ------- + Union[Page, None] + """ + return mapping.from_impl_nullable(self._impl_obj.page) + + @property + def error(self) -> "Error": + """PageError.error + + Unhandled error that was thrown. + + Returns + ------- + Error + """ + return mapping.from_impl(self._impl_obj.error) + + +mapping.register(PageErrorImpl, PageError) + + class BrowserContext(SyncContextManager): @typing.overload def on( @@ -12716,6 +12730,14 @@ def on(self, event: Literal["page"], f: typing.Callable[["Page"], "None"]) -> No **NOTE** Use `page.wait_for_load_state()` to wait until the page gets to a particular state (you should not need it in most cases).""" + @typing.overload + def on( + self, event: Literal["pageerror"], f: typing.Callable[["PageError"], "None"] + ) -> None: + """ + Emitted when unhandled exceptions occur on any pages created through this context. To only listen for `pageError` + events from a particular page, use `page.on('page_error')`.""" + @typing.overload def on( self, event: Literal["request"], f: typing.Callable[["Request"], "None"] @@ -12877,6 +12899,14 @@ def once( **NOTE** Use `page.wait_for_load_state()` to wait until the page gets to a particular state (you should not need it in most cases).""" + @typing.overload + def once( + self, event: Literal["pageerror"], f: typing.Callable[["PageError"], "None"] + ) -> None: + """ + Emitted when unhandled exceptions occur on any pages created through this context. To only listen for `pageError` + events from a particular page, use `page.on('page_error')`.""" + @typing.overload def once( self, event: Literal["request"], f: typing.Callable[["Request"], "None"] @@ -16060,7 +16090,7 @@ def fill( [control](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/control), the control will be filled instead. - To send fine-grained keyboard events, use `locator.type()`. + To send fine-grained keyboard events, use `locator.press_sequentially()`. Parameters ---------- @@ -17954,8 +17984,48 @@ def type( ) -> None: """Locator.type - **NOTE** In most cases, you should use `locator.fill()` instead. You only need to type characters if there - is special keyboard handling on the page. + Focuses the element, and then sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the + text. + + To press a special key, like `Control` or `ArrowDown`, use `locator.press()`. + + **Usage** + + Parameters + ---------- + text : str + A text to type into a focused element. + delay : Union[float, None] + Time to wait between key presses in milliseconds. Defaults to 0. + timeout : Union[float, None] + Maximum time in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can + be changed by using the `browser_context.set_default_timeout()` or `page.set_default_timeout()` methods. + 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 + navigating to inaccessible pages. Defaults to `false`. + """ + + return mapping.from_maybe_impl( + self._sync( + self._impl_obj.type( + text=text, delay=delay, timeout=timeout, noWaitAfter=no_wait_after + ) + ) + ) + + def press_sequentially( + self, + text: str, + *, + delay: typing.Optional[float] = None, + timeout: typing.Optional[float] = None, + no_wait_after: typing.Optional[bool] = None + ) -> None: + """Locator.press_sequentially + + **NOTE** In most cases, you should use `locator.fill()` instead. You only need to press keys one by one if + there is special keyboard handling on the page. Focuses the element, and then sends a `keydown`, `keypress`/`input`, and `keyup` event for each character in the text. @@ -17965,33 +18035,33 @@ def type( **Usage** ```py - await element.type(\"hello\") # types instantly - await element.type(\"world\", delay=100) # types slower, like a user + await locator.press_sequentially(\"hello\") # types instantly + await locator.press_sequentially(\"world\", delay=100) # types slower, like a user ``` ```py - element.type(\"hello\") # types instantly - element.type(\"world\", delay=100) # types slower, like a user + locator.press_sequentially(\"hello\") # types instantly + locator.press_sequentially(\"world\", delay=100) # types slower, like a user ``` An example of typing into a text field and then submitting the form: ```py - element = page.get_by_label(\"Password\") - await element.type(\"my password\") - await element.press(\"Enter\") + locator = page.get_by_label(\"Password\") + await locator.press_sequentially(\"my password\") + await locator.press(\"Enter\") ``` ```py - element = page.get_by_label(\"Password\") - element.type(\"my password\") - element.press(\"Enter\") + locator = page.get_by_label(\"Password\") + locator.press_sequentially(\"my password\") + locator.press(\"Enter\") ``` Parameters ---------- text : str - A text to type into a focused element. + String of characters to sequentially press into a focused element. delay : Union[float, None] Time to wait between key presses in milliseconds. Defaults to 0. timeout : Union[float, None] @@ -18005,7 +18075,7 @@ def type( return mapping.from_maybe_impl( self._sync( - self._impl_obj.type( + self._impl_obj.press_sequentially( text=text, delay=delay, timeout=timeout, noWaitAfter=no_wait_after ) ) diff --git a/scripts/documentation_provider.py b/scripts/documentation_provider.py index 866e9887b..f625143ea 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" @@ -472,6 +472,8 @@ def print_remainder(self) -> None: for [member_name, member] in clazz["members"].items(): if member.get("deprecated"): continue + if class_name in ["Error"]: + continue entry = f"{class_name}.{member_name}" if entry not in self.printed_entries: self.errors.add(f"Method not implemented: {entry}") diff --git a/scripts/expected_api_mismatch.txt b/scripts/expected_api_mismatch.txt index e42b74650..47c084c61 100644 --- a/scripts/expected_api_mismatch.txt +++ b/scripts/expected_api_mismatch.txt @@ -12,8 +12,3 @@ Parameter type mismatch in BrowserContext.route(handler=): documented as Callabl Parameter type mismatch in BrowserContext.unroute(handler=): documented as Union[Callable[[Route, Request], Union[Any, Any]], None], code has Union[Callable[[Route, Request], Any], Callable[[Route], Any], None] Parameter type mismatch in Page.route(handler=): documented as Callable[[Route, Request], Union[Any, Any]], code has Union[Callable[[Route, Request], Any], Callable[[Route], Any]] Parameter type mismatch in Page.unroute(handler=): documented as Union[Callable[[Route, Request], Union[Any, Any]], None], code has Union[Callable[[Route, Request], Any], Callable[[Route], Any], None] - -# Temporary Fix -Method not implemented: Error.name -Method not implemented: Error.stack -Method not implemented: Error.message diff --git a/scripts/generate_api.py b/scripts/generate_api.py index a84327eac..bfd850593 100644 --- a/scripts/generate_api.py +++ b/scripts/generate_api.py @@ -45,12 +45,13 @@ from playwright._impl._fetch import APIRequest, APIRequestContext, APIResponse from playwright._impl._file_chooser import FileChooser from playwright._impl._frame import Frame -from playwright._impl._helper import to_snake_case +from playwright._impl._helper import Error, to_snake_case from playwright._impl._input import Keyboard, Mouse, Touchscreen from playwright._impl._js_handle import JSHandle, Serializable from playwright._impl._locator import FrameLocator, Locator from playwright._impl._network import Request, Response, Route, WebSocket from playwright._impl._page import Page, Worker +from playwright._impl._page_error import PageError from playwright._impl._playwright import Playwright from playwright._impl._selectors import Selectors from playwright._impl._tracing import Tracing @@ -239,6 +240,7 @@ def return_value(value: Any) -> List[str]: from playwright._impl._js_handle import JSHandle as JSHandleImpl from playwright._impl._network import Request as RequestImpl, Response as ResponseImpl, Route as RouteImpl, WebSocket as WebSocketImpl from playwright._impl._page import Page as PageImpl, Worker as WorkerImpl +from playwright._impl._page_error import PageError as PageErrorImpl from playwright._impl._playwright import Playwright as PlaywrightImpl from playwright._impl._selectors import Selectors as SelectorsImpl from playwright._impl._video import Video as VideoImpl @@ -250,7 +252,7 @@ def return_value(value: Any) -> List[str]: """ -all_types = [ +generated_types = [ Request, Response, Route, @@ -271,6 +273,7 @@ def return_value(value: Any) -> List[str]: Download, Video, Page, + PageError, BrowserContext, CDPSession, Browser, @@ -286,6 +289,10 @@ def return_value(value: Any) -> List[str]: APIResponseAssertions, ] +all_types = generated_types + [ + Error, +] + api_globals = globals() assert Serializable diff --git a/scripts/generate_async_api.py b/scripts/generate_async_api.py index f4d96a994..d3579a91c 100755 --- a/scripts/generate_async_api.py +++ b/scripts/generate_async_api.py @@ -20,9 +20,9 @@ from scripts.documentation_provider import DocumentationProvider from scripts.generate_api import ( - all_types, api_globals, arguments, + generated_types, get_type_hints, header, process_type, @@ -131,7 +131,7 @@ def main() -> None: "from playwright._impl._async_base import AsyncEventContextManager, AsyncBase, AsyncContextManager, mapping" ) - for t in all_types: + for t in generated_types: generate(t) documentation_provider.print_remainder() diff --git a/scripts/generate_sync_api.py b/scripts/generate_sync_api.py index 70c262c75..a932fa8a4 100755 --- a/scripts/generate_sync_api.py +++ b/scripts/generate_sync_api.py @@ -21,9 +21,9 @@ from scripts.documentation_provider import DocumentationProvider from scripts.generate_api import ( - all_types, api_globals, arguments, + generated_types, get_type_hints, header, process_type, @@ -132,7 +132,7 @@ def main() -> None: "from playwright._impl._sync_base import EventContextManager, SyncBase, SyncContextManager, mapping" ) - for t in all_types: + for t in generated_types: generate(t) documentation_provider.print_remainder() diff --git a/setup.py b/setup.py index c935ae536..12056ccef 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.37.0" +driver_version = "1.38.0-alpha-sep-4-2023" def extractall(zip: zipfile.ZipFile, path: str) -> None: diff --git a/tests/async/test_browsercontext_events.py b/tests/async/test_browsercontext_events.py index ff37015dc..e65b0750c 100644 --- a/tests/async/test_browsercontext_events.py +++ b/tests/async/test_browsercontext_events.py @@ -188,3 +188,11 @@ async def test_console_event_should_work_with_context_manager(page: Page) -> Non message = await cm_info.value assert message.text == "hello" assert message.page == page + + +async def test_page_error_event_should_work(page: Page) -> None: + async with page.context.expect_event("pageerror") as page_error_info: + await page.set_content('') + page_error = await page_error_info.value + assert page_error.page == page + assert "boom" in page_error.error.stack diff --git a/tests/async/test_locators.py b/tests/async/test_locators.py index 0630fadad..50dc91cfb 100644 --- a/tests/async/test_locators.py +++ b/tests/async/test_locators.py @@ -375,6 +375,12 @@ async def test_locators_should_type(page: Page): assert await page.eval_on_selector("input", "input => input.value") == "hello" +async def test_locators_should_press_sequentially(page: Page): + await page.set_content("") + await page.locator("input").press_sequentially("hello") + assert await page.eval_on_selector("input", "input => input.value") == "hello" + + async def test_locators_should_screenshot( page: Page, server: Server, assert_to_be_golden ): diff --git a/tests/async/test_network.py b/tests/async/test_network.py index f118fe384..f4072fff4 100644 --- a/tests/async/test_network.py +++ b/tests/async/test_network.py @@ -605,7 +605,10 @@ def handle_request(request): == "Server returned nothing (no headers, no data)" ) else: - assert failed_requests[0].failure == "Message Corrupt" + assert failed_requests[0].failure in [ + "Message Corrupt", + "Connection terminated unexpectedly", + ] else: assert failed_requests[0].failure == "NS_ERROR_NET_RESET" assert failed_requests[0].frame diff --git a/tests/async/test_page_network_request.py b/tests/async/test_page_network_request.py new file mode 100644 index 000000000..f2a1383ba --- /dev/null +++ b/tests/async/test_page_network_request.py @@ -0,0 +1,43 @@ +# Copyright (c) Microsoft Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio + +import pytest + +from playwright.async_api import Error, Page, Request +from tests.server import Server + + +async def test_should_not_allow_to_access_frame_on_popup_main_request( + page: Page, server: Server +): + await page.set_content(f'click me') + request_promise = asyncio.ensure_future(page.context.wait_for_event("request")) + popup_promise = asyncio.ensure_future(page.context.wait_for_event("page")) + clicked = asyncio.ensure_future(page.get_by_text("click me").click()) + request: Request = await request_promise + + assert request.is_navigation_request() + + with pytest.raises(Error) as exc_info: + request.frame + assert ( + "Frame for this navigation request is not available" in exc_info.value.message + ) + + response = await request.response() + await response.finished() + await popup_promise + await clicked diff --git a/tests/async/test_page_request_intercept.py b/tests/async/test_page_request_intercept.py index 09e6f56ce..2491645c6 100644 --- a/tests/async/test_page_request_intercept.py +++ b/tests/async/test_page_request_intercept.py @@ -16,7 +16,7 @@ import pytest -from playwright.async_api import Page, Route +from playwright.async_api import Page, Route, expect from tests.server import Server @@ -79,3 +79,18 @@ async def handle(route: Route): await page.goto(server.PREFIX + "/empty.html") request = await request_promise assert request.post_body.decode("utf-8") == '{"foo":"bar"}' + + +async def test_should_fulfill_popup_main_request_using_alias( + page: Page, server: Server +): + async def route_handler(route: Route): + response = await route.fetch() + await route.fulfill(response=response, body="hello") + + await page.context.route("**/*", route_handler) + await page.set_content(f'click me') + [popup, _] = await asyncio.gather( + page.wait_for_event("popup"), page.get_by_text("click me").click() + ) + await expect(popup.locator("body")).to_have_text("hello") diff --git a/tests/async/test_popup.py b/tests/async/test_popup.py index 68ed1273d..42e4c29e5 100644 --- a/tests/async/test_popup.py +++ b/tests/async/test_popup.py @@ -396,6 +396,7 @@ async def test_should_work_with_clicking_target__blank(context, server): popup = await popup_info.value assert await page.evaluate("!!window.opener") is False assert await popup.evaluate("!!window.opener") + assert popup.main_frame.page == popup async def test_should_work_with_fake_clicking_target__blank_and_rel_noopener( diff --git a/tests/async/test_request_fulfill.py b/tests/async/test_request_fulfill.py index b48ae8047..3b5fa99e5 100644 --- a/tests/async/test_request_fulfill.py +++ b/tests/async/test_request_fulfill.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json - from playwright.async_api import Page, Route from tests.server import Server @@ -39,9 +37,7 @@ async def handle(route: Route) -> None: assert response assert response.status == 201 assert response.headers["content-type"] == "application/json" - assert json.loads(await page.evaluate("document.body.textContent")) == { - "bar": "baz" - } + assert await response.json() == {"bar": "baz"} async def test_should_fulfill_json_overriding_existing_response( @@ -73,6 +69,4 @@ async def handle(route: Route) -> None: assert response.headers["content-type"] == "application/json" assert response.headers["foo"] == "bar" assert original["tags"] == ["a", "b"] - assert json.loads(await page.evaluate("document.body.textContent")) == { - "tags": ["c"] - } + assert await response.json() == {"tags": ["c"]} diff --git a/tests/sync/test_request_fulfill.py b/tests/sync/test_request_fulfill.py index d51737389..569cf5e2c 100644 --- a/tests/sync/test_request_fulfill.py +++ b/tests/sync/test_request_fulfill.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json - from playwright.sync_api import Page, Route from tests.server import Server @@ -40,7 +38,7 @@ def handle(route: Route) -> None: assert response assert response.status == 201 assert response.headers["content-type"] == "application/json" - assert json.loads(page.evaluate("document.body.textContent")) == {"bar": "baz"} + assert response.json() == {"bar": "baz"} def test_should_fulfill_json_overriding_existing_response( @@ -72,4 +70,4 @@ def handle(route: Route) -> None: assert response.headers["content-type"] == "application/json" assert response.headers["foo"] == "bar" assert original["tags"] == ["a", "b"] - assert json.loads(page.evaluate("document.body.textContent")) == {"tags": ["c"]} + assert response.json() == {"tags": ["c"]}