├── .bettercodehub.yml ├── .deepsource.toml ├── .githooks ├── README.md └── pre-commit ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── dependabot.yml ├── release-drafter.yml ├── stale.yml └── workflows │ ├── broken-links-crawler.yml │ ├── codeql-analysis.yml │ ├── release-drafter.yml │ ├── run-tests.yml │ └── update-pypi.yml ├── .gitignore ├── .mergify.yml ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── _config.yml ├── dependabot.yml ├── docs ├── DesktopLibraryDocumentation.html ├── MobileLibraryDocumentation.html ├── README.md ├── _config.yml └── generate_docs.py ├── requirements.txt ├── samples ├── 2.png ├── Appium-DesktopTests.robot ├── MobileTests.robot └── WinAppDriver-DesktopTests.robot ├── setup.cfg ├── setup.py ├── src ├── ApplicationLibrary │ ├── DesktopLibrary.py │ ├── Helpers │ │ ├── AppiumCommon.py │ │ └── __init__.py │ ├── MobileLibrary.py │ └── __init__.py └── __init__.py ├── test ├── Desktop │ └── test_desktop.py ├── Helpers │ ├── demo_app.apk │ └── webdriverremotemock.py ├── Mobile │ ├── AndroidMobileTests.robot │ ├── iOSMobileTests.robot │ └── test_mobile.py └── __init__.py └── version.py /.bettercodehub.yml: -------------------------------------------------------------------------------- 1 | component_depth: 3 2 | languages: 3 | - python 4 | -------------------------------------------------------------------------------- /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | test_patterns = [ 4 | "*/test/*/test_*.py" 5 | ] 6 | 7 | [[analyzers]] 8 | name = "python" 9 | enabled = true 10 | 11 | [analyzers.meta] 12 | runtime_version = "3.x.x" 13 | -------------------------------------------------------------------------------- /.githooks/README.md: -------------------------------------------------------------------------------- 1 | Using Git Hooks 2 | =========== 3 | Simply move the file(s) to your local robotframework-applicationlibrary/.git/hooks folder. 4 | 5 | Pre-Commit 6 | ------------- 7 | This file will regenerate the documentation pages and add them to your commit. 8 | -------------------------------------------------------------------------------- /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | python docs/generate_docs.py 3 | git add docs/DesktopLibraryDocumentation.html 4 | git add docs/MobileLibraryDocumentation.html 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug Report 2 | description: File a bug report 3 | title: "[🐛 Bug]: " 4 | labels: [bug, needs-investigation] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thank you for taking the time to file a bug report! 10 | - type: textarea 11 | id: what-happened 12 | attributes: 13 | label: What happened? 14 | description: | 15 | Describe clearly and concisely the bug including instructions showing how to reproduce it. 16 | placeholder: | 17 | Please add as many details as possible to avoid assumptions from our side. How do you 18 | trigger this bug? What did you expect to happen? Please walk us through it step by step. 19 | validations: 20 | required: true 21 | - type: textarea 22 | id: logs 23 | attributes: 24 | label: Relevant log output 25 | description: | 26 | Please copy and paste any relevant log output. This will be automatically formatted 27 | into code, so no need for backticks. 28 | render: shell 29 | validations: 30 | required: true 31 | - type: input 32 | id: operating-system 33 | attributes: 34 | label: Operating System 35 | description: What host operating system are you using to run RobotFramework-ApplicationLibrary? 36 | placeholder: Windows 10? macOS BigSur? Ubuntu? 37 | validations: 38 | required: true 39 | - type: input 40 | id: version 41 | attributes: 42 | label: RobotFramework-ApplicationLibrary version (tag) 43 | description: What version of RobotFramework-ApplicationLibrary are you using? 44 | placeholder: 1.0.1? Please use the full tag, avoid "latest" 45 | validations: 46 | required: true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 💬 General Robot Framework Support and Questions 4 | url: https://forum.robotframework.org/ 5 | about: The Robot Framework Forum is a great place to ask general questions or get support on an issue. 6 | - name: 📖 Application Library Documentation 7 | url: https://accruent.github.io/robotframework-applicationlibrary/ 8 | about: Links to keyword documentation as well as various feature examples. 9 | - name: 📖 Application Library Sample Code 10 | url: https://github.com/Accruent/robotframework-applicationlibrary/tree/master/samples 11 | about: Sample code directory with examples of each library in action. 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Feature Request 2 | description: Propose a new feature or enhancement 3 | title: "[🚀 Feature]: " 4 | labels: [feature, needs-investigation] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thank you for taking the time to help us improve robotframework-applicationlibrary! 10 | - type: textarea 11 | id: feature-description 12 | attributes: 13 | label: Feature and motivation 14 | description: | 15 | Describe clearly and concisely the feature you are proposing, what is the motivation 16 | behind it. 17 | placeholder: | 18 | Help us to understand your proposal by adding as many details as possible, we will look into 19 | it and give you feedback as soon as possible. 20 | validations: 21 | required: true 22 | - type: textarea 23 | id: feature-example 24 | attributes: 25 | label: Usage example 26 | description: | 27 | How would you use this feature? 28 | placeholder: | 29 | A clear example showing how this feature is useful for you and the robotframework-applicationlibrary community. 30 | validations: 31 | required: true -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "09:00" 8 | timezone: US/Central 9 | open-pull-requests-limit: 10 10 | reviewers: 11 | - Wolfe1 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: daily 16 | time: "09:00" 17 | timezone: US/Central 18 | open-pull-requests-limit: 10 19 | reviewers: 20 | - Wolfe1 21 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | categories: 2 | - title: '🚀 Features' 3 | labels: 4 | - 'feature' 5 | - 'enhancement' 6 | - title: '🐛 Bug Fixes' 7 | labels: 8 | - 'fix' 9 | - 'bugfix' 10 | - 'bug' 11 | - title: '🧰 Maintenance' 12 | labels: 13 | - 'dependencies' 14 | - 'maintenance' 15 | - 'docs' 16 | - 'tech debt' 17 | - 'security fix' 18 | - 'security vulnerability' 19 | template: | 20 | ## What’s Changed 21 | $CHANGES 22 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 120 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 60 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.github/workflows/broken-links-crawler.yml: -------------------------------------------------------------------------------- 1 | name: Broken Links Crawler 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | linkChecker: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Link Checker 15 | id: lc 16 | uses: peter-evans/link-checker@v1 17 | with: 18 | args: --exclude github.com -v -r * 19 | - name: Fail if there were link errors 20 | run: exit ${{ steps.lc.outputs.exit_code }} 21 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "Code scanning - action" 2 | 3 | on: 4 | pull_request: 5 | pull_request_target: 6 | types: [labeled] 7 | schedule: 8 | - cron: '0 7 * * 1' 9 | 10 | jobs: 11 | CodeQL-Build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v4 18 | with: 19 | # We must fetch at least the immediate parents so that if this is 20 | # a pull request then we can checkout the head. 21 | fetch-depth: 2 22 | 23 | # Initializes the CodeQL tools for scanning. 24 | - name: Initialize CodeQL 25 | uses: github/codeql-action/init@v3 26 | # Override language selection by uncommenting this and choosing your languages 27 | # with: 28 | # languages: go, javascript, csharp, python, cpp, java 29 | 30 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 31 | # If this step fails, then you should remove it and run the build manually (see below) 32 | - name: Autobuild 33 | uses: github/codeql-action/autobuild@v3 34 | 35 | # ℹ️ Command-line programs to run using the OS shell. 36 | # 📚 https://git.io/JvXDl 37 | 38 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 39 | # and modify them (or add more) to build your code if your project 40 | # uses a compiled language 41 | 42 | #- run: | 43 | # make bootstrap 44 | # make release 45 | 46 | - name: Perform CodeQL Analysis 47 | uses: github/codeql-action/analyze@v3 48 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - master 8 | 9 | jobs: 10 | update_release_draft: 11 | runs-on: ubuntu-latest 12 | steps: 13 | # Drafts your next Release notes as Pull Requests are merged into "master" 14 | - uses: release-drafter/release-drafter@v6.0.0 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | jobs: 13 | upload-demo-apps: 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 120 16 | steps: 17 | - uses: actions/checkout@v4 18 | - shell: bash 19 | env: 20 | SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} 21 | SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} 22 | working-directory: test/Helpers 23 | working-directory: ${{env.working-directory}} 24 | run: | 25 | curl -F "payload=@./demo_app.apk" -F name=demo_app.apk -u "$SAUCE_USERNAME":"$SAUCE_ACCESS_KEY" "https://api.us-west-1.saucelabs.com/v1/storage/upload" 26 | 27 | run-tests-mobile: 28 | runs-on: ubuntu-latest 29 | strategy: 30 | matrix: 31 | python-version: [ 3.7, 3.8, 3.9, '3.10' ] 32 | steps: 33 | - uses: actions/checkout@v4 34 | - name: Set up Python ${{ matrix.python-version }} 35 | uses: actions/setup-python@v5.1.0 36 | with: 37 | python-version: ${{ matrix.python-version }} 38 | 39 | - name: Install dependencies 40 | run: | 41 | python -m pip install --upgrade pip 42 | pip install . 43 | pip install mock 44 | sudo apt-get update 45 | 46 | - name: Run Mobile Robot Tests 47 | uses: nick-fields/retry@v3.0.0 48 | with: 49 | timeout_minutes: 45 50 | max_attempts: 3 51 | command: robot --randomize suites -i Mobile -e BrokenORWindowsOREdge -v sauce_username:${{ secrets.SAUCE_USERNAME }} -v sauce_key:${{ secrets.SAUCE_ACCESS_KEY }} -v GITHUB_RUN_NUMBER:Github.ApplicationLibrary.$GITHUB_RUN_NUMBER --output reports/output.xml test 52 | new_command_on_retry: robot --rerunfailedsuites "reports/output.xml" -i Mobile -e BrokenORWindowsOREdge -v sauce_username:${{ secrets.SAUCE_USERNAME }} -v sauce_key:${{ secrets.SAUCE_ACCESS_KEY }} -v GITHUB_RUN_NUMBER:Github.ApplicationLibrary.$GITHUB_RUN_NUMBER --output reports/output.xml test 53 | 54 | - name: Upload test results 55 | uses: actions/upload-artifact@v3 56 | if: always() 57 | with: 58 | name: reports 59 | path: reports 60 | 61 | run-tests-unit: 62 | runs-on: ubuntu-latest 63 | strategy: 64 | matrix: 65 | python-version: [ 3.7, 3.8, 3.9, '3.10' ] 66 | steps: 67 | - uses: actions/checkout@v4 68 | - name: Set up Python ${{ matrix.python-version }} 69 | uses: actions/setup-python@v5.1.0 70 | with: 71 | python-version: ${{ matrix.python-version }} 72 | 73 | - name: Install dependencies 74 | run: | 75 | python -m pip install --upgrade pip 76 | pip install . 77 | pip install coveralls mock pytest-cov 78 | sudo apt-get update 79 | 80 | - name: Run Unit Tests 81 | run: | 82 | pytest --cov=ApplicationLibrary -v 83 | 84 | - name: Coveralls 85 | env: 86 | GITHUB_TOKEN: ${{ secrets.github_token }} 87 | flag-name: run-${{ matrix.python-version }} 88 | parallel: true 89 | run: | 90 | coveralls --service=github 91 | 92 | generate_report: 93 | if: always() 94 | needs: [run-tests-mobile] 95 | runs-on: ubuntu-latest 96 | steps: 97 | - uses: actions/checkout@v4 98 | - name: Download reportsChrome 99 | uses: actions/download-artifact@v4 100 | with: 101 | name: reports 102 | path: reports 103 | - name: Send report to commit 104 | uses: joonvena/robotframework-reporter-action@v2.4 105 | with: 106 | gh_access_token: ${{ secrets.GITHUB_TOKEN }} 107 | show_passed_tests: false 108 | -------------------------------------------------------------------------------- /.github/workflows/update-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python 🐍 distributions 📦 to PyPI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | release: 7 | types: 8 | - published 9 | jobs: 10 | build-and-publish: 11 | name: Build and publish Python 🐍 distributions 📦 to PyPI 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Python 3.8 16 | uses: actions/setup-python@v5.1.0 17 | with: 18 | python-version: 3.8 19 | 20 | - name: Build ApplicationLibrary 21 | run: | 22 | python -m pip install build --user 23 | 24 | - name: Build a binary wheel and a source tarball 25 | run: | 26 | python -m build --sdist --wheel --outdir dist/ . 27 | 28 | - name: Publish distribution 📦 to PyPI 29 | if: startsWith(github.ref, 'refs/tags') 30 | uses: pypa/gh-action-pypi-publish@master 31 | with: 32 | password: ${{ secrets.PYPI_API_TOKEN }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ project files 2 | .idea 3 | MANIFEST 4 | 5 | .cache/* 6 | build/* 7 | dist/* 8 | src/ApplicationLibrary/__pycache__/* 9 | src/robotframework_applicationlibrary.egg-info/PKG-INFO 10 | *.pyc 11 | test/*/*.html 12 | test/*/*.xml 13 | *.log 14 | *.png 15 | src/ApplicationLibrary.egg-info/* 16 | src/robotframework_applicationlibrary.egg-info/* 17 | .vscode/* 18 | log.html 19 | output.xml 20 | report.html 21 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: Automatic merge for dependabot when everything passes 3 | conditions: 4 | - and: 5 | - author~=^dependabot 6 | - check-success-or-neutral=CodeQL-Build 7 | - check-success-or-neutral=run-tests-mobile (3.7) 8 | - check-success-or-neutral=run-tests-mobile (3.8) 9 | - check-success-or-neutral=run-tests-mobile (3.9) 10 | - check-success-or-neutral=run-tests-mobile (3.10) 11 | - check-success-or-neutral=run-tests-unit (3.7) 12 | - check-success-or-neutral=run-tests-unit (3.8) 13 | - check-success-or-neutral=run-tests-unit (3.9) 14 | - check-success-or-neutral=run-tests-unit (3.10) 15 | - check-success-or-neutral=CodeQL 16 | - check-success-or-neutral=CodeFactor 17 | - "check-success-or-neutral=DeepSource: Python" 18 | - check-success-or-neutral=coverage/coveralls 19 | actions: 20 | merge: 21 | method: merge 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contribution Guidelines 2 | ======================= 3 | 4 | .. contents:: 5 | 6 | 7 | Pull Requests 8 | ~~~~~~~~~~~~~~~ 9 | 10 | In order to contribute to the work we've started on the Application libraries please feel free to submit a Pull Request with 11 | documentation of your changes and the reason they are being made. 12 | 13 | Pull Requests should pass all existing Unit Tests before being submitted. 14 | 15 | 16 | Submitting Bugs 17 | ~~~~~~~~~~~~~~~~ 18 | 19 | Bugs can be added to the `Issue Tracker `_ with the label "Bug". Please include 20 | a description of the issue and any relevant code to best help reproduce the issue. 21 | 22 | 23 | Adding Feature Requests 24 | ~~~~~~~~~~~~~~~~ 25 | 26 | If you have a feature or idea that you would like to submit for consideration - feel free to add this to the 27 | `Issue Tracker `_ with the label "Enhancement". 28 | 29 | Of course, you can always submit a Pull Request with the changes as well! 30 | 31 | 32 | General Code Guidelines 33 | ~~~~~~~~~~~~~~~~~ 34 | 35 | In order to best create a consistent library look and feel please follow the following conventions: 36 | 37 | - `PEP-8` python standards 38 | - Documentation strings on any new method including parameters, parameter types, and a short method description. 39 | - General user readability over condensed code 40 | - New methods should have new Unit Tests added to the appropriate test file 41 | 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE version.py requirements.txt docs/README.md 2 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-architect 2 | source: "/docs" 3 | exclude: ["test/", "src/"] 4 | -------------------------------------------------------------------------------- /dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | robotframework-applicationlibrary 2 | =========== 3 | [![PyPI version](https://badge.fury.io/py/robotframework-applicationlibrary.svg)](https://badge.fury.io/py/robotframework-applicationlibrary) 4 | [![Downloads](https://pepy.tech/badge/robotframework-applicationlibrary)](https://pepy.tech/project/robotframework-applicationlibrary) 5 | [![Build Status](https://github.com/Accruent/robotframework-applicationlibrary/workflows/tests/badge.svg?branch=master)](https://github.com/Accruent/robotframework-applicationlibrary/actions?query=workflow%3Atests) 6 | [![Coverage Status](https://coveralls.io/repos/github/Accruent/robotframework-applicationlibrary/badge.svg?branch=master)](https://coveralls.io/github/Accruent/robotframework-applicationlibrary?branch=master) 7 | [![CodeFactor](https://www.codefactor.io/repository/github/accruent/robotframework-applicationlibrary/badge)](https://www.codefactor.io/repository/github/accruent/robotframework-applicationlibrary) 8 | 9 | Archive Note 10 | -------------- 11 | WinAppDriver has been [abandonded by Microsoft for some time now](https://github.com/microsoft/WinAppDriver/issues/1550), leaving many users in the dark with no real reason to continue its use. 12 | 13 | 14 | Introduction 15 | -------------- 16 | 17 | Robotframework-ApplicationLibrary is a collection of libraries spanning Mobile and Windows Desktop (WinAppDriver) automation using [Robot Framework](https://github.com/robotframework/robotframework). 18 | These libraries are and extensions of the existing [AppiumLibrary](https://github.com/serhatbolsu/robotframework-appiumlibrary). ApplicationLibrary extends the functionality 19 | of AppiumLibrary for Mobile app testing and adds support Windows desktop automation. 20 | 21 | In the course of our own automation as a team, we found that out-of-the-box AppiumLibrary did not fit our needs for mobile testing and needed major rework inorder to get it working with WinAppDriver for Desktop testing. 22 | Originally this code was a part of [RobotFramework-Zoomba](https://github.com/Accruent/robotframework-zoomba) but diverging dependency requirements lead to a need for two separate repositories. 23 | 24 | See the **Keyword Documentation** for the [Mobile](https://accruent.github.io/robotframework-applicationlibrary/MobileLibraryDocumentation.html) or [Desktop](https://accruent.github.io/robotframework-applicationlibrary/DesktopLibraryDocumentation.html) libraries for more specific information about the functionality. 25 | 26 | Example tests can be found in the [samples directory](https://github.com/Accruent/robotframework-applicationlibrary/tree/master/samples). 27 | 28 | Some Features of the Library 29 | -------------- 30 | #### [Mobile Library](https://accruent.github.io/robotframework-applicationlibrary/MobileLibraryDocumentation.html): 31 | Extending the [AppiumLibrary](https://github.com/serhatbolsu/robotframework-appiumlibrary) we added some quality of life 'Wait For And' type keywords: 32 | ```robotframework 33 | Wait For And Click Element locator 34 | Wait For And Click Text text 35 | Wait Until Element Contains locator text 36 | ``` 37 | As well as additional features that have yet to be implemented in AppiumLibrary: 38 | ```robotframework 39 | Drag and Drop source_locator target_locator 40 | Drag And Drop By Offset locator x_offset y_offset 41 | Scroll Down To Text text 42 | Scroll Up To Text text 43 | ``` 44 | 45 | #### [Desktop Library](https://accruent.github.io/robotframework-applicationlibrary/DesktopLibraryDocumentation.html): 46 | Also extends [AppiumLibrary](https://github.com/serhatbolsu/robotframework-appiumlibrary) to tailor it Windows desktop automation. This includes enhancements to base keywords such as [Open Application](https://accruent.github.io/robotframework-applicationlibrary/DesktopLibraryDocumentation.html#Open%20Application) or [Click Element](https://accruent.github.io/robotframework-applicationlibrary/DesktopLibraryDocumentation.html#Click%20Element) to perform better for windows. Other notable additions include: 47 | 48 | Start and Stop the WinAppDriver as needed (best used for suite setup/teardown): 49 | ```robotframework 50 | Driver Setup 51 | Driver Teardown 52 | ``` 53 | Easily switching to new windows or the desktop session: 54 | ```robotframework 55 | Switch Application Desktop 56 | Switch Application By Name remote_url new_window_name 57 | ``` 58 | A variety of keywords for controlling the mouse: 59 | ```robotframework 60 | Mouse Over Element locator 61 | Mouse Over and Click Element locator 62 | Mouse over and Context Click Element locator 63 | Mouse Over By Offset x_offset y_offset 64 | ``` 65 | Keywords for dragging and dropping: 66 | ```robotframework 67 | Drag and Drop source_locator target_locator 68 | Drag And Drop By Offset locator x_offset y_offset 69 | ``` 70 | The ability to send key commands to the application: 71 | ```robotframework 72 | Send Keys \\ue00 p \\ue00 73 | Send Keys To Element locator a b c 74 | ``` 75 | Selecting an element from a combobox or a menu: 76 | ```robotframework 77 | Select Element From ComboBox combobox_locator element_locator 78 | Select Elements From Menu locator_1 locator_2 locator_n 79 | Select Elements From Context Menu locator_1 locator_2 locator_n 80 | ``` 81 | 82 | Selecting an element by an image file (Appium v1.18.0 and higher only): 83 | ```robotframework 84 | Wait For And Click Element image=file.png 85 | ``` 86 | 87 | For WebView2 applications we can control both the application view and the Edge browser view: 88 | 89 | rbmzmun3cR 90 | 91 | With the split from [RobotFramework-Zoomba](https://github.com/Accruent/robotframework-zoomba), the support for this exact example won't work in this current code. An example of this [can be found in the samples directory for robotframework-zoomba version 2.14.3 or lower](https://github.com/Accruent/robotframework-zoomba/blob/2.14.3/samples/WebView-DesktopTest.robot). 92 | 93 | Getting Started 94 | ---------------- 95 | 96 | The Application library is easily installed using the [`setup.py`](https://github.com/Accruent/robotframework-applicationlibrary/blob/master/setup.py) file in the home directory. 97 | Simply run the following command to install ApplicationLibrary and it's dependencies: 98 | 99 | ```python 100 | pip install robotframework-applicationlibrary 101 | ``` 102 | 103 | If you decide to pull the repo locally to make contributions or just want to play around with the code 104 | you can install ApplicationLibrary by running the following from the *root directory*: 105 | ```python 106 | pip install . 107 | ``` 108 | 109 | or if you intend to run unit tests: 110 | ```python 111 | pip install .[testing] 112 | ``` 113 | 114 | To access the keywords in the library simply add the following to your robot file settings (depending on what you are testing): 115 | ```python 116 | *** Settings *** 117 | Library ApplicationLibrary.MobileLibrary 118 | Library ApplicationLibrary.DesktopLibrary 119 | ``` 120 | 121 | Additional Setup Information 122 | --------------------------------- 123 | 124 | If you plan to run Mobile automation you will need to have a running appium server. To do so first have [Node](https://nodejs.org/en/download/) 125 | installed and then run the following: 126 | ```python 127 | npm install -g appium 128 | appium 129 | ``` 130 | 131 | To use the `image` locator strategy you will need to install [opencv4nodejs](https://github.com/justadudewhohacks/opencv4nodejs) via the following command: 132 | ```python 133 | npm install -g opencv4nodejs 134 | ``` 135 | 136 | Alternatively [Appium Desktop](https://github.com/appium/appium-desktop/releases) can be used. 137 | 138 | For Windows automation we suggest [installing and using the WinAppDriver](https://github.com/Microsoft/WinAppDriver/releases) without Appium as it seems to be a bit faster and more stable. 139 | 140 | Make sure to [enable developer mode on your system](https://www.howtogeek.com/292914/what-is-developer-mode-in-windows-10/#:~:text=How%20to%20Enable%20Developer%20Mode,be%20put%20into%20Developer%20Mode.) to allow the WinAppDriver to work. 141 | 142 | Examples 143 | ------------ 144 | Example tests can be found in the [samples directory](https://github.com/Accruent/robotframework-applicationlibrary/tree/master/samples). 145 | 146 | The [test directory](https://github.com/Accruent/robotframework-applicationlibrary/tree/master/test) may also contain tests but be aware that these are used for testing releases and may not be as straight forward to use as the ones in the [samples directory](https://github.com/Accruent/robotframework-applicationlibrary/tree/master/samples). 147 | 148 | 149 | Contributing 150 | ----------------- 151 | 152 | To make contributions please refer to the [CONTRIBUTING](https://github.com/Accruent/robotframework-applicationlibrary/blob/master/CONTRIBUTING.rst) guidelines. 153 | 154 | See the [.githooks](https://github.com/Accruent/robotframework-applicationlibrary/tree/master/.githooks) directory for scripts to help in development. 155 | 156 | Support 157 | --------------- 158 | General Robot Framework questions should be directed to the [community forum](https://forum.robotframework.org/). 159 | 160 | For questions and issues specific to ApplicationLibrary please create an [issue](https://github.com/Accruent/robotframework-applicationlibrary/issues) here on Github. 161 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-architect -------------------------------------------------------------------------------- /docs/generate_docs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from robot import libdoc 4 | sys.path.append(os.path.join(os.path.dirname(sys.path[0]))) 5 | import version 6 | 7 | print("Generating new documentation...") 8 | libraries = ['DesktopLibrary.py', 'MobileLibrary.py'] 9 | curr_dir = os.path.dirname(os.path.realpath(__file__)).replace('docs', '') 10 | for file in libraries: 11 | libdoc.libdoc(curr_dir+'src/ApplicationLibrary/'+file, curr_dir+'docs/'+file.replace('.py', 'Documentation.html'), 12 | name=file.replace(".py", "").replace("Lib", "_Lib"), version=version.VERSION) 13 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | robotframework>=5.0 2 | selenium==3.141.0 3 | robotframework-appiumlibrary==1.6.4 4 | psutil>=5.6.0 5 | Appium-Python-Client==1.3.0 6 | urllib3<=1.26.16 7 | -------------------------------------------------------------------------------- /samples/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Accruent/robotframework-applicationlibrary/93f33e31dc7dd1c10a885dce91861aa20f140e5b/samples/2.png -------------------------------------------------------------------------------- /samples/Appium-DesktopTests.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Documentation ApplicationLibrary Desktop Library Tests. 3 | Library ApplicationLibrary.DesktopLibrary 4 | Suite Setup Start App 5 | Test Setup Launch Application 6 | Test Teardown Quit Application 7 | Suite Teardown Close All Applications 8 | Force Tags Windows 9 | 10 | *** Variables *** 11 | ${REMOTE_URL} http://127.0.0.1:4723/wd/hub 12 | ${APP} Microsoft.WindowsCalculator_8wekyb3d8bbwe!App 13 | ${Notepad} C:/Windows/System32/notepad.exe 14 | 15 | *** Keywords *** 16 | Start App 17 | [Documentation] Sets up the application for quick launching through 'Launch Application' 18 | Open Application ${REMOTE_URL} platformName=Windows deviceName=Windows app=${APP} alias=Main 19 | Maximize Window 20 | Quit Application 21 | 22 | *** Test Cases *** 23 | Wait For And Click Element By Image Keyword Test 24 | Wait For And Click Element image=2.png 25 | Wait Until Element Contains accessibility_id=CalculatorResults 2 26 | 27 | Wait For And Click Element By Id Keyword Test 28 | Wait For And Click Element accessibility_id=num2Button 29 | Wait Until Element Contains accessibility_id=CalculatorResults 2 30 | 31 | Wait For And Click Element By Xpath Keyword Test 32 | Wait For And Click Element xpath=//Button[@Name="Two"] 33 | Wait Until Element Contains accessibility_id=CalculatorResults 2 34 | 35 | Wait For And Click Element By Name Keyword Test 36 | Wait For And Click Element name=Two 37 | Wait Until Element Contains accessibility_id=CalculatorResults 2 38 | 39 | Wait For And Click Element By Class Keyword Test 40 | Wait For And Click Element class=Button 41 | 42 | Wait For And Input Text By Id Keyword Test 43 | Wait For And Input Text accessibility_id=CalculatorResults 12345 44 | Wait Until Element Contains accessibility_id=CalculatorResults 12,345 45 | 46 | Wait For And Input Text By Name Keyword Test 47 | Wait For And Input Text name=Display is 0 12345 48 | Wait Until Element Contains accessibility_id=CalculatorResults 12,345 49 | 50 | Wait For And Long Press Keyword Test 51 | Wait For And Long Press accessibility_id=num2Button 52 | Wait Until Element Contains accessibility_id=CalculatorResults 2 53 | 54 | Wait For And Input Password Keyword Test 55 | Wait For And Input Password accessibility_id=CalculatorResults 12345 56 | Wait Until Element Contains accessibility_id=CalculatorResults 12,345 57 | Wait Until Element Does Not Contain accessibility_id=CalculatorResults 0 58 | 59 | Wait Until Element is Enabled / Disabled Keyword Test 60 | Wait Until Element Is Enabled accessibility_id=MemPlus 61 | Wait Until Element Is Disabled accessibility_id=MemRecall 62 | 63 | Click Webelement Test 64 | ${element} Get Webelement name=One 65 | Click Element ${element} 66 | 67 | Mouse Over Element Keyword Test 68 | Mouse Over Element name=Two 69 | 70 | Mouse Over And Click Element Keyword Test 71 | Mouse Over And Click Element name=Two 72 | Mouse Over And Click Element name=Two x_offset=400 y_offset=100 73 | Wait Until Element Contains accessibility_id=CalculatorResults 23 74 | 75 | Mouse Over And Context Click Element Keyword Test 76 | Mouse Over And Context Click Element name=Two 77 | 78 | Mouse Over And Double Click Element Keyword Test 79 | Mouse Over And Click Element name=Two double_click=True 80 | 81 | Wait For And Mouse Over And Click Element Keyword Test 82 | Wait For And Mouse Over And Click Element name=Two 83 | 84 | Mouse Over by Offset Keyword Test 85 | Mouse Over Element name=Three 86 | Mouse Over By Offset 100 -200 87 | 88 | Click A Point Keyword Test 89 | Mouse Over Element name=Three 90 | Click A Point 91 | Click A Point 100 -200 92 | Click A Point double_click=True 93 | 94 | Context Click A Point Keyword Test 95 | Mouse Over Element name=Three 96 | Context Click A Point 97 | Context Click A Point 100 -200 98 | 99 | Send Keys Keyword Test 100 | Send Keys 24 \ue025 2 \ue007 101 | Page Should Contain Text 26 102 | 103 | Send Keys with Modifier (Alt + 2) 104 | Send Keys \ue00A 2 \ue00A 105 | Wait Until Page Contains Element Name=Scientific Calculator mode 106 | Send Keys \ue00A 1 \ue00A 107 | Wait Until Page Contains Element Name=Standard Calculator mode 108 | 109 | Send Keys To Element Keyword Test 110 | Send Keys To Element name=Display is 0 24 \ue025 2 \ue007 111 | Page Should Contain Text 26 112 | 113 | Save Selenium Screenshot Test 114 | ${file1}= Save Appium Screenshot 115 | ${file2}= Save Appium Screenshot 116 | Should Not Be Equal ${file1} ${file2} 117 | Should Match Regexp ${file1} appium-screenshot-\\d{10}.\\d{0,8}-\\d.png 118 | 119 | Xpath Should Match X Times 120 | Xpath Should Match X Times //Group[@Name="Number pad"][@AutomationId="NumberPad"]/Button 11 121 | 122 | Select Element From Combobox Test 123 | Select Element From ComboBox accessibility_id=TogglePaneButton accessibility_id=Speed 124 | Wait Until Page Contains Element accessibility_id=TogglePaneButton 125 | Select Element From ComboBox accessibility_id=Units1 name=Knots 126 | Wait Until Page Contains Element accessibility_id=TogglePaneButton 127 | Select Element From ComboBox accessibility_id=TogglePaneButton accessibility_id=Standard 128 | 129 | Select Elements From Menu Test 130 | Select Elements From Menu name=Two name=Three name=Four 131 | Wait Until Element Contains accessibility_id=CalculatorResults 234 132 | 133 | Drag And Drop By Touch Tests 134 | drag and drop by touch accessibility_id=AppName name=Five 135 | drag and drop by touch offset accessibility_id=AppName 100 100 136 | Maximize Window 137 | 138 | Tap Tests 139 | Tap name=Five 140 | Wait For And Tap name=Five 141 | Double Tap name=Five 142 | Wait For And Double Tap name=Five 143 | Wait Until Element Contains accessibility_id=CalculatorResults 555,555 144 | 145 | Flick Tests 146 | Select Element From ComboBox accessibility_id=TogglePaneButton accessibility_id=Graphing 147 | Wait Until Page Contains Element accessibility_id=TogglePaneButton 148 | Flick 100 100 149 | Wait For And Click Element accessibility_id=TogglePaneButton 150 | Flick From Element accessibility_id=Standard 0 -100 10 151 | Wait For And Flick From Element accessibility_id=Standard 0 -100 10 152 | Wait Until Page Contains Element accessibility_id=Standard 153 | Wait For And Click Element accessibility_id=Standard 154 | 155 | Screen Recording Keyword Test - See video with logs or in log.html 156 | Start Screen Recording 157 | Wait For And Click Element accessibility_id=num2Button 158 | Wait For And Click Element accessibility_id=num3Button 159 | Wait For And Click Element accessibility_id=num4Button 160 | Wait For And Click Element accessibility_id=num5Button 161 | Wait Until Element Contains accessibility_id=CalculatorResults 2,345 162 | Stop Screen Recording 163 | 164 | Switch Application By Name or Locator 165 | # Select Window by Class Name 166 | Open Application ${REMOTE_URL} platformName=Windows deviceName=Windows app=${Notepad} 167 | Switch Application Main 168 | Switch Application By Locator ${REMOTE_URL} class=Notepad 169 | Wait For And Input Text Name=Text Editor test 170 | Wait Until Element Contains Name=Text Editor test 171 | Quit Application 172 | Wait For And Click Element Name=Don't Save 173 | # Select Window by Xpath 174 | Open Application ${REMOTE_URL} platformName=Windows deviceName=Windows app=${Notepad} 175 | Switch Application Main 176 | Switch Application By Locator ${REMOTE_URL} //Window[contains(@Name, "Notepad")] 177 | Wait For And Input Text Name=Text Editor test 178 | Wait Until Element Contains Name=Text Editor test 179 | Quit Application 180 | Wait For And Click Element Name=Don't Save 181 | # Select Window by Name 182 | Open Application ${REMOTE_URL} platformName=Windows deviceName=Windows app=${Notepad} 183 | Switch Application Main 184 | Switch Application By Name ${REMOTE_URL} Untitled - Notepad 185 | Wait For And Input Text Name=Text Editor test 186 | Wait Until Element Contains Name=Text Editor test 187 | Quit Application 188 | Wait For And Click Element Name=Don't Save 189 | # Select Window by Partial Name 190 | Open Application ${REMOTE_URL} platformName=Windows deviceName=Windows app=${Notepad} 191 | Switch Application Main 192 | Switch Application By Name ${REMOTE_URL} Notepad exact_match=False 193 | Wait For And Input Text Name=Text Editor test 194 | Wait Until Element Contains Name=Text Editor test 195 | Quit Application 196 | Wait For And Click Element Name=Don't Save 197 | # Switch back to the main window to make sure it gets closed 198 | Switch Application Main 199 | 200 | Switch Application Multiple Desktop 201 | # Open Application creates a new desktop session for this isnatnce, useful when running tests on multiple computers 202 | Open Application ${REMOTE_URL} platformName=Windows deviceName=Windows app=${Notepad} alias=Notepad desktop_alias=Desktop2 203 | Switch Application Desktop 204 | Switch Application Desktop2 205 | Switch Application Notepad desktop_alias=Desktop 206 | Quit Application 207 | # Switch back to the main window to make sure it gets closed 208 | Switch Application Main 209 | 210 | Switch To Desktop Test 211 | [Setup] NONE 212 | Switch Application Desktop 213 | Wait For And Click Element name=Start 214 | Wait For And Click Element name=Start 215 | -------------------------------------------------------------------------------- /samples/MobileTests.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Documentation ApplicationLibrary Mobile Library Tests. Requires Appium Server running on port 4723 with an Android device/emulator available. 3 | Library ApplicationLibrary.MobileLibrary 4 | Suite Setup Start App 5 | Test Setup Reset App 6 | Suite Teardown Close Application 7 | Force Tags Mobile 8 | 9 | *** Variables *** 10 | ${REMOTE_URL} http://localhost:4723/wd/hub 11 | ${APP} ${CURDIR}${/}..\\test\\Helpers\\demo_app.apk 12 | ${commandTimeout}= 120 13 | 14 | *** Keywords *** 15 | Start App 16 | Open Application ${REMOTE_URL} platformName=Android automationName=UiAutomator2 deviceName=Android 17 | ... newCommandTimeout=${commandTimeout} app=${APP} 18 | 19 | Reset App 20 | Reset Application 21 | Wait For And Click Element com.touchboarder.android.api.demos:id/buttonDefaultPositive 22 | Wait Until Page Contains API Demos 23 | 24 | *** Test Cases *** 25 | Wait For And Click Element By Accessibility Id Keyword Test 26 | Wait For And Click Element accessibility_id=More options 27 | Wait Until Page Contains Change Log 28 | 29 | Wait For And Click Element By Xpath Keyword Test 30 | Wait For And Click Element //hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.view.ViewGroup/android.widget.FrameLayout[2]/android.widget.RelativeLayout/android.widget.ListView/android.widget.TextView[2] 31 | Wait Until Page Contains Animation 32 | 33 | Wait For And Input Text Keyword Test 34 | Wait For And Click Element accessibility_id=Search 35 | Wait For And Input Text com.touchboarder.android.api.demos:id/search_src_text drag and drop 36 | Wait Until Page Contains Views/Drag and Drop 37 | 38 | Wait For And Long Press Keyword Test 39 | Wait For And Long Press //hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.view.ViewGroup/android.widget.FrameLayout[2]/android.widget.RelativeLayout/android.widget.ListView/android.widget.TextView[2] 40 | Wait Until Page Contains Animation 41 | 42 | Wait For And Input Password Keyword Test 43 | Wait For And Click Element accessibility_id=Search 44 | Wait For And Input Password com.touchboarder.android.api.demos:id/search_src_text drag and drop 45 | Wait Until Page Contains Views/Drag and Drop 46 | 47 | Wait Until Element is Enabled Keyword Test 48 | Wait Until Element Is Enabled accessibility_id=More options 49 | 50 | Drag and Drop Keyword Test 51 | Wait For And Click Element accessibility_id=Search 52 | Wait For And Input Text com.touchboarder.android.api.demos:id/search_src_text drag 53 | wait for and click text Graphics/Shadow Card Drag 54 | Drag and Drop com.touchboarder.android.api.demos:id/card com.touchboarder.android.api.demos:id/shape_select 55 | 56 | Drag and Drop Keyword Test Failure 57 | Wait For And Click Element accessibility_id=Search 58 | Wait For And Input Text com.touchboarder.android.api.demos:id/search_src_text drag 59 | wait for and click text Graphics/Shadow Card Drag 60 | Run keyword And Expect Error ValueError: Element locator 'Not_a_real_id' did not match any elements. 61 | ... Drag and Drop Not_a_real_id com.touchboarder.android.api.demos:id/shape_select 62 | 63 | Drag and Drop By Offset Keyword Test 64 | Wait For And Click Element accessibility_id=Search 65 | Wait For And Input Text com.touchboarder.android.api.demos:id/search_src_text drag 66 | wait for and click text Graphics/Shadow Card Drag 67 | Drag and Drop By Offset com.touchboarder.android.api.demos:id/card 200 200 68 | 69 | Wait Until Element Contains 70 | Wait For And Click Element accessibility_id=Search 71 | Wait For And Input Text com.touchboarder.android.api.demos:id/search_src_text drag 72 | Wait Until Element Contains //hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.view.ViewGroup/android.widget.FrameLayout[2]/android.widget.RelativeLayout/android.widget.ListView/android.widget.TextView[1] 73 | ... Views/Drag and Drop 74 | 75 | Wait Until Element Does Not Contain 76 | Wait For And Click Element accessibility_id=Search 77 | Wait For And Input Text com.touchboarder.android.api.demos:id/search_src_text drag 78 | Wait Until Element Does Not Contain //hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.view.ViewGroup/android.widget.FrameLayout[2]/android.widget.RelativeLayout/android.widget.ListView/android.widget.TextView[1] 79 | ... Some Other Text 80 | 81 | Scroll To Text Keyword Test 82 | Wait For And Click Text API Demos 83 | Wait For And Click Text Graphics 84 | Scroll Down To Text SensorTest 85 | Scroll Up To Text Arcs 86 | 87 | Wait For And Tap Keyword Test 88 | Wait For And Tap accessibility_id=Search 89 | Wait Until Page Contains com.touchboarder.android.api.demos:id/search_src_text 90 | 91 | Save Selenium Screenshot Test 92 | ${file1}= Save Appium Screenshot 93 | ${file2}= Save Appium Screenshot 94 | Should Not Be Equal ${file1} ${file2} 95 | Should Match Regexp ${file1} appium-screenshot-\\d{8,10}.\\d{6,8}-\\d.png 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /samples/WinAppDriver-DesktopTests.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Documentation ApplicationLibrary Desktop Library Tests. 3 | Library ApplicationLibrary.DesktopLibrary 4 | Suite Setup Start App 5 | Test Setup Launch Application 6 | Test Teardown Quit Application 7 | Suite Teardown Driver Teardown 8 | Force Tags Windows 9 | 10 | *** Variables *** 11 | ${REMOTE_URL} http://127.0.0.1:4723 12 | ${APP} Microsoft.WindowsCalculator_8wekyb3d8bbwe!App 13 | ${Notepad} C:/Windows/System32/notepad.exe 14 | 15 | *** Keywords *** 16 | Start App 17 | [Documentation] Sets up the application for quick launching through 'Launch Application' and starts the winappdriver 18 | Driver Setup 19 | Open Application ${REMOTE_URL} platformName=Windows deviceName=Windows app=${APP} alias=Main 20 | Maximize Window 21 | Quit Application 22 | 23 | *** Test Cases *** 24 | Wait For And Click Element By Id Keyword Test 25 | Wait For And Click Element accessibility_id=num2Button 26 | Wait Until Element Contains accessibility_id=CalculatorResults 2 27 | 28 | Wait For And Click Element By Xpath Keyword Test 29 | Wait For And Click Element xpath=//Button[@Name="Two"] 30 | Wait Until Element Contains accessibility_id=CalculatorResults 2 31 | 32 | Wait For And Click Element By Name Keyword Test 33 | Wait For And Click Element name=Two 34 | Wait Until Element Contains accessibility_id=CalculatorResults 2 35 | 36 | Wait For And Click Element By Class Keyword Test 37 | Wait For And Click Element class=Button 38 | 39 | Wait For And Input Text By Id Keyword Test 40 | Wait For And Input Text accessibility_id=CalculatorResults 12345 41 | Wait Until Element Contains accessibility_id=CalculatorResults 12,345 42 | 43 | Wait For And Input Text By Name Keyword Test 44 | Wait For And Input Text name=Display is 0 12345 45 | Wait Until Element Contains accessibility_id=CalculatorResults 12,345 46 | 47 | Wait For And Long Press Keyword Test 48 | Wait For And Long Press accessibility_id=num2Button 49 | Wait Until Element Contains accessibility_id=CalculatorResults 2 50 | 51 | Wait For And Input Password Keyword Test 52 | Wait For And Input Password accessibility_id=CalculatorResults 12345 53 | Wait Until Element Contains accessibility_id=CalculatorResults 12,345 54 | Wait Until Element Does Not Contain accessibility_id=CalculatorResults 0 55 | 56 | Wait Until Element is Enabled / Disabled Keyword Test 57 | Wait Until Element Is Enabled accessibility_id=MemPlus 58 | Wait Until Element Is Disabled accessibility_id=MemRecall 59 | 60 | Click Webelement Test 61 | ${element} Get Webelement name=One 62 | Click Element ${element} 63 | 64 | Mouse Over Element Keyword Test 65 | Mouse Over Element name=Two 66 | 67 | Mouse Over And Click Element Keyword Test 68 | # This test will fail unless run at 1080p 69 | Mouse Over And Click Element name=Two 70 | Mouse Over And Click Element name=Two x_offset=400 y_offset=100 71 | Wait Until Element Contains accessibility_id=CalculatorResults 23 72 | 73 | Mouse Over And Context Click Element Keyword Test 74 | Mouse Over And Context Click Element name=Two 75 | 76 | Mouse Over And Double Click Element Keyword Test 77 | Mouse Over And Click Element name=Two double_click=True 78 | 79 | Wait For And Mouse Over And Click Element Keyword Test 80 | Wait For And Mouse Over And Click Element name=Two 81 | 82 | Mouse Over by Offset Keyword Test 83 | Mouse Over Element name=Three 84 | Mouse Over By Offset 100 -200 85 | 86 | Click A Point Keyword Test 87 | Mouse Over Element name=Three 88 | Click A Point 89 | Click A Point 100 -200 90 | Click A Point double_click=True 91 | 92 | Context Click A Point Keyword Test 93 | Mouse Over Element name=Three 94 | Context Click A Point 95 | Context Click A Point 100 -200 96 | 97 | Send Keys Keyword Test 98 | Send Keys 24 \ue025 2 \ue007 99 | Page Should Contain Text 26 100 | 101 | Send Keys with Modifier (Alt + 2) 102 | Send Keys \ue00A 2 \ue00A 103 | Wait Until Page Contains Element Name=Scientific Calculator mode 104 | Send Keys \ue00A 1 \ue00A 105 | Wait Until Page Contains Element Name=Standard Calculator mode 106 | 107 | Send Keys To Element Keyword Test 108 | Send Keys To Element name=Display is 0 24 \ue025 2 \ue007 109 | Page Should Contain Text 26 110 | 111 | Save Selenium Screenshot Test 112 | ${file1}= Save Appium Screenshot 113 | ${file2}= Save Appium Screenshot 114 | Should Not Be Equal ${file1} ${file2} 115 | Should Match Regexp ${file1} appium-screenshot-\\d{10}.\\d{0,8}-\\d.png 116 | 117 | Xpath Should Match X Times 118 | Xpath Should Match X Times //Group[@Name="Number pad"][@AutomationId="NumberPad"]/Button 11 119 | 120 | Select Element From Combobox Test 121 | Select Element From ComboBox accessibility_id=TogglePaneButton accessibility_id=Speed 122 | Wait Until Page Contains Element accessibility_id=TogglePaneButton 123 | Select Element From ComboBox accessibility_id=Units1 name=Knots 124 | Wait Until Page Contains Element accessibility_id=TogglePaneButton 125 | Select Element From ComboBox accessibility_id=TogglePaneButton accessibility_id=Standard 126 | 127 | Select Elements From Menu Test 128 | Select Elements From Menu name=Two name=Three name=Four 129 | Wait Until Element Contains accessibility_id=CalculatorResults 234 130 | 131 | Drag And Drop By Touch Tests 132 | drag and drop by touch accessibility_id=AppName name=Five 133 | drag and drop by touch offset accessibility_id=AppName 100 100 134 | Maximize Window 135 | 136 | Tap Tests 137 | Tap name=Five 138 | Wait For And Tap name=Five 139 | Double Tap name=Five 140 | Wait For And Double Tap name=Five 141 | Wait Until Element Contains accessibility_id=CalculatorResults 555,555 142 | 143 | Flick Tests 144 | Select Element From ComboBox accessibility_id=TogglePaneButton accessibility_id=Graphing 145 | Wait Until Page Contains Element accessibility_id=TogglePaneButton 146 | Flick 100 100 147 | Wait For And Click Element accessibility_id=TogglePaneButton 148 | Flick From Element accessibility_id=Standard 0 -100 10 149 | Wait For And Flick From Element accessibility_id=Standard 0 -100 10 150 | Wait Until Page Contains Element accessibility_id=Standard 151 | Wait For And Click Element accessibility_id=Standard 152 | 153 | Switch Application By Name or Locator 154 | # Select Window by Class Name 155 | Open Application ${REMOTE_URL} platformName=Windows deviceName=Windows app=${Notepad} 156 | Switch Application Main 157 | Switch Application By Locator ${REMOTE_URL} class=Notepad 158 | Wait For And Input Text Name=Text Editor test 159 | Wait Until Element Contains Name=Text Editor test 160 | Quit Application 161 | Wait For And Click Element Name=Don't Save 162 | # Select Window by Xpath 163 | Open Application ${REMOTE_URL} platformName=Windows deviceName=Windows app=${Notepad} 164 | Switch Application Main 165 | Switch Application By Locator ${REMOTE_URL} //Window[contains(@Name, "Notepad")] 166 | Wait For And Input Text Name=Text Editor test 167 | Wait Until Element Contains Name=Text Editor test 168 | Quit Application 169 | Wait For And Click Element Name=Don't Save 170 | # Select Window by Name 171 | Open Application ${REMOTE_URL} platformName=Windows deviceName=Windows app=${Notepad} 172 | Switch Application Main 173 | Switch Application By Name ${REMOTE_URL} Untitled - Notepad 174 | Wait For And Input Text Name=Text Editor test 175 | Wait Until Element Contains Name=Text Editor test 176 | Quit Application 177 | Wait For And Click Element Name=Don't Save 178 | # Select Window by Partial Name 179 | Open Application ${REMOTE_URL} platformName=Windows deviceName=Windows app=${Notepad} 180 | Switch Application Main 181 | Switch Application By Name ${REMOTE_URL} Notepad exact_match=False 182 | Wait For And Input Text Name=Text Editor test 183 | Wait Until Element Contains Name=Text Editor test 184 | Quit Application 185 | Wait For And Click Element Name=Don't Save 186 | # Switch back to the main window to make sure it gets closed 187 | Switch Application Main 188 | 189 | Switch Application Multiple Desktop 190 | # Open Application creates a new desktop session for this instance, useful when running tests on multiple computers 191 | Open Application ${REMOTE_URL} platformName=Windows deviceName=Windows app=${Notepad} alias=Notepad desktop_alias=Desktop2 192 | Switch Application Desktop 193 | Switch Application Desktop2 194 | Switch Application Notepad desktop_alias=Desktop 195 | Quit Application 196 | # Switch back to the main window to make sure it gets closed 197 | Switch Application Main 198 | 199 | Working Directory Test 200 | [Setup] Open Application ${REMOTE_URL} platformName=Windows deviceName=Windows app=C:/Windows/notepad.exe appWorkingDir=C:/Windows 201 | Switch Application By Name ${REMOTE_URL} Notepad exact_match=False appWorkingDir=C:/Windows/System32 202 | 203 | Switch To Desktop Test 204 | [Setup] Driver Setup 205 | Switch Application Desktop 206 | Wait For And Click Element name=Start 207 | Wait For And Click Element name=Start 208 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | license_files = LICENSE 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import version 4 | 5 | try: 6 | from setuptools import setup 7 | except ImportError: 8 | from distutils.core import setup 9 | 10 | with open("docs/README.md", "r") as fh: 11 | long_description = fh.read() 12 | 13 | with open('requirements.txt') as f: 14 | requirements = f.read().splitlines() 15 | 16 | setup(name='robotframework-applicationlibrary', 17 | version=version.VERSION, 18 | description='Robot Framework framework for mobile and desktop testing.', 19 | long_description=long_description, 20 | long_description_content_type='text/markdown', 21 | url='https://github.com/Accruent/robotframework-applicationlibrary', 22 | maintainer='Brandon Wolfe, Neil Howell, Matt Bozek, Pinky Majhi', 23 | maintainer_email='robosquad@accruent.com', 24 | license='GPL-3.0', 25 | keywords='Robot Framework robot-framework selenium appium winappdriver appium robotframework' 26 | 'desktop windows zoomba python robotframework-library appium-windows appiumlibrary' 27 | 'appium-mobile mobile applicationlibrary application app lib', 28 | platforms='any', 29 | install_requires=requirements, 30 | extras_require={ 31 | 'testing': [ 32 | 'Appium-Python-Client', 33 | 'mock' 34 | ] 35 | }, 36 | classifiers=""" 37 | Development Status :: 5 - Production/Stable 38 | Operating System :: OS Independent 39 | Programming Language :: Python :: 3 40 | Topic :: Software Development :: Testing 41 | Framework :: Robot Framework :: Library 42 | """.strip().splitlines(), 43 | package_dir={'': 'src'}, 44 | packages=['ApplicationLibrary', 'ApplicationLibrary/Helpers'] 45 | ) 46 | -------------------------------------------------------------------------------- /src/ApplicationLibrary/Helpers/AppiumCommon.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | from time import time 3 | from appium.webdriver.common.touch_action import TouchAction 4 | from robot.libraries.BuiltIn import BuiltIn 5 | 6 | SCREENSHOT_COUNTER = itertools.count() 7 | appLib = BuiltIn() 8 | 9 | 10 | def capture_page_screenshot(self, filename=None): 11 | """Takes a screenshot of the current page and embeds it into the log. 12 | 13 | `filename` argument specifies the name of the file to write the 14 | screenshot into. If no `filename` is given, the screenshot is saved into file 15 | `appium-screenshot-.png` under the directory where 16 | the Robot Framework log file is written into. The `filename` is 17 | also considered relative to the same directory, if it is not 18 | given in absolute format. 19 | 20 | `css` can be used to modify how the screenshot is taken. By default 21 | the bakground color is changed to avoid possible problems with 22 | background leaking when the page layout is somehow broken. 23 | 24 | See `Save Appium Screenshot` for a screenshot that will be unique across reports 25 | """ 26 | path, link = self._get_screenshot_paths(filename) 27 | 28 | if hasattr(self._current_application(), 'get_screenshot_as_file'): 29 | self._current_application().get_screenshot_as_file(path) 30 | else: 31 | self._current_application().save_screenshot(path) 32 | 33 | # Image is shown on its own row and thus prev row is closed on purpose 34 | self._html('' 35 | '' % (link, link)) 36 | return link 37 | 38 | 39 | def save_appium_screenshot(self): 40 | """Takes a screenshot with a unique filename to be stored 41 | in Robot Framework compiled reports.""" 42 | timestamp = time() 43 | filename = 'appium-screenshot-' + str(timestamp) + '-' + str(next(SCREENSHOT_COUNTER)) + '.png' 44 | return self.capture_page_screenshot(filename) 45 | 46 | 47 | def wait_until_page_contains(self, text, timeout=None, error=None): 48 | """Internal version to avoid duplicate screenshots""" 49 | if not error: 50 | error = "Text '%s' did not appear in " % text 51 | self._wait_until(timeout, error, self._is_text_present, text) 52 | 53 | 54 | def drag_and_drop(self, source, target, delay=1500): 55 | """Drags the element found with the locator ``source`` to the element found with the 56 | locator ``target``. 57 | 58 | ``Delay`` (iOS Only): Delay between initial button press and dragging, defaults to 1500ms.""" 59 | source_element = self._element_find(source, True, True) 60 | target_element = self._element_find(target, True, True) 61 | appLib.log('Dragging source element "%s" to target element "%s".' % (source, target)) 62 | actions = TouchAction(self._current_application()) 63 | self._platform_dependant_press(actions, source_element, delay) 64 | actions.move_to(target_element) 65 | actions.release().perform() 66 | 67 | 68 | def drag_and_drop_by_offset(self, locator, x_offset=0, y_offset=0, delay=1500): 69 | """Drags the element found with ``locator`` to the given ``x_offset`` and ``y_offset`` 70 | coordinates. 71 | 72 | ``Delay`` (iOS Only): Delay between initial button press and dragging, defaults to 1500ms.""" 73 | element = self._element_find(locator, True, True) 74 | appLib.log('Dragging element "%s" by offset (%s, %s).' % (locator, x_offset, y_offset)) 75 | x_center = element.location['x'] + element.size['width'] / 2 76 | y_center = element.location['y'] + element.size['height'] / 2 77 | actions = TouchAction(self._current_application()) 78 | self._platform_dependant_press(actions, element, delay) 79 | actions.move_to(x=x_center + x_offset, y=y_center + y_offset) 80 | actions.release().perform() 81 | 82 | 83 | def _platform_dependant_press(self, actions, element, delay): 84 | if self._is_ios(): 85 | actions.long_press(element, duration=2000) 86 | actions.wait(delay) 87 | else: 88 | actions.press(element) 89 | -------------------------------------------------------------------------------- /src/ApplicationLibrary/Helpers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Accruent/robotframework-applicationlibrary/93f33e31dc7dd1c10a885dce91861aa20f140e5b/src/ApplicationLibrary/Helpers/__init__.py -------------------------------------------------------------------------------- /src/ApplicationLibrary/MobileLibrary.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | from AppiumLibrary import AppiumLibrary 3 | from robot.api.deco import keyword 4 | from robot.libraries.BuiltIn import BuiltIn 5 | 6 | try: 7 | AppiumCommon = importlib.import_module('Helpers.AppiumCommon', package='Helpers') 8 | except ModuleNotFoundError: 9 | AppiumCommon = importlib.import_module('.Helpers.AppiumCommon', package='ApplicationLibrary') 10 | 11 | appLib = BuiltIn() 12 | 13 | 14 | class MobileLibrary(AppiumLibrary): 15 | """ApplicationLibrary Mobile Library 16 | 17 | This class is the base Library used to generate automated Mobile Tests in the Robot Automation Framework using 18 | Appium. This Library uses and extends the robotframework-appiumlibrary. 19 | 20 | = Locating or Specifying Elements = 21 | 22 | All keywords in MobileLibrary that need to find an element on the page take an argument, either a 23 | ``locator`` or a ``webelement``. ``locator`` is a string that describes how to locate an element using a syntax 24 | specifying different location strategies. ``webelement`` is a variable that 25 | holds a WebElement instance, which is a representation of the element. 26 | 27 | == Using locators == 28 | 29 | By default, when a locator is provided, it is matched against the key attributes 30 | of the particular element type. For iOS and Android, key attribute is ``id`` for 31 | all elements and locating elements is easy using just the ``id``. For example: 32 | 33 | | Click Element id=my_element 34 | 35 | ``id`` and ``xpath`` are not required to be specified, 36 | however ``xpath`` should start with ``//`` else just use ``xpath`` locator as explained below. 37 | 38 | For example: 39 | 40 | | Click Element my_element 41 | | Wait Until Page Contains Element //*[@type="android.widget.EditText"] 42 | 43 | 44 | Appium additionally supports some of the [https://w3c.github.io/webdriver/webdriver-spec.html|Mobile JSON Wire Protocol] locator strategies. 45 | It is also possible to specify the approach MobileLibrary should take 46 | to find an element by specifying a lookup strategy with a locator 47 | prefix. Supported strategies are: 48 | 49 | | *Strategy* | *Example* | *Description* | *Note* | 50 | | identifier | Click Element `|` identifier=my_element | Matches by @id attribute | | 51 | | id | Click Element `|` id=my_element | Matches by @resource-id attribute | | 52 | | accessibility_id | Click Element `|` accessibility_id=button3 | Accessibility options utilize. | | 53 | | xpath | Click Element `|` xpath=//UIATableView/UIATableCell/UIAButton | Matches with arbitrary XPath | | 54 | | class | Click Element `|` class=UIAPickerWheel | Matches by class | | 55 | | android | Click Element `|` android=UiSelector().description('Apps') | Matches by Android UI Automator | | 56 | | ios | Click Element `|` ios=.buttons().withName('Apps') | Matches by iOS UI Automation | | 57 | | nsp | Click Element `|` nsp=name=="login" | Matches by iOSNsPredicate | | 58 | | css | Click Element `|` css=.green_button | Matches by css in webview | | 59 | | name | Click Element `|` name=my_element | Matches by @name attribute | *Only valid* for Selendroid | 60 | 61 | == Using webelements == 62 | 63 | One can pass an argument that contains a WebElement instead of a string locator. 64 | To get a WebElement, use the new `Get WebElements` or `Get WebElement` keyword. 65 | 66 | For example: 67 | | @{elements} Get Webelements class=UIAButton 68 | | Click Element @{elements}[2] 69 | """ 70 | 71 | def __init__(self, timeout=5, run_on_failure='Save Appium Screenshot'): 72 | """MobileLibrary can be imported with optional arguments. 73 | ``timeout`` is the default timeout used to wait for all waiting actions. 74 | It can be later set with `Set Appium Timeout`. 75 | ``run_on_failure`` specifies the name of a keyword (from any available 76 | libraries) to execute when a MobileLibrary keyword fails. 77 | By default `Save Appium Screenshot` will be used to take a screenshot of the current page. 78 | Using the value `No Operation` will disable this feature altogether. See 79 | `Register Keyword To Run On Failure` keyword for more information about this 80 | functionality. 81 | Examples: 82 | | Library | MobileLibrary | 10 | # Sets default timeout to 10 seconds | 83 | | Library | MobileLibrary | timeout=10 | run_on_failure=No Operation | # Sets default timeout to 10 seconds and does nothing on failure | 84 | """ 85 | super().__init__(timeout, run_on_failure) 86 | 87 | @keyword("Wait For And Clear Text") 88 | def wait_for_and_clear_text(self, locator, timeout=None, error=None): 89 | """Wait for and then clear the text field identified by ``locator``. 90 | 91 | Fails if ``timeout`` expires before the element appears. 92 | 93 | ``error`` can be used to override the default error message. 94 | 95 | See `introduction` for details about locating elements.""" 96 | self._wait_until_page_contains_element(locator, timeout, error) 97 | self.clear_text(locator) 98 | 99 | @keyword("Wait For And Click Element") 100 | def wait_for_and_click_element(self, locator, timeout=None, error=None): 101 | """Wait for and click the element identified by ``locator``. 102 | 103 | Fails if ``timeout`` expires before the element appears. 104 | 105 | ``error`` can be used to override the default error message. 106 | 107 | See `introduction` for details about locating elements.""" 108 | self._wait_until_page_contains_element(locator, timeout, error) 109 | self.click_element(locator) 110 | 111 | @keyword("Wait For And Click Text") 112 | def wait_for_and_click_text(self, text, exact_match=False, timeout=None, error=None): 113 | """Wait for and click text identified by ``text``. 114 | 115 | Fails if ``timeout`` expires before the element appears. 116 | 117 | ``error`` can be used to override the default error message. 118 | 119 | By default tries to click first text involves given ``text``. If you would 120 | like to click exactly matching text, then set ``exact_match`` to `True`.""" 121 | self._wait_until_page_contains(text, timeout, error) 122 | self.click_text(text, exact_match) 123 | 124 | @keyword("Wait For And Click Button") 125 | def wait_for_and_click_button(self, locator, timeout=None, error=None): 126 | """Wait for and click the button identified by ``locator``. 127 | 128 | Fails if ``timeout`` expires before the element appears. 129 | 130 | ``error`` can be used to override the default error message. 131 | 132 | See `introduction` for details about locating elements.""" 133 | self._wait_until_page_contains_element(locator, timeout, error) 134 | self.click_button(locator) 135 | 136 | @keyword("Wait For And Input Password") 137 | def wait_for_and_input_password(self, locator, text, timeout=None, error=None): 138 | """Wait for and type the given password into the text field identified by ``locator``. 139 | 140 | Fails if ``timeout`` expires before the element appears. 141 | 142 | ``error`` can be used to override the default error message. 143 | 144 | The difference between this keyword and `Wait For And Input Text` is that this keyword 145 | does not log the given password. See `introduction` for details about locating elements.""" 146 | self._wait_until_page_contains_element(locator, timeout, error) 147 | self.input_password(locator, text) 148 | 149 | @keyword("Wait For And Input Text") 150 | def wait_for_and_input_text(self, locator, text, timeout=None, error=None): 151 | """Wait for and type the given ``locator`` into text field identified by ``locator``. 152 | 153 | Fails if ``timeout`` expires before the element appears. 154 | 155 | ``error`` can be used to override the default error message. 156 | 157 | See `introduction` for details about locating elements.""" 158 | self._wait_until_page_contains_element(locator, timeout, error) 159 | self.input_text(locator, text) 160 | 161 | @keyword("Wait For And Input Value") 162 | def wait_for_and_input_value(self, locator, value, timeout=None, error=None): 163 | """Wait for and set the given ``value`` into the text field identified by ``locator``. 164 | This is an IOS only keyword, input value makes use of set_value. 165 | 166 | Fails if ``timeout`` expires before the element appears. 167 | 168 | ``error`` can be used to override the default error message. 169 | 170 | The difference between this keyword and `Wait For And Input Text` is that this keyword 171 | does not log the given password. See `introduction` for details about locating elements.""" 172 | self._wait_until_page_contains_element(locator, timeout, error) 173 | self.input_value(locator, value) 174 | 175 | @keyword("Wait For And Long Press") 176 | def wait_for_and_long_press(self, locator, duration=5000, timeout=None, error=None): 177 | """Wait for and long press the element identified by ``locator`` with optional duration. 178 | 179 | Fails if ``timeout`` expires before the element appears. 180 | 181 | ``error`` can be used to override the default error message. 182 | 183 | See `introduction` for details about locating elements.""" 184 | self._wait_until_page_contains_element(locator, timeout, error) 185 | self.long_press(locator, duration) 186 | 187 | @keyword("Wait Until Element Contains") 188 | def wait_until_element_contains(self, locator, text, timeout=None, error=None): 189 | """Waits until element specified with ``locator`` contains ``text``. 190 | 191 | Fails if ``timeout`` expires before the element appears. 192 | 193 | ``error`` can be used to override the default error message. 194 | 195 | See also `Wait Until Page Contains`, 196 | `Wait Until Page Does Not Contain` 197 | `Wait Until Page Does Not Contain Element` 198 | """ 199 | self._wait_until_page_contains_element(locator, timeout, error) 200 | self.element_should_contain_text(locator, text, error) 201 | 202 | @keyword("Wait Until Element Does Not Contain") 203 | def wait_until_element_does_not_contain(self, locator, text, timeout=None, error=None): 204 | """Waits until element specified with ``locator`` does not contain ``text``. 205 | 206 | Fails if ``timeout`` expires before the element appears. 207 | 208 | ``error`` can be used to override the default error message. 209 | 210 | See also `Wait Until Element Contains`, 211 | `Wait Until Page Contains`, 212 | `Wait Until Page Does Not Contain` 213 | `Wait Until Page Does Not Contain Element` 214 | """ 215 | self._wait_until_page_contains_element(locator, timeout, error) 216 | self.element_should_not_contain_text(locator, text, error) 217 | 218 | @keyword("Wait Until Element Is Enabled") 219 | def wait_until_element_is_enabled(self, locator, timeout=None, error=None): 220 | """Waits until element specified with ``locator`` is enabled. 221 | 222 | Fails if ``timeout`` expires before the element appears. 223 | 224 | ``error`` can be used to override the default error message. 225 | 226 | See also `Wait Until Element Is Disabled` 227 | """ 228 | self._wait_until_page_contains_element(locator, timeout, error) 229 | self.element_should_be_enabled(locator) 230 | 231 | @keyword("Wait Until Element Is Disabled") 232 | def wait_until_element_is_disabled(self, locator, timeout=None, error=None): 233 | """Waits until element specified with ``locator`` is disabled. 234 | 235 | Fails if ``timeout`` expires before the element appears. 236 | 237 | ``error`` can be used to override the default error message. 238 | 239 | See also `Wait Until Element Is Disabled` 240 | """ 241 | self._wait_until_page_contains_element(locator, timeout, error) 242 | self.element_should_be_disabled(locator) 243 | 244 | @keyword("Drag And Drop") 245 | def drag_and_drop(self, source, target, delay=1500): 246 | """Drags the element found with the locator ``source`` to the element found with the 247 | locator ``target``. 248 | 249 | ``Delay`` (iOS Only): Delay between initial button press and dragging, defaults to 1500ms.""" 250 | AppiumCommon.drag_and_drop(self, source, target, delay) 251 | 252 | @keyword("Drag And Drop By Offset") 253 | def drag_and_drop_by_offset(self, locator, x_offset=0, y_offset=0, delay=1500): 254 | """Drags the element found with ``locator`` to the given ``x_offset`` and ``y_offset`` 255 | coordinates. 256 | 257 | ``Delay`` (iOS Only): Delay between initial button press and dragging, defaults to 1500ms.""" 258 | AppiumCommon.drag_and_drop_by_offset(self, locator, x_offset, y_offset, delay) 259 | 260 | @keyword("Scroll Down To Text") 261 | def scroll_down_to_text(self, text, exact_match=False, swipe_count=10): 262 | """Scrolls down to ``text``. 263 | 264 | In some instances the default scroll behavior does not work. In this case the keyword will use small swipes 265 | to find the element. The ``swipe_count`` limits the number of these swipes before the keyword gives up, 266 | defaults to 10.""" 267 | try: 268 | driver = self._current_application() 269 | element = self._element_find_by_text(text, exact_match) 270 | if not self.get_current_context().startswith("NATIVE"): 271 | element._execute("getElementLocationOnceScrolledIntoView") 272 | else: 273 | driver.execute_script("mobile: scroll", {"direction": 'down', 'elementid': element}) 274 | except ValueError: 275 | self._scroll_to_text(text, 'down', swipe_count) 276 | 277 | @keyword("Scroll Up To Text") 278 | def scroll_up_to_text(self, text, exact_match=False, swipe_count=10): 279 | """Scrolls down to ``text``. 280 | 281 | In some instances the default scroll behavior does not work. In this case the keyword will use small swipes 282 | to find the element. The ``swipe_count`` limits the number of these swipes before the keyword gives up, 283 | defaults to 10.""" 284 | try: 285 | driver = self._current_application() 286 | element = self._element_find_by_text(text, exact_match) 287 | if not self.get_current_context().startswith("NATIVE"): 288 | element._execute("getElementLocationOnceScrolledIntoView") 289 | else: 290 | driver.execute_script("mobile: scroll", {"direction": 'up', 'elementid': element}) 291 | except ValueError: 292 | self._scroll_to_text(text, 'up', swipe_count) 293 | 294 | def scroll_down(self, locator): 295 | """Scrolls down to an element identified by ``locator``.""" 296 | driver = self._current_application() 297 | element = self._element_find(locator, True, True) 298 | if not self.get_current_context().startswith("NATIVE"): 299 | element._execute("getElementLocationOnceScrolledIntoView") 300 | else: 301 | driver.execute_script("mobile: scroll", {"toVisible": 'down', 'elementid': element}) 302 | 303 | def scroll_up(self, locator): 304 | """Scrolls up to an element identified by ``locator``.""" 305 | driver = self._current_application() 306 | element = self._element_find(locator, True, True) 307 | if not self.get_current_context().startswith("NATIVE"): 308 | element._execute("getElementLocationOnceScrolledIntoView") 309 | else: 310 | driver.execute_script("mobile: scroll", {"direction": 'up', 'elementid': element}) 311 | 312 | @keyword("Wait For And Tap") 313 | def wait_for_and_tap(self, locator, x_offset=None, y_offset=None, count=1, timeout=None, 314 | error=None): 315 | """ Wait for and then Tap element identified by ``locator``. 316 | Args: 317 | - ``x_offset`` - (optional) x coordinate to tap, relative to the top left corner of the element. 318 | - ``y_offset`` - (optional) y coordinate. If y is used, x must also be set, and vice versa 319 | - ``count`` - can be used to tap multiple times 320 | - ``timeout`` - time in seconds to locate the element, defaults to global timeout 321 | - ``error`` - (optional) used to override the default error message. 322 | """ 323 | self._wait_until_page_contains_element(locator, timeout, error) 324 | self.tap(locator, x_offset, y_offset, count) 325 | 326 | def capture_page_screenshot(self, filename=None): 327 | """Takes a screenshot of the current page and embeds it into the log. 328 | 329 | `filename` argument specifies the name of the file to write the 330 | screenshot into. If no `filename` is given, the screenshot is saved into file 331 | `appium-screenshot-.png` under the directory where 332 | the Robot Framework log file is written into. The `filename` is 333 | also considered relative to the same directory, if it is not 334 | given in absolute format. 335 | 336 | `css` can be used to modify how the screenshot is taken. By default 337 | the bakground color is changed to avoid possible problems with 338 | background leaking when the page layout is somehow broken. 339 | 340 | See `Save Appium Screenshot` for a screenshot that will be unique across reports 341 | """ 342 | return AppiumCommon.capture_page_screenshot(self, filename) 343 | 344 | @keyword("Save Appium Screenshot") 345 | def save_appium_screenshot(self): 346 | """Takes a screenshot with a unique filename to be stored in Robot Framework compiled 347 | reports.""" 348 | return AppiumCommon.save_appium_screenshot(self) 349 | 350 | # Private 351 | def _wait_until_page_contains(self, text, timeout=None, error=None): 352 | """Internal version to avoid duplicate screenshots""" 353 | AppiumCommon.wait_until_page_contains(self, text, timeout, error) 354 | 355 | def _wait_until_page_contains_element(self, locator, timeout=None, error=None): 356 | """Internal version to avoid duplicate screenshots""" 357 | if not error: 358 | error = "Element '%s' did not appear in " % locator 359 | self._wait_until(timeout, error, self._is_element_present, locator) 360 | 361 | def _platform_dependant_press(self, actions, element, delay=1500): 362 | """Decide press action based on platform""" 363 | AppiumCommon._platform_dependant_press(self, actions, element, delay) 364 | 365 | def _element_find_by_text(self, text, exact_match=False): 366 | if self._is_ios(): 367 | element = self._element_find(text, True, False) 368 | if element: 369 | return element 370 | if exact_match: 371 | _xpath = u'//*[@value="{}" or @label="{}"]'.format(text, text) 372 | else: 373 | _xpath = u'//*[contains(@label,"{}") or contains(@value, "{}") or contains(text(), "{}")]'.format(text, text, text) 374 | elif self._is_android(): 375 | if exact_match: 376 | _xpath = u'//*[@{}="{}"]'.format('text', text) 377 | else: 378 | _xpath = u'//*[contains(@text,"{}")]'.format(text) 379 | return self._element_find(_xpath, True, True) 380 | 381 | def _is_text_visible(self, text, exact_match=False): 382 | element = self._element_find_by_text(text, exact_match) 383 | if element is not None: 384 | return element.is_displayed() 385 | return None 386 | 387 | def _scroll_to_text(self, text, swipe_direction, swipe_count=10): 388 | """This is a more manual attempt in case the first scroll does not work.""" 389 | for _ in range(swipe_count): 390 | if self._is_android() and self._is_text_present(text): 391 | return True 392 | if self._is_ios() and self._is_text_visible(text): 393 | return True 394 | if swipe_direction.lower() == 'up': 395 | self.swipe_by_percent(50, 25, 50, 75) # use swipe by direction if its ever implemented 396 | elif swipe_direction.lower() == 'down': 397 | self.swipe_by_percent(50, 75, 50, 25) # use swipe by direction if its ever implemented 398 | else: 399 | appLib.fail("Swipe_direction: " + swipe_direction + "is not implemented.") 400 | appLib.fail("Text: " + text + " was not found after " + str(swipe_count) + " swipes") 401 | -------------------------------------------------------------------------------- /src/ApplicationLibrary/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Accruent/robotframework-applicationlibrary/93f33e31dc7dd1c10a885dce91861aa20f140e5b/src/ApplicationLibrary/__init__.py -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Accruent/robotframework-applicationlibrary/93f33e31dc7dd1c10a885dce91861aa20f140e5b/src/__init__.py -------------------------------------------------------------------------------- /test/Desktop/test_desktop.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import psutil 4 | import unittest 5 | import subprocess 6 | from selenium.common.exceptions import WebDriverException, NoSuchElementException, InvalidSelectorException 7 | from ApplicationLibrary.DesktopLibrary import DesktopLibrary 8 | from appium import webdriver 9 | from unittest.mock import MagicMock, patch 10 | sys.path.append(os.path.join(os.path.dirname(sys.path[0]), 'Helpers')) 11 | from webdriverremotemock import WebdriverRemoteMock 12 | from selenium.webdriver.remote.webelement import WebElement 13 | from appium.webdriver.common.touch_action import TouchAction 14 | 15 | 16 | def _long_running_function(): 17 | while True: 18 | return 'Error' 19 | 20 | 21 | class TestInternal(unittest.TestCase): 22 | def test_get_keyword_names_successful(self): 23 | DesktopLibrary().get_keyword_names() 24 | 25 | @patch('subprocess.call') 26 | @patch('subprocess.Popen') 27 | def test_driver_setup_and_teardown(self, Popen, call): 28 | Popen.return_value = 1 29 | dl = DesktopLibrary() 30 | dl.driver_setup() 31 | self.assertTrue(dl.winappdriver.process) 32 | dl.driver_teardown() 33 | self.assertFalse(dl.winappdriver.process) 34 | 35 | @patch('subprocess.Popen') 36 | def test_driver_setup_failure(self, Popen): 37 | Popen.side_effect = Exception 38 | dl = DesktopLibrary() 39 | dl.driver_setup() 40 | self.assertFalse(dl.winappdriver.process) 41 | 42 | @patch('subprocess.call') 43 | def test_teardown_without_setup(self, call): 44 | dl = DesktopLibrary() 45 | dl.driver_teardown() 46 | self.assertFalse(dl.winappdriver.process) 47 | 48 | def test_driver_child_process_teardown(self): 49 | mock_child = MagicMock() 50 | dl = DesktopLibrary() 51 | dl.winappdriver.process = MagicMock() 52 | dl.winappdriver.process.pid = 1 53 | psutil.Process.create_time = MagicMock() 54 | psutil.Process.children = MagicMock(return_value=[mock_child]) 55 | self.assertFalse(dl.winappdriver.process is None) 56 | dl.driver_teardown() 57 | self.assertTrue(dl.winappdriver.process is None) 58 | 59 | def test_open_application_successful(self): 60 | dl = DesktopLibrary() 61 | webdriver.Remote = WebdriverRemoteMock 62 | self.assertFalse(dl._cache.current) 63 | dl.open_application('remote_url') 64 | self.assertTrue(dl._cache.current) 65 | 66 | def test_open_application_with_working_dir_successful(self): 67 | dl = DesktopLibrary() 68 | webdriver.Remote = WebdriverRemoteMock 69 | self.assertFalse(dl._cache.current) 70 | dl.open_application('remote_url', appWorkingDir='C:/Windows/System32') 71 | self.assertTrue(dl._cache.current) 72 | 73 | def test_open_application_failure(self): 74 | dl = DesktopLibrary() 75 | dl._open_desktop_session = MagicMock() 76 | webdriver.Remote = MagicMock() 77 | webdriver.Remote.side_effect = WebDriverException('error') 78 | self.assertRaisesRegex(AssertionError, 'Message: error', dl.open_application, 'remote_url') 79 | 80 | def test_open_multiple_applications_successful(self): 81 | dl = DesktopLibrary() 82 | webdriver.Remote = WebdriverRemoteMock 83 | dl.open_application('remote_url', alias='App1') 84 | dl.open_application('remote_url', alias='App2', desktop_alias='Desktop2') 85 | dl.switch_application('Desktop') 86 | dl.switch_application('Desktop2') 87 | self.assertRaisesRegex(RuntimeError, "Non-existing index or alias 'Desktop3'.", dl.switch_application, 88 | 'Desktop3') 89 | 90 | def test_open_application_successful_double(self): 91 | dl = DesktopLibrary() 92 | webdriver.Remote = WebdriverRemoteMock 93 | self.assertFalse(dl._cache.current) 94 | dl.open_application('remote_url') 95 | self.assertTrue(dl._cache.current) 96 | dl.open_application('remote_url') 97 | self.assertTrue(dl._cache.current) 98 | 99 | def test_open_application_splash_catch(self): 100 | dl = DesktopLibrary() 101 | subprocess.Popen = MagicMock() 102 | webdriver.Remote = WebdriverRemoteMock 103 | self.assertFalse(dl._cache.current) 104 | dl.open_application('remote_url', window_name='test', app='testApp', splash_delay=1) 105 | self.assertTrue(dl._cache.current) 106 | 107 | def test_open_application_splash_catch_double(self): 108 | dl = DesktopLibrary() 109 | subprocess.Popen = MagicMock() 110 | webdriver.Remote = WebdriverRemoteMock 111 | self.assertFalse(dl._cache.current) 112 | dl.open_application('remote_url', window_name='test', app='testApp', splash_delay=1) 113 | self.assertTrue(dl._cache.current) 114 | dl.open_application('remote_url', window_name='test2', app='testApp', splash_delay=1) 115 | self.assertTrue(dl._cache.current) 116 | 117 | def test_open_application_window_name_non_exact(self): 118 | dl = DesktopLibrary() 119 | subprocess.Popen = MagicMock() 120 | webdriver.Remote = WebdriverRemoteMock 121 | webdriver.Remote.find_element_by_xpath = MagicMock() 122 | self.assertFalse(dl._cache.current) 123 | dl.open_application('remote_url', window_name='test', app='testApp', exact_match=False) 124 | self.assertTrue(dl._cache.current) 125 | 126 | def test_switch_application_with_end_whitespace(self): 127 | dl = DesktopLibrary() 128 | dl._run_on_failure = MagicMock() 129 | webdriver.Remote = WebdriverRemoteMock 130 | webdriver.Remote.find_element_by_name = MagicMock() 131 | dl.switch_application_by_name('remote_url', window_name='name with end whitespace ') 132 | webdriver.Remote.find_element_by_name.assert_called_with('name with end whitespace ') 133 | 134 | def test_switch_application_failure(self): 135 | dl = DesktopLibrary() 136 | dl._run_on_failure = MagicMock() 137 | webdriver.Remote = WebdriverRemoteMock 138 | webdriver.Remote.find_element_by_name = MagicMock(side_effect=[WebDriverException, WebDriverException]) 139 | self.assertRaisesRegex(AssertionError, 'Error finding window "test" in the desktop session. Is it a top level ' 140 | 'window handle?.', dl.switch_application_by_name, 141 | 'remote_url', window_name='test') 142 | 143 | def test_switch_application_failure_2(self): 144 | dl = DesktopLibrary() 145 | dl._run_on_failure = MagicMock() 146 | webdriver.Remote = WebdriverRemoteMock 147 | webdriver.Remote.find_element_by_name = MagicMock() 148 | webdriver.Remote.find_element_by_name.side_effect = [WebDriverException, "Window", "Window"] 149 | self.assertRaisesRegex(AssertionError, 'Error finding window "test" in the desktop session. Is it a top level ' 150 | 'window handle?.', dl.switch_application_by_name, 151 | 'remote_url', window_name='test') 152 | 153 | def test_switch_application_failure_3(self): 154 | dl = DesktopLibrary() 155 | dl._run_on_failure = MagicMock() 156 | web_driver_mock = WebdriverRemoteMock 157 | webdriver.Remote = MagicMock(side_effect=[web_driver_mock, Exception]) 158 | web_driver_mock.find_element_by_name = MagicMock() 159 | web_driver_mock.quit = MagicMock(return_value=True) 160 | self.assertRaisesRegex(AssertionError, 'Error connecting webdriver to window "test".', 161 | dl.switch_application_by_name, 'remote_url', window_name='test') 162 | 163 | def test_switch_application_failure_4(self): 164 | dl = DesktopLibrary() 165 | dl._run_on_failure = MagicMock() 166 | webdriver.Remote = WebdriverRemoteMock 167 | webdriver.Remote.find_element_by_xpath = MagicMock(side_effect=[WebDriverException, MagicMock(), MagicMock()]) 168 | dl.switch_application_by_name('remote_url', window_name='test', exact_match=False) 169 | 170 | def test_switch_application_by_locator_success(self): 171 | dl = DesktopLibrary() 172 | dl._run_on_failure = MagicMock() 173 | webdriver.Remote = WebdriverRemoteMock 174 | webdriver.Remote.find_element_by_class_name = MagicMock(side_effect=[MagicMock()]) 175 | dl.switch_application_by_locator('remote_url', locator='class=test') 176 | self.assertTrue(dl._cache.current) 177 | 178 | def test_switch_application_by_locator_success_2(self): 179 | dl = DesktopLibrary() 180 | dl._run_on_failure = MagicMock() 181 | webdriver.Remote = WebdriverRemoteMock 182 | webdriver.Remote.find_element_by_class_name = MagicMock(WebDriverException, MagicMock(), MagicMock()) 183 | dl.switch_application_by_locator('remote_url', locator='class=test') 184 | self.assertTrue(dl._cache.current) 185 | 186 | def test_switch_application_by_locator_success_3(self): 187 | dl = DesktopLibrary() 188 | dl._run_on_failure = MagicMock() 189 | webdriver.Remote = WebdriverRemoteMock 190 | webdriver.Remote.find_element_by_name = MagicMock(side_effect=[MagicMock()]) 191 | dl.switch_application_by_locator('remote_url', name='test', app='some_app') 192 | self.assertTrue(dl._cache.current) 193 | 194 | def test_switch_application_by_locator_failure(self): 195 | dl = DesktopLibrary() 196 | dl._run_on_failure = MagicMock() 197 | webdriver.Remote = WebdriverRemoteMock 198 | webdriver.Remote.find_element_by_class_name = MagicMock(side_effect=[WebDriverException, WebDriverException]) 199 | self.assertRaisesRegex(AssertionError, 'Error finding window "class=test" in the desktop session. Is it a top level ' 200 | 'window handle?.', dl.switch_application_by_locator, 201 | 'remote_url', locator='class=test') 202 | 203 | def test_switch_application_by_locator_failure_2(self): 204 | dl = DesktopLibrary() 205 | dl._run_on_failure = MagicMock() 206 | webdriver.Remote = WebdriverRemoteMock 207 | webdriver.Remote.find_element_by_name = MagicMock() 208 | webdriver.Remote.find_element_by_name.side_effect = [WebDriverException, MagicMock(), WebDriverException] 209 | self.assertRaisesRegex(AssertionError, 'Error finding window "name=test" in the desktop session. Is it a top level ' 210 | 'window handle?.', dl.switch_application_by_locator, 211 | 'remote_url', locator='name=test') 212 | 213 | def test_switch_application_by_locator_failure_3(self): 214 | dl = DesktopLibrary() 215 | dl._run_on_failure = MagicMock() 216 | web_driver_mock = WebdriverRemoteMock 217 | webdriver.Remote = MagicMock(side_effect=[web_driver_mock, Exception]) 218 | web_driver_mock.find_element_by_xpath = MagicMock() 219 | web_driver_mock.quit = MagicMock(return_value=True) 220 | self.assertRaisesRegex(AssertionError, 'Error connecting webdriver to window "xpath=//test".', 221 | dl.switch_application_by_locator, 'remote_url', locator='xpath=//test') 222 | 223 | def test_switch_application_multi_desktop_successful(self): 224 | dl = DesktopLibrary() 225 | webdriver.Remote = WebdriverRemoteMock 226 | dl.open_application('remote_url', alias='App1') 227 | dl.open_application('remote_url', alias='App2', desktop_alias='Desktop2') 228 | self.assertEqual(dl.current_desktop, 'Desktop2') 229 | dl.switch_application('App1') 230 | self.assertEqual(dl.current_desktop, 'Desktop2') 231 | dl.switch_application('App1', 'Desktop') 232 | self.assertEqual(dl.current_desktop, 'Desktop') 233 | 234 | def test_switch_application_no_alias_or_index_failure(self): 235 | dl = DesktopLibrary() 236 | webdriver.Remote = WebdriverRemoteMock 237 | dl.open_application('remote_url', alias='App1') 238 | dl.open_application('remote_url', alias='App2', desktop_alias='Desktop2') 239 | dl.switch_application(None) 240 | self.assertFalse(dl._cache.current) 241 | 242 | def test_launch_application_successful(self): 243 | dl = DesktopLibrary() 244 | webdriver.Remote = WebdriverRemoteMock 245 | self.assertFalse(dl._cache.current) 246 | dl.open_application('remote_url') 247 | dl.quit_application() 248 | dl.launch_application() 249 | self.assertTrue(dl._cache.current) 250 | 251 | def test_maximize_window_successful(self): 252 | mock_desk = MagicMock() 253 | self.assertTrue(DesktopLibrary.maximize_window(mock_desk)) 254 | 255 | def test_wait_for_and_clear_text_simple(self): 256 | mock_desk = MagicMock() 257 | DesktopLibrary.wait_for_and_clear_text(mock_desk, "some_locator") 258 | mock_desk.current_element.clear.assert_called() 259 | 260 | def test_wait_for_and_click_element(self): 261 | mock_desk = MagicMock() 262 | DesktopLibrary.wait_for_and_click_element(mock_desk, "some_locator") 263 | mock_desk.current_element.click.assert_called() 264 | 265 | def test_click_element(self): 266 | mock_desk = MagicMock() 267 | DesktopLibrary.click_element(mock_desk, "some_locator") 268 | mock_desk._element_find.assert_called_with("some_locator", True, True) 269 | 270 | def test_wait_for_and_input_password(self): 271 | mock_desk = MagicMock() 272 | DesktopLibrary.wait_for_and_input_password(mock_desk, "some_locator", "some_text") 273 | mock_desk.current_element.send_keys.assert_called_with("some_text") 274 | 275 | def test_wait_for_and_input_text(self): 276 | mock_desk = MagicMock() 277 | DesktopLibrary.wait_for_and_input_text(mock_desk, "some_locator", "some_text") 278 | mock_desk.current_element.send_keys.assert_called_with("some_text") 279 | 280 | @patch("appium.webdriver.common.touch_action.TouchAction.press") 281 | def test_wait_for_and_long_press(self, press): 282 | mock_desk = MagicMock() 283 | DesktopLibrary.wait_for_and_long_press(mock_desk, "some_locator", 1000) 284 | press.assert_called() 285 | 286 | def test_wait_until_element_contains(self): 287 | mock_desk = MagicMock() 288 | DesktopLibrary.wait_until_element_contains(mock_desk, "some_locator", 'test_text') 289 | mock_desk.element_should_contain_text.assert_called_with(unittest.mock.ANY, "test_text", None) 290 | 291 | def test_wait_until_element_does_not_contain(self): 292 | mock_desk = MagicMock() 293 | DesktopLibrary.wait_until_element_does_not_contain(mock_desk, "some_locator", 'test_text') 294 | mock_desk.element_should_not_contain_text.assert_called_with(unittest.mock.ANY, "test_text", None) 295 | 296 | def test_element_should_be_enabled(self): 297 | mock_desk = MagicMock() 298 | mock_desk._check_for_cached_element().is_enabled = MagicMock(return_value=True) 299 | DesktopLibrary.element_should_be_enabled(mock_desk, "some_locator") 300 | mock_desk._check_for_cached_element().is_enabled.assert_called_with() 301 | 302 | def test_element_should_be_enabled_error(self): 303 | mock_desk = MagicMock() 304 | mock_desk._check_for_cached_element().is_enabled = MagicMock(return_value=False) 305 | self.assertRaisesRegex(AssertionError, "Element 'some_locator' should be enabled but did " 306 | "not", DesktopLibrary.element_should_be_enabled, mock_desk, 307 | "some_locator") 308 | mock_desk._check_for_cached_element().is_enabled.assert_called_with() 309 | 310 | def test_element_should_be_enabled_current_element_set(self): 311 | mock_desk = MagicMock() 312 | mock_desk._check_for_cached_element().is_enabled = MagicMock(return_value=True) 313 | DesktopLibrary.element_should_be_enabled(mock_desk, mock_desk.current_element) 314 | mock_desk._check_for_cached_element().is_enabled.assert_called_with() 315 | 316 | def test_element_should_be_disabled(self): 317 | mock_desk = MagicMock() 318 | mock_desk._check_for_cached_element().is_enabled = MagicMock(return_value=False) 319 | DesktopLibrary.element_should_be_disabled(mock_desk, "some_locator") 320 | mock_desk._check_for_cached_element().is_enabled.assert_called_with() 321 | 322 | def test_element_should_be_disabled_error(self): 323 | mock_desk = MagicMock() 324 | mock_desk._check_for_cached_element().is_enabled = MagicMock(return_value=True) 325 | self.assertRaisesRegex(AssertionError, "Element 'some_locator' should be disabled but did " 326 | "not", DesktopLibrary.element_should_be_disabled, mock_desk, 327 | "some_locator") 328 | mock_desk._check_for_cached_element().is_enabled.assert_called_with() 329 | 330 | def test_element_should_be_disabled_current_element_set(self): 331 | mock_desk = MagicMock() 332 | mock_desk.current_element = MagicMock() 333 | mock_desk._check_for_cached_element().is_enabled = MagicMock(return_value=False) 334 | DesktopLibrary.element_should_be_disabled(mock_desk, mock_desk.current_element) 335 | mock_desk._check_for_cached_element().is_enabled.assert_called_with() 336 | 337 | def test_wait_until_element_is_enabled(self): 338 | mock_desk = MagicMock() 339 | DesktopLibrary.wait_until_element_is_enabled(mock_desk, "some_locator") 340 | mock_desk.element_should_be_enabled.assert_called_with(unittest.mock.ANY) 341 | 342 | def test_wait_until_element_is_disabled(self): 343 | mock_desk = MagicMock() 344 | DesktopLibrary.wait_until_element_is_disabled(mock_desk, "some_locator") 345 | mock_desk.element_should_be_disabled.assert_called_with(unittest.mock.ANY) 346 | 347 | def test_is_disabled(self): 348 | mock_desk = MagicMock() 349 | mock_element = MagicMock() 350 | mock_element.is_enabled = MagicMock(return_value=False) 351 | self.assertTrue(DesktopLibrary.is_disabled(mock_desk, mock_element)) 352 | 353 | def test_mouse_over_element(self): 354 | mock_desk = MagicMock() 355 | DesktopLibrary.mouse_over_element(mock_desk, "some_locator") 356 | mock_desk._move_to_element.assert_called_with(unittest.mock.ANY, unittest.mock.ANY, 0, 0) 357 | 358 | def test_mouse_over_element_current_element_set(self): 359 | mock_desk = MagicMock() 360 | mock_desk.current_element = MagicMock() 361 | DesktopLibrary.mouse_over_element(mock_desk, mock_desk.current_element) 362 | mock_desk._move_to_element.assert_called_with(unittest.mock.ANY, unittest.mock.ANY, 0, 0) 363 | 364 | def test_mouse_over_element_with_offset(self): 365 | mock_desk = MagicMock() 366 | DesktopLibrary.mouse_over_element(mock_desk, "some_locator", x_offset=100, y_offset=100) 367 | mock_desk._move_to_element.assert_called_with(unittest.mock.ANY, unittest.mock.ANY, 100, 100) 368 | 369 | def test_mouse_over_and_click_element(self): 370 | mock_desk = MagicMock() 371 | DesktopLibrary.mouse_over_and_click_element(mock_desk, "some_locator") 372 | mock_desk.mouse_over_element.assert_called_with("some_locator", x_offset=0, y_offset=0) 373 | mock_desk.click_a_point.assert_called_with(double_click=False) 374 | 375 | def test_mouse_over_and_click_element_with_offset(self): 376 | mock_desk = MagicMock() 377 | DesktopLibrary.mouse_over_and_click_element(mock_desk, "some_locator", x_offset=100, y_offset=100) 378 | mock_desk.mouse_over_element.assert_called_with("some_locator", x_offset=100, y_offset=100) 379 | mock_desk.click_a_point.assert_called_with(double_click=False) 380 | 381 | def test_mouse_over_and_context_click_element(self): 382 | mock_desk = MagicMock() 383 | DesktopLibrary.mouse_over_and_context_click_element(mock_desk, "some_locator") 384 | mock_desk.mouse_over_element.assert_called_with("some_locator", x_offset=0, y_offset=0) 385 | mock_desk.context_click_a_point.assert_called_with() 386 | 387 | def test_mouse_over_and_context_click_element_with_offset(self): 388 | mock_desk = MagicMock() 389 | DesktopLibrary.mouse_over_and_context_click_element(mock_desk, "some_locator", x_offset=100, y_offset=100) 390 | mock_desk.mouse_over_element.assert_called_with("some_locator", x_offset=100, y_offset=100) 391 | mock_desk.context_click_a_point.assert_called_with() 392 | 393 | def test_mouse_over_and_click_element_with_double_click(self): 394 | mock_desk = MagicMock() 395 | DesktopLibrary.mouse_over_and_click_element(mock_desk, "some_locator", True) 396 | mock_desk.mouse_over_element.assert_called_with("some_locator", x_offset=0, y_offset=0) 397 | mock_desk.click_a_point.assert_called_with(double_click=True) 398 | 399 | def test_wait_for_and_mouse_over_element(self): 400 | mock_desk = MagicMock() 401 | DesktopLibrary.wait_for_and_mouse_over_element(mock_desk, "some_locator") 402 | mock_desk.mouse_over_element.assert_called_with(unittest.mock.ANY, 0, 0) 403 | 404 | def test_wait_for_and_mouse_over_element_with_offset(self): 405 | mock_desk = MagicMock() 406 | DesktopLibrary.wait_for_and_mouse_over_element(mock_desk, "some_locator", x_offset=100, y_offset=100) 407 | mock_desk.mouse_over_element.assert_called_with(unittest.mock.ANY, 100, 100) 408 | 409 | def test_wait_for_and_mouse_over_and_click_element(self): 410 | mock_desk = MagicMock() 411 | DesktopLibrary.wait_for_and_mouse_over_and_click_element(mock_desk, "some_locator") 412 | mock_desk.mouse_over_and_click_element.assert_called_with(unittest.mock.ANY, False, 0, 0) 413 | 414 | def test_wait_for_and_mouse_over_and_click_element_with_offset(self): 415 | mock_desk = MagicMock() 416 | DesktopLibrary.wait_for_and_mouse_over_and_click_element(mock_desk, "some_locator", x_offset=100, y_offset=100) 417 | mock_desk.mouse_over_and_click_element.assert_called_with(unittest.mock.ANY, False, 100, 100) 418 | 419 | def test_wait_for_and_mouse_over_and_click_element_with_double_click(self): 420 | mock_desk = MagicMock() 421 | DesktopLibrary.wait_for_and_mouse_over_and_click_element(mock_desk, "some_locator", double_click=True) 422 | mock_desk.mouse_over_and_click_element.assert_called_with(unittest.mock.ANY, True, 0, 0) 423 | 424 | def test_element_find_by_name(self): 425 | mock_desk = MagicMock() 426 | mock_desk._parse_locator = MagicMock(return_value=['name', 'Capture']) 427 | DesktopLibrary._element_find(mock_desk, "Name='Capture'", True, True) 428 | mock_desk._current_application().find_element_by_name.assert_called_with('Capture') 429 | 430 | def test_elements_find_by_name(self): 431 | mock_desk = MagicMock() 432 | mock_desk._parse_locator = MagicMock(return_value=['name', 'Capture']) 433 | DesktopLibrary._element_find(mock_desk, "Name='Capture'", False, True) 434 | mock_desk._current_application().find_elements_by_name.assert_called_with('Capture') 435 | 436 | def test_element_find_by_image(self): 437 | mock_desk = MagicMock() 438 | mock_desk._parse_locator = MagicMock(return_value=['image', 'file.png']) 439 | DesktopLibrary._element_find(mock_desk, "image='file.png", True, True) 440 | mock_desk._current_application().find_element_by_image.assert_called_with('file.png') 441 | 442 | def test_element_find_by_image_fail(self): 443 | mock_desk = MagicMock() 444 | mock_desk._parse_locator = MagicMock(return_value=['image', 'file.png']) 445 | mock_desk._current_application().find_element_by_image = \ 446 | MagicMock(side_effect=InvalidSelectorException) 447 | self.assertRaisesRegex(AssertionError, 'Selecting by image is only available when using ' 448 | 'Appium v1.18.0 or higher', DesktopLibrary._element_find, mock_desk, 449 | 'file.png', True, True) 450 | 451 | def test_elements_find_by_image(self): 452 | mock_desk = MagicMock() 453 | mock_desk._parse_locator = MagicMock(return_value=['image', 'file.png']) 454 | DesktopLibrary._element_find(mock_desk, "image='file.png", False, True) 455 | mock_desk._current_application().find_elements_by_image.assert_called_with('file.png') 456 | 457 | def test_elements_find_by_image_fail(self): 458 | mock_desk = MagicMock() 459 | mock_desk._parse_locator = MagicMock(return_value=['image', 'file.png']) 460 | mock_desk._current_application().find_elements_by_image = \ 461 | MagicMock(side_effect=InvalidSelectorException) 462 | self.assertRaisesRegex(AssertionError, 'Selecting by image is only available when using ' 463 | 'Appium v1.18.0 or higher', DesktopLibrary._element_find, mock_desk, 464 | 'file.png', False, True) 465 | 466 | def test_element_find_by_accessibility_id(self): 467 | mock_desk = MagicMock() 468 | mock_desk._parse_locator = MagicMock(return_value=['accessibility_id', 'Capture']) 469 | DesktopLibrary._element_find(mock_desk, "accessibility_id='Capture'", True, True) 470 | mock_desk._current_application().find_element_by_accessibility_id.assert_called_with('Capture') 471 | 472 | def test_elements_find_by_accessibility_id(self): 473 | mock_desk = MagicMock() 474 | mock_desk._parse_locator = MagicMock(return_value=['accessibility_id', 'Capture']) 475 | DesktopLibrary._element_find(mock_desk, "accessibility_id='Capture'", False, True) 476 | mock_desk._current_application().find_elements_by_accessibility_id.assert_called_with('Capture') 477 | 478 | def test_element_find_by_element(self): 479 | mock_desk = MagicMock() 480 | mock_element = WebElement(MagicMock(), MagicMock()) 481 | value = DesktopLibrary._element_find(mock_desk, mock_element, True, True) 482 | self.assertEqual(mock_element, value) 483 | 484 | def test_element_find_by_elements(self): 485 | mock_desk = MagicMock() 486 | mock_element = WebElement(MagicMock(), MagicMock()) 487 | value = DesktopLibrary._element_find(mock_desk, mock_element, False, True) 488 | self.assertEqual([mock_element], value) 489 | 490 | def test_element_find_by_class_name(self): 491 | mock_desk = MagicMock() 492 | mock_desk._parse_locator = MagicMock(return_value=['class', 'Capture']) 493 | DesktopLibrary._element_find(mock_desk, "class='Capture'", True, True) 494 | mock_desk._current_application().find_element_by_class_name.assert_called_with('Capture') 495 | 496 | def test_elements_find_by_class_name(self): 497 | mock_desk = MagicMock() 498 | mock_desk._parse_locator = MagicMock(return_value=['class', 'Capture']) 499 | DesktopLibrary._element_find(mock_desk, "class='Capture'", False, True) 500 | mock_desk._current_application().find_elements_by_class_name.assert_called_with('Capture') 501 | 502 | def test_element_find_by_xpath(self): 503 | mock_desk = MagicMock() 504 | mock_desk._parse_locator = MagicMock(return_value=['xpath', 'Capture']) 505 | DesktopLibrary._element_find(mock_desk, "xpath=//TreeItem[@Name='Capture']", True, True) 506 | mock_desk._current_application().find_element_by_xpath.assert_called_with('Capture') 507 | 508 | def test_elements_find_by_xpath(self): 509 | mock_desk = MagicMock() 510 | mock_desk._parse_locator = MagicMock(return_value=['xpath', 'Capture']) 511 | DesktopLibrary._element_find(mock_desk, "xpath=//TreeItem[@Name='Capture']", False, True) 512 | mock_desk._current_application().find_elements_by_xpath.assert_called_with('Capture') 513 | 514 | def test_element_find_by_default_xpath(self): 515 | mock_desk = MagicMock() 516 | mock_desk._parse_locator = MagicMock(return_value=[None, "//TreeItem[@Name='Capture']"]) 517 | DesktopLibrary._element_find(mock_desk, "//TreeItem[@Name='Capture']", True, True) 518 | mock_desk._current_application().find_element_by_xpath.assert_called_with("//TreeItem[@Name='Capture']") 519 | 520 | def test_elements_find_by_default_xpath(self): 521 | mock_desk = MagicMock() 522 | mock_desk._parse_locator = MagicMock(return_value=[None, "//TreeItem[@Name='Capture']"]) 523 | DesktopLibrary._element_find(mock_desk, "//TreeItem[@Name='Capture']", False, True) 524 | mock_desk._current_application().find_elements_by_xpath.assert_called_with("//TreeItem[@Name='Capture']") 525 | 526 | def test_element_find_by_default_accessibility_id(self): 527 | mock_desk = MagicMock() 528 | mock_desk._parse_locator = MagicMock(return_value=[None, "Capture"]) 529 | DesktopLibrary._element_find(mock_desk, "Capture", True, True) 530 | mock_desk._current_application().find_element_by_accessibility_id.assert_called_with('Capture') 531 | 532 | def test_elements_find_by_default_accessibility_id(self): 533 | mock_desk = MagicMock() 534 | mock_desk._parse_locator = MagicMock(return_value=[None, "Capture"]) 535 | DesktopLibrary._element_find(mock_desk, "Capture", False, True) 536 | mock_desk._current_application().find_elements_by_accessibility_id.assert_called_with('Capture') 537 | 538 | def test_element_find_fail(self): 539 | mock_desk = MagicMock() 540 | mock_desk._parse_locator = MagicMock(return_value=['blockbuster_id', '123456789']) 541 | self.assertRaisesRegex(AssertionError, "Element locator with prefix 'blockbuster_id' is not supported", 542 | DesktopLibrary._element_find, mock_desk, "blockbuster_id=123456789", True, True) 543 | 544 | def test_is_element_present_by_name(self): 545 | mock_desk = MagicMock() 546 | mock_desk._parse_locator = MagicMock(return_value=['name', 'Capture']) 547 | DesktopLibrary._is_element_present(mock_desk, "Name='Capture'") 548 | mock_desk._current_application().find_elements_by_name.assert_called_with('Capture') 549 | 550 | def test_is_element_present_by_accessibility_id(self): 551 | mock_desk = MagicMock() 552 | mock_desk._parse_locator = MagicMock(return_value=['accessibility_id', 'Capture']) 553 | DesktopLibrary._is_element_present(mock_desk, "accessibility_id='Capture'") 554 | mock_desk._current_application().find_elements_by_accessibility_id.assert_called_with( 555 | 'Capture') 556 | 557 | def test_is_element_present_by_class_name(self): 558 | mock_desk = MagicMock() 559 | mock_desk._parse_locator = MagicMock(return_value=['class', 'Capture']) 560 | DesktopLibrary._is_element_present(mock_desk, "class='Capture'") 561 | mock_desk._current_application().find_elements_by_class_name.assert_called_with('Capture') 562 | 563 | def test_is_element_present_by_xpath(self): 564 | mock_desk = MagicMock() 565 | mock_desk._parse_locator = MagicMock(return_value=['xpath', 'Capture']) 566 | DesktopLibrary._is_element_present(mock_desk, "xpath=//TreeItem[@Name='Capture']") 567 | mock_desk._current_application().find_elements_by_xpath.assert_called_with('Capture') 568 | 569 | def test_is_element_present_by_default_xpath(self): 570 | mock_desk = MagicMock() 571 | mock_desk._parse_locator = MagicMock(return_value=[None, "//TreeItem[@Name='Capture']"]) 572 | DesktopLibrary._is_element_present(mock_desk, "//TreeItem[@Name='Capture']") 573 | mock_desk._current_application().find_elements_by_xpath.assert_called_with( 574 | "//TreeItem[@Name='Capture']") 575 | 576 | def test_is_element_present_by_default_accessibility_id(self): 577 | mock_desk = MagicMock() 578 | mock_desk._parse_locator = MagicMock(return_value=[None, "Capture"]) 579 | DesktopLibrary._is_element_present(mock_desk, "Capture") 580 | mock_desk._current_application().find_elements_by_accessibility_id.assert_called_with( 581 | 'Capture') 582 | 583 | def test_is_element_present_by_image(self): 584 | mock_desk = MagicMock() 585 | mock_desk._parse_locator = MagicMock(return_value=['image', 'file.png']) 586 | DesktopLibrary._is_element_present(mock_desk, "image='file.png") 587 | mock_desk._current_application().find_elements_by_image.assert_called_with('file.png') 588 | 589 | def test_is_element_present_by_image_fail(self): 590 | mock_desk = MagicMock() 591 | mock_desk._parse_locator = MagicMock(return_value=['image', 'file.png']) 592 | mock_desk._current_application().find_elements_by_image = \ 593 | MagicMock(side_effect=InvalidSelectorException) 594 | self.assertRaisesRegex(AssertionError, 'Selecting by image is only available when using ' 595 | 'Appium v1.18.0 or higher', DesktopLibrary._is_element_present, mock_desk, 596 | 'file.png') 597 | 598 | def test_is_element_present_fail(self): 599 | mock_desk = MagicMock() 600 | mock_desk._parse_locator = MagicMock(return_value=['blockbuster_id', '123456789']) 601 | self.assertRaisesRegex(AssertionError, 602 | "Element locator with prefix 'blockbuster_id' is not supported", 603 | DesktopLibrary._is_element_present, mock_desk, 604 | "blockbuster_id=123456789") 605 | 606 | def test_is_element_present_list_greater_than_0(self): 607 | mock_desk = MagicMock() 608 | mock_desk._parse_locator = MagicMock(return_value=['name', 'Capture']) 609 | mock_desk._current_application().find_elements_by_name = \ 610 | MagicMock(return_value=[MagicMock(), MagicMock()]) 611 | DesktopLibrary._is_element_present(mock_desk, "Name='Capture'") 612 | mock_desk._current_application().find_elements_by_name.assert_called_with('Capture') 613 | 614 | def test_parse_locator_xpath(self): 615 | mock_desk = MagicMock() 616 | parse = DesktopLibrary._parse_locator(mock_desk, '//test') 617 | self.assertEqual(parse, (None, '//test')) 618 | 619 | def test_parse_locator(self): 620 | mock_desk = MagicMock() 621 | parse = DesktopLibrary._parse_locator(mock_desk, 'name=test') 622 | self.assertEqual(parse, ('name', 'test')) 623 | 624 | @patch("selenium.webdriver.common.action_chains.ActionChains.move_by_offset") 625 | def test_mouse_over_by_offset(self, action): 626 | mock_desk = MagicMock() 627 | DesktopLibrary.mouse_over_by_offset(mock_desk, 100, 100) 628 | action.assert_called_with(100, 100) 629 | 630 | @patch("selenium.webdriver.common.action_chains.ActionChains.click") 631 | def test_click_a_point(self, action): 632 | mock_desk = MagicMock() 633 | DesktopLibrary.click_a_point(mock_desk) 634 | action.assert_called_with() 635 | 636 | @patch("selenium.webdriver.common.action_chains.ActionChains.move_by_offset") 637 | @patch("selenium.webdriver.common.action_chains.ActionChains.click") 638 | def test_click_a_point_with_offset(self, click, move): 639 | mock_desk = MagicMock() 640 | DesktopLibrary.click_a_point(mock_desk, x_offset=100, y_offset=100) 641 | move.assert_called_with(100, 100) 642 | click.assert_called_with() 643 | 644 | @patch("selenium.webdriver.common.action_chains.ActionChains.double_click") 645 | def test_click_a_point_with_double_click(self, click): 646 | mock_desk = MagicMock() 647 | DesktopLibrary.click_a_point(mock_desk, double_click=True) 648 | click.assert_called_with() 649 | 650 | @patch("selenium.webdriver.common.action_chains.ActionChains.context_click") 651 | def test_context_click_a_point(self, click): 652 | mock_desk = MagicMock() 653 | DesktopLibrary.context_click_a_point(mock_desk) 654 | click.assert_called_with() 655 | 656 | @patch("selenium.webdriver.common.action_chains.ActionChains.move_by_offset") 657 | @patch("selenium.webdriver.common.action_chains.ActionChains.context_click") 658 | def test_context_click_a_point_with_offset(self, click, move): 659 | mock_desk = MagicMock() 660 | DesktopLibrary.context_click_a_point(mock_desk, x_offset=-400, y_offset=-400) 661 | move.assert_called_with(-400, -400) 662 | click.assert_called_with() 663 | 664 | def test_drag_and_drop(self): 665 | mock_desk = MagicMock() 666 | TouchAction.move_to = MagicMock() 667 | TouchAction.release = MagicMock() 668 | DesktopLibrary.drag_and_drop(mock_desk, "some_locator", "some_other_locator") 669 | mock_desk._platform_dependant_press.assert_called() 670 | TouchAction.move_to.assert_called_with(unittest.mock.ANY) 671 | TouchAction.release.assert_called() 672 | 673 | def test_drag_and_drop_with_offset(self): 674 | mock_desk = MagicMock() 675 | TouchAction.move_to = MagicMock() 676 | TouchAction.release = MagicMock() 677 | DesktopLibrary.drag_and_drop_by_offset(mock_desk, "some_locator", x_offset=100, y_offset=100) 678 | mock_desk._platform_dependant_press.assert_called() 679 | TouchAction.move_to.assert_called_with(x=unittest.mock.ANY, y=unittest.mock.ANY) 680 | TouchAction.release.assert_called() 681 | 682 | @patch("selenium.webdriver.common.action_chains.ActionChains") 683 | def test_move_to_element(self, move): 684 | mock_desk = MagicMock() 685 | DesktopLibrary._move_to_element(mock_desk, move, "some_locator", 0, 0) 686 | move.move_to_element.assert_called_with("some_locator") 687 | 688 | @patch("selenium.webdriver.common.action_chains.ActionChains") 689 | def test_move_to_element_with_offset(self, move): 690 | mock_desk = MagicMock() 691 | DesktopLibrary._move_to_element(mock_desk, move, "some_locator", 100, 100) 692 | move.move_to_element_with_offset.assert_called_with("some_locator", 100, 100) 693 | 694 | @patch("selenium.webdriver.common.action_chains.ActionChains.send_keys") 695 | def test_send_keys(self, send_keys): 696 | mock_desk = MagicMock() 697 | DesktopLibrary.send_keys(mock_desk, 'test', '\ue007') 698 | send_keys.assert_called_with('\ue007') 699 | 700 | @patch('robot.libraries.BuiltIn.BuiltIn.fail') 701 | def test_send_keys_without_args(self, fail): 702 | mock_desk = MagicMock() 703 | DesktopLibrary.send_keys(mock_desk) 704 | fail.assert_called_with('No key arguments specified.') 705 | 706 | @patch("selenium.webdriver.common.action_chains.ActionChains.send_keys_to_element") 707 | def test_send_keys_to_element(self, send_keys): 708 | mock_desk = MagicMock() 709 | DesktopLibrary.send_keys_to_element(mock_desk, 'some_element', 'test', '\ue007') 710 | send_keys.assert_called_with(unittest.mock.ANY, '\ue007') 711 | 712 | @patch('robot.libraries.BuiltIn.BuiltIn.fail') 713 | def test_send_keys_to_element_without_args(self, fail): 714 | mock_desk = MagicMock() 715 | DesktopLibrary.send_keys_to_element(mock_desk, 'some_locator') 716 | fail.assert_called_with('No key arguments specified.') 717 | 718 | def test_capture_page_screenshot(self): 719 | mock_desk = MagicMock() 720 | mock_desk._get_screenshot_paths = MagicMock(return_value=['path', 'link']) 721 | DesktopLibrary.capture_page_screenshot(mock_desk) 722 | mock_desk._get_screenshot_paths.assert_called_with(None) 723 | 724 | def test_capture_page_screenshot_else_case(self): 725 | mock_desk = MagicMock() 726 | mock_desk._get_screenshot_paths = MagicMock(return_value=['path', 'link']) 727 | del mock_desk._current_application().get_screenshot_as_file 728 | DesktopLibrary.capture_page_screenshot(mock_desk, 'filename') 729 | mock_desk._get_screenshot_paths.assert_called_with('filename') 730 | 731 | def test_save_appium_screenshot(self): 732 | mock_desk = MagicMock() 733 | DesktopLibrary.save_appium_screenshot(mock_desk) 734 | mock_desk.capture_page_screenshot.assert_called_with(unittest.mock.ANY) 735 | 736 | def test_select_from_combobox_no_desktop(self): 737 | mock_desk = MagicMock() 738 | DesktopLibrary.select_element_from_combobox(mock_desk, 'some_locator', 'another_locator') 739 | mock_desk.click_element.assert_called_with('another_locator') 740 | 741 | def test_select_from_combobox_with_desktop(self): 742 | mock_desk = MagicMock() 743 | mock_desk.click_element = MagicMock(side_effect=[True, ValueError, True]) 744 | DesktopLibrary.select_element_from_combobox(mock_desk, 'some_locator', 'another_locator') 745 | mock_desk.click_element.assert_called_with('another_locator') 746 | 747 | def test_select_from_combobox_skip_to_desktop(self): 748 | mock_desk = MagicMock() 749 | DesktopLibrary.select_element_from_combobox(mock_desk, 'some_locator', 'another_locator', True) 750 | mock_desk.click_element.assert_called_with('another_locator') 751 | 752 | def test_select_from_combobox_retry(self): 753 | mock_desk = MagicMock() 754 | mock_desk.click_element = MagicMock(side_effect=[True, NoSuchElementException, True]) 755 | DesktopLibrary.select_element_from_combobox(mock_desk, 'some_locator', 'another_locator') 756 | mock_desk.click_element.assert_called_with('another_locator') 757 | 758 | def test_select_from_combobox_retry_desktop(self): 759 | mock_desk = MagicMock() 760 | mock_desk.click_element = MagicMock(side_effect=[True, NoSuchElementException, True]) 761 | DesktopLibrary.select_element_from_combobox(mock_desk, 'some_locator', 'another_locator', True) 762 | mock_desk.click_element.assert_called_with('another_locator') 763 | 764 | def test_select_elements_from_menu_retry_desktop(self): 765 | mock_desk = MagicMock() 766 | mock_desk.click_element = MagicMock(side_effect=[True, NoSuchElementException, True]) 767 | DesktopLibrary.select_elements_from_menu(mock_desk, 'some_locator', 'another_locator') 768 | mock_desk.click_element.assert_called_with('another_locator') 769 | 770 | def test_select_elements_from_menu_retry_desktop_2(self): 771 | mock_desk = MagicMock() 772 | mock_desk.click_element = MagicMock(side_effect=[True, NoSuchElementException, NoSuchElementException, True]) 773 | DesktopLibrary.select_elements_from_menu(mock_desk, 'some_locator', 'another_locator') 774 | mock_desk.click_element.assert_called_with('another_locator') 775 | 776 | def test_select_elements_from_menu_no_desktop(self): 777 | mock_desk = MagicMock() 778 | DesktopLibrary.select_elements_from_menu(mock_desk, 'some_locator', 'another_locator') 779 | mock_desk.click_element.assert_called_with('another_locator') 780 | 781 | def test_select_elements_from_context_menu_retry_desktop(self): 782 | mock_desk = MagicMock() 783 | mock_desk.mouse_over_and_context_click_element = MagicMock(side_effect=[NoSuchElementException, True, True]) 784 | DesktopLibrary.select_elements_from_context_menu(mock_desk, 'some_locator', 'another_locator') 785 | mock_desk.click_element.mouse_over_and_context_click('some_locator') 786 | mock_desk.click_element.assert_called_with('another_locator') 787 | 788 | def test_select_elements_from_context_menu_retry_desktop_2(self): 789 | mock_desk = MagicMock() 790 | mock_desk.mouse_over_and_context_click_element = MagicMock(side_effect=[NoSuchElementException, NoSuchElementException, True, True]) 791 | DesktopLibrary.select_elements_from_context_menu(mock_desk, 'some_locator', 'another_locator') 792 | mock_desk.click_element.mouse_over_and_context_click('some_locator') 793 | mock_desk.click_element.assert_called_with('another_locator') 794 | 795 | def test_select_elements_from_context_menu_retry_desktop_3(self): 796 | mock_desk = MagicMock() 797 | mock_desk.click_element = MagicMock(side_effect=[NoSuchElementException, NoSuchElementException, True, True]) 798 | DesktopLibrary.select_elements_from_context_menu(mock_desk, 'some_locator', 'another_locator') 799 | mock_desk.click_element.mouse_over_and_context_click('some_locator') 800 | mock_desk.click_element.assert_called_with('another_locator') 801 | 802 | def test_select_elements_from_context_menu_no_desktop(self): 803 | mock_desk = MagicMock() 804 | DesktopLibrary.select_elements_from_context_menu(mock_desk, 'some_locator', 'another_locator') 805 | mock_desk.click_element.mouse_over_and_context_click('some_locator') 806 | mock_desk.click_element.assert_called_with('another_locator') 807 | 808 | def test_wait_until_page_contains_private(self): 809 | mock_desk = MagicMock() 810 | DesktopLibrary._wait_until_page_contains(mock_desk, 'some_text', 5) 811 | mock_desk._wait_until.asser_called_with('some_text', 5) 812 | 813 | def test_wait_until_page_contains_element_private(self): 814 | mock_desk = MagicMock() 815 | DesktopLibrary._wait_until_page_contains_element(mock_desk, 'some_element', 5) 816 | mock_desk._wait_until.assert_called_with(5, "Element 'some_element' did not appear in " 817 | "", unittest.mock.ANY, 'some_element') 818 | 819 | def test_wait_until_no_error_timeout(self): 820 | mock_desk = MagicMock() 821 | self.assertRaisesRegex(AssertionError, 822 | 'Error', DesktopLibrary._wait_until_no_error, mock_desk, 823 | 1, _long_running_function) 824 | 825 | @patch("selenium.webdriver.common.touch_actions.TouchActions.flick") 826 | def test_flick(self, flick): 827 | mock_desk = MagicMock() 828 | DesktopLibrary.flick(mock_desk, 50, 100) 829 | flick.assert_called_with(50, 100) 830 | 831 | @patch("selenium.webdriver.common.touch_actions.TouchActions.flick_element") 832 | def test_flick_from_element(self, flick_element): 833 | mock_desk = MagicMock() 834 | DesktopLibrary.flick_from_element(mock_desk, "some_locator", 50, 100, 10) 835 | flick_element.assert_called_with(unittest.mock.ANY, 50, 100, 10) 836 | 837 | @patch("selenium.webdriver.common.touch_actions.TouchActions.flick_element") 838 | def test_flick_from_element_current_element_set(self, flick_element): 839 | mock_desk = MagicMock() 840 | mock_desk.current_element = MagicMock() 841 | DesktopLibrary.flick_from_element(mock_desk, mock_desk.current_element, 50, 100, 10) 842 | flick_element.assert_called_with(unittest.mock.ANY, 50, 100, 10) 843 | 844 | def test_wait_for_and_flick_from_element(self): 845 | mock_desk = MagicMock() 846 | DesktopLibrary.wait_for_and_flick_from_element(mock_desk, "some_locator", 50, 100, 10) 847 | mock_desk.flick_from_element.assert_called_with(unittest.mock.ANY, 50, 100, 10) 848 | 849 | @patch("selenium.webdriver.common.touch_actions.TouchActions.scroll") 850 | def test_scroll(self, scroll): 851 | mock_desk = MagicMock() 852 | DesktopLibrary.scroll(mock_desk, 50, 100) 853 | scroll.assert_called_with(50, 100) 854 | 855 | @patch("selenium.webdriver.common.touch_actions.TouchActions.scroll_from_element") 856 | def test_scroll_from_element(self, scroll_from_element): 857 | mock_desk = MagicMock() 858 | DesktopLibrary.scroll_from_element(mock_desk, "some_locator", 50, 100) 859 | scroll_from_element.assert_called_with(unittest.mock.ANY, 50, 100) 860 | 861 | @patch("selenium.webdriver.common.touch_actions.TouchActions.scroll_from_element") 862 | def test_scroll_from_element_current_element_set(self, scroll_from_element): 863 | mock_desk = MagicMock() 864 | mock_desk.current_element = MagicMock() 865 | DesktopLibrary.scroll_from_element(mock_desk, mock_desk.current_element, 50, 100) 866 | scroll_from_element.assert_called_with(unittest.mock.ANY, 50, 100) 867 | 868 | def test_wait_for_and_scroll_from_element(self): 869 | mock_desk = MagicMock() 870 | DesktopLibrary.wait_for_and_scroll_from_element(mock_desk, "some_locator", 50, 100) 871 | mock_desk.scroll_from_element.assert_called_with(unittest.mock.ANY, 50, 100) 872 | 873 | @patch("selenium.webdriver.common.touch_actions.TouchActions.double_tap") 874 | def test_double_tap(self, double_tap): 875 | mock_desk = MagicMock() 876 | DesktopLibrary.double_tap(mock_desk, "some_locator") 877 | double_tap.assert_called_with(unittest.mock.ANY) 878 | 879 | @patch("selenium.webdriver.common.touch_actions.TouchActions.double_tap") 880 | def test_double_tap_current_element_set(self, double_tap): 881 | mock_desk = MagicMock() 882 | mock_desk.current_element = MagicMock() 883 | DesktopLibrary.double_tap(mock_desk, mock_desk.current_element) 884 | double_tap.assert_called_with(unittest.mock.ANY) 885 | 886 | @patch("appium.webdriver.common.touch_action.TouchAction.tap") 887 | def test_wait_for_and_tap(self, tap): 888 | mock_desk = MagicMock() 889 | DesktopLibrary.wait_for_and_tap(mock_desk, "some_locator") 890 | tap.assert_called_with(unittest.mock.ANY) 891 | 892 | def test_wait_for_and_double_tap(self): 893 | mock_desk = MagicMock() 894 | DesktopLibrary.wait_for_and_double_tap(mock_desk, "some_locator") 895 | mock_desk.double_tap.assert_called_with(unittest.mock.ANY) 896 | 897 | @patch("selenium.webdriver.common.touch_actions.TouchActions.tap_and_hold") 898 | def test_drag_and_drop_by_touch(self, tap_and_hold): 899 | mock_desk = MagicMock() 900 | DesktopLibrary.drag_and_drop_by_touch(mock_desk, "some_locator", "some_other_locator") 901 | tap_and_hold.assert_called_with(unittest.mock.ANY, unittest.mock.ANY) 902 | 903 | @patch("selenium.webdriver.common.touch_actions.TouchActions.tap_and_hold") 904 | def test_drag_and_drop_by_touch_offset(self, tap_and_hold): 905 | mock_desk = MagicMock() 906 | DesktopLibrary.drag_and_drop_by_touch_offset(mock_desk, "some_locator", 50, 100) 907 | tap_and_hold.assert_called_with(unittest.mock.ANY, unittest.mock.ANY) 908 | 909 | def test_start_screen_recording(self): 910 | mock_desk = MagicMock() 911 | mock_desk._recording = None 912 | DesktopLibrary.start_screen_recording(mock_desk) 913 | self.assertTrue(mock_desk._recording == unittest.mock.ANY) 914 | 915 | def test_stop_screen_recording(self): 916 | mock_desk = MagicMock() 917 | mock_desk._recording = None 918 | DesktopLibrary.start_screen_recording(mock_desk) 919 | file = DesktopLibrary.stop_screen_recording(mock_desk) 920 | self.assertTrue(file == unittest.mock.ANY) 921 | 922 | def test_save_recording(self): 923 | mock_desk = MagicMock() 924 | mock_desk._recording = b'some data' 925 | mock_desk._is_remotepath_set = MagicMock(return_value=False) 926 | options = ["username=test"] 927 | mock_desk._get_screenrecord_paths = MagicMock(return_value=("path", "link")) 928 | file = DesktopLibrary._save_recording(mock_desk, "filename", options) 929 | self.assertTrue(file == "path") 930 | os.remove("path") 931 | 932 | def test_get_text(self): 933 | mock_desk = MagicMock() 934 | mock_desk.current_element = None 935 | DesktopLibrary._get_text(mock_desk, 1) 936 | mock_desk._check_for_cached_element.assert_called_with(1) 937 | 938 | def test_get_text_current_element_set(self): 939 | mock_desk = MagicMock() 940 | mock_desk.current_element = MagicMock() 941 | DesktopLibrary._get_text(mock_desk, mock_desk.current_element) 942 | mock_desk._element_find.assert_not_called() 943 | 944 | def test_get_text_element_none(self): 945 | mock_desk = MagicMock() 946 | mock_desk._check_for_cached_element = MagicMock(return_value=None) 947 | result = DesktopLibrary._get_text(mock_desk, mock_desk.current_element) 948 | self.assertEqual(result, None) 949 | 950 | def test_check_for_cached_element_true(self): 951 | mock_desk = MagicMock() 952 | mock_desk.current_element = "a locator" 953 | result = DesktopLibrary._check_for_cached_element(mock_desk, "a locator") 954 | self.assertEqual(result, "a locator") 955 | 956 | def test_check_for_cached_element_false(self): 957 | mock_desk = MagicMock() 958 | DesktopLibrary._check_for_cached_element(mock_desk, "a locator") 959 | mock_desk._element_find.assert_called_with("a locator", True, True) 960 | 961 | def test_platform_dependant_press_private(self): 962 | mock_desk = MagicMock() 963 | DesktopLibrary._platform_dependant_press(mock_desk, MagicMock(), 'some_element') 964 | -------------------------------------------------------------------------------- /test/Helpers/demo_app.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Accruent/robotframework-applicationlibrary/93f33e31dc7dd1c10a885dce91861aa20f140e5b/test/Helpers/demo_app.apk -------------------------------------------------------------------------------- /test/Helpers/webdriverremotemock.py: -------------------------------------------------------------------------------- 1 | # Code from robotframework-appiumlibrary for mocking the webdriver 2 | 3 | import logging 4 | import sys 5 | import unittest 6 | import mock 7 | import base64 8 | 9 | logger = logging.getLogger() 10 | stream_handler = logging.StreamHandler(sys.stdout) 11 | logger.addHandler(stream_handler) 12 | stream_handler = logging.StreamHandler(sys.stderr) 13 | logger.addHandler(stream_handler) 14 | 15 | 16 | class WebdriverRemoteMock(mock.Mock, unittest.TestCase): 17 | def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub', desired_capabilities=None): 18 | super(WebdriverRemoteMock, self).__init__() 19 | self._appiumUrl = command_executor 20 | self._desCapa = desired_capabilities 21 | self._dead = False 22 | self._myData = '' 23 | for key in desired_capabilities: 24 | self.assertNotEqual(desired_capabilities[key], None, 'Null value in desired capabilities') 25 | 26 | def _get_child_mock(self, **kwargs): 27 | return mock.Mock(**kwargs) 28 | 29 | def quit(self): 30 | self._dead = True 31 | 32 | def lock(self): 33 | if self._dead: 34 | raise RuntimeError('Application has been closed') 35 | 36 | def pull_file(self, decode=False): 37 | file = self._myData 38 | if decode: 39 | file = base64.b64decode(file) 40 | return file 41 | 42 | def push_file(self, data, encode=False): 43 | self._myData = base64.b64decode(data) if encode else data 44 | 45 | def find_element_by_name(self, *args): 46 | return Window 47 | 48 | 49 | class Window: 50 | def get_attribute(self, *args): 51 | return 12345 52 | -------------------------------------------------------------------------------- /test/Mobile/AndroidMobileTests.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Documentation ApplicationLibrary Mobile Library Tests. Requires Appium Server running on port 4723 with an Android device/emulator available. 3 | Library ApplicationLibrary.MobileLibrary 4 | Suite Setup Start App 5 | Test Setup Reset App 6 | Suite Teardown Close Application 7 | Force Tags Mobile 8 | 9 | *** Variables *** 10 | ${REMOTE_URL} https://ondemand.us-west-1.saucelabs.com/wd/hub 11 | ${APP} storage:filename=demo_app.apk 12 | ${commandTimeout}= 120 13 | 14 | *** Keywords *** 15 | Start App 16 | Open Application ${REMOTE_URL} platformName=Android deviceName=Android GoogleAPI Emulator 17 | ... newCommandTimeout=${commandTimeout} app=${APP} username=${sauce_username} 18 | ... access_key=${sauce_key} platformVersion=6.0 name=GitHub.${SUITE_NAME} 19 | ... build=${GITHUB_RUN_NUMBER} 20 | 21 | Reset App 22 | Reset Application 23 | Wait For And Click Element com.touchboarder.android.api.demos:id/buttonDefaultPositive 24 | Wait Until Page Contains API Demos 25 | 26 | *** Test Cases *** 27 | Wait For And Click Element By Accessibility Id Keyword Test 28 | Wait For And Click Element accessibility_id=More options 29 | Wait Until Page Contains Change Log 30 | 31 | Wait For And Click Element By Xpath Keyword Test 32 | Wait For And Click Element //hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.view.ViewGroup/android.widget.FrameLayout[2]/android.widget.RelativeLayout/android.widget.ListView/android.widget.TextView[2] 33 | Wait Until Page Contains Animation 34 | 35 | Wait For And Input Text Keyword Test 36 | Wait For And Click Element accessibility_id=Search 37 | Wait For And Input Text com.touchboarder.android.api.demos:id/search_src_text drag and drop 38 | Wait Until Page Contains Views/Drag and Drop 39 | 40 | Wait For And Long Press Keyword Test 41 | Wait For And Long Press //hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.view.ViewGroup/android.widget.FrameLayout[2]/android.widget.RelativeLayout/android.widget.ListView/android.widget.TextView[2] 42 | Wait Until Page Contains Animation 43 | 44 | Wait For And Input Password Keyword Test 45 | Wait For And Click Element accessibility_id=Search 46 | Wait For And Input Password com.touchboarder.android.api.demos:id/search_src_text drag and drop 47 | Wait Until Page Contains Views/Drag and Drop 48 | 49 | Wait Until Element is Enabled Keyword Test 50 | Wait Until Element Is Enabled accessibility_id=More options 51 | 52 | Drag and Drop Keyword Test 53 | Wait For And Click Element accessibility_id=Search 54 | Wait For And Input Text com.touchboarder.android.api.demos:id/search_src_text drag 55 | wait for and click text Graphics/Shadow Card Drag 56 | Drag and Drop com.touchboarder.android.api.demos:id/card com.touchboarder.android.api.demos:id/shape_select 57 | 58 | Drag and Drop Keyword Test Failure 59 | Wait For And Click Element accessibility_id=Search 60 | Wait For And Input Text com.touchboarder.android.api.demos:id/search_src_text drag 61 | wait for and click text Graphics/Shadow Card Drag 62 | Run keyword And Expect Error ValueError: Element locator 'Not_a_real_id' did not match any elements. 63 | ... Drag and Drop Not_a_real_id com.touchboarder.android.api.demos:id/shape_select 64 | 65 | Drag and Drop By Offset Keyword Test 66 | Wait For And Click Element accessibility_id=Search 67 | Wait For And Input Text com.touchboarder.android.api.demos:id/search_src_text drag 68 | wait for and click text Graphics/Shadow Card Drag 69 | ${location1}= Get Element Location com.touchboarder.android.api.demos:id/card 70 | Drag and Drop By Offset com.touchboarder.android.api.demos:id/card 100 100 71 | ${location2}= Get Element Location com.touchboarder.android.api.demos:id/card 72 | Should Not Be Equal "${location2}" "${location1}" 73 | 74 | Wait Until Element Contains 75 | Wait For And Click Element accessibility_id=Search 76 | Wait For And Input Text com.touchboarder.android.api.demos:id/search_src_text drag 77 | Wait Until Element Contains //hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.view.ViewGroup/android.widget.FrameLayout[2]/android.widget.RelativeLayout/android.widget.ListView/android.widget.TextView[1] 78 | ... Views/Drag and Drop 79 | 80 | Wait Until Element Does Not Contain 81 | Wait For And Click Element accessibility_id=Search 82 | Wait For And Input Text com.touchboarder.android.api.demos:id/search_src_text drag 83 | Wait Until Element Does Not Contain //hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.view.ViewGroup/android.widget.FrameLayout[2]/android.widget.RelativeLayout/android.widget.ListView/android.widget.TextView[1] 84 | ... Some Other Text 85 | 86 | Scroll To Text Keyword Test 87 | Wait For And Click Text API Demos 88 | Wait For And Click Text Graphics 89 | Wait Until Page Contains Graphics 90 | Scroll Down To Text SensorTest 91 | Scroll Up To Text Arcs 92 | 93 | Wait For And Tap Keyword Test 94 | Wait For And Tap accessibility_id=Search 95 | Wait Until Page Contains com.touchboarder.android.api.demos:id/search_src_text 96 | 97 | Save Selenium Screenshot Test 98 | ${file1}= Save Appium Screenshot 99 | ${file2}= Save Appium Screenshot 100 | Should Not Be Equal ${file1} ${file2} 101 | Should Match Regexp ${file1} appium-screenshot-\\d{8,10}.\\d{6,8}-\\d.png 102 | 103 | Scroll To Text In WebView Keyword Test 104 | [Setup] Close Application 105 | Open Application ${REMOTE_URL} platformName=Android deviceName=Android GoogleAPI Emulator 106 | ... newCommandTimeout=${commandTimeout} browserName=Chrome username=${sauce_username} 107 | ... access_key=${sauce_key} platformVersion=6.0 108 | Go To Url https://www.win-rar.com/products-winrar.html 109 | Scroll Down //h3[contains(text(),'FEATURES')] 110 | Scroll Up //h3[contains(text(),'OVERVIEW')] 111 | Scroll Down To Text SYSTEM REQUIREMENTS 112 | Scroll Up To Text RAR Products 113 | -------------------------------------------------------------------------------- /test/Mobile/iOSMobileTests.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Documentation ApplicationLibrary Mobile Library Tests. Requires Appium Server running on port 4723 with an Android device/emulator available. 3 | Library ApplicationLibrary.MobileLibrary 4 | Suite Setup Start App 5 | Suite Teardown Close Application 6 | Force Tags Mobile 7 | 8 | *** Variables *** 9 | ${REMOTE_URL} https://ondemand.us-west-1.saucelabs.com/wd/hub 10 | ${commandTimeout}= 120 11 | 12 | *** Keywords *** 13 | Start App 14 | Open Application ${REMOTE_URL} platformName=iOS deviceName=iPhone XS Simulator 15 | ... newCommandTimeout=${commandTimeout} browserName=Safari username=${sauce_username} 16 | ... access_key=${sauce_key} platformVersion=14.5 name=GitHub.${SUITE_NAME} 17 | ... build=${GITHUB_RUN_NUMBER} 18 | 19 | *** Test Cases *** 20 | Scroll To Text In WebView Keyword Test 21 | Go To Url https://www.win-rar.com/products-winrar.html 22 | Scroll Down //h3[contains(text(),'FEATURES')] 23 | Scroll Up //h3[contains(text(),'OVERVIEW')] 24 | Scroll Down To Text SYSTEM REQUIREMENTS 25 | Scroll Up To Text RAR Products 26 | 27 | Wait For And Click Element By Id Keyword Test 28 | Background App -1 29 | Switch To Context NATIVE_APP 30 | Wait For And Click Element Contacts 31 | Wait Until Page Contains Contacts 32 | 33 | Wait For And Click Element By Xpath Keyword Test 34 | Background App -1 35 | Wait For And Click Element //XCUIElementTypeIcon[@name="Contacts"] 36 | Wait Until Page Contains Contacts 37 | 38 | Wait For And Input Text/Password Keyword Test 39 | Wait For And Input Text Search John 40 | Wait Until Page Contains John Appleseed 41 | Wait For And Click Element //XCUIElementTypeButton[@name="Cancel"] 42 | Wait For And Input Password Search John 43 | Wait Until Page Contains John Appleseed 44 | Wait For And Click Element //XCUIElementTypeButton[@name="Cancel"] 45 | 46 | Wait For And Long Press Keyword Test 47 | Wait For And Long Press //XCUIElementTypeCell[@name="John Appleseed"]/XCUIElementTypeOther[1]/XCUIElementTypeOther 48 | Wait Until Page Contains Element message 49 | Click A Point 10 100 50 | 51 | Wait Until Element is Enabled Keyword Test 52 | Wait Until Element Is Enabled Add 53 | 54 | Wait Until Element Contains / Does Not Contain 55 | Wait For And Input Text Search John 56 | Wait Until Element Contains Search John 57 | Wait For And Click Element //XCUIElementTypeButton[@name="Cancel"] 58 | Wait Until Element Does Not Contain Search John 59 | 60 | Drag and Drop Keyword Test 61 | Background App -1 62 | Wait For And Long Press Files 63 | Wait For And Click Element OK 64 | Drag and Drop Files Watch 65 | Page Should Contain Element Folder 66 | Drag and Drop By Offset Shortcuts 180 0 67 | Page Should Contain Element //XCUIElementTypeIcon[@name="Folder"]/XCUIElementTypeIcon[@name="Shortcuts"] 68 | Run keyword And Expect Error ValueError: Element locator 'Not_a_real_id' did not match any elements. 69 | ... Drag and Drop Not_a_real_id Watch 70 | Wait For And Click Element Done 71 | 72 | Scroll To Text In NativeApp Keyword Test 73 | Background App -1 74 | Swipe By Percent 10 50 90 50 75 | Wait For And Click Element Settings 76 | Wait Until Page Contains Settings 77 | Scroll Down To Text Developer 78 | Element Should Be Visible Developer 79 | Scroll Up To Text General 80 | Element Should Be Visible General 81 | Scroll Down To Text Developer 82 | Element Should Be Visible Developer 83 | Scroll Up General 84 | Element Should Be Visible General 85 | 86 | Save Selenium Screenshot Test 87 | ${file1}= Save Appium Screenshot 88 | ${file2}= Save Appium Screenshot 89 | Should Not Be Equal ${file1} ${file2} 90 | Should Match Regexp ${file1} appium-screenshot-\\d{8,10}.\\d{6,8}-\\d.png 91 | -------------------------------------------------------------------------------- /test/Mobile/test_mobile.py: -------------------------------------------------------------------------------- 1 | from ApplicationLibrary.MobileLibrary import MobileLibrary 2 | import unittest 3 | from appium import webdriver 4 | from unittest.mock import MagicMock 5 | from appium.webdriver.common.touch_action import TouchAction 6 | import sys 7 | import os 8 | sys.path.append(os.path.join(os.path.dirname(sys.path[0]), 'Helpers')) 9 | from webdriverremotemock import WebdriverRemoteMock 10 | 11 | 12 | class TestInternal(unittest.TestCase): 13 | 14 | def test_import_defaults(self): 15 | MobileLibrary() 16 | 17 | def test_import_overrides(self): 18 | MobileLibrary(timeout=10, run_on_failure='Capture Page Screenshot') 19 | 20 | def test_wait_for_and_input_text_simple(self): 21 | mock_desk = MagicMock() 22 | webdriver.Remote = WebdriverRemoteMock 23 | MobileLibrary.open_application(mock_desk, 'remote_url') 24 | MobileLibrary.wait_for_and_clear_text(mock_desk, "some_locator") 25 | mock_desk.clear_text.assert_called_with("some_locator") 26 | 27 | def test_wait_for_and_click_element(self): 28 | mock_desk = MagicMock() 29 | webdriver.Remote = WebdriverRemoteMock 30 | MobileLibrary.open_application(mock_desk, 'remote_url') 31 | MobileLibrary.wait_for_and_click_element(mock_desk, "some_locator") 32 | mock_desk.click_element.assert_called_with("some_locator") 33 | 34 | def test_wait_for_and_click_text(self): 35 | mock_desk = MagicMock() 36 | webdriver.Remote = WebdriverRemoteMock 37 | MobileLibrary.open_application(mock_desk, 'remote_url') 38 | MobileLibrary.wait_for_and_click_text(mock_desk, "some_text") 39 | mock_desk.click_text.assert_called_with("some_text", False) 40 | 41 | def test_wait_for_and_click_text_exact(self): 42 | mock_desk = MagicMock() 43 | webdriver.Remote = WebdriverRemoteMock 44 | MobileLibrary.open_application(mock_desk, 'remote_url') 45 | MobileLibrary.wait_for_and_click_text(mock_desk, "some_text", True) 46 | mock_desk.click_text.assert_called_with("some_text", True) 47 | 48 | def test_wait_for_and_click_button(self): 49 | mock_desk = MagicMock() 50 | webdriver.Remote = WebdriverRemoteMock 51 | MobileLibrary.open_application(mock_desk, 'remote_url') 52 | MobileLibrary.wait_for_and_click_button(mock_desk, "some_button") 53 | mock_desk.click_button.assert_called_with("some_button") 54 | 55 | def test_wait_for_and_input_password(self): 56 | mock_desk = MagicMock() 57 | webdriver.Remote = WebdriverRemoteMock 58 | MobileLibrary.open_application(mock_desk, 'remote_url') 59 | MobileLibrary.wait_for_and_input_password(mock_desk, "some_locator", "some_text") 60 | mock_desk.input_password.assert_called_with("some_locator", "some_text") 61 | 62 | def test_wait_for_and_input_text(self): 63 | mock_desk = MagicMock() 64 | webdriver.Remote = WebdriverRemoteMock 65 | MobileLibrary.open_application(mock_desk, 'remote_url') 66 | MobileLibrary.wait_for_and_input_text(mock_desk, "some_locator", "some_text") 67 | mock_desk.input_text.assert_called_with("some_locator", "some_text") 68 | 69 | def test_wait_for_and_input_value(self): 70 | mock_desk = MagicMock() 71 | webdriver.Remote = WebdriverRemoteMock 72 | MobileLibrary.open_application(mock_desk, 'remote_url') 73 | MobileLibrary.wait_for_and_input_value(mock_desk, "some_locator", "some_value") 74 | mock_desk.input_value.assert_called_with("some_locator", "some_value") 75 | 76 | def test_wait_for_and_long_press(self): 77 | mock_desk = MagicMock() 78 | webdriver.Remote = WebdriverRemoteMock 79 | MobileLibrary.open_application(mock_desk, 'remote_url') 80 | MobileLibrary.wait_for_and_long_press(mock_desk, "some_locator", 1000) 81 | mock_desk.long_press.assert_called_with("some_locator", 1000) 82 | 83 | def test_wait_until_element_contains(self): 84 | mock_desk = MagicMock() 85 | webdriver.Remote = WebdriverRemoteMock 86 | MobileLibrary.open_application(mock_desk, 'remote_url') 87 | MobileLibrary.wait_until_element_contains(mock_desk, "some_locator", 'test_text') 88 | mock_desk.element_should_contain_text.assert_called_with("some_locator", 'test_text', None) 89 | 90 | def test_wait_until_element_does_not_contain(self): 91 | mock_desk = MagicMock() 92 | webdriver.Remote = WebdriverRemoteMock 93 | MobileLibrary.open_application(mock_desk, 'remote_url') 94 | MobileLibrary.wait_until_element_does_not_contain(mock_desk, "some_locator", 'test_text') 95 | mock_desk.element_should_not_contain_text.assert_called_with("some_locator", 'test_text', None) 96 | 97 | def test_wait_until_element_is_enabled(self): 98 | mock_desk = MagicMock() 99 | webdriver.Remote = WebdriverRemoteMock 100 | MobileLibrary.open_application(mock_desk, 'remote_url') 101 | MobileLibrary.wait_until_element_is_enabled(mock_desk, "some_locator") 102 | mock_desk.element_should_be_enabled.assert_called_with("some_locator") 103 | 104 | def test_wait_until_element_is_disabled(self): 105 | mock_desk = MagicMock() 106 | webdriver.Remote = WebdriverRemoteMock 107 | MobileLibrary.open_application(mock_desk, 'remote_url') 108 | MobileLibrary.wait_until_element_is_disabled(mock_desk, "some_locator") 109 | mock_desk.element_should_be_disabled.assert_called_with("some_locator") 110 | 111 | def test_drag_and_drop(self): 112 | mock_desk = MagicMock() 113 | webdriver.Remote = WebdriverRemoteMock 114 | TouchAction.move_to = MagicMock() 115 | TouchAction.release = MagicMock() 116 | MobileLibrary.open_application(mock_desk, 'remote_url') 117 | MobileLibrary.drag_and_drop(mock_desk, "some_locator", "some_other_locator") 118 | mock_desk._platform_dependant_press.assert_called() 119 | TouchAction.move_to.assert_called_with(unittest.mock.ANY) 120 | TouchAction.release.assert_called() 121 | 122 | def test_drag_and_drop_ios(self): 123 | mock_desk = MagicMock() 124 | mock_desk._get_platform = MagicMock(return_value='ios') 125 | webdriver.Remote = WebdriverRemoteMock 126 | TouchAction.move_to = MagicMock() 127 | TouchAction.release = MagicMock() 128 | MobileLibrary.open_application(mock_desk, 'remote_url') 129 | MobileLibrary.drag_and_drop(mock_desk, "some_locator", "some_other_locator") 130 | mock_desk._platform_dependant_press.assert_called() 131 | TouchAction.move_to.assert_called_with(unittest.mock.ANY) 132 | TouchAction.release.assert_called() 133 | 134 | def test_drag_and_drop_missing_source(self): 135 | mock_desk = MagicMock() 136 | webdriver.Remote = WebdriverRemoteMock 137 | mock_desk._element_find.side_effect = ValueError 138 | MobileLibrary.open_application(mock_desk, 'remote_url') 139 | self.assertRaises(ValueError, MobileLibrary.drag_and_drop, mock_desk, "some_locator", "some_other_locator") 140 | 141 | def test_drag_and_drop_missing_target(self): 142 | mock_desk = MagicMock() 143 | webdriver.Remote = WebdriverRemoteMock 144 | mock_desk._element_find.side_effect = [True, ValueError] 145 | MobileLibrary.open_application(mock_desk, 'remote_url') 146 | self.assertRaises(ValueError, MobileLibrary.drag_and_drop, mock_desk, "some_locator", "some_other_locator") 147 | 148 | def test_drag_and_drop_with_offset(self): 149 | mock_desk = MagicMock() 150 | webdriver.Remote = WebdriverRemoteMock 151 | TouchAction.move_to = MagicMock() 152 | TouchAction.release = MagicMock() 153 | MobileLibrary.open_application(mock_desk, 'remote_url') 154 | MobileLibrary.drag_and_drop_by_offset(mock_desk, "some_locator", x_offset=100, y_offset=100) 155 | mock_desk._platform_dependant_press.assert_called() 156 | TouchAction.move_to.assert_called_with(x=unittest.mock.ANY, y=unittest.mock.ANY) 157 | TouchAction.release.assert_called() 158 | 159 | def test_drag_and_drop_with_offset_ios(self): 160 | mock_desk = MagicMock() 161 | mock_desk._get_platform = MagicMock(return_value='ios') 162 | webdriver.Remote = WebdriverRemoteMock 163 | TouchAction.move_to = MagicMock() 164 | TouchAction.release = MagicMock() 165 | MobileLibrary.open_application(mock_desk, 'remote_url') 166 | MobileLibrary.drag_and_drop_by_offset(mock_desk, "some_locator", x_offset=100, y_offset=100) 167 | mock_desk._platform_dependant_press.assert_called() 168 | TouchAction.move_to.assert_called_with(x=unittest.mock.ANY, y=unittest.mock.ANY) 169 | TouchAction.release.assert_called() 170 | 171 | def test_drag_and_drop_with_offset_missing_locator(self): 172 | mock_desk = MagicMock() 173 | webdriver.Remote = WebdriverRemoteMock 174 | mock_desk._element_find.side_effect = ValueError 175 | MobileLibrary.open_application(mock_desk, 'remote_url') 176 | self.assertRaises(ValueError, MobileLibrary.drag_and_drop_by_offset, mock_desk, "some_locator", 177 | x_offset=100, y_offset=100) 178 | 179 | def test_scroll_up(self): 180 | mock_desk = MagicMock() 181 | webdriver.Remote = WebdriverRemoteMock 182 | mock_desk.get_current_context = MagicMock(return_value="Web") 183 | MobileLibrary.open_application(mock_desk, 'remote_url') 184 | MobileLibrary.scroll_up(mock_desk, "some_locator") 185 | mock_desk._element_find()._execute.assert_called() 186 | 187 | def test_scroll_up_native_app(self): 188 | mock_desk = MagicMock() 189 | webdriver.Remote = WebdriverRemoteMock 190 | mock_desk._current_application().execute_script = MagicMock() 191 | mock_desk.get_current_context = MagicMock(return_value="NATIVE") 192 | MobileLibrary.open_application(mock_desk, 'remote_url') 193 | MobileLibrary.scroll_up(mock_desk, "some_locator") 194 | mock_desk._current_application().execute_script.assert_called() 195 | 196 | def test_scroll_up_to_text(self): 197 | mock_desk = MagicMock() 198 | webdriver.Remote = WebdriverRemoteMock 199 | mock_desk.get_current_context = MagicMock(return_value="Web") 200 | MobileLibrary.open_application(mock_desk, 'remote_url') 201 | MobileLibrary.scroll_up_to_text(mock_desk, "some_locator") 202 | mock_desk._element_find_by_text()._execute.assert_called() 203 | 204 | def test_scroll_up_to_text_native_app(self): 205 | mock_desk = MagicMock() 206 | webdriver.Remote = WebdriverRemoteMock 207 | mock_desk._current_application().execute_script = MagicMock() 208 | mock_desk.get_current_context = MagicMock(return_value="NATIVE") 209 | MobileLibrary.open_application(mock_desk, 'remote_url') 210 | MobileLibrary.scroll_up_to_text(mock_desk, "some_locator") 211 | mock_desk._current_application().execute_script.assert_called() 212 | 213 | def test_scroll_up_to_text_last_option(self): 214 | mock_desk = MagicMock() 215 | webdriver.Remote = WebdriverRemoteMock 216 | mock_desk._current_application().execute_script = MagicMock(side_effect=ValueError) 217 | MobileLibrary.open_application(mock_desk, 'remote_url') 218 | MobileLibrary.scroll_up_to_text(mock_desk, "some_locator") 219 | mock_desk._scroll_to_text.assert_called() 220 | 221 | def test_scroll_down(self): 222 | mock_desk = MagicMock() 223 | webdriver.Remote = WebdriverRemoteMock 224 | mock_desk.get_current_context = MagicMock(return_value="Web") 225 | MobileLibrary.open_application(mock_desk, 'remote_url') 226 | MobileLibrary.scroll_down(mock_desk, "some_locator") 227 | mock_desk._element_find()._execute.assert_called() 228 | 229 | def test_scroll_down_native_app(self): 230 | mock_desk = MagicMock() 231 | webdriver.Remote = WebdriverRemoteMock 232 | mock_desk._current_application().execute_script = MagicMock() 233 | mock_desk.get_current_context = MagicMock(return_value="NATIVE") 234 | MobileLibrary.open_application(mock_desk, 'remote_url') 235 | MobileLibrary.scroll_down(mock_desk, "some_locator") 236 | mock_desk._current_application().execute_script.assert_called() 237 | 238 | def test_scroll_down_to_text(self): 239 | mock_desk = MagicMock() 240 | webdriver.Remote = WebdriverRemoteMock 241 | mock_desk.get_current_context = MagicMock(return_value="Web") 242 | MobileLibrary.open_application(mock_desk, 'remote_url') 243 | MobileLibrary.scroll_down_to_text(mock_desk, "some_locator") 244 | mock_desk._element_find_by_text()._execute.assert_called() 245 | 246 | def test_scroll_down_to_text_native_app(self): 247 | mock_desk = MagicMock() 248 | webdriver.Remote = WebdriverRemoteMock 249 | mock_desk._current_application().execute_script = MagicMock() 250 | mock_desk.get_current_context = MagicMock(return_value="NATIVE") 251 | MobileLibrary.open_application(mock_desk, 'remote_url') 252 | MobileLibrary.scroll_down_to_text(mock_desk, "some_locator") 253 | mock_desk._current_application().execute_script.assert_called() 254 | 255 | def test_scroll_down_to_text_last_option(self): 256 | mock_desk = MagicMock() 257 | webdriver.Remote = WebdriverRemoteMock 258 | mock_desk._current_application().execute_script = MagicMock(side_effect=ValueError) 259 | mock_desk._is_text_present.side_effect = [False, True] 260 | MobileLibrary.open_application(mock_desk, 'remote_url') 261 | MobileLibrary.scroll_down_to_text(mock_desk, "some_locator") 262 | mock_desk._scroll_to_text.assert_called() 263 | 264 | def test_wait_for_and_tap(self): 265 | mock_desk = MagicMock() 266 | webdriver.Remote = WebdriverRemoteMock 267 | MobileLibrary.open_application(mock_desk, 'remote_url') 268 | MobileLibrary.wait_for_and_tap(mock_desk, "some_locator") 269 | mock_desk.tap.assert_called_with("some_locator", None, None, 1) 270 | 271 | def test_wait_for_and_tap_multiple(self): 272 | mock_desk = MagicMock() 273 | webdriver.Remote = WebdriverRemoteMock 274 | MobileLibrary.open_application(mock_desk, 'remote_url') 275 | MobileLibrary.wait_for_and_tap(mock_desk, "some_locator", count=4) 276 | mock_desk.tap.assert_called_with("some_locator", None, None, 4) 277 | 278 | def test_wait_for_and_tap_override_defaults(self): 279 | mock_desk = MagicMock() 280 | webdriver.Remote = WebdriverRemoteMock 281 | MobileLibrary.open_application(mock_desk, 'remote_url') 282 | MobileLibrary.wait_for_and_tap(mock_desk, "some_locator", x_offset=200, y_offset=100, count=4, 283 | error='some_error', timeout='10s') 284 | mock_desk.tap.assert_called_with("some_locator", 200, 100, 4) 285 | 286 | def test_capture_page_screenshot(self): 287 | mock_desk = MagicMock() 288 | mock_desk._get_screenshot_paths = MagicMock(return_value=['path', 'link']) 289 | MobileLibrary.capture_page_screenshot(mock_desk) 290 | mock_desk._get_screenshot_paths.assert_called() 291 | 292 | def test_capture_page_screenshot_else_case(self): 293 | mock_desk = MagicMock() 294 | mock_desk._get_screenshot_paths = MagicMock(return_value=['path', 'link']) 295 | del mock_desk._current_application().get_screenshot_as_file 296 | MobileLibrary.capture_page_screenshot(mock_desk, 'filename') 297 | mock_desk._get_screenshot_paths.assert_called() 298 | 299 | def test_save_appium_screenshot(self): 300 | mock_desk = MagicMock() 301 | MobileLibrary.save_appium_screenshot(mock_desk) 302 | mock_desk.capture_page_screenshot.assert_called() 303 | 304 | def test_wait_until_page_contains_private(self): 305 | mock_desk = MagicMock() 306 | MobileLibrary._wait_until_page_contains(mock_desk, 'some_text', 5) 307 | mock_desk._wait_until.asser_called_with('some_text', 5) 308 | 309 | def test_wait_until_page_contains_element_private(self): 310 | mock_desk = MagicMock() 311 | MobileLibrary._wait_until_page_contains_element(mock_desk, 'some_element', 5) 312 | mock_desk._wait_until.assert_called_with(5, "Element 'some_element' did not appear in " 313 | "", unittest.mock.ANY, 'some_element') 314 | 315 | def test_platform_dependant_press_private(self): 316 | mock_desk = MagicMock() 317 | mock_desk._is_ios = MagicMock(return_value=False) 318 | MobileLibrary._platform_dependant_press(mock_desk, MagicMock(), 'some_element') 319 | 320 | def test_platform_dependant_press_ios_private(self): 321 | mock_desk = MagicMock() 322 | mock_desk._is_ios = MagicMock(return_value=True) 323 | MobileLibrary._platform_dependant_press(mock_desk, MagicMock(), 'some_element') 324 | 325 | def test_platform_dependant_press_delay_set_private(self): 326 | mock_desk = MagicMock() 327 | mock_desk._is_ios = MagicMock(return_value=True) 328 | MobileLibrary._platform_dependant_press(mock_desk, MagicMock(), 'some_element', delay=500) 329 | 330 | def test_element_find_by_text_ios(self): 331 | mock_desk = MagicMock() 332 | mock_desk._is_ios = MagicMock(return_value=True) 333 | webdriver.Remote = WebdriverRemoteMock 334 | mock_desk._element_find = MagicMock() 335 | MobileLibrary._element_find_by_text(mock_desk, "some_text") 336 | mock_desk._element_find.assert_called_with('some_text', True, False) 337 | 338 | def test_element_find_by_text_ios_exact(self): 339 | mock_desk = MagicMock() 340 | mock_desk._is_ios = MagicMock(return_value=True) 341 | webdriver.Remote = WebdriverRemoteMock 342 | mock_desk._element_find = MagicMock(return_value=False) 343 | MobileLibrary._element_find_by_text(mock_desk, "some_text", True) 344 | mock_desk._element_find.assert_called_with('//*[@value="some_text" or @label="some_text"]', True, True) 345 | 346 | def test_element_find_by_text_ios_not_exact(self): 347 | mock_desk = MagicMock() 348 | mock_desk._is_ios = MagicMock(return_value=True) 349 | webdriver.Remote = WebdriverRemoteMock 350 | mock_desk._element_find = MagicMock(return_value=False) 351 | MobileLibrary._element_find_by_text(mock_desk, "some_text", False) 352 | mock_desk._element_find.assert_called_with('//*[contains(@label,"some_text") or contains(@value, "some_text") ' 353 | 'or contains(text(), "some_text")]', True, True) 354 | 355 | def test_element_find_by_text_android_exact(self): 356 | mock_desk = MagicMock() 357 | mock_desk._is_ios = MagicMock(return_value=False) 358 | mock_desk._is_android = MagicMock(return_value=True) 359 | webdriver.Remote = WebdriverRemoteMock 360 | mock_desk._element_find = MagicMock(return_value=False) 361 | MobileLibrary._element_find_by_text(mock_desk, "some_text", True) 362 | mock_desk._element_find.assert_called_with('//*[@text="some_text"]', True, True) 363 | 364 | def test_element_find_by_text_android_not_exact(self): 365 | mock_desk = MagicMock() 366 | mock_desk._is_ios = MagicMock(return_value=False) 367 | mock_desk._is_android = MagicMock(return_value=True) 368 | webdriver.Remote = WebdriverRemoteMock 369 | mock_desk._element_find = MagicMock(return_value=False) 370 | MobileLibrary._element_find_by_text(mock_desk, "some_text", False) 371 | mock_desk._element_find.assert_called_with('//*[contains(@text,"some_text")]', True, True) 372 | 373 | def test_is_text_visible(self): 374 | mock_desk = MagicMock() 375 | webdriver.Remote = WebdriverRemoteMock 376 | mock_desk._element_find_by_text().is_displayed = MagicMock(return_value=True) 377 | result = MobileLibrary._is_text_visible(mock_desk, "some_text") 378 | self.assertTrue(result) 379 | 380 | def test_is_text_visible_not_found(self): 381 | mock_desk = MagicMock() 382 | webdriver.Remote = WebdriverRemoteMock 383 | mock_desk._element_find_by_text = MagicMock(return_value=None) 384 | result = MobileLibrary._is_text_visible(mock_desk, "some_text") 385 | self.assertIsNone(result) 386 | 387 | def test_scroll_to_text_ios_visible_no_scroll(self): 388 | mock_desk = MagicMock() 389 | webdriver.Remote = WebdriverRemoteMock 390 | mock_desk._is_android = MagicMock(return_value=False) 391 | mock_desk._is_ios = MagicMock(return_value=True) 392 | mock_desk._is_text_visible = MagicMock(return_value=True) 393 | result = MobileLibrary._scroll_to_text(mock_desk, "some_text", "up") 394 | self.assertTrue(result) 395 | 396 | def test_scroll_to_text_android_visible_no_scroll(self): 397 | mock_desk = MagicMock() 398 | webdriver.Remote = WebdriverRemoteMock 399 | mock_desk._is_ios = MagicMock(return_value=False) 400 | mock_desk._is_android = MagicMock(return_value=True) 401 | mock_desk._is_text_present = MagicMock(return_value=True) 402 | result = MobileLibrary._scroll_to_text(mock_desk, "some_text", "up") 403 | self.assertTrue(result) 404 | 405 | def test_scroll_to_text_ios_scroll(self): 406 | mock_desk = MagicMock() 407 | webdriver.Remote = WebdriverRemoteMock 408 | mock_desk._is_android = MagicMock(return_value=False) 409 | mock_desk._is_ios = MagicMock(return_value=True) 410 | mock_desk._is_text_visible = MagicMock(side_effect=[False, True]) 411 | result = MobileLibrary._scroll_to_text(mock_desk, "some_text", "up") 412 | self.assertTrue(result) 413 | 414 | def test_scroll_to_text_android_scroll(self): 415 | mock_desk = MagicMock() 416 | webdriver.Remote = WebdriverRemoteMock 417 | mock_desk._is_android = MagicMock(return_value=True) 418 | mock_desk._is_ios = MagicMock(return_value=False) 419 | mock_desk._is_text_present = MagicMock(side_effect=[False, True]) 420 | result = MobileLibrary._scroll_to_text(mock_desk, "some_text", "down") 421 | self.assertTrue(result) 422 | 423 | def test_scroll_to_text_bad_direction(self): 424 | mock_desk = MagicMock() 425 | webdriver.Remote = WebdriverRemoteMock 426 | mock_desk._is_android = MagicMock(return_value=True) 427 | mock_desk._is_ios = MagicMock(return_value=False) 428 | mock_desk._is_text_present = MagicMock(return_value=False) 429 | self.assertRaises(AssertionError, MobileLibrary._scroll_to_text, mock_desk, "some_text", "sideways") 430 | 431 | def test_scroll_to_text_not_found(self): 432 | mock_desk = MagicMock() 433 | webdriver.Remote = WebdriverRemoteMock 434 | mock_desk._is_android = MagicMock(return_value=True) 435 | mock_desk._is_ios = MagicMock(return_value=False) 436 | mock_desk._is_text_present = MagicMock(return_value=False) 437 | self.assertRaises(AssertionError, MobileLibrary._scroll_to_text, mock_desk, "some_text", "up") 438 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Accruent/robotframework-applicationlibrary/93f33e31dc7dd1c10a885dce91861aa20f140e5b/test/__init__.py -------------------------------------------------------------------------------- /version.py: -------------------------------------------------------------------------------- 1 | VERSION = "1.2.1" 2 | --------------------------------------------------------------------------------