├── .gitignore ├── .pre-commit-config.yaml ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE.txt ├── Pipfile ├── Pipfile.lock ├── README.rst ├── docs ├── .gitignore ├── Makefile ├── api.rst ├── conf.py ├── development.rst ├── examples │ ├── nested_regions.html │ ├── nested_regions.py │ ├── repeated_regions.html │ └── repeated_regions.py ├── index.rst ├── installing.rst ├── make.bat ├── news.rst ├── plugins.rst └── user_guide.rst ├── pyproject.toml ├── requirements └── pipenv.txt ├── setup.cfg ├── setup.py ├── src └── pypom │ ├── __init__.py │ ├── driver.py │ ├── exception.py │ ├── hooks.py │ ├── interfaces │ ├── __init__.py │ └── driver.py │ ├── newsfragments │ └── .gitignore │ ├── page.py │ ├── region.py │ ├── selenium_driver.py │ ├── splinter_driver.py │ └── view.py ├── tests ├── __init__.py ├── conftest.py ├── selenium_specific │ ├── __init__.py │ ├── conftest.py │ ├── test_page.py │ └── test_region.py ├── splinter_specific │ ├── __init__.py │ ├── conftest.py │ ├── test_page.py │ └── test_region.py ├── test_driver.py ├── test_page.py ├── test_plugin.py └── test_region.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .cache 3 | .coverage 4 | .eggs 5 | .pytest_cache 6 | .tox 7 | *.pyc 8 | *.egg-info 9 | build 10 | dist 11 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/ambv/black 3 | rev: stable 4 | hooks: 5 | - id: black 6 | language_version: python3.6 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | jobs: 3 | include: 4 | - stage: 5 | python: 3.6 6 | env: TOXENV=flake8 7 | after_success: skip 8 | - stage: 9 | python: 3.6 10 | env: TOXENV=docs 11 | after_success: skip 12 | - stage: 13 | python: 3.6 14 | - stage: 15 | python: 2.7 16 | - stage: 17 | python: pypy 18 | - stage: 19 | python: pypy3 20 | - stage: deploy 21 | python: 3.6 22 | install: skip 23 | script: skip 24 | deploy: 25 | provider: pypi 26 | user: davehunt 27 | password: 28 | secure: "DNLv5NtW8M1vL+1F3aQlZqaZmPcBuLZID449upMo1OvLTx4w0ridE3YoBVO3/cMbk8JjonnpfUPaz7HGvB5UHFhCnX/UhYHqiP2JZOhxRF2RaDl6vqHvhMRjZPlGGev9A86b87HERHKfdSmYk2BW63cHSwGhKjuyGAM61VN7cSqZ+J6xPAns2WJOqUrzbZqw+ac4VPqnQGpb6BJKLzeADK5v5+ZLHdK1Q7HWWgJbvJBl4GYXGzoZBVbdDLJmNx1MWL+t4VrkFfkOABzOzrVdXwi0Yh2d3Bu6Bl/EZEPbDkyYiWhyep5M9rVOzRNsMbSs5hM3QiMAlBFa97trNQPS3pX/ggfpOelgo7rFJfzZzPQYfrVRPta6UWpLiObDIzQMTrY9QT0R8ckifnfPncVnGC3JgtqbVnc1lBofbQhkx72rl0zHeTqkMpV0z/HzTeXuBR2Roxe+om7uyZS1yC6sc2uElxtYyJ+M+PAKBWk84iY98U2WlDbn68LnPjP8hiJCBYBrIBRjA1xSDRgnxoPK4lgLr7BvZIfudpRoFwnbTwkNe45GFm/i2CBqw521hy3X7WWrBz6OioFmh2GYXaYAYP+Gk/94fTw5nL1AGcgVZabyIa8TTdBbeemzfQSTOrvxttnmxRvpDBPU2r0cLOga8iNMbjJ1pyW9fhO1kP7FuCw=" 29 | distributions: sdist bdist_wheel 30 | on: 31 | tags: true 32 | repo: mozilla/PyPOM 33 | install: pip install tox-travis 34 | script: tox 35 | after_success: 36 | - pip install -r requirements/coveralls.txt 37 | - coveralls 38 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, please read the 5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 9 | 10 | 16 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Unless otherwise stated, this license applies to all files contained 2 | within this source code repository. 3 | 4 | 5 | Mozilla Public License Version 2.0 6 | ================================== 7 | 8 | 1. Definitions 9 | -------------- 10 | 11 | 1.1. "Contributor" 12 | means each individual or legal entity that creates, contributes to 13 | the creation of, or owns Covered Software. 14 | 15 | 1.2. "Contributor Version" 16 | means the combination of the Contributions of others (if any) used 17 | by a Contributor and that particular Contributor's Contribution. 18 | 19 | 1.3. "Contribution" 20 | means Covered Software of a particular Contributor. 21 | 22 | 1.4. "Covered Software" 23 | means Source Code Form to which the initial Contributor has attached 24 | the notice in Exhibit A, the Executable Form of such Source Code 25 | Form, and Modifications of such Source Code Form, in each case 26 | including portions thereof. 27 | 28 | 1.5. "Incompatible With Secondary Licenses" 29 | means 30 | 31 | (a) that the initial Contributor has attached the notice described 32 | in Exhibit B to the Covered Software; or 33 | 34 | (b) that the Covered Software was made available under the terms of 35 | version 1.1 or earlier of the License, but not also under the 36 | terms of a Secondary License. 37 | 38 | 1.6. "Executable Form" 39 | means any form of the work other than Source Code Form. 40 | 41 | 1.7. "Larger Work" 42 | means a work that combines Covered Software with other material, in 43 | a separate file or files, that is not Covered Software. 44 | 45 | 1.8. "License" 46 | means this document. 47 | 48 | 1.9. "Licensable" 49 | means having the right to grant, to the maximum extent possible, 50 | whether at the time of the initial grant or subsequently, any and 51 | all of the rights conveyed by this License. 52 | 53 | 1.10. "Modifications" 54 | means any of the following: 55 | 56 | (a) any file in Source Code Form that results from an addition to, 57 | deletion from, or modification of the contents of Covered 58 | Software; or 59 | 60 | (b) any new file in Source Code Form that contains any Covered 61 | Software. 62 | 63 | 1.11. "Patent Claims" of a Contributor 64 | means any patent claim(s), including without limitation, method, 65 | process, and apparatus claims, in any patent Licensable by such 66 | Contributor that would be infringed, but for the grant of the 67 | License, by the making, using, selling, offering for sale, having 68 | made, import, or transfer of either its Contributions or its 69 | Contributor Version. 70 | 71 | 1.12. "Secondary License" 72 | means either the GNU General Public License, Version 2.0, the GNU 73 | Lesser General Public License, Version 2.1, the GNU Affero General 74 | Public License, Version 3.0, or any later versions of those 75 | licenses. 76 | 77 | 1.13. "Source Code Form" 78 | means the form of the work preferred for making modifications. 79 | 80 | 1.14. "You" (or "Your") 81 | means an individual or a legal entity exercising rights under this 82 | License. For legal entities, "You" includes any entity that 83 | controls, is controlled by, or is under common control with You. For 84 | purposes of this definition, "control" means (a) the power, direct 85 | or indirect, to cause the direction or management of such entity, 86 | whether by contract or otherwise, or (b) ownership of more than 87 | fifty percent (50%) of the outstanding shares or beneficial 88 | ownership of such entity. 89 | 90 | 2. License Grants and Conditions 91 | -------------------------------- 92 | 93 | 2.1. Grants 94 | 95 | Each Contributor hereby grants You a world-wide, royalty-free, 96 | non-exclusive license: 97 | 98 | (a) under intellectual property rights (other than patent or trademark) 99 | Licensable by such Contributor to use, reproduce, make available, 100 | modify, display, perform, distribute, and otherwise exploit its 101 | Contributions, either on an unmodified basis, with Modifications, or 102 | as part of a Larger Work; and 103 | 104 | (b) under Patent Claims of such Contributor to make, use, sell, offer 105 | for sale, have made, import, and otherwise transfer either its 106 | Contributions or its Contributor Version. 107 | 108 | 2.2. Effective Date 109 | 110 | The licenses granted in Section 2.1 with respect to any Contribution 111 | become effective for each Contribution on the date the Contributor first 112 | distributes such Contribution. 113 | 114 | 2.3. Limitations on Grant Scope 115 | 116 | The licenses granted in this Section 2 are the only rights granted under 117 | this License. No additional rights or licenses will be implied from the 118 | distribution or licensing of Covered Software under this License. 119 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 120 | Contributor: 121 | 122 | (a) for any code that a Contributor has removed from Covered Software; 123 | or 124 | 125 | (b) for infringements caused by: (i) Your and any other third party's 126 | modifications of Covered Software, or (ii) the combination of its 127 | Contributions with other software (except as part of its Contributor 128 | Version); or 129 | 130 | (c) under Patent Claims infringed by Covered Software in the absence of 131 | its Contributions. 132 | 133 | This License does not grant any rights in the trademarks, service marks, 134 | or logos of any Contributor (except as may be necessary to comply with 135 | the notice requirements in Section 3.4). 136 | 137 | 2.4. Subsequent Licenses 138 | 139 | No Contributor makes additional grants as a result of Your choice to 140 | distribute the Covered Software under a subsequent version of this 141 | License (see Section 10.2) or under the terms of a Secondary License (if 142 | permitted under the terms of Section 3.3). 143 | 144 | 2.5. Representation 145 | 146 | Each Contributor represents that the Contributor believes its 147 | Contributions are its original creation(s) or it has sufficient rights 148 | to grant the rights to its Contributions conveyed by this License. 149 | 150 | 2.6. Fair Use 151 | 152 | This License is not intended to limit any rights You have under 153 | applicable copyright doctrines of fair use, fair dealing, or other 154 | equivalents. 155 | 156 | 2.7. Conditions 157 | 158 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 159 | in Section 2.1. 160 | 161 | 3. Responsibilities 162 | ------------------- 163 | 164 | 3.1. Distribution of Source Form 165 | 166 | All distribution of Covered Software in Source Code Form, including any 167 | Modifications that You create or to which You contribute, must be under 168 | the terms of this License. You must inform recipients that the Source 169 | Code Form of the Covered Software is governed by the terms of this 170 | License, and how they can obtain a copy of this License. You may not 171 | attempt to alter or restrict the recipients' rights in the Source Code 172 | Form. 173 | 174 | 3.2. Distribution of Executable Form 175 | 176 | If You distribute Covered Software in Executable Form then: 177 | 178 | (a) such Covered Software must also be made available in Source Code 179 | Form, as described in Section 3.1, and You must inform recipients of 180 | the Executable Form how they can obtain a copy of such Source Code 181 | Form by reasonable means in a timely manner, at a charge no more 182 | than the cost of distribution to the recipient; and 183 | 184 | (b) You may distribute such Executable Form under the terms of this 185 | License, or sublicense it under different terms, provided that the 186 | license for the Executable Form does not attempt to limit or alter 187 | the recipients' rights in the Source Code Form under this License. 188 | 189 | 3.3. Distribution of a Larger Work 190 | 191 | You may create and distribute a Larger Work under terms of Your choice, 192 | provided that You also comply with the requirements of this License for 193 | the Covered Software. If the Larger Work is a combination of Covered 194 | Software with a work governed by one or more Secondary Licenses, and the 195 | Covered Software is not Incompatible With Secondary Licenses, this 196 | License permits You to additionally distribute such Covered Software 197 | under the terms of such Secondary License(s), so that the recipient of 198 | the Larger Work may, at their option, further distribute the Covered 199 | Software under the terms of either this License or such Secondary 200 | License(s). 201 | 202 | 3.4. Notices 203 | 204 | You may not remove or alter the substance of any license notices 205 | (including copyright notices, patent notices, disclaimers of warranty, 206 | or limitations of liability) contained within the Source Code Form of 207 | the Covered Software, except that You may alter any license notices to 208 | the extent required to remedy known factual inaccuracies. 209 | 210 | 3.5. Application of Additional Terms 211 | 212 | You may choose to offer, and to charge a fee for, warranty, support, 213 | indemnity or liability obligations to one or more recipients of Covered 214 | Software. However, You may do so only on Your own behalf, and not on 215 | behalf of any Contributor. You must make it absolutely clear that any 216 | such warranty, support, indemnity, or liability obligation is offered by 217 | You alone, and You hereby agree to indemnify every Contributor for any 218 | liability incurred by such Contributor as a result of warranty, support, 219 | indemnity or liability terms You offer. You may include additional 220 | disclaimers of warranty and limitations of liability specific to any 221 | jurisdiction. 222 | 223 | 4. Inability to Comply Due to Statute or Regulation 224 | --------------------------------------------------- 225 | 226 | If it is impossible for You to comply with any of the terms of this 227 | License with respect to some or all of the Covered Software due to 228 | statute, judicial order, or regulation then You must: (a) comply with 229 | the terms of this License to the maximum extent possible; and (b) 230 | describe the limitations and the code they affect. Such description must 231 | be placed in a text file included with all distributions of the Covered 232 | Software under this License. Except to the extent prohibited by statute 233 | or regulation, such description must be sufficiently detailed for a 234 | recipient of ordinary skill to be able to understand it. 235 | 236 | 5. Termination 237 | -------------- 238 | 239 | 5.1. The rights granted under this License will terminate automatically 240 | if You fail to comply with any of its terms. However, if You become 241 | compliant, then the rights granted under this License from a particular 242 | Contributor are reinstated (a) provisionally, unless and until such 243 | Contributor explicitly and finally terminates Your grants, and (b) on an 244 | ongoing basis, if such Contributor fails to notify You of the 245 | non-compliance by some reasonable means prior to 60 days after You have 246 | come back into compliance. Moreover, Your grants from a particular 247 | Contributor are reinstated on an ongoing basis if such Contributor 248 | notifies You of the non-compliance by some reasonable means, this is the 249 | first time You have received notice of non-compliance with this License 250 | from such Contributor, and You become compliant prior to 30 days after 251 | Your receipt of the notice. 252 | 253 | 5.2. If You initiate litigation against any entity by asserting a patent 254 | infringement claim (excluding declaratory judgment actions, 255 | counter-claims, and cross-claims) alleging that a Contributor Version 256 | directly or indirectly infringes any patent, then the rights granted to 257 | You by any and all Contributors for the Covered Software under Section 258 | 2.1 of this License shall terminate. 259 | 260 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 261 | end user license agreements (excluding distributors and resellers) which 262 | have been validly granted by You or Your distributors under this License 263 | prior to termination shall survive termination. 264 | 265 | ************************************************************************ 266 | * * 267 | * 6. Disclaimer of Warranty * 268 | * ------------------------- * 269 | * * 270 | * Covered Software is provided under this License on an "as is" * 271 | * basis, without warranty of any kind, either expressed, implied, or * 272 | * statutory, including, without limitation, warranties that the * 273 | * Covered Software is free of defects, merchantable, fit for a * 274 | * particular purpose or non-infringing. The entire risk as to the * 275 | * quality and performance of the Covered Software is with You. * 276 | * Should any Covered Software prove defective in any respect, You * 277 | * (not any Contributor) assume the cost of any necessary servicing, * 278 | * repair, or correction. This disclaimer of warranty constitutes an * 279 | * essential part of this License. No use of any Covered Software is * 280 | * authorized under this License except under this disclaimer. * 281 | * * 282 | ************************************************************************ 283 | 284 | ************************************************************************ 285 | * * 286 | * 7. Limitation of Liability * 287 | * -------------------------- * 288 | * * 289 | * Under no circumstances and under no legal theory, whether tort * 290 | * (including negligence), contract, or otherwise, shall any * 291 | * Contributor, or anyone who distributes Covered Software as * 292 | * permitted above, be liable to You for any direct, indirect, * 293 | * special, incidental, or consequential damages of any character * 294 | * including, without limitation, damages for lost profits, loss of * 295 | * goodwill, work stoppage, computer failure or malfunction, or any * 296 | * and all other commercial damages or losses, even if such party * 297 | * shall have been informed of the possibility of such damages. This * 298 | * limitation of liability shall not apply to liability for death or * 299 | * personal injury resulting from such party's negligence to the * 300 | * extent applicable law prohibits such limitation. Some * 301 | * jurisdictions do not allow the exclusion or limitation of * 302 | * incidental or consequential damages, so this exclusion and * 303 | * limitation may not apply to You. * 304 | * * 305 | ************************************************************************ 306 | 307 | 8. Litigation 308 | ------------- 309 | 310 | Any litigation relating to this License may be brought only in the 311 | courts of a jurisdiction where the defendant maintains its principal 312 | place of business and such litigation shall be governed by laws of that 313 | jurisdiction, without reference to its conflict-of-law provisions. 314 | Nothing in this Section shall prevent a party's ability to bring 315 | cross-claims or counter-claims. 316 | 317 | 9. Miscellaneous 318 | ---------------- 319 | 320 | This License represents the complete agreement concerning the subject 321 | matter hereof. If any provision of this License is held to be 322 | unenforceable, such provision shall be reformed only to the extent 323 | necessary to make it enforceable. Any law or regulation which provides 324 | that the language of a contract shall be construed against the drafter 325 | shall not be used to construe this License against a Contributor. 326 | 327 | 10. Versions of the License 328 | --------------------------- 329 | 330 | 10.1. New Versions 331 | 332 | Mozilla Foundation is the license steward. Except as provided in Section 333 | 10.3, no one other than the license steward has the right to modify or 334 | publish new versions of this License. Each version will be given a 335 | distinguishing version number. 336 | 337 | 10.2. Effect of New Versions 338 | 339 | You may distribute the Covered Software under the terms of the version 340 | of the License under which You originally received the Covered Software, 341 | or under the terms of any subsequent version published by the license 342 | steward. 343 | 344 | 10.3. Modified Versions 345 | 346 | If you create software not governed by this License, and you want to 347 | create a new license for such software, you may create and use a 348 | modified version of this License if you rename the license and remove 349 | any references to the name of the license steward (except to note that 350 | such modified license differs from this License). 351 | 352 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 353 | Licenses 354 | 355 | If You choose to distribute Source Code Form that is Incompatible With 356 | Secondary Licenses under the terms of this version of the License, the 357 | notice described in Exhibit B of this License must be attached. 358 | 359 | Exhibit A - Source Code Form License Notice 360 | ------------------------------------------- 361 | 362 | This Source Code Form is subject to the terms of the Mozilla Public 363 | License, v. 2.0. If a copy of the MPL was not distributed with this 364 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 365 | 366 | If it is not possible or desirable to put the notice in a particular 367 | file, then You may include the notice in a location (such as a LICENSE 368 | file in a relevant directory) where a recipient would be likely to look 369 | for such a notice. 370 | 371 | You may add additional accurate notices of copyright ownership. 372 | 373 | Exhibit B - "Incompatible With Secondary Licenses" Notice 374 | --------------------------------------------------------- 375 | 376 | This Source Code Form is "Incompatible With Secondary Licenses", as 377 | defined by the Mozilla Public License, v. 2.0. 378 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | pluggy = "*" 8 | pypom = {editable = true, path = ".", extras = ["splinter"]} 9 | selenium = "*" 10 | splinter = "*" 11 | "zope.component" = "*" 12 | "zope.interface" = "*" 13 | 14 | [dev-packages] 15 | coveralls = "*" 16 | flake8 = "*" 17 | flake8-isort = "*" 18 | mock = "*" 19 | pytest-cov = "*" 20 | sphinx = "*" 21 | sphinx-rtd-theme = "*" 22 | towncrier = "*" 23 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "6af2d595746e497b166f91daf48c63381a81782a8802107e36d1a93258d4bffe" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": {}, 8 | "sources": [ 9 | { 10 | "name": "pypi", 11 | "url": "https://pypi.org/simple", 12 | "verify_ssl": true 13 | } 14 | ] 15 | }, 16 | "default": { 17 | "pluggy": { 18 | "hashes": [ 19 | "sha256:25a1bc1d148c9a640211872b4ff859878d422bccb59c9965e04eed468a0aa180", 20 | "sha256:964cedd2b27c492fbf0b7f58b3284a09cf7f99b0f715941fb24a439b3af1bd1a" 21 | ], 22 | "index": "pypi", 23 | "version": "==0.11.0" 24 | }, 25 | "pypom": { 26 | "editable": true, 27 | "extras": [ 28 | "splinter" 29 | ], 30 | "path": "." 31 | }, 32 | "selenium": { 33 | "hashes": [ 34 | "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c", 35 | "sha256:deaf32b60ad91a4611b98d8002757f29e6f2c2d5fcaf202e1c9ad06d6772300d" 36 | ], 37 | "index": "pypi", 38 | "version": "==3.141.0" 39 | }, 40 | "splinter": { 41 | "hashes": [ 42 | "sha256:2d9f370536e6c1607824f5538e0bff9808bc02f086b07622b3790424dd3daff4", 43 | "sha256:5d9913bddb6030979c18d6801578813b02bbf8a03b43fb057f093228ed876d62" 44 | ], 45 | "index": "pypi", 46 | "version": "==0.10.0" 47 | }, 48 | "urllib3": { 49 | "hashes": [ 50 | "sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1", 51 | "sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232" 52 | ], 53 | "version": "==1.25.3" 54 | }, 55 | "zope-component": { 56 | "hashes": [ 57 | "sha256:984a06ba3def0b02b1117fa4c45b56e772e8c29c0340820fbf367e440a93a3a4" 58 | ], 59 | "version": "==4.5" 60 | }, 61 | "zope-interface": { 62 | "hashes": [ 63 | "sha256:086707e0f413ff8800d9c4bc26e174f7ee4c9c8b0302fbad68d083071822316c", 64 | "sha256:1157b1ec2a1f5bf45668421e3955c60c610e31913cc695b407a574efdbae1f7b", 65 | "sha256:11ebddf765bff3bbe8dbce10c86884d87f90ed66ee410a7e6c392086e2c63d02", 66 | "sha256:14b242d53f6f35c2d07aa2c0e13ccb710392bcd203e1b82a1828d216f6f6b11f", 67 | "sha256:20a12ab46a7e72b89ce0671e7d7a6c3c1ca2c2766ac98112f78c5bddaa6e4375", 68 | "sha256:298f82c0ab1b182bd1f34f347ea97dde0fffb9ecf850ecf7f8904b8442a07487", 69 | "sha256:2f6175722da6f23dbfc76c26c241b67b020e1e83ec7fe93c9e5d3dd18667ada2", 70 | "sha256:3b877de633a0f6d81b600624ff9137312d8b1d0f517064dfc39999352ab659f0", 71 | "sha256:4265681e77f5ac5bac0905812b828c9fe1ce80c6f3e3f8574acfb5643aeabc5b", 72 | "sha256:550695c4e7313555549aa1cdb978dc9413d61307531f123558e438871a883d63", 73 | "sha256:5f4d42baed3a14c290a078e2696c5f565501abde1b2f3f1a1c0a94fbf6fbcc39", 74 | "sha256:62dd71dbed8cc6a18379700701d959307823b3b2451bdc018594c48956ace745", 75 | "sha256:7040547e5b882349c0a2cc9b50674b1745db551f330746af434aad4f09fba2cc", 76 | "sha256:7e099fde2cce8b29434684f82977db4e24f0efa8b0508179fce1602d103296a2", 77 | "sha256:7e5c9a5012b2b33e87980cee7d1c82412b2ebabcb5862d53413ba1a2cfde23aa", 78 | "sha256:81295629128f929e73be4ccfdd943a0906e5fe3cdb0d43ff1e5144d16fbb52b1", 79 | "sha256:95cc574b0b83b85be9917d37cd2fad0ce5a0d21b024e1a5804d044aabea636fc", 80 | "sha256:968d5c5702da15c5bf8e4a6e4b67a4d92164e334e9c0b6acf080106678230b98", 81 | "sha256:9e998ba87df77a85c7bed53240a7257afe51a07ee6bc3445a0bf841886da0b97", 82 | "sha256:a0c39e2535a7e9c195af956610dba5a1073071d2d85e9d2e5d789463f63e52ab", 83 | "sha256:a15e75d284178afe529a536b0e8b28b7e107ef39626a7809b4ee64ff3abc9127", 84 | "sha256:a6a6ff82f5f9b9702478035d8f6fb6903885653bff7ec3a1e011edc9b1a7168d", 85 | "sha256:b639f72b95389620c1f881d94739c614d385406ab1d6926a9ffe1c8abbea23fe", 86 | "sha256:bad44274b151d46619a7567010f7cde23a908c6faa84b97598fd2f474a0c6891", 87 | "sha256:bbcef00d09a30948756c5968863316c949d9cedbc7aabac5e8f0ffbdb632e5f1", 88 | "sha256:d788a3999014ddf416f2dc454efa4a5dbeda657c6aba031cf363741273804c6b", 89 | "sha256:eed88ae03e1ef3a75a0e96a55a99d7937ed03e53d0cffc2451c208db445a2966", 90 | "sha256:f99451f3a579e73b5dd58b1b08d1179791d49084371d9a47baad3b22417f0317" 91 | ], 92 | "version": "==4.6.0" 93 | }, 94 | "zope.component": { 95 | "hashes": [ 96 | "sha256:6edfd626c3b593b72895a8cfcf79bff41f4619194ce996a85bce31ac02b94e55", 97 | "sha256:984a06ba3def0b02b1117fa4c45b56e772e8c29c0340820fbf367e440a93a3a4" 98 | ], 99 | "index": "pypi", 100 | "version": "==4.5" 101 | }, 102 | "zope.deferredimport": { 103 | "hashes": [ 104 | "sha256:57b2345e7b5eef47efcd4f634ff16c93e4265de3dcf325afc7315ade48d909e1", 105 | "sha256:9a0c211df44aa95f1c4e6d2626f90b400f56989180d3ef96032d708da3d23e0a" 106 | ], 107 | "version": "==4.3.1" 108 | }, 109 | "zope.deprecation": { 110 | "hashes": [ 111 | "sha256:0d453338f04bacf91bbfba545d8bcdf529aa829e67b705eac8c1a7fdce66e2df", 112 | "sha256:f1480b74995958b24ce37b0ef04d3663d2683e5d6debc96726eff18acf4ea113" 113 | ], 114 | "version": "==4.4.0" 115 | }, 116 | "zope.event": { 117 | "hashes": [ 118 | "sha256:69c27debad9bdacd9ce9b735dad382142281ac770c4a432b533d6d65c4614bcf", 119 | "sha256:d8e97d165fd5a0997b45f5303ae11ea3338becfe68c401dd88ffd2113fe5cae7" 120 | ], 121 | "version": "==4.4" 122 | }, 123 | "zope.hookable": { 124 | "hashes": [ 125 | "sha256:22886e421234e7e8cedc21202e1d0ab59960e40a47dd7240e9659a2d82c51370", 126 | "sha256:39912f446e45b4e1f1951b5ffa2d5c8b074d25727ec51855ae9eab5408f105ab", 127 | "sha256:3adb7ea0871dbc56b78f62c4f5c024851fc74299f4f2a95f913025b076cde220", 128 | "sha256:3d7c4b96341c02553d8b8d71065a9366ef67e6c6feca714f269894646bb8268b", 129 | "sha256:4e826a11a529ed0464ffcecf34b0b7bd1b4928dd5848c5c61bedd7833e8f4801", 130 | "sha256:700d68cc30728de1c4c62088a981c6daeaefdf20a0d81995d2c0b7f442c5f88c", 131 | "sha256:77c82a430cedfbf508d1aa406b2f437363c24fa90c73f577ead0fb5295749b83", 132 | "sha256:c1df3929a3666fc5a0c80d60a0c1e6f6ef97c7f6ed2f1b7cf49f3e6f3d4dde15", 133 | "sha256:dba8b2dd2cd41cb5f37bfa3f3d82721b8ae10e492944e48ddd90a439227f2893", 134 | "sha256:f492540305b15b5591bd7195d61f28946bb071de071cee5d68b6b8414da90fd2" 135 | ], 136 | "version": "==4.2.0" 137 | }, 138 | "zope.interface": { 139 | "hashes": [ 140 | "sha256:086707e0f413ff8800d9c4bc26e174f7ee4c9c8b0302fbad68d083071822316c", 141 | "sha256:1157b1ec2a1f5bf45668421e3955c60c610e31913cc695b407a574efdbae1f7b", 142 | "sha256:11ebddf765bff3bbe8dbce10c86884d87f90ed66ee410a7e6c392086e2c63d02", 143 | "sha256:14b242d53f6f35c2d07aa2c0e13ccb710392bcd203e1b82a1828d216f6f6b11f", 144 | "sha256:1b3d0dcabc7c90b470e59e38a9acaa361be43b3a6ea644c0063951964717f0e5", 145 | "sha256:20a12ab46a7e72b89ce0671e7d7a6c3c1ca2c2766ac98112f78c5bddaa6e4375", 146 | "sha256:298f82c0ab1b182bd1f34f347ea97dde0fffb9ecf850ecf7f8904b8442a07487", 147 | "sha256:2f6175722da6f23dbfc76c26c241b67b020e1e83ec7fe93c9e5d3dd18667ada2", 148 | "sha256:3b877de633a0f6d81b600624ff9137312d8b1d0f517064dfc39999352ab659f0", 149 | "sha256:4265681e77f5ac5bac0905812b828c9fe1ce80c6f3e3f8574acfb5643aeabc5b", 150 | "sha256:550695c4e7313555549aa1cdb978dc9413d61307531f123558e438871a883d63", 151 | "sha256:5f4d42baed3a14c290a078e2696c5f565501abde1b2f3f1a1c0a94fbf6fbcc39", 152 | "sha256:62dd71dbed8cc6a18379700701d959307823b3b2451bdc018594c48956ace745", 153 | "sha256:7040547e5b882349c0a2cc9b50674b1745db551f330746af434aad4f09fba2cc", 154 | "sha256:7e099fde2cce8b29434684f82977db4e24f0efa8b0508179fce1602d103296a2", 155 | "sha256:7e5c9a5012b2b33e87980cee7d1c82412b2ebabcb5862d53413ba1a2cfde23aa", 156 | "sha256:81295629128f929e73be4ccfdd943a0906e5fe3cdb0d43ff1e5144d16fbb52b1", 157 | "sha256:95cc574b0b83b85be9917d37cd2fad0ce5a0d21b024e1a5804d044aabea636fc", 158 | "sha256:968d5c5702da15c5bf8e4a6e4b67a4d92164e334e9c0b6acf080106678230b98", 159 | "sha256:9e998ba87df77a85c7bed53240a7257afe51a07ee6bc3445a0bf841886da0b97", 160 | "sha256:a0c39e2535a7e9c195af956610dba5a1073071d2d85e9d2e5d789463f63e52ab", 161 | "sha256:a15e75d284178afe529a536b0e8b28b7e107ef39626a7809b4ee64ff3abc9127", 162 | "sha256:a6a6ff82f5f9b9702478035d8f6fb6903885653bff7ec3a1e011edc9b1a7168d", 163 | "sha256:b639f72b95389620c1f881d94739c614d385406ab1d6926a9ffe1c8abbea23fe", 164 | "sha256:bad44274b151d46619a7567010f7cde23a908c6faa84b97598fd2f474a0c6891", 165 | "sha256:bbcef00d09a30948756c5968863316c949d9cedbc7aabac5e8f0ffbdb632e5f1", 166 | "sha256:d788a3999014ddf416f2dc454efa4a5dbeda657c6aba031cf363741273804c6b", 167 | "sha256:eed88ae03e1ef3a75a0e96a55a99d7937ed03e53d0cffc2451c208db445a2966", 168 | "sha256:f99451f3a579e73b5dd58b1b08d1179791d49084371d9a47baad3b22417f0317" 169 | ], 170 | "index": "pypi", 171 | "version": "==4.6.0" 172 | }, 173 | "zope.proxy": { 174 | "hashes": [ 175 | "sha256:320a7619992e42142549ebf61e14ce27683b4d14b0cbc45f7c037ba64edb560c", 176 | "sha256:824d4dbabbb7deb84f25fdb96ea1eeca436a1802c3c8d323b3eb4ac9d527d41c", 177 | "sha256:8a32eb9c94908f3544da2dae3f4a9e6961d78819b88ac6b6f4a51cee2d65f4a0", 178 | "sha256:96265fd3bc3ea646f98482e16307a69de21402eeaaaaf4b841c1161ac2f71bb0", 179 | "sha256:ab6d6975d9c51c13cac828ff03168de21fb562b0664c59bcdc4a4b10f39a5b17", 180 | "sha256:af10cb772391772463f65a58348e2de5ecc06693c16d2078be276dc068bcbb54", 181 | "sha256:b8fd3a3de3f7b6452775e92af22af5977b17b69ac86a38a3ddfe870e40a0d05f", 182 | "sha256:bb7088f1bed3b8214284a5e425dc23da56f2f28e8815b7580bfed9e245b6c0b6", 183 | "sha256:bc29b3665eac34f14c4aef5224bef045efcfb1a7d12d78c8685858de5fbf21c0", 184 | "sha256:c39fa6a159affeae5fe31b49d9f5b12bd674fe77271a9a324408b271440c50a7", 185 | "sha256:e946a036ac5b9f897e986ac9dc950a34cffc857d88eae6727b8434fbc4752366" 186 | ], 187 | "version": "==4.3.2" 188 | } 189 | }, 190 | "develop": { 191 | "alabaster": { 192 | "hashes": [ 193 | "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", 194 | "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02" 195 | ], 196 | "version": "==0.7.12" 197 | }, 198 | "atomicwrites": { 199 | "hashes": [ 200 | "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", 201 | "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" 202 | ], 203 | "version": "==1.3.0" 204 | }, 205 | "attrs": { 206 | "hashes": [ 207 | "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", 208 | "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" 209 | ], 210 | "version": "==19.1.0" 211 | }, 212 | "babel": { 213 | "hashes": [ 214 | "sha256:af92e6106cb7c55286b25b38ad7695f8b4efb36a90ba483d7f7a6628c46158ab", 215 | "sha256:e86135ae101e31e2c8ec20a4e0c5220f4eed12487d5cf3f78be7e98d3a57fc28" 216 | ], 217 | "version": "==2.7.0" 218 | }, 219 | "certifi": { 220 | "hashes": [ 221 | "sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939", 222 | "sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695" 223 | ], 224 | "version": "==2019.6.16" 225 | }, 226 | "chardet": { 227 | "hashes": [ 228 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 229 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 230 | ], 231 | "version": "==3.0.4" 232 | }, 233 | "click": { 234 | "hashes": [ 235 | "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", 236 | "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" 237 | ], 238 | "version": "==7.0" 239 | }, 240 | "coverage": { 241 | "hashes": [ 242 | "sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", 243 | "sha256:0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", 244 | "sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", 245 | "sha256:19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", 246 | "sha256:23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", 247 | "sha256:245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", 248 | "sha256:331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", 249 | "sha256:386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", 250 | "sha256:3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", 251 | "sha256:60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", 252 | "sha256:63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", 253 | "sha256:6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", 254 | "sha256:6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", 255 | "sha256:7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", 256 | "sha256:826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", 257 | "sha256:93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", 258 | "sha256:9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", 259 | "sha256:af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", 260 | "sha256:bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", 261 | "sha256:bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", 262 | "sha256:c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", 263 | "sha256:dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", 264 | "sha256:df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", 265 | "sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", 266 | "sha256:e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", 267 | "sha256:e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", 268 | "sha256:eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", 269 | "sha256:eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", 270 | "sha256:ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", 271 | "sha256:efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", 272 | "sha256:fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", 273 | "sha256:ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025" 274 | ], 275 | "version": "==4.5.4" 276 | }, 277 | "coveralls": { 278 | "hashes": [ 279 | "sha256:baa26648430d5c2225ab12d7e2067f75597a4b967034bba7e3d5ab7501d207a1", 280 | "sha256:ff9b7823b15070f26f654837bb02a201d006baaf2083e0514ffd3b34a3ffed81" 281 | ], 282 | "index": "pypi", 283 | "version": "==1.7.0" 284 | }, 285 | "docopt": { 286 | "hashes": [ 287 | "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" 288 | ], 289 | "version": "==0.6.2" 290 | }, 291 | "docutils": { 292 | "hashes": [ 293 | "sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0", 294 | "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827", 295 | "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99" 296 | ], 297 | "version": "==0.15.2" 298 | }, 299 | "entrypoints": { 300 | "hashes": [ 301 | "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", 302 | "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" 303 | ], 304 | "version": "==0.3" 305 | }, 306 | "flake8": { 307 | "hashes": [ 308 | "sha256:19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", 309 | "sha256:8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696" 310 | ], 311 | "index": "pypi", 312 | "version": "==3.7.8" 313 | }, 314 | "flake8-isort": { 315 | "hashes": [ 316 | "sha256:1e67b6b90a9b980ac3ff73782087752d406ce0a729ed928b92797f9fa188917e", 317 | "sha256:81a8495eefed3f2f63f26cd2d766c7b1191e923a15b9106e6233724056572c68" 318 | ], 319 | "index": "pypi", 320 | "version": "==2.7.0" 321 | }, 322 | "idna": { 323 | "hashes": [ 324 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 325 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 326 | ], 327 | "version": "==2.8" 328 | }, 329 | "imagesize": { 330 | "hashes": [ 331 | "sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8", 332 | "sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5" 333 | ], 334 | "version": "==1.1.0" 335 | }, 336 | "importlib-metadata": { 337 | "hashes": [ 338 | "sha256:23d3d873e008a513952355379d93cbcab874c58f4f034ff657c7a87422fa64e8", 339 | "sha256:80d2de76188eabfbfcf27e6a37342c2827801e59c4cc14b0371c56fed43820e3" 340 | ], 341 | "markers": "python_version < '3.8'", 342 | "version": "==0.19" 343 | }, 344 | "incremental": { 345 | "hashes": [ 346 | "sha256:717e12246dddf231a349175f48d74d93e2897244939173b01974ab6661406b9f", 347 | "sha256:7b751696aaf36eebfab537e458929e194460051ccad279c72b755a167eebd4b3" 348 | ], 349 | "version": "==17.5.0" 350 | }, 351 | "isort": { 352 | "hashes": [ 353 | "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", 354 | "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" 355 | ], 356 | "version": "==4.3.21" 357 | }, 358 | "jinja2": { 359 | "hashes": [ 360 | "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", 361 | "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b" 362 | ], 363 | "version": "==2.10.1" 364 | }, 365 | "markupsafe": { 366 | "hashes": [ 367 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 368 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 369 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 370 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 371 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 372 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 373 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 374 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 375 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 376 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 377 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 378 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 379 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 380 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 381 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 382 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 383 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 384 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 385 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 386 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 387 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 388 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 389 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 390 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 391 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 392 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 393 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 394 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" 395 | ], 396 | "version": "==1.1.1" 397 | }, 398 | "mccabe": { 399 | "hashes": [ 400 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 401 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 402 | ], 403 | "version": "==0.6.1" 404 | }, 405 | "mock": { 406 | "hashes": [ 407 | "sha256:21a2c07af3bbc4a77f9d14ac18fcc1782e8e7ea363df718740cdeaf61995b5e7", 408 | "sha256:7868db2825a1563578869d4a011a036503a2f1d60f9ff9dd1e3205cd6e25fcec" 409 | ], 410 | "index": "pypi", 411 | "version": "==3.0.4" 412 | }, 413 | "more-itertools": { 414 | "hashes": [ 415 | "sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", 416 | "sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4" 417 | ], 418 | "version": "==7.2.0" 419 | }, 420 | "packaging": { 421 | "hashes": [ 422 | "sha256:a7ac867b97fdc07ee80a8058fe4435ccd274ecc3b0ed61d852d7d53055528cf9", 423 | "sha256:c491ca87294da7cc01902edbe30a5bc6c4c28172b5138ab4e4aa1b9d7bfaeafe" 424 | ], 425 | "version": "==19.1" 426 | }, 427 | "pluggy": { 428 | "hashes": [ 429 | "sha256:25a1bc1d148c9a640211872b4ff859878d422bccb59c9965e04eed468a0aa180", 430 | "sha256:964cedd2b27c492fbf0b7f58b3284a09cf7f99b0f715941fb24a439b3af1bd1a" 431 | ], 432 | "index": "pypi", 433 | "version": "==0.11.0" 434 | }, 435 | "py": { 436 | "hashes": [ 437 | "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", 438 | "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" 439 | ], 440 | "version": "==1.8.0" 441 | }, 442 | "pycodestyle": { 443 | "hashes": [ 444 | "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", 445 | "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" 446 | ], 447 | "version": "==2.5.0" 448 | }, 449 | "pyflakes": { 450 | "hashes": [ 451 | "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", 452 | "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" 453 | ], 454 | "version": "==2.1.1" 455 | }, 456 | "pygments": { 457 | "hashes": [ 458 | "sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127", 459 | "sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297" 460 | ], 461 | "version": "==2.4.2" 462 | }, 463 | "pyparsing": { 464 | "hashes": [ 465 | "sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", 466 | "sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4" 467 | ], 468 | "version": "==2.4.2" 469 | }, 470 | "pytest": { 471 | "hashes": [ 472 | "sha256:95b1f6db806e5b1b5b443efeb58984c24945508f93a866c1719e1a507a957d7c", 473 | "sha256:c3d5020755f70c82eceda3feaf556af9a341334414a8eca521a18f463bcead88" 474 | ], 475 | "version": "==5.1.1" 476 | }, 477 | "pytest-cov": { 478 | "hashes": [ 479 | "sha256:2b097cde81a302e1047331b48cadacf23577e431b61e9c6f49a1170bbe3d3da6", 480 | "sha256:e00ea4fdde970725482f1f35630d12f074e121a23801aabf2ae154ec6bdd343a" 481 | ], 482 | "index": "pypi", 483 | "version": "==2.7.1" 484 | }, 485 | "pytz": { 486 | "hashes": [ 487 | "sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32", 488 | "sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7" 489 | ], 490 | "version": "==2019.2" 491 | }, 492 | "requests": { 493 | "hashes": [ 494 | "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", 495 | "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" 496 | ], 497 | "version": "==2.22.0" 498 | }, 499 | "six": { 500 | "hashes": [ 501 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 502 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 503 | ], 504 | "version": "==1.12.0" 505 | }, 506 | "snowballstemmer": { 507 | "hashes": [ 508 | "sha256:9f3b9ffe0809d174f7047e121431acf99c89a7040f0ca84f94ba53a498e6d0c9" 509 | ], 510 | "version": "==1.9.0" 511 | }, 512 | "sphinx": { 513 | "hashes": [ 514 | "sha256:423280646fb37944dd3c85c58fb92a20d745793a9f6c511f59da82fa97cd404b", 515 | "sha256:de930f42600a4fef993587633984cc5027dedba2464bcf00ddace26b40f8d9ce" 516 | ], 517 | "index": "pypi", 518 | "version": "==2.0.1" 519 | }, 520 | "sphinx-rtd-theme": { 521 | "hashes": [ 522 | "sha256:00cf895504a7895ee433807c62094cf1e95f065843bf3acd17037c3e9a2becd4", 523 | "sha256:728607e34d60456d736cc7991fd236afb828b21b82f956c5ea75f94c8414040a" 524 | ], 525 | "index": "pypi", 526 | "version": "==0.4.3" 527 | }, 528 | "sphinxcontrib-applehelp": { 529 | "hashes": [ 530 | "sha256:edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897", 531 | "sha256:fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d" 532 | ], 533 | "version": "==1.0.1" 534 | }, 535 | "sphinxcontrib-devhelp": { 536 | "hashes": [ 537 | "sha256:6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34", 538 | "sha256:9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981" 539 | ], 540 | "version": "==1.0.1" 541 | }, 542 | "sphinxcontrib-htmlhelp": { 543 | "hashes": [ 544 | "sha256:4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422", 545 | "sha256:d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7" 546 | ], 547 | "version": "==1.0.2" 548 | }, 549 | "sphinxcontrib-jsmath": { 550 | "hashes": [ 551 | "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", 552 | "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" 553 | ], 554 | "version": "==1.0.1" 555 | }, 556 | "sphinxcontrib-qthelp": { 557 | "hashes": [ 558 | "sha256:513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20", 559 | "sha256:79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f" 560 | ], 561 | "version": "==1.0.2" 562 | }, 563 | "sphinxcontrib-serializinghtml": { 564 | "hashes": [ 565 | "sha256:c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227", 566 | "sha256:db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768" 567 | ], 568 | "version": "==1.1.3" 569 | }, 570 | "testfixtures": { 571 | "hashes": [ 572 | "sha256:665a298976c8d77f311b65c46f16b7cda7229a47dff5ad7c822e5b3371a439e2", 573 | "sha256:9d230c5c80746f9f86a16a1f751a5cf5d8e317d4cc48243a19fb180d22303bce" 574 | ], 575 | "version": "==6.10.0" 576 | }, 577 | "toml": { 578 | "hashes": [ 579 | "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", 580 | "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" 581 | ], 582 | "version": "==0.10.0" 583 | }, 584 | "towncrier": { 585 | "hashes": [ 586 | "sha256:48251a1ae66d2cf7e6fa5552016386831b3e12bb3b2d08eb70374508c17a8196", 587 | "sha256:de19da8b8cb44f18ea7ed3a3823087d2af8fcf497151bb9fd1e1b092ff56ed8d" 588 | ], 589 | "index": "pypi", 590 | "version": "==19.2.0" 591 | }, 592 | "urllib3": { 593 | "hashes": [ 594 | "sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1", 595 | "sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232" 596 | ], 597 | "version": "==1.25.3" 598 | }, 599 | "wcwidth": { 600 | "hashes": [ 601 | "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", 602 | "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" 603 | ], 604 | "version": "==0.1.7" 605 | }, 606 | "zipp": { 607 | "hashes": [ 608 | "sha256:4970c3758f4e89a7857a973b1e2a5d75bcdc47794442f2e2dd4fe8e0466e809a", 609 | "sha256:8a5712cfd3bb4248015eb3b0b3c54a5f6ee3f2425963ef2a0125b8bc40aafaec" 610 | ], 611 | "version": "==0.5.2" 612 | } 613 | } 614 | } 615 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | PyPOM 2 | ===== 3 | 4 | PyPOM is a Python Page Object Model library for Selenium and Splinter tests. 5 | 6 | .. image:: https://img.shields.io/badge/license-MPL%202.0-blue.svg 7 | :target: https://github.com/mozilla/PyPOM/blob/master/LICENSE.txt 8 | :alt: License 9 | .. image:: https://img.shields.io/pypi/v/PyPOM.svg 10 | :target: https://pypi.python.org/pypi/PyPOM/ 11 | :alt: PyPI 12 | .. image:: https://img.shields.io/travis/mozilla/PyPOM.svg 13 | :target: https://travis-ci.org/mozilla/PyPOM/ 14 | :alt: Travis 15 | .. image:: https://img.shields.io/coveralls/mozilla/PyPOM.svg 16 | :target: https://coveralls.io/github/mozilla/PyPOM 17 | :alt: Coverage 18 | .. image:: https://img.shields.io/badge/docs-latest-brightgreen.svg 19 | :target: http://pypom.readthedocs.io/en/latest/ 20 | :alt: Read the Docs 21 | .. image:: https://img.shields.io/github/issues-raw/mozilla/PyPOM.svg 22 | :target: https://github.com/mozilla/PyPOM/issues 23 | :alt: Issues 24 | .. image:: https://img.shields.io/badge/code%20style-black-000000.svg 25 | :target: https://github.com/ambv/black 26 | .. image:: https://api.dependabot.com/badges/status?host=github&repo=mozilla/PyPOM 27 | :target: https://dependabot.com 28 | :alt: Dependabot 29 | 30 | Resources 31 | --------- 32 | 33 | - `Documentation `_ 34 | - `Issue Tracker `_ 35 | - `Code `_ 36 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | _static 3 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PyPOM.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PyPOM.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/PyPOM" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PyPOM" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | Developer Interface 2 | =================== 3 | This part of the documentation describes the interfaces for using PyPOM. 4 | 5 | 6 | .. _Page: 7 | 8 | Page 9 | ---- 10 | 11 | .. py:module:: pypom.page 12 | 13 | .. autoclass:: Page 14 | :inherited-members: 15 | 16 | 17 | .. _Region: 18 | 19 | Region 20 | ------- 21 | 22 | .. py:module:: pypom.region 23 | 24 | .. autoclass:: Region 25 | :inherited-members: 26 | 27 | .. _hooks: 28 | 29 | Hooks 30 | ----- 31 | 32 | .. automodule:: pypom.hooks 33 | :members: 34 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # PyPOM documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Dec 10 11:38:25 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | import shlex 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | sys.path.insert(0, os.path.abspath("..")) 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.viewcode"] 34 | 35 | # intersphinx mappings 36 | intersphinx_mapping = { 37 | "selenium": ("http://seleniumhq.github.io/selenium/docs/api/py/", None), 38 | "splinter": ("https://splinter.readthedocs.io/en/latest/", None), 39 | } 40 | 41 | # Add any paths that contain templates here, relative to this directory. 42 | templates_path = ["_templates"] 43 | 44 | # The suffix(es) of source filenames. 45 | # You can specify multiple suffix as a list of string: 46 | # source_suffix = ['.rst', '.md'] 47 | source_suffix = ".rst" 48 | 49 | # The encoding of source files. 50 | # source_encoding = 'utf-8-sig' 51 | 52 | # The master toctree document. 53 | master_doc = "index" 54 | 55 | # General information about the project. 56 | project = u"PyPOM" 57 | copyright = u"2016, Mozilla" 58 | author = u"Mozilla" 59 | 60 | # The version info for the project you're documenting, acts as replacement for 61 | # |version| and |release|, also used in various other places throughout the 62 | # built documents. 63 | # 64 | # The short X.Y version. 65 | version = u"latest" 66 | # The full version, including alpha/beta/rc tags. 67 | release = u"latest" 68 | 69 | # The language for content autogenerated by Sphinx. Refer to documentation 70 | # for a list of supported languages. 71 | # 72 | # This is also used if you do content translation via gettext catalogs. 73 | # Usually you set "language" from the command line for these cases. 74 | language = None 75 | 76 | # There are two options for replacing |today|: either, you set today to some 77 | # non-false value, then it is used: 78 | # today = '' 79 | # Else, today_fmt is used as the format for a strftime call. 80 | # today_fmt = '%B %d, %Y' 81 | 82 | # List of patterns, relative to source directory, that match files and 83 | # directories to ignore when looking for source files. 84 | exclude_patterns = ["_build"] 85 | 86 | # The reST default role (used for this markup: `text`) to use for all 87 | # documents. 88 | # default_role = None 89 | 90 | # If true, '()' will be appended to :func: etc. cross-reference text. 91 | # add_function_parentheses = True 92 | 93 | # If true, the current module name will be prepended to all description 94 | # unit titles (such as .. function::). 95 | # add_module_names = True 96 | 97 | # If true, sectionauthor and moduleauthor directives will be shown in the 98 | # output. They are ignored by default. 99 | # show_authors = False 100 | 101 | # The name of the Pygments (syntax highlighting) style to use. 102 | pygments_style = "sphinx" 103 | 104 | # A list of ignored prefixes for module index sorting. 105 | # modindex_common_prefix = [] 106 | 107 | # If true, keep warnings as "system message" paragraphs in the built documents. 108 | # keep_warnings = False 109 | 110 | # If true, `todo` and `todoList` produce output, else they produce nothing. 111 | todo_include_todos = False 112 | 113 | # Set the sort order of automatically documented members. 114 | # autodoc_member_order = 'bysource' 115 | 116 | 117 | # -- Options for HTML output ---------------------------------------------- 118 | 119 | # Use ReadTheDocs theme locally, but not when built on RTD itself. RTD 120 | # will be confused if we specify the custom theme there. 121 | on_rtd = os.environ.get("READTHEDOCS", None) == "True" 122 | if not on_rtd: 123 | import sphinx_rtd_theme 124 | 125 | html_theme = "sphinx_rtd_theme" 126 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 127 | else: 128 | html_theme = "default" 129 | 130 | # The theme to use for HTML and HTML Help pages. See the documentation for 131 | # a list of builtin themes. 132 | # html_theme = 'default' 133 | 134 | # Theme options are theme-specific and customize the look and feel of a theme 135 | # further. For a list of options available for each theme, see the 136 | # documentation. 137 | # html_theme_options = {} 138 | 139 | # Add any paths that contain custom themes here, relative to this directory. 140 | # html_theme_path = [] 141 | 142 | # The name for this set of Sphinx documents. If None, it defaults to 143 | # " v documentation". 144 | # html_title = None 145 | 146 | # A shorter title for the navigation bar. Default is the same as html_title. 147 | # html_short_title = None 148 | 149 | # The name of an image file (relative to this directory) to place at the top 150 | # of the sidebar. 151 | # html_logo = None 152 | 153 | # The name of an image file (within the static path) to use as favicon of the 154 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 155 | # pixels large. 156 | # html_favicon = None 157 | 158 | # Add any paths that contain custom static files (such as style sheets) here, 159 | # relative to this directory. They are copied after the builtin static files, 160 | # so a file named "default.css" will overwrite the builtin "default.css". 161 | html_static_path = [] 162 | 163 | # Add any extra paths that contain custom files (such as robots.txt or 164 | # .htaccess) here, relative to this directory. These files are copied 165 | # directly to the root of the documentation. 166 | # html_extra_path = [] 167 | 168 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 169 | # using the given strftime format. 170 | # html_last_updated_fmt = '%b %d, %Y' 171 | 172 | # If true, SmartyPants will be used to convert quotes and dashes to 173 | # typographically correct entities. 174 | # html_use_smartypants = True 175 | 176 | # Custom sidebar templates, maps document names to template names. 177 | # html_sidebars = {} 178 | 179 | # Additional templates that should be rendered to pages, maps page names to 180 | # template names. 181 | # html_additional_pages = {} 182 | 183 | # If false, no module index is generated. 184 | # html_domain_indices = True 185 | 186 | # If false, no index is generated. 187 | # html_use_index = True 188 | 189 | # If true, the index is split into individual pages for each letter. 190 | # html_split_index = False 191 | 192 | # If true, links to the reST sources are added to the pages. 193 | # html_show_sourcelink = True 194 | 195 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 196 | # html_show_sphinx = True 197 | 198 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 199 | # html_show_copyright = True 200 | 201 | # If true, an OpenSearch description file will be output, and all pages will 202 | # contain a tag referring to it. The value of this option must be the 203 | # base URL from which the finished HTML is served. 204 | # html_use_opensearch = '' 205 | 206 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 207 | # html_file_suffix = None 208 | 209 | # Language to be used for generating the HTML full-text search index. 210 | # Sphinx supports the following languages: 211 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 212 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 213 | # html_search_language = 'en' 214 | 215 | # A dictionary with options for the search language support, empty by default. 216 | # Now only 'ja' uses this config value 217 | # html_search_options = {'type': 'default'} 218 | 219 | # The name of a javascript file (relative to the configuration directory) that 220 | # implements a search results scorer. If empty, the default will be used. 221 | # html_search_scorer = 'scorer.js' 222 | 223 | # Output file base name for HTML help builder. 224 | htmlhelp_basename = "PyPOMdoc" 225 | 226 | # -- Options for LaTeX output --------------------------------------------- 227 | 228 | latex_elements = { 229 | # The paper size ('letterpaper' or 'a4paper'). 230 | # 'papersize': 'letterpaper', 231 | # The font size ('10pt', '11pt' or '12pt'). 232 | # 'pointsize': '10pt', 233 | # Additional stuff for the LaTeX preamble. 234 | # 'preamble': '', 235 | # Latex figure (float) alignment 236 | # 'figure_align': 'htbp', 237 | } 238 | 239 | # Grouping the document tree into LaTeX files. List of tuples 240 | # (source start file, target name, title, 241 | # author, documentclass [howto, manual, or own class]). 242 | latex_documents = [ 243 | (master_doc, "PyPOM.tex", u"PyPOM Documentation", u"Mozilla", "manual") 244 | ] 245 | 246 | # The name of an image file (relative to this directory) to place at the top of 247 | # the title page. 248 | # latex_logo = None 249 | 250 | # For "manual" documents, if this is true, then toplevel headings are parts, 251 | # not chapters. 252 | # latex_use_parts = False 253 | 254 | # If true, show page references after internal links. 255 | # latex_show_pagerefs = False 256 | 257 | # If true, show URL addresses after external links. 258 | # latex_show_urls = False 259 | 260 | # Documents to append as an appendix to all manuals. 261 | # latex_appendices = [] 262 | 263 | # If false, no module index is generated. 264 | # latex_domain_indices = True 265 | 266 | 267 | # -- Options for manual page output --------------------------------------- 268 | 269 | # One entry per manual page. List of tuples 270 | # (source start file, name, description, authors, manual section). 271 | man_pages = [(master_doc, "pypom", u"PyPOM Documentation", [author], 1)] 272 | 273 | # If true, show URL addresses after external links. 274 | # man_show_urls = False 275 | 276 | 277 | # -- Options for Texinfo output ------------------------------------------- 278 | 279 | # Grouping the document tree into Texinfo files. List of tuples 280 | # (source start file, target name, title, author, 281 | # dir menu entry, description, category) 282 | texinfo_documents = [ 283 | ( 284 | master_doc, 285 | "PyPOM", 286 | u"PyPOM Documentation", 287 | author, 288 | "PyPOM", 289 | "One line description of project.", 290 | "Miscellaneous", 291 | ) 292 | ] 293 | 294 | # Documents to append as an appendix to all manuals. 295 | # texinfo_appendices = [] 296 | 297 | # If false, no module index is generated. 298 | # texinfo_domain_indices = True 299 | 300 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 301 | # texinfo_show_urls = 'footnote' 302 | 303 | # If true, do not generate a @detailmenu in the "Top" node's menu. 304 | # texinfo_no_detailmenu = False 305 | -------------------------------------------------------------------------------- /docs/development.rst: -------------------------------------------------------------------------------- 1 | Development 2 | =========== 3 | 4 | Automated Testing 5 | ----------------- 6 | 7 | All pull requests and merges are tested in `Travis CI `_ 8 | based on the ``.travis.yml`` file. 9 | 10 | Usually, a link to your specific travis build appears in pull requests, but if 11 | not, you can find it on the 12 | `pull requests page `_ 13 | 14 | The only way to trigger Travis CI to run again for a pull request, is to submit 15 | another change to the pull branch. 16 | 17 | Running Tests 18 | ------------- 19 | 20 | You will need `Tox `_ installed to run the tests 21 | against the supported Python versions. 22 | 23 | .. code-block:: bash 24 | 25 | $ pip install tox 26 | $ tox 27 | -------------------------------------------------------------------------------- /docs/examples/nested_regions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Nested Regions Example

