diff --git a/pyproject.toml b/pyproject.toml index 952f526..3121fb2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,6 +123,9 @@ split-on-trailing-comma = false "PLR", # likewise using specific numbers and strings in tests. ] "__version__.py" = ["D"] +"*.pyi" = [ + "PLR0913", # it's not our fault they have too many parameters! +] ################################################################################ diff --git a/screenpy_playwright/target.py b/screenpy_playwright/target.py index 196c219..50ea6b7 100644 --- a/screenpy_playwright/target.py +++ b/screenpy_playwright/target.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Pattern, Tuple, TypedDict, Union -from playwright.sync_api import Locator +from playwright.sync_api import FrameLocator, Locator from .abilities import BrowseTheWebSynchronously from .exceptions import TargetingError @@ -99,7 +99,7 @@ def __repr__(self) -> str: __str__ = __repr__ -class Target(Locator): +class Target(Locator, FrameLocator): """A described element on a webpage. Uses Playwright's Locator API to describe an element on a webpage, with a @@ -148,12 +148,16 @@ def in_frame(self, frame_locator: str) -> Target: def __getattribute__(self, name: str) -> _Manipulation: """Convert a Playwright Locator strategy into a Manipulation.""" - if not name.startswith("_") and hasattr(Locator, name): - attr = getattr(Locator, name) + is_private = name.startswith("_") + superclass_has_attr = hasattr(Locator, name) or hasattr(FrameLocator, name) + if not is_private and superclass_has_attr: + attr = getattr(Locator, name) or getattr(FrameLocator, name) is_property = type(attr) is property r_type = getattr(attr, "__annotations__", {}).get("return") + is_right_type = r_type is Locator or r_type is FrameLocator + is_right_str = r_type in ["Locator", "FrameLocator"] - if is_property or r_type is Locator or r_type == "Locator": + if is_property or is_right_type or is_right_str: manipulation = _Manipulation(self, name) self.manipulations.append(manipulation) return manipulation @@ -211,9 +215,17 @@ def found_by(self, the_actor: Actor) -> Locator: if manipulation.args is None and manipulation.kwargs is None: locator = getattr(locator, manipulation.name) else: - locator = getattr(locator, manipulation.name)( - *manipulation.args, **manipulation.kwargs - ) + args = [] + for arg in manipulation.args: + arg_to_append = arg + if isinstance(arg, Target): + arg_to_append = arg.found_by(the_actor) + args.append(arg_to_append) + kwargs = { + k: v.found_by(the_actor) if isinstance(v, Target) else v + for k, v in manipulation.kwargs.items() + } + locator = getattr(locator, manipulation.name)(*args, **kwargs) return locator diff --git a/screenpy_playwright/target.pyi b/screenpy_playwright/target.pyi new file mode 100644 index 0000000..434f8c0 --- /dev/null +++ b/screenpy_playwright/target.pyi @@ -0,0 +1,122 @@ +"""Used to overwrite type hints for Locator methods from Playwright. + +Hopefully this is only temporary. -pgoy 2024-JUL-12 +""" + +from collections import UserString +from dataclasses import dataclass +from typing import Pattern, TypedDict + +from playwright.sync_api import FrameLocator, Locator +from screenpy import Actor as Actor +from typing_extensions import NotRequired, Self, Unpack + +from .abilities import BrowseTheWebSynchronously as BrowseTheWebSynchronously +from .exceptions import TargetingError as TargetingError + +_ManipulationArgsType = tuple[str | int | None, ...] + +class _ManipulationKwargsType(TypedDict): + has_text: NotRequired[str | Pattern[str] | None] + has_not_text: NotRequired[str | Pattern[str] | None] + has: NotRequired[Locator | None] + has_not: NotRequired[Locator | None] + exact: NotRequired[bool | None] + checked: NotRequired[bool | None] + disabled: NotRequired[bool | None] + expanded: NotRequired[bool | None] + include_hidden: NotRequired[bool | None] + level: NotRequired[int | None] + name: NotRequired[str | Pattern[str] | None] + pressed: NotRequired[bool | None] + selected: NotRequired[bool | None] + +@dataclass +class _Manipulation(UserString): + def __hash__(self) -> int: ... + def __eq__(self, other: object) -> bool: ... + def __getattr__(self, name: str) -> Target | _Manipulation: ... + def __call__( + self, + *args: Unpack[_ManipulationArgsType], + **kwargs: Unpack[_ManipulationKwargsType], + ) -> Target: ... + def get_locator(self) -> str: ... + def __init__( + self, + target: Target, + name: str, + args: _ManipulationArgsType | None = ..., + kwargs: _ManipulationKwargsType | None = ..., + ) -> None: ... + +class Target(FrameLocator, Locator): + manipulations: list[_Manipulation] + first: Target + last: Target + owner: Target + content_frame: Target + @classmethod + def the(cls, name: str) -> Self: ... + def located_by(self, locator: str) -> Target: ... + def in_frame(self, frame_locator: str) -> Target: ... + def __getattribute__(self, name: str) -> _Manipulation: ... + @property + def target_name(self) -> str: ... + @target_name.setter + def target_name(self, value: str) -> None: ... + def found_by(self, the_actor: Actor) -> Locator: ... + def __init__(self, name: str | None = None) -> None: ... + + # overridden Locator methods + def and_(self, locator: Locator | Target) -> Target: ... + def filter( + self, + *, + has_text: str | Pattern | None = None, + has_not_text: str | Pattern | None = None, + has: Locator | Target | None = None, + has_not: Locator | Target | None = None, + ) -> Target: ... + def frame_locator(self, selector: str) -> Target: ... + def get_by_alt_text( + self, text: str | Pattern, *, exact: bool | None = None + ) -> Target: ... + def get_by_label( + self, text: str | Pattern, *, exact: bool | None = None + ) -> Target: ... + def get_by_placeholder( + self, text: str | Pattern, *, exact: bool | None = None + ) -> Target: ... + def get_by_role( + self, + role: str, + *, + checked: bool | None = None, + disabled: bool | None = None, + expanded: bool | None = None, + include_hidden: bool | None = None, + level: int | None = None, + name: str | Pattern | None = None, + pressed: bool | None = None, + selected: bool | None = None, + exact: bool | None = None, + ) -> Target: ... + def get_by_test_id(self, test_id: str | Pattern) -> Target: ... + def get_by_text( + self, text: str | Pattern, *, exact: bool | None = None + ) -> Target: ... + def get_by_title( + self, text: str | Pattern, *, exact: bool | None = None + ) -> Target: ... + def locator( + self, + selector: str | Locator, + *, + has_text: str | Pattern | None = None, + has_not_text: str | Pattern | None = None, + has: Locator | Target | None = None, + has_not: Locator | Target | None = None, + ) -> Target: ... + def nth(self, index: int) -> Target: ... + def or_(self, locator: Locator | Target) -> Target: ... diff --git a/tests/test_target.py b/tests/test_target.py index 9d22fc4..3167648 100644 --- a/tests/test_target.py +++ b/tests/test_target.py @@ -57,6 +57,7 @@ def test_can_be_instantiated(self) -> None: t4 = Target().located_by("test") t5 = Target() t6 = Target("test").get_by_label("test", exact=True) + t7 = Target("frame").frame_locator("test").content_frame assert isinstance(t1, Target) assert isinstance(t2, Target) @@ -64,6 +65,7 @@ def test_can_be_instantiated(self) -> None: assert isinstance(t4, Target) assert isinstance(t5, Target) assert isinstance(t6, Target) + assert isinstance(t7, Target) def test_auto_describe(self) -> None: t1 = Target().located_by("#yellow")