diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index d9a06fe..c54c231 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -2,6 +2,8 @@ name: Bug report description: Create a report to help us improve title: "[BUG]: " labels: [ bug ] +assignees: Alex-Reif + body: - type: textarea id: description @@ -71,4 +73,4 @@ body: attributes: label: Additional context description: Add any other context about the problem here - placeholder: For example, screenshot or using of additional frameworks like Sizzle library, Robot Framework or JDI, etc. If you can please, send a link to your project. \ No newline at end of file + placeholder: For example, screenshot or using of additional frameworks like Sizzle library, Robot Framework or JDI, etc. If you can please, send a link to your project. diff --git a/.github/ISSUE_TEMPLATE/custom.yml b/.github/ISSUE_TEMPLATE/custom.yml index 68845c8..bdf75c4 100644 --- a/.github/ISSUE_TEMPLATE/custom.yml +++ b/.github/ISSUE_TEMPLATE/custom.yml @@ -2,7 +2,7 @@ name: Help-support template description: Describe you problem happened using Healenium and team will help title: "[Need support]: " labels: [ help wanted ] -assignees: ElenaStepuro +assignees: Alex-Reif body: - type: markdown attributes: @@ -58,4 +58,4 @@ body: attributes: label: Additional context description: Add any other context about the problem here - placeholder: For example, screenshot or using of additional frameworks like Sizzle library, Robot Framework or JDI, etc. If you can please, send a link to your project. \ No newline at end of file + placeholder: For example, screenshot or using of additional frameworks like Sizzle library, Robot Framework or JDI, etc. If you can please, send a link to your project. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b0b6f3a --- /dev/null +++ b/.gitignore @@ -0,0 +1,160 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/healenium-example-python.iml b/.idea/healenium-example-python.iml deleted file mode 100644 index 170d602..0000000 --- a/.idea/healenium-example-python.iml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2d..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 2d83d70..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index e30bcea..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index f249b4e..e1b14f3 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,113 @@ -# healenium +# Python Example with Healenium + +[![Docker Pulls](https://img.shields.io/docker/pulls/healenium/hlm-backend.svg?maxAge=25920)](https://hub.docker.com/u/healenium) +[![License](https://img.shields.io/badge/license-Apache-brightgreen.svg)](https://www.apache.org/licenses/LICENSE-2.0) + Python 3.9.5 + Pytest project with healenium usage example +[1. Start Healenium components](#1-start-healenium-components) +* [Healenium with Selenium-Grid](#run-healenium-with-selenium-grid) +* [Healenium with Selenoid](#run-healenium-with-selenoid) + + +[2. Configuration RemoteWebDriver for Healenium](#2-configuration-remotewebdriver-for-healenium) + +[3. Run test](#3-run-test) + +[4. Monitoring tests running](#4-monitoring-tests-running) + ## How to start -### 1.Start Healenium backend from 'infra' folder -```cd infra``` +### 1. Start Healenium components + +Go into healenium folder + +```sh +cd healenium +``` + +#### Run Healenium with Selenium-Grid: +```sh +docker-compose up -d +``` + +#### Run Healenium with Selenoid + +> Note: `browsers.json` consists of target browsers and appropriate versions. +> Before run healenium you have to manually pull selenoid browser docker images with version specified in browsers.json + +Example pull selenoid chrome image: +```sh +docker pull selenoid/vnc:chrome_111.0 +``` +Full list of browser images you can find [here](https://hub.docker.com/u/selenoid) + + +Run healenium with Selenoid: +```sh +docker-compose -f docker-compose-selenoid.yaml up -d +``` + +ATTENTION + +Verify the next images are Up and Running +- `postgres-db` (PostgreSQL database to store etalon selector / healing / report) +- `hlm-proxy` (Proxy client request to Selenium server) +- `hlm-backend` (CRUD service) +- `selector imitator` (Convert healed locator to convenient format) +- `selenoid`/`selenium-grid` (Selenium server) + +### 2. Configuration RemoteWebDriver for Healenium + +To run using Healenium create RemoteWebDriver with URL ```http://:8085```: + +```py + options = webdriver.ChromeOptions() + self.driver = webdriver.Remote('http://localhost:8085/', options=options) +``` + +To temporarily disable the healing mechanism for certain sections of your code, use the following syntax: -```docker-compose up -d``` +```py + self.driver.execute_script("disable_healing_true") + ... // Your code that does not require healing + self.driver.execute_script("disable_healing_false") +``` -Verify that images ```healenium/hlm-backend:3.2.0```, ```postgres:11-alpine```, ```healenium/hlm-selector-imitator```, ```healenium/hlm-selenium-4-standalone-xpra``` and ```healenium/hlm-proxy:0.2.1``` are up and running +### 3. Run test +To run tests in terminal with pytest you need to go to execute next commands: -### 2. Project structure +```sh +python3 -m venv env ``` -|__healenium-example-python (root) - |__src - |__main - |__locators - |__pages - |__tests - |__selenium tests - |__infra - |__docker-compose.yml -``` - -### 3.Run test -To run tests in terminal with pytest you need to go to ```cd healenium_selenium\tests``` project folder +```sh +source ./env/bin/activate +``` -> If you want to execute tests from test_callback.py file, please use the command: ```pytest test_callback.py``` -> And appropriate command for test_markup.py file: ```pytest test_markup.py``` ->> In case you want to run all tests in project use ```pytest``` command +```sh +python -m pip install -U pytest +``` + +```sh +python -m pip install -U selenium +``` + +```sh +pytest +``` + +> If you want to execute tests from specified file, please use the command: ```python -m pytest ./tests/test_css.py``` +>> In case you want to run all tests in project use ```python -m pytest ./tests/``` command ### 4. Monitoring tests running -You can monitor tests running. To do this go to ```http://:8086``` +You can monitor tests running if you using Healenium with Selenoid plus Selenoid Ui, go to:
+```sh +http://localhost:8080 +``` + +## Community / Support + +* [Telegram chat](https://t.me/healenium) +* [YouTube Channel](https://www.youtube.com/channel/UCsZJ0ri-Hp7IA1A6Fgi4Hvg) + diff --git a/healenium/LICENSE b/healenium/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/healenium/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/healenium/README.md b/healenium/README.md new file mode 100644 index 0000000..bce8fee --- /dev/null +++ b/healenium/README.md @@ -0,0 +1,175 @@ +# Healenium + +[![Docker Pulls](https://img.shields.io/docker/pulls/healenium/hlm-backend.svg?maxAge=25920)](https://hub.docker.com/u/healenium) +[![License](https://img.shields.io/badge/license-Apache-brightgreen.svg)](https://www.apache.org/licenses/LICENSE-2.0) +[![@healenium](https://img.shields.io/badge/Telegram-%40healenium-orange.svg)](https://t.me/healenium) + +### Table of Contents + +[Overall information](#overall-information) + +[Healenium installation](#healenium-installation) +* [Healenium with Selenium-Grid](#run-healenium-with-selenium-grid) +* [Healenium with Selenoid](#run-healenium-with-selenoid) +* [Healenium with Appium](#run-healenium-with-appium-only) + +[Healenium installation without Docker](#healenium-installation-without-docker) + +[Language Examples](#language-examples) +* [Java](#java) +* [Python](#python) +* [C#](#c#) +* [JavaScript](#javascript) + +### Overall information +Self-healing framework based on Selenium and able to use all Selenium supported languages like Java/Python/JS/C# +Healenium acts as proxy between client and selenium server. + +`Docker-compose` includes the following services: +- `postgres-db` (PostgreSQL database to store etalon selector / healing / report) +- `hlm-proxy` (Proxy client request to Selenium server) +- `hlm-backend` (CRUD service) +- `selector imitator` (Convert healed locator to convenient format) +- `selenoid`/`selenium-grid` (Selenium server) + +image + + +### Healenium installation + +Clone Healenium repository: +```sh +git clone https://github.com/healenium/healenium.git +``` + +#### Run Healenium with Selenium-Grid: +```sh +docker-compose up -d +``` + +#### Run Healenium with Selenoid + +> Note: `browsers.json` consists of target browsers and appropriate versions. +> Before run healenium you have to manually pull selenoid browser docker images with version specified in browsers.json + +Example pull selenoid chrome image: +```sh +docker pull selenoid/vnc:chrome_111.0 +``` +Full list of browser images you can find [here](https://hub.docker.com/u/selenoid) + + +Run healenium with Selenoid: +```sh +docker-compose -f docker-compose-selenoid.yaml up -d +``` + +#### Run Healenium with Appium only + +```sh +docker-compose -f docker-compose-appium.yaml up -d +``` +More details about integration Healenium with Appium [here](https://github.com/healenium/healenium-appium) + + +### Healenium installation without Docker + +Go to shell-installation: + +```sh +cd shell-installatio +``` + +There are web and remote options to run healenium. + +1. Start PostgeSql server. +- Create user (healenium_user/YDk2nmNs4s9aCP6K) (example data) +- Set attribute 'Can Login' (true) to user +- Create database (healenium) and set owner healenium_user +- Create schema (healenium) and set owner healenium_user + +2. Specify your db user and password data in the bash script 'start_healenium.sh'. + +3. Setup selenium server (selenium-grid) + +Download healenium services +```sh +download_services.sh +``` + +Run shell command to launch healenium components +```sh +start_healenium.sh +``` + + +### Language examples + +``` + /** + * "http://127.0.0.1:8085/" OR "http://localhost:8085/" if you are using locally running proxy server + * + * if you want to use a remote proxy server, + * specify the ip address of this server - "http://remote_ip_address:8085/" + */ +``` + +###### Java: +```java + String nodeURL = "http://localhost:8085/"; + + ChromeOptions options = new ChromeOptions(); + options.addArguments("--no-sandbox"); + options.addArguments("--disable-dev-shm-usage"); + + WebDriver driver = new RemoteWebDriver(new URL(nodeURL), options); +``` + +###### Python +```py + nodeURL = "http://localhost:8085/" + + options = webdriver.ChromeOptions() + options.add_argument('--no-sandbox') + + current_webdriver = webdriver.Remote( + command_executor=nodeURL, + desired_capabilities=webdriver.DesiredCapabilities.CHROME, + options=options, + ) +``` + +###### C# +```csharp + String nodeURL = "http://localhost:8085/"; + + ChromeOptions optionsChrome = new ChromeOptions(); + optionsChrome.AddArguments("--no-sandbox"); + + RemoteWebDriver driverChrome = new RemoteWebDriver(new Uri(nodeURL), optionsChrome); +``` + +###### JavaScript +```javascript + const NODE_URL = "http://localhost:8085/"; + + let args = [ + "--no-sandbox" + ]; + + let chromeCapabilities = selenium.Capabilities.chrome() + .set('chromeOptions', { args }); + + let builder = new selenium.Builder() + .forBrowser('chrome') + .withCapabilities(chromeCapabilities); + + let driver = await builder.usingServer(NODE_URL).build(); +``` + + +## Community / Support + +* [Telegram chat](https://t.me/healenium) +* [GitHub Issues](https://github.com/healenium/healenium/issues) +* [YouTube Channel](https://www.youtube.com/channel/UCsZJ0ri-Hp7IA1A6Fgi4Hvg) diff --git a/infra/db/sql/init.sql b/healenium/db/sql/init.sql similarity index 100% rename from infra/db/sql/init.sql rename to healenium/db/sql/init.sql diff --git a/healenium/docker-compose-appium.yaml b/healenium/docker-compose-appium.yaml new file mode 100644 index 0000000..d1472db --- /dev/null +++ b/healenium/docker-compose-appium.yaml @@ -0,0 +1,75 @@ +version: "3.8" + +services: + + postgres-db: + image: postgres:15.5-alpine + container_name: postgres-db + restart: always + ports: + - "5432:5432" + volumes: + - ./db/sql/init.sql:/docker-entrypoint-initdb.d/init.sql + environment: + - POSTGRES_DB=healenium + - POSTGRES_USER=healenium_user + - POSTGRES_PASSWORD=YDk2nmNs4s9aCP6K + networks: + - healenium + + healenium: + image: healenium/hlm-backend:3.4.6 + container_name: healenium + restart: on-failure + ports: + - "7878:7878" + links: + - postgres-db + environment: + - SPRING_POSTGRES_DB=healenium + - SPRING_POSTGRES_SCHEMA=healenium + - SPRING_POSTGRES_USER=healenium_user + - SPRING_POSTGRES_PASSWORD=YDk2nmNs4s9aCP6K + - SPRING_POSTGRES_DB_HOST=postgres-db + - KEY_SELECTOR_URL=false + - COLLECT_METRICS=true + - FIND_ELEMENTS_AUTO_HEALING=false + - HLM_LOG_LEVEL=info + volumes: + - ./screenshots/:/screenshots + - ./logs/:/logs + networks: + - healenium + + selector-imitator: + image: healenium/hlm-selector-imitator:1.4 + container_name: selector-imitator + restart: on-failure + ports: + - "8000:8000" + networks: + - healenium + + hlm-proxy: + image: healenium/hlm-proxy:2.1.1 + container_name: hlm-proxy + restart: on-failure + ports: + - "8085:8085" + environment: + - RECOVERY_TRIES=1 + - SCORE_CAP=.6 + - HEAL_ENABLED=true + - SELENIUM_SERVER_URL=http://host.docker.internal:4723/wd/hub + - HEALENIUM_SERVER_URL=http://localhost:7878 + - HEALENIUM_SERVICE=http://healenium:7878 + - IMITATE_SERVICE=http://selector-imitator:8000 + - HLM_LOG_LEVEL=info + volumes: + - ./logs/:/logs + networks: + - healenium + +networks: + healenium: + name: healenium diff --git a/healenium/docker-compose-selenoid.yaml b/healenium/docker-compose-selenoid.yaml new file mode 100644 index 0000000..3cba688 --- /dev/null +++ b/healenium/docker-compose-selenoid.yaml @@ -0,0 +1,99 @@ +version: "3.8" + +services: + + postgres-db: + image: postgres:15.5-alpine + container_name: postgres-db + restart: always + ports: + - "5432:5432" + volumes: + - ./db/sql/init.sql:/docker-entrypoint-initdb.d/init.sql + environment: + - POSTGRES_DB=healenium + - POSTGRES_USER=healenium_user + - POSTGRES_PASSWORD=YDk2nmNs4s9aCP6K + networks: + - healenium + + healenium: + image: healenium/hlm-backend:3.4.6 + container_name: healenium + restart: on-failure + ports: + - "7878:7878" + links: + - postgres-db + environment: + - SPRING_POSTGRES_DB=healenium + - SPRING_POSTGRES_SCHEMA=healenium + - SPRING_POSTGRES_USER=healenium_user + - SPRING_POSTGRES_PASSWORD=YDk2nmNs4s9aCP6K + - SPRING_POSTGRES_DB_HOST=postgres-db + - KEY_SELECTOR_URL=false + - COLLECT_METRICS=true + - FIND_ELEMENTS_AUTO_HEALING=false + - HLM_LOG_LEVEL=info + volumes: + - ./screenshots/:/screenshots + - ./logs/:/logs + networks: + - healenium + + selector-imitator: + image: healenium/hlm-selector-imitator:1.4 + container_name: selector-imitator + restart: on-failure + ports: + - "8000:8000" + networks: + - healenium + + hlm-proxy: + image: healenium/hlm-proxy:2.1.1 + container_name: hlm-proxy + restart: on-failure + ports: + - "8085:8085" + environment: + - RECOVERY_TRIES=1 + - SCORE_CAP=.6 + - HEAL_ENABLED=true + - SELENIUM_SERVER_URL=http://selenoid:4444/wd/hub + - HEALENIUM_SERVER_URL=http://localhost:7878 + - HEALENIUM_SERVICE=http://healenium:7878 + - IMITATE_SERVICE=http://selector-imitator:8000 + - HLM_LOG_LEVEL=info + volumes: + - ./logs/:/logs + networks: + - healenium + + selenoid: + image: aerokube/selenoid:latest-release + volumes: + - ./selenoid-config:/etc/selenoid + - /var/run/docker.sock:/var/run/docker.sock + - ./selenoid-config/video:/opt/selenoid/video + - ./selenoid-config/logs:/opt/selenoid/logs + environment: + - OVERRIDE_VIDEO_OUTPUT_DIR=/path/to/config/video + command: ["-conf", "/etc/selenoid/browsers.json", "-video-output-dir", "/opt/selenoid/video", "-log-output-dir", "/opt/selenoid/logs", "-container-network", "healenium"] + ports: + - "4444:4444" + networks: + - healenium + + selenoid-ui: + image: aerokube/selenoid-ui:latest-release + restart: on-failure + ports: + - "8080:8080" + command: [ "--selenoid-uri", "http://selenoid:4444/" ] + networks: + - healenium + +networks: + healenium: + name: healenium diff --git a/healenium/docker-compose-web.yaml b/healenium/docker-compose-web.yaml new file mode 100644 index 0000000..12f9291 --- /dev/null +++ b/healenium/docker-compose-web.yaml @@ -0,0 +1,54 @@ +version: "3.8" + +services: + + postgres-db: + image: postgres:15.5-alpine + container_name: postgres-db + restart: always + ports: + - "5432:5432" + volumes: + - ./db/sql/init.sql:/docker-entrypoint-initdb.d/init.sql + environment: + - POSTGRES_DB=healenium + - POSTGRES_USER=healenium_user + - POSTGRES_PASSWORD=YDk2nmNs4s9aCP6K + networks: + - healenium + + healenium: + image: healenium/hlm-backend:3.4.6 + container_name: healenium + restart: on-failure + ports: + - "7878:7878" + links: + - postgres-db + environment: + - SPRING_POSTGRES_DB=healenium + - SPRING_POSTGRES_SCHEMA=healenium + - SPRING_POSTGRES_USER=healenium_user + - SPRING_POSTGRES_PASSWORD=YDk2nmNs4s9aCP6K + - SPRING_POSTGRES_DB_HOST=postgres-db + - KEY_SELECTOR_URL=false + - COLLECT_METRICS=true + - HLM_LOG_LEVEL=info + volumes: + - ./screenshots/:/screenshots + - ./logs/:/logs + networks: + - healenium + + selector-imitator: + image: healenium/hlm-selector-imitator:1.4 + container_name: selector-imitator + restart: on-failure + ports: + - "8000:8000" + networks: + - healenium + +networks: + healenium: + name: healenium diff --git a/healenium/docker-compose.yaml b/healenium/docker-compose.yaml new file mode 100644 index 0000000..2a8ff27 --- /dev/null +++ b/healenium/docker-compose.yaml @@ -0,0 +1,127 @@ +version: "3.8" + +services: + + postgres-db: + image: postgres:15.5-alpine + container_name: postgres-db + restart: always + ports: + - "5432:5432" + volumes: + - ./db/sql/init.sql:/docker-entrypoint-initdb.d/init.sql + environment: + - POSTGRES_DB=healenium + - POSTGRES_USER=healenium_user + - POSTGRES_PASSWORD=YDk2nmNs4s9aCP6K + networks: + - healenium + + healenium: + image: healenium/hlm-backend:3.4.6 + container_name: healenium + restart: on-failure + ports: + - "7878:7878" + links: + - postgres-db + environment: + - SPRING_POSTGRES_DB=healenium + - SPRING_POSTGRES_SCHEMA=healenium + - SPRING_POSTGRES_USER=healenium_user + - SPRING_POSTGRES_PASSWORD=YDk2nmNs4s9aCP6K + - SPRING_POSTGRES_DB_HOST=postgres-db + - KEY_SELECTOR_URL=false + - COLLECT_METRICS=true + - FIND_ELEMENTS_AUTO_HEALING=false + - HLM_LOG_LEVEL=info + volumes: + - ./screenshots/:/screenshots + - ./logs/:/logs + networks: + - healenium + + selector-imitator: + image: healenium/hlm-selector-imitator:1.4 + container_name: selector-imitator + restart: on-failure + ports: + - "8000:8000" + networks: + - healenium + + hlm-proxy: + image: healenium/hlm-proxy:2.1.1 + container_name: hlm-proxy + restart: on-failure + ports: + - "8085:8085" + environment: + - RECOVERY_TRIES=1 + - SCORE_CAP=.6 + - HEAL_ENABLED=true + - SELENIUM_SERVER_URL=http://selenium-hub:4444/wd/hub + - HEALENIUM_SERVER_URL=http://localhost:7878 + - HEALENIUM_SERVICE=http://healenium:7878 + - IMITATE_SERVICE=http://selector-imitator:8000 + - HLM_LOG_LEVEL=info + volumes: + - ./logs/:/logs + networks: + - healenium + + chrome: + image: selenium/node-chrome:latest + container_name: node-chrome + shm_size: 2gb + depends_on: + - selenium-hub + environment: + - SE_EVENT_BUS_HOST=selenium-hub + - SE_EVENT_BUS_PUBLISH_PORT=4442 + - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 + - SE_NODE_MAX_INSTANCES=5 + - SE_NODE_MAX_SESSIONS=5 + - SE_NODE_SESSION_TIMEOUT=20 + networks: + - healenium + + edge: + image: selenium/node-edge:latest + container_name: node-edge + shm_size: 2gb + depends_on: + - selenium-hub + environment: + - SE_EVENT_BUS_HOST=selenium-hub + - SE_EVENT_BUS_PUBLISH_PORT=4442 + - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 + networks: + - healenium + + firefox: + image: selenium/node-firefox:latest + container_name: node-firefox + shm_size: 2gb + depends_on: + - selenium-hub + environment: + - SE_EVENT_BUS_HOST=selenium-hub + - SE_EVENT_BUS_PUBLISH_PORT=4442 + - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 + networks: + - healenium + + selenium-hub: + image: selenium/hub:latest + container_name: selenium-hub + ports: + - "4442:4442" + - "4443:4443" + - "4444:4444" + networks: + - healenium + +networks: + healenium: + name: healenium diff --git a/healenium/selenoid-config/browsers.json b/healenium/selenoid-config/browsers.json new file mode 100644 index 0000000..9807ec3 --- /dev/null +++ b/healenium/selenoid-config/browsers.json @@ -0,0 +1,30 @@ +{ + "chrome": { + "default": "111.0", + "versions": { + "111.0": { + "image": "selenoid/vnc:chrome_111.0", + "port": "4444" + }, + "110.0": { + "image": "selenoid/vnc:chrome_110.0", + "port": "4444" + } + } + }, + "firefox": { + "default": "111.0", + "versions": { + "111.0": { + "image": "selenoid/vnc:firefox_111.0", + "port": "4444", + "path": "/wd/hub" + }, + "110.0": { + "image": "selenoid/vnc:firefox_110.0", + "port": "4444", + "path": "/wd/hub" + } + } + } +} diff --git a/healenium/shell-installation/selenium-grid/download_services.sh b/healenium/shell-installation/selenium-grid/download_services.sh new file mode 100644 index 0000000..90b33c1 --- /dev/null +++ b/healenium/shell-installation/selenium-grid/download_services.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Versions of the services +HLM_BACKEND_VERSION="3.4.6" +HLM_PROXY_VERSION="2.1.1" + +# Downloading +curl -L https://github.com/healenium/healenium-backend/releases/download/${HLM_BACKEND_VERSION}/healenium-backend-${HLM_BACKEND_VERSION}.jar > hlm-backend.jar + +curl -L https://github.com/healenium/healenium-proxy/releases/download/${HLM_PROXY_VERSION}/hlm-proxy-${HLM_PROXY_VERSION}.jar > hlm-proxy.jar + +git clone https://github.com/healenium/healenium-selector-imitator.git diff --git a/healenium/shell-installation/selenium-grid/start_healenium.sh b/healenium/shell-installation/selenium-grid/start_healenium.sh new file mode 100644 index 0000000..88fe583 --- /dev/null +++ b/healenium/shell-installation/selenium-grid/start_healenium.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# Requirements hlm_backend +HLM_POSTGRES_DB=healenium +HLM_POSTGRES_SCHEMA=healenium +HLM_POSTGRES_USER=healenium_user +HLM_POSTGRES_PASSWORD=YDk2nmNs4s9aCP6K +HLM_COLLECT_METRICS=true +HLM_SERVER_PORT=7878 +HLM_LOG_LEVEL=info + +# Requirements hlm_proxy +RECOVERY_TRIES=1 +SCORE_CAP=.6 +HEAL_ENABLED=true +SELENIUM_SERVER_URL=http://localhost:4444 +HEALENIUM_SERVER_URL=http://localhost:7878 +HEALENIUM_SERVICE=http://localhost:7878 +IMITATE_SERVICE=http://localhost:8000 + +# Deploy the hlm-backend service +SPRING_POSTGRES_DB=$HLM_POSTGRES_DB SPRING_POSTGRES_SCHEMA=$HLM_POSTGRES_SCHEMA SPRING_POSTGRES_USER=$HLM_POSTGRES_USER SPRING_POSTGRES_PASSWORD=$HLM_POSTGRES_PASSWORD COLLECT_METRICS=$HLM_COLLECT_METRICS SPRING_SERVER_PORT=$HLM_SERVER_PORT HLM_LOG_LEVEL=$HLM_LOG_LEVEL java -jar hlm-backend.jar 2>&1 & echo $! > ./pid-hlm-backend.file & + +# Deploy the hlm-proxy service +RECOVERY_TRIES=$RECOVERY_TRIES SCORE_CAP=$SCORE_CAP HEAL_ENABLED=$HEAL_ENABLED SELENIUM_SERVER_URL=$SELENIUM_SERVER_URL HEALENIUM_SERVER_URL=$HEALENIUM_SERVER_URL HEALENIUM_SERVICE=$HEALENIUM_SERVICE IMITATE_SERVICE=$IMITATE_SERVICE HLM_LOG_LEVEL=$HLM_LOG_LEVEL java -jar hlm-proxy.jar 2>&1 & echo $! > ./pid-hlm-proxy.file & + +# Deploy the imitator service +pip install --upgrade pip + +pip install -r healenium-selector-imitator/requirements.txt + +if [[ $OSTYPE == 'msys'* ]]; +then + python healenium-selector-imitator/app.py & echo $! > ./pid-selector.file & +else + python3 healenium-selector-imitator/app.py & echo $! > ./pid-selector.file & +fi diff --git a/healenium/shell-installation/selenium-grid/stop_healenium.sh b/healenium/shell-installation/selenium-grid/stop_healenium.sh new file mode 100644 index 0000000..813ac0d --- /dev/null +++ b/healenium/shell-installation/selenium-grid/stop_healenium.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +kill $(cat ./pid-hlm-backend.file) $(cat ./pid-hlm-proxy.file) $(cat ./pid-selector.file) \ No newline at end of file diff --git a/healenium/shell-installation/web/download_services.sh b/healenium/shell-installation/web/download_services.sh new file mode 100644 index 0000000..a35c6be --- /dev/null +++ b/healenium/shell-installation/web/download_services.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Versions of the services +HLM_BACKEND_VERSION="3.4.6" + +# Downloading +curl -L https://github.com/healenium/healenium-backend/releases/download/${HLM_BACKEND_VERSION}/healenium-backend-${HLM_BACKEND_VERSION}.jar > hlm-backend.jar + +git clone https://github.com/healenium/healenium-selector-imitator.git diff --git a/healenium/shell-installation/web/start_healenium.sh b/healenium/shell-installation/web/start_healenium.sh new file mode 100644 index 0000000..d05a43e --- /dev/null +++ b/healenium/shell-installation/web/start_healenium.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# Requirements hlm_backend +HLM_POSTGRES_DB=healenium +HLM_POSTGRES_SCHEMA=healenium +HLM_POSTGRES_USER=healenium_user +HLM_POSTGRES_PASSWORD=YDk2nmNs4s9aCP6K +HLM_COLLECT_METRICS=true +HLM_SERVER_PORT=7878 +HLM_LOG_LEVEL=info + +# Deploy the hlm-backend service +SPRING_POSTGRES_DB=$HLM_POSTGRES_DB SPRING_POSTGRES_SCHEMA=$HLM_POSTGRES_SCHEMA SPRING_POSTGRES_USER=$HLM_POSTGRES_USER SPRING_POSTGRES_PASSWORD=$HLM_POSTGRES_PASSWORD COLLECT_METRICS=$HLM_COLLECT_METRICS SPRING_SERVER_PORT=$HLM_SERVER_PORT HLM_LOG_LEVEL=$HLM_LOG_LEVEL java -jar hlm-backend.jar 2>&1 & echo $! > ./pid-hlm-backend.file & + +# Deploy the imitator service +pip install --upgrade pip + +pip install -r healenium-selector-imitator/requirements.txt + +if [[ $OSTYPE == 'msys'* ]]; +then + python healenium-selector-imitator/app.py & echo $! > ./pid-selector.file & +else + python3 healenium-selector-imitator/app.py & echo $! > ./pid-selector.file & +fi diff --git a/healenium/shell-installation/web/stop_healenium.sh b/healenium/shell-installation/web/stop_healenium.sh new file mode 100644 index 0000000..d71537d --- /dev/null +++ b/healenium/shell-installation/web/stop_healenium.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +kill $(cat ./pid-hlm-backend.file) $(cat ./pid-selector.file) \ No newline at end of file diff --git a/infra/docker-compose.yaml b/infra/docker-compose.yaml deleted file mode 100644 index 002fae1..0000000 --- a/infra/docker-compose.yaml +++ /dev/null @@ -1,81 +0,0 @@ -version: "3.9" - -services: - - db: - image: postgres:11-alpine - ports: - - "5432:5432" - volumes: - - ./db/sql/init.sql:/docker-entrypoint-initdb.d/init.sql - restart: always - environment: - - POSTGRES_DB=healenium - - POSTGRES_USER=healenium_user - - POSTGRES_PASSWORD=YDk2nmNs4s9aCP6K - networks: - - healenium - - healenium: - image: healenium/hlm-backend:3.2.0 - container_name: healenium - ports: - - "7878:7878" - links: - - db - environment: - - SPRING_CONTAINER_NAME=healenium - - SPRING_POSTGRES_DB=healenium - - SPRING_POSTGRES_USER=healenium_user - - SPRING_POSTGRES_PASSWORD=YDk2nmNs4s9aCP6K - - SPRING_POSTGRES_URL=jdbc:postgresql://db:5432/healenium?currentSchema=healenium - networks: - - healenium - - selector-imitator: - image: healenium/hlm-selector-imitator:1 - container_name: selector-imitator - restart: on-failure - ports: - - "8000:8000" - networks: - - healenium - - hlm-proxy: - image: healenium/hlm-proxy:0.2.1 - container_name: hlm-proxy - ports: - - "8085:8085" - volumes: - - docker-log-volume:/var/log/dockerlogs - environment: - - "recovery-tries=1" - - "score-cap=.6" - - "heal-enabled=true" - - "serverHost=localhost" - - "serverPort=7878" - - "imitatePort=8000" - networks: - - healenium - - hlm-selenium-3-standalone-tigervnc: - # image: healenium/hlm-selenium-4-standalone-xpra:0.1.1 #для версии XPRA селениум 4.0.0-rc-1 - #image: healenium/hlm-selenium-4-standalone-xpra:1.0 - image: healenium/hlm-selenium-3-standalone-tigervnc:0.1.0 #для версии vnc селениум 3.141.59 - - restart: on-failure - container_name: hlm-selenium-3-standalone-tigervnc - ports: - - "4444:4444" - - "8086:6080" - # - "8086:10000" - volumes: - - docker-log-volume:/var/log/dockerlogs - networks: - - healenium - -volumes: - docker-log-volume: - -networks: - healenium: \ No newline at end of file diff --git a/src/main/constants/locator_type.py b/src/main/constants/locator_type.py new file mode 100644 index 0000000..3c5beec --- /dev/null +++ b/src/main/constants/locator_type.py @@ -0,0 +1,12 @@ +import enum + + +class LocatorType(enum.Enum): + xpath = 'xpath', + css = 'css', + id = 'id', + link_text = 'link text', + name = 'name', + partial_link_text = 'partial link text', + tag = 'tag', + class_name = 'class name' diff --git a/src/main/locators/__pycache__/callback_locators.cpython-38.pyc b/src/main/locators/__pycache__/callback_locators.cpython-38.pyc deleted file mode 100644 index 7ba6e49..0000000 Binary files a/src/main/locators/__pycache__/callback_locators.cpython-38.pyc and /dev/null differ diff --git a/src/main/locators/__pycache__/callback_locators.cpython-39.pyc b/src/main/locators/__pycache__/callback_locators.cpython-39.pyc deleted file mode 100644 index 1d6c5d6..0000000 Binary files a/src/main/locators/__pycache__/callback_locators.cpython-39.pyc and /dev/null differ diff --git a/src/main/locators/__pycache__/markup_locators.cpython-39.pyc b/src/main/locators/__pycache__/markup_locators.cpython-39.pyc deleted file mode 100644 index 2bc3e8b..0000000 Binary files a/src/main/locators/__pycache__/markup_locators.cpython-39.pyc and /dev/null differ diff --git a/src/main/locators/callback_locators.py b/src/main/locators/callback_locators.py deleted file mode 100644 index ef49be1..0000000 --- a/src/main/locators/callback_locators.py +++ /dev/null @@ -1,9 +0,0 @@ - -class Locators(object): - - add_square_button = '//button[contains(@class, "add")]' - update_square_button = '//button[contains(@class, "update")]' - remove_square_button = '//button[contains(@class, "remove")]' - - test_button = '//custom-square[contains(@c, "red")]' - test_button_css = '[c="red"]' \ No newline at end of file diff --git a/src/main/locators/markup_locators.py b/src/main/locators/markup_locators.py deleted file mode 100644 index dced6ca..0000000 --- a/src/main/locators/markup_locators.py +++ /dev/null @@ -1,25 +0,0 @@ - -class Locators(object): - # by id - generate_markup_btn_id = 'markup-generation-button' - test_generated_button = 'random-id' - # by class name - test_button = 'default-btn' - - # by xpath - checkbox_account = '//*[@class="checkbox checkbox_size_m checkbox_theme_alfa-on-white"]' - text_first_select = '(//*[text()="Select Account"])[1]' - text_second_select = '(//*[text()="Select Account"])[2]' - -from selenium.webdriver.common.by import By -class Locators_By(object): - generate_markup_btn_id=(By.ID, 'markup-generation-button') - test_button=(By.XPATH, '//button[contains(@class,"default-btn")]') - - button_invisible=(By.ID,'for-invisible-test') - field_parent=(By.ID,'field-parent') - -class Locators_Request(object): - generate_markup_btn_id = {'by': By.ID, 'value': 'markup-generation-button'} - test_generated_button = {'by': By.ID, 'value': 'random-id'} - test_button = {'by': By.CLASS_NAME, 'value': 'default-btn'} \ No newline at end of file diff --git a/src/main/pages/base_page.py b/src/main/pages/base_page.py index 316bde4..eea3cba 100644 --- a/src/main/pages/base_page.py +++ b/src/main/pages/base_page.py @@ -1,10 +1,14 @@ -class Base_Page(object): +from selenium.webdriver.common.alert import Alert + + +class BasePage(object): mainPageUrl = 'https://sha-test-app.herokuapp.com/' callbackTestPageUrl = 'https://mdn.github.io/web-components-examples/life-cycle-callbacks/' + testEnvPageUrl = 'https://elenastepuro.github.io/test_env/index.html' def __init__(self, driver): self.driver = driver def confirm_alert(self): - allert = self.driver.switch_to_alert() - allert.accept() + Alert(self.driver).accept() + diff --git a/src/main/pages/callback_page.py b/src/main/pages/callback_page.py index 94f32a0..fc7bcb7 100644 --- a/src/main/pages/callback_page.py +++ b/src/main/pages/callback_page.py @@ -1,30 +1,44 @@ +from selenium import webdriver from selenium.webdriver.common.by import By +from src.main.pages.base_page import BasePage -from src.main.locators.callback_locators import Locators -from src.main.pages.base_page import Base_Page +class CallbackPage(BasePage): -class Callback_Page(Base_Page): + add_square_button = '//button[contains(@class, "add")]' + update_square_button = '//button[contains(@class, "update")]' + remove_square_button = '//button[contains(@class, "remove")]' + + test_button = '//custom-square[contains(@c, "red")]' + test_button_css = '[color="red"]' + + driver: webdriver + + def __init__(self): + options = webdriver.ChromeOptions() + options.add_argument('--no-sandbox') + options.add_argument("--disable-dev-shm-usage") + self.driver = webdriver.Remote( + command_executor="http://localhost:8085/", + options=options) def open_browser(self): - self.driver.get(Base_Page.callbackTestPageUrl); + self.driver.get(BasePage.callbackTestPageUrl) return self def click_add_square_button(self): - self.add_square_button = self.driver.find_element(By.XPATH, Locators.add_square_button) - self.add_square_button.click() - - def verify_shadow_element(self): - shadowRoot = self.driver.find_element(By.XPATH, Locators.test_button); - button = self.driver.execute_script("return arguments[0].shadowRoot", shadowRoot) - return button.is_enabled() + add_square_button = self.driver.find_element(By.XPATH, self.add_square_button) + add_square_button.click() def verify_square_element(self): - square = self.driver.find_element(By.CSS_SELECTOR, Locators.test_button_css) + square = self.driver.find_element(By.CSS_SELECTOR, self.test_button_css) return square.is_enabled() def click_update_square_button(self): - self.driver.find_element(By.XPATH, Locators.update_square_button).click() + self.driver.find_element(By.XPATH, self.update_square_button).click() def click_remove_square_button(self): - self.driver.find_element(By.XPATH, Locators.remove_square_button).click() + self.driver.find_element(By.XPATH, self.remove_square_button).click() + + def close(self): + self.driver.quit() diff --git a/src/main/pages/markup_page.py b/src/main/pages/markup_page.py index 1476b42..f4c8a1c 100644 --- a/src/main/pages/markup_page.py +++ b/src/main/pages/markup_page.py @@ -1,95 +1,45 @@ -from selenium.common.exceptions import NoSuchElementException -from selenium.webdriver.support import expected_conditions -from selenium.webdriver.support.ui import WebDriverWait +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.support.wait import WebDriverWait +from selenium.webdriver.support import expected_conditions as ec -from src.main.locators.markup_locators import * -from src.main.pages.base_page import Base_Page +from src.main.pages.base_page import BasePage -class Markup_Page(Base_Page): +class MarkupPage(BasePage): - def open_browser(self): - self.driver.get(Base_Page.mainPageUrl); - return self - - def generate_markup(self): - generate_markup = self.driver.find_element_by_id(Locators.generate_markup_btn_id) - generate_markup.click() - return self - - def click_test_button(self): - self.driver.find_element_by_class_name(Locators.test_button).click() + # by id + generate_markup_btn_id = 'markup-generation-button' + # by class name + test_button = 'default-btn' - def click_test_generated_button(self): - self.driver.find_element_by_id(Locators.test_generated_button).click() + driver: webdriver - def test_button_id_enabled(self): - try: - return self.driver.find_element_by_id(Locators.test_generated_button).is_enabled() - except(NoSuchElementException): - return False - - def displayed_text(self): - try: - return self.driver.find_element_by_xpath(Locators.text_first_select).is_enabled() - except(NoSuchElementException): - return False - - def select_first_checkbox(self): - self.driver.find_element_by_xpath(Locators.checkbox_account).click() - - def verify_first_checkbox(self): - return self.driver.find_element_by_xpath(Locators.checkbox_account).is_enabled() - - -class Markup_Page_By(Base_Page): + def __init__(self): + options = webdriver.ChromeOptions() + options.add_argument('--no-sandbox') + options.add_argument("--disable-dev-shm-usage") + self.driver = webdriver.Remote( + command_executor="http://localhost:8085/", + desired_capabilities=webdriver.DesiredCapabilities.CHROME, + options=options) def open_browser(self): - self.driver.get(Base_Page.mainPageUrl); + self.driver.get(BasePage.mainPageUrl) return self def generate_markup(self): - generate_markup = self.driver.find_element(*Locators_By.generate_markup_btn_id) + generate_markup = self.driver.find_element_by_id(self.generate_markup_btn_id) generate_markup.click() return self def click_test_button(self): - test_button = self.driver.find_element(*Locators_By.test_button) - test_button.click() - return self - - def click_button_for_invisible(self): - button_invisible = self.driver.find_element(*Locators_By.button_invisible) - button_invisible.click() - return self - - def check_that_button_invisible(self): - try: - WebDriverWait(self.driver, 5).until( - expected_conditions.invisibility_of_element(*Locators_By.button_invisible)) - return True - except (Exception): - return False - - -class Markup_Page_Request(Base_Page): + self.driver.find_element_by_class_name(self.test_button).click() - def open_browser(self): - self.driver.get(Base_Page.mainPageUrl) - return self + def close(self): + self.driver.quit() - def generate_markup(self): - generate_markup = self.driver.find_element(**Locators_Request.generate_markup_btn_id) - generate_markup.click() - return self - - def generate_markup(self): - generate_markup = self.driver.find_element(**Locators_Request.generate_markup_btn_id) - generate_markup.click() - return self - - def click_test_button(self): - self.driver.find_element(**Locators_Request.test_button).click() + def click_test_button_wait(self, seconds): + test_btn = WebDriverWait(self.driver, seconds).until(ec.visibility_of_element_located((By.XPATH, "//*[contains(@class, 'default-btn')]"))) + test_btn.click() - def click_test_generated_button(self): - self.driver.find_element(**Locators_Request.test_generated_button).click() \ No newline at end of file diff --git a/src/main/pages/testenv_page.py b/src/main/pages/testenv_page.py new file mode 100644 index 0000000..65916b8 --- /dev/null +++ b/src/main/pages/testenv_page.py @@ -0,0 +1,63 @@ +import logging + +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.support.wait import WebDriverWait + +from src.main.pages.base_page import BasePage +from src.main.search.context import Context +from selenium.webdriver.support import expected_conditions as ec + + +class TestEnvPage(BasePage): + __test__ = False + submit_btn = 'Submit' + submit_form_btn = 'Submit_checkbox' + + driver: webdriver + + def __init__(self): + options = webdriver.ChromeOptions() + self.driver = webdriver.Remote('http://localhost:8085/', options=options) + + def open_browser(self): + self.driver.get(BasePage.testEnvPageUrl) + return self + + def select_checkboxes(self): + checkboxes = self.driver.find_elements(By.XPATH, "//*[contains(@class,'test-form')]//*[@class='input1']") + for ch in checkboxes: + ch.click() + return self + + def click_form_submit_btn(self): + self.driver.find_element_by_id(self.submit_form_btn).click() + return self + + def click_submit_btn(self): + self.driver.find_element_by_id(self.submit_btn).click() + return self + + def select_checkboxes_under_parent(self): + checkboxes = self.driver.find_element(By.XPATH, "//*[contains(@class,'test-form')]").find_elements(By.XPATH, + ".//*[@class='input1']") + for ch in checkboxes: + ch.click() + return self + + def find_test_element(self, locator_type, selector): + logging.info("Find element By ") + result = Context().set_strategy(self.driver, locator_type).execute_strategy(selector) + assert result == True + + def click_wait_btn(self): + self.driver.find_element(By.ID, "Wait_Submit").click() + + def execute_script(self, script): + self.driver.execute_script(script) + + def click_test_button_wait(self): + WebDriverWait(self.driver, 10).until(ec.visibility_of_element_located((By.ID, "wait_new_element"))) + + def close(self): + self.driver.quit() diff --git a/src/main/search/context.py b/src/main/search/context.py new file mode 100644 index 0000000..1b0ae15 --- /dev/null +++ b/src/main/search/context.py @@ -0,0 +1,37 @@ +from src.main.constants.locator_type import LocatorType +from src.main.search.locators.class_name_strategy import ClassNameStrategy +from src.main.search.locators.css_strategy import CssStrategy +from src.main.search.locators.id_strategy import IdStrategy +from src.main.search.locators.link_text_strategy import LinkTextStrategy +from src.main.search.locators.name_strategy import NameStrategy +from src.main.search.locators.partial_link_text_strategy import PartialLinkTextStrategy +from src.main.search.locators.tag_strategy import TagStrategy +from src.main.search.locators.xpath_strategy import XpathStrategy +from src.main.search.strategy import Strategy + + +class Context: + strategy: Strategy + + def set_strategy(self, driver, locator_type): + if locator_type == LocatorType.class_name: + self.strategy = ClassNameStrategy(driver) + if locator_type == LocatorType.xpath: + self.strategy = XpathStrategy(driver) + if locator_type == LocatorType.css: + self.strategy = CssStrategy(driver) + if locator_type == LocatorType.name: + self.strategy = NameStrategy(driver) + if locator_type == LocatorType.tag: + self.strategy = TagStrategy(driver) + if locator_type == LocatorType.partial_link_text: + self.strategy = PartialLinkTextStrategy(driver) + if locator_type == LocatorType.link_text: + self.strategy = LinkTextStrategy(driver) + if locator_type == LocatorType.id: + self.strategy = IdStrategy(driver) + + return self + + def execute_strategy(self, selector): + return self.strategy.do_action(selector) diff --git a/src/main/search/locators/class_name_strategy.py b/src/main/search/locators/class_name_strategy.py new file mode 100644 index 0000000..a9bf4a5 --- /dev/null +++ b/src/main/search/locators/class_name_strategy.py @@ -0,0 +1,7 @@ +from src.main.search.strategy import Strategy + + +class ClassNameStrategy(Strategy): + + def do_action(self) -> str: + pass diff --git a/src/main/search/locators/css_strategy.py b/src/main/search/locators/css_strategy.py new file mode 100644 index 0000000..a38670c --- /dev/null +++ b/src/main/search/locators/css_strategy.py @@ -0,0 +1,15 @@ +from selenium import webdriver +from selenium.webdriver.common.by import By + +from src.main.search.strategy import Strategy + + +class CssStrategy(Strategy): + driver: webdriver + + def __init__(self, driver1): + self.driver = driver1 + + def do_action(self, selector) -> str: + element = self.driver.find_element(By.CSS_SELECTOR, selector) + return element.is_displayed() diff --git a/src/main/search/locators/id_strategy.py b/src/main/search/locators/id_strategy.py new file mode 100644 index 0000000..b9c0c20 --- /dev/null +++ b/src/main/search/locators/id_strategy.py @@ -0,0 +1,15 @@ +from selenium import webdriver +from selenium.webdriver.common.by import By + +from src.main.search.strategy import Strategy + + +class IdStrategy(Strategy): + driver: webdriver + + def __init__(self, driver1): + self.driver = driver1 + + def do_action(self, selector) -> str: + element = self.driver.find_element(By.ID, selector) + return element.is_displayed() diff --git a/src/main/search/locators/link_text_strategy.py b/src/main/search/locators/link_text_strategy.py new file mode 100644 index 0000000..7fa9cf0 --- /dev/null +++ b/src/main/search/locators/link_text_strategy.py @@ -0,0 +1,15 @@ +from selenium import webdriver +from selenium.webdriver.common.by import By + +from src.main.search.strategy import Strategy + + +class LinkTextStrategy(Strategy): + driver: webdriver + + def __init__(self, driver1): + self.driver = driver1 + + def do_action(self, selector) -> str: + element = self.driver.find_element(By.LINK_TEXT, selector) + return element.is_displayed() diff --git a/src/main/search/locators/name_strategy.py b/src/main/search/locators/name_strategy.py new file mode 100644 index 0000000..a2452c6 --- /dev/null +++ b/src/main/search/locators/name_strategy.py @@ -0,0 +1,15 @@ +from selenium import webdriver +from selenium.webdriver.common.by import By + +from src.main.search.strategy import Strategy + + +class NameStrategy(Strategy): + driver: webdriver + + def __init__(self, driver1): + self.driver = driver1 + + def do_action(self, selector) -> str: + element = self.driver.find_element(By.NAME, selector) + return element.is_displayed() diff --git a/src/main/search/locators/partial_link_text_strategy.py b/src/main/search/locators/partial_link_text_strategy.py new file mode 100644 index 0000000..5da8397 --- /dev/null +++ b/src/main/search/locators/partial_link_text_strategy.py @@ -0,0 +1,15 @@ +from selenium import webdriver +from selenium.webdriver.common.by import By + +from src.main.search.strategy import Strategy + + +class PartialLinkTextStrategy(Strategy): + driver: webdriver + + def __init__(self, driver1): + self.driver = driver1 + + def do_action(self, selector) -> str: + element = self.driver.find_element(By.PARTIAL_LINK_TEXT, selector) + return element.is_displayed() diff --git a/src/main/search/locators/tag_strategy.py b/src/main/search/locators/tag_strategy.py new file mode 100644 index 0000000..afcae87 --- /dev/null +++ b/src/main/search/locators/tag_strategy.py @@ -0,0 +1,15 @@ +from selenium import webdriver +from selenium.webdriver.common.by import By + +from src.main.search.strategy import Strategy + + +class TagStrategy(Strategy): + driver: webdriver + + def __init__(self, driver1): + self.driver = driver1 + + def do_action(self, selector) -> str: + element = self.driver.find_element(By.TAG_NAME, selector) + return element.is_displayed() diff --git a/src/main/search/locators/xpath_strategy.py b/src/main/search/locators/xpath_strategy.py new file mode 100644 index 0000000..e500d9f --- /dev/null +++ b/src/main/search/locators/xpath_strategy.py @@ -0,0 +1,15 @@ +from selenium import webdriver +from selenium.webdriver.common.by import By + +from src.main.search.strategy import Strategy + + +class XpathStrategy(Strategy): + driver: webdriver + + def __init__(self, driver1): + self.driver = driver1 + + def do_action(self, selector) -> str: + element = self.driver.find_element(By.XPATH, selector) + return element.is_displayed() diff --git a/src/main/search/strategy.py b/src/main/search/strategy.py new file mode 100644 index 0000000..3458fa3 --- /dev/null +++ b/src/main/search/strategy.py @@ -0,0 +1,7 @@ +from abc import ABC + + +class Strategy(ABC): + + def do_action(self, selector) -> str: + pass diff --git a/tests/test_base.py b/tests/test_base.py deleted file mode 100644 index 3fefabe..0000000 --- a/tests/test_base.py +++ /dev/null @@ -1,23 +0,0 @@ -import pytest -from selenium import webdriver - - -class Test_Base(): - - @pytest.fixture() - def setup_method(self): - options = webdriver.ChromeOptions() - options.add_argument('--no-sandbox') - self.driver = webdriver.Remote( - command_executor="http://localhost:8085/", - desired_capabilities=webdriver.DesiredCapabilities.CHROME, - options=options) - - # options = webdriver.FirefoxOptions() - # browser = webdriver.Remote( - # command_executor="http://localhost:4444/wd/hub/", - # desired_capabilities=webdriver.DesiredCapabilities.FIREFOX, - # options=options) - - def teardown_method(self, method): - self.driver.quit() diff --git a/tests/test_callback.py b/tests/test_callback.py deleted file mode 100644 index bc36fcf..0000000 --- a/tests/test_callback.py +++ /dev/null @@ -1,29 +0,0 @@ -from src.main.pages.callback_page import Callback_Page -from test_base import Test_Base - - -class Test_Callback(Test_Base): - - def test_element_from_shadow_root(self, setup_method): - callback_page=Callback_Page(self.driver) - - callback_page.open_browser() - callback_page.click_add_square_button() - result = callback_page.verify_shadow_element() - assert result == True - - callback_page.click_update_square_button() - result = callback_page.verify_shadow_element() #should be healed - assert result == True - - def test_css_locators(self, setup_method): - callback_page=Callback_Page(self.driver) - - callback_page.open_browser() - callback_page.click_add_square_button() - result = callback_page.verify_square_element() - assert result==True - - callback_page.click_update_square_button() - result = callback_page.verify_square_element() #should be healed - assert result == True \ No newline at end of file diff --git a/tests/test_css.py b/tests/test_css.py new file mode 100644 index 0000000..3772205 --- /dev/null +++ b/tests/test_css.py @@ -0,0 +1,90 @@ +from src.main.constants.locator_type import LocatorType +from src.main.pages.callback_page import CallbackPage +from src.main.pages.testenv_page import TestEnvPage + + +class TestCss: + + def test_css_attribute(self): + callback_page = CallbackPage() + + callback_page.open_browser() + callback_page.click_add_square_button() + result = callback_page.verify_square_element() + assert result == True + + callback_page.click_update_square_button() + result = callback_page.verify_square_element() # should be healed + assert result == True + + callback_page.close() + + def test_css_id(self): + test_page = TestEnvPage() + + test_page.open_browser() + test_page.find_test_element(LocatorType.css, "#change_id") + test_page.click_submit_btn() + test_page.find_test_element(LocatorType.css, "#change_id") + + test_page.close() + + def test_css_id_special_character(self): + test_page = TestEnvPage() + + test_page.open_browser() + test_page.find_test_element(LocatorType.css, "input#change\\:name") + test_page.click_submit_btn() + test_page.find_test_element(LocatorType.css, "input#change\\:name") + + test_page.close() + + def test_css_element(self): + test_page = TestEnvPage() + + test_page.open_browser() + test_page.find_test_element(LocatorType.css, "test_tag") + test_page.click_submit_btn() + test_page.find_test_element(LocatorType.css, "test_tag") + + test_page.close() + + def test_css_disabled(self): + test_page = TestEnvPage() + + test_page.open_browser() + test_page.find_test_element(LocatorType.css, "input:disabled") + test_page.click_submit_btn() + test_page.find_test_element(LocatorType.css, "input:disabled") + + test_page.close() + + def test_css_enabled(self): + test_page = TestEnvPage() + + test_page.open_browser() + test_page.find_test_element(LocatorType.css, "textarea:enabled") + test_page.click_submit_btn() + test_page.find_test_element(LocatorType.css, "textarea:enabled") + + test_page.close() + + def test_css_checked(self): + test_page = TestEnvPage() + + test_page.open_browser() + test_page.find_test_element(LocatorType.css, "input:checked") + test_page.click_submit_btn() + test_page.find_test_element(LocatorType.css, "input:checked") + + test_page.close() + + def test_css_class_name(self): + test_page = TestEnvPage() + + test_page.open_browser() + test_page.find_test_element(LocatorType.css, ".test_class") + test_page.click_submit_btn() + test_page.find_test_element(LocatorType.css, ".test_class") + + test_page.close() diff --git a/tests/test_general.py b/tests/test_general.py new file mode 100644 index 0000000..37be91f --- /dev/null +++ b/tests/test_general.py @@ -0,0 +1,14 @@ +from src.main.pages.testenv_page import TestEnvPage + + +class TestGeneral: + + def test_select_checkboxes(self): + test_page = TestEnvPage() + + test_page.open_browser() + test_page.select_checkboxes() + test_page.click_form_submit_btn() + test_page.select_checkboxes() # should be healed + + test_page.close() diff --git a/tests/test_markup.py b/tests/test_markup.py deleted file mode 100644 index 21e4025..0000000 --- a/tests/test_markup.py +++ /dev/null @@ -1,72 +0,0 @@ -from src.main.pages.markup_page import * -from test_base import Test_Base - - -class Test_Markup(Test_Base): - -# Markup_Page: different locator types with string property - def test_button_click_specific_find_element(self, setup_method): - main_page = Markup_Page(self.driver) - - main_page.open_browser() - main_page.click_test_button() - main_page.confirm_alert() - - main_page.generate_markup() - main_page.click_test_button() #should be healed - main_page.confirm_alert() - - def test_select_checkboxes(self, setup_method): - main_page = Markup_Page(self.driver) - main_page.open_browser().generate_markup() - - while main_page.displayed_text() != True: - main_page.generate_markup() - - for i in [0,1,2,3,4,5]: - main_page.select_first_checkbox() #should be healed - - result = main_page.verify_first_checkbox() #should be healed - assert result == True - - def test_ButtonClickWithId(self, setup_method): - main_page = Markup_Page(self.driver) - - main_page.open_browser().click_test_button() - main_page.confirm_alert() - - while main_page.test_button_id_enabled()!=True: - main_page.generate_markup() - - for i in [0,1,2]: - main_page.click_test_generated_button() #should be healed - main_page.confirm_alert() - main_page.generate_markup() - -# Markup_Page_By: different locator types with By in property - def test_button_click_find_by(self, setup_method): - markup_page_by=Markup_Page_By(self.driver) - - markup_page_by.open_browser() - markup_page_by.click_button_for_invisible() - invisible = markup_page_by.check_that_button_invisible() - - markup_page_by.open_browser() - markup_page_by.click_test_button() - markup_page_by.confirm_alert() - - markup_page_by.generate_markup() - markup_page_by.click_test_button() #should be healed - markup_page_by.confirm_alert() - -# Markup_Page_Request: different locator types with By in request - def test_button_click_find_request(self, setup_method): - markup_page_request=Markup_Page_Request(self.driver) - - markup_page_request.open_browser() - markup_page_request.click_test_button() - markup_page_request.confirm_alert() - - markup_page_request.generate_markup() - markup_page_request.click_test_button() #should be healed - markup_page_request.confirm_alert() \ No newline at end of file diff --git a/tests/test_parent_child.py b/tests/test_parent_child.py new file mode 100644 index 0000000..e233101 --- /dev/null +++ b/tests/test_parent_child.py @@ -0,0 +1,45 @@ +from src.main.constants.locator_type import LocatorType +from src.main.pages.testenv_page import TestEnvPage + + +class TestParentChild: + + def test_select_checkboxes_under_parent(self): + test_page = TestEnvPage() + + test_page.open_browser() + test_page.select_checkboxes_under_parent() + test_page.click_form_submit_btn() + test_page.select_checkboxes_under_parent() + + test_page.close() + + def test_parent_xpath(self): + testenv_page = TestEnvPage() + + testenv_page.open_browser() + testenv_page.find_test_element(LocatorType.xpath, "(//*[@class='input1']//parent::*[contains(@class, 'input1')])[8]") + testenv_page.click_submit_btn() + testenv_page.find_test_element(LocatorType.xpath, "(//*[@class='input1']//parent::*[contains(@class, 'input1')])[8]") + + testenv_page.close() + + def test_css_first_child(self): + testenv_page = TestEnvPage() + + testenv_page.open_browser() + testenv_page.find_test_element(LocatorType.css, "test_tag:first-child") + testenv_page.click_submit_btn() + testenv_page.find_test_element(LocatorType.css, "test_tag:first-child") + + testenv_page.close() + + def test_css_last_child(self): + testenv_page = TestEnvPage() + + testenv_page.open_browser() + testenv_page.find_test_element(LocatorType.css, "child_tag:last-child") + testenv_page.click_submit_btn() + testenv_page.find_test_element(LocatorType.css, "child_tag:last-child") + + testenv_page.close() diff --git a/tests/test_semantic.py b/tests/test_semantic.py new file mode 100644 index 0000000..cbad9d7 --- /dev/null +++ b/tests/test_semantic.py @@ -0,0 +1,69 @@ +from src.main.constants.locator_type import LocatorType +from src.main.pages.markup_page import MarkupPage +from src.main.pages.testenv_page import TestEnvPage + + +class TestSemantic: + + # def test_semantic_class_name(self): + # main_page = MarkupPage() + # + # main_page.open_browser() + # main_page.click_test_button() + # main_page.confirm_alert() + # + # main_page.generate_markup() + # main_page.click_test_button() # should be healed + # main_page.confirm_alert() + # + # main_page.close() + + def test_semantic_id(self): + test_page = TestEnvPage() + + test_page.open_browser() + test_page.find_test_element(LocatorType.id, "change_id") + test_page.click_submit_btn() + test_page.find_test_element(LocatorType.id, "change_id") # should be healed + + test_page.close() + + def test_semantic_link_text(self): + test_page = TestEnvPage() + + test_page.open_browser() + test_page.find_test_element(LocatorType.link_text, "Change: LinkText, PartialLinkText") + test_page.click_submit_btn() + test_page.find_test_element(LocatorType.link_text, "Change: LinkText, PartialLinkText") # should be healed + + test_page.close() + + def test_semantic_partial_link_text(self): + test_page = TestEnvPage() + + test_page.open_browser() + test_page.find_test_element(LocatorType.partial_link_text, "PartialLinkText") + test_page.click_submit_btn() + test_page.find_test_element(LocatorType.partial_link_text, "PartialLinkText") # should be healed + + test_page.close() + + def test_semantic_name(self): + test_page = TestEnvPage() + + test_page.open_browser() + test_page.find_test_element(LocatorType.name, "change_name") + test_page.click_submit_btn() + test_page.find_test_element(LocatorType.name, "change_name") # should be healed + + test_page.close() + + def test_semantic_element(self): + test_page = TestEnvPage() + + test_page.open_browser() + test_page.find_test_element(LocatorType.tag, "test_tag") + test_page.click_submit_btn() + test_page.find_test_element(LocatorType.tag, "test_tag") # should be healed + + test_page.close() diff --git a/tests/test_wait.py b/tests/test_wait.py new file mode 100644 index 0000000..9c7e021 --- /dev/null +++ b/tests/test_wait.py @@ -0,0 +1,15 @@ +from src.main.pages.testenv_page import TestEnvPage + + +class TestWait: + + def test_conditional_wait(self): + test_page = TestEnvPage() + + test_page.open_browser() + test_page.click_wait_btn() + test_page.execute_script("disable_healing_true") + test_page.click_test_button_wait() + test_page.execute_script("disable_healing_false") + + test_page.close() diff --git a/tests/test_xpath.py b/tests/test_xpath.py new file mode 100644 index 0000000..95e6dee --- /dev/null +++ b/tests/test_xpath.py @@ -0,0 +1,115 @@ +from src.main.constants.locator_type import LocatorType +from src.main.pages.testenv_page import TestEnvPage + + +class TestXpath: + + def test_xpath_special_character(self): + test_page = TestEnvPage() + + test_page.open_browser() + test_page.find_test_element(LocatorType.xpath, "//*[@id='change:name']") + test_page.click_submit_btn() + test_page.find_test_element(LocatorType.xpath, "//*[@id='change:name']") + + test_page.close() + + def test_xpath_following(self): + test_page = TestEnvPage() + + test_page.open_browser() + test_page.find_test_element(LocatorType.xpath, "//*[@id='change_className']/following::test_tag") + test_page.click_submit_btn() + test_page.find_test_element(LocatorType.xpath, "//*[@id='change_className']/following::test_tag") + + test_page.close() + + def test_xpath_contains(self): + test_page = TestEnvPage() + + test_page.open_browser() + test_page.find_test_element(LocatorType.xpath, "//input[contains(@class, 'test')]") + test_page.click_submit_btn() + test_page.find_test_element(LocatorType.xpath, "//input[contains(@class, 'test')]") + + test_page.close() + + def test_xpath_not_contains(self): + test_page = TestEnvPage() + + test_page.open_browser() + test_page.find_test_element(LocatorType.xpath, "//input[not(contains(@class, 'input1'))]") + test_page.click_submit_btn() + test_page.find_test_element(LocatorType.xpath, "//input[not(contains(@class, 'input1'))]") + + test_page.close() + + def test_xpath_following_sibling(self): + test_page = TestEnvPage() + + test_page.open_browser() + test_page.find_test_element(LocatorType.xpath, "//*[starts-with(@class, 'test')]/following-sibling::*") + test_page.click_submit_btn() + test_page.find_test_element(LocatorType.xpath, "//*[starts-with(@class, 'test')]/following-sibling::*") + + test_page.close() + + def test_xpath_ancestor(self): + test_page = TestEnvPage() + + test_page.open_browser() + test_page.find_test_element(LocatorType.xpath, "(//*[starts-with(@class, 'test')]/ancestor::div[@class='healenium-form validate-form']//input)[1]") + test_page.click_submit_btn() + test_page.find_test_element(LocatorType.xpath, "(//*[starts-with(@class, 'test')]/ancestor::div[@class='healenium-form validate-form']//input)[1]") + + test_page.close() + + def test_xpath_or(self): + test_page = TestEnvPage() + + test_page.open_browser() + test_page.find_test_element(LocatorType.xpath, "//*[@id='change_id' or @id='omg']") + test_page.click_submit_btn() + test_page.find_test_element(LocatorType.xpath, "//*[@id='change_id' or @id='omg']") + + test_page.close() + + def test_xpath_and(self): + test_page = TestEnvPage() + + test_page.open_browser() + test_page.find_test_element(LocatorType.xpath, "//*[@id='change_id' and @type='text']") + test_page.click_submit_btn() + test_page.find_test_element(LocatorType.xpath, "//*[@id='change_id' and @type='text']") + + test_page.close() + + def test_xpath_starts_with(self): + test_page = TestEnvPage() + + test_page.open_browser() + test_page.find_test_element(LocatorType.xpath, "//*[starts-with(@class, 'test')]") + test_page.click_submit_btn() + test_page.find_test_element(LocatorType.xpath, "//*[starts-with(@class, 'test')]") + + test_page.close() + + def test_xpath_precending(self): + test_page = TestEnvPage() + + test_page.open_browser() + test_page.find_test_element(LocatorType.xpath, "//*[@id='change_className']/preceding::*[@id='change_id']") + test_page.click_submit_btn() + test_page.find_test_element(LocatorType.xpath, "//*[@id='change_className']/preceding::*[@id='change_id']") + + test_page.close() + + def test_xpath_descendant(self): + test_page = TestEnvPage() + + test_page.open_browser() + test_page.find_test_element(LocatorType.xpath, "//*[@id='descendant_change']/descendant::input") + test_page.click_submit_btn() + test_page.find_test_element(LocatorType.xpath, "//*[@id='descendant_change']/descendant::input") + + test_page.close()