├── .gitignore ├── LICENSE ├── MANIFEST.in ├── PageObjectLibrary ├── __init__.py ├── keywords.py ├── locatormap.py ├── pageobject.py └── version.py ├── README.md ├── TESTING.md ├── demo ├── README.md ├── resources │ ├── HomePage.py │ ├── LoginPage.py │ └── config.py ├── tests │ └── demo.robot └── webapp │ ├── README.md │ ├── demoserver.py │ └── docroot │ ├── about.html │ ├── homepage.html │ ├── login.html │ ├── relogin.py │ └── stylesheet.css ├── doc └── pageobjectlibrary.html ├── requirements.txt ├── setup.py └── tests ├── about.robot ├── acceptance ├── demo.robot ├── library.robot └── resources │ └── config.py └── config.args /.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 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | 64 | # robot cruft 65 | output.xml 66 | log.html 67 | report.html 68 | selenium-screenshot*.png 69 | tests/results 70 | 71 | # emacs cruft 72 | \#*\# 73 | \.\#* 74 | *.elc 75 | *~ 76 | 77 | # misc 78 | tmp 79 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | include setup.py 4 | include PageObjectLibrary/*.py 5 | recursive-exclude PageObjectLibrary *~ #*# 6 | -------------------------------------------------------------------------------- /PageObjectLibrary/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from .keywords import PageObjectLibraryKeywords 4 | from .pageobject import PageObject 5 | from .version import __version__ 6 | 7 | 8 | class PageObjectLibrary(PageObjectLibraryKeywords): 9 | 10 | """This project is hosted on github in the repository 11 | [https://github.com/boakley/robotframework-pageobjectlibrary| 12 | boakley/robotframework-pageobjectlibrary] 13 | 14 | *PageObjectLibrary* is a lightweight library which supports using 15 | the page object pattern with 16 | [http://robotframework.org/SeleniumLibrary/doc/SeleniumLibrary.html|SeleniumLibrary]. 17 | This library does not replace SeleniumLibrary; rather, it 18 | provides a framework around which to use SeleniumLibrary and the 19 | lower-level [http://selenium-python.readthedocs.org/|Python 20 | bindings to Selenium] 21 | 22 | This library provides the following keywords: 23 | 24 | | =Keyword Name= | =Synopsis= | 25 | | Go to page | Goes to the given page in the browser | 26 | | The current page should be | Assert that the given page is displayed in the browser | 27 | | Get page name | Returns the name of the current page | 28 | 29 | PageObjectLibrary provides a PageObject class which should be used 30 | as the base class for other page objects. By inheriting from this 31 | class your keywords have access to the following pre-defined 32 | attributes and methods: 33 | 34 | | =Attribute/method= | =Description= | 35 | | ``self.selib` ` | A reference to the SeleniumLibrary instance | 36 | | ``self.browser`` | A reference to the currently open browser | 37 | | ``self.locator`` | A wrapper around the ``_locators`` dictionary | 38 | | ``self.logger`` | A reference to the ``robot.api.logger`` instance | 39 | | ``self._wait_for_page_refresh()`` | a context manager for doing work that causes a page refresh | 40 | 41 | = Using SeleniumLibrary Keywords = 42 | 43 | Within your keywords you have access to the full power of 44 | SeleniumLibrary. You can use ``self.selib`` to access the 45 | library keywords. The following example shows how to call the 46 | ``Capture Page Screenshot`` keyword: 47 | 48 | | self.selib.capture_page_screenshot() 49 | 50 | = Using Selenium Methods = 51 | 52 | The attribute ``self.browser`` is a reference to a Selenium 53 | webdriver object. With this reference you can call any of the 54 | standard Selenium methods provided by the Selenium library. The 55 | following example shows how to find all link elements on a page: 56 | 57 | | elements = self.browser,find_elements_by_tag_name("a") 58 | 59 | = Creating Page Object Classes = 60 | 61 | Page objects should inherit from PageObjectLibrary.PageObject. At a minimum, 62 | the class should define the following attributes: 63 | 64 | | =Attribute= | =Description= | 65 | | ``PAGE_URL`` | The path to the current page, without the \ 66 | hostname and port (eg: ``/dashboard.html``) | 67 | | ``PAGE_TITLE`` | The web page title. This is used by the \ 68 | default implementation of ``_is_current_page``. | 69 | 70 | When using the keywords `Go To Page` or `The Current Page Should Be`, the 71 | PageObjectLibrary will call the method ``_is_current_page`` of the given page. 72 | By default this will compare the current page title to the ``PAGE_TITLE`` attribute 73 | of the page. If you are working on a site where the page titles are not unique, 74 | you can override this method to do any type of logic you need. 75 | 76 | = Page Objects are Normal Robot Libraries = 77 | 78 | All rules that apply to keyword libraries applies to page objects. For 79 | example, the libraries must be on ``PYTHONPATH``. You may also want to define 80 | ``ROBOT_LIBRARY_SCOPE``. Also, the filename and the classname must be identical (minus 81 | the ``.py`` suffix on the file). 82 | 83 | = Locators = 84 | 85 | When writing multiple keywords for a page, you often use the same locators in 86 | many places. PageObject allows you to define your locators in a dictionary, 87 | but them use them with a more convenient dot notation. 88 | 89 | To define locators, create a dictionary named ``_locators``. You can then access 90 | the locators via dot notation within your keywords as ``self.locator.``. The 91 | ``_locators`` dictionary may have nested dictionaries. 92 | 93 | = Waiting for a Page to be Ready = 94 | 95 | One difficulty with writing Selenium tests is knowing when a page has refreshed. 96 | PageObject provides a context manager named ``_wait_for_page_refresh()`` which can 97 | be used to wrap a command that should result in a page refresh. It will get a 98 | reference to the DOM, run the body of the context manager, and then wait for the 99 | DOM to change before returning. 100 | 101 | = Example Page Object Definition = 102 | 103 | | from PageObjectLibrary import PageObject 104 | | from robot.libraries.BuiltIn import BuiltIn 105 | | 106 | | class LoginPage(PageObject): 107 | | PAGE_TITLE = "Login - PageObjectLibrary Demo" 108 | | PAGE_URL = "/" 109 | | 110 | | _locators = { 111 | | "username": "id=id_username", 112 | | "password": "id=id_password", 113 | | "submit_button": "id=id_submit", 114 | | } 115 | | 116 | | def login_as_a_normal_user(self): 117 | | username = BuiltIn().get_variable_value("${USERNAME}"} 118 | | password = BuiltIn().get_variable_value("${PASSWORD}"} 119 | | self.selib.input_text(self.locator.username, username) 120 | | self.selib.input_text(self.locator.password, password) 121 | | 122 | | with self._wait_for_page_refresh(): 123 | | self.click_the_submit_button() 124 | 125 | = Using the Page Object in a Test = 126 | 127 | To use the above page object in a test, you must make sure that 128 | Robot can import it, just like with any other keyword 129 | library. When you use the keyword `Go to page`, the keyword will 130 | automatically load the keyword library and put it at the front of 131 | the Robot Framework library search order (see 132 | [http://robotframework.org/robotframework/latest/libraries/BuiltIn.html#Set%20Library%20Search%20Order|Set Library Search Order]) 133 | 134 | In the following example it is assumed there is a second page 135 | object named ``DashboardPage`` which the browser is expected to go to 136 | if login is successful. 137 | 138 | | ``*** Settings ***`` 139 | | Library PageObjectLibrary 140 | | Library SeleniumLibrary 141 | | Suite Setup Open browser http://www.example.com 142 | | Suite Teardown Close all browsers 143 | | 144 | | ``*** Test Cases ***`` 145 | | Log in to the application 146 | | Go to page LoginPage 147 | | Log in as a normal user 148 | | The current page should be DashboardPage 149 | 150 | """ 151 | 152 | ROBOT_LIBRARY_SCOPE = "TEST SUITE" 153 | -------------------------------------------------------------------------------- /PageObjectLibrary/keywords.py: -------------------------------------------------------------------------------- 1 | """PageObjectLibrary 2 | 3 | A library to support the creation of page objects using 4 | selenium and SeleniuimLibrary. 5 | 6 | Note: The keywords in this file need to work even if there is no 7 | current page object, which is why they are here instead of on the 8 | PageObject model. 9 | 10 | """ 11 | 12 | from __future__ import print_function, absolute_import, unicode_literals 13 | import six 14 | 15 | import robot.api 16 | from robot.libraries.BuiltIn import BuiltIn 17 | 18 | 19 | from .pageobject import PageObject 20 | try: 21 | from urlparse import urlparse 22 | except ImportError: 23 | from urllib.parse import urlparse 24 | 25 | class PageObjectLibraryKeywords(object): 26 | 27 | ROBOT_LIBRARY_SCOPE = "TEST SUITE" 28 | 29 | def __init__(self): 30 | self.builtin = BuiltIn() 31 | self.logger = robot.api.logger 32 | 33 | def the_current_page_should_be(self, page_name): 34 | """Fails if the name of the current page is not the given page name 35 | 36 | ``page_name`` is the name you would use to import the page. 37 | 38 | This keyword will import the given page object, put it at the 39 | front of the Robot library search order, then call the method 40 | ``_is_current_page`` on the library. The default 41 | implementation of this method will compare the page title to 42 | the ``PAGE_TITLE`` attribute of the page object, but this 43 | implementation can be overridden by each page object. 44 | 45 | """ 46 | 47 | page = self._get_page_object(page_name) 48 | 49 | # This causes robot to automatically resolve keyword 50 | # conflicts by looking in the current page first. 51 | if page._is_current_page(): 52 | # only way to get the current order is to set a 53 | # new order. Once done, if there actually was an 54 | # old order, preserve the old but make sure our 55 | # page is at the front of the list 56 | old_order = self.builtin.set_library_search_order() 57 | new_order = ([str(page)],) + old_order 58 | self.builtin.set_library_search_order(new_order) 59 | return 60 | 61 | # If we get here, we're not on the page we think we're on 62 | raise Exception("Expected page to be %s but it was not" % page_name) 63 | 64 | def go_to_page(self, page_name, page_root=None): 65 | """Go to the url for the given page object. 66 | 67 | Unless explicitly provided, the URL root will be based on the 68 | root of the current page. For example, if the current page is 69 | http://www.example.com:8080 and the page object URL is 70 | ``/login``, the url will be http://www.example.com:8080/login 71 | 72 | == Example == 73 | 74 | Given a page object named ``ExampleLoginPage`` with the URL 75 | ``/login``, and a browser open to ``http://www.example.com``, the 76 | following statement will go to ``http://www.example.com/login``, 77 | and place ``ExampleLoginPage`` at the front of Robot's library 78 | search order. 79 | 80 | | Go to Page ExampleLoginPage 81 | 82 | The effect is the same as if you had called the following three 83 | keywords: 84 | 85 | | SeleniumLibrary.Go To http://www.example.com/login 86 | | Import Library ExampleLoginPage 87 | | Set Library Search Order ExampleLoginPage 88 | 89 | Tags: selenium, page-object 90 | 91 | """ 92 | 93 | page = self._get_page_object(page_name) 94 | 95 | url = page_root if page_root is not None else page.selib.get_location() 96 | (scheme, netloc, path, parameters, query, fragment) = urlparse(url) 97 | url = "%s://%s%s" % (scheme, netloc, page.PAGE_URL) 98 | 99 | with page._wait_for_page_refresh(): 100 | page.selib.go_to(url) 101 | # should I be calling this keyword? Should this keyword return 102 | # true/false, or should it throw an exception? 103 | self.the_current_page_should_be(page_name) 104 | 105 | def _get_page_object(self, page_name): 106 | """Import the page object if necessary, then return the handle to the library 107 | 108 | Note: If the page object has already been imported, it won't be imported again. 109 | """ 110 | 111 | try: 112 | page = self.builtin.get_library_instance(page_name) 113 | 114 | except RuntimeError: 115 | self.builtin.import_library(page_name) 116 | page = self.builtin.get_library_instance(page_name) 117 | 118 | return page 119 | -------------------------------------------------------------------------------- /PageObjectLibrary/locatormap.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import six 4 | 5 | 6 | class LocatorMap(dict): 7 | """LocatorMap - a dict-like object that supports dot notation 8 | 9 | This is used to map self._locators to a self.locator attribute, 10 | to make dealing with locators a bit more pleasant. 11 | """ 12 | def __init__(self, args): 13 | super(LocatorMap, self).__init__(args) 14 | if isinstance(args, dict): 15 | for k, v in six.iteritems(args): 16 | if " " in k in k or "." in k: 17 | raise Exception("Keys cannot have spaces or periods in them") 18 | elif not isinstance(v, dict): 19 | self[k] = v 20 | else: 21 | self.__setattr__(k, LocatorMap(v)) 22 | 23 | def __getattr__(self, attr): 24 | return self.get(attr) 25 | 26 | def __setattr__(self, key, value): 27 | self.__setitem__(key, value) 28 | 29 | def __setitem__(self, key, value): 30 | super(LocatorMap, self).__setitem__(key, value) 31 | self.__dict__.update({key: value}) 32 | 33 | def __delattr__(self, item): 34 | self.__delitem__(item) 35 | 36 | def __delitem__(self, key): 37 | super(LocatorMap, self).__delitem__(key) 38 | del self.__dict__[key] 39 | -------------------------------------------------------------------------------- /PageObjectLibrary/pageobject.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from abc import ABCMeta 4 | from contextlib import contextmanager 5 | import warnings 6 | 7 | import robot.api 8 | from robot.libraries.BuiltIn import BuiltIn 9 | 10 | from selenium.webdriver.support.expected_conditions import staleness_of 11 | from selenium.webdriver.support.ui import WebDriverWait 12 | 13 | import six 14 | 15 | from .locatormap import LocatorMap 16 | 17 | 18 | class PageObject(six.with_metaclass(ABCMeta, object)): 19 | """Base class for page objects 20 | 21 | Classes that inherit from this class need to define the 22 | following class variables: 23 | 24 | PAGE_TITLE the title of the page; used by the default 25 | implementation of _is_current_page 26 | PAGE_URL this should be the URL of the page, minus 27 | the hostname and port (eg: /loginpage.html) 28 | 29 | By default, the PageObjectLibrary keyword 'the current page should 30 | be' calls the method _is_current_page. A default implementation is 31 | provided by this class. It compares the current page title to the 32 | class variable PAGE_TITLE. A class can override this method if the 33 | page title is not unique or is indeterminate. 34 | 35 | Classes that inherit from this class have access to the 36 | following properties: 37 | 38 | * selib a reference to an instance of SeleniumLibrary 39 | * browser a reference to the current webdriver instance 40 | * logger a reference to robot.api.logger 41 | * locator a wrapper around the page object's ``_locators`` dictionary 42 | 43 | This class implements the following context managers: 44 | 45 | * _wait_for_page_refresh 46 | 47 | This context manager is designed to be used in page objects when a 48 | keyword should wait to return until the html element has been 49 | refreshed. 50 | 51 | """ 52 | 53 | PAGE_URL = None 54 | PAGE_TITLE = None 55 | 56 | def __init__(self): 57 | self.logger = robot.api.logger 58 | self.locator = LocatorMap(getattr(self, "_locators", {})) 59 | self.builtin = BuiltIn() 60 | 61 | # N.B. selib, browser use @property so that a 62 | # subclass can be instantiated outside of the context of a running 63 | # test (eg: by libdoc, robotframework-hub, etc) 64 | @property 65 | def se2lib(self): 66 | warnings.warn("se2lib is deprecated. Use selib intead.", warnings.DeprecationWarning) 67 | return self.selib 68 | 69 | @property 70 | def selib(self): 71 | return self.builtin.get_library_instance("SeleniumLibrary") 72 | 73 | @property 74 | def browser(self): 75 | return self.selib.driver 76 | 77 | def __str__(self): 78 | return self.__class__.__name__ 79 | 80 | def get_page_name(self): 81 | """Return the name of the current page """ 82 | return self.__class__.__name__ 83 | 84 | @contextmanager 85 | def _wait_for_page_refresh(self, timeout=10): 86 | """Context manager that waits for a page transition. 87 | 88 | This keyword works by waiting for two things to happen: 89 | 90 | 1) the tag to go stale and get replaced, and 91 | 2) the javascript document.readyState variable to be set 92 | to "complete" 93 | """ 94 | old_page = self.browser.find_element_by_tag_name('html') 95 | yield 96 | WebDriverWait(self.browser, timeout).until( 97 | staleness_of(old_page), 98 | message="Old page did not go stale within %ss" % timeout 99 | ) 100 | self.selib.wait_for_condition("return (document.readyState == 'complete')", timeout=10) 101 | 102 | def _is_current_page(self): 103 | """Determine if this page object represents the current page. 104 | 105 | This works by comparing the current page title to the class 106 | variable PAGE_TITLE. 107 | 108 | Unless their page titles are unique, page objects should 109 | override this function. For example, a common solution is to 110 | look at the url of the current page, or to look for a specific 111 | heading or element on the page. 112 | 113 | """ 114 | 115 | actual_title = self.selib.get_title() 116 | expected_title = self.PAGE_TITLE 117 | 118 | if actual_title.lower() == expected_title.lower(): 119 | return True 120 | 121 | self.logger.info("expected title: '%s'" % expected_title) 122 | self.logger.info(" actual title: '%s'" % actual_title) 123 | raise Exception("expected title to be '%s' but it was '%s'" % (expected_title, actual_title)) 124 | return False 125 | -------------------------------------------------------------------------------- /PageObjectLibrary/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.0.0b3" 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PageObjectLibrary 2 | 3 | ## Overview 4 | 5 | PageObjectLibrary is a lightweight [Robot Framework] keyword library that makes it possible to use the Page Object pattern when testing web pages with the keyword based approach of robot framework. 6 | 7 | ## Installing 8 | 9 | ```bash 10 | pip install --upgrade robotframework-pageobjectlibrary 11 | ``` 12 | 13 | ## Source Code 14 | 15 | The source code is hosted on GitHub at the following url: 16 | 17 | * https://github.com/boakley/robotframework-pageobjectlibrary.git 18 | 19 | ## Running the Demo 20 | 21 | In the GitHub repository is a small demonstration suite that includes a self-contained webserver and web site. 22 | 23 | For the demo to run, you must have [robotframework](https://pypi.org/project/robotframework/) 2.9+ and [robotframework-seleniumlibrary](https://pypi.org/project/robotframework-seleniumlibrary/) installed. You must also have cloned the GitHub repository to have access to the demo files. 24 | 25 | To run the demo, clone the GitHub repository, cd to the folder that contains this file, and then run the following command: : 26 | 27 | ```bash 28 | robot -d demo/results demo 29 | ``` 30 | ### A Simple Tutorial 31 | 32 | For a simple tutorial, see 33 | 34 | ## How it Works 35 | 36 | The Page Object library is quite simple. Page Object classes are implemented as standard robot keyword libraries, and relies on robot frameworks built-in [Set library search order keyword]. 37 | 38 | The core concept is that when you use PageObjectLibrary keywords to go to a page or assert you are on a specific page, the keyword will automatically load the library for that page and put it at the front of the library search order, guaranteeing that the Page Object keywords are available to your test case. 39 | 40 | ## Why Page Objects Makes Writing Tests Easier 41 | 42 | The purpose of the Page Object pattern is to encapsulate the knowledge of how a web page is constructed into an object. Your test uses the object as an interface to the application, isolating your test cases from the details of the implementation of a page. 43 | 44 | With Page Objects, developers are free to modify web pages as much as they want, and the only thing they need to do to keep existing tests from failing is to update the Page Object class. Because test cases aren’t directly tied to the implementation, they become more stable and more resistant to change as the website matures. 45 | 46 | ## A Typical Test _Without_ Page Objects 47 | 48 | With traditional testing using Selenium, a simple login test might look something like the following: (using the pipe-separated format for clarity): 49 | 50 | ```robotframework 51 | *** Test Cases *** 52 | | Login with valid credentials 53 | | | Go to | ${ROOT}/Login.html 54 | | | Wait for page to contain | id=id_username 55 | | | Input text | id=id_username | ${USERNAME} 56 | | | Input text | id=id_password | ${PASSWORD} 57 | | | Click button | id=id_form_submit 58 | | | Wait for page to contain | Your Dashboard 59 | | | Location should be | ${ROOT}/dashboard.html 60 | ``` 61 | 62 | Notice how this test is tightly coupled to the implementation of the page. It has to know that the input field has an id of `id_username`, and the password field has an id of `id_password`. It also has to know the URL of the page being tested. 63 | 64 | Of course, you can put those hard-coded values into variables and import them from a resource file or environment variables, which makes it easier to update tests when locators change. However, there’s still the overhead of additional keywords that are often required to make a test robust, such as waiting for a page to be reloaded. The provided PageObject superclass handles some of those details for you. 65 | 66 | ## The Same Test, Using Page Objects 67 | 68 | Using Page Objects, the same test could be written like this: 69 | 70 | ```robotframework 71 | *** Test Cases *** 72 | | Login with valid credentials 73 | | | Go to page | LoginPage 74 | | | Login as a normal user 75 | | | The current page should be | DashboardPage 76 | ``` 77 | 78 | Notice how there are no URLs or element locators in the test whatsoever, and that we’ve been able to eliminate some keywords that typically are necessary for selenium to work but which aren’t part of the test logic *per se*. What we end up with is test case that is nearly indistinguishable from typical acceptance criteria of an agile story. 79 | 80 | ## Writing a Page Object class 81 | 82 | Page Objects are simple python classes that inherit from `PageObjectLibrary.PageObject`. There are only a couple of requirements for the class: 83 | 84 | - The class should define a variable named `PAGE_TITLE` 85 | - The class should define a variable named `PAGE_URL` which is a URI relative to the site root. 86 | 87 | By inheriting from `PageObjectLibrary.PageObject`, methods have access to the following special object attributes: 88 | 89 | - `self.selib` - a reference to an instance of SeleniumLibrary. With this you can call any of the SeleniumLibrary keywords via their python method names (eg: self.selib.input\_text) 90 | - `self.browser` - a reference to the webdriver object created when a browser was opened by SeleniumLibrary. With this you can bypass SeleniumLibrary and directly call all of the functions provided by the core selenium library. 91 | - `self.locator` - a wrapper around the `_locators` dictionary of the page. This dictionary can contain all of the locators used by the Page Object keywords. `self.locators` adds the ability to access the locators with dot notation rather than the slightly more verbose dictionary syntax (eg: `self.locator.username` vs `self._locators["username"]`. 92 | 93 | ## An example Page Object 94 | 95 | A Page Object representing a login page might look like this: 96 | 97 | ```python 98 | from PageObjectLibrary import PageObject 99 | 100 | class LoginPage(PageObject): 101 | PAGE_TITLE = "Login - PageObjectLibrary Demo" 102 | PAGE_URL = "/login.html" 103 | 104 | _locators = { 105 | "username": "id=id_username", 106 | "password": "id=id_password", 107 | "submit_button": "id=id_submit", 108 | } 109 | 110 | def enter_username(self, username): 111 | """Enter the given string into the username field""" 112 | self.selib.input_text(self.locator.username, username) 113 | 114 | def enter_password(self,password): 115 | """Enter the given string into the password field""" 116 | self.selib.input_text(self.locator.password, password) 117 | 118 | def click_the_submit_button(self): 119 | """Click the submit button, and wait for the page to reload""" 120 | with self._wait_for_page_refresh(): 121 | self.selib.click_button(self.locator.submit_button) 122 | ``` 123 | 124 | [robot framework]: http://www.robotframework.org 125 | [Set library search order keyword]: http://robotframework.org/robotframework/latest/libraries/BuiltIn.html#Set%20Library%20Search%20Order 126 | -------------------------------------------------------------------------------- /TESTING.md: -------------------------------------------------------------------------------- 1 | To run a short demo, run the following command from the same directory 2 | as this file: 3 | 4 | $ robot demo 5 | 6 | To run acceptance tests (which includes running the demo), run the following 7 | command in same directory as this file: 8 | 9 | $ robot -A tests/config.args tests 10 | 11 | 12 | By default the tests will run with chrome, but you can change to any 13 | browser supported by your system by setting the variable BROWSER 14 | from the command line. 15 | 16 | Example: 17 | 18 | robot --variable browser:firefox demo 19 | 20 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | This folder contains example tests and page objects, along 2 | with a small web app to be tested. 3 | 4 | For more information on the web app see demo/webapp/README.md 5 | 6 | -------------------------------------------------------------------------------- /demo/resources/HomePage.py: -------------------------------------------------------------------------------- 1 | from PageObjectLibrary import PageObject 2 | 3 | 4 | class HomePage(PageObject): 5 | """Keywords for the Home page of the demo app 6 | 7 | There are no keywords defined for this page. However, by 8 | creating this empty page object we can still use the 9 | PageObjectLibrary keywords "Go to page" and "The current 10 | page should be" 11 | """ 12 | 13 | PAGE_TITLE = "Home - PageObjectLibrary Demo" 14 | PAGE_URL = "/" 15 | 16 | # these are accessible via dot notaton with self.locator 17 | # (eg: self.locator.username, etc) 18 | _locators = { 19 | } 20 | -------------------------------------------------------------------------------- /demo/resources/LoginPage.py: -------------------------------------------------------------------------------- 1 | from PageObjectLibrary import PageObject 2 | 3 | from robot.libraries.BuiltIn import BuiltIn 4 | 5 | 6 | class LoginPage(PageObject): 7 | PAGE_TITLE = "Login - PageObjectLibrary Demo" 8 | PAGE_URL = "/login.html" 9 | 10 | # these are accessible via dot notaton with self.locator 11 | # (eg: self.locator.username, etc) 12 | _locators = { 13 | "username": "id=id_username", 14 | "password": "id=id_password", 15 | "submit_button": "id=id_submit", 16 | } 17 | 18 | def login_as_a_normal_user(self): 19 | config = BuiltIn().get_variable_value("${CONFIG}") 20 | self.enter_username(config.username) 21 | self.enter_password(config.password) 22 | with self._wait_for_page_refresh(): 23 | self.click_the_submit_button() 24 | 25 | def enter_username(self, username): 26 | """Enter the given string into the username field""" 27 | self.selib.input_text(self.locator.username, username) 28 | 29 | def enter_password(self, password): 30 | """Enter the given string into the password field""" 31 | self.selib.input_text(self.locator.password, password) 32 | 33 | def click_the_submit_button(self): 34 | """Click the submit button, and wait for the page to reload""" 35 | with self._wait_for_page_refresh(): 36 | self.selib.click_button(self.locator.submit_button) 37 | -------------------------------------------------------------------------------- /demo/resources/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | 5 | class Config(object): 6 | """Configuration variables for this test suite 7 | 8 | This creates a variable named CONFIG (${CONFIG} when included 9 | in a test as a variable file. 10 | 11 | Example: 12 | 13 | *** Settings *** 14 | | Variable | ../resources/config.py 15 | 16 | *** Test Cases *** 17 | | Example 18 | | | log | username: ${CONFIG}.username 19 | | | log | root url: ${CONFIG}.root_url 20 | 21 | """ 22 | 23 | def __init__(self): 24 | _here = os.path.dirname(__file__) 25 | 26 | sys.path.insert(0, os.path.abspath(os.path.join(_here, "..", ".."))) 27 | sys.path.insert(0, os.path.abspath(os.path.join(_here))) 28 | 29 | self.demo_root = os.path.abspath(os.path.join(_here, "..")) 30 | self.port = 8000 31 | self.root_url = "http://localhost:%s" % self.port 32 | self.username = "test user" 33 | self.password = "password" 34 | 35 | def __str__(self): 36 | return "" % str(self.__dict__) 37 | 38 | 39 | # This creates a variable that robot can see 40 | CONFIG = Config() 41 | -------------------------------------------------------------------------------- /demo/tests/demo.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | # this is the only place where we have to hard-code a path; 3 | # when config.py is loaded it will alter the path to include 4 | # the resources folder. 5 | Variables ../resources/config.py 6 | 7 | Library PageObjectLibrary 8 | Library SeleniumLibrary 9 | Library Process 10 | 11 | Suite Setup Start webapp and open browser 12 | Suite Teardown Stop webapp and close all browsers 13 | 14 | *** Variables *** 15 | ${BROWSER} chrome 16 | 17 | *** Keywords *** 18 | Stop webapp and close all browsers 19 | Terminate all processes 20 | Close all browsers 21 | 22 | Start webapp and open browser 23 | start process python ${CONFIG.demo_root}/webapp/demoserver.py 24 | open browser ${CONFIG.root_url} ${BROWSER} 25 | 26 | *** Test Cases *** 27 | Login smoke test 28 | [Setup] Go to page LoginPage 29 | Login as a normal user 30 | The current page should be HomePage 31 | 32 | Login with valid credentials 33 | [Setup] Go to page LoginPage 34 | Enter username Demo User 35 | Enter password password 36 | Click the submit button 37 | The current page should be HomePage 38 | 39 | Login with invalid credentials 40 | [Setup] Go to page LoginPage 41 | Enter username Demo User 42 | Enter password bogus password 43 | Click the submit button 44 | The current page should be LoginPage 45 | -------------------------------------------------------------------------------- /demo/webapp/README.md: -------------------------------------------------------------------------------- 1 | ## webapp 2 | 3 | This is a simple webapp used for the demo. It serves 4 | up pages in the demo/webapp/docroot folder on port 8000. 5 | 6 | It is started automatically by the demo tests. To start it 7 | manually you can run the following command from the root of 8 | the repository: 9 | 10 | $ python demo/webapp/demoserver.py 11 | 12 | -------------------------------------------------------------------------------- /demo/webapp/demoserver.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function 2 | import socket 3 | import os 4 | import signal 5 | import sys 6 | import cgi 7 | import argparse 8 | 9 | if sys.version_info >= (3, 0): 10 | from http.server import SimpleHTTPRequestHandler 11 | import socketserver 12 | from urllib.parse import urlparse 13 | else: 14 | from SimpleHTTPServer import SimpleHTTPRequestHandler 15 | import SocketServer as socketserver 16 | from urlparse import urlparse 17 | 18 | 19 | def main(): 20 | parser = argparse.ArgumentParser(description="demo web server") 21 | parser.add_argument("-p", "--port", type=int, default=8000, help="port number for the server (default 8000)") 22 | args = parser.parse_args() 23 | 24 | here = os.path.dirname(__file__) 25 | docroot = os.path.relpath(os.path.join(here, "docroot")) 26 | os.chdir(docroot) 27 | 28 | try: 29 | httpd = DemoServer(("", args.port), DemoHandler) 30 | print("serving %s on port %s" % (docroot, 8000)) 31 | print ("^C, or visit http://localhost:%s/admin/shutdown to stop" % args.port) 32 | httpd.serve_forever() 33 | except KeyboardInterrupt: 34 | pass 35 | 36 | 37 | class DemoHandler(SimpleHTTPRequestHandler): 38 | 39 | def redirect(self, uri): 40 | self.send_response(301) 41 | self.send_header('Location', uri) 42 | self.end_headers() 43 | 44 | def do_POST(self): 45 | if self.path.startswith("/authenticate"): 46 | form = self.get_form_data() 47 | if "password" in form and form["password"].value == "password": 48 | self.redirect("/homepage.html") 49 | else: 50 | self.redirect("/login.html") 51 | 52 | def do_GET(self): 53 | url = urlparse(self.path) 54 | print("url: '%s' url.path: '%s'" % (url, url.path)) 55 | if url.path == "" or url.path == "/": 56 | self.redirect("/login.html") 57 | 58 | if url.path == "/authenticate": 59 | self.redirect("/homepage.html") 60 | return 61 | 62 | if url.path == '/admin/shutdown': 63 | print("server shutdown has been requested") 64 | os.kill(os.getpid(), signal.SIGHUP) 65 | 66 | return SimpleHTTPRequestHandler.do_GET(self) 67 | 68 | def get_query_parameters(self): 69 | idx = self.path.find("?") 70 | args = {} 71 | if idx >= 0: 72 | args = urlparse.parse_qs(self.path[idx+1:]) 73 | return args 74 | 75 | def get_form_data(self): 76 | form = cgi.FieldStorage( 77 | fp=self.rfile, 78 | headers=self.headers, 79 | environ={'REQUEST_METHOD':'POST', 80 | 'CONTENT_TYPE':self.headers['Content-Type'], 81 | }) 82 | return form 83 | 84 | 85 | class DemoServer(socketserver.TCPServer): 86 | def server_bind(self): 87 | self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 88 | self.socket.bind(self.server_address) 89 | 90 | 91 | if __name__ == "__main__": 92 | main() 93 | -------------------------------------------------------------------------------- /demo/webapp/docroot/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | About the Demo - PageObjectLibrary Demo 4 | 5 | 6 | 7 |
8 | Home  9 | Log In  10 | About  11 | Shut down  12 |
13 |
14 |

