├── tests ├── __init__.py ├── browser │ ├── __init__.py │ ├── elements │ │ ├── __init__.py │ │ ├── meta_tests.py │ │ ├── font_tests.py │ │ ├── deletes_tests.py │ │ ├── textarea_tests.py │ │ ├── dds_tests.py │ │ ├── dls_tests.py │ │ ├── areas_tests.py │ │ ├── dts_tests.py │ │ ├── ems_tests.py │ │ ├── ols_tests.py │ │ ├── uls_tests.py │ │ ├── images_tests.py │ │ ├── inses_tests.py │ │ ├── ps_tests.py │ │ ├── pres_tests.py │ │ ├── spans_tests.py │ │ ├── metas_tests.py │ │ ├── buttons_tests.py │ │ ├── maps_tests.py │ │ ├── frames_tests.py │ │ ├── textareas_tests.py │ │ ├── labels_tests.py │ │ ├── lis_tests.py │ │ ├── tds_tests.py │ │ ├── tables_tests.py │ │ ├── strongs_tests.py │ │ ├── checkboxes_tests.py │ │ ├── hns_tests.py │ │ ├── radios_tests.py │ │ ├── date_fields_tests.py │ │ ├── hiddens_tests.py │ │ ├── iframes_tests.py │ │ ├── file_fields_tests.py │ │ ├── forms_tests.py │ │ ├── date_time_fields_tests.py │ │ ├── divs_tests.py │ │ ├── text_fields_tests.py │ │ ├── tbodys_tests.py │ │ ├── theads_tests.py │ │ ├── select_lists_tests.py │ │ ├── elements_tests.py │ │ ├── list_tests.py │ │ ├── trs_tests.py │ │ ├── tfoots_tests.py │ │ ├── links_tests.py │ │ ├── table_nesting_tests.py │ │ ├── ul_tests.py │ │ ├── tr_tests.py │ │ ├── ol_tests.py │ │ ├── label_tests.py │ │ ├── area_tests.py │ │ ├── map_tests.py │ │ ├── tbody_tests.py │ │ ├── td_tests.py │ │ ├── form_tests.py │ │ ├── strong_tests.py │ │ ├── hn_tests.py │ │ └── tfoot_tests.py │ ├── special_chars_tests.py │ ├── input_tests.py │ ├── support.py │ ├── screenshot_tests.py │ ├── drag_and_drop_tests.py │ ├── element_hidden_tests.py │ └── alert_tests.py └── unit │ ├── match_elements │ └── __init__.py │ ├── selector_builder │ ├── __init__.py │ ├── textarea_tests.py │ ├── anchor_tests.py │ └── cell_tests.py │ ├── browser_tests.py │ ├── container_tests.py │ └── wait_tests.py ├── nerodia ├── elements │ ├── __init__.py │ ├── table_data_cell.py │ ├── text_area.py │ ├── form.py │ ├── d_list.py │ ├── input.py │ ├── font.py │ ├── image.py │ ├── table_section.py │ ├── hidden.py │ ├── cell.py │ ├── button.py │ ├── table_row.py │ ├── option.py │ ├── table_cell.py │ ├── text_field.py │ ├── file_field.py │ ├── check_box.py │ ├── radio.py │ ├── list.py │ ├── date_field.py │ ├── row.py │ ├── date_time_field.py │ ├── table.py │ └── scroll.py ├── support │ └── __init__.py ├── wait │ ├── __init__.py │ └── timer.py ├── js_snippets │ ├── focus.js │ ├── setText.js │ ├── setValue.js │ ├── getElementTags.js │ ├── isImageLoaded.js │ ├── backgroundColor.js │ ├── selectedOptions.js │ ├── selectOptionsText.js │ ├── selectOptionsLabel.js │ ├── selectOptionsValue.js │ ├── attributeValues.js │ ├── elementObscured.js │ ├── selectedText.js │ ├── getInnerHtml.js │ ├── getInnerText.js │ ├── getTextContent.js │ └── getOuterHtml.js ├── locators │ ├── cell │ │ ├── __init__.py │ │ └── selector_builder.py │ ├── row │ │ ├── __init__.py │ │ └── selector_builder.py │ ├── anchor │ │ ├── __init__.py │ │ └── selector_builder.py │ ├── text_area │ │ ├── __init__.py │ │ └── selector_builder.py │ ├── button │ │ ├── __init__.py │ │ └── matcher.py │ ├── text_field │ │ ├── __init__.py │ │ ├── matcher.py │ │ └── selector_builder.py │ ├── element │ │ ├── __init__.py │ │ └── xpath_support.py │ ├── __init__.py │ └── class_helpers.py ├── cell_container.py ├── module_mapping.py ├── exception.py ├── row_container.py ├── js_snippet.py ├── __init__.py ├── screenshot.py ├── has_window.py ├── logger.py ├── meta_elements.py ├── alert.py ├── user_editable.py └── cookies.py ├── .gitignore ├── .gitmodules ├── requirements.txt ├── docs ├── source │ ├── modules.rst │ ├── api.rst │ ├── installing.rst │ ├── nerodia.wait.rst │ ├── index.rst │ ├── examples.rst │ ├── nerodia.rst │ └── watir.rst ├── Makefile └── make.bat ├── run_examples.py ├── .github ├── actions │ ├── setup-linux │ │ └── action.yml │ └── install-chrome │ │ └── action.yml ├── ISSUE_TEMPLATE.md └── workflows │ └── build.yml ├── setup.cfg ├── examples ├── basic_example.py ├── page_object_example.py └── form_example.py ├── tox.ini ├── LICENSE ├── README.rst ├── setup.py └── generate_attributes.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nerodia/elements/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nerodia/support/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nerodia/wait/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/browser/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/browser/elements/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/match_elements/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/selector_builder/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .idea 3 | .vscode 4 | venv* 5 | *.pyc 6 | docs/build/**/* -------------------------------------------------------------------------------- /nerodia/js_snippets/focus.js: -------------------------------------------------------------------------------- 1 | function(){ 2 | return arguments[0].focus(); 3 | } 4 | -------------------------------------------------------------------------------- /nerodia/js_snippets/setText.js: -------------------------------------------------------------------------------- 1 | function(){arguments[0].textContent=arguments[1]} 2 | -------------------------------------------------------------------------------- /nerodia/js_snippets/setValue.js: -------------------------------------------------------------------------------- 1 | function(){ 2 | arguments[0].value=arguments[1] 3 | } 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "watir"] 2 | path = watir 3 | url = https://github.com/watir/watir.git 4 | -------------------------------------------------------------------------------- /nerodia/locators/cell/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | from .selector_builder import SelectorBuilder 4 | -------------------------------------------------------------------------------- /nerodia/locators/row/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | from .selector_builder import SelectorBuilder 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-instafail 3 | pytest-mock 4 | python-dateutil 5 | selenium 6 | six 7 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | nerodia 2 | ======= 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | nerodia 8 | -------------------------------------------------------------------------------- /nerodia/locators/anchor/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | from .selector_builder import SelectorBuilder 4 | -------------------------------------------------------------------------------- /nerodia/locators/text_area/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | from .selector_builder import SelectorBuilder 4 | -------------------------------------------------------------------------------- /nerodia/js_snippets/getElementTags.js: -------------------------------------------------------------------------------- 1 | function(){ 2 | return arguments[0].map(function(e) {return e.localName}); 3 | } 4 | -------------------------------------------------------------------------------- /nerodia/locators/button/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | from .matcher import Matcher 4 | from .selector_builder import SelectorBuilder 5 | -------------------------------------------------------------------------------- /nerodia/locators/text_field/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | from .matcher import Matcher 4 | from .selector_builder import SelectorBuilder 5 | -------------------------------------------------------------------------------- /nerodia/js_snippets/isImageLoaded.js: -------------------------------------------------------------------------------- 1 | function(){ 2 | return typeof arguments[0].naturalWidth != "undefined" && arguments[0].naturalWidth > 0 3 | } 4 | -------------------------------------------------------------------------------- /run_examples.py: -------------------------------------------------------------------------------- 1 | from examples import (basic_example, 2 | page_object_example) 3 | 4 | basic_example.run() 5 | page_object_example.run() 6 | -------------------------------------------------------------------------------- /nerodia/locators/element/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | from .locator import Locator 4 | from .matcher import Matcher 5 | from .selector_builder import SelectorBuilder 6 | -------------------------------------------------------------------------------- /.github/actions/setup-linux/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Setup Linux' 2 | description: 'Run Linux in Virtual Desktop' 3 | runs: 4 | using: "composite" 5 | steps: 6 | - shell: bash 7 | run: | 8 | Xvfb :99 & 9 | -------------------------------------------------------------------------------- /tests/browser/special_chars_tests.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.page('special_chars.html') 5 | def test_finds_elements_with_single_quotes(browser): 6 | assert browser.div(text="single 'quotes'").exists 7 | -------------------------------------------------------------------------------- /nerodia/js_snippets/backgroundColor.js: -------------------------------------------------------------------------------- 1 | function(){ 2 | if(arguments[1]) { 3 | arguments[0].style.backgroundColor = arguments[1]; 4 | } else { 5 | return arguments[0].style.backgroundColor; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/browser/input_tests.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.page('forms_with_input_elements.html') 5 | class TestInputType(object): 6 | def test_returns_an_email_type(self, browser): 7 | assert browser.input(name='html5_email').type == 'email' 8 | -------------------------------------------------------------------------------- /nerodia/elements/table_data_cell.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | from .table_cell import TableCell 4 | from ..meta_elements import MetaHTMLElement 5 | 6 | 7 | @six.add_metaclass(MetaHTMLElement) 8 | class TableDataCell(TableCell): 9 | _attr_abbr = (str, 'abbr') 10 | pass 11 | -------------------------------------------------------------------------------- /nerodia/locators/__init__.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | 3 | 4 | W3C_FINDERS = { 5 | 'css': By.CSS_SELECTOR, 6 | 'link': By.LINK_TEXT, 7 | 'link_text': By.LINK_TEXT, 8 | 'partial_link_text': By.PARTIAL_LINK_TEXT, 9 | 'xpath': By.XPATH 10 | } 11 | -------------------------------------------------------------------------------- /nerodia/elements/text_area.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | from .html_elements import HTMLElement 4 | from ..meta_elements import MetaHTMLElement 5 | from ..user_editable import UserEditable 6 | 7 | 8 | @six.add_metaclass(MetaHTMLElement) 9 | class TextArea(UserEditable, HTMLElement): 10 | pass 11 | -------------------------------------------------------------------------------- /nerodia/js_snippets/selectedOptions.js: -------------------------------------------------------------------------------- 1 | function(){ 2 | var result = []; 3 | var options = arguments[0].options; 4 | for (var i = 0; i < options.length; i++) { 5 | var option = options[i]; 6 | if (option.selected) { 7 | result.push(option) 8 | } 9 | } 10 | return result; 11 | } 12 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .git,.cache,.tox,build,dist,venv*,watir 3 | ignore = E501 4 | 5 | [metadata] 6 | author = Lucas Tierney 7 | author_email = lucast1533@gmail.com 8 | description-file = README.rst 9 | 10 | [tool:pytest] 11 | addopts = -r=a 12 | log_print = false 13 | python_files = test_*.py *_tests.py 14 | testpaths = tests 15 | -------------------------------------------------------------------------------- /nerodia/cell_container.py: -------------------------------------------------------------------------------- 1 | from .elements.cell import Cell, CellCollection 2 | 3 | 4 | class CellContainer(object): 5 | def cell(self, *args, **kwargs): 6 | return Cell(self, self._extract_selector(*args, **kwargs)) 7 | 8 | def cells(self, *args, **kwargs): 9 | return CellCollection(self, self._extract_selector(*args, **kwargs)) 10 | -------------------------------------------------------------------------------- /nerodia/elements/form.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | from .html_elements import HTMLElement 4 | from ..meta_elements import MetaHTMLElement 5 | 6 | 7 | @six.add_metaclass(MetaHTMLElement) 8 | class Form(HTMLElement): 9 | def submit(self): 10 | self._element_call(lambda: self.el.submit(), self.wait_for_present) 11 | self.browser.after_hooks.run() 12 | -------------------------------------------------------------------------------- /nerodia/js_snippets/selectOptionsText.js: -------------------------------------------------------------------------------- 1 | function(){ 2 | for(var i=0; i 17 | -------------------------------------------------------------------------------- /nerodia/elements/font.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | from .html_elements import HTMLElement 4 | from ..meta_elements import MetaHTMLElement 5 | 6 | 7 | @six.add_metaclass(MetaHTMLElement) 8 | class Font(HTMLElement): 9 | 10 | @property 11 | def size(self): 12 | """ 13 | Returns the size of the font 14 | :rtype: int 15 | 16 | :Example: 17 | 18 | browser.font().size #=> 12 19 | """ 20 | return self.attribute_value('size') 21 | -------------------------------------------------------------------------------- /nerodia/module_mapping.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | MODULE_MAPPING = {'frame': 'i_frame', 4 | 'anchor': 'link', 5 | 'o_list': 'list', 6 | 'u_list': 'list'} 7 | 8 | 9 | def map_module(name): 10 | element_module = re.sub(r'([A-Z]{1})', r'_\1', name)[1:].lower() 11 | if element_module in list(MODULE_MAPPING): # special cases 12 | return MODULE_MAPPING.get(element_module) 13 | else: 14 | return element_module 15 | -------------------------------------------------------------------------------- /nerodia/js_snippets/elementObscured.js: -------------------------------------------------------------------------------- 1 | // Original Author: Florent B. 2 | // Source: https://stackoverflow.com/a/45244889/1200545 3 | function() { 4 | var elem = arguments[0], 5 | box = elem.getBoundingClientRect(), 6 | cx = box.left + box.width / 2, 7 | cy = box.top + box.height / 2, 8 | e = document.elementFromPoint(cx, cy); 9 | for (; e; e = e.parentElement) { 10 | if (e === elem) 11 | return false; 12 | } 13 | return true; 14 | } -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | ========================= 4 | Nerodia API Documentation 5 | ========================= 6 | 7 | Modules 8 | ------- 9 | 10 | Here we include references to the Nerodia API. Specific details for each module and class of Nerodia can be found here. 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | nerodia.rst 16 | nerodia.elements.rst 17 | nerodia.wait.rst 18 | 19 | Indices and tables 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | -------------------------------------------------------------------------------- /nerodia/elements/image.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | from .html_elements import HTMLElement 4 | from ..meta_elements import MetaHTMLElement 5 | 6 | 7 | @six.add_metaclass(MetaHTMLElement) 8 | class Image(HTMLElement): 9 | @property 10 | def loaded(self): 11 | """ 12 | Returns True if the image is loaded 13 | :rtype: bool 14 | """ 15 | if not self.complete: 16 | return False 17 | 18 | return self._element_call(lambda: self._execute_js('isImageLoaded', self.el)) 19 | -------------------------------------------------------------------------------- /nerodia/exception.py: -------------------------------------------------------------------------------- 1 | class Error(Exception): 2 | pass 3 | 4 | 5 | class UnknownObjectException(Error): 6 | pass 7 | 8 | 9 | class ObjectDisabledException(Error): 10 | pass 11 | 12 | 13 | class ObjectReadOnlyException(Error): 14 | pass 15 | 16 | 17 | class NoValueFoundException(Error): 18 | pass 19 | 20 | 21 | class NoMatchingWindowFoundException(Error): 22 | pass 23 | 24 | 25 | class UnknownFrameException(Error): 26 | pass 27 | 28 | 29 | class LocatorException(Error): 30 | pass 31 | -------------------------------------------------------------------------------- /tests/browser/support.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from contextlib import contextmanager 3 | 4 | try: 5 | from StringIO import StringIO 6 | except ImportError: 7 | from io import StringIO 8 | 9 | 10 | @contextmanager 11 | def captured_output(): 12 | new_out, new_err = StringIO(), StringIO() 13 | old_out, old_err = sys.stdout, sys.stderr 14 | try: 15 | sys.stdout, sys.stderr = new_out, new_err 16 | yield sys.stdout, sys.stderr 17 | finally: 18 | sys.stdout, sys.stderr = old_out, old_err 19 | -------------------------------------------------------------------------------- /nerodia/elements/table_section.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | from .html_elements import HTMLElement 4 | from ..meta_elements import MetaHTMLElement 5 | from ..row_container import RowContainer 6 | 7 | 8 | @six.add_metaclass(MetaHTMLElement) 9 | class TableSection(RowContainer, HTMLElement): 10 | def __getitem__(self, idx): 11 | """ 12 | Returns row of this table with given index 13 | 14 | :param idx: row index 15 | :rtype: nerodia.elements.row.Row 16 | """ 17 | return self.row(index=idx) 18 | -------------------------------------------------------------------------------- /docs/source/installing.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Supported Python Versions 5 | ------------------------- 6 | 7 | * Python 2.7 8 | * Python 3.4+ 9 | 10 | Installing 11 | ---------- 12 | 13 | If you have `pip `_ on your system, you can simply install or upgrade:: 14 | 15 | pip install -U nerodia 16 | 17 | Alternately, you can download the source distribution from `PyPI `_ (e.g. nerodia-1.0.0.tar.gz), unarchive it, and run:: 18 | 19 | python setup.py install 20 | -------------------------------------------------------------------------------- /.github/actions/install-chrome/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Install Chrome' 2 | description: 'Install Chrome Stable' 3 | runs: 4 | using: "composite" 5 | steps: 6 | - run: | 7 | wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - 8 | echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee -a /etc/apt/sources.list.d/google-chrome.list 9 | sudo apt-get update -qqy 10 | sudo apt-get -qqy install google-chrome-stable 11 | sudo rm /etc/apt/sources.list.d/google-chrome.list 12 | shell: bash 13 | -------------------------------------------------------------------------------- /nerodia/locators/text_area/selector_builder.py: -------------------------------------------------------------------------------- 1 | from ..element.selector_builder import SelectorBuilder as ElementSelectorBuilder, \ 2 | XPath as ElementXPath 3 | 4 | 5 | class SelectorBuilder(ElementSelectorBuilder): 6 | pass 7 | 8 | 9 | class XPath(ElementXPath): 10 | # private 11 | 12 | # value always requires a wire call since we want the property not the attribute 13 | def _process_attribute(self, key, value): 14 | if key != 'value': 15 | return super(XPath, self)._process_attribute(key, value) 16 | self.built['value'] = value 17 | return None 18 | -------------------------------------------------------------------------------- /nerodia/elements/hidden.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | from .html_elements import InputCollection 4 | from ..elements.input import Input 5 | from ..exception import ObjectDisabledException 6 | from ..meta_elements import MetaHTMLElement 7 | 8 | 9 | @six.add_metaclass(MetaHTMLElement) 10 | class Hidden(Input): 11 | @property 12 | def visible(self): 13 | return False 14 | 15 | def click(self): 16 | raise ObjectDisabledException('click is not available on the hidden field element') 17 | 18 | 19 | @six.add_metaclass(MetaHTMLElement) 20 | class HiddenCollection(InputCollection): 21 | pass 22 | -------------------------------------------------------------------------------- /tests/browser/elements/meta_tests.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytestmark = pytest.mark.page('forms_with_input_elements.html') 4 | 5 | 6 | class TestMeta(object): 7 | def test_returns_true_if_the_element_exists(self, browser): 8 | assert browser.meta(**{'http-equiv': 'Content-Type'}).exists is True 9 | 10 | def test_returns_the_first_meta_if_given_no_args(self, browser): 11 | assert browser.meta().exists 12 | 13 | def test_returns_the_content_attribute_of_the_tag(self, browser): 14 | assert browser.meta(**{'http-equiv': 'Content-Type'}).content == "text/html; charset=utf-8" 15 | -------------------------------------------------------------------------------- /docs/source/nerodia.wait.rst: -------------------------------------------------------------------------------- 1 | nerodia\.wait package 2 | ===================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | nerodia\.wait\.timer module 8 | --------------------------- 9 | 10 | .. automodule:: nerodia.wait.timer 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | nerodia\.wait\.wait module 16 | -------------------------- 17 | 18 | .. automodule:: nerodia.wait.wait 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: nerodia.wait 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /examples/basic_example.py: -------------------------------------------------------------------------------- 1 | """Basic script showing how Nerodia works.""" 2 | from nerodia.browser import Browser 3 | 4 | 5 | def run(): 6 | br = Browser(browser='chrome') 7 | 8 | br.goto("https://watir.com") 9 | 10 | # Check that "Titus" is somewhere in the page text 11 | assert "Watir" in br.text 12 | 13 | # Check "open source" is in the intro 14 | intro_text = br.div(class_name='intro').text 15 | assert "open source" in intro_text 16 | 17 | # Check that the page is correct via the URL 18 | br.link(text='Guides').click() 19 | assert 'watir.com/guides/' in br.url 20 | 21 | br.close() 22 | 23 | 24 | if __name__ == '__main__': 25 | run() 26 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = Nerodia 8 | SOURCEDIR = source 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /nerodia/elements/cell.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | from .html_elements import TableCellCollection 4 | from .table_cell import TableCell 5 | from ..meta_elements import MetaHTMLElement 6 | 7 | 8 | @six.add_metaclass(MetaHTMLElement) 9 | class Cell(TableCell): 10 | pass 11 | 12 | 13 | @six.add_metaclass(MetaHTMLElement) 14 | class CellCollection(TableCellCollection): 15 | @property 16 | def _elements(self): 17 | # we do this craziness since the xpath used will find direct child rows 18 | # before any rows inside thead/tbody/tfoot... 19 | elements = super(CellCollection, self)._elements 20 | return sorted(elements, key=lambda e: int(e.get_attribute('cellIndex'))) 21 | -------------------------------------------------------------------------------- /tests/browser/elements/font_tests.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytestmark = pytest.mark.page('font.html') 4 | 5 | 6 | class TestFont(object): 7 | def test_finds_the_font_element(self, browser): 8 | assert browser.font(index=0).exists 9 | 10 | def test_knows_about_the_color_attribute(self, browser): 11 | assert browser.font(index=0).color == '#ff00ff' 12 | 13 | def test_knows_about_the_face_attribute(self, browser): 14 | assert browser.font(index=0).face == 'Helvetica' 15 | 16 | def test_knows_about_the_size_attribute(self, browser): 17 | assert browser.font(index=0).size == '12' 18 | 19 | def test_finds_all_font_elements(self, browser): 20 | assert len(browser.fonts()) == 1 21 | -------------------------------------------------------------------------------- /nerodia/js_snippets/selectedText.js: -------------------------------------------------------------------------------- 1 | // Code from https://stackoverflow.com/questions/5379120/get-the-highlighted-selected-text 2 | 3 | function() { 4 | var text = ""; 5 | var activeEl = document.activeElement; 6 | var activeElTagName = activeEl ? activeEl.tagName.toLowerCase() : null; 7 | if ( 8 | (activeElTagName == "textarea") || (activeElTagName == "input" && 9 | /^(?:text|search|password|tel|url)$/i.test(activeEl.type)) && 10 | (typeof activeEl.selectionStart == "number") 11 | ) { 12 | text = activeEl.value.slice(activeEl.selectionStart, activeEl.selectionEnd); 13 | } else if (window.getSelection) { 14 | text = window.getSelection().toString(); 15 | } 16 | return text; 17 | } 18 | -------------------------------------------------------------------------------- /nerodia/row_container.py: -------------------------------------------------------------------------------- 1 | class RowContainer(object): 2 | def row(self, *args, **kwargs): 3 | from .elements.row import Row 4 | return Row(self, self._extract_selector(*args, **kwargs)) 5 | 6 | def rows(self, *args, **kwargs): 7 | from .elements.row import RowCollection 8 | return RowCollection(self, self._extract_selector(*args, **kwargs)) 9 | 10 | @property 11 | def strings(self): 12 | """ 13 | A table as a 2D array of strings with the text of each cell 14 | :rtype: list[list[str]] 15 | """ 16 | self.wait_for_exists() 17 | 18 | rows = [] 19 | for row in self.rows(): 20 | rows.append([cell.text for cell in row]) 21 | return rows 22 | -------------------------------------------------------------------------------- /tests/browser/elements/deletes_tests.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytestmark = pytest.mark.page('non_control_elements.html') 4 | 5 | 6 | def test_returns_the_matching_elements(browser): 7 | assert list(browser.deletes(class_name='lead')) == [browser.delete(class_name='lead')] 8 | 9 | 10 | def test_returns_the_number_of_deletes(browser): 11 | assert len(browser.deletes()) == 5 12 | 13 | 14 | def test_returns_the_delete_at_the_given_index(browser): 15 | assert browser.deletes()[0].id == 'lead' 16 | 17 | 18 | def test_iterates_through_deletes_correctly(browser): 19 | count = 0 20 | for index, d in enumerate(browser.deletes()): 21 | assert d.id == browser.delete(index=index).id 22 | count += 1 23 | assert count > 0 24 | -------------------------------------------------------------------------------- /tests/browser/elements/textarea_tests.py: -------------------------------------------------------------------------------- 1 | from re import compile 2 | 3 | import pytest 4 | 5 | pytestmark = pytest.mark.page('forms_with_input_elements.html') 6 | 7 | 8 | class TestTextArea(object): 9 | def test_can_set_a_value(self, browser): 10 | browser.textarea().set('foo') 11 | assert browser.textarea().value == 'foo' 12 | 13 | def test_can_clear_a_value(self, browser): 14 | browser.textarea().set('foo') 15 | browser.textarea().clear() 16 | assert browser.textarea().value == '' 17 | 18 | def test_locates_textarea_by_value(self, browser): 19 | browser.textarea().set('foo') 20 | assert browser.textarea(value=compile(r'foo')).exists 21 | assert browser.textarea(value='foo').exists 22 | -------------------------------------------------------------------------------- /nerodia/js_snippet.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | 3 | 4 | class JSSnippet(object): 5 | # private 6 | 7 | def _execute_js(self, function_name, *args): 8 | filepath = path.abspath(path.join(path.dirname(__file__), 9 | 'js_snippets', 10 | '{}.js'.format(function_name))) 11 | if not path.isfile(filepath): 12 | from nerodia.exception import Error 13 | raise Error('Can not excute script as {!r} does not exist'.format(filepath)) 14 | 15 | with open(filepath, 'r') as myfile: 16 | script = 'return ({}).apply(null, arguments)'.format(myfile.read()) 17 | return self.query_scope.execute_script(script, *args, function_name=function_name) 18 | -------------------------------------------------------------------------------- /tests/browser/elements/dds_tests.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytestmark = pytest.mark.page('definition_lists.html') 4 | 5 | 6 | def test_returns_the_matching_elements(browser): 7 | assert list(browser.dds(text='11 years')) == [browser.dd(text='11 years')] 8 | 9 | 10 | def test_returns_the_number_of_dds(browser): 11 | assert len(browser.dds()) == 11 12 | 13 | 14 | def test_returns_the_dd_at_the_given_index(browser): 15 | assert browser.dds()[1].title == 'education' 16 | 17 | 18 | def test_iterates_through_dds_correctly(browser): 19 | count = 0 20 | for index, d in enumerate(browser.dds()): 21 | dd = browser.dd(index=index) 22 | assert d.id == dd.id 23 | assert d.class_name == dd.class_name 24 | count += 1 25 | assert count > 0 26 | -------------------------------------------------------------------------------- /nerodia/locators/cell/selector_builder.py: -------------------------------------------------------------------------------- 1 | from ..element.selector_builder import SelectorBuilder as ElementSelectorBuilder,\ 2 | XPath as ElementXPath 3 | 4 | 5 | class SelectorBuilder(ElementSelectorBuilder): 6 | @property 7 | def _merge_scope(self): 8 | return False 9 | 10 | 11 | class XPath(ElementXPath): 12 | # private 13 | 14 | @property 15 | def _start_string(self): 16 | return './' if self.adjacent is not None else './*' 17 | 18 | @property 19 | def _tag_string(self): 20 | if self.adjacent is not None: 21 | return super(XPath, self)._text_string 22 | 23 | return '[{} or {}]'.format(self._process_attribute('tag_name', 'th'), 24 | self._process_attribute('tag_name', 'td')) 25 | -------------------------------------------------------------------------------- /tests/browser/elements/dls_tests.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytestmark = pytest.mark.page('definition_lists.html') 4 | 5 | 6 | def test_returns_the_matching_elements(browser): 7 | assert list(browser.dls(title='experience')) == [browser.dl(title='experience')] 8 | 9 | 10 | def test_returns_the_number_of_divs(browser): 11 | assert len(browser.dls()) == 3 12 | 13 | 14 | def test_returns_the_dl_at_the_given_index(browser): 15 | assert browser.dls()[0].id == 'experience-list' 16 | 17 | 18 | def test_iterates_through_dls_correctly(browser): 19 | count = 0 20 | for index, d in enumerate(browser.dls()): 21 | dl = browser.dl(index=index) 22 | assert d.id == dl.id 23 | assert d.class_name == dl.class_name 24 | count += 1 25 | assert count > 0 26 | -------------------------------------------------------------------------------- /tests/browser/elements/areas_tests.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytestmark = pytest.mark.page('images.html') 4 | 5 | 6 | def test_returns_the_matching_elements(browser, page): 7 | assert list(browser.areas(alt="Tables")) == [browser.area(alt="Tables")] 8 | 9 | 10 | def test_returns_the_number_of_areas(browser, page): 11 | assert len(browser.areas()) == 3 12 | 13 | 14 | def test_returns_the_area_at_the_given_index(browser, page): 15 | assert browser.areas()[0].id == 'NCE' 16 | 17 | 18 | def test_iterates_through_areas_correctly(browser, page): 19 | count = 0 20 | for index, a in enumerate(browser.areas()): 21 | area = browser.area(index=index) 22 | assert a.id == area.id 23 | assert a.title == area.title 24 | count += 1 25 | assert count > 0 26 | -------------------------------------------------------------------------------- /tests/browser/elements/dts_tests.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytestmark = pytest.mark.page('definition_lists.html') 4 | 5 | 6 | def test_returns_the_matching_elements(browser): 7 | assert list(browser.dts(class_name='current-industry')) == \ 8 | [browser.dt(class_name='current-industry')] 9 | 10 | 11 | def test_returns_the_number_of_divs(browser): 12 | assert len(browser.dts()) == 11 13 | 14 | 15 | def test_returns_the_dt_at_the_given_index(browser): 16 | assert browser.dts()[0].id == 'experience' 17 | 18 | 19 | def test_iterates_through_dts_correctly(browser): 20 | count = 0 21 | for index, d in enumerate(browser.dts()): 22 | dt = browser.dt(index=index) 23 | assert d.id == dt.id 24 | assert d.class_name == dt.class_name 25 | count += 1 26 | assert count > 0 27 | -------------------------------------------------------------------------------- /tests/browser/elements/ems_tests.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytestmark = pytest.mark.page('non_control_elements.html') 4 | 5 | 6 | def test_returns_the_matching_elements(browser): 7 | assert list(browser.ems(class_name='important-class')) == [ 8 | browser.em(class_name='important-class')] 9 | 10 | 11 | def test_returns_the_number_of_ems(browser): 12 | assert len(browser.ems()) == 1 13 | 14 | 15 | def test_returns_the_div_at_the_given_index(browser): 16 | assert browser.ems()[0].id == 'important-id' 17 | 18 | 19 | def test_iterates_through_ems_correctly(browser): 20 | count = 0 21 | for index, e in enumerate(browser.ems()): 22 | em = browser.em(index=index) 23 | assert e.id == em.id 24 | assert e.class_name == em.class_name 25 | count += 1 26 | assert count > 0 27 | -------------------------------------------------------------------------------- /tests/browser/elements/ols_tests.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytestmark = pytest.mark.page('non_control_elements.html') 4 | 5 | 6 | class TestPs(object): 7 | def test_with_selectors_returns_the_matching_elements(self, browser): 8 | assert list(browser.ps(class_name='lead')) == [browser.p(class_name='lead')] 9 | 10 | def test_returns_the_correct_number_of_ps(self, browser): 11 | assert len(browser.ps()) == 5 12 | 13 | def test_get_item_returns_the_p_at_the_given_index(self, browser): 14 | assert browser.ps()[0].id == 'lead' 15 | 16 | def test_iterates_through_ps_correctly(self, browser): 17 | count = 0 18 | 19 | for index, p in enumerate(browser.ps()): 20 | assert p.id == browser.p(index=index).id 21 | count += 1 22 | 23 | assert count > 0 24 | -------------------------------------------------------------------------------- /tests/browser/screenshot_tests.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | import pytest 4 | 5 | PNG_HEADER = b'\211PNG' 6 | 7 | 8 | @pytest.mark.page('wait.html') 9 | class TestScreenshot(object): 10 | def test_gets_png_representation_of_screenshot(self, browser): 11 | assert browser.screenshot.png()[0:4] == PNG_HEADER 12 | 13 | def test_gets_base64_representation_of_screenshot(self, browser): 14 | image = browser.screenshot.base64() 15 | assert base64.b64decode(image)[0:4] == PNG_HEADER 16 | 17 | def test_saves_screenshot_to_given_file(self, browser): 18 | import tempfile 19 | tmp = tempfile.NamedTemporaryFile(suffix='.png') 20 | try: 21 | browser.screenshot.save(tmp.name) 22 | assert tmp.read()[0:4] == PNG_HEADER 23 | finally: 24 | tmp.close() 25 | -------------------------------------------------------------------------------- /nerodia/elements/button.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | from .input import Input 4 | from ..meta_elements import MetaHTMLElement 5 | 6 | 7 | @six.add_metaclass(MetaHTMLElement) 8 | class Button(Input): 9 | """ 10 | Class representing button elements 11 | 12 | This class covers both