-
Notifications
You must be signed in to change notification settings - Fork 2
Target from parent #75
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
Changes from all commits
0786469
75c1761
11cb245
ac99790
39c1c07
6211d82
f035e43
f4fc7e4
36cbb0e
f882834
f8774d7
dd04ed2
3fa59b0
3ebd357
979320f
78b42e5
0d156e3
f72fa71
bd3c361
c2a5cb0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,12 +3,14 @@ Targets | |
| ======= | ||
|
|
||
| The blocking of the screenplay! | ||
| The Target tells the Actors | ||
|
|
||
| The :class:`~screenpy_selenium.Target` tells the | ||
| :external+screenpy:class:`~screenpy.Actor` | ||
| what part of the website | ||
| they are to interact with. | ||
|
|
||
| Stripping away the metaphor, | ||
| the :ref:`target` combines a locator | ||
| the :class:`~screenpy_selenium.Target` combines a locator | ||
| with a human-readable string. | ||
| The human-readable part | ||
| is what gets read out | ||
|
|
@@ -41,7 +43,7 @@ by passing them to Actions:: | |
|
|
||
| from example_test.ui.login_page import ( | ||
| PASSWORD_FIELD, | ||
| SIGN_IN_BUTTON | ||
| SIGN_IN_BUTTON, | ||
| USERNAME_FIELD, | ||
| ) | ||
|
|
||
|
|
@@ -60,9 +62,9 @@ The resulting log: | |
| | Webster enters "[CENSORED]" into the password field. | ||
| | Websert clicks on the "Sign In" button. | ||
|
|
||
| By default the :ref:`target` will use the locator string as a human-readable | ||
| ``target_name`` in the absence of providing one. This can be convenient if your | ||
| locators are self-describing:: | ||
| By default the :class:`~screenpy_selenium.Target` will use the locator string | ||
| as a human-readable ``target_name`` in the absence of providing one. | ||
| This can be convenient if your locators are self-describing:: | ||
|
|
||
| from screenpy_selenium import Target | ||
| from selenium.webdriver.common.by import By | ||
|
|
@@ -81,5 +83,40 @@ The resulting log: | |
|
|
||
| | Webster enters "foo" into the username-field. | ||
| | Webster enters "[CENSORED]" into the password-field. | ||
| | Websert clicks on the sign-in-button. | ||
| | Webster clicks on the sign-in-button. | ||
|
|
||
|
|
||
| Target in Target | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. polish: This won't affect the output, but i've been trying to write these Can you give these another pass and try to follow that convention?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am 100% terrible at figuring out where to break up the lines. I thought I was doing that. Better to just let me know where they should break. |
||
| ---------------- | ||
|
|
||
| A :class:`~screenpy_selenium.Target` (as seen above) | ||
| will typically have Selenium do a search | ||
| for the locator starting at the root of the DOM:: | ||
|
|
||
| # These are equivalent | ||
|
|
||
| web_element = USERNAME_FIELD.found_by(Webster) | ||
|
|
||
| web_element = driver.find_element(*USERNAME_FIELD) | ||
|
bandophahita marked this conversation as resolved.
|
||
|
|
||
| Selenium also has the ability | ||
| to search for a child WebElement | ||
| starting from an already-found parent WebElement. | ||
| :class:`~screenpy_selenium.Target` can do the same | ||
| by utilizing the method :meth:`~screenpy.target.Target.inside`:: | ||
|
|
||
| # These three are equivalent | ||
|
|
||
| elem1 = USERNAME_FIELD.inside(LOGIN_FORM).found_by(Webster) | ||
|
|
||
| form_elem = driver.find_element(*LOGIN_FORM) | ||
| elem2 = form_elem.find_element(*USERNAME_FIELD) | ||
|
|
||
| elem3 = driver.find_element(*LOGIN_FORM).find_element(*USERNAME_FIELD) | ||
|
|
||
|
|
||
| .. note:: | ||
|
|
||
| :meth:`~screenpy.target.Target.inside` does not mutate :class:`~screenpy_selenium.Target`. | ||
| This is done purposefully so the existing `Target` can be reused. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,8 @@ | |
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING | ||
| import copy | ||
| from typing import TYPE_CHECKING, Union | ||
|
|
||
| from selenium.common.exceptions import WebDriverException | ||
| from selenium.webdriver.common.by import By | ||
|
|
@@ -20,9 +21,12 @@ | |
| from collections.abc import Iterator | ||
|
|
||
| from screenpy.actor import Actor | ||
| from selenium.webdriver.remote.webdriver import WebElement | ||
| from selenium.webdriver.remote.webdriver import WebDriver | ||
| from selenium.webdriver.remote.webelement import WebElement | ||
| from typing_extensions import Self | ||
|
|
||
| WebDriverOrWebElement = Union[WebDriver, WebElement] | ||
|
|
||
|
|
||
| class Target: | ||
| """Describe an element with a human-readable string and a locator. | ||
|
|
@@ -41,6 +45,7 @@ class Target: | |
|
|
||
| _description: str | None = None | ||
| locator: tuple[str, str] | None = None | ||
| parent_target: Target | None = None | ||
|
|
||
| @property | ||
| def target_name(self) -> str | None: | ||
|
|
@@ -112,24 +117,53 @@ def get_locator(self) -> tuple[str, str]: | |
|
|
||
| def found_by(self, the_actor: Actor) -> WebElement: | ||
| """Retrieve the |WebElement| as viewed by the Actor.""" | ||
| browser = the_actor.ability_to(BrowseTheWeb).browser | ||
| driver_or_element: WebDriverOrWebElement | ||
|
bandophahita marked this conversation as resolved.
|
||
| if self.parent_target: | ||
| driver_or_element = self.parent_target.found_by(the_actor) | ||
| else: | ||
| driver_or_element = the_actor.ability_to(BrowseTheWeb).browser | ||
|
|
||
| try: | ||
| return browser.find_element(*self) | ||
| return driver_or_element.find_element(*self) | ||
| except WebDriverException as e: | ||
| msg = f"{e} raised while trying to find {self}." | ||
| raise TargetingError(msg) from e | ||
|
|
||
| def all_found_by(self, the_actor: Actor) -> list[WebElement]: | ||
| """Retrieve a list of |WebElement| objects as viewed by the Actor.""" | ||
| browser = the_actor.ability_to(BrowseTheWeb).browser | ||
| driver_or_element: WebDriverOrWebElement | ||
| if self.parent_target: | ||
| driver_or_element = self.parent_target.found_by(the_actor) | ||
| else: | ||
| driver_or_element = the_actor.ability_to(BrowseTheWeb).browser | ||
|
|
||
| try: | ||
| return browser.find_elements(*self) | ||
| return driver_or_element.find_elements(*self) | ||
| except WebDriverException as e: | ||
| msg = f"{e} raised while trying to find {self}." | ||
| raise TargetingError(msg) from e | ||
|
|
||
| def inside(self, parent_target: Target) -> Self: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue: This is not the correct typing for a method that returns a new object. Is there a generic "whatever class i am" return type, so we can support Target subclasses?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe that technically it is correct even when it's not the same instance. I had the same thought as you.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using mypy and class SubTarget(Target):
pass
t1 = Target("t1").located((By.ID, "t1"))
st = SubTarget("s1").located((By.ID, "s1")).inside(t1)
reveal_type(st)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Man, you're totally right! The docs confirm it: https://mypy.readthedocs.io/en/stable/more_types.html#precise-typing-of-alternative-constructors I thought
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (I was going to be very curious about why our |
||
| """Set the containing parent element for this Target. | ||
|
|
||
| Creates a new Target instance. | ||
| """ | ||
| new_target = copy.copy(self) | ||
| new_target.parent_target = parent_target | ||
| return new_target | ||
|
|
||
| def inside_of(self, parent_target: Target) -> Self: | ||
| """Alias for :meth:`~screenpy_selenium.Target.inside`.""" | ||
| return self.inside(parent_target) | ||
|
|
||
| def within(self, parent_target: Target) -> Self: | ||
| """Alias for :meth:`~screenpy_selenium.Target.inside`.""" | ||
| return self.inside(parent_target) | ||
|
|
||
| def __repr__(self) -> str: | ||
| """A Target is represented by its name.""" | ||
| if self.parent_target: | ||
| return f"{self.target_name} in {self.parent_target}" | ||
| return f"{self.target_name}" | ||
|
|
||
| __str__ = __repr__ | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.