From 18821954bbc3aca750d24b7befadb31eeffdd6a4 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Thu, 29 Jul 2021 08:27:22 +0200 Subject: [PATCH 001/672] chore: follow up for d59301b for waitForEventInfo handling --- playwright/_impl/_wait_helper.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/playwright/_impl/_wait_helper.py b/playwright/_impl/_wait_helper.py index f92445dcb..fb6fb5795 100644 --- a/playwright/_impl/_wait_helper.py +++ b/playwright/_impl/_wait_helper.py @@ -47,14 +47,16 @@ def _wait_for_event_info_before(self, wait_id: str, event: str) -> None: def _wait_for_event_info_after(self, wait_id: str, error: Exception = None) -> None: try: + info = { + "waitId": wait_id, + "phase": "after", + } + if error: + info["error"] = str(error) self._channel.send_no_reply( "waitForEventInfo", { - "info": { - "waitId": wait_id, - "phase": "after", - "error": str(error) if error else None, - } + "info": info, }, ) except Exception: From bb9b09fd9797c50cb64b051c9edf36816aa03727 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Thu, 29 Jul 2021 11:02:32 +0200 Subject: [PATCH 002/672] chore: support client side event logging (#827) --- playwright/_impl/_frame.py | 13 ++++++++- playwright/_impl/_page.py | 46 +++++++++++++++++++++++++++++--- playwright/_impl/_wait_helper.py | 32 ++++++++++++++++++++++ 3 files changed, 86 insertions(+), 5 deletions(-) diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py index 2cb31df80..7622c42d1 100644 --- a/playwright/_impl/_frame.py +++ b/playwright/_impl/_frame.py @@ -153,6 +153,9 @@ def expect_navigation( timeout = self._page._timeout_settings.navigation_timeout() deadline = monotonic_time() + timeout wait_helper = self._setup_navigation_wait_helper("expect_navigation", timeout) + + to_url = f' to "{url}"' if url else "" + wait_helper.log(f"waiting for navigation{to_url} until '{wait_until}'") matcher = ( URLMatcher(self._page._browser_context._options.get("baseURL"), url) if url @@ -163,6 +166,7 @@ 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"]}"') return not matcher or matcher.matches(event["url"]) wait_helper.wait_for_event( @@ -211,8 +215,15 @@ async def wait_for_load_state( if state in self._load_states: return wait_helper = self._setup_navigation_wait_helper("wait_for_load_state", timeout) + + def handle_load_state_event(actual_state: str) -> bool: + wait_helper.log(f'"{actual_state}" event fired') + return actual_state == state + wait_helper.wait_for_event( - self._event_emitter, "loadstate", lambda s: s == state + self._event_emitter, + "loadstate", + handle_load_state_event, ) await wait_helper.result() diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index ed756bf38..960de6541 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -15,6 +15,7 @@ import asyncio import base64 import inspect +import re import sys from pathlib import Path from types import SimpleNamespace @@ -787,6 +788,17 @@ def expect_event( event: str, predicate: Callable = None, timeout: float = None, + ) -> EventContextManagerImpl: + return self._expect_event( + event, predicate, timeout, f'waiting for event "{event}"' + ) + + def _expect_event( + self, + event: str, + predicate: Callable = None, + timeout: float = None, + log_line: str = None, ) -> EventContextManagerImpl: if timeout is None: timeout = self._timeout_settings.timeout() @@ -794,6 +806,8 @@ def expect_event( wait_helper.reject_on_timeout( timeout, f'Timeout while waiting for event "{event}"' ) + if log_line: + wait_helper.log(log_line) if event != Page.Events.Crash: wait_helper.reject_on_event(self, Page.Events.Crash, Error("Page crashed")) if event != Page.Events.Close: @@ -858,8 +872,13 @@ def my_predicate(request: Request) -> bool: return predicate(request) return True - return self.expect_event( - Page.Events.Request, predicate=my_predicate, timeout=timeout + trimmed_url = trim_url(url_or_predicate) + log_line = f"waiting for request {trimmed_url}" if trimmed_url else None + return self._expect_event( + Page.Events.Request, + predicate=my_predicate, + timeout=timeout, + log_line=log_line, ) def expect_request_finished( @@ -892,8 +911,13 @@ def my_predicate(response: Response) -> bool: return predicate(response) return True - return self.expect_event( - Page.Events.Response, predicate=my_predicate, timeout=timeout + trimmed_url = trim_url(url_or_predicate) + log_line = f"waiting for response {trimmed_url}" if trimmed_url else None + return self._expect_event( + Page.Events.Response, + predicate=my_predicate, + timeout=timeout, + log_line=log_line, ) def expect_websocket( @@ -986,3 +1010,17 @@ async def call(self, func: Callable) -> None: "reject", dict(error=dict(error=serialize_error(e, tb))) ) ) + + +def trim_url(param: URLMatchRequest) -> Optional[str]: + if isinstance(param, re.Pattern): + return trim_end(param.pattern) + if isinstance(param, str): + return trim_end(param) + return None + + +def trim_end(s: str) -> str: + if len(s) > 50: + return s[:50] + "\u2026" + return s diff --git a/playwright/_impl/_wait_helper.py b/playwright/_impl/_wait_helper.py index fb6fb5795..cd20f7084 100644 --- a/playwright/_impl/_wait_helper.py +++ b/playwright/_impl/_wait_helper.py @@ -13,6 +13,7 @@ # limitations under the License. import asyncio +import math import uuid from asyncio.tasks import Task from typing import Any, Callable, List, Tuple @@ -31,6 +32,7 @@ def __init__(self, channel_owner: ChannelOwner, event: str) -> None: self._pending_tasks: List[Task] = [] self._channel = channel_owner._channel self._registered_listeners: List[Tuple[EventEmitter, str, Callable]] = [] + self._logs: List[str] = [] self._wait_for_event_info_before(self._wait_id, event) def _wait_for_event_info_before(self, wait_id: str, event: str) -> None: @@ -101,6 +103,9 @@ def _fulfill(self, result: Any) -> None: def _reject(self, exception: Exception) -> None: self._cleanup() + if exception: + base_class = TimeoutError if isinstance(exception, TimeoutError) else Error + exception = base_class(str(exception) + format_log_recording(self._logs)) if not self._result.done(): self._result.set_exception(exception) self._wait_for_event_info_after(self._wait_id, exception) @@ -121,6 +126,22 @@ def listener(event_data: Any = None) -> None: def result(self) -> asyncio.Future: return self._result + def log(self, message: str) -> None: + self._logs.append(message) + try: + self._channel.send_no_reply( + "waitForEventInfo", + { + "info": { + "waitId": self._wait_id, + "phase": "log", + "message": message, + }, + }, + ) + except Exception: + pass + def throw_on_timeout(timeout: float, exception: Exception) -> asyncio.Task: async def throw() -> None: @@ -128,3 +149,14 @@ async def throw() -> None: raise exception return asyncio.create_task(throw()) + + +def format_log_recording(log: List[str]) -> str: + if not log: + return "" + header = " logs " + header_length = 60 + left_length = math.floor((header_length - len(header)) / 2) + right_length = header_length - len(header) - left_length + new_line = "\n" + return f"{new_line}{'=' * left_length}{header}{'=' * right_length}{new_line}{new_line.join(log)}{new_line}{'=' * header_length}" From f57cc82814223bb66c6473b411fe3df4f5394000 Mon Sep 17 00:00:00 2001 From: Kumar Aditya <59607654+kumaraditya303@users.noreply.github.com> Date: Thu, 29 Jul 2021 14:35:27 +0530 Subject: [PATCH 003/672] chore: fix mypy linting for websockets (#828) --- playwright/_impl/_transport.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/playwright/_impl/_transport.py b/playwright/_impl/_transport.py index cf0a8e800..d6161f93d 100644 --- a/playwright/_impl/_transport.py +++ b/playwright/_impl/_transport.py @@ -24,6 +24,7 @@ import websockets from pyee import AsyncIOEventEmitter +from websockets.client import connect as websocket_connect from playwright._impl._api_types import Error from playwright._impl._helper import ParsedMessagePayload @@ -185,7 +186,7 @@ async def wait_until_stopped(self) -> None: async def run(self) -> None: try: - self._connection = await websockets.connect( + self._connection = await websocket_connect( self.ws_endpoint, extra_headers=self.headers ) except Exception as exc: From a2347a79e001d673923b98479845662c53c0fb66 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Thu, 5 Aug 2021 18:16:10 +0200 Subject: [PATCH 004/672] feat(roll): roll Playwright 1.14.0-next-1627933604000 (#832) --- playwright/_impl/_frame.py | 75 +- playwright/_impl/_helper.py | 4 +- playwright/_impl/_locator.py | 440 ++++ playwright/_impl/_page.py | 86 +- playwright/_impl/_transport.py | 4 +- playwright/async_api/__init__.py | 2 + playwright/async_api/_generated.py | 1863 ++++++++++++++++- playwright/sync_api/__init__.py | 2 + playwright/sync_api/_generated.py | 1852 +++++++++++++++- scripts/generate_api.py | 3 + scripts/update_api.sh | 4 +- setup.py | 2 +- tests/assets/dom.html | 2 + tests/async/test_dispatch_event.py | 2 +- tests/async/test_locators.py | 458 ++++ tests/async/test_tap.py | 30 +- .../screenshot-element-bounding-box.png | Bin 0 -> 474 bytes .../screenshot-element-bounding-box.png | Bin 0 -> 311 bytes .../screenshot-element-bounding-box.png | Bin 0 -> 445 bytes tests/sync/test_locators.py | 442 ++++ 20 files changed, 5106 insertions(+), 165 deletions(-) create mode 100644 playwright/_impl/_locator.py create mode 100644 tests/async/test_locators.py create mode 100644 tests/golden-chromium/screenshot-element-bounding-box.png create mode 100644 tests/golden-firefox/screenshot-element-bounding-box.png create mode 100644 tests/golden-webkit/screenshot-element-bounding-box.png create mode 100644 tests/sync/test_locators.py diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py index 7622c42d1..c79a50b95 100644 --- a/playwright/_impl/_frame.py +++ b/playwright/_impl/_frame.py @@ -46,6 +46,7 @@ parse_result, serialize_argument, ) +from playwright._impl._locator import Locator from playwright._impl._network import Response from playwright._impl._wait_helper import WaitHelper @@ -254,7 +255,9 @@ async def evaluate_handle( ) ) - async def query_selector(self, selector: str) -> Optional[ElementHandle]: + async def query_selector( + self, selector: str, strict: bool = None + ) -> Optional[ElementHandle]: return from_nullable_channel( await self._channel.send("querySelector", dict(selector=selector)) ) @@ -270,6 +273,7 @@ async def query_selector_all(self, selector: str) -> List[ElementHandle]: async def wait_for_selector( self, selector: str, + strict: bool = None, timeout: float = None, state: Literal["attached", "detached", "hidden", "visible"] = None, ) -> Optional[ElementHandle]: @@ -277,26 +281,43 @@ async def wait_for_selector( await self._channel.send("waitForSelector", locals_to_params(locals())) ) - async def is_checked(self, selector: str, timeout: float = None) -> bool: + async def is_checked( + self, selector: str, strict: bool = None, timeout: float = None + ) -> bool: return await self._channel.send("isChecked", locals_to_params(locals())) - async def is_disabled(self, selector: str, timeout: float = None) -> bool: + async def is_disabled( + self, selector: str, strict: bool = None, timeout: float = None + ) -> bool: return await self._channel.send("isDisabled", locals_to_params(locals())) - async def is_editable(self, selector: str, timeout: float = None) -> bool: + async def is_editable( + self, selector: str, strict: bool = None, timeout: float = None + ) -> bool: return await self._channel.send("isEditable", locals_to_params(locals())) - async def is_enabled(self, selector: str, timeout: float = None) -> bool: + async def is_enabled( + self, selector: str, strict: bool = None, timeout: float = None + ) -> bool: return await self._channel.send("isEnabled", locals_to_params(locals())) - async def is_hidden(self, selector: str, timeout: float = None) -> bool: + async def is_hidden( + self, selector: str, strict: bool = None, timeout: float = None + ) -> bool: return await self._channel.send("isHidden", locals_to_params(locals())) - async def is_visible(self, selector: str, timeout: float = None) -> bool: + async def is_visible( + self, selector: str, strict: bool = None, timeout: float = None + ) -> bool: return await self._channel.send("isVisible", locals_to_params(locals())) async def dispatch_event( - self, selector: str, type: str, eventInit: Dict = None, timeout: float = None + self, + selector: str, + type: str, + eventInit: Dict = None, + strict: bool = None, + timeout: float = None, ) -> None: await self._channel.send( "dispatchEvent", @@ -308,6 +329,7 @@ async def eval_on_selector( selector: str, expression: str, arg: Serializable = None, + strict: bool = None, ) -> Any: return parse_result( await self._channel.send( @@ -409,6 +431,7 @@ async def click( timeout: float = None, force: bool = None, noWaitAfter: bool = None, + strict: bool = None, trial: bool = None, ) -> None: await self._channel.send("click", locals_to_params(locals())) @@ -423,6 +446,7 @@ async def dblclick( timeout: float = None, force: bool = None, noWaitAfter: bool = None, + strict: bool = None, trial: bool = None, ) -> None: await self._channel.send("dblclick", locals_to_params(locals())) @@ -435,6 +459,7 @@ async def tap( timeout: float = None, force: bool = None, noWaitAfter: bool = None, + strict: bool = None, trial: bool = None, ) -> None: await self._channel.send("tap", locals_to_params(locals())) @@ -445,24 +470,39 @@ async def fill( value: str, timeout: float = None, noWaitAfter: bool = None, + strict: bool = None, force: bool = None, ) -> None: await self._channel.send("fill", locals_to_params(locals())) - async def focus(self, selector: str, timeout: float = None) -> None: + def locator( + self, + selector: str, + ) -> Locator: + return Locator(self, selector) + + async def focus( + self, selector: str, strict: bool = None, timeout: float = None + ) -> None: await self._channel.send("focus", locals_to_params(locals())) - async def text_content(self, selector: str, timeout: float = None) -> Optional[str]: + async def text_content( + self, selector: str, strict: bool = None, timeout: float = None + ) -> Optional[str]: return await self._channel.send("textContent", locals_to_params(locals())) - async def inner_text(self, selector: str, timeout: float = None) -> str: + async def inner_text( + self, selector: str, strict: bool = None, timeout: float = None + ) -> str: return await self._channel.send("innerText", locals_to_params(locals())) - async def inner_html(self, selector: str, timeout: float = None) -> str: + async def inner_html( + self, selector: str, strict: bool = None, timeout: float = None + ) -> str: return await self._channel.send("innerHTML", locals_to_params(locals())) async def get_attribute( - self, selector: str, name: str, timeout: float = None + self, selector: str, name: str, strict: bool = None, timeout: float = None ) -> Optional[str]: return await self._channel.send("getAttribute", locals_to_params(locals())) @@ -473,6 +513,7 @@ async def hover( position: Position = None, timeout: float = None, force: bool = None, + strict: bool = None, trial: bool = None, ) -> None: await self._channel.send("hover", locals_to_params(locals())) @@ -483,6 +524,7 @@ async def drag_and_drop( target: str, force: bool = None, noWaitAfter: bool = None, + strict: bool = None, timeout: float = None, trial: bool = None, ) -> None: @@ -497,6 +539,7 @@ async def select_option( element: Union["ElementHandle", List["ElementHandle"]] = None, timeout: float = None, noWaitAfter: bool = None, + strict: bool = None, force: bool = None, ) -> List[str]: params = locals_to_params( @@ -512,6 +555,7 @@ async def select_option( async def input_value( self, selector: str, + strict: bool = None, timeout: float = None, ) -> str: return await self._channel.send("inputValue", locals_to_params(locals())) @@ -520,6 +564,7 @@ async def set_input_files( self, selector: str, files: Union[str, Path, FilePayload, List[Union[str, Path]], List[FilePayload]], + strict: bool = None, timeout: float = None, noWaitAfter: bool = None, ) -> None: @@ -532,6 +577,7 @@ async def type( selector: str, text: str, delay: float = None, + strict: bool = None, timeout: float = None, noWaitAfter: bool = None, ) -> None: @@ -542,6 +588,7 @@ async def press( selector: str, key: str, delay: float = None, + strict: bool = None, timeout: float = None, noWaitAfter: bool = None, ) -> None: @@ -554,6 +601,7 @@ async def check( timeout: float = None, force: bool = None, noWaitAfter: bool = None, + strict: bool = None, trial: bool = None, ) -> None: await self._channel.send("check", locals_to_params(locals())) @@ -565,6 +613,7 @@ async def uncheck( timeout: float = None, force: bool = None, noWaitAfter: bool = None, + strict: bool = None, trial: bool = None, ) -> None: await self._channel.send("uncheck", locals_to_params(locals())) diff --git a/playwright/_impl/_helper.py b/playwright/_impl/_helper.py index 4b764af99..2ec185122 100644 --- a/playwright/_impl/_helper.py +++ b/playwright/_impl/_helper.py @@ -137,7 +137,9 @@ def __init__(self, parent: Optional["TimeoutSettings"]) -> None: def set_timeout(self, timeout: float) -> None: self._timeout = timeout - def timeout(self) -> float: + def timeout(self, timeout: float = None) -> float: + if timeout is not None: + return timeout if self._timeout is not None: return self._timeout if self._parent: diff --git a/playwright/_impl/_locator.py b/playwright/_impl/_locator.py new file mode 100644 index 000000000..aae3e724f --- /dev/null +++ b/playwright/_impl/_locator.py @@ -0,0 +1,440 @@ +# 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 pathlib +import sys +from typing import ( + TYPE_CHECKING, + Any, + Awaitable, + Callable, + Dict, + List, + Optional, + TypeVar, + Union, + cast, +) + +from playwright._impl._api_structures import FilePayload, FloatRect, Position +from playwright._impl._element_handle import ElementHandle +from playwright._impl._helper import ( + Error, + KeyboardModifier, + MouseButton, + locals_to_params, + monotonic_time, +) +from playwright._impl._js_handle import Serializable + +if sys.version_info >= (3, 8): # pragma: no cover + from typing import Literal +else: # pragma: no cover + from typing_extensions import Literal + +if TYPE_CHECKING: # pragma: no cover + from playwright._impl._frame import Frame + from playwright._impl._js_handle import JSHandle + +T = TypeVar("T") + + +class Locator: + def __init__(self, frame: "Frame", selector: str) -> None: + self._frame = frame + self._selector = selector + self._loop = frame._loop + self._dispatcher_fiber = frame._connection._dispatcher_fiber + + def __repr__(self) -> str: + return f"" + + async def _with_element( + self, + task: Callable[[ElementHandle, float], Awaitable[T]], + timeout: float = None, + ) -> T: + timeout = self._frame.page._timeout_settings.timeout(timeout) + deadline = (monotonic_time() + timeout) if timeout else 0 + handle = await self.element_handle(timeout=timeout) + if not handle: + raise Error(f"Could not resolve {self._selector} to DOM Element") + try: + return await task( + handle, + (deadline - monotonic_time()) if deadline else 0, + ) + finally: + await handle.dispose() + + async def bounding_box(self, timeout: float = None) -> Optional[FloatRect]: + return await self._with_element( + lambda h, _: h.bounding_box(), + timeout, + ) + + async def check( + self, + position: Position = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + trial: bool = None, + ) -> None: + params = locals_to_params(locals()) + return await self._frame.check(self._selector, strict=True, **params) + + async def click( + self, + modifiers: List[KeyboardModifier] = None, + position: Position = None, + delay: float = None, + button: MouseButton = None, + clickCount: int = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + trial: bool = None, + ) -> None: + params = locals_to_params(locals()) + return await self._frame.click(self._selector, strict=True, **params) + + async def dblclick( + self, + modifiers: List[KeyboardModifier] = None, + position: Position = None, + delay: float = None, + button: MouseButton = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + trial: bool = None, + ) -> None: + params = locals_to_params(locals()) + return await self._frame.dblclick(self._selector, strict=True, **params) + + async def dispatch_event( + self, + type: str, + eventInit: Dict = None, + timeout: float = None, + ) -> None: + params = locals_to_params(locals()) + return await self._frame.dispatch_event(self._selector, strict=True, **params) + + async def evaluate( + self, expression: str, arg: Serializable = None, timeout: float = None + ) -> Any: + return await self._with_element( + lambda h, _: h.evaluate(expression, arg), + timeout, + ) + + async def evaluate_all(self, expression: str, arg: Serializable = None) -> None: + params = locals_to_params(locals()) + return await self._frame.eval_on_selector_all(self._selector, **params) + + async def evaluate_handle( + self, expression: str, arg: Serializable = None, timeout: float = None + ) -> "JSHandle": + return await self._with_element( + lambda h, o: h.evaluate_handle(expression, arg), timeout + ) + + async def fill( + self, + value: str, + timeout: float = None, + noWaitAfter: bool = None, + force: bool = None, + ) -> None: + params = locals_to_params(locals()) + return await self._frame.fill(self._selector, strict=True, **params) + + def locator( + self, + selector: str, + ) -> "Locator": + return Locator(self._frame, f"{self._selector} >> {selector}") + + async def element_handle( + self, + timeout: float = None, + ) -> ElementHandle: + params = locals_to_params(locals()) + return cast( + ElementHandle, + await self._frame.wait_for_selector( + self._selector, strict=True, state="attached", **params + ), + ) + + async def element_handles(self) -> List[ElementHandle]: + return await self._frame.query_selector_all(self._selector) + + @property + def first(self) -> "Locator": + return Locator(self._frame, f"{self._selector} >> _nth=first") + + @property + def last(self) -> "Locator": + return Locator(self._frame, f"{self._selector} >> _nth=last") + + def nth(self, index: int) -> "Locator": + return Locator(self._frame, f"{self._selector} >> _nth={index}") + + async def focus(self, timeout: float = None) -> None: + params = locals_to_params(locals()) + return await self._frame.focus(self._selector, strict=True, **params) + + async def count( + self, + ) -> int: + return cast(int, await self.evaluate_all("ee => ee.length")) + + async def get_attribute(self, name: str, timeout: float = None) -> Optional[str]: + params = locals_to_params(locals()) + return await self._frame.get_attribute( + self._selector, + strict=True, + **params, + ) + + async def hover( + self, + modifiers: List[KeyboardModifier] = None, + position: Position = None, + timeout: float = None, + force: bool = None, + trial: bool = None, + ) -> None: + params = locals_to_params(locals()) + return await self._frame.hover( + self._selector, + strict=True, + **params, + ) + + async def inner_html(self, timeout: float = None) -> str: + params = locals_to_params(locals()) + return await self._frame.inner_html( + self._selector, + strict=True, + **params, + ) + + async def inner_text(self, timeout: float = None) -> str: + params = locals_to_params(locals()) + return await self._frame.inner_text( + self._selector, + strict=True, + **params, + ) + + async def input_value(self, timeout: float = None) -> str: + params = locals_to_params(locals()) + return await self._frame.input_value( + self._selector, + strict=True, + **params, + ) + + async def is_checked(self, timeout: float = None) -> bool: + params = locals_to_params(locals()) + return await self._frame.is_checked( + self._selector, + strict=True, + **params, + ) + + async def is_disabled(self, timeout: float = None) -> bool: + params = locals_to_params(locals()) + return await self._frame.is_disabled( + self._selector, + strict=True, + **params, + ) + + async def is_editable(self, timeout: float = None) -> bool: + params = locals_to_params(locals()) + return await self._frame.is_editable( + self._selector, + strict=True, + **params, + ) + + async def is_enabled(self, timeout: float = None) -> bool: + params = locals_to_params(locals()) + return await self._frame.is_editable( + self._selector, + strict=True, + **params, + ) + + async def is_hidden(self, timeout: float = None) -> bool: + params = locals_to_params(locals()) + return await self._frame.is_hidden( + self._selector, + strict=True, + **params, + ) + + async def is_visible(self, timeout: float = None) -> bool: + params = locals_to_params(locals()) + return await self._frame.is_visible( + self._selector, + strict=True, + **params, + ) + + async def press( + self, + key: str, + delay: float = None, + timeout: float = None, + noWaitAfter: bool = None, + ) -> None: + params = locals_to_params(locals()) + return await self._frame.press(self._selector, strict=True, **params) + + async def screenshot( + self, + timeout: float = None, + type: Literal["jpeg", "png"] = None, + path: Union[str, pathlib.Path] = None, + quality: int = None, + omitBackground: bool = None, + ) -> bytes: + params = locals_to_params(locals()) + return await self._with_element( + lambda h, timeout: h.screenshot(timeout=timeout, **params) + ) + + async def scroll_into_view_if_needed( + self, + timeout: float = None, + ) -> None: + return await self._with_element( + lambda h, timeout: h.scroll_into_view_if_needed(timeout=timeout), + timeout, + ) + + async def select_option( + self, + value: Union[str, List[str]] = None, + index: Union[int, List[int]] = None, + label: Union[str, List[str]] = None, + element: Union["ElementHandle", List["ElementHandle"]] = None, + timeout: float = None, + noWaitAfter: bool = None, + force: bool = None, + ) -> List[str]: + params = locals_to_params(locals()) + return await self._frame.select_option( + self._selector, + strict=True, + **params, + ) + + async def select_text(self, force: bool = None, timeout: float = None) -> None: + params = locals_to_params(locals()) + return await self._with_element( + lambda h, timeout: h.select_text(timeout=timeout, **params), timeout + ) + + async def set_input_files( + self, + files: Union[ + str, + pathlib.Path, + FilePayload, + List[Union[str, pathlib.Path]], + List[FilePayload], + ], + timeout: float = None, + noWaitAfter: bool = None, + ) -> None: + params = locals_to_params(locals()) + return await self._frame.set_input_files( + self._selector, + strict=True, + **params, + ) + + async def tap( + self, + modifiers: List[KeyboardModifier] = None, + position: Position = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + trial: bool = None, + ) -> None: + params = locals_to_params(locals()) + return await self._frame.tap( + self._selector, + strict=True, + **params, + ) + + async def text_content(self, timeout: float = None) -> Optional[str]: + params = locals_to_params(locals()) + return await self._frame.text_content( + self._selector, + strict=True, + **params, + ) + + async def type( + self, + text: str, + delay: float = None, + timeout: float = None, + noWaitAfter: bool = None, + ) -> None: + params = locals_to_params(locals()) + return await self._frame.type( + self._selector, + strict=True, + **params, + ) + + async def uncheck( + self, + position: Position = None, + timeout: float = None, + force: bool = None, + noWaitAfter: bool = None, + trial: bool = None, + ) -> None: + params = locals_to_params(locals()) + return await self._frame.uncheck( + self._selector, + strict=True, + **params, + ) + + async def all_inner_texts( + self, + ) -> List[str]: + return await self._frame.eval_on_selector_all( + self._selector, "ee => ee.map(e => e.innerText)" + ) + + async def all_text_contents( + self, + ) -> List[str]: + return await self._frame.eval_on_selector_all( + self._selector, "ee => ee.map(e => e.textContent || '')" + ) diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index 960de6541..fd0c2aa19 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -81,6 +81,7 @@ if TYPE_CHECKING: # pragma: no cover from playwright._impl._browser_context import BrowserContext + from playwright._impl._locator import Locator from playwright._impl._network import WebSocket @@ -314,8 +315,12 @@ def set_default_timeout(self, timeout: float) -> None: self._timeout_settings.set_timeout(timeout) self._channel.send_no_reply("setDefaultTimeoutNoReply", dict(timeout=timeout)) - async def query_selector(self, selector: str) -> Optional[ElementHandle]: - return await self._main_frame.query_selector(selector) + async def query_selector( + self, + selector: str, + strict: bool = None, + ) -> Optional[ElementHandle]: + return await self._main_frame.query_selector(selector, strict) async def query_selector_all(self, selector: str) -> List[ElementHandle]: return await self._main_frame.query_selector_all(selector) @@ -325,29 +330,47 @@ async def wait_for_selector( selector: str, timeout: float = None, state: Literal["attached", "detached", "hidden", "visible"] = None, + strict: bool = None, ) -> Optional[ElementHandle]: return await self._main_frame.wait_for_selector(**locals_to_params(locals())) - async def is_checked(self, selector: str, timeout: float = None) -> bool: + async def is_checked( + self, selector: str, strict: bool = None, timeout: float = None + ) -> bool: return await self._main_frame.is_checked(**locals_to_params(locals())) - async def is_disabled(self, selector: str, timeout: float = None) -> bool: + async def is_disabled( + self, selector: str, strict: bool = None, timeout: float = None + ) -> bool: return await self._main_frame.is_disabled(**locals_to_params(locals())) - async def is_editable(self, selector: str, timeout: float = None) -> bool: + async def is_editable( + self, selector: str, strict: bool = None, timeout: float = None + ) -> bool: return await self._main_frame.is_editable(**locals_to_params(locals())) - async def is_enabled(self, selector: str, timeout: float = None) -> bool: + async def is_enabled( + self, selector: str, strict: bool = None, timeout: float = None + ) -> bool: return await self._main_frame.is_enabled(**locals_to_params(locals())) - async def is_hidden(self, selector: str, timeout: float = None) -> bool: + async def is_hidden( + self, selector: str, strict: bool = None, timeout: float = None + ) -> bool: return await self._main_frame.is_hidden(**locals_to_params(locals())) - async def is_visible(self, selector: str, timeout: float = None) -> bool: + async def is_visible( + self, selector: str, strict: bool = None, timeout: float = None + ) -> bool: return await self._main_frame.is_visible(**locals_to_params(locals())) async def dispatch_event( - self, selector: str, type: str, eventInit: Dict = None, timeout: float = None + self, + selector: str, + type: str, + eventInit: Dict = None, + timeout: float = None, + strict: bool = None, ) -> None: return await self._main_frame.dispatch_event(**locals_to_params(locals())) @@ -364,8 +387,11 @@ async def eval_on_selector( selector: str, expression: str, arg: Serializable = None, + strict: bool = None, ) -> Any: - return await self._main_frame.eval_on_selector(selector, expression, arg) + return await self._main_frame.eval_on_selector( + selector, expression, arg, strict + ) async def eval_on_selector_all( self, @@ -583,6 +609,7 @@ async def click( force: bool = None, noWaitAfter: bool = None, trial: bool = None, + strict: bool = None, ) -> None: return await self._main_frame.click(**locals_to_params(locals())) @@ -596,6 +623,7 @@ async def dblclick( timeout: float = None, force: bool = None, noWaitAfter: bool = None, + strict: bool = None, trial: bool = None, ) -> None: return await self._main_frame.dblclick(**locals_to_params(locals())) @@ -608,6 +636,7 @@ async def tap( timeout: float = None, force: bool = None, noWaitAfter: bool = None, + strict: bool = None, trial: bool = None, ) -> None: return await self._main_frame.tap(**locals_to_params(locals())) @@ -618,24 +647,39 @@ async def fill( value: str, timeout: float = None, noWaitAfter: bool = None, + strict: bool = None, force: bool = None, ) -> None: return await self._main_frame.fill(**locals_to_params(locals())) - async def focus(self, selector: str, timeout: float = None) -> None: + def locator( + self, + selector: str, + ) -> "Locator": + return self._main_frame.locator(selector) + + async def focus( + self, selector: str, strict: bool = None, timeout: float = None + ) -> None: return await self._main_frame.focus(**locals_to_params(locals())) - async def text_content(self, selector: str, timeout: float = None) -> Optional[str]: + async def text_content( + self, selector: str, strict: bool = None, timeout: float = None + ) -> Optional[str]: return await self._main_frame.text_content(**locals_to_params(locals())) - async def inner_text(self, selector: str, timeout: float = None) -> str: + async def inner_text( + self, selector: str, strict: bool = None, timeout: float = None + ) -> str: return await self._main_frame.inner_text(**locals_to_params(locals())) - async def inner_html(self, selector: str, timeout: float = None) -> str: + async def inner_html( + self, selector: str, strict: bool = None, timeout: float = None + ) -> str: return await self._main_frame.inner_html(**locals_to_params(locals())) async def get_attribute( - self, selector: str, name: str, timeout: float = None + self, selector: str, name: str, strict: bool = None, timeout: float = None ) -> Optional[str]: return await self._main_frame.get_attribute(**locals_to_params(locals())) @@ -646,6 +690,7 @@ async def hover( position: Position = None, timeout: float = None, force: bool = None, + strict: bool = None, trial: bool = None, ) -> None: return await self._main_frame.hover(**locals_to_params(locals())) @@ -657,6 +702,7 @@ async def drag_and_drop( force: bool = None, noWaitAfter: bool = None, timeout: float = None, + strict: bool = None, trial: bool = None, ) -> None: return await self._main_frame.drag_and_drop(**locals_to_params(locals())) @@ -671,11 +717,14 @@ async def select_option( timeout: float = None, noWaitAfter: bool = None, force: bool = None, + strict: bool = None, ) -> List[str]: params = locals_to_params(locals()) return await self._main_frame.select_option(**params) - async def input_value(self, selector: str, timeout: float = None) -> str: + async def input_value( + self, selector: str, strict: bool = None, timeout: float = None + ) -> str: params = locals_to_params(locals()) return await self._main_frame.input_value(**params) @@ -684,6 +733,7 @@ async def set_input_files( selector: str, files: Union[str, Path, FilePayload, List[Union[str, Path]], List[FilePayload]], timeout: float = None, + strict: bool = None, noWaitAfter: bool = None, ) -> None: return await self._main_frame.set_input_files(**locals_to_params(locals())) @@ -695,6 +745,7 @@ async def type( delay: float = None, timeout: float = None, noWaitAfter: bool = None, + strict: bool = None, ) -> None: return await self._main_frame.type(**locals_to_params(locals())) @@ -705,6 +756,7 @@ async def press( delay: float = None, timeout: float = None, noWaitAfter: bool = None, + strict: bool = None, ) -> None: return await self._main_frame.press(**locals_to_params(locals())) @@ -715,6 +767,7 @@ async def check( timeout: float = None, force: bool = None, noWaitAfter: bool = None, + strict: bool = None, trial: bool = None, ) -> None: return await self._main_frame.check(**locals_to_params(locals())) @@ -726,6 +779,7 @@ async def uncheck( timeout: float = None, force: bool = None, noWaitAfter: bool = None, + strict: bool = None, trial: bool = None, ) -> None: return await self._main_frame.uncheck(**locals_to_params(locals())) diff --git a/playwright/_impl/_transport.py b/playwright/_impl/_transport.py index d6161f93d..d5b3ec0af 100644 --- a/playwright/_impl/_transport.py +++ b/playwright/_impl/_transport.py @@ -20,7 +20,7 @@ import sys from abc import ABC, abstractmethod from pathlib import Path -from typing import Callable, Dict, Optional +from typing import Callable, Dict, Optional, Union import websockets from pyee import AsyncIOEventEmitter @@ -74,7 +74,7 @@ def serialize_message(self, message: Dict) -> bytes: print("\x1b[32mSEND>\x1b[0m", json.dumps(message, indent=2)) return msg.encode() - def deserialize_message(self, data: bytes) -> ParsedMessagePayload: + def deserialize_message(self, data: Union[str, bytes]) -> ParsedMessagePayload: obj = json.loads(data) if "DEBUGP" in os.environ: # pragma: no cover diff --git a/playwright/async_api/__init__.py b/playwright/async_api/__init__.py index 55e3819e0..046755ea7 100644 --- a/playwright/async_api/__init__.py +++ b/playwright/async_api/__init__.py @@ -36,6 +36,7 @@ Frame, JSHandle, Keyboard, + Locator, Mouse, Page, Playwright, @@ -95,6 +96,7 @@ def async_playwright() -> PlaywrightContextManager: "HttpCredentials", "JSHandle", "Keyboard", + "Locator", "Mouse", "Page", "PdfMargins", diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index 295396e5d..6d2371ef7 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -59,6 +59,7 @@ from playwright._impl._input import Mouse as MouseImpl from playwright._impl._input import Touchscreen as TouchscreenImpl from playwright._impl._js_handle import JSHandle as JSHandleImpl +from playwright._impl._locator import Locator as LocatorImpl from playwright._impl._network import Request as RequestImpl from playwright._impl._network import Response as ResponseImpl from playwright._impl._network import Route as RouteImpl @@ -3062,7 +3063,9 @@ async def evaluate_handle( ) ) - async def query_selector(self, selector: str) -> typing.Optional["ElementHandle"]: + async def query_selector( + self, selector: str, *, strict: bool = None + ) -> typing.Optional["ElementHandle"]: """Frame.query_selector Returns the ElementHandle pointing to the frame element. @@ -3074,6 +3077,9 @@ async def query_selector(self, selector: str) -> typing.Optional["ElementHandle" ---------- selector : str A selector to query for. See [working with selectors](./selectors.md) for more details. + strict : Union[bool, NoneType] + When true, the call requires selector to resolve to a single element. If given selector resolves to more then one + element, the call throws an exception. Returns ------- @@ -3082,7 +3088,8 @@ async def query_selector(self, selector: str) -> typing.Optional["ElementHandle" return mapping.from_impl_nullable( await self._async( - "frame.query_selector", self._impl_obj.query_selector(selector=selector) + "frame.query_selector", + self._impl_obj.query_selector(selector=selector, strict=strict), ) ) @@ -3115,6 +3122,7 @@ async def wait_for_selector( self, selector: str, *, + strict: bool = None, timeout: float = None, state: Literal["attached", "detached", "hidden", "visible"] = None ) -> typing.Optional["ElementHandle"]: @@ -3153,6 +3161,9 @@ async def main(): ---------- selector : str A selector to query for. See [working with selectors](./selectors.md) for more details. + strict : Union[bool, NoneType] + When true, the call requires selector to resolve to a single element. If given selector resolves to more then one + element, the call throws an exception. timeout : Union[float, NoneType] 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. @@ -3174,12 +3185,14 @@ async def main(): await self._async( "frame.wait_for_selector", self._impl_obj.wait_for_selector( - selector=selector, timeout=timeout, state=state + selector=selector, strict=strict, timeout=timeout, state=state ), ) ) - async def is_checked(self, selector: str, *, timeout: float = None) -> bool: + async def is_checked( + self, selector: str, *, strict: bool = None, timeout: float = None + ) -> bool: """Frame.is_checked Returns whether the element is checked. Throws if the element is not a checkbox or radio input. @@ -3189,6 +3202,9 @@ async def is_checked(self, selector: str, *, timeout: float = None) -> bool: selector : str A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. + strict : Union[bool, NoneType] + When true, the call requires selector to resolve to a single element. If given selector resolves to more then one + element, the call throws an exception. timeout : Union[float, NoneType] 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. @@ -3201,11 +3217,15 @@ async def is_checked(self, selector: str, *, timeout: float = None) -> bool: return mapping.from_maybe_impl( await self._async( "frame.is_checked", - self._impl_obj.is_checked(selector=selector, timeout=timeout), + self._impl_obj.is_checked( + selector=selector, strict=strict, timeout=timeout + ), ) ) - async def is_disabled(self, selector: str, *, timeout: float = None) -> bool: + async def is_disabled( + self, selector: str, *, strict: bool = None, timeout: float = None + ) -> bool: """Frame.is_disabled Returns whether the element is disabled, the opposite of [enabled](./actionability.md#enabled). @@ -3215,6 +3235,9 @@ async def is_disabled(self, selector: str, *, timeout: float = None) -> bool: selector : str A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. + strict : Union[bool, NoneType] + When true, the call requires selector to resolve to a single element. If given selector resolves to more then one + element, the call throws an exception. timeout : Union[float, NoneType] 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. @@ -3227,11 +3250,15 @@ async def is_disabled(self, selector: str, *, timeout: float = None) -> bool: return mapping.from_maybe_impl( await self._async( "frame.is_disabled", - self._impl_obj.is_disabled(selector=selector, timeout=timeout), + self._impl_obj.is_disabled( + selector=selector, strict=strict, timeout=timeout + ), ) ) - async def is_editable(self, selector: str, *, timeout: float = None) -> bool: + async def is_editable( + self, selector: str, *, strict: bool = None, timeout: float = None + ) -> bool: """Frame.is_editable Returns whether the element is [editable](./actionability.md#editable). @@ -3241,6 +3268,9 @@ async def is_editable(self, selector: str, *, timeout: float = None) -> bool: selector : str A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. + strict : Union[bool, NoneType] + When true, the call requires selector to resolve to a single element. If given selector resolves to more then one + element, the call throws an exception. timeout : Union[float, NoneType] 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. @@ -3253,11 +3283,15 @@ async def is_editable(self, selector: str, *, timeout: float = None) -> bool: return mapping.from_maybe_impl( await self._async( "frame.is_editable", - self._impl_obj.is_editable(selector=selector, timeout=timeout), + self._impl_obj.is_editable( + selector=selector, strict=strict, timeout=timeout + ), ) ) - async def is_enabled(self, selector: str, *, timeout: float = None) -> bool: + async def is_enabled( + self, selector: str, *, strict: bool = None, timeout: float = None + ) -> bool: """Frame.is_enabled Returns whether the element is [enabled](./actionability.md#enabled). @@ -3267,6 +3301,9 @@ async def is_enabled(self, selector: str, *, timeout: float = None) -> bool: selector : str A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. + strict : Union[bool, NoneType] + When true, the call requires selector to resolve to a single element. If given selector resolves to more then one + element, the call throws an exception. timeout : Union[float, NoneType] 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. @@ -3279,11 +3316,15 @@ async def is_enabled(self, selector: str, *, timeout: float = None) -> bool: return mapping.from_maybe_impl( await self._async( "frame.is_enabled", - self._impl_obj.is_enabled(selector=selector, timeout=timeout), + self._impl_obj.is_enabled( + selector=selector, strict=strict, timeout=timeout + ), ) ) - async def is_hidden(self, selector: str, *, timeout: float = None) -> bool: + async def is_hidden( + self, selector: str, *, strict: bool = None, timeout: float = None + ) -> bool: """Frame.is_hidden Returns whether the element is hidden, the opposite of [visible](./actionability.md#visible). `selector` that does not @@ -3294,6 +3335,9 @@ async def is_hidden(self, selector: str, *, timeout: float = None) -> bool: selector : str A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. + strict : Union[bool, NoneType] + When true, the call requires selector to resolve to a single element. If given selector resolves to more then one + element, the call throws an exception. timeout : Union[float, NoneType] 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. @@ -3306,11 +3350,15 @@ async def is_hidden(self, selector: str, *, timeout: float = None) -> bool: return mapping.from_maybe_impl( await self._async( "frame.is_hidden", - self._impl_obj.is_hidden(selector=selector, timeout=timeout), + self._impl_obj.is_hidden( + selector=selector, strict=strict, timeout=timeout + ), ) ) - async def is_visible(self, selector: str, *, timeout: float = None) -> bool: + async def is_visible( + self, selector: str, *, strict: bool = None, timeout: float = None + ) -> bool: """Frame.is_visible Returns whether the element is [visible](./actionability.md#visible). `selector` that does not match any elements is @@ -3321,6 +3369,9 @@ async def is_visible(self, selector: str, *, timeout: float = None) -> bool: selector : str A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. + strict : Union[bool, NoneType] + When true, the call requires selector to resolve to a single element. If given selector resolves to more then one + element, the call throws an exception. timeout : Union[float, NoneType] 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. @@ -3333,7 +3384,9 @@ async def is_visible(self, selector: str, *, timeout: float = None) -> bool: return mapping.from_maybe_impl( await self._async( "frame.is_visible", - self._impl_obj.is_visible(selector=selector, timeout=timeout), + self._impl_obj.is_visible( + selector=selector, strict=strict, timeout=timeout + ), ) ) @@ -3343,6 +3396,7 @@ async def dispatch_event( type: str, event_init: typing.Dict = None, *, + strict: bool = None, timeout: float = None ) -> NoneType: """Frame.dispatch_event @@ -3384,6 +3438,9 @@ async def dispatch_event( DOM event type: `"click"`, `"dragstart"`, etc. event_init : Union[Dict, NoneType] Optional event-specific initialization properties. + strict : Union[bool, NoneType] + When true, the call requires selector to resolve to a single element. If given selector resolves to more then one + element, the call throws an exception. timeout : Union[float, NoneType] 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. @@ -3396,13 +3453,19 @@ async def dispatch_event( selector=selector, type=type, eventInit=mapping.to_impl(event_init), + strict=strict, timeout=timeout, ), ) ) async def eval_on_selector( - self, selector: str, expression: str, arg: typing.Any = None + self, + selector: str, + expression: str, + arg: typing.Any = None, + *, + strict: bool = None ) -> typing.Any: """Frame.eval_on_selector @@ -3432,6 +3495,9 @@ async def eval_on_selector( as a function. Otherwise, evaluated as an expression. arg : Union[Any, NoneType] Optional argument to pass to `expression`. + strict : Union[bool, NoneType] + When true, the call requires selector to resolve to a single element. If given selector resolves to more then one + element, the call throws an exception. Returns ------- @@ -3442,7 +3508,10 @@ async def eval_on_selector( await self._async( "frame.eval_on_selector", self._impl_obj.eval_on_selector( - selector=selector, expression=expression, arg=mapping.to_impl(arg) + selector=selector, + expression=expression, + arg=mapping.to_impl(arg), + strict=strict, ), ) ) @@ -3641,6 +3710,7 @@ async def click( timeout: float = None, force: bool = None, no_wait_after: bool = None, + strict: bool = None, trial: bool = None ) -> NoneType: """Frame.click @@ -3682,6 +3752,9 @@ async def click( 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`. + strict : Union[bool, NoneType] + When true, the call requires selector to resolve to a single element. If given selector resolves to more then one + element, the call throws an exception. trial : Union[bool, NoneType] When set, this method only performs the [actionability](./actionability.md) checks and skips the action. Defaults to `false`. Useful to wait until the element is ready for the action without performing it. @@ -3700,6 +3773,7 @@ async def click( timeout=timeout, force=force, noWaitAfter=no_wait_after, + strict=strict, trial=trial, ), ) @@ -3718,6 +3792,7 @@ async def dblclick( timeout: float = None, force: bool = None, no_wait_after: bool = None, + strict: bool = None, trial: bool = None ) -> NoneType: """Frame.dblclick @@ -3760,6 +3835,9 @@ async def dblclick( 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`. + strict : Union[bool, NoneType] + When true, the call requires selector to resolve to a single element. If given selector resolves to more then one + element, the call throws an exception. trial : Union[bool, NoneType] When set, this method only performs the [actionability](./actionability.md) checks and skips the action. Defaults to `false`. Useful to wait until the element is ready for the action without performing it. @@ -3777,6 +3855,7 @@ async def dblclick( timeout=timeout, force=force, noWaitAfter=no_wait_after, + strict=strict, trial=trial, ), ) @@ -3793,6 +3872,7 @@ async def tap( timeout: float = None, force: bool = None, no_wait_after: bool = None, + strict: bool = None, trial: bool = None ) -> NoneType: """Frame.tap @@ -3830,6 +3910,9 @@ async def tap( 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`. + strict : Union[bool, NoneType] + When true, the call requires selector to resolve to a single element. If given selector resolves to more then one + element, the call throws an exception. trial : Union[bool, NoneType] When set, this method only performs the [actionability](./actionability.md) checks and skips the action. Defaults to `false`. Useful to wait until the element is ready for the action without performing it. @@ -3845,6 +3928,7 @@ async def tap( timeout=timeout, force=force, noWaitAfter=no_wait_after, + strict=strict, trial=trial, ), ) @@ -3857,6 +3941,7 @@ async def fill( *, timeout: float = None, no_wait_after: bool = None, + strict: bool = None, force: bool = None ) -> NoneType: """Frame.fill @@ -3886,6 +3971,9 @@ async def fill( 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`. + strict : Union[bool, NoneType] + When true, the call requires selector to resolve to a single element. If given selector resolves to more then one + element, the call throws an exception. force : Union[bool, NoneType] Whether to bypass the [actionability](./actionability.md) checks. Defaults to `false`. """ @@ -3898,12 +3986,36 @@ async def fill( value=value, timeout=timeout, noWaitAfter=no_wait_after, + strict=strict, force=force, ), ) ) - async def focus(self, selector: str, *, timeout: float = None) -> NoneType: + def locator(self, selector: str) -> "Locator": + """Frame.locator + + The method returns an element locator that can be used to perform actions in the frame. Locator is resolved to the + element immediately before performing an action, so a series of actions on the same locator can in fact be performed on + different DOM elements. That would happen if the DOM structure between those actions has changed. + + Note that locator always implies visibility, so it will always be locating visible elements. + + Parameters + ---------- + selector : str + A selector to use when resolving DOM element. See [working with selectors](./selectors.md) for more details. + + Returns + ------- + Locator + """ + + return mapping.from_impl(self._impl_obj.locator(selector=selector)) + + async def focus( + self, selector: str, *, strict: bool = None, timeout: float = None + ) -> NoneType: """Frame.focus This method fetches an element with `selector` and focuses it. If there's no element matching `selector`, the method @@ -3914,6 +4026,9 @@ async def focus(self, selector: str, *, timeout: float = None) -> NoneType: selector : str A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. + strict : Union[bool, NoneType] + When true, the call requires selector to resolve to a single element. If given selector resolves to more then one + element, the call throws an exception. timeout : Union[float, NoneType] 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. @@ -3921,12 +4036,13 @@ async def focus(self, selector: str, *, timeout: float = None) -> NoneType: return mapping.from_maybe_impl( await self._async( - "frame.focus", self._impl_obj.focus(selector=selector, timeout=timeout) + "frame.focus", + self._impl_obj.focus(selector=selector, strict=strict, timeout=timeout), ) ) async def text_content( - self, selector: str, *, timeout: float = None + self, selector: str, *, strict: bool = None, timeout: float = None ) -> typing.Optional[str]: """Frame.text_content @@ -3937,6 +4053,9 @@ async def text_content( selector : str A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. + strict : Union[bool, NoneType] + When true, the call requires selector to resolve to a single element. If given selector resolves to more then one + element, the call throws an exception. timeout : Union[float, NoneType] 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. @@ -3949,11 +4068,15 @@ async def text_content( return mapping.from_maybe_impl( await self._async( "frame.text_content", - self._impl_obj.text_content(selector=selector, timeout=timeout), + self._impl_obj.text_content( + selector=selector, strict=strict, timeout=timeout + ), ) ) - async def inner_text(self, selector: str, *, timeout: float = None) -> str: + async def inner_text( + self, selector: str, *, strict: bool = None, timeout: float = None + ) -> str: """Frame.inner_text Returns `element.innerText`. @@ -3963,6 +4086,9 @@ async def inner_text(self, selector: str, *, timeout: float = None) -> str: selector : str A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. + strict : Union[bool, NoneType] + When true, the call requires selector to resolve to a single element. If given selector resolves to more then one + element, the call throws an exception. timeout : Union[float, NoneType] 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. @@ -3975,11 +4101,15 @@ async def inner_text(self, selector: str, *, timeout: float = None) -> str: return mapping.from_maybe_impl( await self._async( "frame.inner_text", - self._impl_obj.inner_text(selector=selector, timeout=timeout), + self._impl_obj.inner_text( + selector=selector, strict=strict, timeout=timeout + ), ) ) - async def inner_html(self, selector: str, *, timeout: float = None) -> str: + async def inner_html( + self, selector: str, *, strict: bool = None, timeout: float = None + ) -> str: """Frame.inner_html Returns `element.innerHTML`. @@ -3989,6 +4119,9 @@ async def inner_html(self, selector: str, *, timeout: float = None) -> str: selector : str A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See [working with selectors](./selectors.md) for more details. + strict : Union[bool, NoneType] + When true, the call requires selector to resolve to a single element. If given selector resolves to more then one + element, the call throws an exception. timeout : Union[float, NoneType] 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. @@ -4001,12 +4134,14 @@ async def inner_html(self, selector: str, *, timeout: float = None) -> str: return mapping.from_maybe_impl( await self._async( "frame.inner_html", - self._impl_obj.inner_html(selector=selector, timeout=timeout), + self._impl_obj.inner_html( + selector=selector, strict=strict, timeout=timeout + ), ) ) async def get_attribute( - self, selector: str, name: str, *, timeout: float = None + self, selector: str, name: str, *, strict: bool = None, timeout: float = None ) -> typing.Optional[str]: """Frame.get_attribute @@ -4019,6 +4154,9 @@ async def get_attribute( [working with selectors](./selectors.md) for more details. name : str Attribute name to get the value for. + strict : Union[bool, NoneType] + When true, the call requires selector to resolve to a single element. If given selector resolves to more then one + element, the call throws an exception. timeout : Union[float, NoneType] 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. @@ -4032,7 +4170,7 @@ async def get_attribute( await self._async( "frame.get_attribute", self._impl_obj.get_attribute( - selector=selector, name=name, timeout=timeout + selector=selector, name=name, strict=strict, timeout=timeout ), ) ) @@ -4047,6 +4185,7 @@ async def hover( position: Position = None, timeout: float = None, force: bool = None, + strict: bool = None, trial: bool = None ) -> NoneType: """Frame.hover @@ -4078,6 +4217,9 @@ async def hover( using the `browser_context.set_default_timeout()` or `page.set_default_timeout()` methods. force : Union[bool, NoneType] Whether to bypass the [actionability](./actionability.md) checks. Defaults to `false`. + strict : Union[bool, NoneType] + When true, the call requires selector to resolve to a single element. If given selector resolves to more then one + element, the call throws an exception. trial : Union[bool, NoneType] When set, this method only performs the [actionability](./actionability.md) checks and skips the action. Defaults to `false`. Useful to wait until the element is ready for the action without performing it. @@ -4092,6 +4234,7 @@ async def hover( position=position, timeout=timeout, force=force, + strict=strict, trial=trial, ), ) @@ -4104,6 +4247,7 @@ async def drag_and_drop( *, force: bool = None, no_wait_after: bool = None, + strict: bool = None, timeout: float = None, trial: bool = None ) -> NoneType: @@ -4119,6 +4263,9 @@ async def drag_and_drop( 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`. + strict : Union[bool, NoneType] + When true, the call requires selector to resolve to a single element. If given selector resolves to more then one + element, the call throws an exception. timeout : Union[float, NoneType] 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. @@ -4135,6 +4282,7 @@ async def drag_and_drop( target=target, force=force, noWaitAfter=no_wait_after, + strict=strict, timeout=timeout, trial=trial, ), @@ -4151,6 +4299,7 @@ async def select_option( element: typing.Union["ElementHandle", typing.List["ElementHandle"]] = None, timeout: float = None, no_wait_after: bool = None, + strict: bool = None, force: bool = None ) -> typing.List[str]: """Frame.select_option @@ -4196,6 +4345,9 @@ async def select_option( 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`. + strict : Union[bool, NoneType] + When true, the call requires selector to resolve to a single element. If given selector resolves to more then one + element, the call throws an exception. force : Union[bool, NoneType] Whether to bypass the [actionability](./actionability.md) checks. Defaults to `false`. @@ -4215,12 +4367,15 @@ async def select_option( element=mapping.to_impl(element), timeout=timeout, noWaitAfter=no_wait_after, + strict=strict, force=force, ), ) ) - async def input_value(self, selector: str, *, timeout: float = None) -> str: + async def input_value( + self, selector: str, *, strict: bool = None, timeout: float = None + ) -> str: """Frame.input_value Returns `input.value` for the selected `` or ` diff --git a/tests/async/test_dispatch_event.py b/tests/async/test_dispatch_event.py index 701e5585f..897d797ef 100644 --- a/tests/async/test_dispatch_event.py +++ b/tests/async/test_dispatch_event.py @@ -123,7 +123,7 @@ async def test_should_be_atomic(selectors, page, utils): return result; }, queryAll(root, selector) { - const result = Array.from(root.query_selector_all(selector)); + const result = Array.from(root.querySelectorAll(selector)); for (const e of result) Promise.resolve().then(() => result.onclick = ""); return result; diff --git a/tests/async/test_locators.py b/tests/async/test_locators.py new file mode 100644 index 000000000..3d66e19c2 --- /dev/null +++ b/tests/async/test_locators.py @@ -0,0 +1,458 @@ +# 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 os + +import pytest + +from playwright._impl._path_utils import get_file_dirname +from playwright.async_api import Error, Page +from tests.server import Server + +_dirname = get_file_dirname() +FILE_TO_UPLOAD = _dirname / ".." / "assets/file-to-upload.txt" + + +async def test_locators_click_should_work(page: Page, server: Server): + await page.goto(server.PREFIX + "/input/button.html") + button = page.locator("button") + await button.click() + assert await page.evaluate("window['result']") == "Clicked" + + +async def test_locators_click_should_work_with_node_removed(page: Page, server: Server): + await page.goto(server.PREFIX + "/input/button.html") + await page.evaluate("delete window['Node']") + button = page.locator("button") + await button.click() + assert await page.evaluate("window['result']") == "Clicked" + + +async def test_locators_click_should_work_for_text_nodes(page: Page, server: Server): + await page.goto(server.PREFIX + "/input/button.html") + await page.evaluate( + """() => { + window['double'] = false; + const button = document.querySelector('button'); + button.addEventListener('dblclick', event => { + window['double'] = true; + }); + }""" + ) + button = page.locator("button") + await button.dblclick() + assert await page.evaluate("double") is True + assert await page.evaluate("result") == "Clicked" + + +async def test_locators_should_have_repr(page: Page, server: Server): + await page.goto(server.PREFIX + "/input/button.html") + button = page.locator("button") + await button.click() + assert ( + str(button) + == f" selector='button'>" + ) + + +async def test_locators_get_attribute_should_work(page: Page, server: Server): + await page.goto(server.PREFIX + "/dom.html") + button = page.locator("#outer") + assert await button.get_attribute("name") == "value" + assert await button.get_attribute("foo") is None + + +async def test_locators_input_value_should_work(page: Page, server: Server): + await page.goto(server.PREFIX + "/dom.html") + await page.fill("#textarea", "input value") + text_area = page.locator("#textarea") + assert await text_area.input_value() == "input value" + + +async def test_locators_inner_html_should_work(page: Page, server: Server): + await page.goto(server.PREFIX + "/dom.html") + locator = page.locator("#outer") + assert await locator.inner_html() == '
Text,\nmore text
' + + +async def test_locators_inner_text_should_work(page: Page, server: Server): + await page.goto(server.PREFIX + "/dom.html") + locator = page.locator("#inner") + assert await locator.inner_text() == "Text, more text" + + +async def test_locators_text_content_should_work(page: Page, server: Server): + await page.goto(server.PREFIX + "/dom.html") + locator = page.locator("#inner") + assert await locator.text_content() == "Text,\nmore text" + + +async def test_locators_is_hidden_and_is_visible_should_work(page: Page): + await page.set_content("
Hi
") + + div = page.locator("div") + assert await div.is_visible() is True + assert await div.is_hidden() is False + + span = page.locator("span") + assert await span.is_visible() is False + assert await span.is_hidden() is True + + +async def test_locators_is_enabled_and_is_disabled_should_work(page: Page): + await page.set_content( + """ + + +
div
+ """ + ) + + div = page.locator("div") + assert await div.is_enabled() is True + assert await div.is_disabled() is False + + button1 = page.locator(':text("button1")') + assert await button1.is_enabled() is False + assert await button1.is_disabled() is True + + button1 = page.locator(':text("button2")') + assert await button1.is_enabled() is True + assert await button1.is_disabled() is False + + +async def test_locators_is_editable_should_work(page: Page): + await page.set_content( + """ + + """ + ) + + input1 = page.locator("#input1") + assert await input1.is_editable() is False + + input2 = page.locator("#input2") + assert await input2.is_editable() is True + + +async def test_locators_is_checked_should_work(page: Page): + await page.set_content( + """ +
Not a checkbox
+ """ + ) + + element = page.locator("input") + assert await element.is_checked() is True + await element.evaluate("e => e.checked = false") + assert await element.is_checked() is False + + +async def test_locators_all_text_contents_should_work(page: Page): + await page.set_content( + """ +
A
B
C
+ """ + ) + + element = page.locator("div") + assert await element.all_text_contents() == ["A", "B", "C"] + + +async def test_locators_all_inner_texts(page: Page): + await page.set_content( + """ +
A
B
C
+ """ + ) + + element = page.locator("div") + assert await element.all_inner_texts() == ["A", "B", "C"] + + +async def test_locators_should_query_existing_element(page: Page, server: Server): + await page.goto(server.PREFIX + "/playground.html") + await page.set_content( + """
A
""" + ) + html = page.locator("html") + second = html.locator(".second") + inner = second.locator(".inner") + assert ( + await page.evaluate("e => e.textContent", await inner.element_handle()) == "A" + ) + + +async def test_locators_evaluate_handle_should_work(page: Page, server: Server): + await page.goto(server.PREFIX + "/dom.html") + outer = page.locator("#outer") + inner = outer.locator("#inner") + check = inner.locator("#check") + text = await inner.evaluate_handle("e => e.firstChild") + await page.evaluate("1 + 1") + assert ( + str(outer) + == f" selector='#outer'>" + ) + assert ( + str(inner) + == f" selector='#outer >> #inner'>" + ) + assert str(text) == "JSHandle@#text=Text,↵more text" + assert ( + str(check) + == f" selector='#outer >> #inner >> #check'>" + ) + + +async def test_locators_should_query_existing_elements(page: Page): + await page.set_content( + """
A

B
""" + ) + html = page.locator("html") + elements = await html.locator("div").element_handles() + assert len(elements) == 2 + result = [] + for element in elements: + result.append(await page.evaluate("e => e.textContent", element)) + assert result == ["A", "B"] + + +async def test_locators_return_empty_array_for_non_existing_elements(page: Page): + await page.set_content( + """
A

B
""" + ) + html = page.locator("html") + elements = await html.locator("abc").element_handles() + assert len(elements) == 0 + assert elements == [] + + +async def test_locators_evaluate_all_should_work(page: Page): + await page.set_content( + """
""" + ) + tweet = page.locator(".tweet .like") + content = await tweet.evaluate_all("nodes => nodes.map(n => n.innerText)") + assert content == ["100", "10"] + + +async def test_locators_evaluate_all_should_work_with_missing_selector(page: Page): + await page.set_content( + """
not-a-child-div
nodes.length") + assert nodes_length == 0 + + +async def test_locators_hover_should_work(page: Page, server: Server): + await page.goto(server.PREFIX + "/input/scrollable.html") + button = page.locator("#button-6") + await button.hover() + assert ( + await page.evaluate("document.querySelector('button:hover').id") == "button-6" + ) + + +async def test_locators_fill_should_work(page: Page, server: Server): + await page.goto(server.PREFIX + "/input/textarea.html") + button = page.locator("input") + await button.fill("some value") + assert await page.evaluate("result") == "some value" + + +async def test_locators_check_should_work(page: Page): + await page.set_content("") + button = page.locator("input") + await button.check() + assert await page.evaluate("checkbox.checked") is True + + +async def test_locators_uncheck_should_work(page: Page): + await page.set_content("") + button = page.locator("input") + await button.uncheck() + assert await page.evaluate("checkbox.checked") is False + + +async def test_locators_select_option_should_work(page: Page, server: Server): + await page.goto(server.PREFIX + "/input/select.html") + select = page.locator("select") + await select.select_option("blue") + assert await page.evaluate("result.onInput") == ["blue"] + assert await page.evaluate("result.onChange") == ["blue"] + + +async def test_locators_focus_should_work(page: Page, server: Server): + await page.goto(server.PREFIX + "/input/button.html") + button = page.locator("button") + assert await button.evaluate("button => document.activeElement === button") is False + await button.focus() + assert await button.evaluate("button => document.activeElement === button") is True + + +async def test_locators_dispatch_event_should_work(page: Page, server: Server): + await page.goto(server.PREFIX + "/input/button.html") + button = page.locator("button") + await button.dispatch_event("click") + assert await page.evaluate("result") == "Clicked" + + +async def test_locators_should_upload_a_file(page: Page, server: Server): + await page.goto(server.PREFIX + "/input/fileupload.html") + input = page.locator("input[type=file]") + + file_path = os.path.relpath(FILE_TO_UPLOAD, os.getcwd()) + await input.set_input_files(file_path) + assert ( + await page.evaluate("e => e.files[0].name", await input.element_handle()) + == "file-to-upload.txt" + ) + + +async def test_locators_should_press(page: Page): + await page.set_content("") + await page.locator("input").press("h") + await page.eval_on_selector("input", "input => input.value") == "h" + + +async def test_locators_should_scroll_into_view(page: Page, server: Server): + await page.goto(server.PREFIX + "/offscreenbuttons.html") + for i in range(11): + button = page.locator(f"#btn{i}") + before = await button.evaluate( + "button => button.getBoundingClientRect().right - window.innerWidth" + ) + assert before == 10 * i + await button.scroll_into_view_if_needed() + after = await button.evaluate( + "button => button.getBoundingClientRect().right - window.innerWidth" + ) + assert after <= 0 + await page.evaluate("window.scrollTo(0, 0)") + + +async def test_locators_should_select_textarea( + page: Page, server: Server, browser_name: str +): + await page.goto(server.PREFIX + "/input/textarea.html") + textarea = page.locator("textarea") + await textarea.evaluate("textarea => textarea.value = 'some value'") + await textarea.select_text() + if browser_name == "firefox": + assert await textarea.evaluate("el => el.selectionStart") == 0 + assert await textarea.evaluate("el => el.selectionEnd") == 10 + else: + assert await page.evaluate("window.getSelection().toString()") == "some value" + + +async def test_locators_should_type(page: Page): + await page.set_content("") + await page.locator("input").type("hello") + await page.eval_on_selector("input", "input => input.value") == "hello" + + +async def test_locators_should_screenshot( + page: Page, server: Server, assert_to_be_golden +): + await page.set_viewport_size( + { + "width": 500, + "height": 500, + } + ) + await page.goto(server.PREFIX + "/grid.html") + await page.evaluate("window.scrollBy(50, 100)") + element = page.locator(".box:nth-of-type(3)") + assert_to_be_golden( + await element.screenshot(), "screenshot-element-bounding-box.png" + ) + + +async def test_locators_should_return_bounding_box(page: Page, server: Server): + await page.set_viewport_size( + { + "width": 500, + "height": 500, + } + ) + await page.goto(server.PREFIX + "/grid.html") + element = page.locator(".box:nth-of-type(13)") + box = await element.bounding_box() + assert box == { + "x": 100, + "y": 50, + "width": 50, + "height": 50, + } + + +async def test_locators_should_respect_first_and_last(page: Page): + await page.set_content( + """ +
+

A

+

A

A

+

A

A

A

+
""" + ) + assert await page.locator("div >> p").count() == 6 + assert await page.locator("div").locator("p").count() == 6 + assert await page.locator("div").first.locator("p").count() == 1 + assert await page.locator("div").last.locator("p").count() == 3 + + +async def test_locators_should_respect_nth(page: Page): + await page.set_content( + """ +
+

A

+

A

A

+

A

A

A

+
""" + ) + assert await page.locator("div >> p").nth(0).count() == 1 + assert await page.locator("div").nth(1).locator("p").count() == 2 + assert await page.locator("div").nth(2).locator("p").count() == 3 + + +async def test_locators_should_throw_on_capture_without_nth(page: Page): + await page.set_content( + """ +

A

+ """ + ) + with pytest.raises(Error, match="Can't query n-th element"): + await page.locator("*css=div >> p").nth(0).click() + + +async def test_locators_should_throw_due_to_strictness(page: Page): + await page.set_content( + """ +
A
B
+ """ + ) + with pytest.raises(Error, match="strict mode violation"): + await page.locator("div").is_visible() + + +async def test_locators_should_throw_due_to_strictness_2(page: Page): + await page.set_content( + """ + + """ + ) + with pytest.raises(Error, match="strict mode violation"): + await page.locator("option").evaluate("e => {}") diff --git a/tests/async/test_tap.py b/tests/async/test_tap.py index f60cfddce..026e3cdcd 100644 --- a/tests/async/test_tap.py +++ b/tests/async/test_tap.py @@ -16,7 +16,7 @@ import pytest -from playwright.async_api import ElementHandle, JSHandle +from playwright.async_api import ElementHandle, JSHandle, Page @pytest.fixture @@ -190,6 +190,34 @@ async def test_should_wait_until_an_element_is_visible_to_tap_it(page): assert await div.text_content() == "clicked" +async def test_locators_tap(page: Page): + await page.set_content( + """ +
a
+
b
+ """ + ) + await page.locator("#a").tap() + element_handle = await track_events(await page.query_selector("#b")) + await page.locator("#b").tap() + assert await element_handle.json_value() == [ + "pointerover", + "pointerenter", + "pointerdown", + "touchstart", + "pointerup", + "pointerout", + "pointerleave", + "touchend", + "mouseover", + "mouseenter", + "mousemove", + "mousedown", + "mouseup", + "click", + ] + + async def track_events(target: ElementHandle) -> JSHandle: return await target.evaluate_handle( """target => { diff --git a/tests/golden-chromium/screenshot-element-bounding-box.png b/tests/golden-chromium/screenshot-element-bounding-box.png new file mode 100644 index 0000000000000000000000000000000000000000..c2c3ddca298aba5c502f56e6656fd9330220b327 GIT binary patch literal 474 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^TK3NDj4wz}vwUf`e5)Ukjg=c4B?ZY6Q{gD+<_wP}}l z6nSQ36>Zn#-f|$iNz7fE$&pKHm-_rCzvuN??>N7ATC?@xO>6j=CvYp+J%0ba_|K}- z?e1;A{tEc13fvXy$m4X`&ax<)>7s7qi)jue-U{}mHHEE$Jc%sMXWq*OW$s-$i%G;W zZF||t6r&}VGkq>ExtSx>>!xYDf7J|T5r=j2<5pbF65(PsvM%I1RJ=tim8p?oS*Dfk z^|OT?x8ELn{&}OJ?ag94p-v0itCJs3c=3Z{t=G@fKZ902`4Zw^|LI!P4gZAOW-CLy zZnhPjNK*3L8l^h>>?RwlH95zZzB$)Wuj{urPJRCQ;w>U!mP`4PSezL|x?Qg=R|`2? z63iqS9eMQe#}9S&%O8Ea|IFgamuF(Pw=sImn|nEL`|j&|;`G&T&-U|Y;!@~!TUip(bP*}7q&3SPEgSM@h9ZZ`&x!)qR}^g{rtKT7+DOSu6{1- HoD!M<45`mj literal 0 HcmV?d00001 diff --git a/tests/golden-firefox/screenshot-element-bounding-box.png b/tests/golden-firefox/screenshot-element-bounding-box.png new file mode 100644 index 0000000000000000000000000000000000000000..9e208f86d8f7690a79f6bf31ccdd2032512693a3 GIT binary patch literal 311 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nETf1WOmAsLNtuPUxdz^zaVj`B z@qPFONtbB0i_;vpqzdFBF)Le3V%-^74w(2>&P%VDpyJ8VkbApmUV0Y?W5NCJ1q!#f z2{KGi{dq{#RgmwA1z*>R>s67{5*_PQl^!~8(otrJyDqAya=w`9fkofFZy)$NAJ1U0 z@SS}9@F%XH^A%t3s&cJp?ci9f@rmyPmt@<+eZ9*vBSNpn-v9p4;nl8vit}0j|7H8R wg(pd%`Ne64TSgt*coyk4XF!TA6BrTzopr0K4LVFaQ7m literal 0 HcmV?d00001 diff --git a/tests/golden-webkit/screenshot-element-bounding-box.png b/tests/golden-webkit/screenshot-element-bounding-box.png new file mode 100644 index 0000000000000000000000000000000000000000..1f74a3bafbc259b654dfd30bea8ac1eb72be4ccb GIT binary patch literal 445 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1SBWM%0B~AY)RhkE)4%caKYZ?lNlHoi#%N% zLn2z=-mvvPoFLNnQ26Dfg&abQC-YrB$iZkB!L_R+vZYC0K*WW`b%nRFXno_1rOpe3 z=d|#x+|qndPi)SdUCVE3Z2`eBeB+G?2?ilS$|-u1QW zZ;tN#6A4T@eqjs??&js6Kf7VQ>&8vX%*;05eDnLUv(hXJwQEoB)=IQ71nk=-$Jc%_ zP@tvz?_HC~dCy(@qt=SacOO+^(C*%>(_Pp5#}wIbP_Hf%Oig82a8rKC*&jFAr+$pQ VbeIWRsLJYD@<);T3K0RTXQytV)U literal 0 HcmV?d00001 diff --git a/tests/sync/test_locators.py b/tests/sync/test_locators.py new file mode 100644 index 000000000..2ffc8388f --- /dev/null +++ b/tests/sync/test_locators.py @@ -0,0 +1,442 @@ +# 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 os + +import pytest + +from playwright._impl._path_utils import get_file_dirname +from playwright.sync_api import Error, Page +from tests.server import Server + +_dirname = get_file_dirname() +FILE_TO_UPLOAD = _dirname / ".." / "assets/file-to-upload.txt" + + +def test_locators_click_should_work(page: Page, server: Server): + page.goto(server.PREFIX + "/input/button.html") + button = page.locator("button") + button.click() + assert page.evaluate("window['result']") == "Clicked" + + +def test_locators_click_should_work_with_node_removed(page: Page, server: Server): + page.goto(server.PREFIX + "/input/button.html") + page.evaluate("delete window['Node']") + button = page.locator("button") + button.click() + assert page.evaluate("window['result']") == "Clicked" + + +def test_locators_click_should_work_for_text_nodes(page: Page, server: Server): + page.goto(server.PREFIX + "/input/button.html") + page.evaluate( + """() => { + window['double'] = false; + const button = document.querySelector('button'); + button.addEventListener('dblclick', event => { + window['double'] = true; + }); + }""" + ) + button = page.locator("button") + button.dblclick() + assert page.evaluate("double") is True + assert page.evaluate("result") == "Clicked" + + +def test_locators_should_have_repr(page: Page, server: Server): + page.goto(server.PREFIX + "/input/button.html") + button = page.locator("button") + button.click() + assert ( + str(button) + == f" selector='button'>" + ) + + +def test_locators_get_attribute_should_work(page: Page, server: Server): + page.goto(server.PREFIX + "/dom.html") + button = page.locator("#outer") + assert button.get_attribute("name") == "value" + assert button.get_attribute("foo") is None + + +def test_locators_input_value_should_work(page: Page, server: Server): + page.goto(server.PREFIX + "/dom.html") + page.fill("#textarea", "input value") + text_area = page.locator("#textarea") + assert text_area.input_value() == "input value" + + +def test_locators_inner_html_should_work(page: Page, server: Server): + page.goto(server.PREFIX + "/dom.html") + locator = page.locator("#outer") + assert locator.inner_html() == '
Text,\nmore text
' + + +def test_locators_inner_text_should_work(page: Page, server: Server): + page.goto(server.PREFIX + "/dom.html") + locator = page.locator("#inner") + assert locator.inner_text() == "Text, more text" + + +def test_locators_text_content_should_work(page: Page, server: Server): + page.goto(server.PREFIX + "/dom.html") + locator = page.locator("#inner") + assert locator.text_content() == "Text,\nmore text" + + +def test_locators_is_hidden_and_is_visible_should_work(page: Page): + page.set_content("
Hi
") + + div = page.locator("div") + assert div.is_visible() is True + assert div.is_hidden() is False + + span = page.locator("span") + assert span.is_visible() is False + assert span.is_hidden() is True + + +def test_locators_is_enabled_and_is_disabled_should_work(page: Page): + page.set_content( + """ + + +
div
+ """ + ) + + div = page.locator("div") + assert div.is_enabled() is True + assert div.is_disabled() is False + + button1 = page.locator(':text("button1")') + assert button1.is_enabled() is False + assert button1.is_disabled() is True + + button1 = page.locator(':text("button2")') + assert button1.is_enabled() is True + assert button1.is_disabled() is False + + +def test_locators_is_editable_should_work(page: Page): + page.set_content( + """ + + """ + ) + + input1 = page.locator("#input1") + assert input1.is_editable() is False + + input2 = page.locator("#input2") + assert input2.is_editable() is True + + +def test_locators_is_checked_should_work(page: Page): + page.set_content( + """ +
Not a checkbox
+ """ + ) + + element = page.locator("input") + assert element.is_checked() is True + element.evaluate("e => e.checked = false") + assert element.is_checked() is False + + +def test_locators_all_text_contents_should_work(page: Page): + page.set_content( + """ +
A
B
C
+ """ + ) + + element = page.locator("div") + assert element.all_text_contents() == ["A", "B", "C"] + + +def test_locators_all_inner_texts(page: Page): + page.set_content( + """ +
A
B
C
+ """ + ) + + element = page.locator("div") + assert element.all_inner_texts() == ["A", "B", "C"] + + +def test_locators_should_query_existing_element(page: Page, server: Server): + page.goto(server.PREFIX + "/playground.html") + page.set_content( + """
A
""" + ) + html = page.locator("html") + second = html.locator(".second") + inner = second.locator(".inner") + assert page.evaluate("e => e.textContent", inner.element_handle()) == "A" + + +def test_locators_evaluate_handle_should_work(page: Page, server: Server): + page.goto(server.PREFIX + "/dom.html") + outer = page.locator("#outer") + inner = outer.locator("#inner") + check = inner.locator("#check") + text = inner.evaluate_handle("e => e.firstChild") + page.evaluate("1 + 1") + assert ( + str(outer) + == f" selector='#outer'>" + ) + assert ( + str(inner) + == f" selector='#outer >> #inner'>" + ) + assert str(text) == "JSHandle@#text=Text,↵more text" + assert ( + str(check) + == f" selector='#outer >> #inner >> #check'>" + ) + + +def test_locators_should_query_existing_elements(page: Page): + page.set_content("""
A

B
""") + html = page.locator("html") + elements = html.locator("div").element_handles() + assert len(elements) == 2 + result = [] + for element in elements: + result.append(page.evaluate("e => e.textContent", element)) + assert result == ["A", "B"] + + +def test_locators_return_empty_array_for_non_existing_elements(page: Page): + page.set_content("""
A

B
""") + html = page.locator("html") + elements = html.locator("abc").element_handles() + assert len(elements) == 0 + assert elements == [] + + +def test_locators_evaluate_all_should_work(page: Page): + page.set_content( + """
""" + ) + tweet = page.locator(".tweet .like") + content = tweet.evaluate_all("nodes => nodes.map(n => n.innerText)") + assert content == ["100", "10"] + + +def test_locators_evaluate_all_should_work_with_missing_selector(page: Page): + page.set_content("""
not-a-child-div
nodes.length") + assert nodes_length == 0 + + +def test_locators_hover_should_work(page: Page, server: Server): + page.goto(server.PREFIX + "/input/scrollable.html") + button = page.locator("#button-6") + button.hover() + assert page.evaluate("document.querySelector('button:hover').id") == "button-6" + + +def test_locators_fill_should_work(page: Page, server: Server): + page.goto(server.PREFIX + "/input/textarea.html") + button = page.locator("input") + button.fill("some value") + assert page.evaluate("result") == "some value" + + +def test_locators_check_should_work(page: Page): + page.set_content("") + button = page.locator("input") + button.check() + assert page.evaluate("checkbox.checked") is True + + +def test_locators_uncheck_should_work(page: Page): + page.set_content("") + button = page.locator("input") + button.uncheck() + assert page.evaluate("checkbox.checked") is False + + +def test_locators_select_option_should_work(page: Page, server: Server): + page.goto(server.PREFIX + "/input/select.html") + select = page.locator("select") + select.select_option("blue") + assert page.evaluate("result.onInput") == ["blue"] + assert page.evaluate("result.onChange") == ["blue"] + + +def test_locators_focus_should_work(page: Page, server: Server): + page.goto(server.PREFIX + "/input/button.html") + button = page.locator("button") + assert button.evaluate("button => document.activeElement === button") is False + button.focus() + assert button.evaluate("button => document.activeElement === button") is True + + +def test_locators_dispatch_event_should_work(page: Page, server: Server): + page.goto(server.PREFIX + "/input/button.html") + button = page.locator("button") + button.dispatch_event("click") + assert page.evaluate("result") == "Clicked" + + +def test_locators_should_upload_a_file(page: Page, server: Server): + page.goto(server.PREFIX + "/input/fileupload.html") + input = page.locator("input[type=file]") + + file_path = os.path.relpath(FILE_TO_UPLOAD, os.getcwd()) + input.set_input_files(file_path) + assert ( + page.evaluate("e => e.files[0].name", input.element_handle()) + == "file-to-upload.txt" + ) + + +def test_locators_should_press(page: Page): + page.set_content("") + page.locator("input").press("h") + page.eval_on_selector("input", "input => input.value") == "h" + + +def test_locators_should_scroll_into_view(page: Page, server: Server): + page.goto(server.PREFIX + "/offscreenbuttons.html") + for i in range(11): + button = page.locator(f"#btn{i}") + before = button.evaluate( + "button => button.getBoundingClientRect().right - window.innerWidth" + ) + assert before == 10 * i + button.scroll_into_view_if_needed() + after = button.evaluate( + "button => button.getBoundingClientRect().right - window.innerWidth" + ) + assert after <= 0 + page.evaluate("window.scrollTo(0, 0)") + + +def test_locators_should_select_textarea(page: Page, server: Server, browser_name: str): + page.goto(server.PREFIX + "/input/textarea.html") + textarea = page.locator("textarea") + textarea.evaluate("textarea => textarea.value = 'some value'") + textarea.select_text() + if browser_name == "firefox": + assert textarea.evaluate("el => el.selectionStart") == 0 + assert textarea.evaluate("el => el.selectionEnd") == 10 + else: + assert page.evaluate("window.getSelection().toString()") == "some value" + + +def test_locators_should_type(page: Page): + page.set_content("") + page.locator("input").type("hello") + page.eval_on_selector("input", "input => input.value") == "hello" + + +def test_locators_should_screenshot(page: Page, server: Server, assert_to_be_golden): + page.set_viewport_size( + { + "width": 500, + "height": 500, + } + ) + page.goto(server.PREFIX + "/grid.html") + page.evaluate("window.scrollBy(50, 100)") + element = page.locator(".box:nth-of-type(3)") + assert_to_be_golden(element.screenshot(), "screenshot-element-bounding-box.png") + + +def test_locators_should_return_bounding_box(page: Page, server: Server): + page.set_viewport_size( + { + "width": 500, + "height": 500, + } + ) + page.goto(server.PREFIX + "/grid.html") + element = page.locator(".box:nth-of-type(13)") + box = element.bounding_box() + assert box == { + "x": 100, + "y": 50, + "width": 50, + "height": 50, + } + + +def test_locators_should_respect_first_and_last(page: Page): + page.set_content( + """ +
+

A

+

A

A

+

A

A

A

+
""" + ) + assert page.locator("div >> p").count() == 6 + assert page.locator("div").locator("p").count() == 6 + assert page.locator("div").first.locator("p").count() == 1 + assert page.locator("div").last.locator("p").count() == 3 + + +def test_locators_should_respect_nth(page: Page): + page.set_content( + """ +
+

A

+

A

A

+

A

A

A

+
""" + ) + assert page.locator("div >> p").nth(0).count() == 1 + assert page.locator("div").nth(1).locator("p").count() == 2 + assert page.locator("div").nth(2).locator("p").count() == 3 + + +def test_locators_should_throw_on_capture_without_nth(page: Page): + page.set_content( + """ +

A

+ """ + ) + with pytest.raises(Error, match="Can't query n-th element"): + page.locator("*css=div >> p").nth(0).click() + + +def test_locators_should_throw_due_to_strictness(page: Page): + page.set_content( + """ +
A
B
+ """ + ) + with pytest.raises(Error, match="strict mode violation"): + page.locator("div").is_visible() + + +def test_locators_should_throw_due_to_strictness_2(page: Page): + page.set_content( + """ + + """ + ) + with pytest.raises(Error, match="strict mode violation"): + page.locator("option").evaluate("e => {}") From 12ba09d279b2bb8da75a853f9cd04cd67121c619 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 9 Aug 2021 01:23:19 +0200 Subject: [PATCH 005/672] feat(roll): roll Playwright 1.14.0-next-1628399336000 (#839) --- playwright/_impl/_frame.py | 2 ++ playwright/_impl/_page.py | 2 ++ playwright/_impl/_tracing.py | 2 +- playwright/async_api/_generated.py | 28 ++++++++++++++++++++++++---- playwright/sync_api/_generated.py | 28 ++++++++++++++++++++++++---- setup.py | 2 +- tests/async/test_console.py | 19 +++++++++++++------ tests/async/test_jshandle.py | 13 ++++++++----- tests/async/test_worker.py | 7 +++++-- tests/sync/test_console.py | 19 +++++++++++++------ tests/sync/test_sync.py | 10 +++++++--- 11 files changed, 100 insertions(+), 32 deletions(-) diff --git a/playwright/_impl/_frame.py b/playwright/_impl/_frame.py index c79a50b95..a947d9cf1 100644 --- a/playwright/_impl/_frame.py +++ b/playwright/_impl/_frame.py @@ -522,6 +522,8 @@ async def drag_and_drop( self, source: str, target: str, + source_position: Position = None, + target_position: Position = None, force: bool = None, noWaitAfter: bool = None, strict: bool = None, diff --git a/playwright/_impl/_page.py b/playwright/_impl/_page.py index fd0c2aa19..5e0bce5cd 100644 --- a/playwright/_impl/_page.py +++ b/playwright/_impl/_page.py @@ -699,6 +699,8 @@ async def drag_and_drop( self, source: str, target: str, + source_position: Position = None, + target_position: Position = None, force: bool = None, noWaitAfter: bool = None, timeout: float = None, diff --git a/playwright/_impl/_tracing.py b/playwright/_impl/_tracing.py index 45b9bb0da..784770888 100644 --- a/playwright/_impl/_tracing.py +++ b/playwright/_impl/_tracing.py @@ -37,7 +37,6 @@ async def start( await self._channel.send("tracingStart", params) async def stop(self, path: Union[pathlib.Path, str] = None) -> None: - await self._channel.send("tracingStop") if path: artifact = cast( Artifact, from_channel(await self._channel.send("tracingExport")) @@ -46,3 +45,4 @@ async def stop(self, path: Union[pathlib.Path, str] = None) -> None: artifact._is_remote = self._context._browser._is_remote await artifact.save_as(path) await artifact.delete() + await self._channel.send("tracingStop") diff --git a/playwright/async_api/_generated.py b/playwright/async_api/_generated.py index 6d2371ef7..c0372b75d 100644 --- a/playwright/async_api/_generated.py +++ b/playwright/async_api/_generated.py @@ -1905,7 +1905,7 @@ async def select_text( async def input_value(self, *, timeout: float = None) -> str: """ElementHandle.input_value - Returns `input.value` for `` or `") page.eval_on_selector("textarea", "t => t.readOnly = true") input1 = page.query_selector("#input1") + assert input1 assert input1.is_editable() is False assert page.is_editable("#input1") is False input2 = page.query_selector("#input2") + assert input2 assert input2.is_editable() assert page.is_editable("#input2") textarea = page.query_selector("textarea") + assert textarea assert textarea.is_editable() is False assert page.is_editable("textarea") is False -def test_is_checked_should_work(page): +def test_is_checked_should_work(page: Page) -> None: page.set_content('
Not a checkbox
') handle = page.query_selector("input") + assert handle assert handle.is_checked() assert page.is_checked("input") handle.evaluate("input => input.checked = false") @@ -557,9 +637,10 @@ def test_is_checked_should_work(page): assert "Not a checkbox or radio button" in exc_info.value.message -def test_input_value(page: Page, server: Server): +def test_input_value(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/input/textarea.html") element = page.query_selector("input") + assert element element.fill("my-text-content") assert element.input_value() == "my-text-content" @@ -567,9 +648,10 @@ def test_input_value(page: Page, server: Server): assert element.input_value() == "" -def test_set_checked(page: Page): +def test_set_checked(page: Page) -> None: page.set_content("``") input = page.query_selector("input") + assert input input.set_checked(True) assert page.evaluate("checkbox.checked") input.set_checked(False) diff --git a/tests/sync/test_element_handle_wait_for_element_state.py b/tests/sync/test_element_handle_wait_for_element_state.py index c5604c2a7..8f1f2912c 100644 --- a/tests/sync/test_element_handle_wait_for_element_state.py +++ b/tests/sync/test_element_handle_wait_for_element_state.py @@ -14,94 +14,105 @@ import pytest -from playwright.async_api import Error +from playwright.sync_api import Error, Page -def test_should_wait_for_visible(page): +def test_should_wait_for_visible(page: Page) -> None: page.set_content('') div = page.query_selector("div") + assert div page.evaluate('setTimeout(() => div.style.display = "block", 500)') assert div.is_visible() is False div.wait_for_element_state("visible") assert div.is_visible() -def test_should_wait_for_already_visible(page): +def test_should_wait_for_already_visible(page: Page) -> None: page.set_content("
content
") div = page.query_selector("div") + assert div div.wait_for_element_state("visible") -def test_should_timeout_waiting_for_visible(page): +def test_should_timeout_waiting_for_visible(page: Page) -> None: page.set_content('
content
') div = page.query_selector("div") + assert div with pytest.raises(Error) as exc_info: div.wait_for_element_state("visible", timeout=1000) assert "Timeout 1000ms exceeded" in exc_info.value.message -def test_should_throw_waiting_for_visible_when_detached(page): +def test_should_throw_waiting_for_visible_when_detached(page: Page) -> None: page.set_content('') div = page.query_selector("div") + assert div page.evaluate("setTimeout(() => div.remove(), 500)") with pytest.raises(Error) as exc_info: div.wait_for_element_state("visible") assert "Element is not attached to the DOM" in exc_info.value.message -def test_should_wait_for_hidden(page): +def test_should_wait_for_hidden(page: Page) -> None: page.set_content("
content
") div = page.query_selector("div") + assert div page.evaluate('setTimeout(() => div.style.display = "none", 500)') assert div.is_hidden() is False div.wait_for_element_state("hidden") assert div.is_hidden() -def test_should_wait_for_already_hidden(page): +def test_should_wait_for_already_hidden(page: Page) -> None: page.set_content("
") div = page.query_selector("div") + assert div div.wait_for_element_state("hidden") -def test_should_wait_for_hidden_when_detached(page): +def test_should_wait_for_hidden_when_detached(page: Page) -> None: page.set_content("
content
") div = page.query_selector("div") + assert div page.evaluate("setTimeout(() => div.remove(), 500)") div.wait_for_element_state("hidden") assert div.is_hidden() -def test_should_wait_for_enabled_button(page, server): +def test_should_wait_for_enabled_button(page: Page) -> None: page.set_content("") span = page.query_selector("text=Target") + assert span assert span.is_enabled() is False page.evaluate("setTimeout(() => button.disabled = false, 500)") span.wait_for_element_state("enabled") assert span.is_enabled() -def test_should_throw_waiting_for_enabled_when_detached(page): +def test_should_throw_waiting_for_enabled_when_detached(page: Page) -> None: page.set_content("") button = page.query_selector("button") + assert button page.evaluate("setTimeout(() => button.remove(), 500)") with pytest.raises(Error) as exc_info: button.wait_for_element_state("enabled") assert "Element is not attached to the DOM" in exc_info.value.message -def test_should_wait_for_disabled_button(page): +def test_should_wait_for_disabled_button(page: Page) -> None: page.set_content("") span = page.query_selector("text=Target") + assert span assert span.is_disabled() is False page.evaluate("setTimeout(() => button.disabled = true, 500)") span.wait_for_element_state("disabled") assert span.is_disabled() -def test_should_wait_for_editable_input(page, server): +def test_should_wait_for_editable_input(page: Page) -> None: page.set_content("") input = page.query_selector("input") + assert input page.evaluate("setTimeout(() => input.readOnly = false, 500)") assert input.is_editable() is False input.wait_for_element_state("editable") diff --git a/tests/sync/test_har.py b/tests/sync/test_har.py index f511c74c9..52e3166cf 100644 --- a/tests/sync/test_har.py +++ b/tests/sync/test_har.py @@ -15,9 +15,13 @@ import base64 import json import os +from pathlib import Path +from playwright.sync_api import Browser +from tests.server import Server -def test_should_work(browser, server, tmpdir): + +def test_should_work(browser: Browser, server: Server, tmpdir: Path) -> None: path = os.path.join(tmpdir, "log.har") context = browser.new_context(record_har_path=path) page = context.new_page() @@ -28,7 +32,7 @@ def test_should_work(browser, server, tmpdir): assert "log" in data -def test_should_omit_content(browser, server, tmpdir): +def test_should_omit_content(browser: Browser, server: Server, tmpdir: Path) -> None: path = os.path.join(tmpdir, "log.har") context = browser.new_context(record_har_path=path, record_har_omit_content=True) page = context.new_page() @@ -43,7 +47,7 @@ def test_should_omit_content(browser, server, tmpdir): assert "text" not in content1 -def test_should_include_content(browser, server, tmpdir): +def test_should_include_content(browser: Browser, server: Server, tmpdir: Path) -> None: path = os.path.join(tmpdir, "log.har") context = browser.new_context(record_har_path=path) page = context.new_page() diff --git a/tests/sync/test_input.py b/tests/sync/test_input.py index 86a64d106..ff28f6a63 100644 --- a/tests/sync/test_input.py +++ b/tests/sync/test_input.py @@ -13,7 +13,10 @@ # limitations under the License. -def test_expect_file_chooser(page, server): +from playwright.sync_api import Page + + +def test_expect_file_chooser(page: Page) -> None: page.set_content("") with page.expect_file_chooser() as fc_info: page.click('input[type="file"]') diff --git a/tests/sync/test_listeners.py b/tests/sync/test_listeners.py index 49d766114..56a7afb2f 100644 --- a/tests/sync/test_listeners.py +++ b/tests/sync/test_listeners.py @@ -13,10 +13,14 @@ # limitations under the License. -def test_listeners(page, server): +from playwright.sync_api import Page, Response +from tests.server import Server + + +def test_listeners(page: Page, server: Server) -> None: log = [] - def print_response(response): + def print_response(response: Response) -> None: log.append(response) page.on("response", print_response) diff --git a/tests/sync/test_locators.py b/tests/sync/test_locators.py index 3c9cac1c5..07050542e 100644 --- a/tests/sync/test_locators.py +++ b/tests/sync/test_locators.py @@ -13,6 +13,7 @@ # limitations under the License. import os +from typing import Callable import pytest @@ -24,14 +25,16 @@ FILE_TO_UPLOAD = _dirname / ".." / "assets/file-to-upload.txt" -def test_locators_click_should_work(page: Page, server: Server): +def test_locators_click_should_work(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/input/button.html") button = page.locator("button") button.click() assert page.evaluate("window['result']") == "Clicked" -def test_locators_click_should_work_with_node_removed(page: Page, server: Server): +def test_locators_click_should_work_with_node_removed( + page: Page, server: Server +) -> None: page.goto(server.PREFIX + "/input/button.html") page.evaluate("delete window['Node']") button = page.locator("button") @@ -39,7 +42,7 @@ def test_locators_click_should_work_with_node_removed(page: Page, server: Server assert page.evaluate("window['result']") == "Clicked" -def test_locators_click_should_work_for_text_nodes(page: Page, server: Server): +def test_locators_click_should_work_for_text_nodes(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/input/button.html") page.evaluate( """() => { @@ -56,7 +59,7 @@ def test_locators_click_should_work_for_text_nodes(page: Page, server: Server): assert page.evaluate("result") == "Clicked" -def test_locators_should_have_repr(page: Page, server: Server): +def test_locators_should_have_repr(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/input/button.html") button = page.locator("button") button.click() @@ -66,39 +69,39 @@ def test_locators_should_have_repr(page: Page, server: Server): ) -def test_locators_get_attribute_should_work(page: Page, server: Server): +def test_locators_get_attribute_should_work(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/dom.html") button = page.locator("#outer") assert button.get_attribute("name") == "value" assert button.get_attribute("foo") is None -def test_locators_input_value_should_work(page: Page, server: Server): +def test_locators_input_value_should_work(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/dom.html") page.fill("#textarea", "input value") text_area = page.locator("#textarea") assert text_area.input_value() == "input value" -def test_locators_inner_html_should_work(page: Page, server: Server): +def test_locators_inner_html_should_work(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/dom.html") locator = page.locator("#outer") assert locator.inner_html() == '
Text,\nmore text
' -def test_locators_inner_text_should_work(page: Page, server: Server): +def test_locators_inner_text_should_work(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/dom.html") locator = page.locator("#inner") assert locator.inner_text() == "Text, more text" -def test_locators_text_content_should_work(page: Page, server: Server): +def test_locators_text_content_should_work(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/dom.html") locator = page.locator("#inner") assert locator.text_content() == "Text,\nmore text" -def test_locators_is_hidden_and_is_visible_should_work(page: Page): +def test_locators_is_hidden_and_is_visible_should_work(page: Page) -> None: page.set_content("
Hi
") div = page.locator("div") @@ -110,7 +113,7 @@ def test_locators_is_hidden_and_is_visible_should_work(page: Page): assert span.is_hidden() is True -def test_locators_is_enabled_and_is_disabled_should_work(page: Page): +def test_locators_is_enabled_and_is_disabled_should_work(page: Page) -> None: page.set_content( """ @@ -132,7 +135,7 @@ def test_locators_is_enabled_and_is_disabled_should_work(page: Page): assert button1.is_disabled() is False -def test_locators_is_editable_should_work(page: Page): +def test_locators_is_editable_should_work(page: Page) -> None: page.set_content( """ @@ -146,7 +149,7 @@ def test_locators_is_editable_should_work(page: Page): assert input2.is_editable() is True -def test_locators_is_checked_should_work(page: Page): +def test_locators_is_checked_should_work(page: Page) -> None: page.set_content( """
Not a checkbox
@@ -159,7 +162,7 @@ def test_locators_is_checked_should_work(page: Page): assert element.is_checked() is False -def test_locators_all_text_contents_should_work(page: Page): +def test_locators_all_text_contents_should_work(page: Page) -> None: page.set_content( """
A
B
C
@@ -170,7 +173,7 @@ def test_locators_all_text_contents_should_work(page: Page): assert element.all_text_contents() == ["A", "B", "C"] -def test_locators_all_inner_texts(page: Page): +def test_locators_all_inner_texts(page: Page) -> None: page.set_content( """
A
B
C
@@ -181,7 +184,7 @@ def test_locators_all_inner_texts(page: Page): assert element.all_inner_texts() == ["A", "B", "C"] -def test_locators_should_query_existing_element(page: Page, server: Server): +def test_locators_should_query_existing_element(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/playground.html") page.set_content( """
A
""" @@ -192,7 +195,7 @@ def test_locators_should_query_existing_element(page: Page, server: Server): assert page.evaluate("e => e.textContent", inner.element_handle()) == "A" -def test_locators_evaluate_handle_should_work(page: Page, server: Server): +def test_locators_evaluate_handle_should_work(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/dom.html") outer = page.locator("#outer") inner = outer.locator("#inner") @@ -214,7 +217,7 @@ def test_locators_evaluate_handle_should_work(page: Page, server: Server): ) -def test_locators_should_query_existing_elements(page: Page): +def test_locators_should_query_existing_elements(page: Page) -> None: page.set_content("""
A

B
""") html = page.locator("html") elements = html.locator("div").element_handles() @@ -225,7 +228,7 @@ def test_locators_should_query_existing_elements(page: Page): assert result == ["A", "B"] -def test_locators_return_empty_array_for_non_existing_elements(page: Page): +def test_locators_return_empty_array_for_non_existing_elements(page: Page) -> None: page.set_content("""
A

B
""") html = page.locator("html") elements = html.locator("abc").element_handles() @@ -233,7 +236,7 @@ def test_locators_return_empty_array_for_non_existing_elements(page: Page): assert elements == [] -def test_locators_evaluate_all_should_work(page: Page): +def test_locators_evaluate_all_should_work(page: Page) -> None: page.set_content( """
""" ) @@ -242,42 +245,42 @@ def test_locators_evaluate_all_should_work(page: Page): assert content == ["100", "10"] -def test_locators_evaluate_all_should_work_with_missing_selector(page: Page): +def test_locators_evaluate_all_should_work_with_missing_selector(page: Page) -> None: page.set_content("""
not-a-child-div
nodes.length") assert nodes_length == 0 -def test_locators_hover_should_work(page: Page, server: Server): +def test_locators_hover_should_work(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/input/scrollable.html") button = page.locator("#button-6") button.hover() assert page.evaluate("document.querySelector('button:hover').id") == "button-6" -def test_locators_fill_should_work(page: Page, server: Server): +def test_locators_fill_should_work(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/input/textarea.html") button = page.locator("input") button.fill("some value") assert page.evaluate("result") == "some value" -def test_locators_check_should_work(page: Page): +def test_locators_check_should_work(page: Page) -> None: page.set_content("") button = page.locator("input") button.check() assert page.evaluate("checkbox.checked") is True -def test_locators_uncheck_should_work(page: Page): +def test_locators_uncheck_should_work(page: Page) -> None: page.set_content("") button = page.locator("input") button.uncheck() assert page.evaluate("checkbox.checked") is False -def test_locators_select_option_should_work(page: Page, server: Server): +def test_locators_select_option_should_work(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/input/select.html") select = page.locator("select") select.select_option("blue") @@ -285,7 +288,7 @@ def test_locators_select_option_should_work(page: Page, server: Server): assert page.evaluate("result.onChange") == ["blue"] -def test_locators_focus_should_work(page: Page, server: Server): +def test_locators_focus_should_work(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/input/button.html") button = page.locator("button") assert button.evaluate("button => document.activeElement === button") is False @@ -293,14 +296,14 @@ def test_locators_focus_should_work(page: Page, server: Server): assert button.evaluate("button => document.activeElement === button") is True -def test_locators_dispatch_event_should_work(page: Page, server: Server): +def test_locators_dispatch_event_should_work(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/input/button.html") button = page.locator("button") button.dispatch_event("click") assert page.evaluate("result") == "Clicked" -def test_locators_should_upload_a_file(page: Page, server: Server): +def test_locators_should_upload_a_file(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/input/fileupload.html") input = page.locator("input[type=file]") @@ -312,13 +315,13 @@ def test_locators_should_upload_a_file(page: Page, server: Server): ) -def test_locators_should_press(page: Page): +def test_locators_should_press(page: Page) -> None: page.set_content("") page.locator("input").press("h") page.eval_on_selector("input", "input => input.value") == "h" -def test_locators_should_scroll_into_view(page: Page, server: Server): +def test_locators_should_scroll_into_view(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/offscreenbuttons.html") for i in range(11): button = page.locator(f"#btn{i}") @@ -334,7 +337,9 @@ def test_locators_should_scroll_into_view(page: Page, server: Server): page.evaluate("window.scrollTo(0, 0)") -def test_locators_should_select_textarea(page: Page, server: Server, browser_name: str): +def test_locators_should_select_textarea( + page: Page, server: Server, browser_name: str +) -> None: page.goto(server.PREFIX + "/input/textarea.html") textarea = page.locator("textarea") textarea.evaluate("textarea => textarea.value = 'some value'") @@ -346,13 +351,15 @@ def test_locators_should_select_textarea(page: Page, server: Server, browser_nam assert page.evaluate("window.getSelection().toString()") == "some value" -def test_locators_should_type(page: Page): +def test_locators_should_type(page: Page) -> None: page.set_content("") page.locator("input").type("hello") page.eval_on_selector("input", "input => input.value") == "hello" -def test_locators_should_screenshot(page: Page, server: Server, assert_to_be_golden): +def test_locators_should_screenshot( + page: Page, server: Server, assert_to_be_golden: Callable[[bytes, str], None] +) -> None: page.set_viewport_size( { "width": 500, @@ -365,7 +372,7 @@ def test_locators_should_screenshot(page: Page, server: Server, assert_to_be_gol assert_to_be_golden(element.screenshot(), "screenshot-element-bounding-box.png") -def test_locators_should_return_bounding_box(page: Page, server: Server): +def test_locators_should_return_bounding_box(page: Page, server: Server) -> None: page.set_viewport_size( { "width": 500, @@ -383,7 +390,7 @@ def test_locators_should_return_bounding_box(page: Page, server: Server): } -def test_locators_should_respect_first_and_last(page: Page): +def test_locators_should_respect_first_and_last(page: Page) -> None: page.set_content( """
@@ -398,7 +405,7 @@ def test_locators_should_respect_first_and_last(page: Page): assert page.locator("div").last.locator("p").count() == 3 -def test_locators_should_respect_nth(page: Page): +def test_locators_should_respect_nth(page: Page) -> None: page.set_content( """
@@ -412,7 +419,7 @@ def test_locators_should_respect_nth(page: Page): assert page.locator("div").nth(2).locator("p").count() == 3 -def test_locators_should_throw_on_capture_without_nth(page: Page): +def test_locators_should_throw_on_capture_without_nth(page: Page) -> None: page.set_content( """

A

@@ -422,7 +429,7 @@ def test_locators_should_throw_on_capture_without_nth(page: Page): page.locator("*css=div >> p").nth(1).click() -def test_locators_should_throw_due_to_strictness(page: Page): +def test_locators_should_throw_due_to_strictness(page: Page) -> None: page.set_content( """
A
B
@@ -432,7 +439,7 @@ def test_locators_should_throw_due_to_strictness(page: Page): page.locator("div").is_visible() -def test_locators_should_throw_due_to_strictness_2(page: Page): +def test_locators_should_throw_due_to_strictness_2(page: Page) -> None: page.set_content( """ @@ -442,7 +449,7 @@ def test_locators_should_throw_due_to_strictness_2(page: Page): page.locator("option").evaluate("e => {}") -def test_locators_set_checked(page: Page): +def test_locators_set_checked(page: Page) -> None: page.set_content("``") locator = page.locator("input") locator.set_checked(True) diff --git a/tests/sync/test_network.py b/tests/sync/test_network.py index c81263799..f974b0a6c 100644 --- a/tests/sync/test_network.py +++ b/tests/sync/test_network.py @@ -18,8 +18,9 @@ from tests.server import Server -def test_response_server_addr(page: Page, server: Server): +def test_response_server_addr(page: Page, server: Server) -> None: response = page.goto(server.EMPTY_PAGE) + assert response server_addr = response.server_addr() assert server_addr assert server_addr["port"] == server.PORT @@ -27,12 +28,17 @@ def test_response_server_addr(page: Page, server: Server): def test_response_security_details( - browser: Browser, https_server: Server, browser_name, is_win, is_linux -): + browser: Browser, + https_server: Server, + browser_name: str, + is_win: bool, + is_linux: bool, +) -> None: if browser_name == "webkit" and is_linux: pytest.skip("https://github.com/microsoft/playwright/issues/6759") page = browser.new_page(ignore_https_errors=True) response = page.goto(https_server.EMPTY_PAGE) + assert response response.finished() security_details = response.security_details() assert security_details @@ -60,7 +66,10 @@ def test_response_security_details( page.close() -def test_response_security_details_none_without_https(page: Page, server: Server): +def test_response_security_details_none_without_https( + page: Page, server: Server +) -> None: response = page.goto(server.EMPTY_PAGE) + assert response security_details = response.security_details() assert security_details is None diff --git a/tests/sync/test_page.py b/tests/sync/test_page.py index 9e59256c2..372aecda6 100644 --- a/tests/sync/test_page.py +++ b/tests/sync/test_page.py @@ -16,7 +16,7 @@ from tests.server import Server -def test_input_value(page: Page, server: Server): +def test_input_value(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/input/textarea.html") page.fill("input", "my-text-content") @@ -26,7 +26,7 @@ def test_input_value(page: Page, server: Server): assert page.input_value("input") == "" -def test_drag_and_drop_helper_method(page: Page, server: Server): +def test_drag_and_drop_helper_method(page: Page, server: Server) -> None: page.goto(server.PREFIX + "/drag-n-drop.html") page.drag_and_drop("#source", "#target") assert ( @@ -37,7 +37,7 @@ def test_drag_and_drop_helper_method(page: Page, server: Server): ) -def test_should_check_box_using_set_checked(page: Page): +def test_should_check_box_using_set_checked(page: Page) -> None: page.set_content("``") page.set_checked("input", True) assert page.evaluate("checkbox.checked") is True @@ -45,7 +45,7 @@ def test_should_check_box_using_set_checked(page: Page): assert page.evaluate("checkbox.checked") is False -def test_should_set_bodysize_and_headersize(page: Page, server: Server): +def test_should_set_bodysize_and_headersize(page: Page, server: Server) -> None: page.goto(server.EMPTY_PAGE) with page.expect_event("request") as req_info: page.evaluate( @@ -57,7 +57,7 @@ def test_should_set_bodysize_and_headersize(page: Page, server: Server): assert req.sizes["requestHeadersSize"] >= 300 -def test_should_set_bodysize_to_0(page: Page, server: Server): +def test_should_set_bodysize_to_0(page: Page, server: Server) -> None: page.goto(server.EMPTY_PAGE) with page.expect_event("request") as req_info: page.evaluate("() => fetch('./get').then(r => r.text())") diff --git a/tests/sync/test_pdf.py b/tests/sync/test_pdf.py index b93de201d..af9217ea7 100644 --- a/tests/sync/test_pdf.py +++ b/tests/sync/test_pdf.py @@ -21,13 +21,13 @@ @pytest.mark.only_browser("chromium") -def test_should_be_able_to_save_pdf_file(page: Page, server, tmpdir: Path): +def test_should_be_able_to_save_pdf_file(page: Page, tmpdir: Path) -> None: output_file = tmpdir / "foo.png" page.pdf(path=str(output_file)) assert os.path.getsize(output_file) > 0 @pytest.mark.only_browser("chromium") -def test_should_be_able_capture_pdf_without_path(page: Page): +def test_should_be_able_capture_pdf_without_path(page: Page) -> None: buffer = page.pdf() assert buffer diff --git a/tests/sync/test_resource_timing.py b/tests/sync/test_resource_timing.py index 00d8de4ab..a68143a6e 100644 --- a/tests/sync/test_resource_timing.py +++ b/tests/sync/test_resource_timing.py @@ -15,8 +15,11 @@ import pytest from flaky import flaky +from playwright.sync_api import Browser, Page +from tests.server import Server -def test_should_work(page, server, is_webkit, is_mac): + +def test_should_work(page: Page, server: Server, is_webkit: bool, is_mac: bool) -> None: if is_webkit and is_mac: pytest.skip() with page.expect_event("requestfinished") as request_info: @@ -35,7 +38,9 @@ def test_should_work(page, server, is_webkit, is_mac): @flaky -def test_should_work_for_subresource(page, server, is_win, is_mac, is_webkit): +def test_should_work_for_subresource( + page: Page, server: Server, is_win: bool, is_mac: bool, is_webkit: bool +) -> None: if is_webkit and is_mac: pytest.skip() requests = [] @@ -63,7 +68,9 @@ def test_should_work_for_subresource(page, server, is_win, is_mac, is_webkit): assert timing["responseEnd"] < 10000 -def test_should_work_for_ssl(browser, https_server, is_mac, is_webkit): +def test_should_work_for_ssl( + browser: Browser, https_server: Server, is_mac: bool, is_webkit: bool +) -> None: if is_webkit and is_mac: pytest.skip() page = browser.new_page(ignore_https_errors=True) @@ -86,7 +93,7 @@ def test_should_work_for_ssl(browser, https_server, is_mac, is_webkit): @pytest.mark.skip_browser("webkit") # In WebKit, redirects don"t carry the timing info -def test_should_work_for_redirect(page, server): +def test_should_work_for_redirect(page: Page, server: Server) -> None: server.set_redirect("/foo.html", "/empty.html") responses = [] page.on("response", lambda response: responses.append(response)) diff --git a/tests/sync/test_sync.py b/tests/sync/test_sync.py index 175a34b65..ef2304507 100644 --- a/tests/sync/test_sync.py +++ b/tests/sync/test_sync.py @@ -25,26 +25,28 @@ TimeoutError, sync_playwright, ) +from tests.server import Server -def test_sync_query_selector(page): +def test_sync_query_selector(page: Page) -> None: page.set_content( """

Bar

""" ) - assert ( - page.query_selector("#foo").inner_text() - == page.query_selector("h1").inner_text() - ) + e1 = page.query_selector("#foo") + assert e1 + e2 = page.query_selector("h1") + assert e2 + assert e1.inner_text() == e2.inner_text() -def test_page_repr(page): +def test_page_repr(page: Page) -> None: page.goto("https://example.com") assert repr(page) == f"" -def test_frame_repr(page: Page): +def test_frame_repr(page: Page) -> None: page.goto("https://example.com") assert ( repr(page.main_frame) @@ -52,18 +54,18 @@ def test_frame_repr(page: Page): ) -def test_browser_context_repr(context: BrowserContext): +def test_browser_context_repr(context: BrowserContext) -> None: assert repr(context) == f"" -def test_browser_repr(browser: Browser): +def test_browser_repr(browser: Browser) -> None: assert ( repr(browser) == f"" ) -def test_browser_type_repr(browser: Browser): +def test_browser_type_repr(browser: Browser) -> None: browser_type = browser._impl_obj._browser_type assert ( repr(browser_type) @@ -71,8 +73,8 @@ def test_browser_type_repr(browser: Browser): ) -def test_dialog_repr(page: Page, server): - def on_dialog(dialog: Dialog): +def test_dialog_repr(page: Page) -> None: + def on_dialog(dialog: Dialog) -> None: dialog.accept() assert ( repr(dialog) @@ -83,7 +85,7 @@ def on_dialog(dialog: Dialog): page.evaluate("alert('yo')") -def test_console_repr(page: Page, server): +def test_console_repr(page: Page) -> None: messages = [] page.on("console", lambda m: messages.append(m)) page.evaluate('() => console.log("Hello world")') @@ -91,7 +93,7 @@ def test_console_repr(page: Page, server): assert repr(message) == f"" -def test_sync_click(page): +def test_sync_click(page: Page) -> None: page.set_content( """ @@ -101,7 +103,7 @@ def test_sync_click(page): assert page.evaluate("()=>window.clicked") -def test_sync_nested_query_selector(page): +def test_sync_nested_query_selector(page: Page) -> None: page.set_content( """
@@ -114,12 +116,15 @@ def test_sync_nested_query_selector(page): """ ) e1 = page.query_selector("#one") + assert e1 e2 = e1.query_selector(".two") + assert e2 e3 = e2.query_selector("label") + assert e3 assert e3.inner_text() == "MyValue" -def test_sync_handle_multiple_pages(context): +def test_sync_handle_multiple_pages(context: BrowserContext) -> None: page1 = context.new_page() page2 = context.new_page() assert len(context.pages) == 2 @@ -136,27 +141,27 @@ def test_sync_handle_multiple_pages(context): page.content() -def test_sync_wait_for_event(page: Page, server): +def test_sync_wait_for_event(page: Page, server: Server) -> None: with page.expect_event("popup", timeout=10000) as popup: page.evaluate("(url) => window.open(url)", server.EMPTY_PAGE) assert popup.value -def test_sync_wait_for_event_raise(page): +def test_sync_wait_for_event_raise(page: Page) -> None: with pytest.raises(Error): with page.expect_event("popup", timeout=500) as popup: assert False assert popup.value is None -def test_sync_make_existing_page_sync(page): +def test_sync_make_existing_page_sync(page: Page) -> None: page = page assert page.evaluate("() => ({'playwright': true})") == {"playwright": True} page.set_content("

myElement

") page.wait_for_selector("text=myElement") -def test_sync_network_events(page, server): +def test_sync_network_events(page: Page, server: Server) -> None: server.set_route( "/hello-world", lambda request: ( @@ -182,7 +187,7 @@ def test_sync_network_events(page, server): ] -def test_console_should_work(page, browser_name): +def test_console_should_work(page: Page, browser_name: str) -> None: messages = [] page.once("console", lambda m: messages.append(m)) page.evaluate('() => console.log("hello", 5, {foo: "bar"})'), @@ -200,7 +205,7 @@ def test_console_should_work(page, browser_name): assert message.args[2].json_value() == {"foo": "bar"} -def test_sync_download(browser: Browser, server): +def test_sync_download(browser: Browser, server: Server) -> None: server.set_route( "/downloadWithFilename", lambda request: ( @@ -224,7 +229,7 @@ def test_sync_download(browser: Browser, server): page.close() -def test_sync_workers_page_workers(page: Page, server): +def test_sync_workers_page_workers(page: Page, server: Server) -> None: with page.expect_event("worker") as event_worker: page.goto(server.PREFIX + "/worker/worker.html") assert event_worker.value @@ -237,7 +242,7 @@ def test_sync_workers_page_workers(page: Page, server): assert len(page.workers) == 0 -def test_sync_playwright_multiple_times(): +def test_sync_playwright_multiple_times() -> None: with pytest.raises(Error) as exc: with sync_playwright() as pw: assert pw.chromium @@ -247,14 +252,14 @@ def test_sync_playwright_multiple_times(): ) -def test_sync_set_default_timeout(page): +def test_sync_set_default_timeout(page: Page) -> None: page.set_default_timeout(1) with pytest.raises(TimeoutError) as exc: page.wait_for_function("false") assert "Timeout 1ms exceeded." in exc.value.message -def test_close_should_reject_all_promises(context): +def test_close_should_reject_all_promises(context: BrowserContext) -> None: new_page = context.new_page() with pytest.raises(Error) as exc_info: new_page._gather( @@ -264,7 +269,7 @@ def test_close_should_reject_all_promises(context): assert "Target closed" in exc_info.value.message -def test_expect_response_should_work(page: Page, server): +def test_expect_response_should_work(page: Page, server: Server) -> None: with page.expect_response("**/*") as resp: page.goto(server.EMPTY_PAGE) assert resp.value diff --git a/tests/sync/test_tap.py b/tests/sync/test_tap.py index 560d3be96..762698b04 100644 --- a/tests/sync/test_tap.py +++ b/tests/sync/test_tap.py @@ -12,19 +12,21 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Generator, Optional + import pytest -from playwright.sync_api import ElementHandle, JSHandle +from playwright.sync_api import Browser, BrowserContext, ElementHandle, JSHandle, Page @pytest.fixture -def context(browser): +def context(browser: Browser) -> Generator[BrowserContext, None, None]: context = browser.new_context(has_touch=True) yield context context.close() -def test_should_send_all_of_the_correct_events(page): +def test_should_send_all_of_the_correct_events(page: Page) -> None: page.set_content( """
a
@@ -52,7 +54,7 @@ def test_should_send_all_of_the_correct_events(page): ] -def test_should_not_send_mouse_events_touchstart_is_canceled(page): +def test_should_not_send_mouse_events_touchstart_is_canceled(page: Page) -> None: page.set_content("hello world") page.evaluate( """() => { @@ -74,7 +76,7 @@ def test_should_not_send_mouse_events_touchstart_is_canceled(page): ] -def test_should_not_send_mouse_events_touchend_is_canceled(page): +def test_should_not_send_mouse_events_touchend_is_canceled(page: Page) -> None: page.set_content("hello world") page.evaluate( """() => { @@ -96,7 +98,8 @@ def test_should_not_send_mouse_events_touchend_is_canceled(page): ] -def track_events(target: ElementHandle) -> JSHandle: +def track_events(target: Optional[ElementHandle]) -> JSHandle: + assert target return target.evaluate_handle( """target => { const events = []; diff --git a/tests/sync/test_tracing.py b/tests/sync/test_tracing.py index 5adf6e679..2cce1548d 100644 --- a/tests/sync/test_tracing.py +++ b/tests/sync/test_tracing.py @@ -14,8 +14,13 @@ from pathlib import Path +from playwright.sync_api import Browser +from tests.server import Server -def test_browser_context_output_trace(browser, server, tmp_path): + +def test_browser_context_output_trace( + browser: Browser, server: Server, tmp_path: Path +) -> None: context = browser.new_context() context.tracing.start(screenshots=True, snapshots=True) page = context.new_page() diff --git a/tests/sync/test_video.py b/tests/sync/test_video.py index f354e92f5..1a2d3ba41 100644 --- a/tests/sync/test_video.py +++ b/tests/sync/test_video.py @@ -13,34 +13,47 @@ # limitations under the License. import os +from pathlib import Path +from typing import Dict +from playwright.sync_api import Browser, BrowserType +from tests.server import Server -def test_should_expose_video_path(browser, tmpdir, server): + +def test_should_expose_video_path( + browser: Browser, tmpdir: Path, server: Server +) -> None: page = browser.new_page( record_video_dir=tmpdir, record_video_size={"width": 100, "height": 200} ) page.goto(server.PREFIX + "/grid.html") - path = page.video.path() + video = page.video + assert video + path = video.path() assert repr(page.video) == f"