Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Attribute Question. #9

Merged
merged 2 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 Locator
from screenpy import Actor
from typing_extensions import Self


class Target:
Expand All @@ -31,10 +32,10 @@ class Target:
locator: str | None
_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 @@ -7,50 +7,47 @@

from screenpy_playwright import BrowseTheWebSynchronously, Click, Enter, 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 @@ -64,7 +61,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 @@ -83,13 +80,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 TestVisit:
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 @@ -36,6 +37,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