├── features ├── __init__.py ├── steps │ ├── __init__.py │ ├── div_steps.py │ ├── span_steps.py │ ├── label_steps.py │ ├── paragraph_steps.py │ ├── shared_element_steps.py │ ├── link_steps.py │ ├── element_steps.py │ ├── navigation_steps.py │ ├── image_steps.py │ ├── text_field_steps.py │ ├── text_area_steps.py │ ├── check_box_steps.py │ ├── button_steps.py │ ├── async_steps.py │ └── select_list_steps.py ├── support │ ├── __init__.py │ ├── html │ │ ├── images │ │ │ └── circle.png │ │ ├── success.html │ │ ├── async_elements.html │ │ └── static_elements.html │ ├── async_page.py │ └── page.py ├── environment.py └── elements │ ├── elements.feature │ ├── span.feature │ ├── label.feature │ ├── link_element.feature │ ├── div.feature │ ├── paragraph.feature │ ├── image.feature │ ├── check_box.feature │ ├── async.feature │ ├── button.feature │ ├── text_field.feature │ ├── text_area.feature │ └── select_list.feature ├── src ├── __init__.py └── page_object │ ├── expected_conditions │ ├── __init__.py │ ├── element_predicate.py │ └── element_exists.py │ ├── elements │ ├── div.py │ ├── label.py │ ├── link.py │ ├── button.py │ ├── option.py │ ├── table_cell.py │ ├── heading.py │ ├── paragraph.py │ ├── radio_button.py │ ├── span.py │ ├── image.py │ ├── table.py │ ├── substitute_web_element.py │ ├── checkbox.py │ ├── text_area.py │ ├── text_field.py │ ├── table_row.py │ ├── select_list.py │ ├── __init__.py │ └── element.py │ ├── __init__.py │ ├── browser.py │ ├── locator_generator.py │ ├── page_factory.py │ ├── browser_factory.py │ ├── page_object.py │ ├── locator.py │ └── accessors.py ├── requirements.pip ├── .travis.yml ├── specs ├── accessors │ ├── __init__.py │ ├── table_accessor_test.py │ ├── plural_accessor_test.py │ └── div_accessor_test.py ├── page_factory │ ├── __init__.py │ ├── on_page_test.py │ └── visit_page_test.py └── __init__.py ├── Makefile ├── Dockerfile ├── setup.py ├── README.md ├── .gitignore └── LICENSE /features/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /features/steps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /features/support/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | -------------------------------------------------------------------------------- /features/support/html/images/circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OnShift/page_object/HEAD/features/support/html/images/circle.png -------------------------------------------------------------------------------- /requirements.pip: -------------------------------------------------------------------------------- 1 | mock==2.0.0 2 | selenium==3.4.3 3 | PyHamcrest==1.8.5 4 | behave==1.2.5 5 | pytest==3.0.3 6 | twine==1.8.1 7 | -e . 8 | -------------------------------------------------------------------------------- /features/environment.py: -------------------------------------------------------------------------------- 1 | from page_object import Browser 2 | 3 | 4 | def after_all(context): 5 | Browser.selenium_browser().close() 6 | -------------------------------------------------------------------------------- /src/page_object/expected_conditions/__init__.py: -------------------------------------------------------------------------------- 1 | from .element_predicate import element_predicate 2 | from .element_exists import element_exists 3 | -------------------------------------------------------------------------------- /features/support/html/success.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Success 4 | 5 | 6 |

Success