About the Demo

15 |

16 | This website is being served by a very simple server 17 | based on SimpleHTTPServer. 18 | The demo server can be started from the command line with the following command: 19 | 20 |

21 | $ python demoserver.py 22 |

23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /demo/webapp/docroot/homepage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Home - PageObjectLibrary Demo 4 | 5 | 6 | 7 |
8 | Home  9 | Login  10 | About  11 | Shut down  12 |
13 |
14 |

Demo Home Page

15 | 16 |

Congratulations, you logged in successfully!

17 | 18 |

19 | This is a small website designed for demonstrating the 20 | 21 | robot framework page object library. 22 | 23 |

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /demo/webapp/docroot/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Login - PageObjectLibrary Demo 4 | 5 | 6 | 7 |
8 | Home  9 | About  10 | Shut down  11 |
12 |
13 |

Welcome!

14 |

Please log in

15 |

16 | In the following form, enter any username you want; the only 17 | valid password is "password". 18 |

19 |
20 |
21 | 22 | 23 |
24 |
25 | 26 | 27 |
28 |

29 | 30 |

31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /demo/webapp/docroot/relogin.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | Authentication Failed - PageObjectLibrary Demo 4 | 5 | 6 | 7 |
8 | Home  9 | About  10 | Shut down  11 |
12 |
13 |

