Skip to content

Commit

Permalink
Merge pull request ScreenPyHQ#9 from ScreenPyHQ/add-attribute-question
Browse files Browse the repository at this point in the history
Add Attribute Question.
  • Loading branch information
perrygoy authored Feb 21, 2024
2 parents e900d5d + 079df43 commit d210a56
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 49 deletions.
2 changes: 2 additions & 0 deletions screenpy_playwright/questions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
"""Questions that can be asked using the Abilities in ScreenPy: Playwright."""

from .attribute import Attribute
from .number import Number
from .text import Text

__all__ = [
"Attribute",
"Number",
"Text",
]
69 changes: 69 additions & 0 deletions screenpy_playwright/questions/attribute.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Get the value of an HTML attribute on a Target."""

from __future__ import annotations

from typing import TYPE_CHECKING

from screenpy import UnableToAnswer

if TYPE_CHECKING:
from screenpy import Actor
from typing_extensions import Self

from ..target import Target


class Attribute:
"""Ask about the value of an HTML attribute on a Target.
Abilities Required:
:class:`~screenpy_playwright.abilities.BrowseTheWebSynchronously`
Examples:
the_actor.should(
See.the(
Attribute("aria-label").of_the(LOGIN_LINK),
ContainsTheText("Log in to the application.")
)
)
the_actor.attempts_to(
MakeNote.of_the(Attribute("href").of_the(LOGIN_LINK).as_(
"login link"
),
)
"""

target: Target | None

def __init__(self, attribute: str) -> None:
self.attribute = attribute
self.target = None

def describe(self) -> str:
"""Describe the Question in present tense."""
return f'The "{self.attribute}" attribute of the {self.target}.'

def of_the(self, target: Target) -> Self:
"""Supply the Target to get the attribute from.
Args:
target: the Target to get the attribute from.
"""
self.target = target
return self

def answered_by(self, the_actor: Actor) -> str | None:
"""Ask the Actor to get the value of the attribute.
Args:
the_actor: the Actor who will answer this Question.
Returns:
str | None: the value of the attribute.
"""
if self.target is None:
msg = "No Target was provided! Supply one with .of_the()."
raise UnableToAnswer(msg)

return self.target.found_by(the_actor).get_attribute(self.attribute)
2 changes: 1 addition & 1 deletion screenpy_playwright/questions/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def of(target: Target) -> Number:
return Number(target)

def describe(self) -> str:
"""Describe the Question.
"""Describe the Question in the present tense.
Returns:
A description of this Question.
Expand Down
2 changes: 1 addition & 1 deletion screenpy_playwright/questions/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def of_the(target: Target) -> Text:
return Text(target)

def describe(self) -> str:
"""Describe the Question.
"""Describe the Question in the present tense.
Returns:
A description of this Question.
Expand Down
7 changes: 4 additions & 3 deletions screenpy_playwright/target.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
if TYPE_CHECKING:
from playwright.sync_api import FrameLocator, Locator, Page
from screenpy import Actor
from typing_extensions import Self


class Target:
Expand All @@ -32,10 +33,10 @@ class Target:
frame_path: list[str]
_description: str | None

@staticmethod
def the(name: str) -> Target:
@classmethod
def the(cls: type[Self], name: str) -> Self:
"""Provide a human-readable description of the target."""
return Target(name)
return cls(name)

def located_by(self, locator: str) -> Target:
"""Provide the Playwright locator which describes the element."""
Expand Down
31 changes: 14 additions & 17 deletions tests/test_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,50 +13,47 @@
Visit,
)

from .useful_mocks import get_mocked_target_and_element
from .useful_mocks import get_mock_target_class, get_mocked_target_and_locator

FakeTarget = get_mock_target_class()
TARGET = FakeTarget()


class TestClick:
def test_can_be_instantiated(self) -> None:
target, _ = get_mocked_target_and_element()

c1 = Click(target)
c2 = Click.on_the(target)
c1 = Click(TARGET)
c2 = Click.on_the(TARGET)

assert isinstance(c1, Click)
assert isinstance(c2, Click)

def test_implements_protocol(self) -> None:
target, _ = get_mocked_target_and_element()

c = Click(target)
c = Click(TARGET)

assert isinstance(c, Describable)
assert isinstance(c, Performable)

def test_describe(self) -> None:
target, _ = get_mocked_target_and_element()
target = FakeTarget()
target._description = "The Holy Hand Grenade"

assert Click(target).describe() == f"Click on the {target}."

def test_perform_click(self, Tester: Actor) -> None:
target, element = get_mocked_target_and_element()
target, locator = get_mocked_target_and_locator()

Click.on_the(target).perform_as(Tester)

target.found_by.assert_called_once_with(Tester)
element.click.assert_called_once()
locator.click.assert_called_once()


class TestEnter:
def test_can_be_instantiated(self) -> None:
target, _ = get_mocked_target_and_element()

e1 = Enter("")
e2 = Enter.the_text("")
e3 = Enter.the_secret("")
e4 = Enter.the_text("").into_the(target)
e4 = Enter.the_text("").into_the(TARGET)

assert isinstance(e1, Enter)
assert isinstance(e2, Enter)
Expand All @@ -70,7 +67,7 @@ def test_implements_protocol(self) -> None:
assert isinstance(e, Performable)

def test_describe(self) -> None:
target, _ = get_mocked_target_and_element()
target = FakeTarget()
target._description = "Sir Robin ran away away, brave brave Sir Robin!"
text = "Sir Robin ran away!"

Expand All @@ -89,13 +86,13 @@ def test_complains_for_no_target(self, Tester: Actor) -> None:
Enter.the_text("").perform_as(Tester)

def test_perform_enter(self, Tester: Actor) -> None:
target, element = get_mocked_target_and_element()
target, locator = get_mocked_target_and_locator()
text = "I wanna be, the very best."

Enter.the_text(text).into_the(target).perform_as(Tester)

target.found_by.assert_called_once_with(Tester)
element.fill.assert_called_once_with(text)
locator.fill.assert_called_once_with(text)


class TestSaveScreenshot:
Expand Down
2 changes: 2 additions & 0 deletions tests/test_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

def test_screenpy_playwright() -> None:
expected = [
"Attribute",
"BrowseTheWebSynchronously",
"Click",
"Enter",
Expand Down Expand Up @@ -38,6 +39,7 @@ def test_actions() -> None:

def test_questions() -> None:
expected = [
"Attribute",
"Number",
"Text",
]
Expand Down
76 changes: 55 additions & 21 deletions tests/test_questions.py
Original file line number Diff line number Diff line change
@@ -1,65 +1,99 @@
from screenpy import Actor, Answerable, Describable
"""Tests for the Questions an Actor can ask using ScreenPy Playwright."""

from screenpy_playwright import Number, Text
from unittest import mock

from .useful_mocks import get_mocked_target_and_element
import pytest
from screenpy import Actor, Answerable, Describable, UnableToAnswer

from screenpy_playwright import Attribute, BrowseTheWebSynchronously, Number, Text

class TestNumber:
from .useful_mocks import get_mock_target_class, get_mocked_target_and_locator

FakeTarget = get_mock_target_class()
TARGET = FakeTarget()


class TestAttribute:
def test_can_be_instantiated(self) -> None:
target, _ = get_mocked_target_and_element()
a1 = Attribute("")
a2 = Attribute("").of_the(TARGET)

assert isinstance(a1, Attribute)
assert isinstance(a2, Attribute)

def test_implements_protocol(self) -> None:
a = Attribute("")

assert isinstance(a, Answerable)
assert isinstance(a, Describable)

def test_raises_error_if_no_target(self, Tester: Actor) -> None:
with pytest.raises(UnableToAnswer):
Attribute("").answered_by(Tester)

def test_ask_for_attribute(self, Tester: Actor) -> None:
attr = "foo"
value = "bar"
target, locator = get_mocked_target_and_locator()
locator.get_attribute.return_value = value
mocked_btws = Tester.ability_to(BrowseTheWebSynchronously)
mocked_btws.current_page = mock.Mock()

n = Number.of(target)
assert Attribute(attr).of_the(target).answered_by(Tester) == value
target.found_by.assert_called_once_with(Tester)
locator.get_attribute.assert_called_once_with(attr)

def test_describe(self) -> None:
assert Attribute("foo").describe() == 'The "foo" attribute of the None.'


class TestNumber:
def test_can_be_instantiated(self) -> None:
n = Number.of(TARGET)

assert isinstance(n, Number)

def test_implements_protocol(self) -> None:
target, _ = get_mocked_target_and_element()

n = Number(target)
n = Number(TARGET)

assert isinstance(n, Answerable)
assert isinstance(n, Describable)

def test_describe(self) -> None:
target, _ = get_mocked_target_and_element()
target = FakeTarget()
target._description = "Somebody once told me"

assert Number.of(target).describe() == f"The number of {target}."

def test_ask_number(self, Tester: Actor) -> None:
target, element = get_mocked_target_and_element()
target, locator = get_mocked_target_and_locator()
num_elements = 10
element.count.return_value = num_elements
locator.count.return_value = num_elements

assert Number.of(target).answered_by(Tester) == num_elements


class TestText:
def test_can_be_instantiated(self) -> None:
target, _ = get_mocked_target_and_element()

t = Text.of_the(target)
t = Text.of_the(TARGET)

assert isinstance(t, Text)

def test_implements_protocol(self) -> None:
target, _ = get_mocked_target_and_element()

t = Text(target)
t = Text(TARGET)

assert isinstance(t, Answerable)
assert isinstance(t, Describable)

def test_describe(self) -> None:
target, _ = get_mocked_target_and_element()
target = FakeTarget()
target._description = "the world is gonna roll me"

assert Text.of_the(target).describe() == f"The text from the {target}."

def test_ask_text(self, Tester: Actor) -> None:
target, element = get_mocked_target_and_element()
target, locator = get_mocked_target_and_locator()
words = "Number 1, the larch."
element.text_content.return_value = words
locator.text_content.return_value = words

assert Text.of_the(target).answered_by(Tester) == words
32 changes: 26 additions & 6 deletions tests/useful_mocks.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,39 @@
from __future__ import annotations

from typing import TYPE_CHECKING, cast
from unittest import mock

from playwright.sync_api import Browser, Locator, Playwright

from screenpy_playwright import Target
from screenpy_playwright import BrowseTheWebSynchronously, Target

if TYPE_CHECKING:
from screenpy import Actor

def get_mocked_target_and_element() -> tuple[mock.Mock, mock.Mock]:

def get_mocked_locator() -> mock.Mock:
return mock.create_autospec(Locator, instance=True)


def get_mock_target_class() -> mock.Mock:
class FakeTarget(Target):
def __new__(cls, *args: object, **kwargs: object) -> FakeTarget: # noqa: ARG003
return mock.create_autospec(FakeTarget, instance=True)

return cast(mock.Mock, FakeTarget)


def get_mocked_target_and_locator() -> tuple[mock.Mock, mock.Mock]:
"""Get a mocked target which finds a mocked element."""
target = mock.Mock(spec=Target)
element = mock.Mock(spec=Locator)
target.found_by.return_value = element
target = get_mock_target_class().the("test object").located_by("test locator")
locator = get_mocked_locator()
target.found_by.return_value = locator

return target, locator


return target, element
def get_mocked_browser(actor: Actor) -> mock.Mock:
return cast(mock.Mock, actor.ability_to(BrowseTheWebSynchronously).browser)


def get_mocked_playwright_and_browser() -> tuple[mock.Mock, mock.Mock]:
Expand Down

0 comments on commit d210a56

Please sign in to comment.