7 | 8 | 9 | -------------------------------------------------------------------------------- /src/page_object/elements/div.py: -------------------------------------------------------------------------------- 1 | from .element import Element 2 | 3 | 4 | class Div(Element): 5 | """ 6 | Element class to represent HTML Div 7 | """ 8 | -------------------------------------------------------------------------------- /src/page_object/elements/label.py: -------------------------------------------------------------------------------- 1 | from .element import Element 2 | 3 | 4 | class Label(Element): 5 | """ 6 | Element class to represent HTML Label 7 | """ 8 | -------------------------------------------------------------------------------- /src/page_object/elements/link.py: -------------------------------------------------------------------------------- 1 | from .element import Element 2 | 3 | 4 | class Link(Element): 5 | """ 6 | Element class to represent HTML Link 7 | """ 8 | -------------------------------------------------------------------------------- /src/page_object/elements/button.py: -------------------------------------------------------------------------------- 1 | from .element import Element 2 | 3 | 4 | class Button(Element): 5 | """ 6 | Element class to represent HTML Button 7 | """ 8 | -------------------------------------------------------------------------------- /src/page_object/elements/option.py: -------------------------------------------------------------------------------- 1 | from .element import Element 2 | 3 | 4 | class Option(Element): 5 | """ 6 | Element class to represent HTML Option 7 | """ 8 | -------------------------------------------------------------------------------- /src/page_object/elements/table_cell.py: -------------------------------------------------------------------------------- 1 | from .element import Element 2 | 3 | 4 | class TableCell(Element): 5 | """ 6 | Element class to represent HTML td 7 | """ 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | services: 4 | - docker 5 | 6 | before_install: 7 | - docker build -t page_object . 8 | 9 | script: 10 | docker run page_object 11 | -------------------------------------------------------------------------------- /src/page_object/elements/heading.py: -------------------------------------------------------------------------------- 1 | from .element import Element 2 | 3 | 4 | class Heading(Element): 5 | """ 6 | Element class to represent HTML headings 7 | """ 8 | -------------------------------------------------------------------------------- /src/page_object/elements/paragraph.py: -------------------------------------------------------------------------------- 1 | from .element import Element 2 | 3 | 4 | class Paragraph(Element): 5 | """ 6 | Element class to represent HTML Paragraph 7 | """ 8 | -------------------------------------------------------------------------------- /src/page_object/elements/radio_button.py: -------------------------------------------------------------------------------- 1 | from .element import Element 2 | 3 | 4 | class RadioButton(Element): 5 | """ 6 | Element class to represent HTML Radio Button 7 | """ 8 | -------------------------------------------------------------------------------- /src/page_object/elements/span.py: -------------------------------------------------------------------------------- 1 | from page_object.elements.element import Element 2 | 3 | 4 | class Span(Element): 5 | """ 6 | Element class to represent HTML Span 7 | """ 8 | -------------------------------------------------------------------------------- /src/page_object/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from page_object.page_object import PageObject 3 | from page_object.page_factory import on, visit 4 | from page_object.browser import Browser 5 | -------------------------------------------------------------------------------- /src/page_object/expected_conditions/element_predicate.py: -------------------------------------------------------------------------------- 1 | class element_predicate(): 2 | 3 | def __init__(self, element, predicate): 4 | self.element = element 5 | self.predicate = predicate 6 | 7 | def __call__(self, drivers=None): 8 | return getattr(self.element, self.predicate)() 9 | -------------------------------------------------------------------------------- /features/steps/div_steps.py: -------------------------------------------------------------------------------- 1 | @when(u'I get the text from the div') 2 | def step_impl(context): 3 | context.expected_text = context.page.div_id() 4 | 5 | 6 | @when(u'I search for the div by "{how}"') 7 | def step_impl(context, how): 8 | method = 'div_{0}'.format(how) 9 | context.expected_text = getattr(context.page, method)() 10 | -------------------------------------------------------------------------------- /features/steps/span_steps.py: -------------------------------------------------------------------------------- 1 | @when(u'I get the text from the span') 2 | def step_impl(context): 3 | context.expected_text = context.page.span_id() 4 | 5 | 6 | @when(u'I search for the span by "{how}"') 7 | def step_impl(context, how): 8 | method = 'span_{0}'.format(how) 9 | context.expected_text = getattr(context.page, method)() 10 | -------------------------------------------------------------------------------- /features/steps/label_steps.py: -------------------------------------------------------------------------------- 1 | @when(u'I get the text from the label') 2 | def step_impl(context): 3 | context.expected_text = context.page.label_id() 4 | 5 | 6 | @when(u'I search for the label by "{how}"') 7 | def step_impl(context, how): 8 | method = 'label_{0}'.format(how) 9 | context.expected_text = getattr(context.page, method)() 10 | -------------------------------------------------------------------------------- /specs/accessors/__init__.py: -------------------------------------------------------------------------------- 1 | from page_object import PageObject 2 | 3 | 4 | class FakeTestPage(PageObject): 5 | 6 | def define_elements(self): 7 | self.divs(name='test_plural', identifier={'css': 'test'}) 8 | self.div(name='test_div', identifier={'css': 'div'}) 9 | self.table(name='test_table', identifier={'css': 'table'}) 10 | -------------------------------------------------------------------------------- /src/page_object/elements/image.py: -------------------------------------------------------------------------------- 1 | from .element import Element 2 | 3 | 4 | class Image(Element): 5 | """ 6 | Element class to represent HTML Image 7 | """ 8 | 9 | @property 10 | def width(self): 11 | return self.attribute('width') 12 | 13 | @property 14 | def height(self): 15 | return self.attribute('height') 16 | -------------------------------------------------------------------------------- /features/steps/paragraph_steps.py: -------------------------------------------------------------------------------- 1 | @when(u'I get the text from the paragraph') 2 | def step_impl(context): 3 | context.expected_text = context.page.paragraph_id() 4 | 5 | 6 | @when(u'I search for the paragraph by "{how}"') 7 | def step_impl(context, how): 8 | method = 'paragraph_{0}'.format(how) 9 | context.expected_text = getattr(context.page, method)() 10 | -------------------------------------------------------------------------------- /specs/page_factory/__init__.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from mock import MagicMock 3 | from page_object import PageObject, Browser 4 | 5 | fake = MagicMock() 6 | 7 | 8 | class FakeTestPage(PageObject): 9 | 10 | def define_elements(self): 11 | self.page_url('www.noop.com') 12 | 13 | 14 | class MagicPage(PageObject): 15 | 16 | def __new__(self, cls): 17 | return fake 18 | -------------------------------------------------------------------------------- /features/elements/elements.feature: -------------------------------------------------------------------------------- 1 | Feature: Generic Element Behavior 2 | 3 | Background: 4 | Given I am on the static elements page 5 | 6 | Scenario: Elements enabled? 7 | When I check an enabled button 8 | Then it should know it is enabled 9 | And it should know that is it not disabled 10 | When I check a disabled button 11 | Then it should know it is not enabled 12 | And it should know that it is disabled 13 | -------------------------------------------------------------------------------- /specs/accessors/table_accessor_test.py: -------------------------------------------------------------------------------- 1 | from hamcrest import assert_that, equal_to, instance_of 2 | from page_object import on 3 | from .. import BaseTestCase 4 | from . import FakeTestPage 5 | from mock import patch 6 | 7 | 8 | class TestTable(BaseTestCase): 9 | 10 | def test_element_method(self): 11 | table = on(FakeTestPage).test_table_element() 12 | self.fake_browser.find_element.assert_called_once_with('css selector', 'table') 13 | -------------------------------------------------------------------------------- /src/page_object/elements/table.py: -------------------------------------------------------------------------------- 1 | from .element import Element 2 | 3 | 4 | class Table(Element): 5 | """ 6 | Element class to represent HTML Table 7 | """ 8 | 9 | def __iter__(self): 10 | return iter(self.row_elements({'css': 'tbody tr'})) 11 | 12 | def find_row(self, predicate=None): 13 | if predicate is None: 14 | raise Exception 15 | return next(row for row in self if predicate(row) is True) 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: specs features test clean install build push release 2 | 3 | specs: 4 | py.test -rwx -s specs/ 5 | 6 | features: 7 | cd features/ && \ 8 | behave 9 | 10 | clean: 11 | rm -rf dist/ 12 | rm -rf build/ 13 | 14 | install: 15 | pip install -r requirements.pip 16 | 17 | build: 18 | python setup.py sdist bdist_wheel egg_info 19 | 20 | push: 21 | twine upload dist/* 22 | 23 | release: clean install build push 24 | 25 | test: specs features 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM selenium/standalone-chrome:3.6.0 2 | 3 | USER root 4 | 5 | RUN apt-get update && \ 6 | apt-get install -y python3.5 python3-pip 7 | 8 | WORKDIR app 9 | 10 | COPY src src 11 | COPY features features 12 | COPY specs specs 13 | COPY setup.py setup.py 14 | COPY requirements.pip requirements.pip 15 | COPY Makefile Makefile 16 | 17 | RUN pip3 install -r requirements.pip 18 | 19 | ENV BROWSER=HEADLESS 20 | 21 | CMD ["make", "test"] 22 | 23 | 24 | -------------------------------------------------------------------------------- /features/steps/shared_element_steps.py: -------------------------------------------------------------------------------- 1 | from hamcrest import assert_that, equal_to, contains_string 2 | 3 | 4 | @then(u'the text should be "{expected_text}"') 5 | def step_impl(context, expected_text): 6 | assert_that(context.expected_text, equal_to(expected_text)) 7 | 8 | 9 | @then(u'I should be on the success page') 10 | def step_impl(context): 11 | assert_that(context.page.html, contains_string('Success')) 12 | assert_that(context.page.title, equal_to('Success')) 13 | -------------------------------------------------------------------------------- /src/page_object/expected_conditions/element_exists.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.remote.webelement import WebElement 2 | 3 | 4 | class element_exists(): 5 | """ 6 | Expected Condition for waiting for element to exists. 7 | """ 8 | 9 | def __init__(self, element): 10 | self.element = element 11 | 12 | def __call__(self, drivers=None): 13 | if isinstance(self.element, WebElement): 14 | return True 15 | 16 | return self.element.exists() 17 | -------------------------------------------------------------------------------- /features/steps/link_steps.py: -------------------------------------------------------------------------------- 1 | from hamcrest import assert_that 2 | 3 | 4 | @when(u'I select the link labeled "{link_text}"') 5 | def step_impl(context, link_text): 6 | context.page.google_search_id() 7 | 8 | 9 | @when(u'I search for the link by "{how}"') 10 | def step_impl(context, how): 11 | method = 'google_search_{0}_element'.format(how) 12 | context.link = getattr(context.page, method)() 13 | 14 | 15 | @then(u'I should be able to select the link') 16 | def step_impl(context): 17 | context.link.click() 18 | -------------------------------------------------------------------------------- /features/elements/span.feature: -------------------------------------------------------------------------------- 1 | Feature: Span Element 2 | 3 | Background: 4 | Given I am on the static elements page 5 | 6 | Scenario: Getting the text from a span 7 | When I get the text from the span 8 | Then the text should be "My alert" 9 | 10 | Scenario Outline: Locating spans on the page 11 | When I search for the span by "" 12 | Then the text should be "My alert" 13 | Examples: 14 | | search_by | 15 | | id | 16 | | class | 17 | | tag | 18 | | css | 19 | -------------------------------------------------------------------------------- /src/page_object/elements/substitute_web_element.py: -------------------------------------------------------------------------------- 1 | class SubstituteWebElement(): 2 | 3 | def __init__(self, locator, identifier, element_type): 4 | self.locator = locator 5 | self.identifier = identifier 6 | self.element_type = element_type 7 | 8 | def exists(self): 9 | element = self._attempt_to_locate() 10 | return not isinstance(element.element, SubstituteWebElement) 11 | 12 | def _attempt_to_locate(self): 13 | return self.locator.element_for(self.element_type, self.identifier) 14 | -------------------------------------------------------------------------------- /features/elements/label.feature: -------------------------------------------------------------------------------- 1 | Feature: Label Element 2 | 3 | Background: 4 | Given I am on the static elements page 5 | 6 | Scenario: Getting the text from a label 7 | When I get the text from the label 8 | Then the text should be "page-object is the best!" 9 | 10 | Scenario Outline: Locating labels on the page 11 | When I search for the label by "" 12 | Then the text should be "page-object is the best!" 13 | 14 | Examples: 15 | | search_by | 16 | | id | 17 | | class | 18 | | css | 19 | -------------------------------------------------------------------------------- /features/elements/link_element.feature: -------------------------------------------------------------------------------- 1 | Feature: Link Element 2 | 3 | Background: 4 | Given I am on the static elements page 5 | 6 | Scenario: Selecting a link 7 | When I select the link labeled "Google Search" 8 | Then I should be on the success page 9 | 10 | Scenario Outline: Locating links on the Page 11 | When I search for the link by "" 12 | Then I should be able to select the link 13 | Examples: 14 | | search_by | 15 | | id | 16 | | class | 17 | | css | 18 | | tag | 19 | -------------------------------------------------------------------------------- /features/elements/div.feature: -------------------------------------------------------------------------------- 1 | Feature: Div Element 2 | 3 | Background: 4 | Given I am on the static elements page 5 | 6 | Scenario: Getting the text from a div 7 | When I get the text from the div 8 | Then the text should be "page-object rocks!" 9 | 10 | Scenario Outline: Locating divs on the page 11 | When I search for the div by "" 12 | Then the text should be "page-object rocks!" 13 | Examples: 14 | | search_by | 15 | | id | 16 | | class | 17 | | tag | 18 | | css | 19 | -------------------------------------------------------------------------------- /src/page_object/elements/checkbox.py: -------------------------------------------------------------------------------- 1 | from .element import Element 2 | 3 | 4 | class CheckBox(Element): 5 | """ 6 | Element class to represent an HTML Checkbox 7 | 8 | Should maybe have a method to set the value to 'checked' or 'unchecked' 9 | """ 10 | 11 | def check(self): 12 | if(not self.element.is_selected()): 13 | self.element.click() 14 | 15 | def uncheck(self): 16 | if(self.element.is_selected()): 17 | self.element.click() 18 | 19 | def is_checked(self): 20 | return self.element.is_selected() 21 | -------------------------------------------------------------------------------- /features/elements/paragraph.feature: -------------------------------------------------------------------------------- 1 | Feature: Paragraph Element 2 | 3 | Background: 4 | Given I am on the static elements page 5 | 6 | Scenario: Getting the text from a paragraph 7 | When I get the text from the paragraph 8 | Then the text should be "Static Elements Page" 9 | 10 | Scenario Outline: Locating divs on the page 11 | When I search for the paragraph by "" 12 | Then the text should be "Static Elements Page" 13 | Examples: 14 | | search_by | 15 | | id | 16 | | class | 17 | | tag | 18 | | css | 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="page_object", 5 | version="0.3.4", 6 | description="Python implementation of the PageObject pattern", 7 | author="Onshift Inc.", 8 | author_email="dev@onshift.com", 9 | url="https://github.com/OnShift/page_object", 10 | license="Apache 2.0", 11 | keywords=['page_object', 'selenium', 'webdriver'], 12 | packages=find_packages('src'), 13 | package_dir={'': 'src'}, 14 | install_requires=[ 15 | 'pyvirtualdisplay==0.2', 16 | 'selenium==3.4.3' 17 | ] 18 | ) 19 | -------------------------------------------------------------------------------- /features/support/async_page.py: -------------------------------------------------------------------------------- 1 | from page_object import PageObject 2 | 3 | 4 | class AsyncPage(PageObject): 5 | 6 | def define_elements(self): 7 | self.button(name='target', identifier={'id': 'target'}) 8 | self.button(name='enable', identifier={'id': 'enable'}) 9 | self.button(name='disable', identifier={'id': 'disable'}) 10 | self.button(name='create', identifier={'id': 'create'}) 11 | self.button(name='hide', identifier={'id': 'hide'}) 12 | self.button(name='unhide', identifier={'id': 'unhide'}) 13 | self.button(name='new', identifier={'id': 'new'}) 14 | -------------------------------------------------------------------------------- /features/elements/image.feature: -------------------------------------------------------------------------------- 1 | Feature: Image Element 2 | 3 | Background: 4 | Given I am on the static elements page 5 | 6 | Scenario: Getting the image element 7 | When I get the image element 8 | Then the image should be "106" pixels wide 9 | And the image should be "106" pixels tall 10 | 11 | Scenario Outline: Locating an image on the page 12 | When I get the image element by "" 13 | Then the image should be "106" pixels wide 14 | And the image should be "106" pixels tall 15 | Examples: 16 | | search_by | 17 | | id | 18 | | class | 19 | | css | 20 | -------------------------------------------------------------------------------- /src/page_object/elements/text_area.py: -------------------------------------------------------------------------------- 1 | from .element import Element 2 | 3 | 4 | class TextArea(Element): 5 | """ 6 | Element class to represent HTML TextArea 7 | """ 8 | 9 | def set_value(self, text): 10 | """ 11 | Set value of text area element 12 | 13 | Args: 14 | text: text to set the value of the text area to. 15 | """ 16 | self.clear() 17 | self.send_keys(text) 18 | 19 | def get_value(self): 20 | """ 21 | Return: 22 | Current value of text field element 23 | """ 24 | return self.attribute('value') 25 | -------------------------------------------------------------------------------- /src/page_object/elements/text_field.py: -------------------------------------------------------------------------------- 1 | from .element import Element 2 | 3 | 4 | class TextField(Element): 5 | """ 6 | Element class to represent HTML TextField 7 | """ 8 | 9 | def set_value(self, text): 10 | """ 11 | Set value of text field element 12 | 13 | Args: 14 | text: text to set the value of the text field. 15 | """ 16 | self.clear() 17 | self.send_keys(text) 18 | 19 | def get_value(self): 20 | """ 21 | Return: 22 | Current value of text field element 23 | """ 24 | return self.attribute('value') 25 | -------------------------------------------------------------------------------- /features/elements/check_box.feature: -------------------------------------------------------------------------------- 1 | Feature: Check Box Element 2 | 3 | Background: 4 | Given I am on the static elements page 5 | 6 | Scenario: Selecting a check box 7 | When I select the First check box 8 | Then the First check box should be selected 9 | When I unselect the First check box 10 | Then the First check box should not be selected 11 | 12 | Scenario Outline: Locating check boxes on the page 13 | When I search for the check box by "" 14 | Then I should be able to check the check box 15 | Examples: 16 | | search_by | 17 | | id | 18 | | class | 19 | | css | 20 | -------------------------------------------------------------------------------- /features/elements/async.feature: -------------------------------------------------------------------------------- 1 | Feature: Async Elements 2 | 3 | Background: 4 | Given I am on the async elements page 5 | 6 | Scenario: Wait for an element to be enabled 7 | Given the button is disabled 8 | When I enable the button 9 | Then I should be able to wait for the button to be enabled 10 | 11 | Scenario: Wait for an element to be present 12 | When I create a new button 13 | Then I should be able to wait for the button to be present 14 | 15 | Scenario: Wait for an element to be visible 16 | Given the button is hidden 17 | When I show the button 18 | Then I should be able to wait for the button to be visible 19 | -------------------------------------------------------------------------------- /src/page_object/browser.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from page_object.browser_factory import BrowserFactory 3 | 4 | 5 | class Browser(): 6 | _factory = None 7 | _browser = None 8 | 9 | @classmethod 10 | def set_browser(cls, browser): 11 | cls._browser = browser 12 | 13 | @classmethod 14 | def selenium_browser(cls): 15 | if cls._browser is None: 16 | cls._browser = cls.factory().selenium_browser() 17 | return cls._browser 18 | 19 | @classmethod 20 | def factory(cls): 21 | if cls._factory is None: 22 | cls._factory = BrowserFactory() 23 | return cls._factory 24 | -------------------------------------------------------------------------------- /features/steps/element_steps.py: -------------------------------------------------------------------------------- 1 | from hamcrest import assert_that, equal_to 2 | 3 | 4 | @then(u'it should know it is enabled') 5 | def step_impl(context): 6 | assert_that(context.element.is_enabled(), equal_to(True)) 7 | 8 | 9 | @then(u'it should know that is it not disabled') 10 | def step_impl(context): 11 | assert_that(context.element.is_disabled(), equal_to(False)) 12 | 13 | 14 | @then(u'it should know it is not enabled') 15 | def step_impl(context): 16 | assert_that(context.element.is_enabled(), equal_to(False)) 17 | 18 | 19 | @then(u'it should know that it is disabled') 20 | def step_impl(context): 21 | assert_that(context.element.is_disabled(), equal_to(True)) 22 | -------------------------------------------------------------------------------- /features/elements/button.feature: -------------------------------------------------------------------------------- 1 | Feature: Button Element 2 | 3 | Background: 4 | Given I am on the static elements page 5 | 6 | Scenario: Clicking a button 7 | When I click the button 8 | Then I should be on the success page 9 | 10 | Scenario Outline: Locating buttons on the page 11 | When I search for the button by "" 12 | Then I should be able to click the button 13 | Examples: 14 | | search_by | 15 | | id | 16 | | class | 17 | | css | 18 | 19 | Scenario: Finding a button dynamically 20 | When I find a button while the script is executing 21 | Then I should see that the button exists 22 | And I should be able to click the button 23 | -------------------------------------------------------------------------------- /features/steps/navigation_steps.py: -------------------------------------------------------------------------------- 1 | from support.page import Page 2 | from support.async_page import AsyncPage 3 | from page_object import Browser 4 | import os 5 | 6 | 7 | @given(u'I am on the static elements page') 8 | def step_impl(context): 9 | path = os.path.abspath("support/html/static_elements.html") 10 | context.page = Page(Browser.selenium_browser()) 11 | context.page.navigate_to('file://{0}'.format(path)) 12 | context.page.define_elements() 13 | 14 | 15 | @given(u'I am on the async elements page') 16 | def step_impl(context): 17 | path = os.path.abspath("support/html/async_elements.html") 18 | context.page = AsyncPage(Browser.selenium_browser()) 19 | context.page.navigate_to('file://{0}'.format(path)) 20 | context.page.define_elements() 21 | -------------------------------------------------------------------------------- /features/steps/image_steps.py: -------------------------------------------------------------------------------- 1 | from hamcrest import assert_that, equal_to 2 | 3 | 4 | @when(u'I get the image element') 5 | def step_impl(context): 6 | context.element = context.page.image_id_element() 7 | 8 | 9 | @when(u'I get the image element by "{how}"') 10 | def step_impl(context, how): 11 | method = 'image_{0}_element'.format(how) 12 | context.element = getattr(context.page, method)() 13 | 14 | 15 | @then(u'the image should be "{expected_pixels}" pixels wide') 16 | def step_impl(context, expected_pixels): 17 | assert_that(context.element.width, equal_to(expected_pixels)) 18 | 19 | 20 | @then(u'the image should be "{expected_pixels}" pixels tall') 21 | def step_impl(context, expected_pixels): 22 | assert_that(context.element.height, equal_to(expected_pixels)) 23 | -------------------------------------------------------------------------------- /specs/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from unittest import TestCase 4 | from mock import MagicMock 5 | from page_object import PageObject, Browser 6 | 7 | fake = MagicMock() 8 | 9 | 10 | class MagicPage(PageObject): 11 | 12 | def __new__(self, cls): 13 | return fake 14 | 15 | 16 | class BaseTestCase(TestCase): 17 | fake_browser = None 18 | 19 | def setup_method(self, method): 20 | self.fake_browser = self.create_mock() 21 | Browser.set_browser(self.fake_browser) 22 | 23 | def create_mock(self, values=None): 24 | mock = MagicMock() 25 | if values is not None: 26 | self.configure_mock(mock, **values) 27 | return mock 28 | 29 | def configure_mock(self, mock, values): 30 | mock.configure_mock(**values) 31 | -------------------------------------------------------------------------------- /features/elements/text_field.feature: -------------------------------------------------------------------------------- 1 | Feature: TextField Element 2 | 3 | Background: 4 | Given I am on the static elements page 5 | 6 | Scenario: Setting and getting a value from a text field 7 | When I type "abcDEF" into the text field 8 | Then the text field should contain "abcDEF" 9 | 10 | Scenario: Clear text field vlaue 11 | When I type "abcDEF" into the text field 12 | Then the text field should contain "abcDEF" 13 | When I clear the text field 14 | Then the text field should contain " " 15 | 16 | Scenario Outline: Locating text fields on the Page 17 | When I search for the text field by "" 18 | Then I should be able to type "I found it" into the field 19 | Examples: 20 | | search_by | 21 | | id | 22 | | class | 23 | | css | 24 | -------------------------------------------------------------------------------- /specs/accessors/plural_accessor_test.py: -------------------------------------------------------------------------------- 1 | from hamcrest import assert_that, equal_to, instance_of 2 | from page_object import on, PageObject 3 | from page_object.locator_generator import LocatorGenerator 4 | from page_object.elements.div import Div 5 | from .. import BaseTestCase 6 | from . import FakeTestPage 7 | from mock import patch 8 | 9 | 10 | class TestPluralAccessors(BaseTestCase): 11 | 12 | @patch('page_object.locator.Locator.divs_for') 13 | def test_define_plural_accessor_method(self, locator): 14 | elements = on(FakeTestPage).test_plural_elements() 15 | locator.assert_called_once_with({'css': 'test'}) 16 | 17 | def test_plural_accessor_methods(self): 18 | page = on(FakeTestPage) 19 | for tag in LocatorGenerator.ELEMENTS: 20 | assert_that('{0}s'.format(tag) in dir(page)) 21 | -------------------------------------------------------------------------------- /src/page_object/elements/table_row.py: -------------------------------------------------------------------------------- 1 | from .element import Element 2 | 3 | 4 | class TableRow(Element): 5 | """ 6 | Element class to represent HTML tr 7 | """ 8 | 9 | def __iter__(self): 10 | """ 11 | Return all cells in row as an iterable 12 | """ 13 | return iter(self.cell_elements({'css': 'th,td'})) 14 | 15 | def get_cells(self, identifier, operation=None): 16 | """ 17 | Method to retrieve row cells 18 | 19 | Args: 20 | identifier: selector for table cells 21 | operation: lambda to perform on each cell element 22 | 23 | Return: 24 | Mapped cells filtered by identifier 25 | """ 26 | if operation is None: 27 | def operation(obj): return obj 28 | 29 | return [operation(cell) for cell in self.cell_elements(identifier)] 30 | -------------------------------------------------------------------------------- /features/elements/text_area.feature: -------------------------------------------------------------------------------- 1 | Feature: Text Area Element 2 | 3 | Background: 4 | Given I am on the static elements page 5 | 6 | Scenario: Setting and getting a value from a text area 7 | When I type "abcdefghijklmnop" into the text area 8 | Then the text area should contain "abcdefghijklmnop" 9 | 10 | Scenario Outline: Locating text area on the Page 11 | When I search for the text area by "" 12 | Then I should be able to type "I found it" into the area 13 | Examples: 14 | | search_by | 15 | | id | 16 | | class | 17 | | css | 18 | 19 | Scenario: Clearing the text area 20 | When I type "abcdefghijklmnop" into the text area 21 | Then the text area should contain "abcdefghijklmnop" 22 | When I clear the text area 23 | Then the text area should contain " " 24 | -------------------------------------------------------------------------------- /src/page_object/elements/select_list.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.support.select import Select 2 | from selenium.webdriver.support.ui import WebDriverWait 3 | from .element import Element 4 | from .option import Option 5 | 6 | 7 | class SelectList(Select, Element): 8 | """ 9 | Element class to represent HTML Select List 10 | """ 11 | 12 | def __init__(self, element): 13 | Select.__init__(self, element) 14 | Element.__init__(self, element) 15 | 16 | def selected(self): 17 | return Option(self.first_selected_option) 18 | 19 | def text(self): 20 | return self.selected().text() 21 | 22 | def value(self): 23 | return self.selected().attribute('value') 24 | 25 | def select(self, text): 26 | return self.select_by_visible_text(text) 27 | 28 | def options_text(self): 29 | return [option.text for option in self.options] 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PageObject 2 | 3 | [![Build Status](https://travis-ci.org/OnShift/page_object.svg?branch=master)](https://travis-ci.org/OnShift/page_object) 4 | 5 | PageObject implementation in Python 6 | 7 | The project [wiki](https://github.com/OnShift/page_object/wiki) 8 | is the first place to go to learn about how to use PageObject. 9 | 10 | This project was inspired by [Jeff Morgan's](https://github.com/cheezy) ruby implementation of this pattern. [page-object](https://github.com/cheezy/page-object) 11 | 12 | ## Known Issues 13 | 14 | See [https://github.com/OnShift/page_object/issues](https://github.com/OnShift/page_object/issues) 15 | 16 | ## Contribute 17 | 18 | * Fork the project. 19 | * Test drive your feature addition or bug fix. 20 | * Make sure you describe your new feature with a behave scenario. 21 | * Provide documentation where needed. 22 | 23 | ## Copyright 24 | 25 | See LICENSE for details. 26 | -------------------------------------------------------------------------------- /src/page_object/locator_generator.py: -------------------------------------------------------------------------------- 1 | class LocatorGenerator: 2 | """ 3 | Class to generate all locator functions 4 | """ 5 | 6 | ELEMENTS = ['select_list', 7 | 'option', 8 | 'image', 9 | 'table', 10 | 'row', 11 | 'cell', 12 | 'div', 13 | 'span', 14 | 'paragraph', 15 | 'label', 16 | 'text_area', 17 | 'text_field', 18 | 'link', 19 | 'heading', 20 | 'button', 21 | 'radio_button', 22 | 'checkbox'] 23 | 24 | def generate_locators(self): 25 | for tag in self.ELEMENTS: 26 | setattr(self, "{}_element".format(tag), getattr(self.locator, "{0}_for".format(tag))) 27 | setattr(self, "{}_elements".format(tag), getattr(self.locator, "{0}s_for".format(tag))) 28 | -------------------------------------------------------------------------------- /specs/page_factory/on_page_test.py: -------------------------------------------------------------------------------- 1 | from hamcrest import assert_that, equal_to 2 | from page_object import on, PageObject 3 | from .. import BaseTestCase 4 | from . import MagicPage, FakeTestPage 5 | 6 | 7 | class TestOn(BaseTestCase): 8 | 9 | def test_creates_instance(self): 10 | page = on(FakeTestPage) 11 | assert_that(isinstance(page, FakeTestPage), equal_to(True)) 12 | assert_that(isinstance(page, PageObject), equal_to(True)) 13 | 14 | def test_calls_define_elements(self): 15 | page = on(MagicPage) 16 | page.define_elements.assert_called_with() 17 | 18 | def test_calls_goto(self): 19 | expect_url_params = {} 20 | page = on(MagicPage, visit_page=True, url_params=expect_url_params) 21 | page.goto.assert_called_with(expect_url_params) 22 | 23 | def test_calls_initalize_page(self): 24 | page = on(MagicPage) 25 | page.initialize_page.assert_called_with() 26 | -------------------------------------------------------------------------------- /features/steps/text_field_steps.py: -------------------------------------------------------------------------------- 1 | from hamcrest import assert_that, equal_to 2 | 3 | 4 | @when(u'I search for the text field by "{how}"') 5 | def step_impl(context, how): 6 | method = 'text_field_{0}_element'.format(how) 7 | context.element = getattr(context.page, method)() 8 | 9 | 10 | @when(u'I type "{text}" into the text field') 11 | def step_impl(context, text): 12 | context.page.set_text_field_id(text) 13 | 14 | 15 | @when(u'I clear the text field') 16 | def step_impl(context): 17 | context.page.text_field_id_element().clear() 18 | 19 | 20 | @then(u'the text field should contain "{expected_text}"') 21 | def step_impl(context, expected_text): 22 | assert_that(context.page.text_field_id(), equal_to(expected_text.strip())) 23 | 24 | 25 | @then(u'I should be able to type "{text}" into the field') 26 | def step_impl(context, text): 27 | context.element.set_value(text) 28 | assert_that(context.element.get_value(), equal_to(text)) 29 | -------------------------------------------------------------------------------- /features/elements/select_list.feature: -------------------------------------------------------------------------------- 1 | Feature: Select List Element 2 | 3 | Background: 4 | Given I am on the static elements page 5 | 6 | Scenario: Selecting an element on the select list 7 | When I select "Test 2" from the select list 8 | Then the current item should be "Test 2" 9 | 10 | Scenario Outline: Locating select lists on the Page 11 | When I search for the select list by "" 12 | Then I should be able to select "Test 2" 13 | And the text for the selected item should be "Test 2" 14 | And the value for the option should be "option2" 15 | Examples: 16 | | search_by | 17 | | id | 18 | | css | 19 | | class | 20 | | tag | 21 | 22 | Scenario: Iterating through the options in the select list 23 | When I search for the select list by "id" 24 | Then option "1" should contain "Test 1" 25 | And option "2" should contain "Test 2" 26 | And each option should contain "Test" 27 | -------------------------------------------------------------------------------- /features/steps/text_area_steps.py: -------------------------------------------------------------------------------- 1 | from hamcrest import assert_that, equal_to 2 | 3 | 4 | @when(u'I type "{text}" into the text area') 5 | def step_impl(context, text): 6 | context.page.set_text_area_id(text) 7 | 8 | 9 | @when(u'I search for the text area by "{how}"') 10 | def step_impl(context, how): 11 | method = 'text_area_{0}_element'.format(how) 12 | context.text_area = getattr(context.page, method)() 13 | 14 | 15 | @when(u'I clear the text area') 16 | def step_impl(context): 17 | context.page.text_area_id_element().clear() 18 | 19 | 20 | @then(u'I should be able to type "{text}" into the area') 21 | def step_impl(context, text): 22 | context.text_area.set_value(text) 23 | assert_that(context.text_area.get_value(), equal_to(text.strip())) 24 | 25 | 26 | @then(u'the text area should contain "{text}"') 27 | def step_impl(context, text): 28 | text_area = context.page.text_area_id_element() 29 | assert_that(text_area.get_value(), equal_to(text.strip())) 30 | -------------------------------------------------------------------------------- /features/steps/check_box_steps.py: -------------------------------------------------------------------------------- 1 | from hamcrest import assert_that, equal_to 2 | 3 | 4 | @when(u'I select the First check box') 5 | def step_impl(context): 6 | context.page.check_cb_id() 7 | 8 | 9 | @when(u'I unselect the First check box') 10 | def step_impl(context): 11 | context.page.uncheck_cb_id() 12 | 13 | 14 | @when(u'I search for the check box by "{how}"') 15 | def step_impl(context, how): 16 | method = 'cb_{0}_element'.format(how) 17 | context.element = getattr(context.page, method)() 18 | 19 | 20 | @then(u'the First check box should be selected') 21 | def step_impl(context): 22 | assert_that(context.page.is_cb_id_checked(), equal_to(True)) 23 | 24 | 25 | @then(u'the First check box should not be selected') 26 | def step_impl(context): 27 | assert_that(context.page.is_cb_id_checked(), equal_to(False)) 28 | 29 | 30 | @then(u'I should be able to check the check box') 31 | def step_impl(context): 32 | context.element.check() 33 | assert_that(context.element.is_checked(), equal_to(True)) 34 | -------------------------------------------------------------------------------- /src/page_object/elements/__init__.py: -------------------------------------------------------------------------------- 1 | from page_object.elements.substitute_web_element import SubstituteWebElement 2 | from page_object.elements.table import Table 3 | from page_object.elements.table_row import TableRow 4 | from page_object.elements.table_cell import TableCell 5 | from page_object.elements.div import Div 6 | from page_object.elements.span import Span 7 | from page_object.elements.paragraph import Paragraph 8 | from page_object.elements.label import Label 9 | from page_object.elements.image import Image 10 | from page_object.elements.select_list import SelectList 11 | from page_object.elements.option import Option 12 | from page_object.elements.text_area import TextArea 13 | from page_object.elements.text_field import TextField 14 | from page_object.elements.link import Link 15 | from page_object.elements.heading import Heading 16 | from page_object.elements.button import Button 17 | from page_object.elements.radio_button import RadioButton 18 | from page_object.elements.checkbox import CheckBox 19 | from page_object.elements.element import Element 20 | -------------------------------------------------------------------------------- /specs/accessors/div_accessor_test.py: -------------------------------------------------------------------------------- 1 | from hamcrest import assert_that, equal_to, instance_of 2 | from page_object import on 3 | from .. import BaseTestCase 4 | from . import FakeTestPage 5 | from mock import patch 6 | 7 | 8 | class TestDiv(BaseTestCase): 9 | 10 | @patch('page_object.locator.Locator.div_for') 11 | def test_element_method(self, mock): 12 | self.element = self.create_mock() 13 | self.configure_mock(mock, {'return_value': self.element}) 14 | 15 | actual_element = on(FakeTestPage).test_div_element() 16 | assert_that(actual_element, equal_to(self.element)) 17 | 18 | @patch('page_object.locator.Locator.div_for') 19 | def test_get_text_method(self, mock): 20 | self.element = self.create_mock() 21 | self.configure_mock(mock, {'return_value': self.element}) 22 | 23 | expected_text = 'I am in a div!' 24 | self.configure_mock(self.element.text, {'return_value': expected_text}) 25 | 26 | actual_text = on(FakeTestPage).test_div() 27 | assert_that(actual_text, equal_to(expected_text)) 28 | -------------------------------------------------------------------------------- /features/steps/button_steps.py: -------------------------------------------------------------------------------- 1 | from hamcrest import assert_that, equal_to 2 | 3 | 4 | @when(u'I click the button') 5 | def step_impl(context): 6 | context.page.button_id() 7 | 8 | 9 | @when(u'I search for the button by "{how}"') 10 | def step_impl(context, how): 11 | method = 'button_{0}_element'.format(how) 12 | context.button = getattr(context.page, method)() 13 | 14 | 15 | @when(u'I find a button while the script is executing') 16 | def step_impl(context): 17 | context.button = context.page.button_element({'id': 'button_id'}) 18 | 19 | 20 | @when(u'I check an enabled button') 21 | def step_impl(context): 22 | context.element = context.page.button_id_element() 23 | 24 | 25 | @when(u'I check a disabled button') 26 | def step_impl(context): 27 | context.element = context.page.disabled_button_element() 28 | 29 | 30 | @then(u'I should be able to click the button') 31 | def step_impl(context): 32 | context.button.click() 33 | 34 | 35 | @then(u'I should see that the button exists') 36 | def step_impl(context): 37 | assert_that(context.button.exists(), equal_to(True)) 38 | -------------------------------------------------------------------------------- /src/page_object/page_factory.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from page_object.browser import Browser 3 | 4 | 5 | def on(page, visit_page=False, url_params={}): 6 | """ 7 | Creates PageObject with current instance of the selenium_browser. 8 | 9 | Args: 10 | page (PageObject): PageObject class name. 11 | visit_page (boolean): Navigate to page. 12 | url_params (dictionary): url parameter object. 13 | 14 | Returns: 15 | New instance of page argument. 16 | """ 17 | page_instance = page(Browser.selenium_browser()) 18 | 19 | if hasattr(page_instance, 'define_elements'): 20 | page_instance.define_elements() 21 | 22 | if visit_page: 23 | page_instance.goto(url_params) 24 | 25 | if hasattr(page_instance, 'initialize_page'): 26 | page_instance.initialize_page() 27 | 28 | return page_instance 29 | 30 | 31 | def visit(page, url_params={}): 32 | """ 33 | Navigate to page url. 34 | 35 | Creates PageObject with current instance of the selenium_browser. 36 | 37 | Args: 38 | page (PageObject): PageObject class name. 39 | url_params (dictionary): url parameter object. 40 | 41 | Returns: 42 | New instance of page argument. 43 | """ 44 | return on(page, visit_page=True, url_params=url_params) 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | /.DS_Store 92 | -------------------------------------------------------------------------------- /src/page_object/browser_factory.py: -------------------------------------------------------------------------------- 1 | import os 2 | from selenium import webdriver 3 | from selenium.webdriver.chrome.options import Options 4 | 5 | 6 | class BrowserFactory(object): 7 | 8 | chrome_options = Options() 9 | chrome_options.add_argument("--no-sandbox") 10 | chrome_options.add_argument("--disable-setuid-sandbox") 11 | chrome_options.add_argument("--window-size=1920,1080") 12 | 13 | drivers = { 14 | "CHROME": "_chrome_driver", 15 | "HEADLESS": "_headless_driver", 16 | "FIREFOX": "_firefox_driver", 17 | "SAFARI": "_safari_driver", 18 | "REMOTE": "_remote_driver" 19 | } 20 | 21 | def selenium_browser(self): 22 | browser = str(os.getenv("BROWSER", "CHROME")) 23 | return getattr(self, self.drivers[browser])() 24 | 25 | def _headless_driver(self): 26 | self.chrome_options.add_argument("--headless") 27 | return self._chrome_driver() 28 | 29 | def _chrome_driver(self): 30 | return webdriver.Chrome(chrome_options=self.chrome_options) 31 | 32 | def _firefox_driver(self): 33 | return webdriver.Firefox() 34 | 35 | def _safari_driver(self): 36 | return webdriver.Safari() 37 | 38 | def _remote_driver(self): 39 | url = str(os.getenv("SELENIUM_URL", "http://localhost:4444/wd/hub")) 40 | capability = str(os.getenv("CAPABILITY", "EDGE")) 41 | return webdriver.Remote(url, getattr(webdriver.DesiredCapabilities, capability)) 42 | -------------------------------------------------------------------------------- /features/steps/async_steps.py: -------------------------------------------------------------------------------- 1 | from hamcrest import assert_that, equal_to 2 | 3 | 4 | @given(u'the button is disabled') 5 | def step_impl(context): 6 | context.page.disable() 7 | context.page.wait(3) 8 | 9 | 10 | @given(u'the button is hidden') 11 | def step_impl(context): 12 | context.page.hide() 13 | context.page.wait(3) 14 | 15 | 16 | @when(u'I enable the button') 17 | def step_impl(context): 18 | context.page.enable() 19 | 20 | 21 | @when(u'I create a new button') 22 | def step_impl(context): 23 | context.page.create() 24 | 25 | 26 | @when(u'I show the button') 27 | def step_impl(context): 28 | context.page.unhide() 29 | 30 | 31 | @then(u'I should be able to wait for the button to be enabled') 32 | def step_impl(context): 33 | element = context.page.target_element() 34 | assert_that(element.is_enabled(), equal_to(False)) 35 | element.when_enabled() 36 | assert_that(element.is_enabled(), equal_to(True)) 37 | 38 | 39 | @then(u'I should be able to wait for the button to be present') 40 | def step_impl(context): 41 | element = context.page.new_element() 42 | assert_that(element.exists(), equal_to(False)) 43 | element.when_present() 44 | assert_that(element.exists(), equal_to(True)) 45 | 46 | 47 | @then(u'I should be able to wait for the button to be visible') 48 | def step_impl(context): 49 | element = context.page.target_element() 50 | assert_that(element.is_visible(), equal_to(False)) 51 | element.when_visible() 52 | assert_that(element.is_visible(), equal_to(True)) 53 | -------------------------------------------------------------------------------- /specs/page_factory/visit_page_test.py: -------------------------------------------------------------------------------- 1 | from hamcrest import assert_that, equal_to 2 | from page_object import visit, PageObject 3 | from .. import BaseTestCase 4 | from . import MagicPage, FakeTestPage 5 | 6 | 7 | class CompleteUrlTestPage(PageObject): 8 | 9 | def define_elements(self): 10 | self.page_url('{base_url}/test_route/{id}?{filter}') 11 | 12 | 13 | class TestVisit(BaseTestCase): 14 | 15 | def set_base_url(self): 16 | import os 17 | os.environ['PAGE_OBJECT_BASE_URL'] = 'www.pageobject.com' 18 | 19 | def test_creates_instance(self): 20 | page = visit(FakeTestPage) 21 | assert_that(isinstance(page, FakeTestPage), equal_to(True)) 22 | assert_that(isinstance(page, PageObject), equal_to(True)) 23 | 24 | def test_should_navigate_to_hardcoded_url(self): 25 | visit(FakeTestPage) 26 | self.fake_browser.get.assert_called_with('www.noop.com') 27 | 28 | def test_builds_url_from_base_url(self): 29 | self.set_base_url() 30 | url_params = {'filter': 'start_date:today', 'id': 18} 31 | visit(CompleteUrlTestPage, url_params=url_params) 32 | 33 | expected_url = 'www.pageobject.com/test_route/18?start_date:today' 34 | self.fake_browser.get.assert_called_with(expected_url) 35 | 36 | def test_calls_define_elements(self): 37 | page = visit(MagicPage) 38 | page.define_elements.assert_called_with() 39 | 40 | def test_calls_goto(self): 41 | expect_url_params = {} 42 | page = visit(MagicPage, url_params=expect_url_params) 43 | page.goto.assert_called_with(expect_url_params) 44 | 45 | def test_calls_initalize_page(self): 46 | page = visit(MagicPage) 47 | page.initialize_page.assert_called_with() 48 | -------------------------------------------------------------------------------- /features/steps/select_list_steps.py: -------------------------------------------------------------------------------- 1 | from hamcrest import assert_that, equal_to 2 | 3 | 4 | @when(u'I select "{option}" from the select list') 5 | def step_impl(context, option): 6 | context.page.set_sel_list_id(option) 7 | 8 | 9 | @when(u'I search for the select list by "{how}"') 10 | def step_impl(context, how): 11 | context.how = how 12 | 13 | 14 | @then(u'the current item should be "{option_text}"') 15 | def step_impl(context, option_text): 16 | assert_that(context.page.get_sel_list_id(), equal_to(option_text)) 17 | 18 | 19 | @then(u'I should be able to select "{option_text}"') 20 | def step_impl(context, option_text): 21 | getattr(context.page, 'set_sel_list_{0}'.format(context.how))(option_text) 22 | 23 | 24 | @then(u'the text for the selected item should be "{option_text}"') 25 | def step_impl(context, option_text): 26 | get_method = 'get_sel_list_{0}'.format(context.how) 27 | selected_text = getattr(context.page, get_method)() 28 | assert_that(selected_text, equal_to(option_text)) 29 | 30 | 31 | @then(u'the value for the option should be "{option_value}"') 32 | def step_impl(context, option_value): 33 | method = 'sel_list_{0}_element'.format(context.how) 34 | select_list = getattr(context.page, method)() 35 | assert_that(select_list.value(), equal_to(option_value)) 36 | 37 | 38 | @then(u'option "{index}" should contain "{option_text}"') 39 | def step_impl(context, index, option_text): 40 | options = context.page.sel_list_id_element().options_text() 41 | assert_that(options[int(index) - 1], equal_to(option_text)) 42 | 43 | 44 | @then(u'each option should contain "{text}"') 45 | def step_impl(context, text): 46 | options = context.page.sel_list_id_element().options_text() 47 | assert_that(all(text in option for option in options)) 48 | -------------------------------------------------------------------------------- /features/support/html/async_elements.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Async Page 4 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/page_object/page_object.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import time 3 | import os 4 | from .accessors import Accessors 5 | from .locator_generator import LocatorGenerator 6 | 7 | 8 | class PageObject(LocatorGenerator, Accessors, object): 9 | """ 10 | Class that when included adds functionality to a page object. 11 | This class will add numerous class and instance methods that you use to define and 12 | interact with web pages. 13 | """ 14 | 15 | def __init__(self, browser): 16 | """ 17 | Args: 18 | browser (selenium.webdriver.browser) : selenium browser instance to use 19 | visit_page (bool) : force browser to navigate to page, defaults to false. 20 | """ 21 | super(PageObject, self).__init__() 22 | self._load_locator(browser) 23 | self.generate_locators() 24 | self.browser = browser 25 | 26 | @property 27 | def base_url(self): 28 | return os.environ.get('PAGE_OBJECT_BASE_URL', None) 29 | 30 | @property 31 | def current_url(self): 32 | """ 33 | Return: 34 | Returns the URL of the current page. 35 | """ 36 | return self.browser.current_url 37 | 38 | @property 39 | def html(self): 40 | """ 41 | Return: 42 | Returns the HTML of the current page. 43 | """ 44 | return self.browser.page_source 45 | 46 | @property 47 | def title(self): 48 | """ 49 | Return: 50 | Returns the title of the current page. 51 | """ 52 | return self.browser.title 53 | 54 | def navigate_to(self, url): 55 | """ 56 | Navigate to url. 57 | 58 | Args: 59 | url (string): Url to navigate to. 60 | """ 61 | self.browser.get(url) 62 | 63 | def wait(self, seconds=5): 64 | """ 65 | Method to wait a given amount of seconds. 66 | Args: 67 | seconds (int): Defaults to 5. 68 | """ 69 | time.sleep(seconds) 70 | 71 | def _load_locator(self, browser): 72 | from page_object.locator import Locator 73 | self.locator = Locator(browser) 74 | -------------------------------------------------------------------------------- /features/support/page.py: -------------------------------------------------------------------------------- 1 | from page_object import PageObject 2 | 3 | 4 | class Page(PageObject): 5 | 6 | def define_elements(self): 7 | self.text_field(name='text_field_id', identifier={'id': 'text_field_id'}) 8 | self.text_field(name='text_field_class', identifier={'class': 'text_field_class'}) 9 | self.text_field(name='text_field_css', identifier={'css': '[data-test-text-field="text_field_css"]'}) 10 | 11 | self.text_area(name='text_area_id', identifier={'id': 'text_area_id'}) 12 | self.text_area(name='text_area_class', identifier={'class': 'text_area_class'}) 13 | self.text_area(name='text_area_css', identifier={'css': '[data-test-text-area="text_area_css"]'}) 14 | 15 | self.select_list(name='sel_list_id', identifier={'id': 'sel_list_id'}) 16 | self.select_list(name='sel_list_class', identifier={'class': 'sel_list_class'}) 17 | self.select_list(name='sel_list_css', identifier={'css': '[data-test-select-list="select_list_css"]'}) 18 | self.select_list(name='sel_list_tag', identifier={'tag': 'select'}) 19 | 20 | self.checkbox(name='cb_id', identifier={'id': 'cb_id'}) 21 | self.checkbox(name='cb_class', identifier={'class': 'cb_class'}) 22 | self.checkbox(name='cb_css', identifier={'css': '[data-test-checkbox="checkbox_css"]'}) 23 | 24 | self.button(name='button_id', identifier={'id': 'button_id'}) 25 | self.button(name='button_class', identifier={'class': 'button_class'}) 26 | self.button(name='button_css', identifier={'css': '[data-test-button="button_css"]'}) 27 | self.button(name='disabled_button', identifier={'id': 'disabled_button'}) 28 | 29 | self.link(name='google_search_id', identifier={'id': 'link_id'}) 30 | self.link(name='google_search_class', identifier={'class': 'link_class'}) 31 | self.link(name='google_search_css', identifier={'css': '[data-test-link="link_css"]'}) 32 | self.link(name='google_search_tag', identifier={'tag': 'a'}) 33 | 34 | self.div(name='div_id', identifier={'id': 'div_id'}) 35 | self.div(name='div_class', identifier={'class': 'div_class'}) 36 | self.div(name='div_css', identifier={'css': '[data-test-div="div_css"]'}) 37 | self.div(name='div_tag', identifier={'tag': 'div'}) 38 | 39 | self.span(name='span_id', identifier={'id': 'span_id'}) 40 | self.span(name='span_class', identifier={'class': 'span_class'}) 41 | self.span(name='span_css', identifier={'css': '[data-test-span="span_css"]'}) 42 | self.span(name='span_tag', identifier={'tag': 'span'}) 43 | 44 | self.paragraph(name='paragraph_id', identifier={'id': 'p_id'}) 45 | self.paragraph(name='paragraph_class', identifier={'class': 'p_class'}) 46 | self.paragraph(name='paragraph_css', identifier={'css': '[data-test-p="p_css"]'}) 47 | self.paragraph(name='paragraph_tag', identifier={'tag': 'p'}) 48 | 49 | self.label(name='label_id', identifier={'id': 'label_id'}) 50 | self.label(name='label_class', identifier={'class': 'label_class'}) 51 | self.label(name='label_css', identifier={'css': '[data-test-label="label_css"]'}) 52 | 53 | self.image(name='image_id', identifier={'id': 'image_id'}) 54 | self.image(name='image_class', identifier={'class': 'image_class'}) 55 | self.image(name='image_css', identifier={'css': '[data-test-image="image_css"]'}) 56 | -------------------------------------------------------------------------------- /src/page_object/elements/element.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.support.ui import WebDriverWait 2 | from selenium.webdriver.common.action_chains import ActionChains 3 | from page_object.locator_generator import LocatorGenerator 4 | from page_object.expected_conditions import element_exists, element_predicate 5 | import os 6 | 7 | 8 | class Element(LocatorGenerator, object): 9 | """ 10 | Base class to represent HTML Elements 11 | """ 12 | 13 | DEFAULT_TIMEOUT = int(os.getenv("DEFAULT_TIMEOUT", 5)) 14 | 15 | def __init__(self, element): 16 | """ 17 | Arg: 18 | element (selenium.webdriver.webelement) 19 | """ 20 | self._load_locator(element) 21 | self.generate_locators() 22 | self.element = element 23 | 24 | def is_enabled(self): 25 | return self.element.is_enabled() 26 | 27 | def is_visible(self): 28 | return self.element.is_displayed() 29 | 30 | def is_disabled(self): 31 | return not self.is_enabled() 32 | 33 | def exists(self): 34 | return element_exists(self.element)() 35 | 36 | def text(self): 37 | """ 38 | Getter for text within the element 39 | """ 40 | return self.element.text 41 | 42 | def clear(self): 43 | """ 44 | Clears the text if its a text entry element. 45 | """ 46 | return self.element.clear() 47 | 48 | def click(self): 49 | """ 50 | Clicks the element. 51 | """ 52 | return self.element.click() 53 | 54 | def hover(self): 55 | """ 56 | Hovers over an element. 57 | """ 58 | # The react-bootstrap overlay trigger doesn't register the move_to_element unless there's 59 | # another mouse action ahead of time. 60 | ActionChains(self.element.parent).move_by_offset(10, 10).move_to_element(self.element).perform() 61 | 62 | def class_name(self): 63 | """ 64 | Gets the given attribute or property of the element. 65 | 66 | Return: 67 | The class for the element 68 | """ 69 | return self.attribute('class') 70 | 71 | def attribute(self, attribute): 72 | """ 73 | Gets the given attribute or property of the element. 74 | 75 | Args: 76 | attribute: name of attribute (e.g. class) 77 | """ 78 | return self.element.get_attribute(attribute) 79 | 80 | def when_present(self, timeout=DEFAULT_TIMEOUT): 81 | """ 82 | Waits for element to be present on the current page. 83 | 84 | Args: 85 | timeout (integer): seconds to wait before throwing exception. 86 | """ 87 | self.wait_until(element_predicate(self, 'exists'), timeout) 88 | 89 | def when_enabled(self, timeout=DEFAULT_TIMEOUT): 90 | """ 91 | Waits for element to be enabled on the current page. 92 | 93 | Args: 94 | timeout (integer): seconds to wait before throwing exception. 95 | """ 96 | self.wait_until(element_predicate(self, 'is_enabled'), timeout) 97 | 98 | def when_visible(self, timeout=DEFAULT_TIMEOUT): 99 | """ 100 | Waits for element to be visible on the current page. 101 | 102 | Args: 103 | timeout (integer): seconds to wait before throwing exception. 104 | """ 105 | self.wait_until(element_predicate(self, 'is_visible'), timeout) 106 | 107 | def wait_until(self, condtion, timeout=DEFAULT_TIMEOUT): 108 | wait = WebDriverWait(self.element, timeout=timeout, poll_frequency=0.1) 109 | wait.until(condtion) 110 | 111 | def send_keys(self, keys): 112 | """ 113 | Simulates key entry into the element. 114 | """ 115 | return self.element.send_keys(keys) 116 | 117 | def _load_locator(self, element): 118 | from page_object.locator import Locator 119 | self.locator = Locator(element) 120 | -------------------------------------------------------------------------------- /src/page_object/locator.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from selenium.webdriver.common.by import By 3 | from selenium.common.exceptions import NoSuchElementException 4 | from page_object.elements import * 5 | 6 | 7 | class Locator(object): 8 | """ 9 | Class to encapsulate all selenium selector behavior. 10 | """ 11 | 12 | def __init__(self, target): 13 | self.target = target 14 | 15 | def select_list_for(self, identifier): 16 | return self.element_for(SelectList, identifier) 17 | 18 | def select_lists_for(self, identifier): 19 | return self.elements_for(SelectList, identifier) 20 | 21 | def option_for(self, identifier): 22 | return self.element_for(Option, identifier) 23 | 24 | def options_for(self, identifier): 25 | return self.elements_for(Option, identifier) 26 | 27 | def button_for(self, identifier): 28 | return self.element_for(Button, identifier) 29 | 30 | def buttons_for(self, identifier): 31 | return self.elements_for(Button, identifier) 32 | 33 | def image_for(self, identifier): 34 | return self.element_for(Image, identifier) 35 | 36 | def images_for(self, identifier): 37 | return self.elements_for(Image, identifier) 38 | 39 | def text_area_for(self, identifier): 40 | return self.element_for(TextArea, identifier) 41 | 42 | def text_areas_for(self, identifier): 43 | return self.elements_for(TextArea, identifier) 44 | 45 | def text_field_for(self, identifier): 46 | return self.element_for(TextField, identifier) 47 | 48 | def text_fields_for(self, identifier): 49 | return self.elements_for(TextField, identifier) 50 | 51 | def div_for(self, identifier): 52 | return self.element_for(Div, identifier) 53 | 54 | def divs_for(self, identifier): 55 | return self.elements_for(Div, identifier) 56 | 57 | def span_for(self, identifier): 58 | return self.element_for(Span, identifier) 59 | 60 | def spans_for(self, identifier): 61 | return self.elements_for(Span, identifier) 62 | 63 | def paragraph_for(self, identifier): 64 | return self.element_for(Paragraph, identifier) 65 | 66 | def paragraphs_for(self, identifier): 67 | return self.elements_for(Paragraph, identifier) 68 | 69 | def label_for(self, identifier): 70 | return self.element_for(Label, identifier) 71 | 72 | def labels_for(self, identifier): 73 | return self.elements_for(Label, identifier) 74 | 75 | def link_for(self, identifier): 76 | return self.element_for(Link, identifier) 77 | 78 | def links_for(self, identifier): 79 | return self.elements_for(Link, identifier) 80 | 81 | def heading_for(self, identifier): 82 | return self.element_for(Heading, identifier) 83 | 84 | def headings_for(self, identifier): 85 | return self.elements_for(Heading, identifier) 86 | 87 | def table_for(self, identifier): 88 | return self.element_for(Table, identifier) 89 | 90 | def tables_for(self, identifier): 91 | return self.elements_for(Table, identifier) 92 | 93 | def cells_for(self, identifier): 94 | return self.elements_for(TableCell, identifier) 95 | 96 | def cell_for(self, identifier): 97 | return self.element_for(TableCell, identifier) 98 | 99 | def rows_for(self, identifier): 100 | return self.elements_for(TableRow, identifier) 101 | 102 | def row_for(self, identifier): 103 | return self.element_for(TableRow, identifier) 104 | 105 | def radio_buttons_for(self, identifier): 106 | return self.elements_for(RadioButton, identifier) 107 | 108 | def radio_button_for(self, identifier): 109 | return self.element_for(RadioButton, identifier) 110 | 111 | def checkbox_for(self, identifier): 112 | return self.element_for(CheckBox, identifier) 113 | 114 | def checkboxs_for(self, identifier): 115 | return self.elements_for(CheckBox, identifier) 116 | 117 | def element_for(self, element_type, identifier): 118 | """ 119 | Method to find a element through selenium 120 | Arg: 121 | element_type (page_object.Element): class to instantiate 122 | identifier (dict): how to identify element and what the value is 123 | 124 | Return: 125 | Returns list of element class for type 126 | """ 127 | how, what = self._parse_identifier(identifier) 128 | try: 129 | element = self.target.find_element(how, what) 130 | except NoSuchElementException: 131 | return Element(SubstituteWebElement(self, identifier, element_type)) 132 | 133 | return element_type(element) 134 | 135 | def elements_for(self, element_type, identifier): 136 | """ 137 | Method to find all elements matching the identifier through selenium 138 | 139 | Arg: 140 | element_type (page_object.Element): class to instantiate 141 | identifier (dict): how to identify element and what the value is 142 | 143 | Return: 144 | Returns list of element class for type 145 | """ 146 | how, what = self._parse_identifier(identifier) 147 | elements = self.target.find_elements(how, what) 148 | return list(map(lambda element: element_type(element), elements)) 149 | 150 | def _parse_identifier(self, identifier): 151 | mapping = { 152 | 'css': By.CSS_SELECTOR, 153 | 'class': By.CLASS_NAME, 154 | 'tag': By.TAG_NAME, 155 | 'id': By.ID 156 | } 157 | key, value = identifier.copy().popitem() 158 | return mapping[key], value 159 | -------------------------------------------------------------------------------- /features/support/html/static_elements.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Static Elements Page 4 | 5 | 6 |

Static Elements Page

7 | 8 | 9 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 30 | 31 | Planets 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 47 | 48 | 49 | 50 | Google Search 51 | 52 | 53 | 54 | 55 | 56 | Milk
57 | Butter 58 | 59 |
60 | 61 | Cheddar 62 | Emmental 63 | Muenster 64 |
65 | 66 |
67 | page-object rocks! 68 |
69 | 70 | 71 | My alert 72 | 73 | 74 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 |
TableHeader
Data1Data2
Data3Data4
96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 |
Col1Col2
Data1Data2
Data3Data4
Data5
118 | 119 |
120 | 121 | 122 | 123 | 124 |
125 | 126 | image_alt 127 | 128 |
129 |
130 | 131 | 132 | 133 | 134 |
    135 |
  • Item One
  • 136 |
  • Item Two
  • 137 |
  • Item Three
  • 138 |
139 | 140 |
    141 |
  1. Number One
  2. 142 |
  3. Number Two
  4. 143 |
  5. Number Three
  6. 144 |
145 | 146 | Hello 147 | Hello 148 | Hello 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 |

h1's are cool

157 |

h2's are cool

158 |

h3's are cool

159 |

h4's are cool

160 |
h5's are cool
161 |
h6's are cool
162 | New Window 163 | Another New Window 164 | 165 | 166 |
167 | 168 |
169 | Success 170 |
171 | 172 |
HTML 5 Article
173 |
HTML 5 Header
174 |
HTML 5 Footer
175 | 176 |
177 | The summary 178 | The details 179 |
180 | 181 |
182 | The Pulpit Rock 183 |
184 | This text have some text in bold 185 | This text have some text in italic 186 | 187 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /src/page_object/accessors.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from page_object.locator_generator import LocatorGenerator 3 | 4 | class Accessors(object): 5 | """ 6 | Contains the methods that are inserted into your page objects when you inherit from the PageObject class. 7 | These methods will generate accessor methods for specfic elements, to interact with elements. 8 | 9 | Args: 10 | name (string): The name you want to refer to the element as. 11 | identifier (dict): Method to identify the element. 12 | 13 | Example: 14 | self.table(name = "example", { 'css' : "[data-test-*='example']" }) 15 | 16 | Will generate: 17 | - self.example_element() 18 | """ 19 | 20 | def __init__(self): 21 | for tag in LocatorGenerator.ELEMENTS: 22 | setattr(self, "{0}s".format(tag), self._define_plural_accessor(tag)) 23 | 24 | def page_url(self, url): 25 | def navigate_to(url_params): 26 | complete_url = url.format(base_url=self.base_url, **url_params) 27 | self.browser.get(complete_url) 28 | 29 | setattr(self, "goto", navigate_to) 30 | 31 | def table(self, name, identifier): 32 | """ 33 | Will generate: 34 | "{name}_element": returns an element 35 | """ 36 | 37 | self._standard_methods(name, identifier, 'table_for') 38 | 39 | def image(self, name, identifier): 40 | """ 41 | Will generate: 42 | "{name}_element": returns an element 43 | """ 44 | self._standard_methods(name, identifier, 'image_for') 45 | 46 | def button(self, name, identifier): 47 | """ 48 | Will generate: 49 | "{name}_element": returns an element 50 | "{name}" : clicks the element 51 | """ 52 | def click(): 53 | return getattr(self, "{0}_element".format(name))().click() 54 | 55 | setattr(self, "{0}".format(name), click) 56 | 57 | self._standard_methods(name, identifier, 'button_for') 58 | 59 | def link(self, name, identifier): 60 | """ 61 | Will generate: 62 | "{name}_element": returns an element 63 | "{name} : clicks the element 64 | """ 65 | 66 | def click_method(): 67 | return getattr(self, "{0}_element".format(name))().click() 68 | 69 | setattr(self, name, click_method) 70 | 71 | self._standard_methods(name, identifier, 'link_for') 72 | 73 | def text_area(self, name, identifier): 74 | """ 75 | Will generate: 76 | "{name}_element": returns an element 77 | "set_{name} : sets text of element 78 | """ 79 | def set_method(value): 80 | return getattr(self, "{0}_element".format(name))().set_value(value) 81 | 82 | setattr(self, "set_{0}".format(name), set_method) 83 | 84 | self._standard_methods(name, identifier, 'text_area_for') 85 | 86 | def text_field(self, name, identifier): 87 | """ 88 | Will generate: 89 | "{name}_element": returns an element 90 | "set_{name} : sets text of element 91 | """ 92 | def set_method(value): 93 | return getattr(self, "{0}_element".format(name))().set_value(value) 94 | 95 | def get_method(): 96 | return getattr(self, "{0}_element".format(name))().get_value() 97 | 98 | setattr(self, "set_{0}".format(name), set_method) 99 | setattr(self, "{0}".format(name), get_method) 100 | 101 | self._standard_methods(name, identifier, 'text_field_for') 102 | 103 | def div(self, name, identifier): 104 | """ 105 | Will generate: 106 | "{name}_element": returns an element 107 | "{name}: : returns text of element 108 | """ 109 | 110 | def text(): 111 | return getattr(self, "{0}_element".format(name))().text() 112 | 113 | setattr(self, "{0}".format(name), text) 114 | 115 | self._standard_methods(name, identifier, 'div_for') 116 | 117 | def span(self, name, identifier): 118 | """ 119 | Will generate: 120 | "{name}_element": returns an element 121 | "{name}: : returns text of element 122 | """ 123 | def text(): 124 | return getattr(self, "{0}_element".format(name))().text() 125 | 126 | setattr(self, "{0}".format(name), text) 127 | 128 | self._standard_methods(name, identifier, 'span_for') 129 | 130 | def paragraph(self, name, identifier): 131 | """ 132 | Will generate: 133 | "{name}_element": returns an element 134 | "{name}: : returns text of element 135 | """ 136 | def text(): 137 | return getattr(self, "{0}_element".format(name))().text() 138 | 139 | setattr(self, "{0}".format(name), text) 140 | 141 | self._standard_methods(name, identifier, 'paragraph_for') 142 | 143 | def label(self, name, identifier): 144 | """ 145 | Will generate: 146 | "{name}_element": returns an element 147 | "{name}: : returns text of element 148 | """ 149 | def text(): 150 | return getattr(self, "{0}_element".format(name))().text() 151 | 152 | setattr(self, "{0}".format(name), text) 153 | 154 | self._standard_methods(name, identifier, 'label_for') 155 | 156 | def h1(self, name, identifier): 157 | """ 158 | Will generate: 159 | "{name}_element": returns an element 160 | "{name}: : returns text of element 161 | """ 162 | 163 | def text(): 164 | return getattr(self, "{0}_element".format(name))().text() 165 | 166 | setattr(self, "{0}".format(name), text) 167 | 168 | self._standard_methods(name, identifier, 'heading_for') 169 | 170 | def h2(self, name, identifier): 171 | """ 172 | Will generate: 173 | "{name}_element": returns an element 174 | "{name}: : returns text of element 175 | """ 176 | 177 | def text(): 178 | return getattr(self, "{0}_element".format(name))().text() 179 | 180 | setattr(self, "{0}".format(name), text) 181 | 182 | self._standard_methods(name, identifier, 'heading_for') 183 | 184 | def h3(self, name, identifier): 185 | """ 186 | Will generate: 187 | "{name}_element": returns an element 188 | "{name}: : returns text of element 189 | """ 190 | 191 | def text(): 192 | return getattr(self, "{0}_element".format(name))().text() 193 | 194 | setattr(self, "{0}".format(name), text) 195 | 196 | self._standard_methods(name, identifier, 'heading_for') 197 | 198 | def h4(self, name, identifier): 199 | """ 200 | Will generate: 201 | "{name}_element": returns an element 202 | "{name}: : returns text of element 203 | """ 204 | 205 | def text(): 206 | return getattr(self, "{0}_element".format(name))().text() 207 | 208 | setattr(self, "{0}".format(name), text) 209 | 210 | self._standard_methods(name, identifier, 'heading_for') 211 | 212 | def h5(self, name, identifier): 213 | """ 214 | Will generate: 215 | "{name}_element": returns an element 216 | "{name}: : returns text of element 217 | """ 218 | 219 | def text(): 220 | return getattr(self, "{0}_element".format(name))().text() 221 | 222 | setattr(self, "{0}".format(name), text) 223 | 224 | self._standard_methods(name, identifier, 'heading_for') 225 | 226 | def h6(self, name, identifier): 227 | """ 228 | Will generate: 229 | "{name}_element": returns an element 230 | "{name}: : returns text of element 231 | """ 232 | 233 | def text(): 234 | return getattr(self, "{0}_element".format(name))().text() 235 | 236 | setattr(self, "{0}".format(name), text) 237 | 238 | self._standard_methods(name, identifier, 'heading_for') 239 | 240 | def select_list(self, name, identifier): 241 | """ 242 | Will generate: 243 | "{name}_element": returns an element 244 | "set_{name}" : sets the option by option text 245 | "{name}_options": returns options text as list of strings 246 | """ 247 | 248 | def set_method(text): 249 | return getattr(self, "{0}_element".format(name))().select(text) 250 | 251 | def get_method(): 252 | return getattr(self, "{0}_element".format(name))().text() 253 | 254 | def options(): 255 | return getattr(self, "{0}_element".format(name))().options_text() 256 | 257 | setattr(self, "set_{0}".format(name), set_method) 258 | setattr(self, "get_{0}".format(name), get_method) 259 | setattr(self, "{0}_options".format(name), options) 260 | 261 | self._standard_methods(name, identifier, 'select_list_for') 262 | 263 | def checkbox(self, name, identifier): 264 | """ 265 | Will generate: 266 | "{name}_element": returns an element 267 | "uncheck_{name}": to uncheck a checked checkbox 268 | "check_{name}" : to check an unchecked checkbox 269 | """ 270 | 271 | def check(): 272 | return getattr(self, "{0}_element".format(name))().check() 273 | 274 | def uncheck(): 275 | return getattr(self, "{0}_element".format(name))().uncheck() 276 | 277 | def is_checked(): 278 | return getattr(self, "{0}_element".format(name))().is_checked() 279 | 280 | setattr(self, "check_{0}".format(name), check) 281 | setattr(self, "uncheck_{0}".format(name), uncheck) 282 | setattr(self, "is_{0}_checked".format(name), is_checked) 283 | 284 | self._standard_methods(name, identifier, 'checkbox_for') 285 | 286 | def _define_plural_accessor(self, tag): 287 | def plural_accessor(name, identifier): 288 | def elements(): 289 | return getattr(self.locator, "{0}s_for".format(tag))(identifier) 290 | 291 | setattr(self, "{0}_elements".format(name), elements) 292 | 293 | return plural_accessor 294 | 295 | def _standard_methods(self, name, identifier, method): 296 | def element(): 297 | return getattr(self.locator, method)(identifier) 298 | 299 | setattr(self, "{}_element".format(name), element) 300 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------