Authentication failure

14 | 15 | Unrecognized username/password combination. 16 | 17 |

Please log in

18 |

19 | In the following form, enter any username you want; the only 20 | valid password is "password". 21 |

22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 |

32 | 33 |

34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /demo/webapp/docroot/stylesheet.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Helvetica", Helvetica, Arial, sans-serif; 3 | background: #ddd; 4 | margin-left: 60px; 5 | margin: 30px; 6 | max-width: 960px; 7 | } 8 | .command-prompt { 9 | font-family: monospace; 10 | white-space: pre; 11 | background: #f2f6f8; 12 | border: 1px solid; 13 | color: #; 14 | max-width: 40em; 15 | box-shadow: 5px 5px 2px #888888; 16 | } 17 | 18 | .align-right { 19 | float: right; 20 | } 21 | .header { 22 | background: #000000; 23 | } 24 | .header a { 25 | margin: 10px; 26 | font-size: .9em; 27 | color: #ddd; 28 | text-decoration: none; 29 | } 30 | 31 | .form_entry { 32 | font-family: "Helvetica", Helvetica, Arial, sans-serif; 33 | margin-bottom: 1em; 34 | } 35 | 36 | h1 { 37 | color: #b22222; 38 | } 39 | label { 40 | display: block; 41 | } 42 | -------------------------------------------------------------------------------- /doc/pageobjectlibrary.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 161 | 231 | 244 | 267 | 318 | 521 | 525 | 537 | 550 | 553 | 554 | 555 | 556 | 557 |
558 |

