Repository: mherrmann/helium Branch: master Commit: ac354ad19f71 Files: 104 Total size: 258.7 KB Directory structure: gitextract_83h9li4j/ ├── .gitattributes ├── .gitignore ├── .readthedocs.yaml ├── LICENSE.txt ├── NOTICE.txt ├── README.md ├── docs/ │ ├── Makefile │ ├── README.md │ ├── api.rst │ ├── cheatsheet.md │ ├── conf.py │ ├── contributors.rst │ ├── index.rst │ ├── installation.rst │ └── make.bat ├── helium/ │ ├── __init__.py │ └── _impl/ │ ├── __init__.py │ ├── match_type.py │ ├── selenium_wrappers.py │ └── util/ │ ├── __init__.py │ ├── dictionary.py │ ├── geom.py │ ├── html.py │ ├── inspect_.py │ ├── lang.py │ ├── path.py │ ├── system.py │ └── xpath.py ├── requirements/ │ ├── base.txt │ ├── docs.txt │ └── test.txt ├── setup.py └── tests/ ├── __init__.py ├── api/ │ ├── __init__.py │ ├── data/ │ │ ├── default.css │ │ ├── js/ │ │ │ ├── jquery.ui-contextmenu.js │ │ │ └── util.js │ │ ├── test_alert.html │ │ ├── test_aria.html │ │ ├── test_click.html │ │ ├── test_doubleclick.html │ │ ├── test_drag/ │ │ │ ├── default.html │ │ │ ├── html5.html │ │ │ └── test_drag.css │ │ ├── test_file_upload/ │ │ │ └── test_file_upload.html │ │ ├── test_gui_elements.html │ │ ├── test_gui_elements_iframe.html │ │ ├── test_hover.html │ │ ├── test_iframe/ │ │ │ ├── iframe.html │ │ │ ├── main.html │ │ │ └── nested_iframe.html │ │ ├── test_implicit_wait.html │ │ ├── test_leaked_password.html │ │ ├── test_point.html │ │ ├── test_rightclick.html │ │ ├── test_scroll.html │ │ ├── test_start_go_to.html │ │ ├── test_tables.html │ │ ├── test_text_impl.html │ │ ├── test_wait_until.html │ │ ├── test_window/ │ │ │ ├── popup.html │ │ │ └── test_window.html │ │ ├── test_window_handling/ │ │ │ ├── main.html │ │ │ ├── main_immediate_popup.html │ │ │ └── popup.html │ │ └── test_write.html │ ├── test_alert.py │ ├── test_aria.py │ ├── test_chrome_options.py │ ├── test_click.py │ ├── test_doubleclick.py │ ├── test_drag.py │ ├── test_file_upload.py │ ├── test_find_all.py │ ├── test_gui_elements.py │ ├── test_highlight.py │ ├── test_hover.py │ ├── test_iframe.py │ ├── test_implicit_wait.py │ ├── test_kill_service_at_exit.py │ ├── test_kill_service_at_exit_chrome.py │ ├── test_leaked_password.py │ ├── test_no_driver.py │ ├── test_point.py │ ├── test_press.py │ ├── test_repr.py │ ├── test_rightclick.py │ ├── test_s.py │ ├── test_scroll.py │ ├── test_start_go_to.py │ ├── test_tables.py │ ├── test_text_impl.py │ ├── test_wait_until.py │ ├── test_window.py │ ├── test_window_handling.py │ ├── test_write.py │ └── util.py └── unit/ ├── __init__.py └── test__impl/ ├── __init__.py ├── test_selenium_wrappers.py └── test_util/ ├── __init__.py ├── test_dictionary.py ├── test_html.py └── test_xpath.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ * text=auto *.sh text eol=lf ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # Installer logs pip-log.txt pip-delete-this-directory.txt # Translations *.mo *.pot # Sphinx documentation docs/_build/ docs/modules/ # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Personal Files personal/ sample-data/ pyproject.toml poetry.lock # IDE / Editor .idea .vscode geckodriver.log ================================================ FILE: .readthedocs.yaml ================================================ version: 2 python: install: - requirements: requirements/docs.txt build: os: ubuntu-22.04 tools: python: "3.11" sphinx: configuration: docs/conf.py ================================================ FILE: LICENSE.txt ================================================ MIT License Copyright (c) 2020 Michael Herrmann Copyright (c) 2013 - 2019 Michael Herrmann & Tytus Dobrzynski Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: NOTICE.txt ================================================ LICENSES OF THIRD-PARTY LIBRARIES ================================= Helium is based on several free and open source software libraries and could not have been developed without them. Here we describle the licensing terms under which we are using them. Selenium Java & Python Bindings =============================== URL: https://selenium.dev License: Apache License, Version 2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. InternetExplorerDriver ====================== URL: https://selenium.dev License: Apache License, Version 2.0 (see above) ChromeDriver ============ URL: https://code.google.com/p/chromedriver/ License: New BSD License Copyright 2015 The Chromium Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Apache Commons Exec =================== URL: http://commons.apache.org/proper/commons-exec/ License: Apache License, Version 2.0 (see above). JTwig HtmlUtils =============== URL: https://github.com/lyncode/jtwig/blob/d540f2160467146a7dde0bdf9ac979687f78f194/jtwig-functions/src/main/java/com/lyncode/jtwig/functions/util/HtmlUtils.java Version: d540f2160467146a7dde0bdf9ac979687f78f194 License: Apache License, Version 2.0 (see above). geckodriver =========== URL: https://github.com/mozilla/geckodriver License: Mozilla Public License Version 2.0 ---------------------------------- 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. ================================================ FILE: README.md ================================================ # Lighter web automation with Python Helium is a Python library for automating browsers such as Chrome and Firefox. For example: ![Helium Demo](docs/helium-demo.gif) ## Installation To get started with Helium, you need Python 3 and Chrome or Firefox. I would recommend creating a virtual environment. This lets you install Helium for just your current project, instead of globally on your whole computer. To create and activate a virtual environment, type the following commands into a command prompt window: ```bash python3 -m venv venv # On Mac/Linux: source venv/bin/activate # On Windows: call venv\scripts\activate.bat ``` Then, you can install Helium with `pip`: ```bash python -m pip install helium ``` Now enter `python` into the command prompt and (for instance) the commands in the animation at the top of this page (`from helium import *`, ...). ## Your first script I've compiled a [cheatsheet](docs/cheatsheet.md) that quickly teaches you all you need to know to be productive with Helium. For a more complete reference of Helium's features, please see the [documentation](https://helium.readthedocs.io/en/latest/). ## Connection to Selenium Under the hood, Helium forwards each call to Selenium. The difference is that Helium's API is much more high-level. In Selenium, you need to use HTML IDs, XPaths and CSS selectors to identify web page elements. Helium on the other hand lets you refer to elements by user-visible labels. As a result, Helium scripts are typically 30-50% shorter than similar Selenium scripts. What's more, they are easier to read and more stable with respect to changes in the underlying web page. Because Helium is simply a wrapper around Selenium, you can freely mix the two libraries. For example: ```python # A Helium function: driver = start_chrome() # A Selenium API: driver.execute_script("alert('Hi!');") ``` So in other words, you don't lose anything by using Helium over pure Selenium. In addition to its more high-level API, Helium simplifies further tasks that are traditionally painful in Selenium: - **iFrames:** Unlike Selenium, Helium lets you interact with elements inside nested iFrames, without having to first "switch to" the iFrame. - **Window management.** Helium notices when popups open or close and focuses / defocuses them like a user would. You can also easily switch to a window by (parts of) its title. No more having to iterate over Selenium window handles. - **Implicit waits.** By default, if you try click on an element with Selenium and that element is not yet present on the page, your script fails. Helium by default waits up to 10 seconds for the element to appear. - **Explicit waits.** Helium gives you a much nicer API for waiting for a condition on the web page to become true. For example: To wait for an element to appear in Selenium, you would write: ```python element = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, "myDynamicElement")) ) ``` With Helium, you can write: ```python wait_until(Button('Download').exists) ``` ## Status of this project I have too little spare time to maintain this project for free. If you'd like my help, please go to my [web site](http://herrmann.io) to ask about my consulting rates. Otherwise, unless it is very easy for me, I will usually not respond to emails or issues on the issue tracker. I will however accept and merge PRs. So if you add some functionality to Helium that may be useful for others, do share it with us by creating a Pull Request. For instructions, please see [Contributing](#Contributing) below. ## How you can help I find Helium extremely useful in my own projects and feel it should be more widely known. Here's how you can help with this: - Star this project on GitHub. - Tell your friends and colleagues about it. - [Share it on Twitter with one click](https://twitter.com/intent/tweet?text=I%20find%20Helium%20very%20useful%20for%20web%20automation%20with%20Python%3A%20https%3A//github.com/mherrmann/helium) - Share it on other social media - Write a blog post about Helium. With this, I think we can eventually make Helium the de-facto standard for web automation in Python. ## Contributing Pull Requests are very welcome. Please follow the same coding conventions as the rest of the code, in particular the use of tabs over spaces. Also, read through my [PR guidelines](https://gist.github.com/mherrmann/5ce21814789152c17abd91c0b3eaadca). Doing this will save you (and me) unnecessary effort. Before you submit a PR, ensure that the tests still work: ```bash pip install -Ur requirements/test.txt python setup.py test ``` This runs the tests against Chrome. To run them against Firefox, set the environment variable `TEST_BROWSER` to `firefox`. Eg. on Mac/Linux: ```bash TEST_BROWSER=firefox python setup.py test ``` On Windows: ```bash set TEST_BROWSER=firefox python setup.py test ``` If you do add new functionality, you should also add tests for it. Please see the [`tests/`](tests) directory for what this might look like. ## History I (Michael Herrmann) originally developed Helium in 2013 for a Polish IT startup called BugFree software. (It could be that you have seen Helium before at https://heliumhq.com.) We shut down the company at the end of 2019 and I felt it would be a shame if Helium simply disappeared from the face of the earth. So I invested some time to modernize it and bring it into a state suitable for open source. Helium used to be available for both Java and Python. But because I now only use it from Python, I didn't have time to bring the Java implementation up to speed as well. Similarly for Internet Explorer: Helium used to support it, but since I have no need for it, I removed the (probably broken) old implementation. The name Helium was chosen because it is also a chemical element like Selenium, but it is lighter. ================================================ FILE: docs/Makefile ================================================ # Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) ================================================ FILE: docs/README.md ================================================ # Welcome to Helium's documentation The documentation is built using [sphinx](https://www.sphinx-doc.org/en/master/index.html) and the theme used is [sphinx-rtd-theme](https://sphinx-rtd-theme.readthedocs.io/en/stable/). ## Setting up documentation locally Ensure you have `python` and `pip` installed on your system and then run this command in the project root: ```bash pip install -Ur requirements/docs.txt make -C docs/ html ``` This will install all development dependencies for the project and then build the documentation in HTML format in `docs/_build/` directory. Open `docs/_build/index.html` in your browser to see the documentation. ================================================ FILE: docs/api.rst ================================================ API === .. automodule:: helium :members: ================================================ FILE: docs/cheatsheet.md ================================================ # Helium cheatsheet This page very quickly teaches you the most important parts of Helium's API. ## Importing All of Helium's public functions lie directly in the module `helium`. You can for instance import them as follows: ```python from helium import * ``` ## Starting a browser Helium currently supports Chrome and Firefox. You can start them with the following functions: ```python start_chrome() start_firefox() ``` You can optionally pass a URL to open (eg. `start_chrome('google.com')`) ## Headless browser When you type the above commands, you will actually see a browser window open. This is useful for developing your scripts. However, once you run them, you may not want this window to appear. You can achieve this by adding `headless=True`: ```python start_chrome(headless=True) start_chrome('google.com', headless=True) ``` (Similarly for `start_firefox(...)` of course.) ## Interacting with a web site The following example shows the most typical statements in a Helium script: ```python from helium import * start_chrome('google.com') write('helium selenium github') press(ENTER) click('mherrmann/helium') go_to('github.com/login') write('username', into='Username') write('password', into='Password') click('Sign in') kill_browser() ``` Most of your own code will (hopefully) be as simple as the above. ## Element types The above example used pure strings such as `Sign in` to identify elements on the web page. But Helium also lets you target elements more specifically. For instance: * [`Link('Sign in')`](https://github.com/mherrmann/helium/blob/0667ddb9be531367a0d707ad8f5fcfb75c528521/helium/__init__.py#L643) * [`Button('Sign in')`](https://github.com/mherrmann/helium/blob/0667ddb9be531367a0d707ad8f5fcfb75c528521/helium/__init__.py#L706) * [`TextField('First name')`](https://github.com/mherrmann/helium/blob/0667ddb9be531367a0d707ad8f5fcfb75c528521/helium/__init__.py#L768) * [`CheckBox('I accept')`](https://github.com/mherrmann/helium/blob/0667ddb9be531367a0d707ad8f5fcfb75c528521/helium/__init__.py#L867) * [`RadioButton('Windows')`](https://github.com/mherrmann/helium/blob/0667ddb9be531367a0d707ad8f5fcfb75c528521/helium/__init__.py#L907) * [`Image(alt='Helium logo')`](https://github.com/mherrmann/helium/blob/0667ddb9be531367a0d707ad8f5fcfb75c528521/helium/__init__.py#L739) You can pass them into other functions such as `click(Link('Sign in'))`. But you can also use them to _read_ data from the web site. For instance: ```python print(TextField('First name').value) ``` A common use case is to use `.exists()` to check for the existence of an element. For example: ```python if Text('Accept cookies?').exists(): click('I accept') ``` I also often find `Text(...).value` useful for reading out data: ```python name = Text(to_right_of='Name:', below=Image(alt='Profile picture')).value ``` For a full list of element types and their properties, please see [the source code](https://github.com/mherrmann/helium/blob/0667ddb9be531367a0d707ad8f5fcfb75c528521/helium/__init__.py#L470-L1008). ## Finding elements relative to others You already saw in the previous section how `above=...` and `to_right_of=...` let you find elements relative to other elements. You can similarly use `below=...` and `to_left_of`. Here are some more examples. ```python Text(above='Balance', below='Transactions').value Link(to_right_of='Invoice:') Image(to_right_of=Link('Sign in', below=Text('Navigation'))) ``` ## Waiting for elements to appear (or other conditions) Use [`wait_until(...)`](https://github.com/mherrmann/helium/blob/0667ddb9be531367a0d707ad8f5fcfb75c528521/helium/__init__.py#L410) to wait for a condition to become true. For example: ```python wait_until(Button('Download').exists) ``` But you can also use this to wait for an arbitrary condition: ```python wait_until(lambda: TextField('Balance').value == '$2M') ``` ## jQuery-style selectors Sometimes, you do need to fall back to using HTML IDs, CSS Selectors or XPaths to identify an element on the web page. Helium's [`S(...)`](https://github.com/mherrmann/helium/blob/0667ddb9be531367a0d707ad8f5fcfb75c528521/helium/__init__.py#L568) predicate lets you do this. The parameter you pass to it is interpreted as follows: * If it starts with an ``@``, then it identifies elements by HTML ``name``. Eg. ``S("@btnName")`` identifies an element with ``name="btnName"``. * If it starts with ``//``, then Helium interprets it as an XPath. * Otherwise, Helium interprets it as a CSS selector. This in particular lets you write ``S("#myId")`` to identify an element with ``id="myId"``, or ``S(".myClass")`` to identify elements with ``class="myClass"``. As before, you can combine `S(...)` with other functions such as `click(S(...))`, or use it to extract data. For an example of this, see [below](#finding-all-elements). ## Combining Helium and Selenium's APIs All Helium does is translate your high-level commands into low-level Selenium function calls. Because of this, you can freely mix Selenium and Helium. For example: ```python # A Helium function: driver = start_chrome() # A Selenium API: driver.execute_script("alert('Hi!');") ``` You can also get / set the Selenium WebDriver which Helium uses via [`get_driver()`](https://github.com/mherrmann/helium/blob/0667ddb9be531367a0d707ad8f5fcfb75c528521/helium/__init__.py#L104) and [`set_driver(...)`](https://github.com/mherrmann/helium/blob/0667ddb9be531367a0d707ad8f5fcfb75c528521/helium/__init__.py#L97). With the WebDriver instance, you can execute any Selenium commands you want. To use Helium's API's to obtain Selenium `WebElement`s, use the `.web_element` property of Helium's various GUI elements. For instance: ```python # Get the CSS class of the "Helium" link: Link('Helium').web_element.get_attribute('class') ``` Here, `.get_attribute(...)` is a Selenium API. ## Finding all elements The `.web_element` property and the `S(...)` predicate are particularly useful for extracting multiple pieces of data from a web page. To do this, you can use Helium's [`find_all(...)`](https://github.com/mherrmann/helium/blob/0667ddb9be531367a0d707ad8f5fcfb75c528521/helium/__init__.py#L281) function. As its name implies, it lets you find all occurrences of an element on a page. For example: ```python email_cells = find_all(S("table > tr > td", below="Email")) emails = [cell.web_element.text for cell in email_cells] ``` ## Implicit waits When you issue a command such as `click('Download')`, Helium by default waits up to 10 seconds for the respective element to appear. This feature is called "implicit waiting". You can change the 10 second default to a different value via the [`Config` class](https://github.com/mherrmann/helium/blob/0667ddb9be531367a0d707ad8f5fcfb75c528521/helium/__init__.py#L437): ```python Config.implicit_wait_secs = 30 ``` However, before you do this, it may be better to add explicit waits to your code, such as `wait_until(Button('Download').exists)`. ## Alerts The [`Alert` class](https://github.com/mherrmann/helium/blob/0667ddb9be531367a0d707ad8f5fcfb75c528521/helium/__init__.py#L970) lets you interface with JavaScript popup boxes. Use `Alert().accept()`, `Alert().dismiss()` to click "Ok" or "Cancel", `Alert().text` to read the message shown, or `write(..., into=Alert())` to enter a value. ## File uploads, drag and drop, combo boxes, popups Use [`attach_file(...)`](https://github.com/mherrmann/helium/blob/0667ddb9be531367a0d707ad8f5fcfb75c528521/helium/__init__.py#L388), [`drag_file(...)`](https://github.com/mherrmann/helium/blob/0667ddb9be531367a0d707ad8f5fcfb75c528521/helium/__init__.py#L375), [`drag(...)`](https://github.com/mherrmann/helium/blob/0667ddb9be531367a0d707ad8f5fcfb75c528521/helium/__init__.py#L252), [`select(...)`](https://github.com/mherrmann/helium/blob/0667ddb9be531367a0d707ad8f5fcfb75c528521/helium/__init__.py#L362), [`switch_to(...)`](https://github.com/mherrmann/helium/blob/0667ddb9be531367a0d707ad8f5fcfb75c528521/helium/__init__.py#L1057). ## Clicking at x, y coordinates Sometimes, you may want to click at a specific `(x, y)` coordinate, or at an offset of an element. ### Create a `Point` to specify the coordinates and use it to click: ```python from helium import click point = Point(x=100, y=200) click(point) # Clicks at (100, 200) ``` ### Adjusting a Point by a offset You can modify a point's position using addition or subtraction by a delta: ```python delta = (20, -10) click(Point(100, 200) + delta) # Clicks at (120, 190) ``` See the [`Point` class](https://github.com/mherrmann/helium/blob/0667ddb9be531367a0d707ad8f5fcfb75c528521/helium/__init__.py#L1010) for more. ## Taking a screenshot Use Selenium's API: ```python get_driver().save_screenshot(r'C:\screenshot.png') ``` Note the leading `r`. This is required because the string contains a backslash `\`. ================================================ FILE: docs/conf.py ================================================ import os import sys from datetime import date sys.path.insert(0, os.path.abspath('..')) # -- Project information ----------------------------------------------------- project = 'helium' copyright = '%s, Michael Herrmann' % date.today().year author = 'Michael Herrmann' # Also update ../setup.py when you change this: release = '7.0.0' # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.githubpages', 'sphinx_rtd_theme'] html_theme = "sphinx_rtd_theme" # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] autodoc_member_order = 'bysource' ================================================ FILE: docs/contributors.rst ================================================ Contributors to this project ============================ .. Please use this format to add your contributions to this file `SocialUsernameName `_ (**Your Name**) - *Description of your contribution in a few words* - `mherrmann `_ (**Michael Herrmann**) - *Project creator and maintainer* - `IgnisDa `_ (**Diptesh Choudhuri**) - *Documentation maintainer* ================================================ FILE: docs/index.rst ================================================ Welcome to Helium's documentation! ================================== Helium is a Python library for automating web sites. It is based on `Selenium-python `_. Selenium is great, but difficult to use. Helium wraps around Selenium to give you a simpler API. Helium's name comes from being a lighter chemical element than Selenium. For a quick overview of Helium's features, please see `the project home page `_. Here, in the documentation, you will find a more comprehensive reference. .. toctree:: :maxdepth: 2 :caption: Contents: installation.rst api.rst contributors.rst Indices and tables ================== * :ref:`genindex` * :ref:`search` ================================================ FILE: docs/installation.rst ================================================ Installation ============ To install Helium, you need Python 3 and Chrome or Firefox. If you already know Python, then the following command should be all you need: .. code-block:: bash pip install helium Otherwise - Hi! I would recommend you create a virtual environment in the current directory. Any libraries you download (such as Helium) will be placed there. Enter the following into a command prompt: .. code-block:: bash python3 -m venv venv This creates a virtual environment in the `venv/` directory. To activate it: .. code-block:: bash # On Mac/Linux, bash shell: source venv/bin/activate # On Windows: call venv\Scripts\activate.bat Then, install Helium using `pip`: .. code-block:: bash python -m pip install helium Now enter :code:`python` into the command prompt and the command :code:`from helium import *` and you are ready to get started! ================================================ FILE: docs/make.bat ================================================ @ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd ================================================ FILE: helium/__init__.py ================================================ """ Helium's API is contained in module ``helium``. It is a simple Python API that makes specifying web automation cases as simple as describing them to someone looking over their shoulder at a screen. The public functions and classes of Helium are listed below. If you wish to use Helium functions in your Python scripts you can import them from the ``helium`` module:: from helium import * """ from collections import namedtuple, OrderedDict from copy import copy from helium._impl import APIImpl from helium._impl.util.html import get_easily_readable_snippet from helium._impl.util.inspect_ import repr_args from selenium.common import NoSuchElementException from selenium.webdriver.common.keys import Keys import helium._impl def start_chrome(url=None, headless=False, maximize=False, options=None): """ :param url: URL to open. :type url: str :param headless: Whether to start Chrome in headless mode. :type headless: bool :param maximize: Whether to maximize the Chrome window. Ignored when `headless` is set to `True`. :type maximize: bool :param options: ChromeOptions to use for starting the browser :type options: :py:class:`selenium.webdriver.ChromeOptions` Starts an instance of Google Chrome:: start_chrome() You can optionally open a URL:: start_chrome("google.com") The `headless` switch lets you prevent the browser window from appearing on your screen:: start_chrome(headless=True) start_chrome("google.com", headless=True) For more advanced configuration, use the `options` parameter:: from selenium.webdriver import ChromeOptions options = ChromeOptions() options.add_argument('--proxy-server=1.2.3.4:5678') options.set_capability('goog:loggingPrefs', {'performance': 'ALL'}) start_chrome(options=options) When no compatible ChromeDriver is found on your `PATH`, then `start_chrome` automatically downloads it using Selenium Manager. On shutdown of the Python interpreter, Helium terminates the ChromeDriver process but does not close the browser itself. If you want to close the browser at the end of your script, use the following command:: kill_browser() """ return _get_api_impl().start_chrome_impl(url, headless, maximize, options) def start_firefox(url=None, headless=False, options=None, profile=None): """ :param url: URL to open. :type url: str :param headless: Whether to start Firefox in headless mode. :type headless: bool :param options: FirefoxOptions to use for starting the browser. :type options: :py:class:`selenium.webdriver.FirefoxOptions` :param profile: FirefoxProfile to use for starting the browser. :type profile: :py:class:`selenium.webdriver.FirefoxProfile` Starts an instance of Firefox:: start_firefox() If this doesn't work for you, then it may be that Helium's copy of geckodriver is not compatible with your version of Firefox. To fix this, place a copy of geckodriver on your `PATH`. You can optionally open a URL:: start_firefox("google.com") The `headless` switch lets you prevent the browser window from appearing on your screen:: start_firefox(headless=True) start_firefox("google.com", headless=True) For more advanced configuration, use the `options` parameter:: from selenium.webdriver import FirefoxOptions options = FirefoxOptions() options.add_argument("--width=2560") options.add_argument("--height=1440") start_firefox(options=options) To set proxy, useragent, etc. (ie. things you find in about:config), use the `profile` parameter:: from selenium.webdriver import FirefoxProfile profile = FirefoxProfile() SOCKS5_PROXY_HOST = "0.0.0.0" PROXY_PORT = 0 profile.set_preference("network.proxy.type", 1) profile.set_preference("network.proxy.socks", SOCKS5_PROXY_HOST) profile.set_preference("network.proxy.socks_port", PROXY_PORT) profile.set_preference("network.proxy.socks_remote_dns", True) profile.set_preference("network.proxy.socks_version", 5) profile.set_preference("network.proxy.no_proxies_on", "localhost, 10.20.30.40") USER_AGENT = "Mozilla/5.0 ..." profile.set_preference("general.useragent.override", USER_AGENT) start_firefox(profile=profile) On shutdown of the Python interpreter, Helium cleans up all resources used for controlling the browser (such as the geckodriver process), but does not close the browser itself. If you want to terminate the browser at the end of your script, use the following command:: kill_browser() """ return _get_api_impl().start_firefox_impl(url, headless, options, profile) def go_to(url): """ :param url: URL to open. :type url: str Opens the specified URL in the current web browser window. For instance:: go_to("google.com") """ _get_api_impl().go_to_impl(url) def set_driver(driver): """ Sets the Selenium WebDriver used to execute Helium commands. See also :py:func:`get_driver`. """ _get_api_impl().set_driver_impl(driver) def get_driver(): """ Returns the Selenium WebDriver currently used by Helium to execute all commands. Each Helium command such as ``click("Login")`` is translated to a sequence of Selenium commands that are issued to this driver. """ return _get_api_impl().get_driver_impl() def write(text, into=None): """ :param text: The text to be written. :type text: one of str, unicode :param into: The element to write into. :type into: one of str, unicode, :py:class:`HTMLElement`, \ :py:class:`selenium.webdriver.remote.webelement.WebElement`, :py:class:`Alert` Types the given text into the active window. If parameter 'into' is given, writes the text into the text field or element identified by that parameter. Common examples of 'write' are:: write("Hello World!") write("user12345", into="Username:") write("Michael", into=Alert("Please enter your name")) """ _get_api_impl().write_impl(text, into) def press(key): """ :param key: Key or combination of keys to be pressed. Presses the given key or key combination. To press a normal letter key such as 'a' simply call `press` for it:: press('a') You can also simulate the pressing of upper case characters that way:: press('A') The special keys you can press are those given by Selenium's class :py:class:`selenium.webdriver.common.keys.Keys`. Helium makes all those keys available through its namespace, so you can just use them without having to refer to :py:class:`selenium.webdriver.common.keys.Keys`. For instance, to press the Enter key:: press(ENTER) To press multiple keys at the same time, concatenate them with `+`. For example, to press Control + a, call:: press(CONTROL + 'a') """ _get_api_impl().press_impl(key) NULL = Keys.NULL CANCEL = Keys.CANCEL HELP = Keys.HELP BACK_SPACE = Keys.BACK_SPACE TAB = Keys.TAB CLEAR = Keys.CLEAR RETURN = Keys.RETURN ENTER = Keys.ENTER SHIFT = Keys.SHIFT LEFT_SHIFT = Keys.LEFT_SHIFT CONTROL = Keys.CONTROL LEFT_CONTROL = Keys.LEFT_CONTROL ALT = Keys.ALT LEFT_ALT = Keys.LEFT_ALT PAUSE = Keys.PAUSE ESCAPE = Keys.ESCAPE SPACE = Keys.SPACE PAGE_UP = Keys.PAGE_UP PAGE_DOWN = Keys.PAGE_DOWN END = Keys.END HOME = Keys.HOME LEFT = Keys.LEFT ARROW_LEFT = Keys.ARROW_LEFT UP = Keys.UP ARROW_UP = Keys.ARROW_UP RIGHT = Keys.RIGHT ARROW_RIGHT = Keys.ARROW_RIGHT DOWN = Keys.DOWN ARROW_DOWN = Keys.ARROW_DOWN INSERT = Keys.INSERT DELETE = Keys.DELETE SEMICOLON = Keys.SEMICOLON EQUALS = Keys.EQUALS NUMPAD0 = Keys.NUMPAD0 NUMPAD1 = Keys.NUMPAD1 NUMPAD2 = Keys.NUMPAD2 NUMPAD3 = Keys.NUMPAD3 NUMPAD4 = Keys.NUMPAD4 NUMPAD5 = Keys.NUMPAD5 NUMPAD6 = Keys.NUMPAD6 NUMPAD7 = Keys.NUMPAD7 NUMPAD8 = Keys.NUMPAD8 NUMPAD9 = Keys.NUMPAD9 MULTIPLY = Keys.MULTIPLY ADD = Keys.ADD SEPARATOR = Keys.SEPARATOR SUBTRACT = Keys.SUBTRACT DECIMAL = Keys.DECIMAL DIVIDE = Keys.DIVIDE F1 = Keys.F1 F2 = Keys.F2 F3 = Keys.F3 F4 = Keys.F4 F5 = Keys.F5 F6 = Keys.F6 F7 = Keys.F7 F8 = Keys.F8 F9 = Keys.F9 F10 = Keys.F10 F11 = Keys.F11 F12 = Keys.F12 META = Keys.META COMMAND = Keys.COMMAND def click(element): """ :param element: The element or point to click. :type element: str, unicode, :py:class:`HTMLElement`, \ :py:class:`selenium.webdriver.remote.webelement.WebElement` or :py:class:`Point` Clicks on the given element or point. Common examples are:: click("Sign in") click(Button("OK")) click(Point(200, 300)) click(ComboBox("File type").top_left + (50, 0)) """ _get_api_impl().click_impl(element) def doubleclick(element): """ :param element: The element or point to click. :type element: str, unicode, :py:class:`HTMLElement`, \ :py:class:`selenium.webdriver.remote.webelement.WebElement` or :py:class:`Point` Performs a double-click on the given element or point. For example:: doubleclick("Double click here") doubleclick(Image("Directories")) doubleclick(Point(200, 300)) doubleclick(TextField("Username").top_left - (0, 20)) """ _get_api_impl().doubleclick_impl(element) def drag(element, to): """ :param element: The element or point to drag. :type element: str, unicode, :py:class:`HTMLElement`, \ :py:class:`selenium.webdriver.remote.webelement.WebElement` or :py:class:`Point` :param to: The element or point to drag to. :type to: str, unicode, :py:class:`HTMLElement`, \ :py:class:`selenium.webdriver.remote.webelement.WebElement` or :py:class:`Point` Drags the given element or point to the given location. For example:: drag("Drag me!", to="Drop here.") The dragging is performed by hovering the mouse cursor over ``element``, pressing and holding the left mouse button, moving the mouse cursor over ``to``, and then releasing the left mouse button again. This function is exclusively used for dragging elements inside one web page. If you wish to drag a file from the hard disk onto the browser window (eg. to initiate a file upload), use function :py:func:`drag_file`. """ _get_api_impl().drag_impl(element, to) def press_mouse_on(element): _get_api_impl().press_mouse_on_impl(element) def release_mouse_over(element): _get_api_impl().release_mouse_over_impl(element) def find_all(predicate): """ Lets you find all occurrences of the given GUI element predicate. For instance, the following statement returns a list of all buttons with label "Open":: find_all(Button("Open")) Other examples are:: find_all(Window()) find_all(TextField("Address line 1")) The function returns a list of elements of the same type as the passed-in parameter. For instance, ``find_all(Button(...))`` yields a list whose elements are of type :py:class:`Button`. In a typical usage scenario, you want to pick out one of the occurrences returned by :py:func:`find_all`. In such cases, :py:func:`list.sort` can be very useful. For example, to find the leftmost "Open" button, you can write:: buttons = find_all(Button("Open")) leftmost_button = sorted(buttons, key=lambda button: button.x)[0] """ return _get_api_impl().find_all_impl(predicate) def scroll_down(num_pixels=100): """ Scrolls down the page the given number of pixels. """ _get_api_impl().scroll_down_impl(num_pixels) def scroll_up(num_pixels=100): """ Scrolls the the page up the given number of pixels. """ _get_api_impl().scroll_up_impl(num_pixels) def scroll_right(num_pixels=100): """ Scrolls the page to the right the given number of pixels. """ _get_api_impl().scroll_right_impl(num_pixels) def scroll_left(num_pixels=100): """ Scrolls the page to the left the given number of pixels. """ _get_api_impl().scroll_left_impl(num_pixels) def hover(element): """ :param element: The element or point to hover. :type element: str, unicode, :py:class:`HTMLElement`, \ :py:class:`selenium.webdriver.remote.webelement.WebElement` or :py:class:`Point` Hovers the mouse cursor over the given element or point. For example:: hover("File size") hover(Button("OK")) hover(Link("Download")) hover(Point(200, 300)) hover(ComboBox("File type").top_left + (50, 0)) """ _get_api_impl().hover_impl(element) def rightclick(element): """ :param element: The element or point to click. :type element: str, unicode, :py:class:`HTMLElement`, \ :py:class:`selenium.webdriver.remote.webelement.WebElement` or :py:class:`Point` Performs a right click on the given element or point. For example:: rightclick("Something") rightclick(Point(200, 300)) rightclick(Image("captcha")) """ _get_api_impl().rightclick_impl(element) def select(combo_box, value): """ :param combo_box: The combo box whose value should be changed. :type combo_box: str, unicode or :py:class:`ComboBox` :param value: The visible value of the combo box to be selected. Selects a value from a combo box. For example:: select("Language", "English") select(ComboBox("Language"), "English") """ _get_api_impl().select_impl(combo_box, value) def drag_file(file_path, to): """ Simulates the dragging of a file from the computer over the browser window and dropping it over the given element. This allows, for example, to attach files to emails in Gmail:: click("COMPOSE") write("example@gmail.com", into="To") write("Email subject", into="Subject") drag_file(r"C:\\Documents\\notes.txt", to="Drop files here") """ _get_api_impl().drag_file_impl(file_path, to) def attach_file(file_path, to=None): """ :param file_path: The path of the file to be attached. :param to: The file input element to which the file should be attached. Allows attaching a file to a file input element. For instance:: attach_file("c:/test.txt", to="Please select a file:") The file input element is identified by its label. If you omit the ``to=`` parameter, then Helium attaches the file to the first file input element it finds on the page. """ _get_api_impl().attach_file_impl(file_path, to=to) def refresh(): """ Refreshes the current page. If an alert dialog is open, then Helium first closes it. """ _get_api_impl().refresh_impl() def wait_until(condition_fn, timeout_secs=10, interval_secs=0.5): """ :param condition_fn: A function taking no arguments that represents the \ condition to be waited for. :param timeout_secs: The timeout, in seconds, after which the condition is \ deemed to have failed. :param interval_secs: The interval, in seconds, at which the condition \ function is polled to determine whether the wait has succeeded. Waits until the given condition function evaluates to true. This is most commonly used to wait for an element to exist:: wait_until(Text("Finished!").exists) More elaborate conditions are also possible using Python lambda expressions. For instance, to wait until a text no longer exists:: wait_until(lambda: not Text("Uploading...").exists()) ``wait_until`` raises :py:class:`selenium.common.exceptions.TimeoutException` if the condition is not satisfied within the given number of seconds. The parameter ``interval_secs`` specifies the number of seconds Helium waits between evaluating the condition function. """ _get_api_impl().wait_until_impl(condition_fn, timeout_secs, interval_secs) class Config: """ This class contains Helium's run-time configuration. To modify Helium's behaviour, simply assign to the properties of this class. For instance:: Config.implicit_wait_secs = 0 """ implicit_wait_secs = 10 """ ``implicit_wait_secs`` is Helium's analogue to Selenium's ``.implicitly_wait(secs)``. Suppose you have a script that executes the following command:: >>> click("Download") If the "Download" element is not immediately available, then Helium waits up to ``implicit_wait_secs`` for it to appear before raising a ``LookupError``. This is useful in situations where the page takes slightly longer to load, or a GUI element only appears after a certain time. To disable Helium's implicit waits, simply execute:: Config.implicit_wait_secs = 0 Helium's implicit waits do not affect commands :py:func:`find_all` or :py:func:`GUIElement.exists`. Note also that setting ``implicit_wait_secs`` does not affect the underlying Selenium driver (see :py:func:`get_driver`). For the best results, it is recommended to not use Selenium's ``.implicitly_wait(...)`` in conjunction with Helium. """ class GUIElement: def __init__(self): self._driver = _get_api_impl().require_driver() self._args = [] self._kwargs = OrderedDict() self._impl_cached = None def exists(self): """ Evaluates to true if this GUI element exists. """ return self._impl.exists() def with_impl(self, impl): result = copy(self) result._impl = impl return result @property def _impl(self): if self._impl_cached is None: impl_class = \ getattr(helium._impl, self.__class__.__name__ + 'Impl') self._impl_cached = impl_class( self._driver, *self._args, **self._kwargs ) return self._impl_cached @_impl.setter def _impl(self, value): self._impl_cached = value def __repr__(self): return self._repr_constructor_args(self._args, self._kwargs) def _repr_constructor_args(self, args=None, kwargs=None): if args is None: args = [] if kwargs is None: kwargs = {} return '%s(%s)' % ( self.__class__.__name__, repr_args(self.__init__, args, kwargs, repr) ) def _is_bound(self): return self._impl_cached is not None and self._impl_cached._is_bound() class HTMLElement(GUIElement): def __init__( self, below=None, to_right_of=None, above=None, to_left_of=None ): super(HTMLElement, self).__init__() self._kwargs['below'] = below self._kwargs['to_right_of'] = to_right_of self._kwargs['above'] = above self._kwargs['to_left_of'] = to_left_of @property def width(self): """ The width of this HTML element, in pixels. """ return self._impl.width @property def height(self): """ The height of this HTML element, in pixels. """ return self._impl.height @property def x(self): """ The x-coordinate on the page of the top-left point of this HTML element. """ return self._impl.x @property def y(self): """ The y-coordinate on the page of the top-left point of this HTML element. """ return self._impl.y @property def top_left(self): """ The top left corner of this element, as a :py:class:`helium.Point`. This point has exactly the coordinates given by this element's `.x` and `.y` properties. `top_left` is for instance useful for clicking at an offset of an element:: click(Button("OK").top_left + (30, 15)) """ return self._impl.top_left @property def web_element(self): """ The Selenium WebElement corresponding to this element. """ return self._impl.web_element def __repr__(self): if self._is_bound(): try: element_html = self.web_element.get_attribute('outerHTML') except NoSuchElementException: # This can happen when the element is not in the current iframe. # We could call `self._impl.first_occurrence.get_attribute(...)` # instead of `self.web_element.get_attribute(...)` to avoid it. # However, this would change the current frame. That seems too # surprising a side effect for a repr(...) call. So we instead # catch the error and fall back to the super implementation # further below. pass else: return get_easily_readable_snippet(element_html) return super(HTMLElement, self).__repr__() class S(HTMLElement): """ :param selector: The selector used to identify the HTML element(s). A jQuery-style selector for identifying HTML elements by ID, name, CSS class, CSS selector or XPath. For example: Say you have an element with ID "myId" on a web page, such as ``
``. Then you can identify this element using ``S`` as follows:: S("#myId") The parameter which you pass to ``S(...)`` is interpreted by Helium according to these rules: * If it starts with an ``@``, then it identifies elements by HTML ``name``. Eg. ``S("@btnName")`` identifies an element with ``name="btnName"``. * If it starts with ``//``, then Helium interprets it as an XPath. * Otherwise, Helium interprets it as a CSS selector. This in particular lets you write ``S("#myId")`` to identify an element with ``id="myId"``, or ``S(".myClass")`` to identify elements with ``class="myClass"``. ``S`` also makes it possible to read plain text data from a web page. For example, suppose you have a table of people's email addresses. Then you can read the list of email addresses as follows:: email_cells = find_all(S("table > tr > td", below="Email")) emails = [cell.web_element.text for cell in email_cells] Where ``email`` is the column header (``Email``). Similarly to ``below`` and ``to_right_of``, the keyword parameters ``above`` and ``to_left_of`` can be used to search for elements above and to the left of other web elements. """ def __init__(self, selector, below=None, to_right_of=None, above=None, to_left_of=None): super(S, self).__init__( below=below, to_right_of=to_right_of, above=above, to_left_of=to_left_of ) self._args.append(selector) class Text(HTMLElement): """ Lets you identify any text or label on a web page. This is most useful for checking whether a particular text exists:: if Text("Do you want to proceed?").exists(): click("Yes") ``Text`` also makes it possible to read plain text data from a web page. For example, suppose you have a table of people's email addresses. Then you can read John's email addresses as follows:: Text(below="Email", to_right_of="John").value Similarly to ``below`` and ``to_right_of``, the keyword parameters ``above`` and ``to_left_of`` can be used to search for texts above and to the left of other web elements. """ def __init__( self, text=None, below=None, to_right_of=None, above=None, to_left_of=None ): super(Text, self).__init__( below=below, to_right_of=to_right_of, above=above, to_left_of=to_left_of ) self._args.append(text) @property def value(self): """ Returns the current value of this Text object. """ return self._impl.value class Link(HTMLElement): """ Lets you identify a link on a web page. A typical usage of ``Link`` is:: click(Link("Sign in")) You can also read a ``Link``'s properties. This is most typically used to check for a link's existence before clicking on it:: if Link("Sign in").exists(): click(Link("Sign in")) When there are multiple occurrences of a link on a page, you can disambiguate between them using the keyword parameters ``below``, ``to_right_of``, ``above`` and ``to_left_of``. For instance:: click(Link("Block User", to_right_of="John Doe")) """ def __init__( self, text=None, below=None, to_right_of=None, above=None, to_left_of=None ): super(Link, self).__init__( below=below, to_right_of=to_right_of, above=above, to_left_of=to_left_of ) self._args.append(text) @property def href(self): """ Returns the URL of the page the link goes to. """ return self._impl.href class ListItem(HTMLElement): """ Lets you identify a list item (HTML ``
  • `` element) on a web page. This is often useful for interacting with elements of a navigation bar:: click(ListItem("News Feed")) In other cases such as an automated test, you might want to query the properties of a ``ListItem``. For example, the following line checks whether a list item with text "List item 1" exists, and raises an error if not:: assert ListItem("List item 1").exists() When there are multiple occurrences of a list item on a page, you can disambiguate between them using the keyword parameters ``below``, ``to_right_of``, ``above`` and ``to_left_of``. For instance:: click(ListItem("List item 1", below="My first list:")) """ def __init__( self, text=None, below=None, to_right_of=None, above=None, to_left_of=None ): super(ListItem, self).__init__( below=below, to_right_of=to_right_of, above=above, to_left_of=to_left_of ) self._args.append(text) class Button(HTMLElement): """ Lets you identify a button on a web page. A typical usage of ``Button`` is:: click(Button("Log In")) ``Button`` also lets you read a button's properties. For example, the following snippet clicks button "OK" only if it exists:: if Button("OK").exists(): click(Button("OK")) When there are multiple occurrences of a button on a page, you can disambiguate between them using the keyword parameters ``below``, ``to_right_of``, ``above`` and ``to_left_of``. For instance:: click(Button("Log In", below=TextField("Password"))) """ def __init__( self, text=None, below=None, to_right_of=None, above=None, to_left_of=None ): super(Button, self).__init__( below=below, to_right_of=to_right_of, above=above, to_left_of=to_left_of ) self._args.append(text) def is_enabled(self): """ Returns true if this UI element can currently be interacted with. """ return self._impl.is_enabled() class Image(HTMLElement): """ Lets you identify an image (HTML ```` element) on a web page. Typically, this is done via the image's alt text. For instance:: click(Image(alt="Helium Logo")) You can also query an image's properties. For example, the following snippet clicks on the image with alt text "Helium Logo" only if it exists:: if Image("Helium Logo").exists(): click(Image("Helium Logo")) When there are multiple occurrences of an image on a page, you can disambiguate between them using the keyword parameters ``below``, ``to_right_of``, ``above`` and ``to_left_of``. For instance:: click(Image("Helium Logo", to_left_of=ListItem("Download"))) """ def __init__( self, alt=None, below=None, to_right_of=None, above=None, to_left_of=None ): super(Image, self).__init__( below=below, to_right_of=to_right_of, above=above, to_left_of=to_left_of ) self._args.append(alt) class TextField(HTMLElement): """ Lets you identify a text field on a web page. This is most typically done to read the value of a text field. For example:: TextField("First name").value This returns the value of the "First name" text field. If it is empty, the empty string "" is returned. When there are multiple occurrences of a text field on a page, you can disambiguate between them using the keyword parameters ``below``, ``to_right_of``, ``above`` and ``to_left_of``. For instance:: TextField("Address line 1", below="Billing Address:").value """ def __init__( self, label=None, below=None, to_right_of=None, above=None, to_left_of=None ): super(TextField, self).__init__( below=below, to_right_of=to_right_of, above=above, to_left_of=to_left_of ) self._args.append(label) @property def value(self): """ Returns the current value of this text field. '' if there is no value. """ return self._impl.value def is_enabled(self): """ Returns true if this UI element can currently be interacted with. The difference between a text field being 'enabled' and 'editable' is mostly visual: If a text field is not enabled, it is usually greyed out, whereas if it is not editable it looks normal. See also ``is_editable``. """ return self._impl.is_enabled() def is_editable(self): """ Returns true if the value of this UI element can be modified. The difference between a text field being 'enabled' and 'editable' is mostly visual: If a text field is not enabled, it is usually greyed out, whereas if it is not editable it looks normal. See also ``is_enabled``. """ return self._impl.is_editable() class ComboBox(HTMLElement): """ Lets you identify a combo box on a web page. This can for instance be used to determine the current value of a combo box:: ComboBox("Language").value A ComboBox may be *editable*, which means that it is possible to type in arbitrary values in addition to selecting from a predefined drop-down list of values. The property :py:func:`ComboBox.is_editable` can be used to determine whether this is the case for a particular combo box instance. When there are multiple occurrences of a combo box on a page, you can disambiguate between them using the keyword parameters ``below``, ``to_right_of``, ``above`` and ``to_left_of``. For instance:: select(ComboBox(to_right_of="John Doe", below="Status"), "Active") This sets the Status of John Doe to Active on the page. """ def __init__( self, label=None, below=None, to_right_of=None, above=None, to_left_of=None ): super(ComboBox, self).__init__( below=below, to_right_of=to_right_of, above=above, to_left_of=to_left_of ) self._args.append(label) def is_editable(self): """ Returns whether this combo box allows entering an arbitrary text in addition to selecting predefined values from a drop-down list. """ return self._impl.is_editable() @property def value(self): """ Returns the currently selected combo box value. """ return self._impl.value @property def options(self): """ Returns a list of all possible options available to choose from in the ComboBox. """ return self._impl.options class CheckBox(HTMLElement): """ Lets you identify a check box on a web page. To tick a currently unselected check box, use:: click(CheckBox("I agree")) ``CheckBox`` also lets you read the properties of a check box. For example, the method :py:func:`CheckBox.is_checked` can be used to only click a check box if it isn't already checked:: if not CheckBox("I agree").is_checked(): click(CheckBox("I agree")) When there are multiple occurrences of a check box on a page, you can disambiguate between them using the keyword parameters ``below``, ``to_right_of``, ``above`` and ``to_left_of``. For instance:: click(CheckBox("Stay signed in", below=Button("Sign in"))) """ def __init__( self, label=None, below=None, to_right_of=None, above=None, to_left_of=None ): super(CheckBox, self).__init__( below=below, to_right_of=to_right_of, above=above, to_left_of=to_left_of ) self._args.append(label) def is_enabled(self): """ Returns True if this GUI element can currently be interacted with. """ return self._impl.is_enabled() def is_checked(self): """ Returns True if this GUI element is checked (selected). """ return self._impl.is_checked() class RadioButton(HTMLElement): """ Lets you identify a radio button on a web page. To select a currently unselected radio button, use:: click(RadioButton("Windows")) ``RadioButton`` also lets you read the properties of a radio button. For example, the method :py:func:`RadioButton.is_selected` can be used to only click a radio button if it isn't already selected:: if not RadioButton("Windows").is_selected(): click(RadioButton("Windows")) When there are multiple occurrences of a radio button on a page, you can disambiguate between them using the keyword parameters ``below``, ``to_right_of``, ``above`` and ``to_left_of``. For instance:: click(RadioButton("I accept", below="License Agreement")) """ def __init__( self, label=None, below=None, to_right_of=None, above=None, to_left_of=None ): super(RadioButton, self).__init__( below=below, to_right_of=to_right_of, above=above, to_left_of=to_left_of ) self._args.append(label) def is_selected(self): """ Returns true if this radio button is selected. """ return self._impl.is_selected() class Window(GUIElement): """ Lets you identify individual windows of the currently open browser session. """ def __init__(self, title=None): super(Window, self).__init__() self._args.append(title) @property def title(self): """ Returns the title of this Window. """ return self._impl.title @property def handle(self): """ Returns the Selenium driver window handle assigned to this window. Note that this window handle is simply an abstract identifier and bears no relationship to the corresponding operating system handle (HWND on Windows). """ return self._impl.handle def __repr__(self): if self._is_bound(): return self._repr_constructor_args([self.title]) else: return super(Window, self).__repr__() class Alert(GUIElement): """ Lets you identify and interact with JavaScript alert boxes. """ def __init__(self, search_text=None): super(Alert, self).__init__() self._args.append(search_text) @property def text(self): """ The text displayed in the alert box. """ return self._impl.text def accept(self): """ Accepts this alert. This typically corresponds to clicking the "OK" button inside the alert. The typical way to use this method is:: >>> Alert().accept() This accepts the currently open alert. """ self._impl.accept() def dismiss(self): """ Dismisses this alert. This typically corresponds to clicking the "Cancel" or "Close" button of the alert. The typical way to use this method is:: >>> Alert().dismiss() This dismisses the currently open alert. """ self._impl.dismiss() def __repr__(self): if self._is_bound(): return self._repr_constructor_args([self.text]) else: return super(Alert, self).__repr__() class Point(namedtuple('Point', ['x', 'y'])): """ A clickable point. To create a ``Point`` at an offset of an existing point, use ``+`` and ``-``:: >>> point = Point(x=10, y=25) >>> point + (10, 0) Point(x=20, y=25) >>> point - (0, 10) Point(x=10, y=15) """ def __new__(cls, x=0, y=0): return cls.__bases__[0].__new__(cls, x, y) def __init__(self, x=0, y=0): # tuple is immutable so we can't do anything here. The initialization # happens in __new__(...) above. pass @property def x(self): """ The x coordinate of the point. """ return self[0] @property def y(self): """ The y coordinate of the point. """ return self[1] def __eq__(self, other): return (self.x, self.y) == other def __ne__(self, other): return not self == other def __hash__(self): return self.x + 7 * self.y def __add__(self, delta): dx, dy = delta return Point(self.x + dx, self.y + dy) def __radd__(self, delta): return self.__add__(delta) def __sub__(self, delta): dx, dy = delta return Point(self.x - dx, self.y - dy) def __rsub__(self, delta): x, y = delta return Point(x - self.x, y - self.y) def switch_to(window): """ :param window: The title (string) of a browser window or a \ :py:class:`Window` object Switches to the given browser window. For example:: switch_to("Google") This searches for a browser window whose title contains "Google", and activates it. If there are multiple windows with the same title, then you can use :py:func:`find_all` to find all open windows, pick out the one you want and pass that to ``switch_to``. For example, the following snippet switches to the first window in the list of open windows:: switch_to(find_all(Window())[0]) """ _get_api_impl().switch_to_impl(window) def kill_browser(): """ Closes the current browser with all associated windows and potentially open dialogs. Dialogs opened as a response to the browser closing (eg. "Are you sure you want to leave this page?") are also ignored and closed. This function is most commonly used to close the browser at the end of an automation run:: start_chrome() ... # Close Chrome: kill_browser() """ _get_api_impl().kill_browser_impl() def highlight(element): """ :param element: The element to highlight. Highlights the given element on the webpage by drawing a red rectangle around it. This is useful for debugging purposes. For example:: highlight("Helium") highlight(Button("Sign in")) """ _get_api_impl().highlight_impl(element) def _get_api_impl(): global _API_IMPL if _API_IMPL is None: _API_IMPL = APIImpl() return _API_IMPL _API_IMPL = None ================================================ FILE: helium/_impl/__init__.py ================================================ from copy import copy from helium._impl.match_type import PREFIX_IGNORE_CASE from helium._impl.selenium_wrappers import WebElementWrapper, \ WebDriverWrapper, FrameIterator, FramesChangedWhileIterating from helium._impl.util.dictionary import inverse from helium._impl.util.system import is_windows, get_canonical_os_name from helium._impl.util.xpath import lower, predicate, predicate_or from inspect import getfullargspec, ismethod, isfunction from selenium.common.exceptions import UnexpectedAlertPresentException, \ ElementNotVisibleException, MoveTargetOutOfBoundsException, \ WebDriverException, StaleElementReferenceException, \ NoAlertPresentException, NoSuchWindowException from selenium.webdriver.common.by import By from selenium.webdriver.firefox.service import Service as ServiceFirefox from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support.ui import Select from selenium.webdriver import Chrome, ChromeOptions, Firefox, FirefoxOptions from time import sleep, time import atexit import re def might_spawn_window(f): def f_decorated(self, *args, **kwargs): driver = self.require_driver() if driver.is_ie() and AlertImpl(driver).exists(): # Accessing .window_handles in IE when an alert is present raises an # UnexpectedAlertPresentException. When DesiredCapability # 'unexpectedAlertBehaviour' is not 'ignore' (the default is # 'dismiss'), this leads to the alert being closed. Since we don't # want to unintentionally close alert dialogs, we therefore do not # access .window_handles in IE when an alert is present. return f(self, *args, **kwargs) window_handles_before = driver.window_handles[:] result = f(self, *args, **kwargs) # As above, don't access .window_handles in IE if an alert is present: if not (driver.is_ie() and AlertImpl(driver).exists()): if driver.is_firefox(): # Unlike Chrome, Firefox does not wait for new windows to open. # Give it a little time to do so: sleep(.2) new_window_handles = [ h for h in driver.window_handles if h not in window_handles_before ] if new_window_handles: driver.switch_to.window(new_window_handles[0]) return result return f_decorated def handle_unexpected_alert(f): def f_decorated(*args, **kwargs): try: return f(*args, **kwargs) except UnexpectedAlertPresentException: raise UnexpectedAlertPresentException( "This command is not supported when an alert is present. To " "accept the alert (this usually corresponds to clicking 'OK') " "use `Alert().accept()`. To dismiss the alert (ie. 'cancel' " "it), use `Alert().dismiss()`. If the alert contains a text " "field, you can use write(...) to set its value. " "Eg.: `write('hi there!')`." ) return f_decorated class APIImpl: DRIVER_REQUIRED_MESSAGE = \ "This operation requires a browser window. Please call one of " \ "the following functions first:\n" \ " * start_chrome()\n" \ " * start_firefox()\n" \ " * set_driver(...)" def __init__(self): self.driver = None def start_firefox_impl( self, url=None, headless=False, options=None, profile=None ): firefox_driver = self._start_firefox_driver(headless, options, profile) return self._start(firefox_driver, url) def _start_firefox_driver(self, headless, options, profile): firefox_options = FirefoxOptions() if options is None else options if headless: firefox_options.add_argument('--headless') kwargs = { 'options': firefox_options } if profile: firefox_options.profile = profile service_log_path = 'nul' if is_windows() else '/dev/null' service = ServiceFirefox(log_path=service_log_path) result = Firefox(service=service, **kwargs) return result def start_chrome_impl( self, url=None, headless=False, maximize=False, options=None ): chrome_driver = self._start_chrome_driver(headless, maximize, options) return self._start(chrome_driver, url) def _start_chrome_driver(self, headless, maximize, options): chrome_options = self._get_chrome_options(headless, maximize, options) result = Chrome(options=chrome_options) atexit.register(self._kill_service, result.service) return result def _get_chrome_options(self, headless, maximize, options): result = ChromeOptions() if options is None else options # Prevent Chrome's debug logs from appearing in our console window: result.add_experimental_option('excludeSwitches', ['enable-logging']) if headless: result.add_argument('--headless') elif maximize: result.add_argument('--start-maximized') # Chrome 140.0.7339.185 or earlier introduced password leak detection. # Writing leaked credentials into an input field sometimes brings up a # blocking browser notification "The password you just used was found in # a data breach". Prevent this: prefs = dict(result.experimental_options.get('prefs', {})) if 'profile.password_manager_leak_detection' not in prefs: prefs['profile.password_manager_leak_detection'] = False result.add_experimental_option('prefs', prefs) return result def _kill_service(self, service): old = service.send_remote_shutdown_command service.send_remote_shutdown_command = lambda: None try: service.stop() finally: service.send_remote_shutdown_command = old def _start(self, browser, url=None): self.set_driver_impl(browser) if url is not None: self.go_to_impl(url) return self.get_driver_impl() @might_spawn_window @handle_unexpected_alert def go_to_impl(self, url): if '://' not in url: url = 'http://' + url self.require_driver().get(url) def set_driver_impl(self, driver): self.driver = WebDriverWrapper(driver) def get_driver_impl(self): if self.driver is not None: return self.driver.unwrap() @might_spawn_window @handle_unexpected_alert def write_impl(self, text, into=None): if into is not None: from helium import GUIElement if isinstance(into, GUIElement): into = into._impl self._handle_alerts( self._write_no_alert, self._write_with_alert, text, into=into ) def _write_no_alert(self, text, into=None): if into: if isinstance(into, str): into = TextFieldImpl(self.require_driver(), into) def _write(elt): if hasattr(elt, 'clear') and callable(elt.clear): elt.clear() elt.send_keys(text) self._manipulate(into, _write) else: self.require_driver().switch_to.active_element.send_keys(text) def _write_with_alert(self, text, into=None): if into is None: into = AlertImpl(self.require_driver()) if not isinstance(into, AlertImpl): raise UnexpectedAlertPresentException( "into=%r is not allowed when an alert is present." % into ) into._write(text) def _handle_alerts(self, no_alert, with_alert, *args, **kwargs): driver = self.require_driver() if not AlertImpl(driver).exists(): return no_alert(*args, **kwargs) return with_alert(*args, **kwargs) @might_spawn_window @handle_unexpected_alert def press_impl(self, key): self.require_driver().switch_to.active_element.send_keys(key) def click_impl(self, element): self._perform_mouse_action(element, self._click) def doubleclick_impl(self, element): self._perform_mouse_action(element, self._doubleclick) def hover_impl(self, element): self._perform_mouse_action(element, self._hover) def rightclick_impl(self, element): self._perform_mouse_action(element, self._rightclick) def press_mouse_on_impl(self, element): self._perform_mouse_action(element, self._press_mouse_on) def release_mouse_over_impl(self, element): self._perform_mouse_action(element, self._release_mouse_over) def _click(self, selenium_elt, offset): self._move_to_element(selenium_elt, offset).click().perform() def _doubleclick(self, selenium_elt, offset): self._move_to_element(selenium_elt, offset).double_click().perform() def _hover(self, selenium_elt, offset): self._move_to_element(selenium_elt, offset).perform() def _rightclick(self, selenium_elt, offset): self._move_to_element(selenium_elt, offset).context_click().perform() def _press_mouse_on(self, selenium_elt, offset): self._move_to_element(selenium_elt, offset).click_and_hold().perform() def _release_mouse_over(self, selenium_elt, offset): self._move_to_element(selenium_elt, offset).release().perform() def _move_to_element(self, element, offset): result = self.require_driver().action() if offset is not None: result.move_to_element_with_offset( element, offset[0] - element.size['width'] / 2, offset[1] - element.size['height'] / 2 ) else: result.move_to_element(element) return result def drag_impl(self, element, to): with DragHelper(self) as drag_helper: self._perform_mouse_action(element, drag_helper.start_dragging) self._perform_mouse_action(to, drag_helper.drop_on_target) @might_spawn_window @handle_unexpected_alert def _perform_mouse_action(self, element, action): element, offset = self._unwrap_clickable_element(element) self._manipulate(element, lambda wew: action(wew.unwrap(), offset)) def _unwrap_clickable_element(self, elt): from helium import HTMLElement, Point offset = None if isinstance(elt, str): elt = ClickableText(self.require_driver(), elt) elif isinstance(elt, HTMLElement): elt = elt._impl elif isinstance(elt, Point): elt, offset = self._point_to_element_and_offset(elt) return elt, offset def _point_to_element_and_offset(self, point): driver = self.require_driver() element = WebElementWrapper(driver.execute_script( 'return document.elementFromPoint(%r, %r);' % (point.x, point.y) )) offset = point - (element.location.left, element.location.top) if offset == (0, 0) and driver.is_firefox(): # In some CSS settings (eg. test_point.html), the (0, 0) point of # buttons in Firefox is not clickable! The reason for this is that # Firefox styles buttons to not be perfect squares, but have an # indent in the corners. This workaround makes `click(btn.top_left)` # work even when this happens: offset = (1, 1) return element, offset @handle_unexpected_alert def find_all_impl(self, predicate): while True: try: return [ predicate.with_impl(bound_gui_elt_impl) for bound_gui_elt_impl in predicate._impl.iter_all() ] except FramesChangedWhileIterating: # Try again. pass def scroll_down_impl(self, num_pixels): self._scroll_by(0, num_pixels) def scroll_up_impl(self, num_pixels): self._scroll_by(0, -num_pixels) def scroll_right_impl(self, num_pixels): self._scroll_by(num_pixels, 0) def scroll_left_impl(self, num_pixels): self._scroll_by(-num_pixels, 0) @handle_unexpected_alert def _scroll_by(self, dx_pixels, dy_pixels): self.require_driver().execute_script( 'window.scrollBy(arguments[0], arguments[1]);', dx_pixels, dy_pixels ) @might_spawn_window @handle_unexpected_alert def select_impl(self, combo_box, value): from helium import ComboBox if isinstance(combo_box, str): combo_box = ComboBoxImpl(self.require_driver(), combo_box) elif isinstance(combo_box, ComboBox): combo_box = combo_box._impl def _select(web_element): if isinstance(web_element, WebElementWrapper): web_element = web_element.unwrap() Select(web_element).select_by_visible_text(value) self._manipulate(combo_box, _select) def _manipulate(self, gui_or_web_elt, action): driver = self.require_driver() if hasattr(gui_or_web_elt, 'perform') \ and callable(gui_or_web_elt.perform): driver.last_manipulated_element = gui_or_web_elt.perform(action) else: if isinstance(gui_or_web_elt, WebElement): gui_or_web_elt = WebElementWrapper(gui_or_web_elt) action(gui_or_web_elt) driver.last_manipulated_element = gui_or_web_elt @handle_unexpected_alert def drag_file_impl(self, file_path, to): to, _ = self._unwrap_clickable_element(to) drag_and_drop = DragAndDropFile(self.require_driver(), file_path) drag_and_drop.begin() try: # Some web apps (Gmail in particular) only register for the 'drop' # event when user has dragged the file over the document. We # therefore simulate this dragging over the document first: drag_and_drop.drag_over_document() self._manipulate(to, lambda elt: drag_and_drop.drop_on(elt)) finally: drag_and_drop.end() @might_spawn_window @handle_unexpected_alert def attach_file_impl(self, file_path, to=None): from helium import Point driver = self.require_driver() if to is None: to = FileInput(driver) elif isinstance(to, str): to = FileInput(driver, to) elif isinstance(to, Point): to, _ = self._point_to_element_and_offset(to) self._manipulate(to, lambda elt: elt.send_keys(file_path)) def refresh_impl(self): self._handle_alerts( self._refresh_no_alert, self._refresh_with_alert ) def _refresh_no_alert(self): self.require_driver().refresh() def _refresh_with_alert(self): AlertImpl(self.require_driver()).accept() self._refresh_no_alert() def wait_until_impl(self, condition_fn, timeout_secs=10, interval_secs=0.5): if ismethod(condition_fn): is_bound = condition_fn.__self__ is not None args_spec = getfullargspec(condition_fn).args unfilled_args = len(args_spec) - (1 if is_bound else 0) else: if not isfunction(condition_fn): condition_fn = condition_fn.__call__ args_spec = getfullargspec(condition_fn).args unfilled_args = len(args_spec) def condition(driver): try: return condition_fn(driver) if unfilled_args else condition_fn() except FramesChangedWhileIterating: return False wait = WebDriverWait( self.require_driver().unwrap(), timeout_secs, poll_frequency=interval_secs ) wait.until(condition) @handle_unexpected_alert def switch_to_impl(self, window): driver = self.require_driver() from helium import Window if isinstance(window, str): window = WindowImpl(driver, window) elif isinstance(window, Window): window = window._impl driver.switch_to.window(window.handle) def kill_browser_impl(self): self.require_driver().quit() self.driver = None @handle_unexpected_alert def highlight_impl(self, element): driver = self.require_driver() from helium import HTMLElement, Text if isinstance(element, str): element = Text(element) if isinstance(element, HTMLElement): element = element._impl try: element = element.first_occurrence except AttributeError: pass previous_style = element.get_attribute("style") if isinstance(element, WebElementWrapper): element = element.unwrap() driver.execute_script( "arguments[0].setAttribute(" "'style', 'border: 2px solid red; font-weight: bold;'" ");", element ) driver.execute_script( "var target = arguments[0];" "var previousStyle = arguments[1];" "setTimeout(" "function() {" "target.setAttribute('style', previousStyle);" "}, 2000" ");", element, previous_style ) def require_driver(self): if not self.driver: raise RuntimeError(self.DRIVER_REQUIRED_MESSAGE) return self.driver class DragHelper: def __init__(self, api_impl): self.api_impl = api_impl self.is_html_5_drag = None def __enter__(self): self._execute_script( "window.helium = {};" "window.helium.dragHelper = {" " createEvent: function(type) {" " var event = document.createEvent('CustomEvent');" " event.initCustomEvent(type, true, true, null);" " event.dataTransfer = {" " data: {}," " setData: function(type, val) {" " this.data[type] = val;" " }," " getData: function(type) {" " return this.data[type];" " }" " };" " return event;" " }" "};" ) return self def start_dragging(self, element, offset): if self._attempt_html_5_drag(element): self.is_html_5_drag = True else: self.api_impl._press_mouse_on(element, offset) def drop_on_target(self, target, offset): if self.is_html_5_drag: self._complete_html_5_drag(target) else: self.api_impl._release_mouse_over(target, offset) def _attempt_html_5_drag(self, element_to_drag): return self._execute_script( "var source = arguments[0];" "function getDraggableParent(element) {" " var previousParent = null;" " while (element != null && element != previousParent) {" " previousParent = element;" " if ('draggable' in element) {" " var draggable = element.draggable;" " if (draggable === true)" " return element;" " if (typeof draggable == 'string' " " || draggable instanceof String)" " if (draggable.toLowerCase() == 'true')" " return element;" " }" " element = element.parentNode;" " }" " return null;" "}" "var draggableParent = getDraggableParent(source);" "if (draggableParent == null)" " return false;" "window.helium.dragHelper.draggedElement = draggableParent;" "var dragStart = window.helium.dragHelper.createEvent('dragstart');" "source.dispatchEvent(dragStart);" "window.helium.dragHelper.dataTransfer = dragStart.dataTransfer;" "return true;", element_to_drag ) def _complete_html_5_drag(self, on): self._execute_script( "var target = arguments[0];" "var drop = window.helium.dragHelper.createEvent('drop');" "drop.dataTransfer = window.helium.dragHelper.dataTransfer;" "target.dispatchEvent(drop);" "var dragEnd = window.helium.dragHelper.createEvent('dragend');" "dragEnd.dataTransfer = window.helium.dragHelper.dataTransfer;" "window.helium.dragHelper.draggedElement.dispatchEvent(dragEnd);", on ) def __exit__(self, *_): self._execute_script("delete window.helium;") def _execute_script(self, script, *args): return self.api_impl.require_driver().execute_script(script, *args) class DragAndDropFile: def __init__(self, driver, file_path): self.driver = driver self.file_path = file_path self.file_input_element = None self.dragover_event = None def begin(self): self._create_file_input_element() try: self.file_input_element.send_keys(self.file_path) except: self.end() raise def _create_file_input_element(self): # The input needs to be visible to Selenium to allow sending keys to it # in Firefox and IE. # According to http://stackoverflow.com/questions/6101461/ # Selenium criteria whether an element is visible or not are the # following: # - visibility != hidden # - display != none (is also checked against every parent element) # - opacity != 0 # - height and width are both > 0 # - for an input, the attribute type != hidden # So let's make sure its all good! self.file_input_element = self.driver.execute_script( "var input = document.createElement('input');" "input.type = 'file';" "input.style.display = 'block';" "input.style.opacity = '1';" "input.style.visibility = 'visible';" "input.style.height = '1px';" "input.style.width = '1px';" "if (document.body.childElementCount > 0) { " " document.body.insertBefore(input, document.body.childNodes[0]);" "} else { " " document.body.appendChild(input);" "}" "return input;" ) def drag_over_document(self): # According to the HTML5 spec, we need to dispatch the dragenter event # once, and then the dragover event continuously, every 350+-200ms: # http://www.w3.org/html/wg/drafts/html/master/editing.html#current-drag # -operation # Especially IE implements this spec very tightly, and considers the # dragging to be over if no dragover event occurs for more than ~1sec. # We thus need to ensure that we keep dispatching the dragover event. # This line used to read `_dispatch_event(..., to='document')`. However, # this doesn't work when adding a photo to a tweet on Twitter. # Dispatching the event to document.body fixes this, and also works for # Gmail: self._dispatch_event('dragenter', to='document.body') self.dragover_event = self._prepare_continuous_event( 'dragover', 'document', interval_msecs=300 ) self.dragover_event.start() def _dispatch_event(self, event_name, to): script, args = self._prepare_dispatch_event(event_name, to) self.driver.execute_script(script, *args) def _prepare_continuous_event(self, event_name, to, interval_msecs): script, args = self._prepare_dispatch_event(event_name, to) return JavaScriptInterval(self.driver, script, args, interval_msecs) def _prepare_dispatch_event(self, event_name, to): script = \ "var files = arguments[0].files;" \ "var items = [];" \ "var types = [];" \ "for (var i = 0; i < files.length; i++) {" \ " items[i] = {kind: 'file', type: files[i].type};" \ " types[i] = 'Files';" \ "}" \ "var event = document.createEvent('CustomEvent');" \ "event.initCustomEvent(arguments[1], true, true, 0);" \ "event.dataTransfer = {" \ " files: files," \ " items: items," \ " types: types" \ "};" \ "arguments[2].dispatchEvent(event);" if isinstance(to, str): script = script.replace('arguments[2]', to) args = self.file_input_element, event_name, else: args = self.file_input_element, event_name, to.unwrap() return script, args def drop_on(self, target): self.dragover_event.stop() self._dispatch_event('drop', to=target) def end(self): if self.file_input_element is not None: self.driver.execute_script( "arguments[0].parentNode.removeChild(arguments[0]);", self.file_input_element ) self.file_input_element = None class JavaScriptInterval: def __init__(self, driver, script, args, interval_msecs): self.driver = driver self.script = script self.args = args self.interval_msecs = interval_msecs self._interval_id = None def start(self): setinterval_script = ( "var originalArguments = arguments;" "return setInterval(function() {" " arguments = originalArguments;" " %s" "}, %d);" ) % (self.script, self.interval_msecs) self._interval_id = \ self.driver.execute_script(setinterval_script, *self.args) def stop(self): self.driver.execute_script( "clearInterval(arguments[0]);", self._interval_id ) self._interval_id = None class GUIElementImpl: def __init__(self, driver): self._bound_occurrence = None self._driver = driver def iter_all(self, ignore_frame_changes=False): if self._is_bound(): yield self else: while True: try: for occurrence in self.iter_all_occurrences(): yield self.bound_to_occurrence(occurrence) except FramesChangedWhileIterating: if not ignore_frame_changes: raise break def _is_bound(self): return self._bound_occurrence is not None def iter_all_occurrences(self): raise NotImplementedError() def bound_to_occurrence(self, occurrence): result = copy(self) result._bound_occurrence = occurrence return result def exists(self): try: next(self.iter_all(ignore_frame_changes=True)) except StopIteration: return False else: return True @property def first_occurrence(self): if not self._is_bound(): self._bind_to_first_occurrence() return self._bound_occurrence def _bind_to_first_occurrence(self): self.perform(lambda _: None) # _perform_no_wait(...) below now sets _bound_occurrence. def perform(self, action): from helium import Config end_time = time() + Config.implicit_wait_secs # Try to perform `action` at least once: result = self._perform_no_wait(action) while result is None and time() < end_time: result = self._perform_no_wait(action) if result is not None: return result raise LookupError() def _perform_no_wait(self, action): for bound_gui_elt_impl in self.iter_all(ignore_frame_changes=True): occurrence = bound_gui_elt_impl.first_occurrence try: action(occurrence) except Exception as e: if not self.should_ignore_exception(e): raise else: self._bound_occurrence = occurrence return occurrence def should_ignore_exception(self, exception): if isinstance(exception, ElementNotVisibleException): return True if isinstance(exception, MoveTargetOutOfBoundsException): return True if isinstance(exception, StaleElementReferenceException): return True if isinstance(exception, WebDriverException): msg = exception.msg if 'is not clickable at point' in msg \ and 'Other element would receive the click' in msg: # This can happen when the element has moved. return True return False class HTMLElementImpl(GUIElementImpl): def __init__( self, driver, below=None, to_right_of=None, above=None, to_left_of=None ): super(HTMLElementImpl, self).__init__(driver) self.below = self._unwrap_element(below) self.to_right_of = self._unwrap_element(to_right_of) self.above = self._unwrap_element(above) self.to_left_of = self._unwrap_element(to_left_of) self.matches = PREFIX_IGNORE_CASE() def find_anywhere_in_curr_frame(self): raise NotImplementedError() @property def width(self): return self.first_occurrence.location.width @property def height(self): return self.first_occurrence.location.height @property def x(self): return self.first_occurrence.location.left @property def y(self): return self.first_occurrence.location.top @property def top_left(self): from helium import Point return Point(self.x, self.y) @property def web_element(self): return self.first_occurrence.unwrap() def iter_all_occurrences(self): self._handle_closed_window() self._driver.switch_to.default_content() already_yielded = set() for frame_index in FrameIterator(self._driver): for occurrence in self._find_all_in_curr_frame(): if occurrence.target in already_yielded: # We have seen this element before, but its frame had a # different index. This means that the frames have changed. # Abort: return occurrence.frame_index = frame_index yield occurrence already_yielded.add(occurrence.target) def _handle_closed_window(self): window_handles = self._driver.window_handles try: curr_window_handle = self._driver.current_window_handle except NoSuchWindowException: window_has_been_closed = True else: window_has_been_closed = curr_window_handle not in window_handles if window_has_been_closed: self._driver.switch_to.window(window_handles[0]) def _find_all_in_curr_frame(self): search_regions = self._get_search_regions_in_curr_frame() for occurrence in self.find_anywhere_in_curr_frame(): if not occurrence.is_displayed(): continue if self._is_in_any_search_region(occurrence, search_regions): yield occurrence def _get_search_regions_in_curr_frame(self): result = [] if self.below: result.append([ elt.location.is_above for elt in self._resolve_in_curr_frame(self.below) ]) if self.to_right_of: result.append([ elt.location.is_to_left_of for elt in self._resolve_in_curr_frame(self.to_right_of) ]) if self.above: result.append([ elt.location.is_below for elt in self._resolve_in_curr_frame(self.above) ]) if self.to_left_of: result.append([ elt.location.is_to_right_of for elt in self._resolve_in_curr_frame(self.to_left_of) ]) return result def _resolve_in_curr_frame(self, element): if element._is_bound(): return [element.first_occurrence] return element._find_all_in_curr_frame() def _is_in_any_search_region(self, element, search_regions): for direction in search_regions: found = False for search_region in direction: if search_region(element.location): found = True break if not found: return False return True def _is_enabled(self): """ Useful for subclasses. """ return self.first_occurrence.get_attribute('disabled') is None def _unwrap_element(self, element): if isinstance(element, str): return TextImpl(self._driver, element) from helium import HTMLElement if isinstance(element, HTMLElement): return element._impl return element class SImpl(HTMLElementImpl): def __init__(self, driver, selector, **kwargs): super(SImpl, self).__init__(driver, **kwargs) self.selector = selector def find_anywhere_in_curr_frame(self): wrap = lambda web_elements: list(map(WebElementWrapper, web_elements)) if self.selector.startswith('@'): return wrap(self._driver.find_elements(By.NAME, self.selector[1:])) if self.selector.startswith('//'): return wrap(self._driver.find_elements(By.XPATH, self.selector)) return wrap(self._driver.find_elements(By.CSS_SELECTOR, self.selector)) class HTMLElementIdentifiedByXPath(HTMLElementImpl): def find_anywhere_in_curr_frame(self): x_path = self.get_xpath() return self._sort_search_result( list(map( WebElementWrapper, self._driver.find_elements(By.XPATH, x_path) )) ) def _sort_search_result(self, search_result): keys_to_result_items = [] for web_elt in search_result: try: key = self.get_sort_index(web_elt) except StaleElementReferenceException: pass else: keys_to_result_items.append((key, web_elt)) sort_key = lambda tpl: tpl[0] keys_to_result_items.sort(key=sort_key) result_item = lambda tpl: tpl[1] return list(map(result_item, keys_to_result_items)) def get_xpath(self): raise NotImplementedError() def get_sort_index(self, web_element): return self._driver.get_distance_to_last_manipulated(web_element) + 1 class HTMLElementContainingText(HTMLElementIdentifiedByXPath): def __init__(self, driver, text=None, **kwargs): super(HTMLElementContainingText, self).__init__(driver, **kwargs) self.search_text = text def get_xpath(self): xpath_base = "//" + self.get_xpath_node_selector() + \ predicate(self.matches.xpath('.', self.search_text)) return '%s[not(self::script)][not(.%s)]' % (xpath_base, xpath_base) def get_xpath_node_selector(self): return '*' class TextImpl(HTMLElementContainingText): def __init__(self, driver, text=None, include_free_text=True, **kwargs): super(TextImpl, self).__init__(driver, text, **kwargs) self.include_free_text = include_free_text @property def value(self): return self.first_occurrence.text def get_xpath(self): button_impl = ButtonImpl(self._driver, self.search_text) link_impl = LinkImpl(self._driver, self.search_text) components = [ self._get_search_text_xpath(), button_impl.get_input_button_xpath(), link_impl.get_xpath() ] if self.search_text and self.include_free_text: components.append( FreeText(self._driver, self.search_text).get_xpath() ) return ' | '.join(components) def _get_search_text_xpath(self): if self.search_text: result = super(TextImpl, self).get_xpath() else: no_descendant_with_same_text = \ "not(.//*[normalize-space(.)=normalize-space(self::*)])" result = '//*[text() and %s]' % no_descendant_with_same_text return result + "[not(self::option)]" + \ ("" if self.include_free_text else "[count(*) <= 1]") class FreeText(HTMLElementContainingText): def get_xpath_node_selector(self): return 'text()' def get_xpath(self): return super(FreeText, self).get_xpath() + '/..' class LinkImpl(HTMLElementContainingText): def get_xpath_node_selector(self): return 'a' def get_xpath(self): return super(LinkImpl, self).get_xpath() + ' | ' + \ "//a" + \ predicate(self.matches.xpath('@title', self.search_text)) + \ ' | ' + "//*[@role='link']" + \ predicate(self.matches.xpath('.', self.search_text)) @property def href(self): return self.web_element.get_attribute('href') class ListItemImpl(HTMLElementContainingText): def get_xpath_node_selector(self): return 'li' class ButtonImpl(HTMLElementContainingText): def get_xpath_node_selector(self): return 'button' def is_enabled(self): aria_disabled = self.first_occurrence.get_attribute('aria-disabled') return self._is_enabled() \ and (not aria_disabled or aria_disabled.lower() == 'false') def get_xpath(self): has_aria_label = self.matches.xpath('@aria-label', self.search_text) has_text = self.matches.xpath('.', self.search_text) has_text_or_aria_label = predicate_or(has_aria_label, has_text) return ' | '.join([ super(ButtonImpl, self).get_xpath(), self.get_input_button_xpath(), "//*[@role='button']" + has_text_or_aria_label, "//button" + predicate(has_aria_label) ]) def get_input_button_xpath(self): if self.search_text: has_value = self.matches.xpath('@value', self.search_text) has_label = self.matches.xpath('@label', self.search_text) has_aria_label = self.matches.xpath('@aria-label', self.search_text) has_title = self.matches.xpath('@title', self.search_text) has_text = \ predicate_or(has_value, has_label, has_aria_label, has_title) else: has_text = '' return "//input[@type='submit' or @type='button']" + has_text class ImageImpl(HTMLElementIdentifiedByXPath): def __init__(self, driver, alt, **kwargs): super(ImageImpl, self).__init__(driver, **kwargs) self.alt = alt def get_xpath(self): return "//img" + predicate(self.matches.xpath('@alt', self.alt)) class LabelledElement(HTMLElementImpl): SECONDARY_SEARCH_DIMENSION_PENALTY_FACTOR = 1.5 def __init__(self, driver, label=None, **kwargs): super(LabelledElement, self).__init__(driver, **kwargs) self.label = label def find_anywhere_in_curr_frame(self): if not self.label: result = self._find_elts() else: labels = TextImpl( self._driver, self.label, include_free_text=False ).find_anywhere_in_curr_frame() if labels: result = list(self._filter_elts_belonging_to_labels( self._find_elts(), labels )) else: result = self._find_elts_by_free_text() return sorted(result, key=self._driver.get_distance_to_last_manipulated) def _find_elts(self, xpath=None): if xpath is None: xpath = self.get_xpath() return list(map( WebElementWrapper, self._driver.find_elements(By.XPATH, xpath) )) def _find_elts_by_free_text(self): elt_types = [ xpath.strip().lstrip('/') for xpath in self.get_xpath().split('|') ] labels = '//text()' + predicate(self.matches.xpath('.', self.label)) xpath = ' | '.join( [(labels + '/%s::' + elt_type + '[1]') % ('preceding-sibling' if 'checkbox' in elt_type or 'radio' in elt_type else 'following') for elt_type in elt_types] ) return self._find_elts(xpath) def get_xpath(self): raise NotImplementedError() def get_primary_search_direction(self): return 'to_right_of' def get_secondary_search_direction(self): return 'below' def _filter_elts_belonging_to_labels(self, all_elts, labels): for label, elt in self._get_labels_with_explicit_elts(all_elts, labels): yield elt labels.remove(label) all_elts.remove(elt) labels_to_elts = self._get_related_elts(all_elts, labels) labels_to_elts = self._ensure_at_most_one_label_per_elt(labels_to_elts) self._retain_closest(labels_to_elts) for elts_for_label in list(labels_to_elts.values()): assert len(elts_for_label) <= 1 if elts_for_label: yield next(iter(elts_for_label)) def _get_labels_with_explicit_elts(self, all_elts, labels): for label in labels: try: if label.tag_name == 'label': label_target = label.get_attribute('for') if label_target: for elt in all_elts: elt_id = elt.get_attribute('id') if elt_id.lower() == label_target.lower(): yield label, elt except Exception as e: if not self.should_ignore_exception(e): raise def _get_related_elts(self, all_elts, labels): result = {} for label in labels: for elt in all_elts: try: if self._are_related(elt, label): if label not in result: result[label] = set() result[label].add(elt) except Exception as e: if not self.should_ignore_exception(e): raise return result def _are_related(self, elt, label): if elt.location.intersects(label.location): return True prim_search_dir = self.get_primary_search_direction() sec_search_dir = self.get_secondary_search_direction() return label.location.distance_to(elt.location) <= 150 and ( elt.location.is_in_direction(prim_search_dir, label.location) or elt.location.is_in_direction(sec_search_dir, label.location) ) def _ensure_at_most_one_label_per_elt(self, labels_to_elts): elts_to_labels = inverse(labels_to_elts) self._retain_closest(elts_to_labels) return inverse(elts_to_labels) def _retain_closest(self, pivots_to_elts): for pivot, elts in list(pivots_to_elts.items()): if elts: closest = self._find_closest(pivot, elts) if closest: pivots_to_elts[pivot] = {closest} def _find_closest(self, to_pivot, among_elts): distances = [] for elt in among_elts: try: distance = self._compute_distance(elt, to_pivot) except Exception as e: if not self.should_ignore_exception(e): raise else: distances.append((distance, elt)) if distances: # Provide `key=` to prevent a TypeError that happens when Python # attempts to sort on the second items of the tuples when the first # items are equal. return sorted(distances, key=lambda tpl: tpl[0])[0][1] def _compute_distance(self, elt_1, elt_2): loc_1 = elt_1.location loc_2 = elt_2.location if loc_1.is_in_direction(self.get_secondary_search_direction(), loc_2): factor = self.SECONDARY_SEARCH_DIMENSION_PENALTY_FACTOR else: factor = 1 return factor * loc_1.distance_to(loc_2) class CompositeElement(HTMLElementImpl): def __init__(self, driver, *args, **kwargs): super(CompositeElement, self).__init__(driver, **kwargs) self.args = [driver] + list(args) self.kwargs = kwargs self._first_element = None @property def first_element(self): if self._first_element is None: self._bind_to_first_occurrence() # find_anywhere_in_curr_frame() below now sets _first_element return self._first_element def find_anywhere_in_curr_frame(self): already_yielded = [] for element in self.get_elements(): for bound_gui_elt_impl in element.find_anywhere_in_curr_frame(): if self._first_element is None: self._first_element = element if bound_gui_elt_impl not in already_yielded: yield bound_gui_elt_impl already_yielded.append(bound_gui_elt_impl) def get_elements(self): for element_type in self.get_element_types(): yield element_type(*self.args, **self.kwargs) def get_element_types(self): raise NotImplementedError() class ClickableText(CompositeElement): def get_element_types(self): return [ButtonImpl, TextImpl, ImageImpl] class TextFieldImpl(CompositeElement): def get_element_types(self): return [ StandardTextFieldWithPlaceholder, StandardTextFieldWithLabel, AriaTextFieldWithLabel ] @property def value(self): return self.first_element.value def is_enabled(self): return self.first_element.is_enabled() def is_editable(self): return self.first_element.is_editable() class StandardTextFieldWithLabel(LabelledElement): @property def value(self): return self.first_occurrence.get_attribute('value') or '' def is_enabled(self): return self._is_enabled() def is_editable(self): return self.first_occurrence.get_attribute('readOnly') is None def get_xpath(self): return \ "//input[%s='text' or %s='email' or %s='password' or %s='number' " \ "or %s='date' or %s='time' or %s='tel' or string-length(@type)=0]"\ % ((lower('@type'), ) * 7) + \ " | //textarea | //*[@contenteditable='true']" class AriaTextFieldWithLabel(LabelledElement): @property def value(self): return self.first_occurrence.text def is_enabled(self): return self._is_enabled() def is_editable(self): return self.first_occurrence.get_attribute('readOnly') is None def get_xpath(self): return "//*[@role='textbox']" class StandardTextFieldWithPlaceholder(HTMLElementIdentifiedByXPath): def __init__(self, driver, label, **kwargs): super(StandardTextFieldWithPlaceholder, self).__init__(driver, **kwargs) self.label = label @property def value(self): return self.first_occurrence.get_attribute('value') or '' def is_enabled(self): return self._is_enabled() def is_editable(self): return self.first_occurrence.get_attribute('readOnly') is None def get_xpath(self): return "(%s)%s" % ( StandardTextFieldWithLabel(self.label).get_xpath(), predicate(self.matches.xpath('@placeholder', self.label)) ) class FileInput(LabelledElement): def get_xpath(self): return "//input[@type='file']" class ComboBoxImpl(CompositeElement): def get_element_types(self): return [ComboBoxIdentifiedByDisplayedValue, ComboBoxIdentifiedByLabel] def is_editable(self): return self.first_occurrence.tag_name != 'select' @property def value(self): selected_value = self._select_driver.first_selected_option if selected_value: return selected_value.text return None @property def options(self): return [option.text for option in self._select_driver.options] @property def _select_driver(self): return Select(self.web_element) class ComboBoxIdentifiedByLabel(LabelledElement): def get_xpath(self): return "//select | //input[@list]" class ComboBoxIdentifiedByDisplayedValue(HTMLElementContainingText): def get_xpath_node_selector(self): return 'option' def get_xpath(self): option_xpath = \ super(ComboBoxIdentifiedByDisplayedValue, self).get_xpath() return option_xpath + '/ancestor::select[1]' def find_anywhere_in_curr_frame(self): all_cbs_with_a_matching_value = super( ComboBoxIdentifiedByDisplayedValue, self ).find_anywhere_in_curr_frame() result = [] for cb in all_cbs_with_a_matching_value: for selected_option in Select(cb.unwrap()).all_selected_options: if self.matches.text(selected_option.text, self.search_text): result.append(cb) break return result class CheckBoxImpl(LabelledElement): def is_enabled(self): return self._is_enabled() def is_checked(self): return self.first_occurrence.get_attribute('checked') is not None def get_xpath(self): return "//input[@type='checkbox']" def get_primary_search_direction(self): return 'to_left_of' def get_secondary_search_direction(self): return 'to_right_of' class RadioButtonImpl(LabelledElement): def is_selected(self): return self.first_occurrence.get_attribute('checked') is not None def get_xpath(self): return "//input[@type='radio']" def get_primary_search_direction(self): return 'to_left_of' def get_secondary_search_direction(self): return 'to_right_of' class WindowImpl(GUIElementImpl): def __init__(self, driver, title=None): super(WindowImpl, self).__init__(driver) self.search_title = title def iter_all_occurrences(self): result_scores = [] for handle in self._driver.window_handles: window = WindowImpl.SeleniumWindow(self._driver, handle) if self.search_title is None: result_scores.append((0, window)) else: title = window.title if title.startswith(self.search_title): score = len(title) - len(self.search_title) result_scores.append((score, window)) score = lambda tpl: tpl[0] result_scores.sort(key=score) for score, window in result_scores: yield window @property def title(self): return self.first_occurrence.title @property def handle(self): return self.first_occurrence.handle class SeleniumWindow: def __init__(self, driver, handle): self.driver = driver self.handle = handle self._window_handle_before = None @property def title(self): with self: return self.driver.title def __enter__(self): try: self._window_handle_before = self.driver.current_window_handle except NoSuchWindowException as window_closed: do_switch = True else: do_switch = self._window_handle_before != self.handle if do_switch: self.driver.switch_to.window(self.handle) def __exit__(self, *_): if self._window_handle_before and \ self.driver.current_window_handle != self._window_handle_before: self.driver.switch_to.window(self._window_handle_before) class AlertImpl(GUIElementImpl): def __init__(self, driver, search_text=None): super(AlertImpl, self).__init__(driver) self.search_text = search_text def iter_all_occurrences(self): try: result = self._driver.switch_to.alert text = result.text if self.search_text is None or text.startswith(self.search_text): yield result except NoAlertPresentException: pass @property def text(self): return self.first_occurrence.text def accept(self): first_occurrence = self.first_occurrence try: first_occurrence.accept() except WebDriverException as e: # Attempt to work around Selenium issue 3544: # https://code.google.com/p/selenium/issues/detail?id=3544 msg = e.msg if msg and re.match( r"a\.document\.getElementsByTagName\([^\)]*\)\[0\] is " r"undefined", msg ): sleep(0.25) first_occurrence.accept() else: raise def dismiss(self): self.first_occurrence.dismiss() def _write(self, text): self.first_occurrence.send_keys(text) ================================================ FILE: helium/_impl/match_type.py ================================================ from helium._impl.util.xpath import lower, replace_nbsp class MatchType: def xpath(self, value, text): raise NotImplementedError() def text(self, value, text): raise NotImplementedError() class PREFIX_IGNORE_CASE(MatchType): def xpath(self, value, text): if not text: return '' # Asterisks '*' are sometimes used to mark required fields. Eg.: # # The starts-with filter below would be too strict to include such # matches. To get around this, we ignore asterisks unless the searched # text itself contains one. if '*' in text: strip_asterisks = value else: strip_asterisks = "translate(%s, '*', '')" % value # if text contains apostrophes (single quotes) then they need to be # treated with care if "'" in text: text = "concat('%s')" % ("',\"'\",'".join(text.split("'"))) else: text = "'%s'" % text return "starts-with(normalize-space(%s), %s)" % ( lower(replace_nbsp(strip_asterisks)), text.lower() ) def text(self, value, text): if not text: return True return value.lower().lstrip().startswith(text.lower()) ================================================ FILE: helium/_impl/selenium_wrappers.py ================================================ from helium._impl.util.geom import Rectangle from selenium.common.exceptions import StaleElementReferenceException, \ NoSuchFrameException, WebDriverException, NoSuchElementException from selenium.webdriver.common.action_chains import ActionChains from urllib.error import URLError import sys class Wrapper: def __init__(self, target): self.target = target def __getattr__(self, item): return getattr(self.target, item) def unwrap(self): return self.target def __hash__(self): return hash(self.target) def __eq__(self, other): return self.target == other.target def __ne__(self, other): return not self == other class WebDriverWrapper(Wrapper): def __init__(self, target): super(WebDriverWrapper, self).__init__(target) self.last_manipulated_element = None def action(self): return ActionChains(self.target) def get_distance_to_last_manipulated(self, web_element): if not self.last_manipulated_element: return 0 try: if hasattr(self.last_manipulated_element, 'location'): last_location = self.last_manipulated_element.location return last_location.distance_to(web_element.location) except StaleElementReferenceException: return 0 else: # No .location. This happens when last_manipulated_element is an # Alert or a Window. return 0 def is_firefox(self): return self.browser_name == 'firefox' @property def browser_name(self): return self.target.capabilities['browserName'] def is_ie(self): return self.browser_name == 'internet explorer' def _translate_url_errors_caused_by_server_shutdown(f): def f_decorated(*args, **kwargs): try: return f(*args, **kwargs) except URLError as url_error: if _is_caused_by_server_shutdown(url_error): raise StaleElementReferenceException( 'The Selenium server this element belonged to is no longer ' 'available.' ) else: raise return f_decorated def _is_caused_by_server_shutdown(url_error): try: CONNECTION_REFUSED = 10061 return url_error.args[0][0] == CONNECTION_REFUSED except (IndexError, TypeError): return False def handle_element_being_in_other_frame(f): def f_decorated(self, *args, **kwargs): if not self.frame_index: return f(self, *args, **kwargs) try: return f(self, *args, **kwargs) except (StaleElementReferenceException, NoSuchElementException) \ as original_exc: try: frame_iterator = FrameIterator(self.target.parent) frame_iterator.switch_to_frame(self.frame_index) except NoSuchFrameException: raise original_exc else: return f(self, *args, **kwargs) return f_decorated class WebElementWrapper: def __init__(self, target, frame_index=None): self.target = target self.frame_index = frame_index self._cached_location = None @property @handle_element_being_in_other_frame @_translate_url_errors_caused_by_server_shutdown def location(self): if self._cached_location is None: # Cache access to web_element.location as it's expensive: location = self.target.location x, y = location['x'], location['y'] # Cache access to web_element.size as it's expensive: size = self.target.size width, height = size['width'], size['height'] self._cached_location = Rectangle(x, y, width, height) return self._cached_location def is_displayed(self): try: return self.target.is_displayed() and self.location.intersects( Rectangle(0, 0, sys.maxsize, sys.maxsize) ) except StaleElementReferenceException: return False @handle_element_being_in_other_frame def get_attribute(self, attr_name): return self.target.get_attribute(attr_name) @property @handle_element_being_in_other_frame def text(self): return self.target.text @handle_element_being_in_other_frame def clear(self): self.target.clear() @handle_element_being_in_other_frame def send_keys(self, keys): self.target.send_keys(keys) @property @handle_element_being_in_other_frame def tag_name(self): return self.target.tag_name def unwrap(self): return self.target def __repr__(self): return '<%s>%s' % (self.tag_name, self.target.text, self.tag_name) class FrameIterator: def __init__(self, driver, start_frame=None): if start_frame is None: start_frame = [] self.driver = driver self.start_frame = start_frame def __iter__(self): yield [] for new_frame in range(sys.maxsize): try: self.driver.switch_to.frame(new_frame) except WebDriverException: break else: new_start_frame = self.start_frame + [new_frame] for result in FrameIterator(self.driver, new_start_frame): yield [new_frame] + result try: self.switch_to_frame(self.start_frame) except NoSuchFrameException: raise FramesChangedWhileIterating() def switch_to_frame(self, frame_index_path): self.driver.switch_to.default_content() for frame_index in frame_index_path: self.driver.switch_to.frame(frame_index) class FramesChangedWhileIterating(Exception): pass ================================================ FILE: helium/_impl/util/__init__.py ================================================ ================================================ FILE: helium/_impl/util/dictionary.py ================================================ def inverse(dictionary): """ {a: {b}} -> {b: {a}} """ result = {} for key, values in dictionary.items(): for value in values: if value not in result: result[value] = set() result[value].add(key) return result ================================================ FILE: helium/_impl/util/geom.py ================================================ from collections import namedtuple from math import sqrt class Rectangle: def __init__(self, left=0, top=0, width=0, height=0): self.left = left self.top = top self.right = left + width self.bottom = top + height @classmethod def from_w_h(cls, width, height): return cls(0, 0, width, height) @classmethod def from_tuple_l_t_w_h(cls, l_t_w_h=None): if l_t_w_h is None: l_t_w_h = (0, 0, 0, 0) return cls(*l_t_w_h) @classmethod def from_tuple_w_h(cls, w_h): return cls.from_w_h(*w_h) @classmethod def from_struct_l_t_r_b(cls, struct): return cls.from_l_t_r_b( struct.left, struct.top, struct.right, struct.bottom ) @classmethod def from_l_t_r_b(cls, left, top, right, bottom): return cls(left, top, right - left, bottom - top) @property def width(self): return self.right - self.left @property def height(self): return self.bottom - self.top @property def center(self): return Point(self.left + self.width / 2, self.top + self.height / 2) @property def east(self): return self.clip(Point(self.right - 1, self.center.y)) @property def west(self): return Point(self.left, self.center.y) @property def north(self): return Point(self.center.x, self.top) @property def south(self): return self.clip(Point(self.center.x, self.bottom - 1)) @property def northeast(self): return Point(self.east.x, self.north.y) @property def southeast(self): return Point(self.east.x, self.south.y) @property def southwest(self): return Point(self.west.x, self.south.y) @property def northwest(self): return Point(self.west.x, self.north.y) @property def area(self): if not self: return 0 return self.width * self.height def __contains__(self, point): return self.left <= point.x < self.right and \ self.top <= point.y < self.bottom def translate(self, dx, dy): self.left += dx self.right += dx self.top += dy self.bottom += dy return self def clip(self, point): return Point( min(max(point[0], self.left), max(self.left, self.right - 1)), min(max(point[1], self.top), max(self.top, self.bottom - 1)) ) def intersect(self, rectangle): left = max(self.left, rectangle.left) top = max(self.top, rectangle.top) right = min(self.right, rectangle.right) bottom = min(self.bottom, rectangle.bottom) return self.from_l_t_r_b(left, top, right, bottom) or Rectangle() def intersects(self, rectangle): return bool(self.intersect(rectangle)) def as_numpy_slice(self): return slice(self.top, self.bottom), slice(self.left, self.right) def is_to_left_of(self, other): self_starts_to_left_of_other = self.left < other.left self_overlaps_other_top = self.top <= other.top < self.bottom other_overlaps_self_top = other.top <= self.top < other.bottom return self_starts_to_left_of_other and ( self_overlaps_other_top or other_overlaps_self_top ) def is_to_right_of(self, other): return other.is_to_left_of(self) def is_above(self, other): self_starts_above_other = self.top < other.top self_overlaps_other_left = self.left <= other.left < self.right other_overlaps_self_left = other.left <= self.left < other.right return self_starts_above_other and ( self_overlaps_other_left or other_overlaps_self_left ) def is_below(self, other): return other.is_above(self) def is_in_direction(self, in_direction, of_other): return getattr(self, 'is_' + in_direction)(of_other) def distance_to(self, other): leftmost = self if self.left < other.left else other rightmost = self if leftmost == other else other distance_x = max(0, rightmost.left - leftmost.right) topmost = self if self.top < other.top else other bottommost = self if topmost == other else other distance_y = max(0, bottommost.top - topmost.bottom) return sqrt(distance_x ** 2 + distance_y ** 2) def __eq__(self, other): if not isinstance(other, Rectangle): return False return self.left == other.left and self.top == other.top and \ self.right == other.right and self.bottom == other.bottom def __ne__(self, other): return not self.__eq__(other) def __bool__(self): return bool(self.width > 0 and self.height > 0) def __repr__(self): return type(self).__name__ + '(left=%d, top=%d, width=%d, height=%d)' \ % (self.left, self.top, self.width, self.height) def __hash__(self): return self.left + 7 * self.top + 11 * self.right + 13 * self.bottom class Point(namedtuple('Point', ['x', 'y'])): def __new__(cls, x=0, y=0): return cls.__bases__[0].__new__(cls, x, y) def __init__(self, x=0, y=0): # tuple is immutable so can't do anything here. The initialization # happens in __new__(...) above. pass @classmethod def from_tuple(cls, tpl): return cls(*tpl) def __eq__(self, other): return (self.x, self.y) == other def __ne__(self, other): return not self == other def __add__(self, other): dx, dy = other return Point(self.x + dx, self.y + dy) def __radd__(self, other): return self.__add__(other) def __sub__(self, other): dx, dy = other return Point(self.x - dx, self.y - dy) def __rsub__(self, other): x, y = other dx, dy = self return Point(x - dx, y - dy) def __mul__(self, scalar): if isinstance(scalar, (int, float)): return Point(self.x * scalar, self.y * scalar) else: raise ValueError("Invalid argument") def __rmul__(self, scalar): return self.__mul__(scalar) def __div__(self, scalar): if isinstance(scalar, (int, float)): return Point(self.x / scalar, self.y / scalar) else: raise ValueError("Invalid argument") def __bool__(self): return bool(self.x) or bool(self.y) class Direction: def __init__(self, unit_vector): self.unit_vector = unit_vector def iterate_points_starting_at(self, point, offsets): for offset in offsets: yield point + offset * self.unit_vector def is_horizontal(self): return bool(self.unit_vector.x) def is_vertical(self): return not self.is_horizontal() @property def orthog_vector(self): return Point(-self.unit_vector[1], self.unit_vector[0]) def __eq__(self, other): return self.unit_vector == other.unit_vector def __repr__(self): for module_element in dir(self.__module__): if self == getattr(self.__module__, module_element): return module_element NORTH = Direction(Point(0, -1)) EAST = Direction(Point(1, 0)) SOUTH = Direction(Point(0, 1)) WEST = Direction(Point(-1, 0)) ================================================ FILE: helium/_impl/util/html.py ================================================ from html.parser import HTMLParser import re def strip_tags(html): s = TagStripper() s.feed(html) return s.get_data() class TagStripper(HTMLParser): def __init__(self): HTMLParser.__init__(self) self.reset() self.fed = [] def handle_data(self, d): self.fed.append(d) def get_data(self): return ''.join(self.fed) def get_easily_readable_snippet(html): html = normalize_whitespace(html) try: inner_start = html.index('>') + 1 inner_end = html.rindex('<', inner_start) except ValueError: return html opening_tag = html[:inner_start] closing_tag = html[inner_end:] inner = html[inner_start:inner_end] if '<' in inner or len(inner) > 60: return '%s...%s' % (opening_tag, closing_tag) else: return html def normalize_whitespace(html): result = html.strip() # Remove multiple spaces: result = re.sub(r'\s+', ' ', result) # Remove spaces after opening or before closing tags: result = result.replace('> ', '>').replace(' <', '<') return result ================================================ FILE: helium/_impl/util/inspect_.py ================================================ from helium._impl.util.lang import isbound import inspect def repr_args(f, args=None, kwargs=None, repr_fn=repr): if args is None: args = [] if kwargs is None: kwargs = {} arg_names, _, _, defaults = inspect.getfullargspec(f)[:4] if isbound(f): # Skip 'self' parameter: arg_names = arg_names[1:] num_defaults = 0 if defaults is None else len(defaults) num_requireds = len(arg_names) - num_defaults result = [] for i, arg_name in enumerate(arg_names): has_default = i >= len(arg_names) - num_defaults if has_default: default_value = defaults[i - num_requireds] if i < len(args): # Normal arg value = args[i] prefix = '' value_is_default = has_default and value == default_value elif arg_name in kwargs: # Keyword arg value = kwargs[arg_name] prefix = arg_name + '=' value_is_default = has_default and value == default_value else: # Optional arg without given value value_is_default = True if not value_is_default: result.append(prefix + repr_fn(value)) for vararg in args[len(arg_names):]: result.append(repr_fn(vararg)) for kwarg in kwargs: if kwarg not in arg_names: result.append(kwarg + '=' + repr_fn(kwargs[kwarg])) return ', '.join(result) ================================================ FILE: helium/_impl/util/lang.py ================================================ class TemporaryAttrValue: def __init__(self, obj, attr, value): self.obj = obj self.attr = attr self.value = value self.value_before = None def __enter__(self): self.value_before = getattr(self.obj, self.attr) setattr(self.obj, self.attr, self.value) def __exit__(self, *_): setattr(self.obj, self.attr, self.value_before) self.value_before = None def isbound(method_or_fn): try: return method_or_fn.__self__ is not None except AttributeError: # Python 3 try: return method_or_fn.__self__ is not None except AttributeError: return False ================================================ FILE: helium/_impl/util/path.py ================================================ from errno import EEXIST from os.path import split, isdir from os import makedirs def get_components(path): folders = [] while True: path, folder = split(path) if folder != "": folders.append(folder) else: if path != "": folders.append(path) break return list(reversed(folders)) def ensure_exists(path): """http://stackoverflow.com/a/600612/190597 (tzot)""" try: makedirs(path, exist_ok=True) # Python>3.2 except TypeError: try: makedirs(path) except OSError as exc: # Python >2.5 if exc.errno == EEXIST and isdir(path): pass else: raise ================================================ FILE: helium/_impl/util/system.py ================================================ """ Gives information about the current operating system. """ import sys def is_windows(): return sys.platform in ('win32', 'cygwin') def is_mac(): return sys.platform == 'darwin' def is_linux(): return sys.platform.startswith('linux') def get_canonical_os_name(): if is_windows(): return 'windows' elif is_mac(): return 'mac' elif is_linux(): return 'linux' ================================================ FILE: helium/_impl/util/xpath.py ================================================ # -*- coding: utf-8 -*- def lower(text): alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝ' return "translate(%s, '%s', '%s')" % (text, alphabet, alphabet.lower()) def replace_nbsp(text, by=' '): return "translate(%s, '\u00a0', %r)" % (text, by) def predicate(condition): return '[%s]' % condition if condition else '' def predicate_or(*conditions): return predicate(' or '.join([c for c in conditions if c])) ================================================ FILE: requirements/base.txt ================================================ # Also update setup.py when you edit this file. selenium>=4.29.0 ================================================ FILE: requirements/docs.txt ================================================ -r base.txt sphinx-rtd-theme==3.0.2 sphinx==8.2.3 ================================================ FILE: requirements/test.txt ================================================ -r base.txt setuptools<60 psutil pywin32; platform_system=='Windows' ================================================ FILE: setup.py ================================================ from setuptools import setup, find_packages setup( name = 'helium', # Also update docs/conf.py when you change this: version = '7.0.0', author = 'Michael Herrmann', author_email = 'michael+removethisifyouarehuman@herrmann.io', description = 'Lighter browser automation based on Selenium.', keywords = 'helium selenium browser automation', url = 'https://github.com/mherrmann/helium', python_requires='>=3', packages = find_packages(exclude=['tests', 'tests.*']), install_requires = [ # Also update requirements/base.txt when you make changes here. 'selenium>=4.16.0' ], package_data = { 'helium._impl': ['webdrivers/**/*'] }, zip_safe = False, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Topic :: Software Development :: Testing', 'Topic :: Software Development :: Libraries', 'Programming Language :: Python', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX :: Linux', 'Operating System :: MacOS :: MacOS X' ], test_suite='tests' ) ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/api/__init__.py ================================================ from helium import start_chrome, start_firefox, go_to, set_driver, \ kill_browser from selenium.webdriver import ChromeOptions from selenium.webdriver.common.by import By from tests.api.util import get_data_file_url from time import time, sleep from unittest import TestCase import os def test_browser_name(): try: browser_name = os.environ['TEST_BROWSER'] except KeyError: return 'chrome' else: return browser_name class BrowserAT(TestCase): @classmethod def setUpClass(cls): if _TEST_BROWSER is None: cls.driver = start_browser() cls.started_browser = True else: cls.driver = _TEST_BROWSER cls.started_browser = False set_driver(cls.driver) def setUp(self): go_to(self.get_url()) def get_url(self): return get_data_file_url(self.get_page()) def get_page(self): raise NotImplementedError() def read_result_from_browser(self, timeout_secs=3): start_time = time() while time() < start_time + timeout_secs: result = self.driver\ .find_element(By.ID, 'result').get_attribute('innerHTML') if result: return result sleep(0.2) return '' def assertFindsEltWithId(self, predicate, id_): self.assertEqual(id_, predicate.web_element.get_attribute('id')) @classmethod def tearDownClass(cls): if cls.started_browser: kill_browser() _TEST_BROWSER = None def setUpModule(): global _TEST_BROWSER _TEST_BROWSER = start_browser() def tearDownModule(): global _TEST_BROWSER if _TEST_BROWSER is not None: kill_browser() _TEST_BROWSER = None def start_browser(url=None): browser_name = test_browser_name() kwargs = {} if browser_name in ('chrome', 'firefox'): kwargs['headless'] = True if browser_name == 'chrome': options = ChromeOptions() # Fix the locale for inputting dates and times: options.add_argument('lang=de-DE') kwargs['options'] = options return _TEST_BROWSERS[browser_name](url, **kwargs) _TEST_BROWSERS = { 'firefox': start_firefox, 'chrome': start_chrome } ================================================ FILE: tests/api/data/default.css ================================================ #result { clear: both; } ================================================ FILE: tests/api/data/js/jquery.ui-contextmenu.js ================================================ /******************************************************************************* * jquery.ui-contextmenu.js plugin. * * jQuery plugin that provides a context menu (based on the jQueryUI menu widget). * * @see https://github.com/mar10/jquery-ui-contextmenu * * Copyright (c) 2013, Martin Wendt (http://wwWendt.de). Licensed MIT. */ ;(function($, window, document, undefined) { "use strict"; var supportSelectstart = "onselectstart" in document.createElement("div"); /** Return command without leading '#' (default to ""). */ function normCommand(cmd){ return (cmd && cmd.match(/^#/)) ? cmd.substring(1) : (cmd || ""); } $.widget("moogle.contextmenu", { version: "1.2.2", options: { delegate: null, // selector hide: { effect: "fadeOut", duration: "fast"}, ignoreParentSelect: true, // Don't trigger 'select' for sub-menu parents menu: null, // selector or jQuery pointing to
      , or a definition hash position: null, // popup positon preventSelect: false, // disable text selection of target show: { effect: "slideDown", duration: "fast"}, taphold: false, // open menu on taphold events (requires external plugins) // Events: beforeOpen: $.noop, // menu about to open; return `false` to prevent opening blur: $.noop, // menu option lost focus close: $.noop, // menu was closed create: $.noop, // menu was initialized createMenu: $.noop, // menu was initialized (original UI Menu) focus: $.noop, // menu option got focus open: $.noop, // menu was opened select: $.noop // menu option was selected; return `false` to prevent closing }, /** Constructor */ _create: function () { var eventNames, targetId, opts = this.options; this.$headStyle = null; this.$menu = null; this.menuIsTemp = false; this.currentTarget = null; if(opts.preventSelect){ // Create a global style for all potential menu targets // If the contextmenu was bound to `document`, we apply the // selector relative to the tag instead targetId = ($(this.element).is(document) ? $("body") : this.element).uniqueId().attr("id"); this.$headStyle = $("
      Textbox:
      Textbox value
      ================================================ FILE: tests/api/data/test_click.html ================================================ test_click Click me!

      ================================================ FILE: tests/api/data/test_doubleclick.html ================================================ test_doubleclick

      Doubleclick here.

      ================================================ FILE: tests/api/data/test_drag/default.html ================================================

      Drag me.

      ================================================ FILE: tests/api/data/test_drag/html5.html ================================================

      Drag me.

      ================================================ FILE: tests/api/data/test_drag/test_drag.css ================================================ .dropTarget { float: left; width: 100px; height: 35px; margin: 10px; padding: 10px; border: 1px solid #aaaaaa; text-align: center; } .dropTarget p { margin-top: 10px; } ================================================ FILE: tests/api/data/test_file_upload/test_file_upload.html ================================================ test_file_upload
      Drop the file here!

      ================================================ FILE: tests/api/data/test_gui_elements.html ================================================ Test page for browser system tests
      Empty Text Field: Another Text Field:
      Example Text Field: CheckBox
      Disabled Text Field: LHS CheckBox
      ReadOnly Text Field: Language:
      Język polski: Language:
      Deutsch:
        Column 1 Column 2
      Drop Down List:
      Row 1:
      Editable ComboBox:
      Row 2:
      Right Labeled CheckBox Left Labeled CheckBox
      Ticked CheckBox Disabled CheckBox
      RadioButton 1 Left Labeled RadioButton 1 Text with id Link with empty href heliumhq.com
      RadioButton 2 Left Labeled RadioButton 2 Input type=Text:
       Text with leading &nbsp;
      DIV with role=button
      Span with role=link Dolphin
      Input type=tel:
      contenteditable Paragraph:

      TextField in iframe:
      Your email's been sent!
      Single'quote. Double"quote.
      He said "double quotes".
      VERÖFFENTLICHEN
      Input type=date: Input type=time:
      • ListItem 1
      • ListItem 2

      EUR/USD

      1.3487


      Free text not surrounded by tags
      Text field labelled by free text:

      Checkboxes

      unchecked
      checked

      Radio buttons

      male
      female

      ================================================ FILE: tests/api/data/test_gui_elements_iframe.html ================================================ ================================================ FILE: tests/api/data/test_hover.html ================================================ test_hover

      ================================================ FILE: tests/api/data/test_iframe/iframe.html ================================================ test_iframe - iframe This text is inside an iframe. ================================================ FILE: tests/api/data/test_iframe/main.html ================================================ test_iframe - main ================================================ FILE: tests/api/data/test_iframe/nested_iframe.html ================================================ test_iframe - nested iframe This text is inside a nested iframe. ================================================ FILE: tests/api/data/test_implicit_wait.html ================================================ test_auto_wait ================================================ FILE: tests/api/data/test_leaked_password.html ================================================ Leaked Password Test
      Username:
      Password:
      ================================================ FILE: tests/api/data/test_point.html ================================================ test_point

      ================================================ FILE: tests/api/data/test_rightclick.html ================================================ test_rightclick

      Perform a normal rightclick here.

      Rightclick here for context menu.

      ================================================ FILE: tests/api/data/test_scroll.html ================================================
      This is a 5000 x 5000 pixel div
      ================================================ FILE: tests/api/data/test_start_go_to.html ================================================ test_start_go_to ================================================ FILE: tests/api/data/test_tables.html ================================================

      Table no. 1

      T1H1 T1H2 T1H3
      T1R1C1 T1R1C2 T1R1C3
      T1R2C1 T1R2C2 T1R2C3
      T1R3C1 T1R3C2 T1R3C3

      Table no. 2

      T2H1 T2H2 T2H3
      T2R1C1 T2R1C2 T2R1C3
      T2R2C1 T2R2C2 T2R2C3
      T2R3C1 T2R3C2 T2R3C3

      Table no. 3

      T3H1 T3H2 T3H3
      T3R1C1 T3R1C2 T3R1C3
      T3R2C1 T3R2C2 T3R2C3
      T3R3C1 T3R3C2 T3R3C3

      User email addresses

      Name Email Country
      John email1@domain.com USA
      Abdul email2@domain.com Yemen
      Chang email3@domain.com China
      ================================================ FILE: tests/api/data/test_text_impl.html ================================================

      A paragraph

      A paragraph inside a div

      Another paragraph inside the div

      ================================================ FILE: tests/api/data/test_wait_until.html ================================================ test_wait_until ================================================ FILE: tests/api/data/test_window/popup.html ================================================ test_window - popup ================================================ FILE: tests/api/data/test_window/test_window.html ================================================ test_window Click here to open a popup. ================================================ FILE: tests/api/data/test_window_handling/main.html ================================================ test_window_handling - Main Text field:
      Open popup ================================================ FILE: tests/api/data/test_window_handling/main_immediate_popup.html ================================================ test_window_handling - Main (immediate popup) ================================================ FILE: tests/api/data/test_window_handling/popup.html ================================================ test_window_handling - Popup In popup.
      Text field: ================================================ FILE: tests/api/data/test_write.html ================================================ test_write
      Autofocus text field:
      Normal text field:
      Input type=date:
      Input type=time:
      ================================================ FILE: tests/api/test_alert.py ================================================ from helium import click, Alert, press, ENTER, write, TextField, Config, \ wait_until from helium._impl.util.lang import TemporaryAttrValue from helium._impl.util.system import is_mac from tests.api import BrowserAT, test_browser_name from selenium.common.exceptions import UnexpectedAlertPresentException from time import time, sleep from unittest import skipIf import selenium class AlertAT(): UNEXPECTED_ALERT_PRESENT_EXCEPTION_MSG = \ "This command is not supported when an alert is present. To accept " \ "the alert (this usually corresponds to clicking 'OK') use `Alert()." \ "accept()`. To dismiss the alert (ie. 'cancel' it), use `Alert()." \ "dismiss()`. If the alert contains a text field, you can use " \ "write(...) to set its value. Eg.: `write('hi there!')`." def get_page(self): return 'test_alert.html' def get_link_to_open_alert(self): raise NotImplementedError() def get_expected_alert_text(self): raise NotImplementedError() def get_expected_alert_accepted_result(self): raise NotImplementedError() def get_expected_alert_dismissed_result(self): return self.get_expected_alert_accepted_result() def setUp(self): super(AlertAT, self).setUp() click(self.get_link_to_open_alert()) wait_until(Alert().exists) def tearDown(self): if Alert().exists(): # We need to call .accept() instead of .dismiss() here to work # around ChromeDriver bug 764: # https://code.google.com/p/chromedriver/issues/detail?id=764 Alert().accept() super(AlertAT, self).tearDown() def test_alert_exists(self): self.assertTrue(Alert().exists()) def test_alert_text_exists(self): self.assertTrue(Alert(self.get_expected_alert_text()).exists()) def test_alert_text_not_exists(self): self.assertFalse(Alert('Wrong text').exists()) def test_alert_text(self): self.assertEqual(self.get_expected_alert_text(), Alert().text) def test_alert_accept(self): Alert().accept() self._expect_result(self.get_expected_alert_accepted_result()) @skipIf( is_mac() and test_browser_name() == 'chrome', "Chrome driver on OSX does not support dismissing JS alerts. " + "See: https://code.google.com/p/chromedriver/issues/detail?id=764" ) def test_alert_dismiss(self): Alert().dismiss() self._expect_result(self.get_expected_alert_dismissed_result()) def test_click_with_open_alert_raises_exception(self): with self.assertRaises(UnexpectedAlertPresentException) as cm: click("OK") msg = self._get_unhandled_alert_exception_msg(cm.exception) self.assertEqual( self.UNEXPECTED_ALERT_PRESENT_EXCEPTION_MSG, msg ) def test_press_with_open_alert_raises_exception(self): with self.assertRaises(UnexpectedAlertPresentException) as cm: press(ENTER) msg = self._get_unhandled_alert_exception_msg(cm.exception) self.assertEqual( self.UNEXPECTED_ALERT_PRESENT_EXCEPTION_MSG, msg ) """ This method waits up to one second for the given result to appear. It should not be needed but Chrome sometimes returns from .accept()/.dismiss() before the JavaScript in test_alert.html has set the corresponding result. """ def _expect_result(self, expected_result, timeout_secs=1): start_time = time() while time() < start_time + timeout_secs: actual_result = self.read_result_from_browser(timeout_secs=.3) if actual_result == expected_result: return sleep(0.2) self.assertEqual(expected_result, actual_result) def _get_unhandled_alert_exception_msg(self, e): if selenium.__version__ == '2.43.0': # Selenium 2.43.0 has a regression where accessing the .msg field # of an UnexpectedAlertPresentException raises an AttributeError - # See: https://code.google.com/p/selenium/issues/detail?id=7886 return e.args[0] else: return e.msg class AlertTest(AlertAT, BrowserAT): def get_link_to_open_alert(self): return 'Display alert' def get_expected_alert_text(self): return 'Hello World!' def get_expected_alert_accepted_result(self): return 'Alert displayed' class ConfirmationDialogTest(AlertAT, BrowserAT): def get_link_to_open_alert(self): return 'Ask for confirmation' def get_expected_alert_text(self): return 'Proceed?' def get_expected_alert_accepted_result(self): return 'Accepted' def get_expected_alert_dismissed_result(self): return 'Dismissed' class PromptTest(AlertAT, BrowserAT): def get_link_to_open_alert(self): return 'Prompt for value' def get_expected_alert_text(self): return 'Please enter a value' def get_expected_alert_accepted_result(self): return 'Value entered: ' def test_write_value(self): write("1") Alert().accept() self._expect_result('Value entered: 1') def test_write_into_label_raises_exception(self): with self.assertRaises(UnexpectedAlertPresentException) as cm: write("3", into="Please enter a value") msg = self._get_unhandled_alert_exception_msg(cm.exception) self.assertEqual( self.UNEXPECTED_ALERT_PRESENT_EXCEPTION_MSG, msg ) def test_write_into_text_field_raises_exception(self): with self.assertRaises(UnexpectedAlertPresentException) as cm: write("4", into=TextField("Please enter a value")) msg = self._get_unhandled_alert_exception_msg(cm.exception) self.assertEqual( self.UNEXPECTED_ALERT_PRESENT_EXCEPTION_MSG, msg ) def test_write_into_non_existent_label_raises_exception(self): with self.assertRaises(UnexpectedAlertPresentException) as cm: write("5", into="Please enter a value") msg = self._get_unhandled_alert_exception_msg(cm.exception) self.assertEqual( self.UNEXPECTED_ALERT_PRESENT_EXCEPTION_MSG, msg ) def test_write_into_alert(self): write("7", into=Alert()) Alert().accept() self._expect_result("Value entered: 7") def test_write_into_labelled_alert(self): write("8", into=Alert(self.get_expected_alert_text())) Alert().accept() self._expect_result("Value entered: 8") def test_write_into_non_existent_alert(self): with TemporaryAttrValue(Config, 'implicit_wait_secs', 1): with self.assertRaises(LookupError): write("8", into=Alert("Non-existent")) ================================================ FILE: tests/api/test_aria.py ================================================ from helium import Button, TextField from tests.api import BrowserAT class AriaTest(BrowserAT): def get_page(self): return 'test_aria.html' def test_aria_label_button_exists(self): self.assertTrue(Button("Close").exists()) def test_aria_label_button_is_enabled(self): self.assertTrue(Button("Close").is_enabled()) def test_aria_label_disabled_button_is_enabled(self): self.assertFalse(Button("Disabled Close").is_enabled()) def test_aria_label_non_existent_button(self): self.assertFalse(Button("This doesnt exist").exists()) def test_aria_label_div_button_exists(self): self.assertTrue(Button("Attach files").exists()) def test_aria_label_div_button_is_enabled(self): self.assertTrue(Button("Attach files").is_enabled()) def test_aria_label_div_disabled_button_is_enabled(self): self.assertFalse(Button("Disabled Attach files").is_enabled()) def test_aria_label_submit_button_exists(self): self.assertTrue(Button("Submit").exists()) def test_aria_textbox_exists(self): self.assertTrue(TextField("Textbox").exists()) def test_aria_textbox_value(self): self.assertEqual("Textbox value", TextField("Textbox").value) ================================================ FILE: tests/api/test_chrome_options.py ================================================ from helium import start_chrome, kill_browser from os.path import join from tests.api import test_browser_name from unittest import TestCase, skipIf from selenium.webdriver.chrome.options import Options as ChromeOptions from contextlib import contextmanager import json @skipIf(test_browser_name() != 'chrome', 'Only run this test for Chrome') class ChromeOptionsTest(TestCase): def test_start_chrome_does_not_override_custom_prefs(self): test_value = 'download.default_directory' prefs = {'download.default_directory': test_value} with chrome_with_prefs(prefs) as actual_prefs: self.assertEqual( test_value, actual_prefs['download']['default_directory'] ) def test_start_chrome_respects_custom_password_leak_detection(self): prefs = {'profile.password_manager_leak_detection': True} with chrome_with_prefs(prefs) as actual_prefs: self.assertTrue( actual_prefs['profile']['password_manager_leak_detection'] ) @contextmanager def chrome_with_prefs(prefs): try: options = ChromeOptions() options.add_experimental_option('prefs', prefs) driver = start_chrome(headless=True, options=options) user_data_dir = driver.capabilities['chrome']['userDataDir'] prefs_file = join(user_data_dir, 'Default', 'Preferences') with open(prefs_file, 'r') as f: yield json.load(f) finally: kill_browser() ================================================ FILE: tests/api/test_click.py ================================================ from helium import click, Config from helium._impl.util.lang import TemporaryAttrValue from tests.api import BrowserAT class ClickTest(BrowserAT): def get_page(self): return 'test_click.html' def test_click(self): click("Click me!") self.assertEqual('Success!', self.read_result_from_browser()) def test_click_non_existent_element(self): with TemporaryAttrValue(Config, 'implicit_wait_secs', 1): with self.assertRaises(LookupError): click("Non-existent") ================================================ FILE: tests/api/test_doubleclick.py ================================================ from helium import doubleclick from tests.api import BrowserAT class DoubleclickTest(BrowserAT): def get_page(self): return 'test_doubleclick.html' def test_double_click(self): doubleclick('Doubleclick here.') self.assertEqual('Success!', self.read_result_from_browser()) ================================================ FILE: tests/api/test_drag.py ================================================ from helium import * from selenium.webdriver.common.by import By from tests.api import BrowserAT class DragTest(BrowserAT): def setUp(self): super().setUp() self.drag_target = self.driver.find_element(By.ID, 'target') def get_page(self): return 'test_drag/default.html' def test_drag(self): drag("Drag me.", to=self.drag_target) self.assertEqual('Success!', self.read_result_from_browser()) def test_drag_to_point(self): target_loc = self.drag_target.location target_size = self.drag_target.size target_point = Point( target_loc['x'] + target_size['width'] / 2, target_loc['y'] + target_size['height'] / 2 ) self.assertTrue(Text('Drag me').exists()) drag("Drag me.", to=target_point) self.assertEqual('Success!', self.read_result_from_browser()) class Html5DragIT(BrowserAT): def get_page(self): return 'test_drag/html5.html' def test_html5_drag(self): drag("Drag me.", to=self.driver.find_element(By.ID, 'target')) self.assertEqual('Success!', self.read_result_from_browser()) ================================================ FILE: tests/api/test_file_upload.py ================================================ from helium import attach_file, drag_file, TextField, Text from tests.api import BrowserAT from tests.api.util import get_data_file class FileUploadTest(BrowserAT): def get_page(self): return 'test_file_upload/test_file_upload.html' def setUp(self): super().setUp() self.file_to_upload = get_data_file( 'test_file_upload', 'upload_this.png' ) def test_normal_file_upload_is_not_text_field(self): self.assertFalse(TextField("Normal file upload").exists()) def test_attach_file_to_normal_file_upload(self): attach_file(self.file_to_upload, to='Normal file upload') self.assertEqual('Success!', self.read_result_from_browser()) def test_attach_file_no_to(self): attach_file(self.file_to_upload) self.assertEqual('Success!', self.read_result_from_browser()) def test_attach_file_to_point(self): attach_file( self.file_to_upload, to=Text('Normal file upload').top_left + (200, 10) ) self.assertEqual('Success!', self.read_result_from_browser()) def test_drag_file_to_appearing_drop_area(self): drag_file(self.file_to_upload, to='Drop the file here!') self.assertEqual('Success!', self.read_result_from_browser()) ================================================ FILE: tests/api/test_find_all.py ================================================ from selenium.common.exceptions import StaleElementReferenceException from helium import find_all, Button, TextField, write from tests.api import BrowserAT class FindAllTest(BrowserAT): def get_page(self): return 'test_gui_elements.html' def test_find_all_duplicate_button(self): self.assertEqual(4, len(find_all(Button("Duplicate Button")))) def test_find_all_duplicate_button_to_right_of(self): self.assertEqual( 2, len(find_all(Button("Duplicate Button", to_right_of="Row 1"))) ) def test_find_all_duplicate_button_below_to_right_of(self): self.assertEqual( 1, len(find_all(Button( "Duplicate Button", below="Column 1", to_right_of="Row 1" ))) ) def test_find_all_nested_search_areas(self): # `test_find_all_duplicate_button_below_to_right_of` above showed that # there is only one button that fulfills the following criteria: button = \ Button("Duplicate Button", below="Column 1", to_right_of="Row 1") # Similarly, there should only be one button below this button: self.assertEqual( 1, len(find_all(Button("Duplicate Button", below=button))) ) def test_find_all_non_existent_button(self): self.assertEqual([], find_all(Button("Non-existent Button"))) def test_find_all_yields_api_elements(self): self.assertIsInstance( find_all(TextField('Example Text Field'))[0], TextField ) def test_interact_with_found_elements(self): all_tfs = find_all(TextField()) example_tf = None for text_field in all_tfs: try: id_ = text_field.web_element.get_attribute('id') except StaleElementReferenceException: # This may happen for found web elements in different iframes. # TODO: Improve this, eg. by adding a .getId() property to # TextField (/HTMLElement) which handles this problem. pass else: if id_ == 'exampleTextFieldId': example_tf = text_field self.assertIsNotNone(example_tf) write("test_interact_with_found_elements", into=example_tf) self.assertEqual( "test_interact_with_found_elements", TextField("Example Text Field").value ) def test_bound_element_as_spatial_constraint(self): counts = [] for button in find_all(Button("Duplicate Button")): below_this = find_all(Button("Duplicate Button", below=button)) counts.append(len(below_this)) self.assertEqual( sorted(counts), [0, 0, 1, 1], "Two buttons (those in row 1) have 1 button below, two (those in " "row 2) have 0." ) def test_very_nested_search_areas(self): self.assertEqual( 1, len(find_all( Button('Duplicate Button', to_left_of=Button( 'Duplicate Button', below=Button('Duplicate Button') )) )) ) ================================================ FILE: tests/api/test_gui_elements.py ================================================ # -*- coding: utf-8 -*- from helium import Button, TextField, ComboBox, CheckBox, click, \ RadioButton, write, Text, find_all, Link, ListItem, Image, select, Config from tests.api import BrowserAT class GUIElementsTest(BrowserAT): def get_page(self): return 'test_gui_elements.html' @classmethod def setUpClass(cls): super().setUpClass() # If a test does fail, ensure it happens quickly: cls.implicit_wait_secs_before = Config.implicit_wait_secs Config.implicit_wait_secs = .5 @classmethod def tearDownClass(cls): Config.implicit_wait_secs = cls.implicit_wait_secs_before super().tearDownClass() # Button tests: def test_button_exists(self): self.assertTrue(Button("Enabled Button").exists()) def test_submit_button_exists(self): self.assertTrue(Button("Submit Button").exists()) def test_submit_button_exists_lower_case(self): self.assertTrue(Button("submit button").exists()) def test_input_button_exists(self): self.assertTrue(Button("Input Button").exists()) def test_button_not_exists(self): self.assertFalse(Button("Nonexistent Button").exists()) def test_text_field_does_not_exist_as_button(self): self.assertFalse(Button("Example Text Field").exists()) def test_enabled_button(self): self.assertIs(True, Button("Enabled Button").is_enabled()) def test_disabled_button(self): self.assertFalse(Button("Disabled Button").is_enabled()) def test_button_no_text(self): self.assertEqual(2, len(find_all(Button(to_right_of='Row 1')))) def test_div_button_exists(self): self.assertTrue(Button("DIV with role=button").exists()) def test_button_tag_button_exists(self): self.assertTrue(Button("Button tag without type").exists()) def test_submit_button_can_be_found_by_title(self): self.assertTrue(Button("submitButtonTitle").exists()) # TextField tests: def test_text_field_exists(self): self.assertIs(True, TextField("Example Text Field").exists()) def test_text_field_lower_case_exists(self): self.assertIs(True, TextField("example text field").exists()) def test_text_field_in_second_col_exists(self): self.assertIs(True, TextField("Another Text Field").exists()) def test_text_field_not_exists(self): self.assertFalse(TextField("Nonexistent TextField").exists()) def test_text_field_is_editable_false(self): self.assertIs(False, TextField("ReadOnly Text Field").is_editable()) def test_text_field_is_editable(self): self.assertTrue(TextField("Example Text Field").is_editable()) def test_text_field_is_enabled(self): self.assertIs(True, TextField("Example Text Field").is_enabled()) def test_text_field_is_enabled_false(self): self.assertFalse(TextField("Disabled Text Field").is_enabled()) def test_text_field_value(self): self.assertEqual("Lorem ipsum", TextField("Example Text Field").value) def test_text_field_with_placeholder_exists(self): self.assertIs(True, TextField("Placeholder Text Field").exists()) def test_text_field_no_type_specified_with_placeholder_exists(self): self.assertIs( True, TextField("Placeholder Text Field without type").exists() ) def test_empty_text_field_value(self): self.assertEqual('', TextField("Empty Text Field").value) def test_read_readonly_text_field(self): self.assertEqual( 'This is read only', TextField("ReadOnly Text Field").value ) def test_read_disabled_text_field(self): self.assertEqual( 'This is disabled', TextField("Disabled Text Field").value ) def test_read_german_text_field(self): self.assertEqual( 'Heizölrückstoßabdämpfung', TextField("Deutsch").value ) def test_text_field_input_type_upper_case_text(self): self.assertTrue(TextField('Input type=Text').exists()) def test_write_into_labelled_text_field(self): write('Some text', into='Labelled Text Field') self.assertEqual('Some text', TextField('Labelled Text Field').value) def test_required_text_field_marked_with_asterisk_exists(self): self.assertIs(True, TextField("Required Text Field").exists()) def test_text_field_labelled_by_free_text(self): self.assertEqual( "TF labelled by free text", TextField("Text field labelled by free text").value ) def test_input_type_tel(self): self.assertFindsEltWithId(TextField("Input type=tel"), "inputTypeTel") def test_input_type_date(self): self.assertFindsEltWithId(TextField("Input type=date"), "inputTypeDate") def test_input_type_time(self): self.assertFindsEltWithId(TextField("Input type=time"), "inputTypeTime") def test_text_field_to_right_of_text_field(self): self.assertFindsEltWithId( TextField(to_right_of=TextField("Required Text Field")), "inputTypeTel" ) def test_contenteditable_paragrapth(self): self.assertFindsEltWithId( TextField("contenteditable Paragraph"), "contenteditableParagraphId" ) # ComboBox tests: def test_combo_box_exists(self): self.assertIs(True, ComboBox("Drop Down List").exists()) def test_combo_box_exists_lower_case(self): self.assertIs(True, ComboBox("drop down list").exists()) def test_drop_down_list_is_editable_false(self): self.assertIs(False, ComboBox("Drop Down List").is_editable()) def test_editable_combo_box_is_editable(self): self.assertTrue(ComboBox("Editable ComboBox").is_editable()) def test_combo_box_options(self): options = ComboBox("Drop Down List").options self.assertListEqual( options, ['Option One', 'Option Two', 'Option Three'] ) def test_reads_value_of_combo_box(self): self.assertEqual('Option One', ComboBox("Drop Down List").value) def test_select_value_from_combo_box(self): self.assertEqual('Option One', ComboBox("Drop Down List").value) select("Drop Down List", "Option Two") self.assertEqual('Option Two', ComboBox("Drop Down List").value) select(ComboBox("Drop Down List"), "Option Three") self.assertEqual('Option Three', ComboBox("Drop Down List").value) def test_combo_box_identified_by_value(self): combo_box = ComboBox("Select a value...") self.assertTrue(combo_box.exists()) self.assertEqual("Select a value...", combo_box.value) self.assertFalse(combo_box.is_editable()) self.assertEqual( ["Select a value...", "Value 1"], combo_box.options ) def test_combo_box_preceded_by_combo_with_name_as_label(self): self.assertEqual( "combo1", ComboBox("Combo1").web_element.get_attribute("id") ) # CheckBox tests: def test_check_box_exists(self): self.assertIs(True, CheckBox("CheckBox").exists()) def test_check_box_exists_lower_case(self): self.assertIs(True, CheckBox("checkbox").exists()) def test_left_hand_side_check_box_exists(self): self.assertIs(True, CheckBox("LHS CheckBox").exists()) def test_check_box_not_exists(self): self.assertFalse(CheckBox("Nonexistent CheckBox").exists()) def test_text_field_does_not_exist_as_check_box(self): self.assertFalse(CheckBox("Empty Text Field").exists()) def test_ticked_check_box_exists(self): self.assertIs(True, CheckBox("Ticked CheckBox").exists()) def test_ticked_check_box_is_enabled(self): self.assertIs(True, CheckBox("Ticked CheckBox").is_enabled()) def test_right_labelled_check_box_exists(self): self.assertIs(True, CheckBox("Right Labeled CheckBox").exists()) def test_left_labelled_check_box_exists(self): self.assertIs(True, CheckBox("Left Labeled CheckBox").exists()) def test_disabled_check_box_exists(self): self.assertIs(True, CheckBox("Disabled CheckBox").exists()) def test_ticked_check_box_is_checked(self): self.assertIs(True, CheckBox("Ticked CheckBox").is_checked()) def test_right_labelled_check_box_is_not_checked(self): self.assertFalse(CheckBox("Right Labeled CheckBox").is_checked()) def test_left_labelled_check_box_is_not_checked(self): self.assertIs(False, CheckBox("Left Labeled CheckBox").is_checked()) def test_disabled_check_box_is_not_checked(self): self.assertIs(False, CheckBox("Disabled CheckBox").is_checked()) def test_untick_check_box(self): ticked_check_box = CheckBox("Ticked CheckBox") click(ticked_check_box) self.assertIs(False, ticked_check_box.is_checked()) def test_disabled_check_box_is_not_enabled(self): self.assertIs(False, CheckBox("Disabled CheckBox").is_enabled()) def test_check_box_enclosed_by_label(self): self.assertFindsEltWithId( CheckBox("CheckBox enclosed by label"), "checkBoxEnclosedByLabel" ) def test_checkboxes_labelled_by_free_text(self): self.assertTrue(CheckBox("unchecked").exists()) self.assertTrue(CheckBox("checked").exists()) self.assertTrue(CheckBox("checked").is_checked()) self.assertFalse(CheckBox("unchecked").is_checked()) # RadioButton tests: def test_first_radio_button_exists(self): self.assertIs(True, RadioButton("RadioButton 1").exists()) def test_first_radio_button_exists_lower_case(self): self.assertIs(True, RadioButton("radiobutton 1").exists()) def test_second_radio_button_exists(self): self.assertIs(True, RadioButton("RadioButton 2").exists()) def test_left_labelled_radio_button_one_exists(self): self.assertIs(True, RadioButton("Left Labeled RadioButton 1").exists()) def test_left_labelled_radio_button_two_exists(self): self.assertIs(True, RadioButton("Left Labeled RadioButton 2").exists()) def test_first_radio_button_is_selected(self): self.assertIs(True, RadioButton("RadioButton 1").is_selected()) def test_second_radio_button_is_not_selected(self): self.assertIs(False, RadioButton("RadioButton 2").is_selected()) def test_select_second_radio_button(self): click(RadioButton("RadioButton 2")) self.assertIs(False, RadioButton("RadioButton 1").is_selected()) self.assertIs(True, RadioButton("RadioButton 2").is_selected()) def test_radio_button_not_exists(self): self.assertIs(False, RadioButton("Nonexistent option").exists()) def test_text_field_is_not_a_radio_button(self): self.assertIs(False, RadioButton("Empty Text Field").exists()) def test_radiobuttons_labelled_by_free_text(self): self.assertTrue(RadioButton("male").exists()) self.assertTrue(RadioButton("female").exists()) self.assertTrue(RadioButton("male").is_selected()) self.assertFalse(RadioButton("female").is_selected()) # Text tests: def test_text_exists_submit_button(self): self.assertTrue(Text("Submit Button").exists()) def test_text_exists_submit_button_lower_case(self): self.assertTrue(Text("submit button").exists()) def test_text_exists_link_with_title(self): self.assertTrue(Text("Link with title").exists()) def test_text_exists_link_with_title_lower_case(self): self.assertTrue(Text("link with title").exists()) def test_text_with_leading_nbsp_exists(self): self.assertTrue(Text("Text with leading  ").exists()) def test_read_text_value(self): self.assertEqual(Text(to_right_of=Text("EUR/USD")).value, "1.3487") def test_free_text_not_surrounded_by_tags_exists(self): self.assertTrue(Text("Free text not surrounded by tags").exists()) def test_text_with_apostrophe(self): self.assertTrue(Text("Your email's been sent!").exists()) def test_text_with_double_quotes(self): self.assertTrue(Text('He said "double quotes".').exists()) def test_text_with_single_and_double_quotes(self): self.assertTrue(Text("Single'quote. Double\"quote.").exists()) def test_text_uppercase_umlaut(self): self.assertTrue(Text('VERÖFFENTLICHEN').exists()) # Link tests: def test_link_exists(self): self.assertTrue(Link("Link").exists()) def test_link_with_title_exists(self): self.assertTrue(Link('Link with title').exists()) def test_link_no_text(self): self.assertEqual(4, len(find_all(Link()))) def test_span_with_role_link_exists_as_link(self): self.assertTrue(Link("Span with role=link").exists()) def test_link_href(self): self.assertEqual(Link("heliumhq.com").href, "http://heliumhq.com/") def test_link_empty_href(self): self.assertEqual(Link("Link with empty href").href, "") # ListItem tests: def test_list_item_no_text(self): all_list_items = find_all(ListItem(below="HTML Unordered List")) texts = {list_item.web_element.text for list_item in all_list_items} self.assertEqual({'ListItem 1', 'ListItem 2'}, texts) # Image tests: def test_image_not_exists(self): self.assertFalse(Image("Non-existent").exists()) def test_image_exists(self): self.assertTrue(Image("Dolphin").exists()) # Misc tests: def test_text_field_combo_box_with_same_name(self): text_field = TextField("Language") combo_box = ComboBox("Language") self.assertNotEqual(text_field.y, combo_box.y) ================================================ FILE: tests/api/test_highlight.py ================================================ from helium import highlight, Button, Text, Config from helium._impl.util.lang import TemporaryAttrValue from tests.api import BrowserAT class HighlightTest(BrowserAT): def get_page(self): return 'test_gui_elements.html' def test_highlight(self): button = Button("Input Button") highlight(button) self._check_is_highlighted(button) def test_highlight_string(self): highlight("Text with id") self._check_is_highlighted(Text("Text with id")) def test_highlight_nonexistent(self): with TemporaryAttrValue(Config, 'implicit_wait_secs', .5): with self.assertRaises(LookupError): highlight(Button("foo")) def _check_is_highlighted(self, html_element): style = html_element.web_element.get_attribute("style") self.assertTrue("border: 2px solid red;" in style, style) self.assertTrue("font-weight: bold;" in style, style) ================================================ FILE: tests/api/test_hover.py ================================================ from helium import hover, Config from helium._impl.util.lang import TemporaryAttrValue from helium._impl.util.system import is_windows from tests.api import BrowserAT class HoverTest(BrowserAT): def get_page(self): return 'test_hover.html' def setUp(self): # This test fails if the mouse cursor happens to be over one of the # links in test_hover.html. Move the mouse cursor to (0, 0) to # prevent spurious test failures: self._move_mouse_cursor_to_origin() super().setUp() def _move_mouse_cursor_to_origin(self): if is_windows(): from win32api import SetCursorPos SetCursorPos((0, 0)) # Feel free to add implementation for OSX/Linux here... def test_hover_one(self): hover('Dropdown 1') result = self.read_result_from_browser() self.assertEqual( 'Dropdown 1', result, "Got unexpected result %r. Maybe the mouse cursor was over the " "browser window and interfered with the test?" % result ) def test_hover_two_consecutively(self): hover('Dropdown 2') hover('Item C') result = self.read_result_from_browser() self.assertEqual( 'Dropdown 2 - Item C', result, "Got unexpected result %r. Maybe the mouse cursor was over the " "browser window and interfered with the test?" % result ) def test_hover_hidden(self): with TemporaryAttrValue(Config, 'implicit_wait_secs', 1): try: hover("Item C") except LookupError: pass # Success! else: self.fail( "Didn't receive expected LookupError. Maybe the mouse " "cursor was over the browser window and interfered with " "the test?" ) ================================================ FILE: tests/api/test_iframe.py ================================================ from helium import Text, get_driver, find_all from tests.api import BrowserAT class IframeTest(BrowserAT): def get_page(self): return "test_iframe/main.html" def test_test_text_in_iframe_exists(self): self.assertTrue(Text("This text is inside an iframe.").exists()) def test_text_in_nested_iframe_exists(self): self.assertTrue(Text("This text is inside a nested iframe.").exists()) def test_finds_element_in_parent_iframe(self): self.test_text_in_nested_iframe_exists() # Now we're "focused" on the nested IFrame. Check that we can still # find the element an the parent IFrame: self.test_test_text_in_iframe_exists() def test_access_attributes_across_iframes(self): text = Text("This text is inside an iframe.") self.assertEqual("This text is inside an iframe.", text.value) get_driver().switch_to.default_content() self.assertEqual("This text is inside an iframe.", text.value) def test_repr(self): text, = find_all(Text("This text is inside an iframe.")) get_driver().switch_to.default_content() # The text is now outside the current iframe. `repr(...)` should still # work without producing any errors: self.assertEqual("Text('This text is inside an iframe.')", repr(text)) ================================================ FILE: tests/api/test_implicit_wait.py ================================================ from helium import click, Config from helium._impl.util.lang import TemporaryAttrValue from tests.api import BrowserAT from time import time class ImplicitWaitTest(BrowserAT): def get_page(self): return 'test_implicit_wait.html' def test_click_text_implicit_wait(self): click("Click me!") start_time = time() click("Now click me!") end_time = time() self.assertEqual('Success!', self.read_result_from_browser()) self.assertGreaterEqual(end_time - start_time, 3.0) def test_click_text_no_implicit_wait(self): with TemporaryAttrValue(Config, 'implicit_wait_secs', 0): with self.assertRaises(LookupError): click("Non-existent") def test_click_text_too_small_implicit_wait_secs(self): with TemporaryAttrValue(Config, 'implicit_wait_secs', 1): click("Click me!") with self.assertRaises(LookupError): click("Now click me!") ================================================ FILE: tests/api/test_kill_service_at_exit.py ================================================ from psutil import NoSuchProcess import psutil class KillServiceAtExitAT: def test_kill_service_at_exit(self): self.start_browser_in_sub_process() self.assertEqual([], self.get_new_running_services()) def start_browser_in_sub_process(self): raise NotImplementedError() def get_new_running_services(self): return [s for s in self.get_running_services() if s not in self.running_services_before] def setUp(self): self.running_services_before = self.get_running_services() self.running_browsers_before = self.get_running_browsers() def tearDown(self): for service in self.get_new_running_services(): try: service.terminate() except NoSuchProcess: # Process terminated already. pass for browser in self.get_new_running_browsers(): try: browser.terminate() except NoSuchProcess: # Process terminated already. pass def get_new_running_browsers(self): return [s for s in self.get_running_browsers() if s not in self.running_browsers_before] def get_running_services(self): return self._get_running_processes(self.get_service_process_names()) def get_running_browsers(self): return self._get_running_processes([self.get_browser_process_name()]) def _get_running_processes(self, image_names): result = [] for p in psutil.process_iter(): if p.name in image_names: result.append(p) return result def get_service_process_names(self): raise NotImplementedError() def get_browser_process_name(self): raise NotImplementedError() def start_browser(self): raise NotImplementedError() ================================================ FILE: tests/api/test_kill_service_at_exit_chrome.py ================================================ from helium import start_chrome from helium._impl.util.system import is_windows from tests.api import test_browser_name from tests.api.test_kill_service_at_exit import KillServiceAtExitAT from tests.api.util import InSubProcess from unittest import TestCase, skipIf @skipIf(test_browser_name() != 'chrome', 'Only run this test for Chrome') class KillServiceAtExitChromeTest(KillServiceAtExitAT, TestCase): def get_service_process_names(self): if is_windows(): return ['chromedriver.exe'] return ['chromedriver'] def get_browser_process_name(self): return 'chrome' + ('.exe' if is_windows() else '') def start_browser_in_sub_process(self): with ChromeInSubProcess(): pass class ChromeInSubProcess(InSubProcess): @classmethod def main(cls): start_chrome(headless=True) cls.synchronize_with_parent_process() if __name__ == '__main__': ChromeInSubProcess.main() ================================================ FILE: tests/api/test_leaked_password.py ================================================ from helium import write, click, Text, wait_until from tests.api import BrowserAT class LeakedPasswordTest(BrowserAT): def get_page(self): return 'test_leaked_password.html' def test_submit_leaked_password(self): # Chrome 140.0.7339.185 or earlier introduced password leak detection. # Writing leaked credentials into an input field sometimes brings up a # browser notification "The password you just used was found in a data # breach". Test that Helium prevents this: write('testuser', into='Username') write('testpassword', into='Password') click('Submit') wait_until(Text('You logged in with testuser:testpassword').exists) ================================================ FILE: tests/api/test_no_driver.py ================================================ from helium import * from helium._impl import APIImpl from unittest import TestCase class NoDriverTest(TestCase): def test_go_to_requires_driver(self): self._check_requires_driver(lambda: go_to('google.com')) def test_write_requires_driver(self): self._check_requires_driver(lambda: write('foo')) def test_press_requires_driver(self): self._check_requires_driver(lambda: press(ENTER)) def test_click_requires_driver(self): self._check_requires_driver(lambda: click("Sign in")) def test_doubleclick_requires_driver(self): self._check_requires_driver(lambda: doubleclick("Sign in")) def test_drag_requires_driver(self): self._check_requires_driver(lambda: drag("Drag me", to="Drop here")) def test_find_all_requires_driver(self): self._check_requires_driver(lambda: find_all(Button())) def test_scroll_down_requires_driver(self): self._check_requires_driver(lambda: scroll_down()) def test_scroll_up_requires_driver(self): self._check_requires_driver(lambda: scroll_up()) def test_scroll_right_requires_driver(self): self._check_requires_driver(lambda: scroll_right()) def test_scroll_left_requires_driver(self): self._check_requires_driver(lambda: scroll_left()) def test_hover_requires_driver(self): self._check_requires_driver(lambda: hover("Hi there!")) def test_rightclick_requires_driver(self): self._check_requires_driver(lambda: rightclick("Hi there!")) def test_select_requires_driver(self): self._check_requires_driver(lambda: select("Language", "English")) def test_drag_file_requires_driver(self): self._check_requires_driver( lambda: drag_file(r'C:\test.txt', to="Here") ) def test_attach_file_requires_driver(self): self._check_requires_driver(lambda: attach_file(r'C:\test.txt')) def test_refresh_requires_driver(self): self._check_requires_driver(lambda: refresh()) def test_wait_until_requires_driver(self): self._check_requires_driver(lambda: wait_until(lambda: True)) def test_switch_to_requires_driver(self): self._check_requires_driver(lambda: switch_to('Popup')) def test_kill_browser_requires_driver(self): self._check_requires_driver(lambda: switch_to('Popup')) def test_highlight_requires_driver(self): self._check_requires_driver(lambda: switch_to('Popup')) def test_s_requires_driver(self): self._check_requires_driver(lambda: S('#home')) def test_text_requires_driver(self): self._check_requires_driver(lambda: Text('Home')) def test_link_requires_driver(self): self._check_requires_driver(lambda: Link('Home')) def test_list_item_requires_driver(self): self._check_requires_driver(lambda: ListItem('Home')) def test_button_requires_driver(self): self._check_requires_driver(lambda: Button('Home')) def test_image_requires_driver(self): self._check_requires_driver(lambda: Image('Logo')) def test_text_field_requires_driver(self): self._check_requires_driver(lambda: TextField('File name')) def test_combo_box_requires_driver(self): self._check_requires_driver(lambda: ComboBox('Language')) def test_check_box_requires_driver(self): self._check_requires_driver(lambda: CheckBox('True?')) def test_radio_button_requires_driver(self): self._check_requires_driver(lambda: RadioButton('Option A')) def test_window_requires_driver(self): self._check_requires_driver(lambda: Window('Main')) def test_alert_requires_driver(self): self._check_requires_driver(lambda: Alert()) def _check_requires_driver(self, function): with self.assertRaises(RuntimeError) as cm: function() self.assertEqual(APIImpl.DRIVER_REQUIRED_MESSAGE, cm.exception.args[0]) ================================================ FILE: tests/api/test_point.py ================================================ from helium import click, Point, Button, hover, rightclick, doubleclick, drag from tests.api import BrowserAT, test_browser_name from re import search class PointTest(BrowserAT): """ Tests helium.Point. The tests allow for a coordinate difference between browsers of up to +/- 1 pixel. For instance: In Firefox, Button("Button 1").center is (39, 12), in IE and Chrome it is (39, 13). This really is because Firefox lays out the page slightly differently, so that the button is further up on the page. """ def get_page(self): return 'test_point.html' def setUp(self): super().setUp() if test_browser_name() != 'chrome': # Imagine two consecutive tests that hover the mouse cursor to the # same position. In the second test, Firefox does not generate a # "mouse move" event (probably because the cursor is already in the # "correct" location). However, this prevents our JavaScript from # firing, which updates the status text element required for this # text. Fix this by re-setting the mouse cursor. Chrome does not # suffer from this problem. hover(Point(0, 0)) def test_top_left(self): self.assert_is_in_range( Point(2, 3), Button("Button 1").top_left, delta=(0, 1) ) def assert_is_in_range(self, expected, point, delta): x, y = point expected_x, expected_y = expected delta_x, delta_y = delta self.assert_around(expected_x, x, delta_x) self.assert_around(expected_y, y, delta_y) def assert_around(self, expected, actual, delta, msg=None): self.assertIn( actual, list(range(expected - delta, expected + delta + 1)), msg ) def test_click_top_left(self): click(Button("Button 1").top_left) self.assert_result_is( "Button 1 clicked at offset (0, 0).", offset_delta=(1, 1) ) def test_click_point(self): click(Point(39, 13)) self.assert_result_is( "Button 1 clicked at offset (37, 10).", offset_delta=(0, 1) ) def test_click_top_left_offset(self): click(Button("Button 3").top_left + (3, 4)) self.assert_result_is("Button 3 clicked at offset (3, 4).") def test_hover_top_left(self): hover(Button("Button 1").top_left) self.assert_result_is( "Button 1 hovered at offset (0, 0).", offset_delta=(1, 1) ) def test_hover_point(self): hover(Point(39, 13)) self.assert_result_is( "Button 1 hovered at offset (37, 10).", offset_delta=(0, 1) ) def test_hover_top_left_offset(self): hover(Button("Button 3").top_left + (3, 4)) self.assert_result_is("Button 3 hovered at offset (3, 4).") def test_rightclick_top_left(self): rightclick(Button("Button 1").top_left) self.assert_result_is( "Button 1 rightclicked at offset (0, 0).", offset_delta=(1, 1) ) def test_rightclick_point(self): rightclick(Point(39, 13)) self.assert_result_is( "Button 1 rightclicked at offset (37, 10).", offset_delta=(0, 1) ) def test_rightclick_top_left_offset(self): rightclick(Button("Button 3").top_left + (3, 4)) self.assert_result_is( "Button 3 rightclicked at offset (3, 4)." ) def test_doubleclick_top_left(self): doubleclick(Button("Button 1").top_left) self.assert_result_is( "Button 1 doubleclicked at offset (0, 0).", offset_delta=(1, 1) ) def test_doubleclick_point(self): doubleclick(Point(39, 13)) self.assert_result_is( "Button 1 doubleclicked at offset (37, 10).", offset_delta=(0, 1) ) def test_doubleclick_top_left_offset(self): doubleclick(Button("Button 3").top_left + (3, 4)) self.assert_result_is("Button 3 doubleclicked at offset (3, 4).") def test_drag_point(self): drag(Button("Button 1").top_left, to=Point(39, 13)) self.assert_result_is( "Button 1 clicked at offset (37, 10).", offset_delta=(0, 1) ) def assert_result_is(self, expected, offset_delta=(0, 0)): actual = self.read_result_from_browser() expected_offset = self._extract_offset(expected) actual_offset = self._extract_offset(actual) expected_x, expected_y = eval(expected_offset) actual_x, actual_y = eval(actual_offset) delta_x, delta_y = offset_delta self.assert_around( expected_x, actual_x, delta_x, "Offset (%r, %r) is not in expected range (%r+-%r, %r+-%r)." % ( actual_x, actual_y, expected_x, delta_x, expected_y, delta_y ) ) self.assert_around( expected_x, actual_x, delta_x, "Offset (%r, %r) is not in expected range (%r+-%r, %r+-%r)." % ( actual_x, actual_y, expected_x, delta_x, expected_y, delta_y ) ) expected_prefix, expected_suffix = expected.split(expected_offset) actual_prefix, actual_suffix = actual.split(actual_offset) self.assertEqual(expected_prefix, actual_prefix) self.assertEqual(expected_suffix, actual_suffix) def _extract_offset(self, result_in_browser): return search(r"(\([^,]+, [^\)]+\))", result_in_browser).group(1) ================================================ FILE: tests/api/test_press.py ================================================ from helium import press, TextField, SHIFT from tests.api import BrowserAT class PressTest(BrowserAT): def get_page(self): return 'test_write.html' def test_press_single_character(self): press('a') self.assertEqual('a', TextField('Autofocus text field').value) def test_press_upper_case_character(self): press('A') self.assertEqual('A', TextField('Autofocus text field').value) def test_press_shift_plus_lower_case_character(self): press(SHIFT + 'a') self.assertEqual('A', TextField('Autofocus text field').value) ================================================ FILE: tests/api/test_repr.py ================================================ from helium import * from helium import HTMLElement from tests.api import BrowserAT import re class UnboundReprTest(BrowserAT): def get_page(self): return 'test_gui_elements.html' def test_unbound_s_repr(self): self.assertEqual( "S('.cssClass')", repr(S('.cssClass')) ) def test_unbound_s_repr_below(self): self.assertEqual( "S('.cssClass', below='Home')", repr(S('.cssClass', below='Home')) ) def test_unbound_text_repr(self): self.assertEqual( "Text('Hello World!')", repr(Text('Hello World!')) ) def test_unbound_link_repr(self): self.assertEqual( "Link('Download')", repr(Link('Download')) ) def test_unbound_list_item_repr(self): self.assertEqual( "ListItem('Home')", repr(ListItem('Home')) ) def test_unbound_button_repr(self): self.assertEqual( "Button('Home')", repr(Button('Home')) ) def test_unbound_image_repr(self): self.assertEqual( "Image('Logo')", repr(Image('Logo')) ) def test_unbound_text_field_repr(self): self.assertEqual( "TextField('File name')", repr(TextField('File name')) ) def test_unbound_combo_box_repr(self): self.assertEqual( "ComboBox('Language')", repr(ComboBox('Language')) ) def test_unbound_check_box_repr(self): self.assertEqual( "CheckBox('True?')", repr(CheckBox('True?')) ) def test_unbound_radio_button_repr(self): self.assertEqual( "RadioButton('Option A')", repr(RadioButton('Option A')) ) def test_unbound_window_repr(self): self.assertEqual( "Window('Main')", repr(Window('Main')) ) def test_unbound_alert_repr(self): self.assertEqual( "Alert()", repr(Alert()) ) def test_unbound_alert_repr_with_search_text(self): self.assertEqual( "Alert('Hello World')", repr(Alert('Hello World')) ) class BoundReprTest(BrowserAT): def get_page(self): return 'test_gui_elements.html' def test_bound_s_repr(self): bound_s = self._bind(S("#checkBoxId")) self._assertHtmlEltWithMultipleAttributesEquals( '', repr(bound_s) ) def test_bound_s_repr_long_content(self): body = self._bind(S("body")) self.assertEqual("...", repr(body)) def test_bound_button_repr(self): bound_button = self._bind(Button('Enabled Button')) self.assertEqual( '', repr(bound_button) ) def test_bound_link_repr_nested_tag(self): link = self._bind(Link("Link with title")) self._assertHtmlEltWithMultipleAttributesEquals( '...', repr(link) ) def test_bound_repr_duplicate_button(self): self.assertEqual( '[,' ' ,' ' ,' ' ]', repr(find_all(Button("Duplicate Button"))) ) def test_bound_window_repr(self): bound_window = self._bind(Window()) self.assertEqual( "Window('Test page for browser system tests')", repr(bound_window) ) def test_bound_window_repr_with_search_text(self): bound_window = self._bind(Window('Test page for')) self.assertEqual( "Window('Test page for browser system tests')", repr(bound_window) ) def _bind(self, predicate): if isinstance(predicate, HTMLElement): # Reading a property such as web_element waits for the element to # exist and binds the predicate to it: predicate.web_element else: assert isinstance(predicate, Window) # Reading a property such as handle waits for the element to # exist and binds the predicate to it: predicate.handle return predicate def _assertHtmlEltWithMultipleAttributesEquals(self, expected, actual): start_tag_exp, remainder_exp = expected.split('>', 1) start_tag_act, remainder_act = actual.split('>', 1) attributes_re = '[a-zA-Z]+="[^"]+"' attributes_exp = re.findall(attributes_re, start_tag_exp) attributes_act = re.findall(attributes_re, start_tag_act) self.assertEqual(set(attributes_exp), set(attributes_act)) self.assertEqual(remainder_exp, remainder_act) class BoundAlertReprTest(BrowserAT): def get_page(self): return 'test_alert.html' def setUp(self): super().setUp() click("Display alert") def test_bound_alert_repr(self): alert = Alert() # Bind alert: alert.text self.assertEqual("Alert('Hello World!')", repr(alert)) def test_bound_alert_repr_with_partial_search_text(self): alert = Alert('Hello') # Bind alert: alert.text self.assertEqual("Alert('Hello World!')", repr(alert)) def tearDown(self): Alert().accept() super().tearDown() ================================================ FILE: tests/api/test_rightclick.py ================================================ from helium import click, rightclick from tests.api import BrowserAT class RightclickTest(BrowserAT): def get_page(self): return 'test_rightclick.html' def test_simple_rightclick(self): rightclick("Perform a normal rightclick here.") self.assertEqual( "Normal rightclick performed.", self.read_result_from_browser() ) def test_rightclick_select_normal_item(self): rightclick("Rightclick here for context menu.") click("Normal item") self.assertEqual( "Normal item selected.", self.read_result_from_browser() ) ================================================ FILE: tests/api/test_s.py ================================================ from helium import S from tests.api import BrowserAT class STest(BrowserAT): def get_page(self): return 'test_gui_elements.html' def test_find_by_id(self): self.assertFindsEltWithId(S("#checkBoxId"), 'checkBoxId') def test_find_by_name(self): self.assertFindsEltWithId(S("@checkBoxName"), 'checkBoxId') def test_find_by_class(self): self.assertFindsEltWithId(S(".checkBoxClass"), 'checkBoxId') def test_find_by_xpath(self): self.assertFindsEltWithId( S("//input[@type='checkbox' and @id='checkBoxId']"), 'checkBoxId' ) def test_find_by_css_selector(self): self.assertFindsEltWithId(S('input.checkBoxClass'), 'checkBoxId') ================================================ FILE: tests/api/test_scroll.py ================================================ from helium import scroll_down, scroll_left, scroll_right, scroll_up from tests.api import BrowserAT class ScrollTest(BrowserAT): def get_page(self): return 'test_scroll.html' def test_scroll_up_when_at_top_of_page(self): scroll_up() self.assert_scroll_position_equals(0, 0) def test_scroll_down(self): scroll_down() self.assert_scroll_position_equals(0, 100) def test_scroll_down_then_up(self): scroll_down() scroll_up() self.assert_scroll_position_equals(0, 0) def test_scroll_down_then_up_pixels(self): scroll_down(175) scroll_up(100) self.assert_scroll_position_equals(0, 75) def test_scroll_left_when_at_start_of_page(self): scroll_left() self.assert_scroll_position_equals(0, 0) def test_scroll_right(self): scroll_right() self.assert_scroll_position_equals(100, 0) def test_scroll_right_then_left(self): scroll_right() scroll_left() self.assert_scroll_position_equals(0, 0) def test_scroll_right_then_left_pixels(self): scroll_right(175) scroll_left(100) self.assert_scroll_position_equals(75, 0) def tearDown(self): # Recent versions of Chrome(Driver) don't reset the scroll position when # reloading the page. Force-reset it: self.driver.execute_script('window.scrollTo(0, 0);') super().tearDown() def assert_scroll_position_equals(self, x, y): scroll_position_x = self.driver.execute_script( 'return window.pageXOffset || document.documentElement.scrollLeft ' '|| document.body.scrollLeft' ) self.assertEqual(x, scroll_position_x) scroll_position_y = self.driver.execute_script( 'return window.pageYOffset || document.documentElement.scrollTop ' '|| document.body.scrollTop' ) self.assertEqual(y, scroll_position_y) ================================================ FILE: tests/api/test_start_go_to.py ================================================ from helium import go_to from tests.api import start_browser from tests.api.util import get_data_file_url from os import path from unittest import TestCase class StartGoToTest(TestCase): def setUp(self): self.url = get_data_file_url('test_start_go_to.html') self.driver = None def test_go_to(self): self.driver = start_browser() go_to(self.url) self.assertUrlEquals(self.url, self.driver.current_url) def assertUrlEquals(self, expected, actual): expected = str(path.normpath(expected.lower().replace('\\', '/'))) actual = str(path.normpath(actual.lower().replace('\\', '/'))) self.assertEqual(expected, actual) def test_start_with_url(self): self.driver = start_browser(self.url) self.assertUrlEquals(self.url, self.driver.current_url) def tearDown(self): if self.driver is not None: self.driver.quit() ================================================ FILE: tests/api/test_tables.py ================================================ from helium import * from tests.api import BrowserAT class TablesTest(BrowserAT): def get_page(self): return 'test_tables.html' def test_s_below_above(self): second_table_cells = find_all( S("table > tbody > tr > td", below=Text("Table no. 2"), above=Text("Table no. 3") ) ) self.assertEqual(len(second_table_cells), 9) self.assertListEqual( sorted([cell.web_element.text for cell in second_table_cells]), ['T2R1C1', 'T2R1C2', 'T2R1C3', 'T2R2C1', 'T2R2C2', 'T2R2C3', 'T2R3C1', 'T2R3C2', 'T2R3C3'] ) def test_s_read_table_column(self): email_cells = find_all(S("table > tbody > tr > td", below="Email")) self.assertEqual(len(email_cells), 3) self.assertListEqual( sorted([cell.web_element.text for cell in email_cells]), ['email1@domain.com', 'email2@domain.com', 'email3@domain.com'] ) def test_text_below_to_left_of(self): self.assertEqual( 'Abdul', Text(below='Name', to_left_of='email2@domain.com').value ) ================================================ FILE: tests/api/test_text_impl.py ================================================ from helium._impl import TextImpl from helium._impl.selenium_wrappers import WebDriverWrapper from selenium.webdriver.common.by import By from tests.api import BrowserAT class TextImplTest(BrowserAT): def get_page(self): return 'test_text_impl.html' def test_empty_search_text_xpath(self): xpath = TextImpl(WebDriverWrapper(self.driver))._get_search_text_xpath() text_elements = self.driver.find_elements(By.XPATH, xpath) texts = [w.get_attribute('innerHTML') for w in text_elements] self.assertEqual( ["A paragraph", "A paragraph inside a div", "Another paragraph inside the div"], sorted(texts) ) ================================================ FILE: tests/api/test_wait_until.py ================================================ from helium import click, wait_until, Text from tests.api import BrowserAT from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By from selenium.webdriver.support.expected_conditions import \ presence_of_element_located from time import time class WaitUntilTest(BrowserAT): def get_page(self): return 'test_wait_until.html' def test_wait_until_text_exists(self): click("Click me!") start_time = time() wait_until(Text("Success!").exists) end_time = time() self.assertGreaterEqual(end_time - start_time, 0.8) def test_wait_until_presence_of_element_located(self): click("Click me!") start_time = time() wait_until(presence_of_element_located((By.ID, "result"))) end_time = time() self.assertGreaterEqual(end_time - start_time, 0.8) def test_wait_until_lambda_expires(self): with self.assertRaises(TimeoutException): wait_until(lambda: False, timeout_secs=1) def test_wait_until_lambda_with_driver_expires(self): with self.assertRaises(TimeoutException): wait_until(lambda driver: False, timeout_secs=0.1) ================================================ FILE: tests/api/test_window.py ================================================ from helium import Window, click, go_to, get_driver, wait_until from tests.api.util import get_data_file_url from tests.api import BrowserAT class WindowTest(BrowserAT): def get_page(self): return 'test_window/test_window.html' def test_window_exists(self): self.assertTrue(Window('test_window').exists()) def test_window_not_exists(self): self.assertFalse(Window('non-existent').exists()) def test_no_arg_window_exists(self): self.assertTrue(Window().exists()) def test_handle(self): self.assertTrue(Window('test_window').handle) def test_title(self): self.assertEqual('test_window', Window('test_window').title) class MultipleWindowTest(WindowTest): """ The purpose of this Test is to run the same tests as WindowTest, but with an additional pop up window open. """ @classmethod def setUpClass(cls): super().setUpClass() go_to(get_data_file_url('test_window/test_window.html')) click("Click here to open a popup.") wait_until(Window('test_window - popup').exists) def test_popup_window_exists(self): self.assertTrue(Window('test_window - popup').exists()) def setUp(self): # Don't let super go_to(...): pass @classmethod def tearDownClass(cls): popup_window_handle = Window("test_window - popup").handle main_window_handle = Window("test_window").handle get_driver().switch_to.window(popup_window_handle) get_driver().close() get_driver().switch_to.window(main_window_handle) super().tearDownClass() ================================================ FILE: tests/api/test_window_handling.py ================================================ from helium import write, click, switch_to, TextField, Text, get_driver, \ Link, wait_until from selenium.webdriver.common.by import By from tests.api import BrowserAT, test_browser_name from unittest import skipIf class WindowHandlingTest(BrowserAT): def get_page(self): return 'test_window_handling/main.html' def test_write_writes_in_active_window(self): write("Main window") self.assertEqual("Main window", self._get_value('mainTextField')) self._open_popup() write("Popup") self.assertEqual("Popup", self._get_value('popupTextField')) def test_write_searches_in_active_window(self): write("Main window", into="Text field") self.assertEqual("Main window", self._get_value('mainTextField')) self._open_popup() write("Popup", into="Text field") self.assertEqual("Popup", self._get_value('popupTextField')) def test_switch_to_search_text_field(self): write("Main window", into="Text field") self.assertEqual("Main window", TextField("Text field").value) self._open_popup() write("Popup", into="Text field") self.assertEqual("Popup", TextField("Text field").value) switch_to("test_window_handling - Main") self.assertEqual("Main window", TextField("Text field").value) def test_handles_closed_window_gracefully(self): self._open_popup() get_driver().close() is_back_in_main_window = Link("Open popup").exists() self.assertTrue(is_back_in_main_window) def test_switch_to_after_window_closed(self): self._open_popup() get_driver().close() switch_to('test_window_handling - Main') def setUp(self): super().setUp() self.main_window_handle = self.driver.current_window_handle def tearDown(self): for window_handle in self.driver.window_handles: if window_handle != self.main_window_handle: self.driver.switch_to.window(window_handle) self.driver.close() self.driver.switch_to.window(self.main_window_handle) super().tearDown() def _get_value(self, element_id): return self.driver.find_element(By.ID, element_id).get_attribute('value') def _open_popup(self): click("Open popup") wait_until(self._is_in_popup) def _is_in_popup(self): return get_driver().title == 'test_window_handling - Popup' class WindowHandlingOnStartBrowserTest(BrowserAT): def get_page(self): return 'test_window_handling/main_immediate_popup.html' @skipIf(test_browser_name() == 'firefox', 'This test fails on Firefox') def test_switches_to_popup(self): self.assertTrue(Text("In popup.").exists()) ================================================ FILE: tests/api/test_write.py ================================================ from helium import write, TextField from tests.api import BrowserAT class WriteTest(BrowserAT): def get_page(self): return 'test_write.html' def test_write(self): write('Hello World!') self.assertEqual( 'Hello World!', TextField('Autofocus text field').value ) def test_write_into(self): value = 'Hi there!' label = 'Normal text field' write(value, into=label) self.assertEqual(value, TextField(label).value) def test_write_into_text_field_to_right_of(self): value = 'Hi there!' label = 'Normal text field' write(value, into=TextField(to_right_of=label)) self.assertEqual(value, TextField(label).value) def test_write_into_input_type_date(self): label = 'Input type=date' write('23.08.2024', into=TextField(to_right_of=label)) self.assertEqual('2024-08-23', TextField(label).value) def test_write_into_input_type_time(self): label = 'Input type=time' write('1749', into=TextField(to_right_of=label)) self.assertEqual('17:49', TextField(label).value) ================================================ FILE: tests/api/util.py ================================================ from os.path import dirname, join from pathlib import Path from subprocess import Popen, PIPE, STDOUT import os import sys def get_data_file(*rel_path): return join(dirname(__file__), 'data', *rel_path) def get_data_file_url(data_file): return Path(get_data_file(data_file)).as_uri() class InSubProcess: """ Important: You need to call `synchronize_with_parent_process()` in your sub- class's `main` method. """ def __init__(self): self.sub_process = None def __enter__(self): self.sub_process = Popen( ['python', '-m', self.__class__.__module__], stdin=PIPE, stdout=PIPE, stderr=STDOUT, universal_newlines=True, cwd=os.getcwd(), env=os.environ ) self.sub_process.__enter__() self.wait_for_sub_process() def wait_for_sub_process(self): line = self.sub_process.stdout.readline() assert 'Sub process started.\n' == line, \ 'Sub process invocation failed:\n' + \ line + self.sub_process.stdout.read() @classmethod def synchronize_with_parent_process(cls): # Let parent process know we've started: sys.stdout.write('Sub process started.\n') sys.stdout.flush() # Wait until parent process is finished: input('') def __exit__(self, *args): self.sub_process.stdin.write('\n') self.sub_process.stdin.flush() self.sub_process.__exit__(*args) self.sub_process.wait() assert self.sub_process.returncode == 0, \ repr(self.sub_process.returncode) ================================================ FILE: tests/unit/__init__.py ================================================ ================================================ FILE: tests/unit/test__impl/__init__.py ================================================ ================================================ FILE: tests/unit/test__impl/test_selenium_wrappers.py ================================================ from helium._impl.selenium_wrappers import FrameIterator, \ FramesChangedWhileIterating from selenium.common.exceptions import NoSuchFrameException from unittest import TestCase class FrameIteratorTest(TestCase): def test_only_main_frame(self): self.assertEqual([[]], list(FrameIterator(StubWebDriver()))) def test_one_frame(self): driver = StubWebDriver(Frame()) self.assertEqual([[], [0]], list(FrameIterator(driver))) def test_two_frames(self): driver = StubWebDriver(Frame(), Frame()) self.assertEqual([[], [0], [1]], list(FrameIterator(driver))) def test_nested_frame(self): driver = StubWebDriver(Frame(Frame())) self.assertEqual([[], [0], [0, 0]], list(FrameIterator(driver))) def test_complex(self): driver = StubWebDriver(Frame(Frame()), Frame()) self.assertEqual([[], [0], [0, 0], [1]], list(FrameIterator(driver))) def test_disappearing_frame(self): child_frame = Frame() first_frame = Frame(child_frame) driver = StubWebDriver(first_frame) # We allow precisely 2 frame switches: One to first_frame and one to # child_frame. After this, FrameIterator tries to switch back to # first_frame, to see whether it has other children besides child_frame. # This is where we raise a NoSuchFrameException (by limiting the num. # of frame switches to 2). This simulates a situation where first_frame # disappears during iteration. driver.switch_to = TargetLocatorFailingAfterNFrameSwitches(driver, 2) with self.assertRaises(FramesChangedWhileIterating): list(FrameIterator(driver)) class StubWebDriver: def __init__(self, *frames): self.frames = list(frames) self.switch_to = StubTargetLocator(self) self.current_frame = None class StubTargetLocator: def __init__(self, driver): self.driver = driver def default_content(self): self.driver.current_frame = None def frame(self, index): if self.driver.current_frame is None: children = self.driver.frames else: children = self.driver.current_frame.children try: new_frame = children[index] except IndexError: raise NoSuchFrameException() else: self.driver.current_frame = new_frame class Frame: def __init__(self, *children): self.children = children class TargetLocatorFailingAfterNFrameSwitches(StubTargetLocator): def __init__(self, driver, num_allowed_frame_switches): super(TargetLocatorFailingAfterNFrameSwitches, self).__init__(driver) self.num_allowed_frame_switches = num_allowed_frame_switches def frame(self, index): if self.num_allowed_frame_switches > 0: self.num_allowed_frame_switches -= 1 return super(TargetLocatorFailingAfterNFrameSwitches, self)\ .frame(index) raise NoSuchFrameException() ================================================ FILE: tests/unit/test__impl/test_util/__init__.py ================================================ ================================================ FILE: tests/unit/test__impl/test_util/test_dictionary.py ================================================ from helium._impl.util.dictionary import inverse from unittest import TestCase class InverseTest(TestCase): def test_inverse_empty(self): self.assertEqual({}, inverse({})) def test_inverse(self): names_for_ints = { 0: {"zero", "naught"}, 1: {"one"} } ints_for_names = { "zero": {0}, "naught" : {0}, "one": {1} } self.assertEqual(ints_for_names, inverse(names_for_ints)) ================================================ FILE: tests/unit/test__impl/test_util/test_html.py ================================================ from unittest import TestCase from helium._impl.util.html import normalize_whitespace, \ get_easily_readable_snippet class GetEasilyReadableSnippetTest(TestCase): def test_no_tag(self): self.assertEqual( 'Hello World!', get_easily_readable_snippet('Hello World!') ) def test_completely_empty_tag(self): self.assertEqual('<>', get_easily_readable_snippet('<>')) def test_empty_tag_with_attributes(self): empty_tag_with_attrs = \ '' self.assertEqual( empty_tag_with_attrs, get_easily_readable_snippet(empty_tag_with_attrs) ) def test_tag_with_nested_tags(self): self.assertEqual( '...', get_easily_readable_snippet('

      Hello World!

      ') ) def test_tag_with_long_content(self): tag_with_long_content = '%s' % ('x' * 100) self.assertEqual( '...', get_easily_readable_snippet(tag_with_long_content) ) class NormalizeWhitespaceTest(TestCase): def test_string_without_whitespace(self): self.assertEqual('Foo', normalize_whitespace('Foo')) def test_string_one_whitespace(self): self.assertEqual('Hello World!', normalize_whitespace('Hello World!')) def test_string_leading_whitespace(self): self.assertEqual('Hello World!', normalize_whitespace(' Hello World!')) def test_string_complex_whitespace(self): self.assertEqual( 'Hello World!', normalize_whitespace('\n\t Hello\t\t World! \n') ) def test_tag_with_spaces_around_inner_html(self): self.assertEqual( 'Hi there!', normalize_whitespace(' Hi there! ') ) ================================================ FILE: tests/unit/test__impl/test_util/test_xpath.py ================================================ from helium._impl.util.xpath import predicate_or from unittest import TestCase class PredicateOrTest(TestCase): def test_no_args(self): self.assertEqual('', predicate_or()) def test_one_arg(self): self.assertEqual('[a=b]', predicate_or('a=b')) def test_two_args(self): self.assertEqual('[a=b or c=d]', predicate_or('a=b', 'c=d')) def test_one_empty_arg(self): self.assertEqual('', predicate_or('')) def test_empty_arg_among_normal_args(self): self.assertEqual('[a=b or c=d]', predicate_or('a=b', '', 'c=d'))