5 |
Main Page 6 | 7 | 16 | 23 |
24 | 25 | -------------------------------------------------------------------------------- /docs/examples/nested_regions.py: -------------------------------------------------------------------------------- 1 | from pypom import Region, Page 2 | from selenium.webdriver.common.by import By 3 | 4 | 5 | class MainPage(Page): 6 | @property 7 | def menu1(self): 8 | root = self.find_element(By.ID, "menu1") 9 | return Menu(self, root=root) 10 | 11 | @property 12 | def menu2(self): 13 | root = self.find_element(By.ID, "menu2") 14 | return Menu(self, root=root) 15 | 16 | 17 | class Menu(Region): 18 | @property 19 | def entries(self): 20 | return [ 21 | Entry(self.page, item) for item in self.find_elements(*Entry.entry_locator) 22 | ] 23 | 24 | 25 | class Entry(Region): 26 | entry_locator = (By.CLASS_NAME, "entry") 27 | 28 | @property 29 | def name(self): 30 | return self.root.text 31 | -------------------------------------------------------------------------------- /docs/examples/repeated_regions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Repeated Regions Example

5 | 6 |
    7 |
  1. 8 | Result 1 9 | detail 10 |
  2. 11 |
  3. 12 | Result 2 13 | detail 14 |
  4. 15 |
  5. 16 | Result 3 17 | detail 18 |
  6. 19 |
  7. 20 | Result 4 21 | detail 22 |
  8. 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/examples/repeated_regions.py: -------------------------------------------------------------------------------- 1 | from pypom import Page, Region 2 | from selenium.webdriver.common.by import By 3 | 4 | 5 | class Results(Page): 6 | _result_locator = (By.CLASS_NAME, "result") 7 | 8 | @property 9 | def results(self): 10 | return [ 11 | self.Result(self, el) for el in self.find_elements(*self._result_locator) 12 | ] 13 | 14 | class Result(Region): 15 | _name_locator = (By.CLASS_NAME, "name") 16 | _detail_locator = (By.TAG_NAME, "a") 17 | 18 | @property 19 | def name(self): 20 | return self.find_element(*self._name_locator).text 21 | 22 | @property 23 | def detail_link(self): 24 | return self.find_element(*self._detail_locator).get_property("href") 25 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | PyPOM - Python Page Object Model 2 | ================================ 3 | 4 | PyPOM, or Python Page Object Model, is a Python library that provides a base 5 | page object model for use with Selenium_ or Splinter_ functional tests. 6 | 7 | It is tested on Python 2.7 and 3.6. 8 | 9 | Contributions are welcome. Feel free to fork_ and contribute! 10 | 11 | .. _Selenium: http://seleniumhq.org/ 12 | .. _Splinter: https://github.com/cobrateam/splinter 13 | .. _fork: https://github.com/mozilla/PyPOM 14 | 15 | 16 | .. toctree:: 17 | :maxdepth: 2 18 | 19 | installing 20 | user_guide 21 | plugins 22 | api 23 | development 24 | news 25 | -------------------------------------------------------------------------------- /docs/installing.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Requirements 5 | ------------ 6 | 7 | PyPOM requires Python >= 2.7. 8 | 9 | Install PyPOM 10 | ----------------------- 11 | 12 | To install PyPOM using `pip `_: 13 | 14 | .. code-block:: bash 15 | 16 | $ pip install PyPOM 17 | 18 | If you want to use PyPOM with Splinter install the optional 19 | splinter support: 20 | 21 | .. code-block:: bash 22 | 23 | $ pip install PyPOM[splinter] 24 | 25 | To install from source: 26 | 27 | .. code-block:: bash 28 | 29 | $ python setup.py develop 30 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 1>NUL 2>NUL 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PyPOM.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PyPOM.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /docs/news.rst: -------------------------------------------------------------------------------- 1 | Release Notes 2 | ============= 3 | 4 | .. towncrier release notes start 5 | 6 | 2.2.0 (2018-10-29) 7 | ================== 8 | 9 | Deprecations and Removals 10 | ------------------------- 11 | 12 | - Removed PhantomJS support from Splinter driver due to removal in Splinter v0.9.0. (#93) 13 | 14 | 15 | 2.1.0 (2018-08-13) 16 | ================== 17 | 18 | Bugfixes 19 | -------- 20 | 21 | - Replace use of ``implprefix`` with ``HookimplMarker`` due to deprecation. 22 | 23 | Existing PyPOM plugins will need to be updated to import the `hookimpl` and use 24 | it to decorate hook implementations rather than rely on the prefix of the 25 | function names. 26 | 27 | Before:: 28 | 29 | def pypom_after_wait_for_page_to_load(page): 30 | pass 31 | 32 | After:: 33 | 34 | from pypom import hookimpl 35 | 36 | @hookimpl 37 | def pypom_after_wait_for_page_to_load(page): 38 | pass (#90) 39 | 40 | 41 | 2.0.0 (2018-04-17) 42 | ================== 43 | 44 | * Added support for plugins. 45 | 46 | * This introduces plugin hooks ``pypom_after_wait_for_page_to_load`` and 47 | ``pypom_after_wait_for_region_to_load``. 48 | * In order to take advantage of plugin support you must avoid implementing 49 | ``wait_for_page_to_load`` or ``wait_for_region_to_load`` in your page 50 | objects. 51 | * This was previously the only way to implement a custom wait for your pages 52 | and regions, but now means the calls to plugin hooks would be bypassed. 53 | * Custom waits can now be achieved by implementing a ``loaded`` property on 54 | the page or region, which returns ``True`` when the page or region has 55 | finished loading. 56 | * See the user guide for more details. 57 | 58 | * Any unused ``url_kwargs`` after formatting ``URL_TEMPLATE`` are added as URL 59 | query string parameters. 60 | 61 | 1.3.0 (2018-02-28) 62 | ================== 63 | 64 | * Added support for EventFiringWebDriver 65 | 66 | * Thanks to `@Greums `_ for the PR 67 | 68 | 1.2.0 (2017-06-20) 69 | ================== 70 | 71 | * Dropped support for Python 2.6 72 | 73 | 1.1.1 (2016-11-21) 74 | ================== 75 | 76 | * Fixed packaging of ``pypom.interfaces`` 77 | 78 | 1.1.0 (2016-11-17) 79 | ================== 80 | 81 | * Added support for Splinter 82 | 83 | * Thanks to `@davidemoro `_ for the PR 84 | 85 | 1.0.0 (2016-05-24) 86 | ================== 87 | 88 | * Official release 89 | -------------------------------------------------------------------------------- /docs/plugins.rst: -------------------------------------------------------------------------------- 1 | Plugins 2 | ======= 3 | 4 | Plugin support was added in v2.0. 5 | 6 | Writing plugins 7 | --------------- 8 | 9 | PyPOM uses `pluggy`_ to enable support for plugins. In order to write a plugin 10 | you can create an installable Python package with a specific entry point. For 11 | example, the following (incomplete) ``setup.py`` will register a plugin named 12 | screenshot:: 13 | 14 | from setuptools import setup 15 | 16 | setup(name='PyPOM-screenshot', 17 | description='plugin for PyPOM that takes a lot of screenshots', 18 | packages=['pypom_screenshot'], 19 | install_requires=['PyPOM'], 20 | entry_points={'pypom.plugin': ['screenshot = pypom_screenshot.plugin']}) 21 | 22 | Then, in your package implement one or more of the plugin :ref:`hooks` provided 23 | by PyPOM. The following example will take a screenshot whenever a page or 24 | region has finished loading:: 25 | 26 | 27 | from pypom import hookimpl 28 | 29 | @hookimpl 30 | def pypom_after_wait_for_page_to_load(page): 31 | page.selenium.get_screenshot_as_file(page.__class__.__name__ + '.png') 32 | 33 | 34 | @hookimpl 35 | def pypom_after_wait_for_region_to_load(region): 36 | region.root.screenshot(region.__class__.__name__ + '.png') 37 | 38 | .. _pluggy: https://pluggy.readthedocs.io/ 39 | -------------------------------------------------------------------------------- /docs/user_guide.rst: -------------------------------------------------------------------------------- 1 | User Guide 2 | ========== 3 | 4 | .. contents:: :depth: 3 5 | 6 | Upgrading to 2.0 7 | ---------------- 8 | 9 | Plugin support was introduced in v2.0, and if you're upgrading from an earlier 10 | version you may need to make some changes to take advantage of any plugins. 11 | Before this version, to implement a custom wait for pages/regions to finish 12 | loading it was necessary to implement ``wait_for_page_to_load`` or 13 | ``wait_for_region_to_load``. If you haven't implemented either of these, you 14 | don't need to do anything to upgrade. If you have, then whilst your custom 15 | waits will still work, we now support plugins that can be triggered after a 16 | page/region load, and these calls are made from the base classes. By overriding 17 | the default behaviour, you may be missing out on triggering any plugin 18 | behaviours. Rather than having to remember to always call the same method from 19 | the parent, you can simply change your custom wait to a new ``loaded`` property 20 | that returns ``True`` when the page/region has loaded. 21 | 22 | So, if you have implemented your own 23 | :py:func:`~pypom.page.Page.wait_for_page_to_load` like this:: 24 | 25 | def wait_for_page_to_load(self): 26 | self.wait.until(lambda s: self.seed_url in s.current_url) 27 | 28 | You will want to change it to use :py:attr:`~pypom.page.Page.loaded` like this:: 29 | 30 | @property 31 | def loaded(self): 32 | return self.seed_url in self.selenium.current_url 33 | 34 | Similarly, if you have implemented your own 35 | :py:func:`~pypom.region.Region.wait_for_region_to_load` like this:: 36 | 37 | def wait_for_region_to_load(self): 38 | self.wait.until(lambda s: self.root.is_displayed()) 39 | 40 | You will want to change it to use :py:attr:`~pypom.region.Region.loaded` like 41 | this:: 42 | 43 | @property 44 | def loaded(self): 45 | return self.root.is_displayed() 46 | 47 | Drivers 48 | ------- 49 | 50 | PyPOM requires a driver object to be instantiated, and supports multiple driver 51 | types. The examples in this guide will assume that you have a driver instance. 52 | 53 | Selenium 54 | ~~~~~~~~ 55 | 56 | To instantiate a Selenium_ driver you will need a 57 | :py:class:`~selenium.webdriver.remote.webdriver.WebDriver` object:: 58 | 59 | from selenium.webdriver import Firefox 60 | driver = Firefox() 61 | 62 | Splinter 63 | ~~~~~~~~ 64 | 65 | To instantiate a Splinter_ driver you will need a :py:class:`~splinter.Browser` 66 | object:: 67 | 68 | from splinter import Browser 69 | driver = Browser() 70 | 71 | Pages 72 | ----- 73 | 74 | Page objects are representations of web pages. They provide functions to allow 75 | simulating user actions, and providing properties that return state from the 76 | page. The :py:class:`~pypom.page.Page` class provided by PyPOM provides a 77 | simple implementation that can be sub-classed to apply to your project. 78 | 79 | The following very simple example instantiates a page object representing the 80 | landing page of the Mozilla website:: 81 | 82 | from pypom import Page 83 | 84 | class Mozilla(Page): 85 | pass 86 | 87 | page = Mozilla(driver) 88 | 89 | If a page has a seed URL then you can call the :py:func:`~pypom.page.Page.open` 90 | function to open the page in the browser. There are a number of ways to specify 91 | a seed URL. 92 | 93 | Base URL 94 | ~~~~~~~~ 95 | 96 | A base URL can be passed to a page object on instantiation. If no URL template 97 | is provided, then calling :py:func:`~pypom.page.Page.open` will open this base 98 | URL:: 99 | 100 | from pypom import Page 101 | 102 | class Mozilla(Page): 103 | pass 104 | 105 | base_url = 'https://www.mozilla.org' 106 | page = Mozilla(driver, base_url).open() 107 | 108 | URL templates 109 | ~~~~~~~~~~~~~ 110 | 111 | By setting a value for :py:attr:`~pypom.page.Page.URL_TEMPLATE`, pages can 112 | specify either an absolute URL or one that is relative to the base URL (when 113 | provided). In the following example, the URL https://www.mozilla.org/about/ 114 | will be opened:: 115 | 116 | from pypom import Page 117 | 118 | class Mozilla(Page): 119 | URL_TEMPLATE = '/about/' 120 | 121 | base_url = 'https://www.mozilla.org' 122 | page = Mozilla(driver, base_url).open() 123 | 124 | As this is a template, any additional keyword arguments passed when 125 | instantiating the page object will attempt to resolve any placeholders. In the 126 | following example, the URL https://www.mozilla.org/de/about/ will be opened:: 127 | 128 | from pypom import Page 129 | 130 | class Mozilla(Page): 131 | URL_TEMPLATE = '/{locale}/about/' 132 | 133 | base_url = 'https://www.mozilla.org' 134 | page = Mozilla(driver, base_url, locale='de').open() 135 | 136 | URL parameters 137 | ~~~~~~~~~~~~~~ 138 | 139 | Any keyword arguments provided that are not used as placeholders in the URL 140 | template are added as query string parameters. In the following example, the 141 | URL https://developer.mozilla.org/fr/search?q=bold&topic=css will be opened:: 142 | 143 | from pypom import Page 144 | 145 | class Search(Page): 146 | URL_TEMPLATE = '/{locale}/search' 147 | 148 | base_url = 'https://developer.mozilla.org/' 149 | page = Search(driver, base_url, locale='fr', q='bold', topic='css').open() 150 | 151 | Waiting for pages to load 152 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 153 | 154 | Whenever a driver detects that a page is loading, it does its best to block 155 | until it's complete. Unfortunately, as the driver does not know your 156 | application, it's quite common for it to return earlier than a user would 157 | consider the page to be ready. For this reason, the 158 | :py:attr:`~pypom.page.Page.loaded` property can be overridden and customised 159 | for your project's needs by returning ``True`` when the page has loaded. This 160 | property is polled by :py:func:`~pypom.page.Page.wait_for_page_to_load`, which 161 | is called by :py:func:`~pypom.page.Page.open` after loading the seed URL, and 162 | can be called directly by functions that cause a page to load. 163 | 164 | The following example waits for the seed URL to be in the current URL. You can 165 | use this so long as the URL is not rewritten or redirected by your 166 | application:: 167 | 168 | from pypom import Page 169 | 170 | class Mozilla(Page): 171 | 172 | @property 173 | def loaded(self): 174 | return self.seed_url in self.selenium.current_url 175 | 176 | Other things to wait for might include when elements are displayed or enabled, 177 | or when an element has a particular class. This will be very dependent on your 178 | application. 179 | 180 | Regions 181 | ------- 182 | 183 | Region objects represent one or more elements of a web page that are repeated 184 | multiple times on a page, or shared between multiple web pages. They prevent 185 | duplication, and can improve the readability and maintainability of your page 186 | objects. 187 | 188 | Root elements 189 | ~~~~~~~~~~~~~ 190 | 191 | It's important for page regions to have a root element. This is the element 192 | that any child elements will be located within. This means that page region 193 | locators do not need to be unique on the page, only unique within the context 194 | of the root element. 195 | 196 | If your page region contains a :py:attr:`~pypom.region.Region._root_locator` 197 | attribute, this will be used to locate the root element every time an instance 198 | of the region is created. This is recommended for most page regions as it 199 | avoids issues when the root element becomes stale. 200 | 201 | Alternatively, you can locate the root element yourself and pass it to the 202 | region on construction. This is useful when creating regions that are repeated 203 | on a single page. 204 | 205 | The root element can later be accessed via the 206 | :py:attr:`~pypom.region.Region.root` attribute on the region, which may be 207 | necessary if you need to interact with it. 208 | 209 | Repeating regions 210 | ~~~~~~~~~~~~~~~~~ 211 | 212 | Page regions are useful when you have multiple items on a page that share the 213 | same characteristics, such as a list of search results. By creating a page 214 | region, you can interact with any of these items in a common way: 215 | 216 | The following example uses Selenium_ to locate all results on a page and return 217 | a list of ``Result`` regions. This can be used to determine the number of 218 | results, and each result can be accessed from this list for further state or 219 | interactions. Refer to `locating elements`_ for more information on how to 220 | write locators for your driver: 221 | 222 | .. literalinclude:: examples/repeated_regions.html 223 | :language: html 224 | :emphasize-lines: 6-23 225 | 226 | .. literalinclude:: examples/repeated_regions.py 227 | :language: python 228 | :emphasize-lines: 6-8 229 | :lines: 5-24 230 | 231 | Nested regions 232 | ~~~~~~~~~~~~~~ 233 | 234 | Regions can be nested inside other regions (i.e. a menu region with multiple entry 235 | regions). In the following example a main page contains two menu regions that 236 | include multiple repeated entry regions: 237 | 238 | .. literalinclude:: examples/nested_regions.html 239 | :language: html 240 | 241 | As a region requires a page object to be passed you need 242 | to pass ``self.page`` when instantiating nested regions: 243 | 244 | .. literalinclude:: examples/nested_regions.py 245 | :language: python 246 | :emphasize-lines: 4-5,9-10,16-18 247 | :lines: 5-30 248 | 249 | 250 | Shared regions 251 | ~~~~~~~~~~~~~~ 252 | 253 | Pages with common characteristics can use regions to avoid duplication. 254 | Examples of this include page headers, navigation menus, login forms, and 255 | footers. These regions can either be defined in a base page object that is 256 | inherited by the pages that contain the region, or they can exist in their own 257 | module: 258 | 259 | In the following example, any page objects that extend ``Base`` will inherit 260 | the ``header`` property, and be able to check if it's displayed. Refer to 261 | `locating elements`_ for more information on how to write locators for your 262 | driver:: 263 | 264 | from pypom import Page, Region 265 | from selenium.webdriver.common.by import By 266 | 267 | class Base(Page): 268 | 269 | @property 270 | def header(self): 271 | return self.Header(self) 272 | 273 | class Header(Region): 274 | _root_locator = (By.ID, 'header') 275 | 276 | def is_displayed(self): 277 | return self.root.is_displayed() 278 | 279 | Waiting for regions to load 280 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 281 | 282 | The :py:attr:`~pypom.region.Region.loaded` property function can be 283 | overridden and customised for your project's needs by returning ``True`` when 284 | the region has loaded to ensure it's ready for interaction. This property is 285 | polled by :py:attr:`~pypom.region.Region.wait_for_region_to_load`, which is 286 | called whenever a region is instantiated, and can be called directly by 287 | functions that a region to reload. 288 | 289 | The following example waits for an element within a page region to be 290 | displayed:: 291 | 292 | from pypom import Region 293 | 294 | class Header(Region): 295 | 296 | @property 297 | def loaded(self): 298 | return self.root.is_displayed() 299 | 300 | Other things to wait for might include when elements are displayed or enabled, 301 | or when an element has a particular class. This will be very dependent on your 302 | application. 303 | 304 | Locating elements 305 | ----------------- 306 | 307 | Each driver has its own approach to locating elements. A suggested approach is 308 | to store your locators at the top of your page/region classes. Ideally these 309 | should be preceeded with a single underscore to indicate that they're primarily 310 | reserved for internal use. These attributes can be stored as a two item tuple 311 | containing both the strategy and locator, and can then be unpacked when passed 312 | to a method that requires the arguments to be separated. 313 | 314 | Selenium 315 | ~~~~~~~~ 316 | 317 | The :py:class:`~selenium.webdriver.common.by.By` class covers the common 318 | locator strategies for Selenium_. The following example shows a locator being 319 | defined and used in a page object:: 320 | 321 | from pypom import Page 322 | from selenium.webdriver.common.by import By 323 | 324 | class Mozilla(Page): 325 | _logo_locator = (By.ID, 'logo') 326 | 327 | @property 328 | def loaded(self): 329 | logo = self.find_element(*self._logo_locator) 330 | return logo.is_displayed() 331 | 332 | Splinter 333 | ~~~~~~~~ 334 | 335 | The available locator strategies for Splinter_ are: 336 | 337 | * name 338 | * id 339 | * css 340 | * xpath 341 | * text 342 | * value 343 | * tag 344 | 345 | The following example shows a locator being defined and used in a page object:: 346 | 347 | from pypom import Page 348 | from selenium.webdriver.common.by import By 349 | 350 | class Mozilla(Page): 351 | _logo_locator = ('id', 'logo') 352 | 353 | @property 354 | def loaded(self): 355 | logo = self.find_element(*self._logo_locator) 356 | return logo.is_displayed() 357 | 358 | Explicit waits 359 | -------------- 360 | 361 | For convenience, a :py:class:`~selenium.webdriver.support.wait.WebDriverWait` 362 | object is instantiated with an optional timeout (with a default of 10 seconds) 363 | for every page. This allows your page objects to define an explicit wait 364 | whenever an interaction causes a reponse that a real user would wait for before 365 | continuing. For example, checking a box might make a button become enabled. If 366 | we didn't wait for the button to become enabled we may try clicking on it too 367 | early, and nothing would happen. Another example of where explicit waits are 368 | common is when `waiting for pages to load`_ or `waiting for regions to load`_. 369 | 370 | The following example uses Selenium_ to demonstrate a wait that is necessary 371 | after checking a box that causes a button to become enabled. Refer to 372 | `locating elements`_ for more information on how to write locators for your 373 | driver:: 374 | 375 | from pypom import Page 376 | from selenium.webdriver.common.by import By 377 | 378 | class Mozilla(Page): 379 | _privacy_policy_locator = (By.ID, 'privacy') 380 | _sign_me_up_locator = (By.ID, 'sign_up') 381 | 382 | def accept_privacy_policy(self): 383 | self.find_element(*self._privacy_policy_locator).click() 384 | sign_me_up = self.find_element(*self._sign_me_up_locator) 385 | self.wait.until(lambda s: sign_me_up.is_enabled()) 386 | 387 | You can either specify a timeout by passing the optional ``timeout`` keyword 388 | argument when instantiating a page object, or you can override the 389 | :py:func:`~pypom.page.Page.__init__` method if you want your timeout to be 390 | inherited by a base project page class. 391 | 392 | .. note:: 393 | 394 | The default timeout of 10 seconds may be considered excessive, and you may 395 | wish to reduce it. Increasing the timeout is not recommended. If you have 396 | interactions that take longer than the default you may find that you have 397 | a performance issue that will considerably affect the user experience. 398 | 399 | .. _Selenium: http://docs.seleniumhq.org/ 400 | .. _Splinter: https://github.com/cobrateam/splinter 401 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.towncrier] 2 | package = "pypom" 3 | package_dir = "src" 4 | filename = "docs/news.rst" 5 | title_format = "{version} ({project_date})" 6 | -------------------------------------------------------------------------------- /requirements/pipenv.txt: -------------------------------------------------------------------------------- 1 | pipenv==2018.10.13 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [coverage:run] 5 | source = src/pypom 6 | 7 | [flake8] 8 | exclude = .eggs,.tox,docs 9 | ignore=E501 10 | 11 | [isort] 12 | default_section = THIRDPARTY 13 | known_first_party = pypom 14 | skip = .tox, build, docs 15 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | splinter_requires = ["splinter"] 4 | 5 | setup( 6 | name="PyPOM", 7 | use_scm_version=True, 8 | description="python page object model for selenium", 9 | long_description=open("README.rst").read(), 10 | author="Dave Hunt", 11 | author_email="dhunt@mozilla.com", 12 | url="https://github.com/mozilla/PyPOM", 13 | package_dir={"": "src"}, 14 | packages=["pypom", "pypom.interfaces"], 15 | install_requires=["zope.interface", "zope.component", "pluggy", "selenium"], 16 | setup_requires=["setuptools_scm"], 17 | extras_require={"splinter": splinter_requires}, 18 | license="Mozilla Public License 2.0 (MPL 2.0)", 19 | keywords="pypom page object model selenium", 20 | classifiers=[ 21 | "Development Status :: 5 - Production/Stable", 22 | "Intended Audience :: Developers", 23 | "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", 24 | "Operating System :: POSIX", 25 | "Operating System :: Microsoft :: Windows", 26 | "Operating System :: MacOS :: MacOS X", 27 | "Topic :: Software Development :: Quality Assurance", 28 | "Topic :: Software Development :: Testing", 29 | "Topic :: Utilities", 30 | "Programming Language :: Python", 31 | "Programming Language :: Python :: 2.7", 32 | "Programming Language :: Python :: 3.6", 33 | ], 34 | ) 35 | -------------------------------------------------------------------------------- /src/pypom/__init__.py: -------------------------------------------------------------------------------- 1 | from .page import Page # noqa 2 | from .region import Region # noqa 3 | 4 | import pluggy 5 | import selenium # noqa 6 | 7 | # register selenium support 8 | from .selenium_driver import register as registerSelenium 9 | 10 | registerSelenium() 11 | 12 | try: 13 | import splinter # noqa 14 | except ImportError: # pragma: no cover 15 | pass # pragma: no cover 16 | else: 17 | from .splinter_driver import register as registerSplinter 18 | 19 | registerSplinter() 20 | 21 | hookimpl = pluggy.HookimplMarker("pypom") 22 | -------------------------------------------------------------------------------- /src/pypom/driver.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | from zope import component 6 | from zope.interface import classImplements 7 | 8 | from .interfaces import IDriver 9 | 10 | 11 | def registerDriver(iface, driver, class_implements=[]): 12 | """ Register driver adapter used by page object""" 13 | for class_item in class_implements: 14 | classImplements(class_item, iface) 15 | 16 | component.provideAdapter(factory=driver, adapts=[iface], provides=IDriver) 17 | -------------------------------------------------------------------------------- /src/pypom/exception.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | 6 | class UsageError(Exception): 7 | """PyPOM usage error.""" 8 | 9 | pass 10 | -------------------------------------------------------------------------------- /src/pypom/hooks.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | from pluggy import HookspecMarker 6 | 7 | hookspec = HookspecMarker("pypom") 8 | 9 | 10 | @hookspec 11 | def pypom_after_wait_for_page_to_load(page): 12 | """Called after waiting for the page to load""" 13 | 14 | 15 | @hookspec 16 | def pypom_after_wait_for_region_to_load(region): 17 | """Called after waiting for the region to load""" 18 | -------------------------------------------------------------------------------- /src/pypom/interfaces/__init__.py: -------------------------------------------------------------------------------- 1 | from .driver import IDriver # noqa 2 | -------------------------------------------------------------------------------- /src/pypom/interfaces/driver.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | from zope.interface import Interface 6 | 7 | 8 | class ISplinter(Interface): 9 | """ Marker interface for Splinter""" 10 | 11 | 12 | class IDriver(Interface): 13 | """ Driver interface """ 14 | 15 | def wait_factory(timeout): 16 | """Returns a WebDriverWait like property for a given timeout. 17 | 18 | :param timeout: Timeout used by WebDriverWait like calls 19 | :type timeout: int 20 | """ 21 | 22 | def open(url): 23 | """Open the page. 24 | Navigates to :py:attr:`url` 25 | """ 26 | 27 | def find_element(strategy, locator, root=None): 28 | """Finds an element on the page. 29 | 30 | :param strategy: Location strategy to use (type depends on the driver implementation) 31 | :param locator: Location of target element. 32 | :param root: (optional) root node. 33 | :type strategy: str 34 | :type locator: str 35 | :type root: web element object or None. 36 | :return: web element object 37 | :rtype: it depends on the driver implementation 38 | """ 39 | 40 | def find_elements(strategy, locator, root=None): 41 | """Finds elements on the page. 42 | 43 | :param strategy: Location strategy to use (type depends on the driver implementation) 44 | :param locator: Location of target elements. 45 | :param root: (optional) root node. 46 | :type strategy: str 47 | :type locator: str 48 | :type root: web element object or None. 49 | :return: iterable of web element objects 50 | :rtype: iterable (if depends on the driver implementation) 51 | """ 52 | 53 | def is_element_present(strategy, locator, root=None): 54 | """Checks whether an element is present. 55 | 56 | :param strategy: Location strategy to use (type depends on the driver implementation) 57 | :param locator: Location of target element. 58 | :param root: (optional) root node. 59 | :type strategy: str 60 | :type locator: str 61 | :type root: web element object or None. 62 | :return: ``True`` if element is present, else ``False``. 63 | :rtype: bool 64 | """ 65 | 66 | def is_element_displayed(strategy, locator, root=None): 67 | """Checks whether an element is displayed. 68 | 69 | :param strategy: Location strategy to use (type depends on the driver implementation) 70 | :param locator: Location of target element. 71 | :param root: (optional) root node. 72 | :type strategy: str 73 | :type locator: str 74 | :type root: web element object or None. 75 | :return: ``True`` if element is displayed, else ``False``. 76 | :rtype: bool 77 | """ 78 | -------------------------------------------------------------------------------- /src/pypom/newsfragments/.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | -------------------------------------------------------------------------------- /src/pypom/page.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import collections 6 | import sys 7 | 8 | from .exception import UsageError 9 | from .view import WebView 10 | 11 | if sys.version_info >= (3,): 12 | import urllib.parse as urlparse 13 | from urllib.parse import urlencode 14 | else: 15 | import urlparse 16 | from urllib import urlencode 17 | 18 | 19 | def iterable(arg): 20 | if isinstance(arg, collections.Iterable) and not isinstance(arg, str): 21 | return arg 22 | return [arg] 23 | 24 | 25 | class Page(WebView): 26 | """A page object. 27 | 28 | Used as a base class for your project's page objects. 29 | 30 | :param driver: A driver. 31 | :param base_url: (optional) Base URL. 32 | :param timeout: (optional) Timeout used for explicit waits. Defaults to ``10``. 33 | :param url_kwargs: (optional) Keyword arguments used when generating the :py:attr:`seed_url`. 34 | :type driver: :py:class:`~selenium.webdriver.remote.webdriver.WebDriver` or :py:class:`~splinter.browser.Browser` 35 | :type base_url: str 36 | :type timeout: int 37 | 38 | Usage (Selenium):: 39 | 40 | from pypom import Page 41 | from selenium.webdriver import Firefox 42 | 43 | class Mozilla(Page): 44 | URL_TEMPLATE = 'https://www.mozilla.org/{locale}' 45 | 46 | driver = Firefox() 47 | page = Mozilla(driver, locale='en-US') 48 | page.open() 49 | 50 | Usage (Splinter):: 51 | 52 | from pypom import Page 53 | from splinter import Browser 54 | 55 | class Mozilla(Page): 56 | URL_TEMPLATE = 'https://www.mozilla.org/{locale}' 57 | 58 | driver = Browser() 59 | page = Mozilla(driver, locale='en-US') 60 | page.open() 61 | 62 | """ 63 | 64 | URL_TEMPLATE = None 65 | """Template string representing a URL that can be used to open the page. 66 | 67 | This string is formatted and can contain names of keyword arguments passed 68 | during construction of the page object. The template should either assume 69 | that its result will be appended to the value of :py:attr:`base_url`, or 70 | should yield an absolute URL. 71 | 72 | Examples:: 73 | 74 | URL_TEMPLATE = 'https://www.mozilla.org/' # absolute URL 75 | URL_TEMPLATE = '/search' # relative to base URL 76 | URL_TEMPLATE = '/search?q={term}' # keyword argument expansion 77 | 78 | """ 79 | 80 | def __init__(self, driver, base_url=None, timeout=10, **url_kwargs): 81 | super(Page, self).__init__(driver, timeout) 82 | self.base_url = base_url 83 | self.url_kwargs = url_kwargs 84 | 85 | @property 86 | def seed_url(self): 87 | """A URL that can be used to open the page. 88 | 89 | The URL is formatted from :py:attr:`URL_TEMPLATE`, which is then 90 | appended to :py:attr:`base_url` unless the template results in an 91 | absolute URL. 92 | 93 | :return: URL that can be used to open the page. 94 | :rtype: str 95 | 96 | """ 97 | url = self.base_url 98 | if self.URL_TEMPLATE is not None: 99 | url = urlparse.urljoin( 100 | self.base_url, self.URL_TEMPLATE.format(**self.url_kwargs) 101 | ) 102 | 103 | if not url: 104 | return None 105 | 106 | url_parts = list(urlparse.urlparse(url)) 107 | query = urlparse.parse_qsl(url_parts[4]) 108 | 109 | for k, v in self.url_kwargs.items(): 110 | if v is None: 111 | continue 112 | if "{{{}}}".format(k) not in str(self.URL_TEMPLATE): 113 | for i in iterable(v): 114 | query.append((k, i)) 115 | 116 | url_parts[4] = urlencode(query) 117 | return urlparse.urlunparse(url_parts) 118 | 119 | def open(self): 120 | """Open the page. 121 | 122 | Navigates to :py:attr:`seed_url` and calls :py:func:`wait_for_page_to_load`. 123 | 124 | :return: The current page object. 125 | :rtype: :py:class:`Page` 126 | :raises: UsageError 127 | 128 | """ 129 | if self.seed_url: 130 | self.driver_adapter.open(self.seed_url) 131 | self.wait_for_page_to_load() 132 | return self 133 | raise UsageError("Set a base URL or URL_TEMPLATE to open this page.") 134 | 135 | def wait_for_page_to_load(self): 136 | """Wait for the page to load.""" 137 | self.wait.until(lambda _: self.loaded) 138 | self.pm.hook.pypom_after_wait_for_page_to_load(page=self) 139 | return self 140 | 141 | @property 142 | def loaded(self): 143 | """Loaded state of the page. 144 | 145 | By default the driver will try to wait for any page loads to be 146 | complete, however it's not uncommon for it to return early. To address 147 | this you can override :py:attr:`loaded` to return ``True`` when the 148 | page has finished loading. 149 | 150 | :return: ``True`` if page is loaded, else ``False``. 151 | :rtype: bool 152 | 153 | Usage (Selenium):: 154 | 155 | from pypom import Page 156 | from selenium.webdriver.common.by import By 157 | 158 | class Mozilla(Page): 159 | 160 | @property 161 | def loaded(self): 162 | body = self.find_element(By.TAG_NAME, 'body') 163 | return 'loaded' in body.get_attribute('class') 164 | 165 | Usage (Splinter):: 166 | 167 | from pypom import Page 168 | 169 | class Mozilla(Page): 170 | 171 | def loaded(self): 172 | body = self.find_element('tag', 'body') 173 | return 'loaded' in body['class'] 174 | 175 | Examples:: 176 | 177 | # wait for the seed_url value to be in the current URL 178 | self.seed_url in self.selenium.current_url 179 | 180 | """ 181 | return True 182 | -------------------------------------------------------------------------------- /src/pypom/region.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | from .view import WebView 6 | 7 | 8 | class Region(WebView): 9 | """A page region object. 10 | 11 | Used as a base class for your project's page region objects. 12 | 13 | :param page: Page object this region appears in. 14 | :param root: (optional) element that serves as the root for the region. 15 | :type page: :py:class:`~.page.Page` 16 | :type root: :py:class:`~selenium.webdriver.remote.webelement.WebElement` or :py:class:`~splinter.driver.webdriver.WebDriverElement` 17 | 18 | Usage (Selenium):: 19 | 20 | from pypom import Page, Region 21 | from selenium.webdriver import Firefox 22 | from selenium.webdriver.common.by import By 23 | 24 | class Mozilla(Page): 25 | URL_TEMPLATE = 'https://www.mozilla.org/' 26 | 27 | @property 28 | def newsletter(self): 29 | return Newsletter(self) 30 | 31 | class Newsletter(Region): 32 | _root_locator = (By.ID, 'newsletter-form') 33 | _submit_locator = (By.ID, 'footer_email_submit') 34 | 35 | def sign_up(self): 36 | self.find_element(*self._submit_locator).click() 37 | 38 | driver = Firefox() 39 | page = Mozilla(driver).open() 40 | page.newsletter.sign_up() 41 | 42 | Usage (Splinter):: 43 | 44 | from pypom import Page, Region 45 | from splinter import Browser 46 | 47 | class Mozilla(Page): 48 | URL_TEMPLATE = 'https://www.mozilla.org/' 49 | 50 | @property 51 | def newsletter(self): 52 | return Newsletter(self) 53 | 54 | class Newsletter(Region): 55 | _root_locator = ('id', 'newsletter-form') 56 | _submit_locator = ('id', 'footer_email_submit') 57 | 58 | def sign_up(self): 59 | self.find_element(*self._submit_locator).click() 60 | 61 | driver = Browser() 62 | page = Mozilla(driver).open() 63 | page.newsletter.sign_up() 64 | 65 | """ 66 | 67 | _root_locator = None 68 | 69 | def __init__(self, page, root=None): 70 | super(Region, self).__init__(page.driver, page.timeout, pm=page.pm) 71 | self._root = root 72 | self.page = page 73 | self.wait_for_region_to_load() 74 | 75 | @property 76 | def root(self): 77 | """Root element for the page region. 78 | 79 | Page regions should define a root element either by passing this on 80 | instantiation or by defining a :py:attr:`_root_locator` attribute. To 81 | reduce the chances of hitting :py:class:`~selenium.common.exceptions.StaleElementReferenceException` 82 | or similar you should use :py:attr:`_root_locator`, as this is looked up every 83 | time the :py:attr:`root` property is accessed. 84 | """ 85 | if self._root is None and self._root_locator is not None: 86 | return self.page.find_element(*self._root_locator) 87 | return self._root 88 | 89 | def wait_for_region_to_load(self): 90 | """Wait for the page region to load.""" 91 | self.wait.until(lambda _: self.loaded) 92 | self.pm.hook.pypom_after_wait_for_region_to_load(region=self) 93 | return self 94 | 95 | def find_element(self, strategy, locator): 96 | """Finds an element on the page. 97 | 98 | :param strategy: Location strategy to use. See :py:class:`~selenium.webdriver.common.by.By` or :py:attr:`~pypom.splinter_driver.ALLOWED_STRATEGIES`. 99 | :param locator: Location of target element. 100 | :type strategy: str 101 | :type locator: str 102 | :return: An element. 103 | :rytpe: :py:class:`~selenium.webdriver.remote.webelement.WebElement` or :py:class:`~splinter.driver.webdriver.WebDriverElement` 104 | 105 | """ 106 | return self.driver_adapter.find_element(strategy, locator, root=self.root) 107 | 108 | def find_elements(self, strategy, locator): 109 | """Finds elements on the page. 110 | 111 | :param strategy: Location strategy to use. See :py:class:`~selenium.webdriver.common.by.By` or :py:attr:`~pypom.splinter_driver.ALLOWED_STRATEGIES`. 112 | :param locator: Location of target elements. 113 | :type strategy: str 114 | :type locator: str 115 | :return: List of :py:class:`~selenium.webdriver.remote.webelement.WebElement` or :py:class:`~splinter.element_list.ElementList` 116 | :rtype: list 117 | 118 | """ 119 | return self.driver_adapter.find_elements(strategy, locator, root=self.root) 120 | 121 | def is_element_present(self, strategy, locator): 122 | """Checks whether an element is present. 123 | 124 | :param strategy: Location strategy to use. See :py:class:`~selenium.webdriver.common.by.By` or :py:attr:`~pypom.splinter_driver.ALLOWED_STRATEGIES`. 125 | :param locator: Location of target element. 126 | :type strategy: str 127 | :type locator: str 128 | :return: ``True`` if element is present, else ``False``. 129 | :rtype: bool 130 | 131 | """ 132 | return self.driver_adapter.is_element_present(strategy, locator, root=self.root) 133 | 134 | def is_element_displayed(self, strategy, locator): 135 | """Checks whether an element is displayed. 136 | 137 | :param strategy: Location strategy to use. See :py:class:`~selenium.webdriver.common.by.By` or :py:attr:`~pypom.splinter_driver.ALLOWED_STRATEGIES`. 138 | :param locator: Location of target element. 139 | :type strategy: str 140 | :type locator: str 141 | :return: ``True`` if element is displayed, else ``False``. 142 | :rtype: bool 143 | 144 | """ 145 | return self.driver_adapter.is_element_displayed( 146 | strategy, locator, root=self.root 147 | ) 148 | 149 | @property 150 | def loaded(self): 151 | """Loaded state of the page region. 152 | 153 | You may need to initialise your page region before it's ready for you 154 | to interact with it. If this is the case, you can override 155 | :py:attr:`loaded` to return ``True`` when the region has finished 156 | loading. 157 | 158 | :return: ``True`` if page is loaded, else ``False``. 159 | :rtype: bool 160 | 161 | Usage (Selenium):: 162 | 163 | from pypom import Page, Region 164 | from selenium.webdriver.common.by import By 165 | 166 | class Mozilla(Page): 167 | URL_TEMPLATE = 'https://www.mozilla.org/' 168 | 169 | @property 170 | def newsletter(self): 171 | return Newsletter(self) 172 | 173 | class Newsletter(Region): 174 | _root_locator = (By.ID, 'newsletter-form') 175 | 176 | @property 177 | def loaded(self): 178 | return 'loaded' in self.root.get_attribute('class') 179 | 180 | Usage (Splinter):: 181 | 182 | from pypom import Page, Region 183 | 184 | class Mozilla(Page): 185 | URL_TEMPLATE = 'https://www.mozilla.org/' 186 | 187 | @property 188 | def newsletter(self): 189 | return Newsletter(self) 190 | 191 | class Newsletter(Region): 192 | _root_locator = ('id', 'newsletter-form') 193 | 194 | @property 195 | def loaded(self): 196 | return 'loaded' in self.root['class'] 197 | 198 | """ 199 | return True 200 | -------------------------------------------------------------------------------- /src/pypom/selenium_driver.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | 6 | from selenium.common.exceptions import NoSuchElementException 7 | from selenium.webdriver import ( 8 | Android, 9 | BlackBerry, 10 | Chrome, 11 | Edge, 12 | Firefox, 13 | Ie, 14 | Opera, 15 | PhantomJS, 16 | Remote, 17 | Safari, 18 | ) 19 | from selenium.webdriver.support.events import EventFiringWebDriver 20 | from selenium.webdriver.support.ui import WebDriverWait 21 | from zope.interface import Interface, implementer 22 | 23 | from .driver import registerDriver 24 | from .interfaces import IDriver 25 | 26 | 27 | class ISelenium(Interface): 28 | """ Marker interface for Selenium""" 29 | 30 | 31 | @implementer(IDriver) 32 | class Selenium(object): 33 | def __init__(self, driver): 34 | self.driver = driver 35 | 36 | def wait_factory(self, timeout): 37 | """Returns a WebDriverWait like property for a given timeout. 38 | 39 | :param timeout: Timeout used by WebDriverWait calls 40 | :type timeout: int 41 | """ 42 | return WebDriverWait(self.driver, timeout) 43 | 44 | def open(self, url): 45 | """Open the page. 46 | Navigates to :py:attr:`url` 47 | """ 48 | self.driver.get(url) 49 | 50 | def find_element(self, strategy, locator, root=None): 51 | """Finds an element on the page. 52 | 53 | :param strategy: Location strategy to use. See :py:class:`~selenium.webdriver.common.by.By` for valid values. 54 | :param locator: Location of target element. 55 | :param root: (optional) root node. 56 | :type strategy: str 57 | :type locator: str 58 | :type root: str :py:class:`~selenium.webdriver.remote.webelement.WebElement` object or None. 59 | :return: :py:class:`~selenium.webdriver.remote.webelement.WebElement` object. 60 | :rtype: selenium.webdriver.remote.webelement.WebElement 61 | 62 | """ 63 | if root is not None: 64 | return root.find_element(strategy, locator) 65 | return self.driver.find_element(strategy, locator) 66 | 67 | def find_elements(self, strategy, locator, root=None): 68 | """Finds elements on the page. 69 | 70 | :param strategy: Location strategy to use. See :py:class:`~selenium.webdriver.common.by.By` for valid values. 71 | :param locator: Location of target elements. 72 | :param root: (optional) root node. 73 | :type strategy: str 74 | :type locator: str 75 | :type root: str :py:class:`~selenium.webdriver.remote.webelement.WebElement` object or None. 76 | :return: List of :py:class:`~selenium.webdriver.remote.webelement.WebElement` objects. 77 | :rtype: list 78 | 79 | """ 80 | if root is not None: 81 | return root.find_elements(strategy, locator) 82 | return self.driver.find_elements(strategy, locator) 83 | 84 | def is_element_present(self, strategy, locator, root=None): 85 | """Checks whether an element is present. 86 | 87 | :param strategy: Location strategy to use. See :py:class:`~selenium.webdriver.common.by.By` for valid values. 88 | :param locator: Location of target element. 89 | :param root: (optional) root node. 90 | :type strategy: str 91 | :type locator: str 92 | :type root: str :py:class:`~selenium.webdriver.remote.webelement.WebElement` object or None. 93 | :return: ``True`` if element is present, else ``False``. 94 | :rtype: bool 95 | 96 | """ 97 | try: 98 | return self.find_element(strategy, locator, root=root) 99 | except NoSuchElementException: 100 | return False 101 | 102 | def is_element_displayed(self, strategy, locator, root=None): 103 | """Checks whether an element is displayed. 104 | 105 | :param strategy: Location strategy to use. See :py:class:`~selenium.webdriver.common.by.By` for valid values. 106 | :param locator: Location of target element. 107 | :param root: (optional) root node. 108 | :type strategy: str 109 | :type locator: str 110 | :type root: str :py:class:`~selenium.webdriver.remote.webelement.WebElement` object or None. 111 | :return: ``True`` if element is displayed, else ``False``. 112 | :rtype: bool 113 | 114 | """ 115 | try: 116 | return self.find_element(strategy, locator, root=root).is_displayed() 117 | except NoSuchElementException: 118 | return False 119 | 120 | 121 | def register(): 122 | """ Register the Selenium specific driver implementation. 123 | 124 | This register call is performed by the init module if 125 | selenium is available. 126 | """ 127 | registerDriver( 128 | ISelenium, 129 | Selenium, 130 | class_implements=[ 131 | Firefox, 132 | Chrome, 133 | Ie, 134 | Edge, 135 | Opera, 136 | Safari, 137 | BlackBerry, 138 | PhantomJS, 139 | Android, 140 | Remote, 141 | EventFiringWebDriver, 142 | ], 143 | ) 144 | -------------------------------------------------------------------------------- /src/pypom/splinter_driver.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | 6 | from splinter.driver.webdriver.chrome import WebDriver as ChromeWebDriver 7 | from splinter.driver.webdriver.firefox import WebDriver as FirefoxWebDriver 8 | from splinter.driver.webdriver.remote import WebDriver as RemoteWebDriver 9 | from zope.interface import Interface, implementer 10 | 11 | from .driver import registerDriver 12 | from .exception import UsageError 13 | from .interfaces import IDriver 14 | from .selenium_driver import Selenium 15 | 16 | ALLOWED_STRATEGIES = ["name", "id", "css", "xpath", "text", "value", "tag"] 17 | 18 | 19 | class ISplinter(Interface): 20 | """ Marker interface for Splinter""" 21 | 22 | 23 | @implementer(IDriver) 24 | class Splinter(Selenium): 25 | def __init__(self, driver): 26 | self.driver = driver 27 | 28 | def open(self, url): 29 | """Open the page. 30 | Navigates to :py:attr:`url` 31 | """ 32 | self.driver.visit(url) 33 | 34 | def find_element(self, strategy, locator, root=None): 35 | """Finds an element on the page. 36 | 37 | :param strategy: Location strategy to use. See pypom.splinter_driver.ALLOWED_STRATEGIES for valid values. 38 | :param locator: Location of target element. 39 | :type strategy: str 40 | :type locator: str 41 | :return: :py:class:`~splinter.driver.webdriver.WebDriverElement`. 42 | :rtype: splinter.driver.webdriver.WebDriverElement 43 | 44 | """ 45 | elements = self.find_elements(strategy, locator, root=root) 46 | return elements and elements.first or None 47 | 48 | def find_elements(self, strategy, locator, root=None): 49 | """Finds elements on the page. 50 | 51 | :param strategy: Location strategy to use. See pypom.splinter_driver.ALLOWED_STRATEGIES for valid values. 52 | :param locator: Location of target elements. 53 | :type strategy: str 54 | :type locator: str 55 | :return: List of :py:class:`~splinter.driver.webdriver.WebDriverElement` 56 | :rtype: :py:class:`splinter.element_list.ElementList` 57 | 58 | """ 59 | node = root or self.driver 60 | 61 | if strategy in ALLOWED_STRATEGIES: 62 | return getattr(node, "find_by_" + strategy)(locator) 63 | raise UsageError("Strategy not allowed") 64 | 65 | def is_element_present(self, strategy, locator, root=None): 66 | """Checks whether an element is present. 67 | 68 | :param strategy: Location strategy to use. See pypom.splinter_driver.ALLOWED_STRATEGIES for valid values. 69 | :param locator: Location of target element. 70 | :type strategy: str 71 | :type locator: str 72 | :return: ``True`` if element is present, else ``False``. 73 | :rtype: bool 74 | 75 | """ 76 | return self.find_element(strategy, locator, root=root) and True or False 77 | 78 | def is_element_displayed(self, strategy, locator, root=None): 79 | """Checks whether an element is displayed. 80 | 81 | :param strategy: Location strategy to use. See pypom.splinter_driver.ALLOWED_STRATEGIES for valid values. 82 | :param locator: Location of target element. 83 | :type strategy: str 84 | :type locator: str 85 | :return: ``True`` if element is displayed, else ``False``. 86 | :rtype: bool 87 | 88 | """ 89 | 90 | element = self.find_element(strategy, locator, root=root) 91 | return element and element.visible or False 92 | 93 | 94 | def register(): 95 | """ Register the Selenium specific driver implementation. 96 | 97 | This register call is performed by the init module if 98 | selenium is available. 99 | """ 100 | registerDriver( 101 | ISplinter, 102 | Splinter, 103 | class_implements=[ 104 | FirefoxWebDriver, 105 | ChromeWebDriver, 106 | RemoteWebDriver, 107 | ], 108 | ) 109 | -------------------------------------------------------------------------------- /src/pypom/view.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | from warnings import warn 6 | 7 | from pluggy import PluginManager 8 | 9 | from pypom import hooks 10 | 11 | from .interfaces import IDriver 12 | 13 | 14 | class WebView(object): 15 | def __init__(self, driver, timeout, pm=None): 16 | self.driver = driver 17 | self.driver_adapter = IDriver(driver) 18 | self.timeout = timeout 19 | self.pm = pm 20 | if self.pm is None: 21 | self.pm = PluginManager("pypom") 22 | self.pm.add_hookspecs(hooks) 23 | self.pm.load_setuptools_entrypoints("pypom.plugin") 24 | self.pm.check_pending() 25 | self.wait = self.driver_adapter.wait_factory(self.timeout) 26 | 27 | @property 28 | def selenium(self): 29 | """Backwards compatibility attribute""" 30 | warn("use driver instead", DeprecationWarning, stacklevel=2) 31 | return self.driver 32 | 33 | def find_element(self, strategy, locator): 34 | return self.driver_adapter.find_element(strategy, locator) 35 | 36 | def find_elements(self, strategy, locator): 37 | """Finds elements on the page. 38 | 39 | :param strategy: Location strategy to use. See :py:class:`~selenium.webdriver.common.by.By` or :py:attr:`~pypom.splinter_driver.ALLOWED_STRATEGIES`. 40 | :param locator: Location of target elements. 41 | :type strategy: str 42 | :type locator: str 43 | :return: List of :py:class:`~selenium.webdriver.remote.webelement.WebElement` or :py:class:`~splinter.element_list.ElementList` 44 | :rtype: list 45 | 46 | """ 47 | return self.driver_adapter.find_elements(strategy, locator) 48 | 49 | def is_element_present(self, strategy, locator): 50 | """Checks whether an element is present. 51 | 52 | :param strategy: Location strategy to use. See :py:class:`~selenium.webdriver.common.by.By` or :py:attr:`~pypom.splinter_driver.ALLOWED_STRATEGIES`. 53 | :param locator: Location of target element. 54 | :type strategy: str 55 | :type locator: str 56 | :return: ``True`` if element is present, else ``False``. 57 | :rtype: bool 58 | 59 | """ 60 | return self.driver_adapter.is_element_present(strategy, locator) 61 | 62 | def is_element_displayed(self, strategy, locator): 63 | """Checks whether an element is displayed. 64 | 65 | :param strategy: Location strategy to use. See :py:class:`~selenium.webdriver.common.by.By` or :py:attr:`~pypom.splinter_driver.ALLOWED_STRATEGIES`. 66 | :param locator: Location of target element. 67 | :type strategy: str 68 | :type locator: str 69 | :return: ``True`` if element is displayed, else ``False``. 70 | :rtype: bool 71 | 72 | """ 73 | return self.driver_adapter.is_element_displayed(strategy, locator) 74 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/PyPOM/9cb84df9d27b428b4e7423d1bbe6502e92990154/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import pytest 6 | from mock import Mock 7 | from zope.interface import alsoProvides 8 | 9 | from pypom.selenium_driver import ISelenium 10 | from pypom.splinter_driver import ALLOWED_STRATEGIES, ISplinter 11 | 12 | 13 | @pytest.fixture 14 | def base_url(): 15 | return "https://www.mozilla.org/" 16 | 17 | 18 | @pytest.fixture 19 | def element(driver): 20 | element = Mock() 21 | driver.find_element.return_value = element 22 | return element 23 | 24 | 25 | @pytest.fixture 26 | def page(driver, base_url): 27 | from pypom import Page 28 | 29 | return Page(driver, base_url) 30 | 31 | 32 | @pytest.fixture(params=[ISelenium, ISplinter], ids=["selenium", "splinter"]) 33 | def driver_interface(request): 34 | return request.param 35 | 36 | 37 | @pytest.fixture 38 | def driver(request, driver_interface): 39 | """ All drivers """ 40 | mock = Mock() 41 | alsoProvides(mock, driver_interface) 42 | return mock 43 | 44 | 45 | @pytest.fixture(params=ALLOWED_STRATEGIES) 46 | def splinter_strategy(request): 47 | return request.param 48 | -------------------------------------------------------------------------------- /tests/selenium_specific/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/PyPOM/9cb84df9d27b428b4e7423d1bbe6502e92990154/tests/selenium_specific/__init__.py -------------------------------------------------------------------------------- /tests/selenium_specific/conftest.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import pytest 6 | from mock import Mock 7 | from zope.interface import alsoProvides 8 | 9 | from pypom.selenium_driver import ISelenium 10 | 11 | 12 | @pytest.fixture 13 | def element(selenium): 14 | element = Mock() 15 | selenium.find_element.return_value = element 16 | return element 17 | 18 | 19 | @pytest.fixture 20 | def page(selenium, base_url): 21 | from pypom import Page 22 | 23 | return Page(selenium, base_url) 24 | 25 | 26 | @pytest.fixture 27 | def selenium(): 28 | """ Selenium driver """ 29 | mock = Mock() 30 | alsoProvides(mock, ISelenium) 31 | return mock 32 | -------------------------------------------------------------------------------- /tests/selenium_specific/test_page.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import random 6 | 7 | 8 | def test_find_element_selenium(page, selenium): 9 | locator = (str(random.random()), str(random.random())) 10 | page.find_element(*locator) 11 | selenium.find_element.assert_called_once_with(*locator) 12 | 13 | 14 | def test_find_elements_selenium(page, selenium): 15 | locator = (str(random.random()), str(random.random())) 16 | page.find_elements(*locator) 17 | selenium.find_elements.assert_called_once_with(*locator) 18 | 19 | 20 | def test_is_element_present_selenium(page, selenium): 21 | locator = (str(random.random()), str(random.random())) 22 | assert page.is_element_present(*locator) 23 | selenium.find_element.assert_called_once_with(*locator) 24 | 25 | 26 | def test_is_element_present_not_present_selenium(page, selenium): 27 | locator = (str(random.random()), str(random.random())) 28 | from selenium.common.exceptions import NoSuchElementException 29 | 30 | selenium.find_element.side_effect = NoSuchElementException() 31 | assert not page.is_element_present(*locator) 32 | selenium.find_element.assert_called_once_with(*locator) 33 | 34 | 35 | def test_is_element_displayed_selenium(page, selenium): 36 | locator = (str(random.random()), str(random.random())) 37 | assert page.is_element_displayed(*locator) 38 | selenium.find_element.assert_called_once_with(*locator) 39 | 40 | 41 | def test_is_element_displayed_not_present_selenium(page, selenium): 42 | locator = (str(random.random()), str(random.random())) 43 | from selenium.common.exceptions import NoSuchElementException 44 | 45 | selenium.find_element.side_effect = NoSuchElementException() 46 | assert not page.is_element_displayed(*locator) 47 | selenium.find_element.assert_called_once_with(*locator) 48 | selenium.find_element.is_displayed.assert_not_called() 49 | 50 | 51 | def test_is_element_displayed_not_displayed_selenium(page, selenium): 52 | locator = (str(random.random()), str(random.random())) 53 | element = selenium.find_element() 54 | element.is_displayed.return_value = False 55 | assert not page.is_element_displayed(*locator) 56 | selenium.find_element.assert_called_with(*locator) 57 | element.is_displayed.assert_called_once_with() 58 | -------------------------------------------------------------------------------- /tests/selenium_specific/test_region.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import random 6 | 7 | import pytest 8 | from mock import Mock 9 | 10 | from pypom import Region 11 | 12 | 13 | class TestNoRoot: 14 | def test_find_element_selenium(self, page, selenium): 15 | locator = (str(random.random()), str(random.random())) 16 | Region(page).find_element(*locator) 17 | selenium.find_element.assert_called_once_with(*locator) 18 | 19 | def test_find_elements_selenium(self, page, selenium): 20 | locator = (str(random.random()), str(random.random())) 21 | Region(page).find_elements(*locator) 22 | selenium.find_elements.assert_called_once_with(*locator) 23 | 24 | def test_is_element_displayed_selenium(self, page, selenium): 25 | locator = (str(random.random()), str(random.random())) 26 | assert Region(page).is_element_displayed(*locator) 27 | selenium.find_element.assert_called_once_with(*locator) 28 | 29 | def test_is_element_displayed_not_present_selenium(self, page, selenium): 30 | locator = (str(random.random()), str(random.random())) 31 | from selenium.common.exceptions import NoSuchElementException 32 | 33 | selenium.find_element.side_effect = NoSuchElementException() 34 | assert not Region(page).is_element_displayed(*locator) 35 | selenium.find_element.assert_called_once_with(*locator) 36 | selenium.find_element.is_displayed.assert_not_called() 37 | 38 | def test_is_element_displayed_hidden_selenium(self, page, selenium): 39 | locator = (str(random.random()), str(random.random())) 40 | hidden_element = selenium.find_element() 41 | hidden_element.is_displayed.return_value = False 42 | assert not Region(page).is_element_displayed(*locator) 43 | selenium.find_element.assert_called_with(*locator) 44 | hidden_element.is_displayed.assert_called_once_with() 45 | 46 | 47 | class TestRootElement: 48 | def test_find_element_selenium(self, page, selenium): 49 | root_element = Mock() 50 | locator = (str(random.random()), str(random.random())) 51 | Region(page, root=root_element).find_element(*locator) 52 | root_element.find_element.assert_called_once_with(*locator) 53 | selenium.find_element.assert_not_called() 54 | 55 | def test_find_elements_selenium(self, page, selenium): 56 | root_element = Mock() 57 | locator = (str(random.random()), str(random.random())) 58 | Region(page, root=root_element).find_elements(*locator) 59 | root_element.find_elements.assert_called_once_with(*locator) 60 | selenium.find_elements.assert_not_called() 61 | 62 | def test_is_element_present_selenium(self, page, selenium): 63 | root_element = Mock() 64 | locator = (str(random.random()), str(random.random())) 65 | assert Region(page, root=root_element).is_element_present(*locator) 66 | root_element.find_element.assert_called_once_with(*locator) 67 | selenium.find_element.assert_not_called() 68 | 69 | def test_is_element_present_not_preset_selenium(self, page, selenium): 70 | root_element = Mock() 71 | locator = (str(random.random()), str(random.random())) 72 | from selenium.common.exceptions import NoSuchElementException 73 | 74 | root_element.find_element.side_effect = NoSuchElementException() 75 | assert not Region(page, root=root_element).is_element_present(*locator) 76 | root_element.find_element.assert_called_once_with(*locator) 77 | selenium.find_element.assert_not_called() 78 | 79 | def test_is_element_displayed_selenium(self, page, selenium): 80 | root_element = Mock() 81 | locator = (str(random.random()), str(random.random())) 82 | assert Region(page, root=root_element).is_element_displayed(*locator) 83 | root_element.find_element.assert_called_once_with(*locator) 84 | selenium.find_element.assert_not_called() 85 | 86 | def test_is_element_displayed_not_present_selenium(self, page, selenium): 87 | root_element = Mock() 88 | locator = (str(random.random()), str(random.random())) 89 | from selenium.common.exceptions import NoSuchElementException 90 | 91 | root_element.find_element.side_effect = NoSuchElementException() 92 | region = Region(page, root=root_element) 93 | assert not region.is_element_displayed(*locator) 94 | root_element.find_element.assert_called_once_with(*locator) 95 | root_element.find_element.is_displayed.assert_not_called() 96 | 97 | def test_is_element_displayed_hidden_selenium(self, page, selenium): 98 | root_element = Mock() 99 | locator = (str(random.random()), str(random.random())) 100 | hidden_element = root_element.find_element() 101 | hidden_element.is_displayed.return_value = False 102 | region = Region(page, root=root_element) 103 | assert not region.is_element_displayed(*locator) 104 | root_element.find_element.assert_called_with(*locator) 105 | hidden_element.is_displayed.assert_called_once_with() 106 | 107 | 108 | class TestRootLocator: 109 | @pytest.fixture 110 | def region(self, page): 111 | class MyRegion(Region): 112 | _root_locator = (str(random.random()), str(random.random())) 113 | 114 | return MyRegion(page) 115 | 116 | def test_root_selenium(self, element, region, selenium): 117 | assert element == region.root 118 | selenium.find_element.assert_called_once_with(*region._root_locator) 119 | 120 | def test_find_element_selenium(self, element, region, selenium): 121 | locator = (str(random.random()), str(random.random())) 122 | region.find_element(*locator) 123 | selenium.find_element.assert_called_once_with(*region._root_locator) 124 | element.find_element.assert_called_once_with(*locator) 125 | 126 | def test_find_elements_selenium(self, element, region, selenium): 127 | locator = (str(random.random()), str(random.random())) 128 | region.find_elements(*locator) 129 | selenium.find_element.assert_called_once_with(*region._root_locator) 130 | element.find_elements.assert_called_once_with(*locator) 131 | 132 | def test_is_element_present_selenium(self, element, region, selenium): 133 | locator = (str(random.random()), str(random.random())) 134 | assert region.is_element_present(*locator) 135 | selenium.find_element.assert_called_once_with(*region._root_locator) 136 | element.find_element.assert_called_once_with(*locator) 137 | 138 | def test_is_element_present_not_present_selenium(self, element, region, selenium): 139 | locator = (str(random.random()), str(random.random())) 140 | from selenium.common.exceptions import NoSuchElementException 141 | 142 | element.find_element.side_effect = NoSuchElementException() 143 | assert not region.is_element_present(*locator) 144 | selenium.find_element.assert_called_once_with(*region._root_locator) 145 | element.find_element.assert_called_once_with(*locator) 146 | 147 | def test_is_element_displayed_selenium(self, element, region, selenium): 148 | locator = (str(random.random()), str(random.random())) 149 | assert region.is_element_displayed(*locator) 150 | selenium.find_element.assert_called_once_with(*region._root_locator) 151 | element.find_element.assert_called_once_with(*locator) 152 | 153 | def test_is_element_displayed_not_present_selenium(self, element, region, selenium): 154 | locator = (str(random.random()), str(random.random())) 155 | from selenium.common.exceptions import NoSuchElementException 156 | 157 | element.find_element.side_effect = NoSuchElementException() 158 | assert not region.is_element_displayed(*locator) 159 | element.find_element.assert_called_once_with(*locator) 160 | element.find_element.is_displayed.assert_not_called() 161 | 162 | def test_is_element_displayed_hidden_selenium(self, element, region, selenium): 163 | locator = (str(random.random()), str(random.random())) 164 | hidden_element = element.find_element() 165 | hidden_element.is_displayed.return_value = False 166 | assert not region.is_element_displayed(*locator) 167 | element.find_element.assert_called_with(*locator) 168 | hidden_element.is_displayed.assert_called_once_with() 169 | -------------------------------------------------------------------------------- /tests/splinter_specific/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/PyPOM/9cb84df9d27b428b4e7423d1bbe6502e92990154/tests/splinter_specific/__init__.py -------------------------------------------------------------------------------- /tests/splinter_specific/conftest.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import pytest 6 | from mock import Mock 7 | from zope.interface import alsoProvides 8 | 9 | from pypom.splinter_driver import ISplinter 10 | 11 | 12 | @pytest.fixture 13 | def element(splinter): 14 | element = Mock() 15 | splinter.find_element.return_value = element 16 | return element 17 | 18 | 19 | @pytest.fixture 20 | def page(splinter, base_url): 21 | from pypom import Page 22 | 23 | return Page(splinter, base_url) 24 | 25 | 26 | @pytest.fixture 27 | def splinter(): 28 | """ Splinter driver """ 29 | mock = Mock() 30 | alsoProvides(mock, ISplinter) 31 | return mock 32 | -------------------------------------------------------------------------------- /tests/splinter_specific/test_page.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import random 6 | 7 | 8 | def test_find_element_splinter(page, splinter, splinter_strategy): 9 | locator = (splinter_strategy, str(random.random())) 10 | page.find_element(*locator) 11 | getattr( 12 | page.driver, "find_by_{0}".format(splinter_strategy) 13 | ).assert_called_once_with(locator[1]) 14 | 15 | 16 | def test_find_elements_splinter(page, splinter, splinter_strategy): 17 | locator = (splinter_strategy, str(random.random())) 18 | page.find_elements(*locator) 19 | getattr( 20 | page.driver, "find_by_{0}".format(splinter_strategy) 21 | ).assert_called_once_with(locator[1]) 22 | 23 | 24 | def test_is_element_present_splinter(page, splinter, splinter_strategy): 25 | locator = (splinter_strategy, str(random.random())) 26 | from splinter.element_list import ElementList 27 | from mock import Mock 28 | 29 | page.driver.configure_mock( 30 | **{"find_by_{0}.return_value".format(splinter_strategy): ElementList([Mock()])} 31 | ) 32 | assert page.is_element_present(*locator) 33 | getattr( 34 | page.driver, "find_by_{0}".format(splinter_strategy) 35 | ).assert_called_once_with(locator[1]) 36 | 37 | 38 | def test_is_element_present_not_present_splinter(page, splinter, splinter_strategy): 39 | locator = (splinter_strategy, str(random.random())) 40 | from splinter.element_list import ElementList 41 | 42 | page.driver.configure_mock( 43 | **{"find_by_{0}.return_value".format(splinter_strategy): ElementList([])} 44 | ) 45 | assert not page.is_element_present(*locator) 46 | getattr( 47 | page.driver, "find_by_{0}".format(splinter_strategy) 48 | ).assert_called_once_with(locator[1]) 49 | 50 | 51 | def test_is_element_displayed_splinter(page, splinter, splinter_strategy): 52 | locator = (splinter_strategy, str(random.random())) 53 | 54 | from mock import PropertyMock 55 | 56 | visible_mock = PropertyMock(return_value=True) 57 | page.driver.configure_mock( 58 | **{ 59 | "find_by_{0}.return_value.first.visible".format( 60 | splinter_strategy 61 | ): visible_mock 62 | } 63 | ) 64 | type( 65 | getattr(page.driver, "find_by_{0}".format(splinter_strategy)).return_value.first 66 | ).visible = visible_mock 67 | assert page.is_element_displayed(*locator) 68 | getattr( 69 | page.driver, "find_by_{0}".format(splinter_strategy) 70 | ).assert_called_once_with(locator[1]) 71 | visible_mock.assert_called_with() 72 | 73 | 74 | def test_is_element_displayed_not_present_splinter(page, splinter, splinter_strategy): 75 | locator = (splinter_strategy, str(random.random())) 76 | from splinter.element_list import ElementList 77 | 78 | page.driver.configure_mock( 79 | **{"find_by_{0}.return_value".format(splinter_strategy): ElementList([])} 80 | ) 81 | assert not page.is_element_displayed(*locator) 82 | getattr( 83 | page.driver, "find_by_{0}".format(splinter_strategy) 84 | ).assert_called_once_with(locator[1]) 85 | 86 | 87 | def test_is_element_displayed_not_displayed_splinter(page, splinter, splinter_strategy): 88 | locator = (splinter_strategy, str(random.random())) 89 | 90 | from mock import PropertyMock 91 | 92 | visible_mock = PropertyMock(return_value=False) 93 | page.driver.configure_mock( 94 | **{ 95 | "find_by_{0}.return_value.first.visible".format( 96 | splinter_strategy 97 | ): visible_mock 98 | } 99 | ) 100 | type( 101 | getattr(page.driver, "find_by_{0}".format(splinter_strategy)).return_value.first 102 | ).visible = visible_mock 103 | assert not page.is_element_displayed(*locator) 104 | getattr( 105 | page.driver, "find_by_{0}".format(splinter_strategy) 106 | ).assert_called_once_with(locator[1]) 107 | visible_mock.assert_called_with() 108 | -------------------------------------------------------------------------------- /tests/splinter_specific/test_region.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import random 6 | 7 | import pytest 8 | from mock import MagicMock, Mock, patch 9 | 10 | from pypom import Region 11 | 12 | 13 | class TestNoRootSplinter: 14 | def test_no_root_usage_error(self, page, splinter): 15 | locator = ("not_valid_strategy", str(random.random())) 16 | from pypom.exception import UsageError 17 | 18 | with pytest.raises(UsageError): 19 | Region(page).find_element(*locator) 20 | 21 | def test_find_element_splinter(self, page, splinter, splinter_strategy): 22 | locator = (splinter_strategy, str(random.random())) 23 | from splinter.element_list import ElementList 24 | 25 | page.driver.configure_mock( 26 | **{"find_by_{0}.return_value".format(splinter_strategy): ElementList([])} 27 | ) 28 | Region(page).find_element(*locator) 29 | getattr( 30 | page.driver, "find_by_{0}".format(splinter_strategy) 31 | ).assert_called_once_with(locator[1]) 32 | 33 | def test_find_elements_splinter(self, page, splinter, splinter_strategy): 34 | locator = (splinter_strategy, str(random.random())) 35 | from splinter.element_list import ElementList 36 | 37 | page.driver.configure_mock( 38 | **{"find_by_{0}.return_value".format(splinter_strategy): ElementList([])} 39 | ) 40 | Region(page).find_elements(*locator) 41 | getattr( 42 | page.driver, "find_by_{0}".format(splinter_strategy) 43 | ).assert_called_once_with(locator[1]) 44 | 45 | def test_is_element_displayed_splinter(self, page, splinter, splinter_strategy): 46 | locator = (splinter_strategy, str(random.random())) 47 | 48 | from mock import PropertyMock 49 | 50 | visible_mock = PropertyMock(return_value=True) 51 | page.driver.configure_mock( 52 | **{ 53 | "find_by_{0}.return_value.first.visible".format( 54 | splinter_strategy 55 | ): visible_mock 56 | } 57 | ) 58 | type( 59 | getattr( 60 | page.driver, "find_by_{0}".format(splinter_strategy) 61 | ).return_value.first 62 | ).visible = visible_mock 63 | assert Region(page).is_element_displayed(*locator) 64 | 65 | getattr( 66 | page.driver, "find_by_{0}".format(splinter_strategy) 67 | ).assert_called_once_with(locator[1]) 68 | visible_mock.assert_called_with() 69 | 70 | def test_is_element_displayed_not_present_splinter( 71 | self, page, splinter, splinter_strategy 72 | ): 73 | locator = (str(random.random()), str(random.random())) 74 | from splinter.element_list import ElementList 75 | 76 | with patch( 77 | "pypom.splinter_driver.Splinter.find_element", new_callable=MagicMock() 78 | ) as mock_find_element: 79 | mock_find_element.return_value = ElementList([]) 80 | assert not Region(page).is_element_displayed(*locator) 81 | 82 | def test_is_element_displayed_hidden_splinter( 83 | self, page, splinter, splinter_strategy 84 | ): 85 | locator = (splinter_strategy, str(random.random())) 86 | hidden_element = splinter.find_element() 87 | hidden_element.is_displayed.return_value = False 88 | region = Region(page) 89 | with patch( 90 | "pypom.splinter_driver.Splinter.find_element", new_callable=Mock() 91 | ) as mock_find_element: 92 | visible_mock = Mock().visible.return_value = False 93 | first_mock = Mock().first.return_value = visible_mock 94 | mock_find_element.return_value = first_mock 95 | assert not region.is_element_displayed(*locator) 96 | 97 | 98 | class TestRootElementSplinter: 99 | def test_no_root_usage_error(self, page, splinter): 100 | root_element = MagicMock() 101 | locator = ("not_valid_strategy", str(random.random())) 102 | from pypom.exception import UsageError 103 | 104 | with pytest.raises(UsageError): 105 | Region(page, root=root_element).find_element(*locator) 106 | 107 | def test_find_element_splinter(self, page, splinter, splinter_strategy): 108 | root_element = MagicMock() 109 | root_element.configure_mock( 110 | **{"find_by_{0}.return_value".format(splinter_strategy): Mock()} 111 | ) 112 | locator = (splinter_strategy, str(random.random())) 113 | Region(page, root=root_element).find_element(*locator) 114 | getattr( 115 | root_element, "find_by_{0}".format(splinter_strategy) 116 | ).assert_called_once_with(locator[1]) 117 | 118 | def test_find_elements_splinter(self, page, splinter, splinter_strategy): 119 | root_element = MagicMock() 120 | root_element.configure_mock( 121 | **{"find_by_{0}.return_value".format(splinter_strategy): Mock()} 122 | ) 123 | locator = (splinter_strategy, str(random.random())) 124 | Region(page, root=root_element).find_elements(*locator) 125 | getattr( 126 | root_element, "find_by_{0}".format(splinter_strategy) 127 | ).assert_called_once_with(locator[1]) 128 | 129 | def test_is_element_present_splinter(self, page, splinter, splinter_strategy): 130 | root_element = Mock() 131 | locator = (splinter_strategy, str(random.random())) 132 | from splinter.element_list import ElementList 133 | 134 | with patch( 135 | "pypom.splinter_driver.Splinter.find_element", new_callable=MagicMock() 136 | ) as mock_find_element: 137 | mock_find_element.return_value = ElementList([Mock()]) 138 | assert Region(page, root=root_element).is_element_present(*locator) 139 | mock_find_element.assert_called_once_with(*locator, root=root_element) 140 | 141 | def test_is_element_present_not_preset_splinter( 142 | self, page, splinter, splinter_strategy 143 | ): 144 | root_element = MagicMock() 145 | from splinter.element_list import ElementList 146 | 147 | root_element.configure_mock( 148 | **{"find_by_{0}.return_value".format(splinter_strategy): ElementList([])} 149 | ) 150 | locator = (splinter_strategy, str(random.random())) 151 | assert not Region(page, root=root_element).is_element_present(*locator) 152 | 153 | def test_is_element_displayed_splinter(self, page, splinter, splinter_strategy): 154 | root_element = MagicMock() 155 | root_element.configure_mock( 156 | **{"find_by_{0}.return_value.first.visible".format(splinter_strategy): True} 157 | ) 158 | locator = (splinter_strategy, str(random.random())) 159 | region = Region(page, root=root_element) 160 | assert region.is_element_displayed(*locator) 161 | 162 | def test_is_element_displayed_not_present_splinter( 163 | self, page, splinter, splinter_strategy 164 | ): 165 | root_element = Mock() 166 | locator = (splinter_strategy, str(random.random())) 167 | region = Region(page, root=root_element) 168 | from splinter.element_list import ElementList 169 | 170 | with patch( 171 | "pypom.splinter_driver.Splinter.find_element", new_callable=MagicMock() 172 | ) as mock_find_element: 173 | mock_find_element.return_value = ElementList([]) 174 | assert not region.is_element_displayed(*locator) 175 | 176 | def test_is_element_displayed_hidden_splinter( 177 | self, page, splinter, splinter_strategy 178 | ): 179 | root_element = MagicMock() 180 | root_element.configure_mock( 181 | **{ 182 | "find_by_{0}.return_value.first.visible".format( 183 | splinter_strategy 184 | ): False 185 | } 186 | ) 187 | locator = (splinter_strategy, str(random.random())) 188 | region = Region(page, root=root_element) 189 | assert not region.is_element_displayed(*locator) 190 | 191 | 192 | class TestRootLocatorSplinter: 193 | @pytest.fixture 194 | def region(self, page, splinter_strategy): 195 | class MyRegion(Region): 196 | _root_locator = (splinter_strategy, str(random.random())) 197 | 198 | return MyRegion(page) 199 | 200 | def test_root_splinter(self, region, splinter_strategy): 201 | region.root 202 | getattr( 203 | region.driver, "find_by_{0}".format(splinter_strategy) 204 | ).assert_called_once_with(region._root_locator[1]) 205 | 206 | def test_find_element_splinter(self, region, splinter_strategy): 207 | locator = (splinter_strategy, str(random.random())) 208 | region.find_element(*locator) 209 | 210 | getattr( 211 | region.root, "find_by_{0}".format(splinter_strategy) 212 | ).assert_called_once_with(locator[1]) 213 | 214 | def test_find_elements_splinter(self, region, splinter_strategy): 215 | locator = (splinter_strategy, str(random.random())) 216 | region.find_elements(*locator) 217 | 218 | getattr( 219 | region.root, "find_by_{0}".format(splinter_strategy) 220 | ).assert_called_once_with(locator[1]) 221 | 222 | def test_is_element_present_splinter(self, region, splinter_strategy): 223 | assert region._root_locator[0] == splinter_strategy 224 | locator = (splinter_strategy, str(random.random())) 225 | 226 | assert region.is_element_present(*locator) 227 | getattr( 228 | region.root, "find_by_{0}".format(splinter_strategy) 229 | ).assert_called_once_with(locator[1]) 230 | 231 | def test_is_element_present_not_present_splinter(self, region, splinter_strategy): 232 | from splinter.element_list import ElementList 233 | 234 | locator = (splinter_strategy, str(random.random())) 235 | with patch( 236 | "pypom.splinter_driver.Splinter.find_elements", new_callable=MagicMock() 237 | ) as mock_find_elements: 238 | mock_find_elements.return_value = ElementList([]) 239 | assert not region.is_element_present(*locator) 240 | 241 | def test_is_element_displayed_splinter(self, region, splinter_strategy): 242 | locator = (splinter_strategy, str(random.random())) 243 | with patch( 244 | "pypom.splinter_driver.Splinter.find_element", new_callable=MagicMock() 245 | ) as mock_find_element: 246 | mock_find_element.return_value.first.visible = True 247 | assert region.is_element_displayed(*locator) 248 | 249 | def test_is_element_displayed_not_present_splinter(self, region, splinter_strategy): 250 | locator = (splinter_strategy, str(random.random())) 251 | from splinter.element_list import ElementList 252 | 253 | with patch( 254 | "pypom.splinter_driver.Splinter.find_element", new_callable=Mock() 255 | ) as mock_find_element: 256 | mock_find_element.return_value = ElementList([]) 257 | assert not region.is_element_displayed(*locator) 258 | 259 | def test_is_element_displayed_hidden_splinter(self, region, splinter_strategy): 260 | locator = (splinter_strategy, str(random.random())) 261 | with patch( 262 | "pypom.splinter_driver.Splinter.find_element", new_callable=Mock() 263 | ) as mock_find_element: 264 | visible_mock = Mock().visible = False 265 | first_mock = Mock().first.return_value = visible_mock 266 | mock_find_element.return_value = first_mock 267 | assert not region.is_element_displayed(*locator) 268 | -------------------------------------------------------------------------------- /tests/test_driver.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | 6 | def test_register_driver(): 7 | """ We are testing the registerDriver hook""" 8 | import pytest 9 | from zope.interface import implementer 10 | from zope.interface import Interface 11 | from pypom.driver import registerDriver 12 | from pypom.interfaces import IDriver 13 | 14 | class IFakeDriver(Interface): 15 | """ A fake marker interface""" 16 | 17 | @implementer(IFakeDriver) 18 | class FakeDriver: 19 | """ A fake driver """ 20 | 21 | def __init__(self, driver): 22 | self.driver = driver 23 | 24 | fake_driver = FakeDriver(None) 25 | 26 | # no register driver, look up error 27 | with pytest.raises(TypeError): 28 | IDriver(fake_driver) 29 | 30 | registerDriver(IFakeDriver, FakeDriver) 31 | 32 | # driver implementation available after registerDriver 33 | adapted_driver = IDriver(fake_driver) 34 | 35 | # same instance of adapted_driver 36 | assert isinstance(adapted_driver, FakeDriver) 37 | 38 | 39 | def test_multiple_register_driver(): 40 | """ We are testing the registerDriver hook, 41 | multiple registrations""" 42 | from zope.interface import implementer 43 | from zope.interface import Interface 44 | from pypom.driver import registerDriver 45 | from pypom.interfaces import IDriver 46 | 47 | class IFakeDriver(Interface): 48 | """ A fake marker interface""" 49 | 50 | @implementer(IFakeDriver) 51 | class FakeDriver: 52 | """ A fake driver """ 53 | 54 | def __init__(self, driver): 55 | self.driver = driver 56 | 57 | class IFakeDriver2(Interface): 58 | """ Another fake marker interface""" 59 | 60 | @implementer(IFakeDriver2) 61 | class FakeDriver2: 62 | """ Another fake driver """ 63 | 64 | def __init__(self, driver): 65 | self.driver = driver 66 | 67 | fake_driver = FakeDriver(None) 68 | fake_driver2 = FakeDriver2(None) 69 | 70 | registerDriver(IFakeDriver, FakeDriver) 71 | registerDriver(IFakeDriver2, IFakeDriver2) 72 | 73 | # driver implementation available after registerDriver 74 | adapted_driver = IDriver(fake_driver) 75 | adapted_driver2 = IDriver(fake_driver2) 76 | 77 | # same instance of adapted_driver 78 | assert isinstance(adapted_driver, FakeDriver) 79 | assert isinstance(adapted_driver2, FakeDriver2) 80 | 81 | 82 | def test_register_driver_class_implements(): 83 | """ We are testing the registerDriver hook with 84 | class implements""" 85 | from zope.interface import Interface 86 | from pypom.driver import registerDriver 87 | from pypom.interfaces import IDriver 88 | 89 | class IFakeDriver(Interface): 90 | """ A fake marker interface""" 91 | 92 | class FakeDriver: 93 | """ A fake driver """ 94 | 95 | def __init__(self, driver): 96 | self.driver = driver 97 | 98 | fake_driver = FakeDriver(None) 99 | 100 | registerDriver(IFakeDriver, FakeDriver, [FakeDriver]) 101 | 102 | # driver implementation available after registerDriver 103 | adapted_driver = IDriver(fake_driver) 104 | 105 | # same instance of adapted_driver 106 | assert isinstance(adapted_driver, FakeDriver) 107 | -------------------------------------------------------------------------------- /tests/test_page.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import random 6 | 7 | import pytest 8 | 9 | from pypom import Page 10 | 11 | 12 | def test_base_url(base_url, page): 13 | assert base_url == page.seed_url 14 | 15 | 16 | def test_seed_url_absolute(base_url, driver): 17 | url_template = "https://www.test.com/" 18 | 19 | class MyPage(Page): 20 | URL_TEMPLATE = url_template 21 | 22 | page = MyPage(driver, base_url) 23 | assert url_template == page.seed_url 24 | 25 | 26 | def test_seed_url_absolute_keywords_tokens(base_url, driver): 27 | value = str(random.random()) 28 | absolute_url = "https://www.test.com/" 29 | 30 | class MyPage(Page): 31 | URL_TEMPLATE = absolute_url + "{key}" 32 | 33 | page = MyPage(driver, base_url, key=value) 34 | assert absolute_url + value == page.seed_url 35 | 36 | 37 | def test_seed_url_absolute_keywords_params(base_url, driver): 38 | value = str(random.random()) 39 | absolute_url = "https://www.test.com/" 40 | 41 | class MyPage(Page): 42 | URL_TEMPLATE = absolute_url 43 | 44 | page = MyPage(driver, base_url, key=value) 45 | assert "{}?key={}".format(absolute_url, value) == page.seed_url 46 | 47 | 48 | def test_seed_url_absolute_keywords_params_none(base_url, driver): 49 | value = None 50 | absolute_url = "https://www.test.com/" 51 | 52 | class MyPage(Page): 53 | URL_TEMPLATE = absolute_url 54 | 55 | page = MyPage(driver, base_url, key=value) 56 | assert absolute_url == page.seed_url 57 | 58 | 59 | def test_seed_url_absolute_keywords_tokens_and_params(base_url, driver): 60 | values = (str(random.random()), str(random.random())) 61 | absolute_url = "https://www.test.com/" 62 | 63 | class MyPage(Page): 64 | URL_TEMPLATE = absolute_url + "?key1={key1}" 65 | 66 | page = MyPage(driver, base_url, key1=values[0], key2=values[1]) 67 | assert "{}?key1={}&key2={}".format(absolute_url, *values) == page.seed_url 68 | 69 | 70 | def test_seed_url_empty(driver): 71 | page = Page(driver) 72 | assert page.seed_url is None 73 | 74 | 75 | def test_seed_url_keywords_tokens(base_url, driver): 76 | value = str(random.random()) 77 | 78 | class MyPage(Page): 79 | URL_TEMPLATE = "{key}" 80 | 81 | page = MyPage(driver, base_url, key=value) 82 | assert base_url + value == page.seed_url 83 | 84 | 85 | def test_seed_url_keywords_params(base_url, driver): 86 | value = str(random.random()) 87 | page = Page(driver, base_url, key=value) 88 | assert "{}?key={}".format(base_url, value) == page.seed_url 89 | 90 | 91 | def test_seed_url_keywords_params_space(base_url, driver): 92 | value = "a value" 93 | page = Page(driver, base_url, key=value) 94 | assert "{}?key={}".format(base_url, "a+value") == page.seed_url 95 | 96 | 97 | def test_seed_url_keywords_params_special(base_url, driver): 98 | value = "mozilla&co" 99 | page = Page(driver, base_url, key=value) 100 | assert "{}?key={}".format(base_url, "mozilla%26co") == page.seed_url 101 | 102 | 103 | def test_seed_url_keywords_multiple_params(base_url, driver): 104 | value = ("foo", "bar") 105 | page = Page(driver, base_url, key=value) 106 | seed_url = page.seed_url 107 | assert "key={}".format(value[0]) in seed_url 108 | assert "key={}".format(value[1]) in seed_url 109 | import re 110 | 111 | assert re.match(r"{}\?key=(foo|bar)&key=(foo|bar)".format(base_url), seed_url) 112 | 113 | 114 | def test_seed_url_keywords_multiple_params_special(base_url, driver): 115 | value = ("foo", "mozilla&co") 116 | page = Page(driver, base_url, key=value) 117 | seed_url = page.seed_url 118 | assert "key=foo" in seed_url 119 | assert "key=mozilla%26co" in seed_url 120 | import re 121 | 122 | assert re.match( 123 | r"{}\?key=(foo|mozilla%26co)&key=(foo|mozilla%26co)".format(base_url), seed_url 124 | ) 125 | 126 | 127 | def test_seed_url_keywords_keywords_and_params(base_url, driver): 128 | values = (str(random.random()), str(random.random())) 129 | 130 | class MyPage(Page): 131 | URL_TEMPLATE = "?key1={key1}" 132 | 133 | page = MyPage(driver, base_url, key1=values[0], key2=values[1]) 134 | assert "{}?key1={}&key2={}".format(base_url, *values) == page.seed_url 135 | 136 | 137 | def test_seed_url_prepend(base_url, driver): 138 | url_template = str(random.random()) 139 | 140 | class MyPage(Page): 141 | URL_TEMPLATE = url_template 142 | 143 | page = MyPage(driver, base_url) 144 | assert base_url + url_template == page.seed_url 145 | 146 | 147 | def test_open(page, driver): 148 | assert isinstance(page.open(), Page) 149 | 150 | 151 | def test_open_seed_url_none(driver): 152 | from pypom.exception import UsageError 153 | 154 | page = Page(driver) 155 | with pytest.raises(UsageError): 156 | page.open() 157 | 158 | 159 | def test_open_timeout(base_url, driver): 160 | class MyPage(Page): 161 | def wait_for_page_to_load(self): 162 | self.wait.until(lambda s: False) 163 | 164 | page = MyPage(driver, base_url, timeout=0) 165 | from selenium.common.exceptions import TimeoutException 166 | 167 | with pytest.raises(TimeoutException): 168 | page.open() 169 | 170 | 171 | def test_open_timeout_loaded(base_url, driver): 172 | class MyPage(Page): 173 | @property 174 | def loaded(self): 175 | return False 176 | 177 | page = MyPage(driver, base_url, timeout=0) 178 | from selenium.common.exceptions import TimeoutException 179 | 180 | with pytest.raises(TimeoutException): 181 | page.open() 182 | 183 | 184 | def test_wait_for_page(page, driver): 185 | assert isinstance(page.wait_for_page_to_load(), Page) 186 | 187 | 188 | def test_wait_for_page_timeout(base_url, driver): 189 | class MyPage(Page): 190 | def wait_for_page_to_load(self): 191 | self.wait.until(lambda s: False) 192 | 193 | page = MyPage(driver, base_url, timeout=0) 194 | from selenium.common.exceptions import TimeoutException 195 | 196 | with pytest.raises(TimeoutException): 197 | page.wait_for_page_to_load() 198 | 199 | 200 | def test_wait_for_page_timeout_loaded(base_url, driver): 201 | class MyPage(Page): 202 | @property 203 | def loaded(self): 204 | return False 205 | 206 | page = MyPage(driver, base_url, timeout=0) 207 | from selenium.common.exceptions import TimeoutException 208 | 209 | with pytest.raises(TimeoutException): 210 | page.wait_for_page_to_load() 211 | 212 | 213 | def test_wait_for_page_empty_base_url(driver): 214 | assert isinstance(Page(driver).wait_for_page_to_load(), Page) 215 | 216 | 217 | def test_bwc_selenium(page, driver_interface): 218 | """ Backwards compatibility with old selenium attribute """ 219 | driver = page.selenium 220 | assert driver == page.driver 221 | 222 | 223 | def test_loaded(page, driver): 224 | assert page.loaded is True 225 | -------------------------------------------------------------------------------- /tests/test_plugin.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | from pypom import Region, hookimpl 6 | 7 | 8 | def test_after_wait_for_page_to_load(page): 9 | log = [] 10 | 11 | class Plugin: 12 | @hookimpl 13 | def pypom_after_wait_for_page_to_load(self, page): 14 | log.append(1) 15 | 16 | @hookimpl 17 | def pypom_after_wait_for_region_to_load(self, region): 18 | log.append(2) 19 | 20 | page.pm.register(Plugin()) 21 | page.open() 22 | Region(page) 23 | assert log == [1, 2] 24 | -------------------------------------------------------------------------------- /tests/test_region.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | import pytest 6 | from mock import Mock 7 | 8 | from pypom import Region 9 | 10 | 11 | class TestWaitForRegion: 12 | def test_wait_for_region(self, page): 13 | assert isinstance(Region(page).wait_for_region_to_load(), Region) 14 | 15 | def test_wait_for_region_timeout(self, page): 16 | class MyRegion(Region): 17 | def wait_for_region_to_load(self): 18 | self.wait.until(lambda s: False) 19 | 20 | page.timeout = 0 21 | from selenium.common.exceptions import TimeoutException 22 | 23 | with pytest.raises(TimeoutException): 24 | MyRegion(page) 25 | 26 | def test_wait_for_region_timeout_loaded(self, page): 27 | class MyRegion(Region): 28 | @property 29 | def loaded(self): 30 | return False 31 | 32 | page.timeout = 0 33 | from selenium.common.exceptions import TimeoutException 34 | 35 | with pytest.raises(TimeoutException): 36 | MyRegion(page) 37 | 38 | 39 | class TestNoRoot: 40 | def test_root(self, page): 41 | assert Region(page).root is None 42 | 43 | 44 | class TestRootElement: 45 | def test_root(self, page, driver): 46 | element = Mock() 47 | assert Region(page, root=element).root == element 48 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{27,36,py,py3}, flake8, docs 3 | 4 | [testenv] 5 | passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH 6 | deps = -rrequirements/pipenv.txt 7 | commands = 8 | pipenv install --dev --skip-lock 9 | pipenv run pytest --cov {posargs} 10 | - pipenv run coveralls 11 | 12 | [testenv:flake8] 13 | skip_install = true 14 | commands = 15 | pipenv install --dev 16 | pipenv run flake8 {posargs} 17 | 18 | [testenv:docs] 19 | changedir = docs 20 | commands = 21 | pipenv install --dev 22 | pipenv run sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html 23 | --------------------------------------------------------------------------------