Opening library documentation failed

559 |
    560 |
  • Verify that you have JavaScript enabled in your browser.
  • 561 |
  • Make sure you are using a modern enough browser. Firefox 3.5, IE 8, or equivalent is required, newer browsers are recommended.
  • 562 |
  • Check are there messages in your browser's JavaScript error log. Please report the problem if you suspect you have encountered a bug.
  • 563 |
564 |
565 | 566 | 570 | 571 | 770 | 771 | 816 | 817 | 836 | 837 | 848 | 849 | 860 | 861 | 877 | 878 | 900 | 901 | 902 | 910 | 911 | 912 | 913 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | robotframework 2 | robotframework-seleniumlibrary 3 | selenium 4 | six 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from io import open 2 | from setuptools import setup 3 | 4 | exec(open('PageObjectLibrary/version.py').read()) 5 | 6 | setup( 7 | name = 'robotframework-pageobjectlibrary', 8 | version = __version__, 9 | author = 'Bryan Oakley', 10 | author_email = 'bryan.oakley@gmail.com', 11 | url = 'https://github.com/boakley/robotframework-pageobjectlibrary/', 12 | keywords = 'robotframework', 13 | license = 'Apache License 2.0', 14 | description = 'RobotFramework library that implements the Page Object pattern', 15 | long_description = open('README.md', encoding='latin-1').read(), 16 | zip_safe = True, 17 | include_package_data=True, 18 | install_requires = ['robotframework', 'robotframework-seleniumlibrary', 'selenium', 'six'], 19 | classifiers = [ 20 | "Development Status :: 4 - Beta", 21 | "License :: OSI Approved :: Apache Software License", 22 | "Operating System :: OS Independent", 23 | "Framework :: Robot Framework", 24 | "Programming Language :: Python", 25 | "Topic :: Software Development :: Testing", 26 | "Topic :: Software Development :: Quality Assurance", 27 | "Intended Audience :: Developers", 28 | ], 29 | packages = [ 30 | 'PageObjectLibrary', 31 | ], 32 | scripts = [], 33 | ) 34 | -------------------------------------------------------------------------------- /tests/about.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Documentation Show the browser "about:" page, for debugging purposes 3 | Library SeleniumLibrary 4 | 5 | *** Variables *** 6 | ${BROWSER} chrome 7 | 8 | *** Test Cases *** 9 | The browser 'about:' page 10 | [Documentation] 11 | ... Displays the browser's 'about:' page 12 | 13 | [Setup] open browser about: ${BROWSER} 14 | 15 | capture page screenshot 16 | 17 | [Teardown] close browser 18 | -------------------------------------------------------------------------------- /tests/acceptance/demo.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | | Documentation | Verify that the demo works as expected 3 | | Library | Process 4 | | Variables | resources/config.py 5 | 6 | *** Variables *** 7 | | ${BROWSER} | chrome 8 | 9 | *** Test Cases *** 10 | | Verify the demo works without error 11 | | | [Documentation] 12 | | | ... | Verify that the demo runs and completes with no errors 13 | | | ${result}= | Run process | robot | --outputdir | ${OUTPUT_DIR}/demo_results | demo | cwd=${CONFIG.repo_root} 14 | | | Set suite metadata | Status code | ${result.rc} 15 | | | Set suite metadata | Demo log.html | [demo_results/log.html|demo_results/log./html] 16 | | | run keyword if | '${result.rc}' != 0 | log | stdout: ${result.stdout}\nstderr: ${result.stderr} 17 | | | Should be equal as integers | ${result.rc} | 0 18 | | | ... | expected result code to be zero but it was ${result.rc} 19 | | | ... | values=False 20 | -------------------------------------------------------------------------------- /tests/acceptance/library.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library SeleniumLibrary 3 | Library PageObjectLibrary 4 | Library Process 5 | Variables resources/config.py 6 | 7 | Suite Setup Set report metadata 8 | Suite Teardown Stop webapp and close all browsers 9 | 10 | *** Variables *** 11 | ${BROWSER} chrome 12 | 13 | *** Keywords *** 14 | Set report metadata 15 | ${selib version}= evaluate SeleniumLibrary.__version__ SeleniumLibrary 16 | ${polib version}= evaluate PageObjectLibrary.__version__ PageObjectLibrary 17 | Set suite metadata SeleniumLibrary version ${selib version} append=True top=True 18 | Set suite metadata PageObjectLibrary version ${polib version} append=True top=True 19 | 20 | Start webapp and open browser 21 | start process ${CONFIG.python} ${CONFIG.demo_root}/webapp/demoserver.py 22 | open browser ${CONFIG.root_url} ${BROWSER} 23 | 24 | Stop webapp and close all browsers 25 | Terminate all processes 26 | Close all browsers 27 | 28 | *** Test Cases *** 29 | Library is importable 30 | import library PageObjectLibrary 31 | -------------------------------------------------------------------------------- /tests/acceptance/resources/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | class Config(object): 5 | """Configuration for this test suite 6 | 7 | This creates a variable named CONFIG (${CONFIG} when included 8 | in a test as a variable file. It also configures PYTHONPATH 9 | so that the tests can find the libraries being tested. 10 | 11 | Example: 12 | 13 | *** Settings *** 14 | | Variable | ../resources/config.py 15 | 16 | *** Test Cases *** 17 | | Example 18 | | | log | username: ${CONFIG.username} 19 | | | log | root url: ${CONFIG.root_url} 20 | 21 | """ 22 | 23 | def __init__(self): 24 | _here = os.path.abspath(os.path.dirname(__file__)) 25 | _root = os.path.join(_here, "..", "..", "..") 26 | 27 | self.repo_root = _root 28 | self.demo_root = os.path.join(_root, "demo", ) 29 | 30 | self.python = sys.executable 31 | self.port = 8015 32 | self.root_url = "http://localhost:%s" % self.port 33 | self.username="test user" 34 | self.password="password" 35 | 36 | # add the repository root, the test folder, and also this directory 37 | # to the path 38 | sys.path.insert(0, _root) 39 | sys.path.insert(0, os.path.join(_root, "tests")) 40 | sys.path.insert(0, os.path.join(_here)) 41 | 42 | def __str__(self): 43 | return "" % str(self.__dict__) 44 | 45 | # This creates a variable that robot can see named ${CONFIG} 46 | CONFIG = Config() 47 | -------------------------------------------------------------------------------- /tests/config.args: -------------------------------------------------------------------------------- 1 | --outputdir tests/results 2 | --variable browser:chrome 3 | --------------------------------------------------------------------------------