diff --git a/examples/script_sample.py b/examples/script_sample.py deleted file mode 100644 index f48500e..0000000 --- a/examples/script_sample.py +++ /dev/null @@ -1,22 +0,0 @@ -""" Make an instance of Pylenium and automate the world! """ - - -from selenium.webdriver.common.keys import Keys -from pylenium import Pylenium - - -search_field = "[name='q']" -images_link = "[href*='tbm=isch']" - - -def main(): - py = Pylenium() - py.visit('https://google.com') - py.get(search_field).type('puppies', Keys.ENTER) - py.get(images_link).click() - print(py.title) - py.quit() - - -if __name__ == '__main__': - main() diff --git a/examples/test_sample.py b/examples/test_sample.py index 330672a..a880092 100644 --- a/examples/test_sample.py +++ b/examples/test_sample.py @@ -1,39 +1,54 @@ -""" Use @pytest.fixtures when writing UI or end-to-end tests. """ +""" Examples will be added to this directory and its files. +However, the best source for info and details is in the +official documentation here: -import pytest -from selenium.webdriver.common.keys import Keys -from pylenium import Pylenium - - -@pytest.fixture -def py_(): - # Code before `yield` is executed Before Each test. - # By default, fixtures are mapped to each test. - # You can change the scope by using: - # `@pytest.fixture(scope=SCOPE)` where SCOPE is 'class', 'module' or 'session' - _py = Pylenium() - - # Then `yield` or `return` the instance of Pylenium - yield _py - - # Code after `yield` is executed After Each test. - # In this case, once the test is complete, quit the driver. - # This will be executed whether the test passed or failed. - _py.quit() +https://elsnoman.gitbook.io/pylenium +You can also contact the author, @CarlosKidman +on Twitter or LinkedIn. +""" -def test_using_fixture(py_): - """ You pass in the name of the fixture as seen on the line above. - - This is the RECOMMENDED option when writing automated tests. - To find more info on PyTest and Fixtures, go to their docs: - - https://docs.pytest.org/en/latest/fixture.html - - * You can pass in any number of fixtures and fixtures can call other fixtures! - * You can store fixtures locally in test files or in conftest.py global files - """ - py_.visit('https://google.com') - py_.get('[name="q"]').type('puppies', Keys.ENTER) - assert 'puppies' in py_.title +# You can mix Selenium into some Pylenium commands +from selenium.webdriver.common.by import By +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.support import expected_conditions as ec + + +# pass in the `py` fixture into your test function +# this _is_ Pylenium! +def test_pylenium_basics(py): + # Use Cypress-like commands like `.visit()` + py.visit('https://google.com') + # `.get()` uses CSS to locate a single element + py.get('[name="q"]').type('puppies', Keys.ENTER) + # `assert` followed by a boolean expression + assert 'puppies' in py.title + + +def test_access_selenium(py): + py.visit('https://google.com') + # access the wrapped WebDriver with `py.webdriver` + search_field = py.webdriver.find_element_by_css_selector('[name="q"]') + # access the wrapped WebElement with `Element.webelement` + assert py.get('[name"q"]').webelement.is_enabled() + # you can store elements and objects to be used later since + # we don't rely on Promises or chaining in Python + search_field.send_keys('puppies', Keys.ENTER) + assert 'puppies' in py.title + + +def test_chaining_commands(py): + py.visit('https://google.com').get('[name="q"]').type('puppies', Keys.ENTER) + assert 'puppies' in py.title + + +def test_waiting(py): + py.visit('https://google.com') + # wait using expected conditions + # default `.wait()` uses WebDriverWait which returns Selenium's WebElement objects + py.wait().until(ec.visibility_of_element_located((By.CSS_SELECTOR, '[name="q"]'))).send_keys('puppies') + # use_py=True to use a PyleniumWait which returns Pylenium's Element and Elements objects + py.wait(use_py=True).until(lambda _: py.get('[name="q"]')).type(Keys.ENTER) + # wait using lambda function + assert py.wait().until(lambda x: 'puppies' in x.title) diff --git a/pylenium/driver.py b/pylenium/driver.py index 2788356..32c23b2 100644 --- a/pylenium/driver.py +++ b/pylenium/driver.py @@ -2,9 +2,11 @@ import requests from faker import Faker +from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.support.wait import WebDriverWait +from selenium.webdriver.support import expected_conditions as ec from pylenium import webdriver_factory from pylenium.config import PyleniumConfig @@ -14,6 +16,104 @@ from pylenium.wait import PyleniumWait +class PyleniumShould: + def __init__(self, py: 'Pylenium', timeout: int, ignored_exceptions: list = None): + self._py = py + self._wait: PyleniumWait = self._py.wait(timeout=timeout, use_py=True, ignored_exceptions=ignored_exceptions) + + def have_title(self, title: str) -> 'Pylenium': + """ An expectation that the title matches the given title. + + Args: + title: The title to match. + + Returns: + The current instance of Pylenium. + + Raises: + `AssertionError` if the condition is not met within the timeout. + """ + self._py.log.step('.should().have_title()', True) + try: + value = self._wait.until(ec.title_is(title)) + except TimeoutException: + value = False + if value: + return self._py + else: + self._py.log.failed('.should().have_title()') + raise AssertionError(f'Title was not {title}, but was {self._py.title}') + + def contain_title(self, string: str) -> 'Pylenium': + """ An expectation that the title contains the given string. + + Args: + string: The case-sensitive string for the title to contain. + + Returns: + The current instance of Pylenium. + + Raises: + `AssertionError` if the condition is not met within the timeout. + """ + self._py.log.step('.should().contain_title()', True) + try: + value = self._wait.until(ec.title_contains(string)) + except TimeoutException: + value = False + if value: + return self._py + else: + self._py.log.failed('.should().contain_title()') + raise AssertionError(f'Title did not contain {string}, but was {self._py.title}') + + def have_url(self, url: str) -> 'Pylenium': + """ An expectation that the URL matches the given url. + + Args: + url: The url to match. + + Returns: + The current instance of Pylenium. + + Raises: + `AssertionError` if the condition is not met within the timeout. + """ + self._py.log.step('.should().have_url()', True) + try: + value = self._wait.until(ec.url_to_be(url)) + except TimeoutException: + value = False + if value: + return self._py + else: + self._py.log.failed('.should().contain_title()') + raise AssertionError(f'URL was not {url}, but was {self._py.url}') + + def contain_url(self, string: str) -> 'Pylenium': + """ An expectation that the URL contains the given string. + + Args: + string: The case-sensitive string for the url to contain. + + Returns: + The current instance of Pylenium. + + Raises: + `AssertionError` if the condition is not met within the timeout. + """ + self._py.log.step('.should().contain_url()', True) + try: + value = self._wait.until(ec.url_contains(string)) + except TimeoutException: + value = False + if value: + return self._py + else: + self._py.log.failed('.should().contain_url()', True) + raise AssertionError(f'URL did not contain {string}, but was {self._py.url}') + + class Pylenium: """ The Pylenium API. @@ -122,11 +222,12 @@ def contains(self, text: str, timeout: int = 0) -> Element: The first element that is found, even if multiple elements match the query. """ self.log.step(f'py.contains() - Find the element with text: ``{text}``') + locator = (By.XPATH, f'//*[contains(text(), "{text}")]') element = self.wait(timeout).until( - lambda _: self._webdriver.find_element(By.XPATH, f'//*[contains(text(), "{text}")]'), + lambda x: x.find_element(*locator), f'Could not find element with the text ``{text}``' ) - return Element(self, element) + return Element(self, element, locator) def get(self, css: str, timeout: int = 0) -> Element: """ Get the DOM element that matches the `css` selector. @@ -139,11 +240,12 @@ def get(self, css: str, timeout: int = 0) -> Element: The first element that is found, even if multiple elements match the query. """ self.log.step(f'py.get() - Find the element with css: ``{css}``') + by = By.CSS_SELECTOR element = self.wait(timeout).until( - lambda _: self._webdriver.find_element(By.CSS_SELECTOR, css), + lambda x: x.find_element(by, css), f'Could not find element with the CSS ``{css}``' ) - return Element(self, element) + return Element(self, element, locator=(by, css)) def find(self, css: str, at_least_one=True, timeout: int = 0) -> Elements: """ Finds all DOM elements that match the `css` selector. @@ -156,16 +258,17 @@ def find(self, css: str, at_least_one=True, timeout: int = 0) -> Elements: Returns: A list of the found elements. """ + by = By.CSS_SELECTOR if at_least_one: self.log.step(f'py.find() - Find at least one element with css: ``{css}``') elements = self.wait(timeout).until( - lambda _: self.webdriver.find_elements(By.CSS_SELECTOR, css), + lambda x: x.find_elements(by, css), f'Could not find any elements with the CSS ``{css}``' ) else: - self.log.action(f'py.find() - Find elements with css (no wait): ``{css}``') - elements = self.webdriver.find_elements(By.CSS_SELECTOR, css) - return Elements(self, elements) + self.log.step(f'py.find() - Find elements with css (no wait): ``{css}``') + elements = self.webdriver.find_elements(by, css) + return Elements(self, elements, locator=(by, css)) def xpath(self, xpath: str, at_least_one=True, timeout: int = 0) -> Union[Element, Elements]: """ Finds all DOM elements that match the `xpath` selector. @@ -178,22 +281,39 @@ def xpath(self, xpath: str, at_least_one=True, timeout: int = 0) -> Union[Elemen Returns: A list of the found elements. If only one is found, return that as Element. """ + by = By.XPATH if at_least_one: self.log.step(f'py.xpath() - Find at least one element with xpath: ``{xpath}``') elements = self.wait(timeout).until( - lambda _: self.webdriver.find_elements(By.XPATH, xpath), + lambda x: x.find_elements(by, xpath), f'Could not find any elements with the CSS ``{xpath}``' ) else: self.log.step(f'py.xpath() - Find elements with xpath (no wait): ``{xpath}``') - elements = self.webdriver.find_elements(By.CSS_SELECTOR, xpath) + elements = self.webdriver.find_elements(by, xpath) if len(elements) == 1: self.log.info('Only 1 element matched your xpath') - return Element(self, elements[0]) + return Element(self, elements[0], locator=(by, xpath)) self.log.info(f'{len(elements)} elements matched your xpath') - return Elements(self, elements) + return Elements(self, elements, locator=(by, xpath)) + + # EXPECTATIONS # + ################ + + def should(self, timeout: int = 0, ignored_exceptions: list = None) -> PyleniumShould: + """ A collection of expectations for this driver. + + Examples: + py.should().contain_title('QA at the Point') + py.should().have_url('https://qap.dev') + """ + if timeout: + wait_time = timeout + else: + wait_time = self.config.driver.wait_time + return PyleniumShould(self, wait_time, ignored_exceptions) # UTILITIES # ############# diff --git a/pylenium/element.py b/pylenium/element.py index d0f77d3..c8d2642 100644 --- a/pylenium/element.py +++ b/pylenium/element.py @@ -1,17 +1,517 @@ -from typing import List, Union +import time +from typing import List, Union, Tuple, Optional -from selenium.common.exceptions import NoSuchElementException +from selenium.common.exceptions import NoSuchElementException, TimeoutException from selenium.webdriver import ActionChains from selenium.webdriver.common.by import By from selenium.webdriver.remote.webdriver import WebElement from selenium.webdriver.support.select import Select +from selenium.webdriver.support import expected_conditions as ec + + +class ElementWait: + def __init__(self, webelement, timeout: int, ignored_exceptions: list = None): + self._webelement = webelement + self._timeout = 10 if timeout == 0 else timeout + if ignored_exceptions: + self._ignored_exceptions = ignored_exceptions + else: + self._ignored_exceptions = ( + NoSuchElementException + ) + + def until(self, method, message=''): + screen = None + stacktrace = None + + end_time = time.time() + self._timeout + while True: + try: + value = method(self._webelement) + if value: + return value + except self._ignored_exceptions as exc: + screen = getattr(exc, 'screen', None) + stacktrace = getattr(exc, 'stacktrace', None) + time.sleep(0.5) + if time.time() > end_time: + break + raise TimeoutException(message, screen, stacktrace) + + +class ElementShould: + """ Expectations for the current element that is already in the DOM. """ + def __init__(self, py, element: 'Element', timeout: int, ignored_exceptions: list = None): + self._py = py + self._element = element + self._wait = ElementWait(element.webelement, timeout, ignored_exceptions) + + # POSITIVE CONDITIONS # + ####################### + + def be_clickable(self) -> 'Element': + """ An expectation that the element is displayed and enabled so you can click it. + + Returns: + The current element. + + Raises: + `AssertionError` if the condition is not met in the specified amount of time. + """ + self._py.log.step('.should().be_clickable()', True) + try: + value = self._wait.until(lambda e: e.is_displayed() and e.is_enabled()) + except TimeoutException: + value = False + if value: + return self._element + else: + self._py.log.failed('.should().be_clickable()') + raise AssertionError('Element was not clickable') + + def be_checked(self) -> 'Element': + """ An expectation that the element is checked. + + Returns: + The current element. + + Raises: + `AssertionError` if the condition is not met in the specified amount of time. + """ + self._py.log.step('.should().be_checked()', True) + try: + value = self._wait.until(lambda e: self._element.is_checked()) + except TimeoutException: + value = False + if value: + return self._element + else: + self._py.log.failed('.should().be_checked()') + raise AssertionError('Element was not checked') + + def be_disabled(self) -> 'Element': + """ An expectation that the element is disabled. + + Returns: + The current element. + + Raises: + `AssertionError` if the condition is not met in the specified amount of time. + """ + self._py.log.step('.should().be_disabled()', True) + try: + value = self._wait.until(lambda e: not e.is_enabled()) + except TimeoutException: + value = False + if value: + return self._element + else: + self._py.log.failed('.should().be_disabled()') + raise AssertionError('Element was not disabled') + + def be_enabled(self) -> 'Element': + """ An expectation that the element is enabled. + + Returns: + The current element. + + Raises: + `AssertionError` if the condition is not met in the specified amount of time. + """ + self._py.log.step('.should().be_enabled()', True) + try: + value = self._wait.until(lambda e: e.is_enabled()) + except TimeoutException: + value = False + if value: + return self._element + else: + self._py.log.failed('.should().be_enabled()') + raise AssertionError('Element was not enabled') + + def be_focused(self) -> 'Element': + """ An expectation that the element is focused. + + Returns: + The current element. + + Raises: + `AssertionError` if the condition is not met in the specified amount of time. + """ + self._py.log.step('.should().be_focused()', True) + try: + value = self._wait.until(lambda e: e == self._py.webdriver.switch_to.active_element) + except TimeoutException: + value = False + + if value: + return self._element + else: + self._py.log.failed('.should().be_focused()') + raise AssertionError('Element did not have focus') + + def be_hidden(self) -> 'Element': + """ An expectation that the element is not displayed but still in the DOM (aka hidden). + + Returns: + The current element. + + Raises: + `AssertionError` if the condition is not met in the specified amount of time. + """ + self._py.log.step('.should().be_hidden()', True) + try: + value = self._wait.until(lambda e: e and not e.is_displayed()) + except TimeoutException: + value = False + + if value: + return self._element + else: + self._py.log.failed('.should().be_hidden()') + raise AssertionError('Element was not hidden') + + def be_selected(self) -> 'Element': + """ An expectation that the element is selected. + + Returns: + The current element. + + Raises: + `AssertionError` if the condition is not met in the specified amount of time. + """ + self._py.log.step('.should().be_selected()', True) + try: + value = self._wait.until(lambda e: e.is_selected()) + except TimeoutException: + value = False + + if value: + return self._element + else: + self._py.log.failed('.should().be_selected()') + raise AssertionError('Element was not selected') + + def be_visible(self) -> 'Element': + """ An expectation that the element is displayed. + + Returns: + The current element. + + Raises: + `AssertionError` if the condition is not met in the specified amount of time. + """ + self._py.log.step('.should().be_visible()', True) + try: + value = self._wait.until(lambda e: e and e.is_displayed()) + except TimeoutException: + value = False + + if value: + return self._element + else: + self._py.log.failed('.should().be_visible()') + raise AssertionError('Element was not visible') + + def have_attr(self, attr: str, value: str) -> 'Element': + """ An expectation that the element has the given attribute with the given value. + + Args: + attr: The name of the attribute. + value: The value of the attribute. + + Returns: + The current element. + + Raises: + `AssertionError` if the condition is not met in the specified amount of time. + """ + self._py.log.step('.should().have_attr()', True) + try: + val = self._wait.until(lambda e: e.get_attribute(attr) == value) + except TimeoutException: + val = False + + if val: + return self._element + else: + self._py.log.failed('.should().have_attr()') + raise AssertionError(f'Element did not have attribute ``{attr}`` with the value of ``{value}``') + + def have_class(self, class_name: str) -> 'Element': + """ An expectation that the element has the given className. + + Args: + class_name: The `.className` of the element + + Returns: + The current element. + + Raises: + `AssertionError` if the condition is not met in the specified amount of time. + """ + self._py.log.step('.should().have_class()', True) + try: + val = self._wait.until(lambda e: e.get_attribute('class') == class_name) + except TimeoutException: + val = False + + if val: + return self._element + else: + self._py.log.failed('.should().have_class()') + raise AssertionError(f'Element did not have className ``{class_name}``') + + def have_prop(self, prop: str, value: str) -> 'Element': + """ An expectation that the element has the given property with the given value. + + Args: + prop: The name of the property. + value: The value of the property. + + Returns: + The current element. + + Raises: + `AssertionError` if the condition is not met in the specified amount of time. + """ + self._py.log.step('.should().have_prop()', True) + try: + val = self._wait.until(lambda e: e.get_property(property) == value) + except TimeoutException: + val = False + + if val: + return self._element + else: + self._py.log.failed('.should().have_prop()') + raise AssertionError(f'Element did not have property ``{prop}`` with the value of ``{value}``') + + def have_text(self, text, case_sensitive=True) -> 'Element': + """ An expectation that the element has the given text. + + Args: + text: The exact text to match. + case_sensitive: False if you want to ignore casing and leading/trailing spaces. + + Returns: + The current element. + + Raises: + `AssertionError` if the condition is not met in the specified amount of time. + """ + self._py.log.step('.should().have_text()', True) + try: + if case_sensitive: + value = self._wait.until(lambda e: e.text == text) + else: + value = self._wait.until(lambda e: e.text.strip().lower() == text.lower()) + except TimeoutException: + value = False + + if value: + return self._element + else: + self._py.log.failed('.should().have_text()') + raise AssertionError(f'Element did not have text matching ``{text}``') + + def contain_text(self, text, case_sensitive=True) -> 'Element': + """ An expectation that the element contains the given text. + + Args: + text: The text that the element should contain. + case_sensitive: False if you want to ignore casing and leading/trailing spaces. + + Returns: + The current element. + + Raises: + `AssertionError` if the condition is not met in the specified amount of time. + """ + self._py.log.step('.should().contain_text()', True) + try: + if case_sensitive: + value = self._wait.until(lambda e: text in e.text) + else: + value = self._wait.until(lambda e: text.lower() in e.text.strip().lower()) + except TimeoutException: + value = False + + if value: + return self._element + else: + self._py.log.failed('.should().contain_text()') + raise AssertionError(f'Element did not contain the text ``{text}``') + + def have_value(self, value) -> 'Element': + """ An expectation that the element has the given value. + + Args: + value: The exact value to match. Pass `None` if you expect the element not to have the value attribute. + + Returns: + The current element. + + Raises: + `AssertionError` if the condition is not met in the specified amount of time. + + Examples: + * An element with no `value` attribute will yield `None` + * An element with a `value` attribute with no value will yield an empty string `""` + * An element with a `value` attribute with a value will yield the value + """ + self._py.log.step('.should().have_value()', True) + try: + val = self._wait.until(lambda e: e.get_attribute('value') == value) + except TimeoutException: + val = False + + if val: + return self._element + else: + self._py.log.failed('.should().have_value()') + raise AssertionError(f'Element did not have value matching ``{value}``') + + # NEGATIVE CONDITIONS # + ####################### + + def not_be_focused(self) -> 'Element': + """ An expectation that the element is not focused. + + Returns: + The current element. + + Raises: + `AssertionError` if the condition is not met in the specified amount of time. + """ + self._py.log.step('.should().not_be_focused()', True) + try: + value = self._wait.until(lambda e: e != self._py.webdriver.switch_to.active_element) + except TimeoutException: + value = False + + if value: + return self._element + else: + self._py.log.failed('.should().not_be_focused()') + raise AssertionError('Element had focus') + + def not_exist(self) -> 'Pylenium': + """ An expectation that the element no longer exists in the DOM. + + Returns: + The current element. + + Raises: + `AssertionError` if the condition is not met in the specified amount of time. + + Examples: + # wait for a loading spinner to appear and then disappear once the load is complete + py.get(#spinner).should().not_exist() + """ + self._py.log.step('.should().not_exist()', True) + try: + value = self._wait.until(ec.invisibility_of_element(self._element.webelement)) + except TimeoutException: + value = False + if value: + return self._py + else: + self._py.log.failed('.should().not_exist()') + raise AssertionError('Element was still visible or still in the DOM') + + def not_have_attr(self, attr: str, value: str) -> 'Element': + """ An expectation that the element does not have the given attribute with the given value. + + Either the attribute does not exist on the element or the value does not match the given value. + + Args: + attr: The name of the attribute. + value: The value of the attribute. + + Returns: + The current element. + + Raises: + `AssertionError` if the condition is not met in the specified amount of time. + """ + self._py.log.step('.should().not_have_attr()', True) + try: + val = self._wait.until(lambda e: e.get_attribute(attr) is None or e.get_attribute(attr) != value) + except TimeoutException: + val = False + + if val: + return self._element + else: + self._py.log.failed('.should().not_have_attr()') + raise AssertionError(f'Element still had attribute ``{attr}`` with the value of ``{value}``') + + def not_have_value(self, value) -> 'Element': + """ An expectation that the element does not have the given value. + + Args: + value: The exact value not to match. Pass `None` if you expect the element to have the value attribute. + + Returns: + The current element. + + Raises: + `AssertionError` if the condition is not met in the specified amount of time. + + Examples: + * An element with no `value` attribute will yield `None` + * An element with a `value` attribute with no value will yield an empty string `""` + * An element with a `value` attribute with a value will yield the value + """ + self._py.log.step('.should().not_have_value()', True) + try: + val = self._wait.until(lambda e: e.get_attribute('value') != value) + except TimeoutException: + val = False + + if val: + return self._element + else: + self._py.log.failed('.should().not_have_value()') + raise AssertionError(f'Element had value matching ``{value}``') + + def not_have_text(self, text, case_sensitive=True) -> 'Element': + """ An expectation that the element does not have the given text. + + Args: + text: The exact text to match. + case_sensitive: False if you want to ignore casing and leading/trailing spaces. + + Returns: + The current element. + + Raises: + `AssertionError` if the condition is not met in the specified amount of time. + """ + self._py.log.step('.should().not_have_text()', True) + try: + if case_sensitive: + value = self._wait.until(lambda e: e.text != text) + else: + value = self._wait.until(lambda e: e.text.strip().lower() != text.lower()) + except TimeoutException: + value = False + + if value: + return self._element + else: + self._py.log.failed('.should().not_have_text()') + raise AssertionError(f'Element had the text matching ``{text}``') + class Elements(List['Element']): """ Represents a list of DOM elements. """ - def __init__(self, py, web_elements): - self._list = [Element(py, element) for element in web_elements] - self.py = py + def __init__(self, py, web_elements, locator: Optional[Tuple]): + self._list = [Element(py, element, None) for element in web_elements] + self._py = py + self.locator = locator super().__init__(self._list) @property @@ -19,6 +519,10 @@ def length(self) -> int: """ The number of elements in the list. """ return len(self._list) + def is_empty(self) -> bool: + """ Checks if there are no elements in the list. """ + return self.length == 0 + def first(self) -> 'Element': """ Gets the first element in the list. @@ -54,7 +558,7 @@ def check(self, allow_selected=False) -> 'Elements': `ValueError` if any elements are already selected. `ValueError` if any elements are not checkboxes or radio buttons. """ - self.py.log.action('elements.check() - Check all checkboxes or radio buttons in this list', True) + self._py.log.action('elements.check() - Check all checkboxes or radio buttons in this list', True) for element in self._list: element.check(allow_selected) return self @@ -69,7 +573,7 @@ def uncheck(self, allow_deselected=False) -> 'Elements': `ValueError` if any elements are already selected. `ValueError` if any elements are not checkboxes or radio buttons. """ - self.py.log.action('elements.uncheck() - Uncheck all checkboxes or radio buttons in this list', True) + self._py.log.action('elements.uncheck() - Uncheck all checkboxes or radio buttons in this list', True) for element in self._list: element.uncheck(allow_deselected) return self @@ -88,13 +592,16 @@ def are_checked(self) -> bool: class Element: """ Represents a DOM element. """ - def __init__(self, py, web_element: WebElement): + def __init__(self, py, web_element: WebElement, locator: Optional[Tuple]): self._py = py - self._webelement = web_element + self._webelement = web_element, + self.locator = locator @property def webelement(self) -> WebElement: """ The current instance of the Selenium's `WebElement` API. """ + if isinstance(self._webelement, Tuple): + return self._webelement[0] return self._webelement @property @@ -114,6 +621,21 @@ def text(self) -> str: self.py.log.step('.text - Get the text in this element', True) return self.webelement.text + # EXPECTATIONS # + ################ + + def should(self, timeout: int = 0, ignored_exceptions: list = None) -> ElementShould: + """ A collection of expectations for this element. + + Examples: + py.get('#foo').should().be_clickable() + """ + if timeout: + wait_time = timeout + else: + wait_time = self._py.config.driver.wait_time + return ElementShould(self.py, self, wait_time, ignored_exceptions) + # METHODS # ########### @@ -139,6 +661,18 @@ def get_attribute(self, attribute: str): else: return value + def get_property(self, property: str): + """ Gets the property's value. + + Args: + property: The name of the element's property. + + Returns: + The value of the attribute. + """ + self.py.log.step(f'.get_property() - Get the {property} value of this element', True) + return self.webelement.get_property(property) + # CONDITIONS # ############## @@ -167,6 +701,24 @@ def is_displayed(self) -> bool: self.py.log.step('Check if this element is displayed', True) return self.webelement.is_displayed() + def is_enabled(self) -> bool: + """ Check that this element is enabled. + + Returns: + True if the element is enabled, else False + """ + self.py.log.step('Check if this element is enabled', True) + return self.webelement.is_enabled() + + def is_selected(self) -> bool: + """ Check that this element is selected. + + Returns: + True if the element is selected, else False + """ + self.py.log.step('Check if this element is selected', True) + return self.webelement.is_selected() + # ACTIONS # ########### @@ -396,11 +948,12 @@ def contains(self, text: str, timeout: int = 0) -> 'Element': The first element that is found, even if multiple elements match the query. """ self.py.log.step(f'.contains() - Find the element that contains text: ``{text}``', True) + locator = (By.XPATH, f'.//*[contains(text(), "{text}")]') element = self.py.wait(timeout).until( - lambda _: self.webelement.find_element(By.XPATH, f'//*[contains(text(), "{text}")]'), + lambda _: self.webelement.find_element(*locator), f'Could not find element with the text ``{text}``' ) - return Element(self.py, element) + return Element(self.py, element, locator) def get(self, css: str, timeout: int = 0) -> 'Element': """ Gets the DOM element that matches the `css` selector in this element's context. @@ -413,11 +966,12 @@ def get(self, css: str, timeout: int = 0) -> 'Element': The first element that is found, even if multiple elements match the query. """ self.py.log.step(f'.get() - Find the element that has css: ``{css}``', True) + by = By.CSS_SELECTOR element = self.py.wait(timeout).until( - lambda _: self.webelement.find_element(By.CSS_SELECTOR, css), + lambda _: self.webelement.find_element(by, css), f'Could not find element with the CSS ``{css}``' ) - return Element(self.py, element) + return Element(self.py, element, locator=(by, css)) def find(self, css: str, at_least_one=True, timeout: int = 0) -> Elements: """ Finds all DOM elements that match the `css` selector in this element's context. @@ -430,15 +984,17 @@ def find(self, css: str, at_least_one=True, timeout: int = 0) -> Elements: Returns: A list of the found elements. """ - self.py.log.step(f'.find() - Find the elements with css: ``{css}``', True) + by = By.CSS_SELECTOR if at_least_one: + self.py.log.step(f'.find() - Find at least one element with css: ``{css}``', True) elements = self.py.wait(timeout).until( - lambda _: self.webelement.find_elements(By.CSS_SELECTOR, css), + lambda _: self.webelement.find_elements(by, css), f'Could not find any elements with CSS ``{css}``' ) else: - elements = self.webelement.find_elements(By.CSS_SELECTOR, css) - return Elements(self.py, elements) + self.py.log.step(f'.find() - Find elements with css (no wait): ``{css}``', True) + elements = self.webelement.find_elements(by, css) + return Elements(self.py, elements, locator=(by, css)) def xpath(self, xpath: str, at_least_one=True, timeout: int = 0) -> Union['Element', Elements]: """ Finds all DOM elements that match the `xpath` selector. @@ -451,26 +1007,28 @@ def xpath(self, xpath: str, at_least_one=True, timeout: int = 0) -> Union['Eleme Returns: A list of the found elements. If only one is found, return that as Element. """ - self.py.log.step(f'.xpath() - Find the elements with xpath: ``{xpath}``', True) + by = By.XPATH if at_least_one: + self.py.log.step(f'.xpath() - Find at least one element with xpath: ``{xpath}``', True) elements = self.py.wait(timeout).until( - lambda _: self.webelement.find_elements(By.XPATH, xpath), - f'Could not find any elements with the CSS ``{xpath}``' + lambda _: self.webelement.find_elements(by, xpath), + f'Could not find any elements with the xpath ``{xpath}``' ) else: - elements = self.webelement.find_elements(By.CSS_SELECTOR, xpath) + self.py.log.step(f'.xpath() - Find elements with xpath (no wait): ``{xpath}``', True) + elements = self.webelement.find_elements(by, xpath) if len(elements) == 1: # If only one is found, return the single Element - return Element(self, elements[0]) + return Element(self, elements[0], locator=(by, xpath)) - return Elements(self, elements) + return Elements(self, elements, locator=(by, xpath)) def children(self) -> Elements: """ Gets the Child elements. """ self.py.log.info('.children() - Get the children of this element', True) elements = self.py.webdriver.execute_script('return arguments[0].children;', self.webelement) - return Elements(self.py, elements) + return Elements(self.py, elements, None) def parent(self) -> 'Element': """ Gets the Parent element. """ @@ -480,7 +1038,7 @@ def parent(self) -> 'Element': return elem.parentNode; ''' element = self.py.webdriver.execute_script(js, self.webelement) - return Element(self.py, element) + return Element(self.py, element, None) def siblings(self) -> Elements: """ Gets the Sibling elements. """ @@ -499,7 +1057,7 @@ def siblings(self) -> Elements: return siblings; ''' elements = self.py.webdriver.execute_script(js, self.webelement) - return Elements(self.py, elements) + return Elements(self.py, elements, None) # UTILITIES # ############# diff --git a/pylenium/wait.py b/pylenium/wait.py index 744c233..97c72f4 100644 --- a/pylenium/wait.py +++ b/pylenium/wait.py @@ -1,7 +1,6 @@ import time from typing import Tuple, Optional, Union -from selenium.common.exceptions import NoSuchElementException, TimeoutException from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.support.wait import WebDriverWait @@ -53,10 +52,10 @@ def until(self, method, message=''): """ value = self._wait.until(method, message) if isinstance(value, WebElement): - return Element(self._py, value) + return Element(self._py, value, None) if isinstance(value, list): try: - return Elements(self._py, value) + return Elements(self._py, value, None) except: pass # not a list of WebElement return value @@ -76,33 +75,3 @@ def build(self, timeout: int, use_py=False, ignored_exceptions: list = None) -> return PyleniumWait(self._py, self._webdriver, timeout, ignored_exceptions) else: return WebDriverWait(self._webdriver, timeout, ignored_exceptions=ignored_exceptions) - - -class ElementWait: - def __init__(self, webelement, timeout, ignored_exceptions: list = None): - self._webelement = webelement - self._timeout = 10 if timeout == 0 else timeout - if ignored_exceptions: - self._ignored_exceptions = ignored_exceptions - else: - self._ignored_exceptions = ( - NoSuchElementException - ) - - def until(self, method, message=''): - screen = None - stacktrace = None - - end_time = time.time() + self._timeout - while True: - try: - value = method(self._webelement) - if value: - return value - except self._ignored_exceptions as exc: - screen = getattr(exc, 'screen', None) - stacktrace = getattr(exc, 'stacktrace', None) - time.sleep(0.5) - if time.time() > end_time: - break - raise TimeoutException(message, screen, stacktrace) diff --git a/setup.py b/setup.py index 2585507..184564c 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='pyleniumio', - version='1.2.11', + version='1.3.0', packages=[ 'pylenium' ], diff --git a/tests/test_element.py b/tests/test_element.py index 4fe2385..be0c49b 100644 --- a/tests/test_element.py +++ b/tests/test_element.py @@ -1,8 +1,10 @@ +import pytest + def test_element_with_no_siblings(py): py.visit('https://deckshop.pro') elements = py.get("a[href='/spy/']").siblings() - assert elements.length == 0 + assert elements.is_empty() def test_element_parent_and_siblings(py): @@ -14,20 +16,20 @@ def test_element_parent_and_siblings(py): def test_element_text(py): py.visit('https://deckshop.pro') - assert py.contains('More info').text == 'More info' + assert py.contains('More info').should().have_text('More info') def test_find_in_element_context(py): py.visit('https://deckshop.pro') headers = py.find('h5') - assert 'Mega Knight' in headers[1].get("a").text + assert headers[1].get('a').should().contain_text('Royal') def test_input_type_and_get_value(py): py.visit('https://deckshop.pro') search_field = py.get('#smartSearch') - assert search_field.type('golem').get_attribute('value') == 'golem' - assert search_field.clear().get_attribute('value') == '' + assert search_field.type('golem').should().have_value('golem') + assert search_field.clear().should().have_value('') def test_children(py): @@ -37,6 +39,39 @@ def test_children(py): def test_forced_click(py): - py.visit('https://jane.com') - py.get('[data-testid="share"]').click() - py.get('[data-testid="si-pinterest"]').click(force=True) + py.visit('https://amazon.com') + # without forcing, this raises ElementNotInteractableException + py.get("#nav-al-your-account > a").click(force=True) + + +def test_element_should_be_clickable(py): + py.visit('https://deckshop.pro') + assert py.get("a.nav-link[href='/spy/']").should().be_clickable() + + +def test_element_should_not_be_clickable(py): + py.visit('https://deckshop.pro') + with pytest.raises(AssertionError): + py.get('#smartHelp').should().be_visible() + + +def test_element_should_be_visible(py): + py.visit('http://book.theautomatedtester.co.uk/chapter1') + py.get('#loadajax').click() + assert py.get('#ajaxdiv').should().be_visible() + + +def test_element_should_be_hidden(py): + py.visit('https://deckshop.pro') + assert py.get('#smartHelp').should().be_hidden() + + +def test_element_should_be_focused(py): + py.visit('https://deckshop.pro') + py.get('#smartSearch').click() + assert py.get('#smartSearch').should().be_focused() + + +def test_element_should_not_be_focused(py): + py.visit('https://deckshop.pro') + assert py.get('#smartSearch').should().not_be_focused() diff --git a/tests/test_element_actions.py b/tests/test_element_actions.py index c1947c7..855e7e9 100644 --- a/tests/test_element_actions.py +++ b/tests/test_element_actions.py @@ -3,7 +3,7 @@ def test_check_single_box(py): py.visit(f'{URL}/checkboxes') - assert py.get('[type="checkbox"]').check().is_checked() is True + assert py.get('[type="checkbox"]').check().should().be_checked() assert py.get('[type="checkbox"]').uncheck().is_checked() is False diff --git a/tests/test_pydriver.py b/tests/test_pydriver.py index 467840f..a28f7fd 100644 --- a/tests/test_pydriver.py +++ b/tests/test_pydriver.py @@ -12,7 +12,7 @@ def test_execute_script(py): def test_google_search(py): py.visit('https://google.com') py.get("[name='q']").type('puppies', Keys.ENTER) - assert 'puppies' in py.title + assert py.should().contain_title('puppies') def test_cookies(py): @@ -43,7 +43,7 @@ def test_viewport(py): def test_find_single_element_with_xpath(py): py.visit('https://google.com') py.xpath('//*[@name="q"]').type('QA at the Point', Keys.ENTER) - assert 'QA at the Point' in py.title + assert py.should().contain_title('QA at the Point') def test_find_elements_with_xpath(py): @@ -54,7 +54,7 @@ def test_find_elements_with_xpath(py): def test_hover_and_click_to_page_transition(py): py.visit('https://qap.dev') py.get('a[href="/about"]').hover().get('a[href="/leadership"][class*=Header]').click() - assert py.contains('Carlos Kidman').text == 'Carlos Kidman' + assert py.contains('Carlos Kidman').should().have_text('Carlos Kidman') def test_pylenium_wait_until(py):