├── .gitattributes ├── .github └── workflows │ └── run-tests.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── Makefile ├── make.bat └── source │ ├── _static │ ├── css │ │ └── custom.css │ └── img │ │ ├── article-page.png │ │ ├── empty-test.png │ │ ├── example-test.png │ │ ├── header-page.png │ │ ├── interactive-console.png │ │ ├── suite-example.png │ │ ├── test-with-data-table.png │ │ └── test-with-pages.png │ ├── browsers.md │ ├── command-line-interface.md │ ├── conf.py │ ├── finding-elements.md │ ├── golem-actions.md │ ├── golem-test-framework.md │ ├── golem_public_api │ ├── browser.md │ ├── execution.md │ ├── golem-expected-conditions.md │ ├── index.rst │ ├── webdriver-class.md │ └── webelement-class.md │ ├── gui.md │ ├── guides │ ├── index.rst │ ├── run-from-jenkins.md │ ├── standalone-executable.md │ └── using-multiple-browsers.md │ ├── index.rst │ ├── installation.md │ ├── interactive-mode.md │ ├── pages.md │ ├── report.md │ ├── running-tests.md │ ├── settings.md │ ├── suites.md │ ├── test-data.md │ ├── tests.md │ ├── tutorial-part-1.md │ ├── tutorial-part-2.md │ └── waiting-for-elements.md ├── golem ├── __init__.py ├── actions.py ├── bin │ ├── __init__.py │ ├── golem_admin.py │ ├── golem_init.py │ └── golem_standalone.py ├── browser.py ├── cli │ ├── __init__.py │ ├── argument_parser.py │ ├── commands.py │ └── messages.py ├── core │ ├── __init__.py │ ├── environment_manager.py │ ├── errors.py │ ├── exceptions.py │ ├── file_manager.py │ ├── page.py │ ├── parsing_utils.py │ ├── project.py │ ├── secrets_manager.py │ ├── session.py │ ├── settings_manager.py │ ├── suite.py │ ├── tags_manager.py │ ├── test.py │ ├── test_data.py │ ├── test_directory.py │ ├── test_parser.py │ └── utils.py ├── execution.py ├── execution_runner │ ├── __init__.py │ ├── execution_runner.py │ ├── interactive.py │ └── multiprocess_executor.py ├── gui │ ├── __init__.py │ ├── api.py │ ├── gui_start.py │ ├── gui_utils.py │ ├── report.py │ ├── static │ │ ├── css │ │ │ ├── bootstrap │ │ │ │ ├── bootstrap-theme.min.css │ │ │ │ └── bootstrap.min.css │ │ │ ├── code-editor-common.css │ │ │ ├── font-awesome │ │ │ │ └── font-awesome.min.css │ │ │ ├── fonts │ │ │ │ ├── FontAwesome.otf │ │ │ │ ├── fontawesome-webfont.eot │ │ │ │ ├── fontawesome-webfont.svg │ │ │ │ ├── fontawesome-webfont.ttf │ │ │ │ ├── fontawesome-webfont.woff │ │ │ │ ├── fontawesome-webfont.woff2 │ │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ │ ├── glyphicons-halflings-regular.woff2 │ │ │ │ ├── lato-bol-webfont.woff │ │ │ │ └── lato-lig-webfont.woff │ │ │ ├── json_code_editor.css │ │ │ ├── list_common.css │ │ │ ├── main.css │ │ │ ├── page_object.css │ │ │ ├── report.css │ │ │ ├── suite.css │ │ │ ├── test_case.css │ │ │ ├── test_case_code.css │ │ │ ├── test_case_common.css │ │ │ └── toastr │ │ │ │ └── toastr.min.css │ │ ├── img │ │ │ ├── icons │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ └── favicon.ico │ │ │ ├── plus_sign.png │ │ │ └── plus_sign2.png │ │ └── js │ │ │ ├── drivers.js │ │ │ ├── environments.js │ │ │ ├── external │ │ │ ├── bootstrap.min.js │ │ │ ├── chart.min.js │ │ │ ├── code_mirror │ │ │ │ ├── addon │ │ │ │ │ ├── edit │ │ │ │ │ │ └── matchbrackets.js │ │ │ │ │ └── hint │ │ │ │ │ │ ├── python-hint.js │ │ │ │ │ │ ├── simple-hint.css │ │ │ │ │ │ └── simple-hint.js │ │ │ │ ├── codemirror.css │ │ │ │ ├── codemirror.js │ │ │ │ ├── javascript.js │ │ │ │ └── python.js │ │ │ ├── datatable │ │ │ │ ├── dataTables.bootstrap.js │ │ │ │ └── datatables.min.js │ │ │ ├── jquery.autocomplete.min.js │ │ │ ├── jquery.min.js │ │ │ ├── sortable.min.js │ │ │ ├── toastr.min.js │ │ │ └── treeview.js │ │ │ ├── file.js │ │ │ ├── index.js │ │ │ ├── list │ │ │ ├── list_common.js │ │ │ ├── page_list.js │ │ │ ├── suite_list.js │ │ │ └── test_list.js │ │ │ ├── main.js │ │ │ ├── new_user.js │ │ │ ├── page.js │ │ │ ├── page_code.js │ │ │ ├── report_dashboard.js │ │ │ ├── report_dashboard_old.js │ │ │ ├── report_execution.js │ │ │ ├── report_test.js │ │ │ ├── settings.js │ │ │ ├── suite.js │ │ │ ├── suite_code.js │ │ │ ├── test.js │ │ │ ├── test_code.js │ │ │ ├── test_common.js │ │ │ └── users.js │ ├── templates │ │ ├── 404.html │ │ ├── common_element_error.html │ │ ├── drivers.html │ │ ├── environments.html │ │ ├── index.html │ │ ├── layout.html │ │ ├── list │ │ │ ├── page_list.html │ │ │ ├── suite_list.html │ │ │ └── test_list.html │ │ ├── login.html │ │ ├── not_permission.html │ │ ├── page_builder │ │ │ ├── page.html │ │ │ └── page_code.html │ │ ├── report │ │ │ ├── report_dashboard.html │ │ │ ├── report_dashboard_old.html │ │ │ ├── report_execution.html │ │ │ ├── report_execution_static.html │ │ │ └── report_test.html │ │ ├── settings │ │ │ ├── global_settings.html │ │ │ └── project_settings.html │ │ ├── suite.html │ │ ├── suite_code.html │ │ ├── test_builder │ │ │ ├── test.html │ │ │ └── test_code.html │ │ └── users │ │ │ ├── reset_password.html │ │ │ ├── user_form.html │ │ │ ├── user_profile.html │ │ │ └── users.html │ ├── user_management.py │ └── web_app.py ├── helpers.py ├── main.py ├── report │ ├── __init__.py │ ├── cli_report.py │ ├── execution_report.py │ ├── html_report.py │ ├── junit_report.py │ ├── report.py │ ├── test_report.py │ └── utils.py ├── test_runner │ ├── __init__.py │ ├── conf.py │ ├── test_logger.py │ ├── test_runner.py │ └── test_runner_utils.py └── webdriver │ ├── __init__.py │ ├── common.py │ ├── extended_driver.py │ ├── extended_webelement.py │ └── golem_expected_conditions.py ├── images ├── example-test-code.png ├── execution-report.png ├── report-dashboard.png ├── test-case.png └── test-execution-detail.png ├── pytest.ini ├── requirements.txt ├── requirements_dev.txt ├── setup.cfg ├── setup.py ├── tests ├── browser_test.py ├── cli │ └── commands_test.py ├── conftest.py ├── core │ ├── environment_manager_test.py │ ├── file_manager_test.py │ ├── page_test.py │ ├── parsing_utils_test.py │ ├── project_test.py │ ├── secrets_manager_test.py │ ├── settings_manager_test.py │ ├── suite_test.py │ ├── tags_manager_test.py │ ├── test_data_test.py │ ├── test_directory_test.py │ ├── test_parser_test.py │ ├── test_test.py │ └── utils_test.py ├── execution_runner │ └── execution_runner_test.py ├── golem_admin_cli_test.py ├── golem_cli_test.py ├── gui │ ├── gui_utils_test.py │ └── user_management_test.py ├── helpers_test.py ├── report │ ├── cli_report_test.py │ ├── execution_report_test.py │ ├── html_report_test.py │ ├── junit_report_test.py │ ├── report_test.py │ └── test_report_test.py └── test_runner │ └── test_runner_test.py └── tox.ini /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.bat -text -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - "*" 7 | pull_request: 8 | branches: 9 | - "master" 10 | 11 | jobs: 12 | build: 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | include: 18 | - { python: '3.6', os: ubuntu-latest } 19 | - { python: '3.7', os: ubuntu-latest } 20 | - { python: '3.8', os: ubuntu-latest } 21 | - { python: '3.9', os: ubuntu-latest } 22 | - { python: '3.7', os: windows-latest } 23 | - { python: '3.7', os: macos-latest } 24 | 25 | steps: 26 | - uses: actions/checkout@v2 27 | - uses: actions/setup-python@v2 28 | with: 29 | python-version: ${{ matrix.python }} 30 | - name: Install dependencies 31 | run: python -m pip install --upgrade pip setuptools wheel pytest 32 | - name: Install 33 | run: | 34 | python setup.py install 35 | - name: Test with pytest 36 | run: | 37 | pytest tests -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | *__pycache__/ 3 | *.pyc 4 | .cache/ 5 | .coverage 6 | .eggs/ 7 | .tox 8 | .idea 9 | .vscode 10 | *.pytest_cache/ 11 | build/ 12 | dist/ 13 | geckodriver.log 14 | docs/build/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Golem 2 | 3 | Thank you for your interest in contributing to Golem! 4 | 5 | ## Questions 6 | 7 | If you have a question please use the Gitter channel: https://gitter.im/golem-framework/golem 8 | 9 | ## Submitting a Bug 10 | 11 | When reporting new bugs, please make sure that the report includes as much information as possible. Not all of the following is required, it depends on the issue. 12 | 13 | - A reproducible test case 14 | - Golem version 15 | - Operating System and Python version 16 | - Selenium version 17 | - Webdriver version 18 | - Browser version 19 | - Screenshot, traceback, or log 20 | 21 | Please note: Always try to use the latest version of Selenium, the Webdriver executables, and the browsers before raising a bug. 22 | 23 | ## Suggestions 24 | 25 | Enhancements and new feature ideas are welcome, however, time is limited. We encourage people to try and submit a pull request. Read how to do that below. 26 | 27 | ## How to Contribute Code 28 | 29 | If you have improvements or fixes for Golem, send us your pull requests! For those 30 | just getting started, Github has a [howto](https://help.github.com/articles/using-pull-requests/). 31 | 32 | ## Development Guide 33 | 34 | ### Running Unit Tests 35 | 36 | ``` 37 | pip install pytest 38 | pytest run tests 39 | ``` 40 | 41 | To run only fast tests use the following command: 42 | 43 | ``` 44 | pytest run tests --fast 45 | ``` 46 | 47 | ### Running Integration and UI tests 48 | 49 | Golem has suites for integration and UI tests. The steps to run them can be found here: https://github.com/golemhq/golem-tests 50 | 51 | ### Code Style 52 | 53 | The code style for the project is [PEP 8](https://www.python.org/dev/peps/pep-0008/) with the following additions: 54 | 55 | - Line length limit is extended to 90 characters 56 | - Line length can exceed 90 characters if readability would be reduced otherwise 57 | - Single quote strings are preferred unless the string itself contains a single quote. 58 | 59 | ### Building the Docs 60 | 61 | ``` 62 | pip install sphinx 63 | pip install recommonmark 64 | cd docs 65 | make html 66 | ``` 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Luciano Renzi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include LICENSE 3 | 4 | recursive-include golem/gui/static *.* 5 | recursive-include golem/gui/templates *.* 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golem - Test Automation 2 | 3 | [![Tests](https://github.com/golemhq/golem/actions/workflows/run-tests.yml/badge.svg?branch=master)](https://github.com/golemhq/golem/actions/workflows/run-tests.yml?query=branch:master) 4 | [![Documentation Status](https://readthedocs.org/projects/golem-framework/badge/?version=latest)](https://golem-framework.readthedocs.io/en/latest/?badge=latest) 5 | [![Join the chat at https://gitter.im/golem-framework/golem](https://badges.gitter.im/golem-framework/golem.svg)](https://gitter.im/golem-framework/golem?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 6 | 7 | Golem is a test framework and a complete tool for browser automation. 8 | Tests can be written with code in Python, codeless using the web IDE, or both. 9 | 10 | ![Execution Report Demo](https://raw.githubusercontent.com/golemhq/resources/master/img/demo_report_running_execution.gif) 11 | 12 | **Tests can be written with the web app** 13 |

14 | 15 |

16 | 17 | **But, they are still Python code** 18 |

19 | 20 |

21 | 22 | ## Batteries Included 23 | 24 | * Multi-user web IDE 25 | * Extended classes for [WebDriver](https://golem-framework.readthedocs.io/en/latest/golem_public_api/webdriver-class.html) and [WebElement](https://golem-framework.readthedocs.io/en/latest/golem_public_api/webelement-class.html) 26 | * More than 200 self documenting [Actions](https://golem-framework.readthedocs.io/en/latest/golem-actions.html) 27 | * [Webdriver-manager](https://github.com/golemhq/webdriver-manager) utility 28 | * Built in parallel test support 29 | * Reporting engine 30 | 31 |
32 | 33 | **Golem is still in beta!**. Read the changelog before upgrading. 34 | 35 |
36 | 37 | ## Screen Captures 38 | 39 | **Report Dashboard** 40 |

41 | 42 |

43 | 44 | **Execution Report** 45 |

46 | 47 |

48 | 49 | **Test Execution Detail** 50 |

51 | 52 |

53 | 54 | ## Installation 55 | 56 | Golem works with Python 3.6+ 57 | 58 | ``` 59 | pip install golem-framework 60 | ``` 61 | 62 | Read the full installation guide here: [https://golem-framework.readthedocs.io/en/latest/installation.html](https://golem-framework.readthedocs.io/en/latest/installation.html) 63 | 64 | ## Quick Start 65 | 66 | **Create a test directory anywhere in your machine** 67 | 68 | ``` 69 | golem-admin createdirectory 70 | ``` 71 | 72 | **Download the latest webdriver executables** 73 | 74 | ``` 75 | cd 76 | webdriver-manager update 77 | ``` 78 | 79 | Webdriver executables are downloaded to the *drivers* folder. For more information check [this page](https://golem-framework.readthedocs.io/en/latest/browsers.html) of the documentation. 80 | 81 | **Start the Web Module** 82 | 83 | ``` 84 | golem gui 85 | ``` 86 | 87 | The Web Module can be accessed at http://localhost:5000/ 88 | 89 | By default, the following user is available: username: *admin* / password: *admin* 90 | 91 | **Run a Test From Console** 92 | 93 | ``` 94 | golem run 95 | golem run 96 | ``` 97 | 98 | Args: 99 | 100 | * -b | --browsers: a list of browsers, by default use defined in settings.json or Chrome 101 | * -p | --processes: run in parallel, default 1 (not parallel) 102 | * -e | --environments: a list of environments, the default is none 103 | * -t | --tags: filter tests by tags 104 | 105 | ## Documentation 106 | 107 | [https://golem-framework.readthedocs.io/](https://golem-framework.readthedocs.io/) 108 | 109 | ## Questions 110 | 111 | If you have any question please use the [Gitter channel](https://gitter.im/golem-framework/golem). 112 | 113 | ## Contributing 114 | 115 | If you found a bug or want to contribute code please read the [contributing guide](https://github.com/golemhq/golem/blob/master/CONTRIBUTING.md). 116 | 117 | ## License 118 | 119 | [MIT](https://tldrlegal.com/license/mit-license) 120 | 121 | ## Credits 122 | 123 | Logo based on ["to believe"](https://www.toicon.com/icons/feather_believe) by Shannon E Thomas, [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) 124 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = Golem 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=python -msphinx 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=Golem 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 20 | echo.then set the SPHINXBUILD environment variable to point to the full 21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 22 | echo.Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/source/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | table { 4 | font-family: arial, sans-serif; 5 | border-collapse: collapse; 6 | width: 100%; 7 | } 8 | 9 | td, th { 10 | border: 1px solid #dddddd; 11 | text-align: left; 12 | padding: 8px; 13 | } 14 | 15 | tr:nth-child(even) { 16 | background-color: #dddddd; 17 | } 18 | 19 | p.note { 20 | 21 | } 22 | 23 | .border-image { 24 | border: 1px solid #d3d3d3; 25 | padding: 10px; 26 | box-sizing: border-box; 27 | -webkit-box-sizing: border-box; 28 | -moz-box-sizing: border-box; 29 | } 30 | 31 | #permissionTable tr { 32 | background-color: white; 33 | } 34 | 35 | #permissionTable td, #permissionTable th { 36 | padding: 5px; 37 | } 38 | 39 | #permissionTable td { 40 | text-align: center; 41 | } 42 | 43 | .align-left { 44 | text-align: left !important; 45 | } 46 | -------------------------------------------------------------------------------- /docs/source/_static/img/article-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/docs/source/_static/img/article-page.png -------------------------------------------------------------------------------- /docs/source/_static/img/empty-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/docs/source/_static/img/empty-test.png -------------------------------------------------------------------------------- /docs/source/_static/img/example-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/docs/source/_static/img/example-test.png -------------------------------------------------------------------------------- /docs/source/_static/img/header-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/docs/source/_static/img/header-page.png -------------------------------------------------------------------------------- /docs/source/_static/img/interactive-console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/docs/source/_static/img/interactive-console.png -------------------------------------------------------------------------------- /docs/source/_static/img/suite-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/docs/source/_static/img/suite-example.png -------------------------------------------------------------------------------- /docs/source/_static/img/test-with-data-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/docs/source/_static/img/test-with-data-table.png -------------------------------------------------------------------------------- /docs/source/_static/img/test-with-pages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/docs/source/_static/img/test-with-pages.png -------------------------------------------------------------------------------- /docs/source/command-line-interface.md: -------------------------------------------------------------------------------- 1 | The CLI 2 | ================================================== 3 | 4 | ## golem-admin 5 | 6 | ### createdirectory 7 | 8 | ``` 9 | golem-admin createdirectory 10 | ``` 11 | 12 | Used to generate a new test directory. 13 | Name must be a relative or absolute path for the new test directory. 14 | Use '.' to use the current directory. 15 | 16 | ## golem 17 | 18 | ### run 19 | 20 | The command to run tests or suites from the command line: 21 | 22 | ``` 23 | golem run [-b|--browsers] 24 | [-p|--processes] [-e|--environments] [-t|--tags] 25 | [-i|--interactive] [-r|--report] [--report-folder] 26 | [--report-name] [-l|--cli-log-level][--timestamp] 27 | ``` 28 | 29 | #### -b, \-\-browsers 30 | 31 | One or more browsers to use for the tests. 32 | If not provided, the browsers defined inside the suite or the default browser defined in settings will be used. 33 | The valid options are listed [here](browsers.html#valid-options). 34 | 35 | #### -p, \-\-processes 36 | 37 | The number of tests to run in parallel. The default is 1. 38 | 39 | #### -e, \-\-environments 40 | 41 | The environments to use for the execution. 42 | If not provided, the environments defined inside the suite will be used. 43 | 44 | #### -t, \-\-tags 45 | 46 | Filter the tests by tags. 47 | 48 | Example, run all tests with tag "smoke": 49 | 50 | ``` 51 | golem run project_name . --tags smoke 52 | ``` 53 | 54 | Or using a tag expression: 55 | 56 | ``` 57 | golem run project suite --tags "smoke and (regression or not 'release 001')" 58 | ``` 59 | 60 | #### -i, \-\-interactive 61 | 62 | Run the test in interactive mode. 63 | This is required for the *interactive_mode* and *set_trace* actions. 64 | 65 | See [Interactive Mode](interactive-mode.html) 66 | 67 | #### -r, \-\-report 68 | 69 | Select which reports should be generated at the end of the execution. 70 | Options are: *junit*, *html*, *html-no-images*, and *json* 71 | 72 | #### \-\-report-folder 73 | 74 | Absolute path to the generated reports. 75 | The default is ```//projects//resports//``` 76 | 77 | #### \-\-report-name 78 | 79 | Name of the generated reports. The default is 'report' 80 | 81 | #### \-l, \-\-cli-log-level 82 | 83 | command line log level. 84 | Options are: DEBUG, INFO, WARNING, ERROR, CRITICAL. Default is INFO. 85 | 86 | #### \-\-timestamp 87 | 88 | Used by the execution. Optional. 89 | The default is auto-generated with the format: 'year.month.day.hour.minutes.seconds.milliseconds' 90 | 91 | ### gui 92 | 93 | ``` 94 | golem gui [--host -p|--port -d|--debug] 95 | ``` 96 | 97 | Start Golem Web Module (GUI). 98 | Default host is 127.0.0.1 (localhost). 99 | Use host 0.0.0.0 to make the GUI publicly accessible. 100 | Default port is 5000. 101 | Debug runs the application in debug mode, default is False. 102 | Do not run in debug mode on production machines. 103 | 104 | See [GUI - Web Module](gui.html) for more info. 105 | 106 | ### createproject 107 | 108 | ``` 109 | golem createproject 110 | ``` 111 | 112 | Creates a new project with the given name. Creates the base files and folders. 113 | 114 | ### createtest 115 | 116 | ``` 117 | golem createtest 118 | ``` 119 | 120 | Creates a new test inside the given project. 121 | 122 | ### createsuite 123 | 124 | ``` 125 | golem createsuite 126 | ``` 127 | 128 | Creates a new suite inside the given project. 129 | 130 | ### createsuperuser 131 | 132 | ``` 133 | golem createuser [-u|--username -e|email -p|--password -n|--noinput] 134 | ``` 135 | 136 | Create a new superuser. 137 | The command is interactive unless username and password are provided. 138 | Email is optional. 139 | 140 | ## webdriver-manager 141 | 142 | ### update 143 | 144 | ``` 145 | webdriver-manager update -b chrome 146 | ``` 147 | 148 | To learn more about the Webdriver Manager see: . 149 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | from recommonmark.parser import CommonMarkParser 2 | 3 | import os 4 | import sys 5 | 6 | # insert Golem path into the system 7 | sys.path.insert(0, os.path.abspath("..")) 8 | 9 | import golem 10 | 11 | 12 | def setup(app): 13 | app.add_stylesheet('css/custom.css') 14 | 15 | # Add any Sphinx extension module names here, as strings. They can be 16 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 17 | # ones. 18 | extensions = ['sphinx.ext.coverage', 'sphinx.ext.autodoc'] 19 | 20 | # Add any paths that contain templates here, relative to this directory. 21 | templates_path = ['_templates'] 22 | 23 | # The suffix(es) of source filenames. 24 | # You can specify multiple suffix as a list of string: 25 | # 26 | # source_suffix = ['.rst', '.md'] 27 | source_suffix = ['.rst', '.md'] 28 | 29 | source_parsers = { 30 | '.md': CommonMarkParser, 31 | } 32 | 33 | # The master toctree document. 34 | master_doc = 'index' 35 | 36 | # General information about the project. 37 | project = 'Golem' 38 | copyright = '2017, Luciano Renzi' 39 | author = 'Luciano Renzi' 40 | 41 | html_theme = 'alabaster' 42 | html_sidebars = { 43 | '**': [ 44 | 'about.html', 45 | 'navigation.html', 46 | 'relations.html', 47 | 'searchbox.html', 48 | ] 49 | } 50 | html_theme_options = { 51 | 'description': 'test automation framework', 52 | 'page_width': '1050px', 53 | 'github_user': 'golemhq', 54 | 'github_repo': 'golem', 55 | 'github_type': 'star', 56 | 'github_count': 'true', 57 | 'analytics_id': 'UA-139149408-1' 58 | } 59 | 60 | # The version info for the project you're documenting, acts as replacement for 61 | # |version| and |release|, also used in various other places throughout the 62 | # built documents. 63 | # 64 | # The short X.Y version. 65 | version = golem.__version__ 66 | # The full version, including alpha/beta/rc tags. 67 | release = golem.__version__ 68 | 69 | # The language for content autogenerated by Sphinx. Refer to documentation 70 | # for a list of supported languages. 71 | # 72 | # This is also used if you do content translation via gettext catalogs. 73 | # Usually you set "language" from the command line for these cases. 74 | language = None 75 | 76 | # List of patterns, relative to source directory, that match files and 77 | # directories to ignore when looking for source files. 78 | # This patterns also effect to html_static_path and html_extra_path 79 | exclude_patterns = [] 80 | 81 | # The name of the Pygments (syntax highlighting) style to use. 82 | pygments_style = 'sphinx' 83 | 84 | # If true, `todo` and `todoList` produce output, else they produce nothing. 85 | todo_include_todos = False 86 | 87 | 88 | # Add any paths that contain custom static files (such as style sheets) here, 89 | # relative to this directory. They are copied after the builtin static files, 90 | # so a file named "default.css" will overwrite the builtin "default.css". 91 | html_static_path = ['_static'] 92 | 93 | 94 | # -- Options for HTMLHelp output ------------------------------------------ 95 | 96 | # Output file base name for HTML help builder. 97 | htmlhelp_basename = 'Golemdoc' 98 | 99 | 100 | # -- Options for LaTeX output --------------------------------------------- 101 | 102 | latex_elements = { 103 | # The paper size ('letterpaper' or 'a4paper'). 104 | # 105 | # 'papersize': 'letterpaper', 106 | 107 | # The font size ('10pt', '11pt' or '12pt'). 108 | # 109 | # 'pointsize': '10pt', 110 | 111 | # Additional stuff for the LaTeX preamble. 112 | # 113 | # 'preamble': '', 114 | 115 | # Latex figure (float) alignment 116 | # 117 | # 'figure_align': 'htbp', 118 | } 119 | 120 | # Grouping the document tree into LaTeX files. List of tuples 121 | # (source start file, target name, title, 122 | # author, documentclass [howto, manual, or own class]). 123 | latex_documents = [ 124 | (master_doc, 'Golem.tex', 'Golem Documentation', 125 | 'Luciano Renzi', 'manual'), 126 | ] 127 | 128 | 129 | # -- Options for manual page output --------------------------------------- 130 | 131 | # One entry per manual page. List of tuples 132 | # (source start file, name, description, authors, manual section). 133 | man_pages = [ 134 | (master_doc, 'golem', 'Golem Documentation', 135 | [author], 1) 136 | ] 137 | 138 | 139 | # -- Options for Texinfo output ------------------------------------------- 140 | 141 | # Grouping the document tree into Texinfo files. List of tuples 142 | # (source start file, target name, title, author, 143 | # dir menu entry, description, category) 144 | texinfo_documents = [ 145 | (master_doc, 'Golem', 'Golem Documentation', 146 | author, 'Golem', 'One line description of project.', 147 | 'Miscellaneous'), 148 | ] 149 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /docs/source/finding-elements.md: -------------------------------------------------------------------------------- 1 | Finding Elements 2 | ================================================== 3 | 4 | ## Actions and Elements 5 | 6 | Golem actions that require a WebElement can be defined in four different ways: 7 | 8 | An element tuple: 9 | 10 | ```python 11 | from golem import actions 12 | 13 | input = ('id', 'myElementId', 'Username input') 14 | actions.send_keys(input, 'my username') 15 | 16 | # the third element is optional 17 | button = ('css', 'button.btn-default') 18 | actions.click(button) 19 | ``` 20 | 21 | A css selector string: 22 | 23 | ```python 24 | from golem import actions 25 | 26 | actions.send_keys('#myElementId', 'my username') 27 | actions.click('button.btn-default') 28 | ``` 29 | 30 | An XPath selector string: 31 | 32 | ```python 33 | from golem import actions 34 | 35 | actions.send_keys('//input[@id="myElementId"]', 'my username') 36 | actions.click('//button[@class="btn-default"]') 37 | ``` 38 | 39 | A WebElement object: 40 | 41 | ```python 42 | from golem import actions 43 | 44 | webelement = actions.get_browser().find(id='myElementId') 45 | actions.send_keys(webelement, 'my username') 46 | 47 | webelement = actions.get_browser().find(css='button.btn-default') 48 | actions.click(webelement) 49 | ``` 50 | 51 | 52 | ## find and find_all 53 | 54 | The browser has two methods used to find elements: **find** and **find_all** 55 | 56 | ### find() 57 | 58 | GolemExtendedDriver.**find**(*element=None, id=None, name=None, link_text=None, partial_link_text=None, css=None, xpath=None, tag_name=None, timeout=None, wait_displayed=None*) 59 | 60 | The **find()** method provides a few ways to find elements. 61 | Only one search criteria must be provided. 62 | *element* must be a CSS selector string, an XPath selector string, an element tuple, or a WebElement object. 63 | 64 | The *timeout* argument determines how much time to wait until the element is present. 65 | If this is not provided, the value defined in settings by the *search_timeout* key will be used. 66 | This is considered the global search timeout. 67 | 68 | The *wait_displayed* argument makes **find()** wait for the element to be displayed (visible) as well. 69 | This value is taken by default from the *wait_displayed* key in settings. 70 | 71 | Some examples: 72 | 73 | ```python 74 | from golem import actions 75 | 76 | 77 | browser = actions.get_browser() 78 | 79 | # by an element tuple 80 | element = browser.find(('id', 'someId')) 81 | 82 | # by a css selector (positional argument) 83 | element = browser.find('input.someClass') 84 | 85 | # by an XPath selector (positional argument) 86 | element = browser.find('//input[@class="someClass"]') 87 | 88 | # by a WebElement object 89 | element = browser.find(id='someId') 90 | element = browser.find(element) 91 | 92 | # by css selector (keyword argument) 93 | element = browser.find(css='input.someClass') 94 | 95 | # by id 96 | element = browser.find(id='someId') 97 | 98 | # by name 99 | element = browser.find(name='someName') 100 | 101 | # by link text 102 | element = browser.find(link_text='link text') 103 | 104 | # by partial link text 105 | element = browser.find(partial_link_text='link') 106 | 107 | # by xpath (keyword argument) 108 | element = browser.find(xpath="//input[@id='someId']") 109 | 110 | # by tag name 111 | element = browser.find(tag_name='input') 112 | ``` 113 | 114 | ### find_all() 115 | 116 | GolemExtendedDriver.**find_all**(*element=None, id=None, name=None, link_text=None, partial_link_text=None, css=None, xpath=None, tag_name=None*) 117 | 118 | Finds all the elements that match the selected criteria. 119 | Only one search criteria must be provided. Returns a list of WebElements. 120 | *element* must be a CSS selector string, an XPath selector string, an element tuple, or a WebElement object. 121 | 122 | ```python 123 | from golem import actions 124 | 125 | browser = actions.get_browser() 126 | table_rows = browser.find_all('table.myTable > tbody > tr') 127 | ``` 128 | 129 | ## Finding children elements 130 | 131 | WebElements also have the *find()* and *find_all()* methods. They can be used to find children elements from a parent element. 132 | 133 | ```python 134 | from golem import actions 135 | 136 | browser = actions.get_browser() 137 | 138 | table_rows = browser.find('table.myTable').find_all('tr') 139 | 140 | for row in table_rows: 141 | print(row.find('td.resultColumn').text) 142 | ``` 143 | 144 | ## element() and elements() Shortcuts 145 | 146 | **element()** and **elements()** provide handy shortcuts to **find()** and **find_all()** respectively. 147 | 148 | ```python 149 | from golem.browser import element, elements 150 | 151 | 152 | title = element(id='headerTitle') 153 | print(title.text) 154 | 155 | table_rows = elements('table > tbody > tr') 156 | print(len(table_rows)) 157 | ``` -------------------------------------------------------------------------------- /docs/source/golem-test-framework.md: -------------------------------------------------------------------------------- 1 | Test Framework 2 | ================================================== 3 | 4 | ## Test File 5 | 6 | A test file contains one or more tests (functions that start with 'test') 7 | 8 | Test files also can have the setup and teardown hooks: 9 | 10 | ### Setup 11 | 12 | A function that is executed before every test of a test file. 13 | 14 | ### Teardown 15 | 16 | A function that is executed after all tests even if there were exceptions or errors. 17 | 18 | ### Example 19 | 20 | ```python 21 | 22 | description = 'the description for my test' 23 | 24 | pages = ['login', 'menu', 'releases'] 25 | 26 | def setup(data): 27 | navigate(data.env.url) 28 | login.login(data.env.user1) 29 | 30 | def test(data): 31 | menu.navigate_to('Releases') 32 | data.store('release_name', 'release_001') 33 | releases.create_release(data.release_name) 34 | releases.assert_release_exists(data.release_name) 35 | 36 | def teardown(data): 37 | releases.remove_release(data.release_name) 38 | ``` 39 | 40 | ## Test Results 41 | 42 | The test can end with one of the following result statuses: 43 | 44 | **Success**: The test run without any errors. 45 | 46 | **Failure**: The test threw an AssertionError. 47 | 48 | Possible reasons for a test to end with failure: 49 | * Actions that start with 'assert_'. 50 | ```python 51 | actions.assert_title('My App Title') 52 | ``` 53 | * A call to *fail()* action. 54 | ```python 55 | actions.fail('this is a failure') 56 | ``` 57 | * A normal Python assert statement. 58 | ```python 59 | assert browser.title == 'My App Title', 'this is the incorrect title' 60 | ``` 61 | 62 | **Error**: 63 | 64 | The test had at least one error. Possible reasons for errors: 65 | * Actions that start with 'verify_'. 66 | * An error added manually: 67 | ```python 68 | actions.error('my error message') 69 | ``` 70 | 71 | **Code error**: 72 | 73 | Any exception that is not an AssertionError will mark the test as 'code error'. 74 | 75 | Example: 76 | 77 | test1.py 78 | ```python 79 | def test(data): 80 | send_keys('#button', 'there is something missing here' 81 | ``` 82 | 83 | ```bash 84 | >golem run project1 test1 85 | 17:55:25 INFO Test execution started: test1 86 | 17:55:25 ERROR SyntaxError: unexpected EOF while parsing 87 | Traceback (most recent call last): 88 | File "C:\...\testdir\projects\project1\tests\test1.py" 89 | SyntaxError: unexpected EOF while parsing 90 | 17:55:25 INFO Test Result: CODE ERROR 91 | ``` 92 | 93 | ## Assertions and Verifications 94 | 95 | ### Soft Assertions 96 | 97 | Every action that starts with "verify" is a soft assertion, meaning that the error will be recorded. The test will be marked as 'error' at the end, but the test execution will not be stopped. 98 | 99 | ### Hard Assertions 100 | 101 | Every action that starts with "assert" is a hard assertion, meaning that it will stop the test execution at that point. The teardown method will still be executed afterward. 102 | 103 | ### Assertion Actions Example 104 | 105 | **verify_element_present(element)**: 106 | - It adds an error to the error list 107 | - Logs the error to console and to file 108 | - Takes a screenshot if *screenshot_on_error* setting is True 109 | - The test is not stopped. 110 | - The test result will be: 'error' 111 | 112 | **assert_element_present(element)**: 113 | - An AssertionError is thrown 114 | - It adds an error to the list 115 | - Logs the error to console and to file 116 | - Takes a screenshot if *screenshot_on_error* setting is True 117 | - The test is stopped, jumps to teardown function 118 | - The test result will be: 'failure' 119 | -------------------------------------------------------------------------------- /docs/source/golem_public_api/browser.md: -------------------------------------------------------------------------------- 1 | Browser Module 2 | ================================================== 3 | 4 | Functions to manipulate WebDriver Browser instances. 5 | 6 | Location: golem.**browser** 7 | 8 | 9 | ## **open_browser**(browser_name=None, capabilities=None, remote_url=None, browser_id=None) 10 | 11 | When no arguments are provided the browser is selected from the CLI -b|--browsers argument, the suite *browsers* list, or the *default_browser* setting. 12 | 13 | This can be overridden in two ways: 14 | - a local webdriver instance or 15 | - a remote Selenium Grid driver instance. 16 | 17 | To open a local Webdriver instance pass browser_name with a [valid value](../browsers.html#valid-options) 18 | 19 | To open a remote Selenium Grid driver pass a capabilities dictionary and 20 | a remote_url. 21 | The minimum capabilities required is: 22 | ``` 23 | { 24 | browserName: 'chrome' 25 | version: '' 26 | platform: '' 27 | } 28 | ``` 29 | More info here: [https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities](https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities) 30 | 31 | If remote_url is None it will be taken from the `remote_url` setting. 32 | 33 | When opening more than one browser instance per test 34 | provide a browser_id to switch between browsers later on 35 | 36 | Returns: 37 | the opened browser 38 | 39 | ## **get_browser**() 40 | 41 | Returns the active browser. Starts a new one if there is none. 42 | 43 | ## **activate_browser**(browser_id) 44 | 45 | Activate an opened browser. 46 | Only needed when the test starts more than one browser instance. 47 | 48 | Raises: 49 | - InvalidBrowserIdError: The browser Id does not correspond to an opened browser 50 | 51 | Returns: the active browser 52 | 53 | ## **element**(*args, **kwargs) 54 | 55 | Shortcut to golem.browser.get_browser().find(). 56 | 57 | See [find](webdriver-class.html#find-args-kwargs-small-golem-small). 58 | 59 | ## **elements**(*args, **kwargs) 60 | 61 | Shortcut to golem.browser.get_browser().find_all() 62 | 63 | See [find_all](webdriver-class.html#find-all-args-kwargs-small-golem-small). -------------------------------------------------------------------------------- /docs/source/golem_public_api/execution.md: -------------------------------------------------------------------------------- 1 | Execution Module 2 | ================================================== 3 | 4 | This module stores values specific to a test file execution. 5 | These values should be read-only, modifying them can cause errors. 6 | 7 | Location: golem.**execution** 8 | 9 | **Example:** 10 | 11 | test.py 12 | ``` 13 | from golem import execution 14 | from golem.browser import open_browser 15 | 16 | 17 | def test(data): 18 | print('Running test:', execution.test_name) 19 | open_browser() 20 | execution.browser.navigate('http://...') 21 | execution.logger.info('log this message') 22 | ``` 23 | 24 | ## test_file 25 | 26 | Module path of the current test file, relative to the tests folder. 27 | 28 | ## browser 29 | 30 | The current active browser object. None if there is no open browser yet. 31 | 32 | ## browser_definition 33 | 34 | The browser definition passed to this test. 35 | 36 | ## browsers 37 | 38 | A dictionary with all the open browsers. 39 | 40 | ## data 41 | 42 | The data object. 43 | 44 | ## secrets 45 | 46 | The secrets data object. 47 | 48 | ## description 49 | 50 | The description of the test. 51 | 52 | ## settings 53 | 54 | The settings passed to this test. 55 | 56 | ## test_dirname 57 | 58 | The directory path where the test is located. 59 | 60 | ## test_path 61 | 62 | Full path to the test file. 63 | 64 | ## project_name 65 | 66 | Name of the project. 67 | 68 | ## project_path 69 | 70 | Path to the project directory. 71 | 72 | ## testdir 73 | 74 | Golem root directory. 75 | 76 | ## execution_reportdir 77 | 78 | Path for the execution report. 79 | 80 | ## testfile_reportdir 81 | 82 | Path for the test file report 83 | 84 | ## logger 85 | 86 | Test logger object. 87 | 88 | ## tags 89 | 90 | The list of tags passed to the execution. 91 | 92 | ## environment 93 | 94 | Name of the environment passed to the test. 95 | None is no environment was selected. 96 | 97 | ## Values for each test function 98 | 99 | ### test_name 100 | 101 | Current test function name. 102 | 103 | ### steps 104 | 105 | Steps collected by the current test function. 106 | 107 | ### errors 108 | 109 | A list of errors collected by the test function. 110 | 111 | ### test_reportdir 112 | 113 | Path for the test function report. 114 | 115 | ### timers 116 | 117 | A dictionary with timers, used by the *timer_start* and *timer_stop* actions. 118 | -------------------------------------------------------------------------------- /docs/source/golem_public_api/golem-expected-conditions.md: -------------------------------------------------------------------------------- 1 | Golem Expected Conditions 2 | ================================================== 3 | 4 | Some extra expected conditions. 5 | Located at golem.webdriver.golem_expected_conditions. 6 | 7 | **element_to_be_enabled(element)** 8 | 9 | An Expectation for checking an element is enabled 10 | 11 | **text_to_be_present_in_page(text)** 12 | 13 | An Expectation for checking page contains text 14 | 15 | **element_text_to_be(element, text)** 16 | 17 | An expectation for checking the given text matches element text 18 | 19 | **element_text_to_contain(element, text)** 20 | 21 | An expectation for checking element contains the given text 22 | 23 | **element_to_have_attribute(element, attribute)** 24 | 25 | An expectation for checking element has attribute 26 | 27 | **window_present_by_partial_title(partial_title)** 28 | 29 | An expectation for checking a window is present by partial title 30 | 31 | **window_present_by_partial_url(partial_url)** 32 | 33 | An expectation for checking a window is present by partial url 34 | 35 | **window_present_by_title(title)** 36 | 37 | An expectation for checking a window is present by title 38 | 39 | **window_present_by_url(url)** 40 | 41 | An expectation for checking a window is present by url -------------------------------------------------------------------------------- /docs/source/golem_public_api/index.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ================================= 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | :glob: 7 | 8 | browser 9 | /golem-actions 10 | execution 11 | webdriver-class 12 | webelement-class 13 | golem-expected-conditions 14 | 15 | 16 | .. Indices and tables 17 | .. ================== 18 | 19 | .. * :ref:`genindex` 20 | .. * :ref:`modindex` 21 | .. * :ref:`search` 22 | -------------------------------------------------------------------------------- /docs/source/guides/index.rst: -------------------------------------------------------------------------------- 1 | Guides 2 | ================================= 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | :glob: 7 | 8 | run-from-jenkins 9 | using-multiple-browsers 10 | standalone-executable 11 | 12 | 13 | .. Indices and tables 14 | .. ================== 15 | 16 | .. * :ref:`genindex` 17 | .. * :ref:`modindex` 18 | .. * :ref:`search` 19 | -------------------------------------------------------------------------------- /docs/source/guides/run-from-jenkins.md: -------------------------------------------------------------------------------- 1 | Running Tests with Jenkins 2 | ================================================== 3 | 4 | In this guide let's see how Golem tests can be run in jenkins. 5 | 6 | ## Pre-requisites 7 | 8 | - Jenkins is installed. 9 | - Python 3.6+ is installed in the Jenkins machine. 10 | - A Golem directory with tests is stored in a git repository. 11 | 12 | ## Steps 13 | 14 | In Jenkins go to Dashboard > Manage Jenkins > Global Tool Configuration 15 | 16 | In Python > Python installations section add a Python version with the path to the executable: 17 | 18 | ![](https://raw.githubusercontent.com/golemhq/resources/master/img/jenkins-guide/jenkins-python-installation.jpg) 19 | 20 | We will be using ShiningPanda to manage the virtual environments in the Jenkins job: 21 | [https://plugins.jenkins.io/shiningpanda/](https://plugins.jenkins.io/shiningpanda/). 22 | 23 | In Jenkins go to Dashboard > Manage Jenkins > Manage Plugins. 24 | Install the ShiningPanda plugin and restart Jenkins. 25 | 26 | Create a new Jenkins job of type "Freestyle project" 27 | 28 | Define the location of the tests in the Source Code Management section: 29 | 30 | ![](https://raw.githubusercontent.com/golemhq/resources/master/img/jenkins-guide/jenkins-define-source-repo.jpg) 31 | 32 | Add a build step of type "Virtualenv Builder": 33 | 34 | ![](https://raw.githubusercontent.com/golemhq/resources/master/img/jenkins-guide/jenkins-build-step.jpg) 35 | 36 | Add a post-build action that collects the generated JUnit XML report: 37 | 38 | ![](https://raw.githubusercontent.com/golemhq/resources/master/img/jenkins-guide/jenkins-post-build-action.jpg) 39 | 40 | Run! 41 | 42 | ![](https://raw.githubusercontent.com/golemhq/resources/master/img/jenkins-guide/jenkins-final-result.jpg) 43 | 44 | 45 | -------------------------------------------------------------------------------- /docs/source/guides/standalone-executable.md: -------------------------------------------------------------------------------- 1 | Generate a Standalone Executable 2 | ================================================== 3 | 4 | ## Using PyInstaller 5 | 6 | A Golem standalone executable without any dependencies (including Python) can be generated using [PyInstaller](https://pyinstaller.readthedocs.io/). 7 | 8 | Note: the executable must be generated in the same platform that it will be used (e.g.: Windows 10 64 with Python 3.7) 9 | 10 | ### Steps 11 | 12 | Create an empty virtualenv (having the required packages only reduces the final executable size): 13 | 14 | ``` 15 | virtualenv env 16 | ``` 17 | 18 | Clone the repo and install: 19 | 20 | ``` 21 | git clone https://github.com/golemhq/golem.git 22 | cd golem 23 | pip install . 24 | ``` 25 | 26 | Install PyInstaller 27 | 28 | ``` 29 | pip install pyinstaller 30 | ``` 31 | 32 | Install python3-dev if needed (Linux) 33 | ``` 34 | apt-get install python3-dev 35 | ``` 36 | 37 | Generate the executable 38 | 39 | Linux: 40 | ``` 41 | pyinstaller golem/bin/golem_standalone.py --onefile -n golem --add-data "golem/gui/templates:golem/gui/templates" --add-data "golem/gui/static:golem/gui/static" 42 | ``` 43 | 44 | Windows: 45 | ``` 46 | pyinstaller golem\bin\golem_standalone.py --onefile -n golem --add-data "golem\gui\templates;golem\gui\templates" --add-data "golem\gui\static;golem\gui\static" 47 | ``` 48 | 49 | Where: 50 | 51 | ```--onefile``` generates a single file instead of a folder 52 | 53 | ```-n golem``` is the name of the executable 54 | 55 | ```--add-data``` includes the templates and static files required by the GUI 56 | 57 | The executable is generated in the *dist* folder. 58 | 59 | 60 | ## How to Use the Standalone Executable 61 | 62 | Put the executable in your path. 63 | 64 | The executable includes the *golem*, *golem-admin*, and *webdriver-manager* interfaces. 65 | 66 | Usage: 67 | 68 | ``` 69 | golem golem-admin createdirectory . 70 | golem webdriver-manager update 71 | golem gui 72 | golem run project test 73 | ``` 74 | -------------------------------------------------------------------------------- /docs/source/guides/using-multiple-browsers.md: -------------------------------------------------------------------------------- 1 | Using Multiple Browsers Sessions 2 | ================================================== 3 | 4 | Sometimes a test requires two or more different browser sessions opened at the same time. 5 | 6 | A browser is opened by default when using an action that needs a browser. 7 | To open a browser explicitly use the **open_browser** action or *golem.browser.**open_browser()***. 8 | 9 | ## Open Multiple Browsers 10 | 11 | To open a second browser use **open_browser** again and pass an id to identify it. 12 | The first browser has 'main' as its id by default. 13 | 14 | The list of opened browsers is stored in golem.execution.browsers. 15 | 16 | To use a browser when there is more than one, it has to be activated first: 17 | 18 | ```python 19 | open_browser() 20 | open_browser('second') 21 | activate_browser('second') 22 | ``` 23 | 24 | As an example, testing a chat application with two concurrent users: 25 | 26 | ```python 27 | def test(data): 28 | navigate('https://app-url.com/') # browser opened with id='main' 29 | open_browser('second browser') # second browser opened 30 | navigate('https://app-url.com/') 31 | activate_browser('main') 32 | send_chat_message('hey there!') 33 | activate_browser('second browser') 34 | assert_message_received('hey there!') 35 | ``` -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Golem 2 | ================================= 3 | 4 | Intro 5 | ******** 6 | Golem is a complete test automation tool and framework for end-to-end testing. 7 | It creates powerful, robust and maintainable test suites, yet, it is easy to pick up and learn even without a lot of programming knowledge. 8 | It is based on Selenium Webdriver and it can be extended using Python. 9 | 10 | **It can:** 11 | 12 | - Use the Page Object pattern 13 | - Write tests with multi data sets (data-driven) 14 | - Run tests in parallel 15 | - Test APIs 16 | - Run tests remotely (Selenium Grid or a cloud testing provider) 17 | - It can be executed from Jenkins or any other CI tool 18 | 19 | 20 | **It has:** 21 | 22 | - A complete GUI module (a web application) to write and execute tests 23 | - A reporting engine and a web reports module 24 | - An interactive console 25 | 26 | 27 | Selenium 28 | ******** 29 | 30 | Some prior knowledge of Selenium is required to work with Golem. 31 | Golem extends Selenium webdriver and webelement classes and uses a simplified interface for finding elements. 32 | It is advised to understand how standard Selenium works. 33 | `This guide `_ is a good place to start. 34 | 35 | 36 | Contents 37 | ******** 38 | 39 | .. toctree:: 40 | :maxdepth: 2 41 | :glob: 42 | 43 | installation 44 | tutorial-part-1 45 | tutorial-part-2 46 | browsers 47 | finding-elements 48 | waiting-for-elements 49 | golem-actions 50 | tests 51 | pages 52 | suites 53 | test-data 54 | running-tests 55 | report 56 | settings 57 | golem-test-framework 58 | gui 59 | command-line-interface 60 | interactive-mode 61 | golem_public_api/index 62 | guides/index 63 | 64 | .. Indices and tables 65 | .. ================== 66 | 67 | .. * :ref:`genindex` 68 | .. * :ref:`modindex` 69 | .. * :ref:`search` 70 | -------------------------------------------------------------------------------- /docs/source/installation.md: -------------------------------------------------------------------------------- 1 | Installation 2 | ================================================== 3 | 4 | ## Requirements 5 | 6 | ### Python 7 | 8 | Golem requires Python 3.6 or higher. 9 | 10 | **Windows**: 11 | 12 | The Windows installer works fine, you can get it from here: [python.org/downloads/](http://www.python.org/downloads/) 13 | 14 | **Mac**: 15 | 16 | To install on Mac OS follow these instructions: [Installing Python 3 on Mac OS X](http://python-guide.readthedocs.io/en/latest/starting/install3/osx/) 17 | 18 | **Linux**: 19 | 20 | Debian 8 and Ubuntu 14.04 comes with Python 3.4 pre-installed, newer Linux distributions might come with newer Python versions. 21 | 22 | Since Linux tends to have both Python 2 and 3 installed alongside each other, the command to execute the latter should be 'python3' instead of just 'python'. 23 | 24 | ### PIP 25 | 26 | PIP is the package manager for Python. It is required to install Golem and its dependencies. Check if you already have it. PIP comes bundled with the newer versions of Python. 27 | 28 | ``` 29 | pip --version 30 | ``` 31 | or 32 | ``` 33 | pip3 --version 34 | ``` 35 | 36 | If you don't have PIP installed, follow [these instructions](https://pip.pypa.io/en/stable/installing/). 37 | 38 | 39 | ## Create a Virtual Environment 40 | 41 | It is recommended to install Golem and its dependencies in a [virtual environment](http://www.virtualenv.org/en/latest/) instead of globally. To do that, follow these steps: 42 | 43 | ### Install Virtualenv 44 | 45 | ``` 46 | pip install virtualenv 47 | ``` 48 | 49 | Create a new virtualenv in the './env' folder 50 | 51 | ``` 52 | virtualenv env 53 | ``` 54 | 55 | If the virtual environment is being created with Python 2 instead of 3, use the following command instead: 56 | 57 | ``` 58 | virtualenv env -p python3 59 | ``` 60 | 61 | ### Activate the Environment 62 | 63 | To use a virtual environment it needs to be activated first. 64 | 65 | **Windows**: 66 | 67 | ``` 68 | env\scripts\activate 69 | ``` 70 | 71 | **Mac/Linux**: 72 | 73 | ``` 74 | source env/bin/activate 75 | ``` 76 | 77 | ## Install Golem Using PIP 78 | 79 | The quickest and the preferred way to install Golem. 80 | 81 | ``` 82 | pip install golem-framework 83 | ``` 84 | 85 | 86 | ## Installing From Source 87 | 88 | ``` 89 | pip install -U https://github.com/golemhq/golem/archive/master.tar.gz 90 | ``` 91 | -------------------------------------------------------------------------------- /docs/source/interactive-mode.md: -------------------------------------------------------------------------------- 1 | Interactive Mode 2 | ================================================== 3 | 4 | With Golem the execution of a test can be paused at any point to start an interactive console with all the actions available. 5 | This is useful for debugging tests. 6 | 7 | ## interactive_mode Action 8 | 9 | To start the interactive console at any point of a test just add the **interactive_mode** action. Example: 10 | 11 | **test.py** 12 | ```python 13 | def test(data): 14 | navigate('http://wikipedia.org/') 15 | interactive_mode() 16 | click(page.button) 17 | take_screenshot('final screenshot') 18 | ``` 19 | 20 | When the test reaches the second step, the interactive console is going to start: 21 | 22 | 23 | ![interactive-console](_static/img/interactive-console.png "Interactive Console") 24 | 25 | 26 | When the interactive console is terminated, the test will resume the execution from that point on. 27 | 28 | 29 |
30 |

Note

31 |

If the test is not run with the -i flag, the interactive_mode action will be ignored

32 |
33 | 34 | 35 | ## Quick Interactive Mode 36 | 37 | It is possible to start a quick interactive shell by not providing a project and test to the run command: 38 | 39 | ``` 40 | golem run -i 41 | ``` 42 | 43 | This will start an interactive console with a clean slate. 44 | 45 | ```text 46 | >golem run -i 47 | Entering interactive mode 48 | type exit() to stop 49 | type help() for more info 50 | >>> navigate('https://en.wikipedia.org') 51 | 12:47:54 INFO Navigate to: 'https://en.wikipedia.org' 52 | >>> send_keys('#searchInput', 'automation') 53 | 12:48:58 INFO Write 'automation' in element #searchInput 54 | >>> click('#searchButton') 55 | 12:49:18 INFO Click #searchButton 56 | >>> get_browser().title 57 | 'Automation - Wikipedia' 58 | >>> assert_title_contains('Automation') 59 | 12:49:50 INFO Assert page title contains 'Automation' 60 | ``` 61 | 62 | ## Python Debugger 63 | 64 | It is possible to add a Python debugger breakpoint (pdb.set_trace()) using the **set_trace** action. 65 | As with the **interactive_mode**, the test must be run with the *-i* flag for this action to take effect. 66 | More info about pdb [here](https://docs.python.org/3/library/pdb.html). 67 | 68 | Example: 69 | 70 | **test.py** 71 | ```python 72 | def test(data): 73 | navigate('https://en.wikipedia.org') 74 | set_trace() 75 | ``` 76 | 77 | ```text 78 | >golem run project test -i 79 | 12:57:11 INFO Test execution started: test 80 | 12:57:11 INFO Browser: chrome 81 | 12:57:11 INFO Navigate to: 'https://en.wikipedia.org' 82 | --Return-- 83 | > c:\[...]\golem\actions.py(1578)set_trace()->None 84 | -> pdb.set_trace() 85 | (Pdb) 86 | ``` -------------------------------------------------------------------------------- /docs/source/pages.md: -------------------------------------------------------------------------------- 1 | Pages 2 | ================================================== 3 | 4 | A page in Golem is a module that can be imported into a test. 5 | It can be used as a Page Object. 6 | 7 | ## Page Elements 8 | 9 | A way to store element selectors in a page is by using a tuple. 10 | This is the default behavior for the Web Module. 11 | 12 | ```python 13 | input = ('id', 'myID', 'My Input') 14 | button = ('css', 'button.btn-default', 'My Button') 15 | ``` 16 | 17 | The third value in the tuple is optional and it is used as a friendly name by the execution report. 18 | 19 | ## Page Functions 20 | 21 | A page can have functions and these will be available from the test after importing the page into it. 22 | These functions will also be available when using the Web Module as regular actions do. 23 | 24 | **Example 1:** 25 | 26 | page1.py 27 | ```python 28 | from golem.browser import element 29 | 30 | title = ('css', 'h1') 31 | 32 | def assert_title(expected_title): 33 | assert element(title).text == expected_title 34 | ``` 35 | 36 | test1.py 37 | ```python 38 | pages = ['page1'] 39 | 40 | def test(data): 41 | navigate('http://...') 42 | page1.assert_title('My Expected Title') 43 | ``` 44 | 45 | **Example 2:** 46 | 47 | page2.py 48 | ```python 49 | from golem.browser import elements 50 | 51 | table_rows = ('css', 'table > tbody > tr') 52 | 53 | def assert_row_amount(expected_amount): 54 | rows = elements(table_rows) 55 | assert len(rows) == expected_amount, 'Incorrect amount of rows' 56 | ``` 57 | 58 | test2.py 59 | ```python 60 | pages = ['page2'] 61 | 62 | def test(data): 63 | navigate('http://...') 64 | page2.assert_row_amount(5) 65 | ``` -------------------------------------------------------------------------------- /docs/source/report.md: -------------------------------------------------------------------------------- 1 | Report 2 | ================================================== 3 | 4 | ## Default Report 5 | 6 | When an execution is run a JSON report is generated in this location: 7 | 8 | ``` 9 | /projects//reports///report.json 10 | ``` 11 | 12 | ## Generate Reports After Execution 13 | 14 | These are the available report types: 15 | 16 | * html (single html file, screenshots included) 17 | * html-no-images (single html file, without screenshots) 18 | * json 19 | * junit (XML compatible with Jenkins) 20 | 21 | Example: 22 | ``` 23 | golem run project suite -r junit html html-no-images json 24 | ``` 25 | 26 | ### Report Location 27 | 28 | The location of the reports can be specified with the --report-folder argument: 29 | 30 | ``` 31 | golem run project suite -r html --report-folder /the/path/to/the/report 32 | ``` 33 | 34 | ### Report Name 35 | 36 | By default, the report name is 'report' ('report.xml', 'report.html', 'report-no-images.html' and 'report.json') 37 | 38 | The name of the reports can be modified with the --report-name argument: 39 | 40 | ``` 41 | golem run project suite -r html --report-name report_name 42 | ``` 43 | 44 | ## Modify Screenshot Format, Size, and Quality 45 | 46 | The size and compression of the screenshots can be modified to reduce the size on disk. 47 | 48 | For example: 49 | 50 | Given the default settings (PNG image, no resize, no compression), a screenshot was ~**149kb**. 51 | 52 | When these settings were applied: 53 | 54 | ```JSON 55 | { 56 | "screenshots": { 57 | "format": "jpg", 58 | "quality": 50, 59 | "resize": 70 60 | } 61 | } 62 | ``` 63 | 64 | Then the same screenshot takes ~**35kb**. 65 | 66 | Experiment to find optimum settings. More info on screenshot formatting [here](settings.html#screenshots). 67 | -------------------------------------------------------------------------------- /docs/source/running-tests.md: -------------------------------------------------------------------------------- 1 | Running tests 2 | ================================================== 3 | 4 | ## Run a single test 5 | 6 | A test file can be run using the file path or the test module path. 7 | In both cases it should be relative to the *tests* folder. 8 | ``` 9 | golem run project_name test_name 10 | golem run project_name test_name.py 11 | golem run project_name folder.test_name 12 | golem run project_name folder/test_name.py 13 | ``` 14 | 15 | ## Run a suite 16 | 17 | Similar to a test, a suite can be run using the file path or the test module path. 18 | In both cases it should be relative to the *suites* folder. 19 | 20 | ``` 21 | golem run project_name suite_name 22 | golem run project_name suite_name.py 23 | golem run project_name folder.suite_name 24 | golem run project_name folder/suite_name.py 25 | ``` 26 | 27 | ## Run every test in a directory 28 | 29 | To select all the tests in a directory and subdirectories a relative path can be supplied. 30 | The path has to be relative to the *tests* folder. 31 | 32 | ``` 33 | golem run project_name folder/ 34 | ``` 35 | 36 | ### Run every test in a project 37 | 38 | ``` 39 | golem run project_name . 40 | ``` 41 | 42 | ## Select Browsers 43 | 44 | ``` 45 | golem run project suite -b chrome firefox 46 | ``` 47 | 48 | Every selected test will be run for each of the selected browsers. 49 | The browsers can also be defined inside a suite. 50 | If no browser is set, the default defined in settings will be used. 51 | The valid options for browsers are listed [here](browsers.html#valid-options). 52 | 53 | ## Run Tests in Parallel 54 | 55 | To run test files in parallel the number of processes must be set to more than 1. 56 | This can be done through the *golem run* command or by the *processes* attribute of a suite. 57 | 58 | ``` 59 | golem run project suite_name -p 3 60 | ``` 61 | 62 | ## Select Environments 63 | 64 | Select which [environments](test-data.html#environments) to use for a test execution: 65 | 66 | ``` 67 | golem run project suite -e internal staging 68 | ``` 69 | 70 | ## Filter Tests by Tags 71 | 72 | The selected tests for an execution can be filtered by tags. 73 | 74 | ``` 75 | golem run project suite -t smoke "release 42.11.01" 76 | ``` 77 | 78 | Multiple tags are always used with *and* operator. 79 | To use a combination of *and*, *or*, and *not*, a tag expression must be used: 80 | 81 | ``` 82 | golem run project suite -t "smoke and (regression or not 'release 001')" 83 | ``` 84 | -------------------------------------------------------------------------------- /docs/source/settings.md: -------------------------------------------------------------------------------- 1 | Settings 2 | ================================================== 3 | 4 | Settings are defined in the settings.json file. They modify certain Golem behaviors. 5 | There is a global settings.json file and a project settings.json file. 6 | Note: project settings override global settings. 7 | 8 | 9 | ## Setting List 10 | 11 | 12 | ### search_timeout 13 | 14 | Default time to wait looking for an element until it is present. Default is 20 seconds. 15 | 16 | ### wait_displayed 17 | 18 | Wait for elements to be present and displayed. Default is False. 19 | 20 | ### screenshot_on_error 21 | 22 | Take a screenshot on error by default. Default is True. 23 | 24 | ### screenshot_on_step 25 | 26 | Take a screenshot on every step. Default is False. 27 | 28 | ### screenshot_on_end 29 | 30 | Take a screenshot after 'test' function ends. Default is False. 31 | 32 | ### highlight_elements 33 | 34 | Highlight elements on the screen when found. Default is False 35 | 36 | ### wait_hook 37 | 38 | Custom wait method to use for every action, that can be specific to each application. It must be defined inside extend.py 39 | 40 | ### default_browser 41 | 42 | Define the driver to use unless overriden by the -b/--browsers flag. Default is 'chrome'. The valid options are listed [here](browsers.html#valid-options). 43 | 44 | ### chromedriver_path 45 | 46 | Path to the Chrome driver executable. 47 | 48 | ### edgedriver_path 49 | 50 | Path to the Edge driver executable. 51 | 52 | ### geckodriver_path 53 | 54 | Path to the Gecko driver executable. 55 | 56 | ### iedriver_path 57 | 58 | Path to the Internet Explorer driver driver executable. 59 | 60 | ### operadriver_path 61 | 62 | Path to the Opera driver executable. 63 | 64 | ### opera_binary_path 65 | 66 | The path to the Opera binary file. Used to fix "Error: cannot find Opera binary" error. 67 | 68 | ### remote_url 69 | 70 | The URL to use when connecting to a remote webdriver, for example, when using selenium grid. Default is 'http://localhost:4444/wd/hub' 71 | 72 | ### remote_browsers 73 | 74 | Defines a list of remote browsers with its capabilities, required to run tests with Selenium Grid or another remote device provider. 75 | The minimum capabilities required are 'browserName', 'version' and 'platform', read [this](https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities) for more info. 76 | 77 | Example: settings.json 78 | ``` 79 | { 80 | 81 | "remote_browsers": { 82 | "chrome_60_mac": { 83 | "browserName": "chrome", 84 | "version": "60.0", 85 | "platform": "macOS 10.12" 86 | }, 87 | "firefox_56_windows": { 88 | "browserName": "firefox", 89 | "version": "56.0", 90 | "platform": "Windows 10" 91 | } 92 | } 93 | 94 | } 95 | ``` 96 | 97 | 98 | ### implicit_actions_import 99 | Import golem.actions module implicitly to the tests. 100 | Modifies test saving behavior when using the GUI test builder. 101 | Default is true. 102 | 103 | ### implicit_page_import 104 | Import pages implicitly to the test from a list of strings. 105 | When true, imported pages are saved as a list of strings. When false, import statements are used instead. 106 | Default is true. 107 | 108 | ### screenshots 109 | 110 | Modify screenshot format, size, and quality before saving to disk. 111 | 112 | Requires Pillow to be installed. It must be installed separately: ```pip install pillow``` 113 | 114 | It should be an object with the following attributes: format, quality, width, height, and resize 115 | 116 | * **format**: "jpg" or "png". The default is "png". 117 | * **quality**: an int in 1..95 range. The default is 75. This only applies to "jpg" files. 118 | * **width**: defines the width of screenshots. If "height" is not set, this will maintain the aspect ratio. 119 | * **height**: defines the height of screenshots. If "width" is not set, this will maintain the aspect ratio. 120 | * **resize**: the percentage to resize screenshots. Must be int or string in the format "55" or "55%". To resize by percentage do not set width or height. 121 | 122 | Example: 123 | ```JSON 124 | { 125 | "screenshots": { 126 | "format": "jpg", 127 | "quality": 50, 128 | "resize": 70 129 | } 130 | } 131 | ``` 132 | 133 | ### cli_log_level 134 | 135 | command line log level. 136 | Options are: DEBUG, INFO, WARNING, ERROR, CRITICAL. Default is INFO. 137 | 138 | ### log_all_events 139 | 140 | Log all events or just Golem events. Default is true. 141 | 142 | ### start_maximized 143 | 144 | Start the browser maximized. Default is true. -------------------------------------------------------------------------------- /docs/source/suites.md: -------------------------------------------------------------------------------- 1 | Suites 2 | ================================================== 3 | 4 | A suite can execute a set of tests with the specified configuration. 5 | A suite contains a list of *tests*, *browsers*, and *environments* and the number of *processes*, and *tags*. 6 | Consider the following example: 7 | 8 | **full_regression.py** 9 | ```python 10 | 11 | browsers = ['firefox', 'chrome'] 12 | 13 | environments = [] 14 | 15 | processes = 2 16 | 17 | tags = [] 18 | 19 | tests = [ 20 | 'test1', 21 | 'test2', 22 | 'some_folder.test3', 23 | ] 24 | 25 | ``` 26 | 27 | 28 | 29 |
30 |

Note

31 |

This suite will execute all marked tests, once per each browser, environment and test set

32 |
33 | 34 | 35 | ### Test Parallelization 36 | 37 | The 'processes = 2' tells Golem how many tests should be executed at the same time. The default is one (one at a time). How many tests can be parallelized depends on your test infrastructure. 38 | 39 | 40 | ### Environments 41 | 42 | Environments are defined in the environments.json file inside a project. See [Environments](test-data.html#environments). 43 | -------------------------------------------------------------------------------- /docs/source/tests.md: -------------------------------------------------------------------------------- 1 | Tests 2 | ================================================== 3 | 4 | Tests are functions that begin with 'test' and are located in Python modules in the test folder of a project. 5 | 6 | To create a test first start a Golem test directory, if you don't already have one, and add a project to it: 7 | 8 | ``` 9 | golem-admin createdirectory 10 | cd 11 | golem createproject 12 | ``` 13 | 14 | Then add a test file inside that project: 15 | 16 | ``` 17 | golem createtest 18 | ``` 19 | 20 | A project and a test can also be created using the Web Module: 21 | 22 | ``` 23 | golem gui 24 | ``` 25 | 26 | ## Test Structure 27 | 28 | ```python 29 | 30 | description = '' 31 | 32 | tags = [] 33 | 34 | pages = [] 35 | 36 | skip = False 37 | 38 | 39 | def setup(data): 40 | pass 41 | 42 | 43 | def test_one(data): 44 | pass 45 | 46 | 47 | def test_two(data): 48 | pass 49 | 50 | 51 | def teardown(data): 52 | pass 53 | ``` 54 | 55 | 56 | A test file must implement at least one **test** function that receives a data object as argument. 57 | 58 | ## Multiple Tests per File 59 | 60 | All test functions inside a test file are run in sequence. The data is shared between tests. 61 | The browser session is shared as well, unless a test explicitly closes the current open browser. 62 | 63 | ## Test Data 64 | 65 | Test data can be defined inside the file or in a separate CSV file. 66 | For detailed info about see: [Test Data](test-data.html) 67 | 68 | ### CSV Data 69 | 70 | It should be defined in a CSV file with the same name and in the same folder as the test. 71 | 72 | ### Infile Test Data 73 | 74 | A test can have data defined as a list of dictionaries. 75 | 76 | ```python 77 | 78 | data = [ 79 | { 80 | 'url': 'http://url1.com', 81 | 'title': 'title1' 82 | }, 83 | { 84 | 'url': 'http://url2.com', 85 | 'title': 'title2' 86 | } 87 | ] 88 | 89 | def test(data): 90 | navigate(data.url) 91 | assert_title(data.title) 92 | ``` 93 | 94 | Note: when saving a test using the Test Module, if the *test_data* setting is not 'infile', any data stored in the test will be moved to a CSV file. 95 | 96 | ## Skip flag 97 | 98 | A flag variable to indicate that this test should be skipped. 99 | It should be a boolean or a string to use as skip message. 100 | Note: tests will only be skipped when running from a suite. 101 | 102 | ## Tags 103 | 104 | A list of tags (strings). 105 | Tags can be used to filter tests when running a suite. 106 | See [Filter Tests by Tags](running-tests.html#filter-tests-by-tags). 107 | 108 | ## Implicit vs Explicit Imports 109 | 110 | By default, the test runner imports the golem.actions module and any page module implicitly during the execution. 111 | Pages are saved as a list of strings. 112 | The GUI test builder complies with this format and generates code like the following: 113 | 114 | ```python 115 | pages = ['page1'] 116 | 117 | 118 | def test(data): 119 | navigate('https://...') 120 | page1.custom_funtion() 121 | ``` 122 | 123 | This behaviour can be turned off by setting [implicit_actions_import](settings.html#implicit-actions-import) and [implicit_page_import](settings.html#implicit-page-import) to false. 124 | 125 | Then, the test structure will be: 126 | 127 | ```python 128 | from golem import actions 129 | 130 | from projects..pages import page1 131 | 132 | 133 | def test(data): 134 | actions.navigate('https://...') 135 | page1.custom_funtion() 136 | ``` 137 | 138 | 139 | ### GUI Test Builder and Imports Statements 140 | 141 | The GUI test builder only supports import statements for the **golem.actions** module and any Python module 142 | inside the **pages** folder; and only when the implicit modes are turned off. 143 | Any other import statements will be discarded when saving a test from the GUI test builder. -------------------------------------------------------------------------------- /docs/source/tutorial-part-1.md: -------------------------------------------------------------------------------- 1 | Tutorial - Part 1 2 | ================================================== 3 | 4 | Let's create the first test in Golem and learn the main features along the way. 5 | This tutorial assumes Golem is already installed. If not, head over to the [Installation](installation.html) section. 6 | 7 | 8 | ## Create a Test Directory 9 | 10 | A **Test Directory** needs to be created first. This directory contains the initial folder structure and some config files. 11 | To create a Test Directory, open a console wherever you want the new directory to be and execute this command: 12 | 13 | ``` 14 | golem-admin createdirectory 15 | ``` 16 | 17 | This will create a **testdir** folder that will be used for all subsequent projects. 18 | 19 | 20 | ## Download Webdriver 21 | 22 | Each browser requires its own Webdriver executable. 23 | Golem uses the [webdriver-manager](https://github.com/golemhq/webdriver-manager) tool to download these automatically. 24 | 25 | ``` 26 | cd 27 | webdriver-manager update 28 | ``` 29 | 30 | The Webdriver executables are downloaded into the *drivers* folder inside the Test Directory. 31 | 32 | The settings.json file contains a key for each browser that should point to the Webdriver file for that browser. 33 | For example: 34 | 35 | *settings.json* 36 | ``` 37 | { 38 | "chromedriver_path": "./drivers/chromedriver*" 39 | } 40 | ``` 41 | 42 | The '\*' wildcard at the end of the path is used to match the highest version available, in the case there's more than one present. 43 | 44 | This doesn't need to be modified unless the Webdriver files are located elsewhere. 45 | 46 | 47 | For more detail, check [this page](browsers.html#webdriver-manager). 48 | 49 | 50 | ## Start the Web Module 51 | 52 | To start the Golem Web Module, run the following command: 53 | 54 | ``` 55 | golem gui 56 | ``` 57 | 58 | The Web Module can be accessed at [http://localhost:5000/](http://localhost:5000/) 59 | 60 | The following superuser is available at the start: username: **admin** / password: **admin** 61 | 62 | 63 | 64 | ## Create a New Project 65 | 66 | A new **project** can be created by using the Web Module or by the following command: 67 | 68 | ``` 69 | cd 70 | golem createproject 71 | ``` 72 | 73 | A new project has the following structure: 74 | ``` 75 | project_name/ 76 | pages/ 77 | reports/ 78 | suites/ 79 | tests/ 80 | environments.json 81 | settings.json 82 | ``` 83 | 84 | 85 | Once the test directory and the project are created you can proceed to [Tutorial - Part 2](tutorial-part-2.html) -------------------------------------------------------------------------------- /docs/source/waiting-for-elements.md: -------------------------------------------------------------------------------- 1 | Waiting for Elements 2 | ================================================== 3 | 4 | ## Implicit Wait 5 | 6 | There is an implicit search timeout that is applied to every time a web element is searched. 7 | This search timeout is defined in the settings file by using the [search_timeout](settings.html#search-timeout) setting key. 8 | 9 | It can also be set by using the [set_search_timeout](golem-actions.html#set-search-timeout-timeout) and [get_search_timeout](golem-actions.html#get-search-timeout) actions. 10 | 11 | Note that this timeout only waits for the element to be present (to exist in the DOM). It does not wait for the element to be visible, clickable, enabled, etc. 12 | 13 | 14 | ## Explicit Wait 15 | 16 | There are a few ways to wait for an element to be present. 17 | 18 | Using the [wait_for_element_present](golem-actions.html#wait-for-element-present-element-timeout-30) action: 19 | 20 | ```python 21 | from golem import actions 22 | 23 | actions.wait_for_element_present('#button-id', 15) 24 | actions.click('#button-id') 25 | ``` 26 | 27 | Using the *timeout* argument in the find methods: 28 | 29 | ```python 30 | from golem import actions 31 | 32 | button = actions.get_browser().find('#button-id', timeout=15) 33 | button.click() 34 | ``` 35 | 36 | Using the wait_for_element_present method of the WebDriver class: 37 | 38 | ```python 39 | from golem import actions 40 | 41 | actions.get_browser().wait_for_element_present('#button-id', timeout=15) 42 | ``` 43 | 44 | ## Wait for Element Displayed 45 | 46 | Very often an element needs to be displayed (visible) besides being present. Here are the ways to wait for an element to be visible. 47 | 48 | Using the [wait_displayed](settings.html#wait-displayed) setting. This is defined globally for every search. 49 | 50 | Using the [wait_for_element_displayed](golem-actions.html#wait-for-element-displayed-element-timeout-30) action. 51 | 52 | Using the *wait_displayed* argument in the find methods: 53 | 54 | ```python 55 | from golem import actions 56 | 57 | actions.get_browser().find('#button-id', 15, wait_displayed=True).click() 58 | ``` 59 | 60 | Using the *wait_displayed* method of the WebElement class: 61 | 62 | ```python 63 | from golem import actions 64 | 65 | button = actions.get_browser().find('#button-id') 66 | button.wait_displayed().click() 67 | ``` -------------------------------------------------------------------------------- /golem/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.10.1" 2 | -------------------------------------------------------------------------------- /golem/bin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/golem/bin/__init__.py -------------------------------------------------------------------------------- /golem/bin/golem_admin.py: -------------------------------------------------------------------------------- 1 | """A CLI admin script used to generate the initial test directory that 2 | contains the projects and all the required fields for Golem to 3 | work. 4 | """ 5 | from golem.cli import argument_parser, commands, messages 6 | 7 | 8 | def main(): 9 | parser = argument_parser.get_admin_parser() 10 | args = parser.parse_args() 11 | if args.help: 12 | print(messages.ADMIN_USAGE_MSG) 13 | elif args.command: 14 | if args.command == 'createdirectory': 15 | commands.createdirectory_command(args.name, args.no_confirm) 16 | else: 17 | print(messages.ADMIN_USAGE_MSG) 18 | -------------------------------------------------------------------------------- /golem/bin/golem_init.py: -------------------------------------------------------------------------------- 1 | """CLI script to start golem""" 2 | import os 3 | import sys 4 | 5 | from golem.main import execute_from_command_line 6 | 7 | 8 | def main(): 9 | sys.dont_write_bytecode = True 10 | execute_from_command_line(os.getcwd()) 11 | -------------------------------------------------------------------------------- /golem/bin/golem_standalone.py: -------------------------------------------------------------------------------- 1 | """Golem standalone script 2 | 3 | Use PyInstaller to generate an executable: 4 | 5 | pyinstaller golem/bin/golem_standalone.py --distpath . --onefile -n golem 6 | --add-data "golem/gui/templates:golem/gui/templates" 7 | --add-data "golem/gui/static:golem/gui/static" 8 | 9 | Note: use `;` (semi-colon) instead of `:` (colon) in Windows 10 | """ 11 | import os 12 | import sys 13 | from multiprocessing import freeze_support 14 | 15 | from golem.main import execute_from_command_line 16 | from golem.bin import golem_admin 17 | from webdriver_manager.main import main as webdriver_manager_main 18 | from golem.cli.messages import STANDALONE_USAGE 19 | 20 | 21 | if __name__ == '__main__': 22 | freeze_support() 23 | if len(sys.argv) > 1: 24 | if sys.argv[1] in ['golem-admin', 'admin']: 25 | del sys.argv[1] 26 | golem_admin.main() 27 | elif sys.argv[1] == 'webdriver-manager': 28 | del sys.argv[1] 29 | webdriver_manager_main() 30 | else: 31 | execute_from_command_line(os.getcwd()) 32 | else: 33 | print(STANDALONE_USAGE) 34 | -------------------------------------------------------------------------------- /golem/cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/golem/cli/__init__.py -------------------------------------------------------------------------------- /golem/cli/argument_parser.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | 4 | def get_parser(): 5 | parser = argparse.ArgumentParser(prog='golem', add_help=False) 6 | parser.add_argument('-h', '--help', nargs='?', const=True, default=False) 7 | parser.add_argument('--golem-dir', type=str) 8 | parser.add_argument('-v', '--version', action='store_true', default=False) 9 | subparsers = parser.add_subparsers(dest='command') 10 | 11 | # run 12 | report_choices = ['junit', 'html', 'html-no-images', 'json'] 13 | parser_run = subparsers.add_parser('run', add_help=False) 14 | parser_run.add_argument('project', nargs='?', default='') 15 | parser_run.add_argument('test_query', nargs='?', default='') 16 | parser_run.add_argument('-b', '--browsers', nargs='*', default=[], type=str) 17 | parser_run.add_argument('-p', '--processes', nargs='?', default=1, type=int) 18 | parser_run.add_argument('-e', '--environments', nargs='+', default=[], type=str) 19 | parser_run.add_argument('--test-functions', nargs='*', default=[], type=str) 20 | parser_run.add_argument('-t', '--tags', nargs='*', default=[], type=str) 21 | parser_run.add_argument('-i', '--interactive', action='store_true', default=False) 22 | parser_run.add_argument('-r', '--report', nargs='+', choices=report_choices, 23 | default=[], type=str) 24 | parser_run.add_argument('--report-folder', nargs='?', type=str) 25 | parser_run.add_argument('--report-name', nargs='?', type=str) 26 | parser_run.add_argument('--timestamp', nargs='?', type=str) 27 | parser_run.add_argument('-l', '--cli-log-level') 28 | parser_run.add_argument('-h', '--help', action='store_true') 29 | 30 | # gui 31 | parser_gui = subparsers.add_parser('gui', add_help=False) 32 | parser_gui.add_argument('--host', action='store', nargs='?', default=None) 33 | parser_gui.add_argument('-p', '--port', action='store', nargs='?', default=5000, type=int) 34 | parser_gui.add_argument('-d', '--debug', action='store_true', default=False) 35 | parser_gui.add_argument('-h', '--help', action='store_true') 36 | 37 | # createproject 38 | parser_createproject = subparsers.add_parser('createproject', add_help=False) 39 | parser_createproject.add_argument('project') 40 | parser_createproject.add_argument('-h', '--help', action='store_true') 41 | 42 | # createtest 43 | parser_createtest = subparsers.add_parser('createtest', add_help=False) 44 | parser_createtest.add_argument('project') 45 | parser_createtest.add_argument('test') 46 | parser_createtest.add_argument('-h', '--help', action='store_true') 47 | 48 | # createsuite 49 | parser_createsuite = subparsers.add_parser('createsuite', add_help=False) 50 | parser_createsuite.add_argument('project') 51 | parser_createsuite.add_argument('suite') 52 | parser_createsuite.add_argument('-h', '--help', action='store_true') 53 | 54 | # createsuperuser 55 | subparsers.add_parser('createuser', add_help=False) 56 | 57 | # createsuperuser 58 | parser_createsuperuser = subparsers.add_parser('createsuperuser', add_help=False) 59 | parser_createsuperuser.add_argument('-u', '--username') 60 | parser_createsuperuser.add_argument('-e', '--email') 61 | parser_createsuperuser.add_argument('-p', '--password') 62 | parser_createsuperuser.add_argument('-n', '--noinput', action='store_true', default=False) 63 | parser_createsuperuser.add_argument('-h', '--help', action='store_true') 64 | 65 | return parser 66 | 67 | 68 | def get_admin_parser(): 69 | parser = argparse.ArgumentParser(prog='golem-admin', add_help=False) 70 | parser.add_argument('-h', '--help', nargs='?', const=True, default=False) 71 | subparsers = parser.add_subparsers(dest='command') 72 | 73 | # createdirectory 74 | parser_createdirectory = subparsers.add_parser('createdirectory', add_help=False) 75 | parser_createdirectory.add_argument('name') 76 | parser_createdirectory.add_argument('-y', '--yes', action='store_true', 77 | default=False, dest='no_confirm') 78 | parser_createdirectory.add_argument('-h', '--help', action='store_true') 79 | 80 | return parser 81 | -------------------------------------------------------------------------------- /golem/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/golem/core/__init__.py -------------------------------------------------------------------------------- /golem/core/environment_manager.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | from golem.core import utils, session 5 | 6 | 7 | def get_envs(project): 8 | """get the list of envs defined in the environments.json file""" 9 | envs = [] 10 | env_data = get_environment_data(project) 11 | if env_data: 12 | envs = list(env_data.keys()) 13 | return envs 14 | 15 | 16 | def get_environment_data(project): 17 | """get the full env data defined in the environments.json file""" 18 | env_data = {} 19 | env_path = environments_file_path(project) 20 | if os.path.isfile(env_path): 21 | json_data = utils.load_json_from_file(env_path, ignore_failure=True, default={}) 22 | if json_data: 23 | env_data = json_data 24 | return env_data 25 | 26 | 27 | def get_environments_as_string(project): 28 | """get the contents of environments.json file as string""" 29 | env_data = '' 30 | env_path = environments_file_path(project) 31 | if os.path.isfile(env_path): 32 | with open(env_path, encoding='utf-8') as f: 33 | env_data = f.read() 34 | return env_data 35 | 36 | 37 | def save_environments(project, env_data): 38 | """save environments.json file contents. 39 | env_data must be a valid json string. 40 | Returns a string with the error or empty string otherwise""" 41 | error = '' 42 | if len(env_data): 43 | try: 44 | json.loads(env_data) 45 | except json.JSONDecodeError: 46 | error = 'must be valid JSON' 47 | if not error: 48 | env_path = environments_file_path(project) 49 | with open(env_path, 'w', encoding='utf-8') as f: 50 | f.write(env_data) 51 | return error 52 | 53 | 54 | def environments_file_path(project): 55 | """Path to environments.json file""" 56 | return os.path.join(session.testdir, 'projects', project, 'environments.json') 57 | -------------------------------------------------------------------------------- /golem/core/errors.py: -------------------------------------------------------------------------------- 1 | 2 | invalid_test_directory = ('Error: {} is not an valid Golem test directory; ' 3 | '.golem file not found') 4 | -------------------------------------------------------------------------------- /golem/core/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | class IncorrectSelectorType(Exception): 3 | pass 4 | 5 | 6 | class ElementNotFound(Exception): 7 | pass 8 | 9 | 10 | class TextNotPresent(Exception): 11 | pass 12 | 13 | 14 | class ElementNotDisplayed(Exception): 15 | pass 16 | -------------------------------------------------------------------------------- /golem/core/parsing_utils.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | 4 | def ast_parse_file(filename): 5 | """Parse a Python file using ast. 6 | Returns a ast.Module node 7 | """ 8 | with open(filename, "rt", encoding='utf-8') as file: 9 | return ast.parse(file.read(), filename=filename) 10 | 11 | 12 | def top_level_functions(ast_node): 13 | """Given an ast.Module node return the names of the top level functions""" 14 | return [f.name for f in ast_node.body if isinstance(f, ast.FunctionDef)] 15 | 16 | 17 | def top_level_assignments(ast_node): 18 | """Given an ast Module node, 19 | return the names of the top level assignments. 20 | e.g.: "foo = 2" -> returns ['foo'] 21 | https://greentreesnakes.readthedocs.io/en/latest/nodes.html#Assign 22 | """ 23 | assignments = [] 24 | for v in ast_node.body: 25 | if type(v) == ast.Assign: 26 | if len(v.targets) == 1: 27 | assignments.append(v.targets[0].id) 28 | return assignments 29 | -------------------------------------------------------------------------------- /golem/core/secrets_manager.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from golem.core import utils, session 4 | 5 | 6 | def get_secrets(project): 7 | secrets_data = {} 8 | secrets_json_path = os.path.join(session.testdir, 'projects', project, 'secrets.json') 9 | if os.path.exists(secrets_json_path): 10 | json_data = utils.load_json_from_file(secrets_json_path, ignore_failure=True, default={}) 11 | if json_data: 12 | secrets_data = json_data 13 | return secrets_data 14 | -------------------------------------------------------------------------------- /golem/core/session.py: -------------------------------------------------------------------------------- 1 | """Global values for current session""" 2 | 3 | testdir = None 4 | 5 | settings = {} 6 | -------------------------------------------------------------------------------- /golem/core/test_directory.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import webdriver_manager 4 | 5 | from golem.core import file_manager, settings_manager, session 6 | from golem.gui.user_management import Users 7 | 8 | 9 | def create_test_directory(testdir): 10 | file_manager.create_directory(path_list=[testdir], add_init=True) 11 | file_manager.create_directory(path_list=[testdir, 'projects'], add_init=True) 12 | file_manager.create_directory(path_list=[testdir, 'drivers'], add_init=False) 13 | settings_manager.create_global_settings_file(testdir) 14 | create_testdir_golem_file(testdir) 15 | session.testdir = testdir 16 | Users.create_super_user('admin', 'admin') 17 | 18 | 19 | def create_testdir_golem_file(testdir): 20 | """Create .golem file""" 21 | golem_file = os.path.join(testdir, '.golem') 22 | with open(golem_file, 'w') as f: 23 | secret_key = os.urandom(24).hex() 24 | f.write('[gui]\n') 25 | f.write(f'secret_key = {secret_key}\n') 26 | 27 | 28 | def get_projects(): 29 | path = os.path.join(session.testdir, 'projects') 30 | projects = next(os.walk(path))[1] 31 | projects = [x for x in projects if x != '__pycache__'] 32 | return projects 33 | 34 | 35 | def project_exists(project): 36 | return project in get_projects() 37 | 38 | 39 | def is_valid_test_directory(testdir): 40 | """Verify `testdir` is a valid test directory path. 41 | It must contain a .golem file. 42 | """ 43 | return os.path.isfile(os.path.join(testdir, '.golem')) 44 | 45 | 46 | def drivers_path(): 47 | return os.path.join(session.testdir, 'drivers') 48 | 49 | 50 | # def get_driver_versions(): 51 | # return webdriver_manager.versions(drivers_path()) 52 | 53 | 54 | def get_driver_folder_files(): 55 | """Get the list of files in testdir/drivers folder. 56 | Folders are ignored. TODO 57 | """ 58 | files = [] 59 | path = drivers_path() 60 | lst = os.listdir(path) 61 | for elem in lst: 62 | if os.path.isfile(os.path.join(path, elem)): 63 | files.append(elem) 64 | return files 65 | 66 | 67 | def delete_driver_file(filename): 68 | errors = [] 69 | path = os.path.join(drivers_path(), filename) 70 | if not os.path.isfile(path): 71 | errors.append(f'File {filename} does not exist') 72 | else: 73 | try: 74 | os.remove(path) 75 | except Exception as e: 76 | errors.append(f'There was an error removing file {filename}') 77 | return errors 78 | 79 | 80 | def update_driver(driver_name): 81 | if driver_name not in ['chromedriver', 'geckodriver']: 82 | return f'{driver_name} is not a valid driver name' 83 | webdriver_manager.update(driver_name, drivers_path()) 84 | return '' # TODO: webdriver-manager should return actual error messages 85 | -------------------------------------------------------------------------------- /golem/execution.py: -------------------------------------------------------------------------------- 1 | """ Stored values specific to a single test execution. """ 2 | 3 | test_file = None 4 | browser = None 5 | browser_definition = None 6 | browsers = {} 7 | data = None 8 | secrets = None 9 | description = None 10 | settings = None 11 | test_dirname = None 12 | test_path = None 13 | project_name = None 14 | project_path = None 15 | testdir = None 16 | execution_reportdir = None 17 | testfile_reportdir = None 18 | logger = None 19 | tags = [] 20 | environment = None 21 | 22 | # values below correspond to the current running test function 23 | test_name = None 24 | steps = [] 25 | errors = [] 26 | test_reportdir = None 27 | timers = {} 28 | -------------------------------------------------------------------------------- /golem/execution_runner/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/golem/execution_runner/__init__.py -------------------------------------------------------------------------------- /golem/execution_runner/interactive.py: -------------------------------------------------------------------------------- 1 | """Golem interactive mode.""" 2 | from golem.core import utils, settings_manager, session 3 | from golem import execution 4 | from golem import actions 5 | from golem.gui import gui_utils 6 | from golem.execution_runner.execution_runner import define_browsers 7 | from golem.test_runner import test_logger 8 | 9 | 10 | def interactive(settings, cli_browsers): 11 | """Starts the Golem interactive shell.""" 12 | browsers = utils.choose_browser_by_precedence( 13 | cli_browsers=cli_browsers, suite_browsers=[], 14 | settings_default_browser=settings['default_browser']) 15 | execution.browser_name = browsers[0] 16 | remote_browsers = settings_manager.get_remote_browsers(session.settings) 17 | default_browsers = gui_utils.get_supported_browsers_suggestions() 18 | browser_defs = define_browsers(browsers, remote_browsers, default_browsers, []) 19 | execution.testdir = session.testdir 20 | execution.browser_definition = browser_defs[0] 21 | execution.settings = settings 22 | execution.settings['interactive'] = True 23 | execution.logger = test_logger.get_logger( 24 | cli_log_level=execution.settings['cli_log_level'], 25 | log_all_events=execution.settings['log_all_events']) 26 | actions.interactive_mode() 27 | -------------------------------------------------------------------------------- /golem/execution_runner/multiprocess_executor.py: -------------------------------------------------------------------------------- 1 | """The multiprocess_executor method runs all the test cases 2 | provided in parallel using multiprocessing. 3 | """ 4 | from multiprocessing import Pool 5 | from multiprocessing.pool import ApplyResult 6 | 7 | from golem.core import session 8 | from golem.test_runner.test_runner import run_test 9 | 10 | 11 | def multiprocess_executor(project, execution_list, has_failed_tests, test_functions, processes=1, 12 | tags=None, is_suite=False): 13 | """Runs a list of tests in parallel using multiprocessing""" 14 | pool = Pool(processes=processes, maxtasksperchild=1) 15 | results = [] 16 | for test in execution_list: 17 | args = (session.testdir, 18 | project, 19 | test.name, 20 | test.data_set, 21 | test.secrets, 22 | test.browser, 23 | test.env, 24 | session.settings, 25 | test.reportdir, 26 | test.set_name, 27 | test_functions, 28 | has_failed_tests, 29 | tags, 30 | is_suite) 31 | apply_async = pool.apply_async(run_test, args=args) 32 | results.append(apply_async) 33 | map(ApplyResult.wait, results) 34 | pool.close() 35 | pool.join() 36 | -------------------------------------------------------------------------------- /golem/gui/__init__.py: -------------------------------------------------------------------------------- 1 | """The Golem GUI web application""" 2 | import os 3 | import sys 4 | 5 | from flask import Flask, g, render_template 6 | from flask_login import current_user, LoginManager 7 | 8 | import golem 9 | from . import gui_utils, user_management 10 | from golem.core import session, settings_manager, errors, test_directory 11 | from .api import api_bp 12 | from .web_app import webapp_bp 13 | from .report import report_bp 14 | 15 | 16 | def create_app(): 17 | """Call this function to create a Golem GUI app object. 18 | If called externally (e.g.: from a WSGI server) the cwd 19 | should be a valid Golem test directory""" 20 | if not session.testdir: 21 | testdir = os.getcwd() 22 | if not test_directory.is_valid_test_directory(testdir): 23 | sys.exit(errors.invalid_test_directory.format(testdir)) 24 | else: 25 | session.testdir = testdir 26 | if not session.settings: 27 | session.settings = settings_manager.get_global_settings() 28 | app = Flask(__name__) 29 | app.secret_key = gui_utils.get_secret_key() 30 | app.config['SESSION_TYPE'] = 'filesystem' 31 | app.config['GOLEM_VERSION'] = golem.__version__ 32 | login_manager = LoginManager() 33 | login_manager.login_view = 'webapp.login' 34 | login_manager.init_app(app) 35 | app.register_blueprint(webapp_bp) 36 | app.register_blueprint(report_bp) 37 | app.register_blueprint(api_bp) 38 | app.jinja_env.globals['get_user_projects'] = gui_utils.ProjectsCache.get_user_projects 39 | 40 | @login_manager.user_loader 41 | def load_user(user_id): 42 | return user_management.Users.get_user_by_id(user_id) 43 | 44 | @app.before_request 45 | def before_request(): 46 | g.user = current_user 47 | 48 | @app.errorhandler(404) 49 | def page_not_found(error): 50 | return render_template('404.html', message=error.description), 404 51 | 52 | return app 53 | -------------------------------------------------------------------------------- /golem/gui/gui_start.py: -------------------------------------------------------------------------------- 1 | """A function to start the GUI application.""" 2 | import sys 3 | import os 4 | 5 | from golem import gui 6 | 7 | from werkzeug import _reloader 8 | 9 | 10 | ORIGINAL_GET_ARGS = None 11 | 12 | 13 | def run_gui(host=None, port=5000, debug=False): 14 | # Patch Werkzeug._reloader._get_args_for_reloading() 15 | # The Flask development server reloader does not work when 16 | # started from the Golem standalone (PyInstaller) in Linux 17 | # TODO 18 | patch_werkzeug_get_args_for_reloading_wrapper() 19 | app = gui.create_app() 20 | app.run(host=host, port=port, debug=debug) 21 | 22 | 23 | def patch_werkzeug_get_args_for_reloading_wrapper(): 24 | global ORIGINAL_GET_ARGS 25 | if ORIGINAL_GET_ARGS is None: 26 | ORIGINAL_GET_ARGS = _reloader._get_args_for_reloading 27 | _reloader._get_args_for_reloading = _get_args_for_reloading_wrapper 28 | 29 | 30 | def _get_args_for_reloading_wrapper(): 31 | rv = ORIGINAL_GET_ARGS() 32 | __main__ = sys.modules["__main__"] 33 | py_script = rv[1] 34 | if __main__.__package__ is None: 35 | # Executed a file, like "python app.py". 36 | if os.name != 'nt' and os.path.isfile(py_script) and os.access(py_script, os.X_OK): 37 | # The file is marked as executable. Nix adds a wrapper that 38 | # shouldn't be called with the Python executable. 39 | rv.pop(0) 40 | return rv 41 | -------------------------------------------------------------------------------- /golem/gui/static/css/code-editor-common.css: -------------------------------------------------------------------------------- 1 | 2 | #codeEditorContainer { 3 | border: 1px solid #e0e0e0; 4 | border-radius: 3px 5 | } 6 | 7 | .CodeMirror { 8 | font-size: 13px; 9 | height: auto; 10 | } 11 | 12 | .CodeMirror-scroll { 13 | min-height: 400px 14 | } -------------------------------------------------------------------------------- /golem/gui/static/css/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/golem/gui/static/css/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /golem/gui/static/css/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/golem/gui/static/css/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /golem/gui/static/css/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/golem/gui/static/css/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /golem/gui/static/css/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/golem/gui/static/css/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /golem/gui/static/css/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/golem/gui/static/css/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /golem/gui/static/css/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/golem/gui/static/css/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /golem/gui/static/css/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/golem/gui/static/css/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /golem/gui/static/css/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/golem/gui/static/css/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /golem/gui/static/css/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/golem/gui/static/css/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /golem/gui/static/css/fonts/lato-bol-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/golem/gui/static/css/fonts/lato-bol-webfont.woff -------------------------------------------------------------------------------- /golem/gui/static/css/fonts/lato-lig-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/golem/gui/static/css/fonts/lato-lig-webfont.woff -------------------------------------------------------------------------------- /golem/gui/static/css/json_code_editor.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .codeEditorContainer { 4 | margin-top: 15px; 5 | } 6 | 7 | .CodeMirror { 8 | font-size:13px; 9 | height: auto; 10 | border-radius: 3px; 11 | border: 1px solid #e0e0e0; 12 | } 13 | 14 | .CodeMirror-scroll { 15 | min-height: 150px 16 | } -------------------------------------------------------------------------------- /golem/gui/static/css/list_common.css: -------------------------------------------------------------------------------- 1 | .box-container{ 2 | margin-bottom: 15px; 3 | padding: 10px; 4 | border-radius: 3px 5 | } 6 | 7 | .folder-content { 8 | font-size: 16px; 9 | margin: 0; 10 | padding: 0; 11 | list-style: none 12 | } 13 | 14 | #breadcrumb { 15 | font-size: 16px; 16 | font-weight: 700; 17 | } 18 | 19 | #breadcrumb a { 20 | text-decoration: none; 21 | color: #369; 22 | } 23 | 24 | .folder-content .folder-content { 25 | margin-left: 1em; 26 | position: relative 27 | } 28 | 29 | .folder-content .folder-content:before { 30 | content: ""; 31 | display: block; 32 | width: 0; 33 | position: absolute; 34 | top: 0; 35 | bottom: 0; 36 | left: 0; 37 | border-left: 1px solid; 38 | z-index: 10 39 | } 40 | 41 | .folder-content>.tree-element { 42 | padding: 0 1em; 43 | line-height: 2em; 44 | color: #369; 45 | font-weight: 700; 46 | position: relative; 47 | overflow: hidden; 48 | white-space: nowrap; 49 | } 50 | 51 | .folder-content .folder-content .tree-element:before { 52 | content:""; 53 | display: block; 54 | width: 10px; 55 | height: 0; 56 | border-top: 1px solid; 57 | margin-top: -1px; 58 | position: absolute; 59 | top: 1em; 60 | left: 0 61 | } 62 | 63 | .folder-content .folder-content .tree-element:last-child:before { 64 | background: #fff; 65 | height: auto; 66 | top: 1em; 67 | bottom: 0; 68 | z-index: 11 69 | } 70 | 71 | .folder-content .folder-content .tree-element.file:last-child:hover:before { 72 | background: #fafafa; 73 | border-left: 1px solid #fff 74 | } 75 | 76 | .folder-icon { 77 | margin-right:5px; 78 | } 79 | 80 | .tree-element a, #bottomMenu a { 81 | text-decoration: none; 82 | color: #369; 83 | } 84 | 85 | .tree-element a.file-link { 86 | max-width: calc(100% - 90px); 87 | overflow: hidden; 88 | float: left; 89 | text-overflow: ellipsis; 90 | } 91 | 92 | .folder-content li[type="test"] a.list-item-link { 93 | max-width: calc(100% - 105px); 94 | } 95 | 96 | .folder-content li button { 97 | text-decoration: none; 98 | color: #369; 99 | border: none; 100 | background: transparent; 101 | margin: 0px; 102 | padding: 0px; 103 | outline: 0; 104 | } 105 | 106 | .tree-element:not(.folder):hover { 107 | background-color: #fafafa; 108 | } 109 | 110 | .tree-element:not(.folder):hover > a.file-link { 111 | word-break: break-word; 112 | white-space: pre-wrap; 113 | } 114 | 115 | .new-element-input { 116 | max-width: 500px; 117 | font-size: 16px; 118 | padding-left: 2px 119 | } 120 | 121 | #rootFolderContent { 122 | min-height: 5px 123 | } 124 | 125 | .folder-menu-button-toggle:hover, .folder-menu-button-toggle:active, .folder-menu-button-toggle:focus { 126 | background-color: inherit !important; 127 | box-shadow: unset; 128 | outline: unset !important 129 | } 130 | 131 | .folder-menu.active { 132 | display: none 133 | } 134 | 135 | .folder-menu.active { 136 | display: block 137 | } 138 | 139 | #bottomMenu { 140 | padding-left: 1em 141 | } 142 | 143 | .file-menu { 144 | display: none; 145 | background-color: #fafafa; 146 | position: absolute; 147 | right: 0px; 148 | top: 0px; 149 | padding-left:5px; 150 | } 151 | 152 | li.tree-element:hover > .file-menu { 153 | display: block; 154 | } 155 | 156 | .file-menu .file-menu-button { 157 | padding-left: 5px 158 | } -------------------------------------------------------------------------------- /golem/gui/static/css/page_object.css: -------------------------------------------------------------------------------- 1 | 2 | .element { 3 | margin-bottom: 10px; 4 | width: 100%; 5 | } 6 | 7 | .function { 8 | margin-bottom: 10px; 9 | } 10 | 11 | pre { 12 | margin-bottom: 0px; 13 | } 14 | 15 | .step-remove-icon { 16 | float: right; 17 | height: 34px; 18 | line-height: 34px; 19 | width: 16px; 20 | } 21 | 22 | .step-remove-icon a { 23 | color: black; 24 | } -------------------------------------------------------------------------------- /golem/gui/static/css/suite.css: -------------------------------------------------------------------------------- 1 | .tree, .tree ul { 2 | margin:0; 3 | padding:0; 4 | list-style:none 5 | } 6 | 7 | .tree ul { 8 | margin-left:1em; 9 | position:relative 10 | } 11 | 12 | .tree ul ul { 13 | margin-left:.5em 14 | } 15 | 16 | .tree ul:before { 17 | content:""; 18 | display:block; 19 | width:0; 20 | position:absolute; 21 | top:0; 22 | bottom:0; 23 | left:0; 24 | border-left:1px solid 25 | } 26 | 27 | .tree li { 28 | margin:0; 29 | padding:0 1em; 30 | line-height:2em; 31 | color:#369; 32 | font-weight:700; 33 | position:relative 34 | } 35 | 36 | .tree ul li:before { 37 | content:""; 38 | display:block; 39 | width:10px; 40 | height:0; 41 | border-top:1px solid; 42 | margin-top:-1px; 43 | position:absolute; 44 | top:1em; 45 | left:0 46 | } 47 | 48 | .tree ul li:last-child:before { 49 | background:#fff; 50 | height:auto; 51 | top:1em; 52 | bottom:0 53 | } 54 | 55 | .indicator { 56 | margin-right: 4px; 57 | } 58 | 59 | .tree li { 60 | word-break: break-all 61 | } 62 | 63 | .tree li[data-type="test"] label { 64 | margin-bottom: 0px 65 | } 66 | 67 | .tree li a { 68 | text-decoration: none; 69 | color:#369; 70 | } 71 | 72 | .tree li button, .tree li button:active, .tree li button:focus { 73 | text-decoration: none; 74 | color:#369; 75 | border:none; 76 | background:transparent; 77 | margin:0px 0px 0px 0px; 78 | padding:0px 0px 0px 0px; 79 | outline: 0; 80 | } 81 | 82 | .select-testcase-checkbox { 83 | float: left; 84 | font-size: 20px; 85 | margin-top: 6px !important; 86 | margin-right: 5px !important; 87 | } 88 | 89 | .form-group{ 90 | margin-bottom: 5px; 91 | } 92 | -------------------------------------------------------------------------------- /golem/gui/static/css/test_case.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | overflow: scroll 4 | } 5 | 6 | body.modal-open { 7 | overflow: hidden 8 | } 9 | 10 | 11 | .input-group { 12 | margin-bottom: 5px; 13 | width: 100%; 14 | } 15 | 16 | #stepsContainerContainer { 17 | padding-bottom: 10px; 18 | } 19 | 20 | #setupSteps { 21 | margin-bottom: 20px; 22 | } 23 | 24 | 25 | #testFunctions { 26 | clear: both 27 | } 28 | 29 | .test-function { 30 | overflow: auto 31 | } 32 | 33 | .testFunctionNameContainer { 34 | clear: both 35 | } 36 | 37 | .testFunctionNameContainer h4 { 38 | display: inline-block; 39 | float: left; 40 | } 41 | 42 | .steps { 43 | overflow: auto; 44 | clear: both 45 | } 46 | 47 | .step-input-container { 48 | padding-left: 0px; 49 | padding-right: 10px; 50 | } 51 | 52 | .step-numbering { 53 | float: left; 54 | height: 34px; 55 | line-height: 34px; 56 | font-weight: bold; 57 | margin-right: 5px; 58 | width: 16px; 59 | cursor: move; 60 | cursor: -webkit-grabbing; 61 | } 62 | 63 | .step-remove-icon { 64 | float: right; 65 | height: 34px; 66 | line-height: 34px; 67 | font-weight: bold; 68 | width: 16px; 69 | } 70 | 71 | .step-remove-icon a{ 72 | color: black; 73 | } 74 | 75 | #steps { 76 | margin-bottom: 5px; 77 | float: left; 78 | width: 100% 79 | } 80 | 81 | #pageObjectsContainerContainer { 82 | padding-bottom: 10px; 83 | } 84 | 85 | #descriptionContainer { 86 | padding-bottom: 10px; 87 | } 88 | 89 | .step { 90 | float: left; 91 | width: 100%; 92 | clear: both; 93 | margin-bottom: 5px; 94 | } 95 | 96 | .add-new-icon { 97 | width: 25px; 98 | vertical-align: inherit; 99 | } 100 | 101 | .input-middle-btn button{ 102 | border-radius: 0px; 103 | border-left: 0px; 104 | border-right: 0px; 105 | } 106 | 107 | .params { 108 | float: left; 109 | padding-left: 0px; 110 | padding-right: 0px; 111 | width: calc(100% - 307px); 112 | } 113 | 114 | .parameter-container { 115 | width: 264px; 116 | float: left; 117 | } 118 | 119 | .step-first-input, .parameter-input { 120 | padding: 6px 121 | } 122 | 123 | .step-first-input-container { 124 | float: left; 125 | width: 264px; 126 | } 127 | 128 | @media (min-width: 1400px) { 129 | 130 | .step-first-input-container { 131 | width: 296px; 132 | } 133 | 134 | .parameter-container { 135 | width: 296px; 136 | } 137 | 138 | .params { 139 | width: calc(100% - 335px); 140 | } 141 | } 142 | 143 | #pageObjects>.page>.page-name{ 144 | cursor: default; 145 | } 146 | 147 | #pageModal.modal .modal-dialog{ 148 | width: auto; 149 | margin: 10px; 150 | height: 90%; 151 | } 152 | 153 | #pageModal.modal .modal-dialog .modal-content{ 154 | height: 100%; 155 | } 156 | 157 | #pageModal.modal .modal-dialog .modal-content .modal-body{ 158 | width: 100%; 159 | height: calc(100% - 41px); 160 | padding: 0px; 161 | } 162 | 163 | #pageModal #pageModalIframe { 164 | border: none; 165 | width: 100%; 166 | height: 100%; 167 | border-bottom-left-radius: 6px; 168 | border-bottom-right-radius: 6px; 169 | } 170 | 171 | @media (min-width: 900px){ 172 | #pageModal.modal .modal-dialog{ 173 | width: 85%; 174 | margin: 30px auto; 175 | } 176 | } 177 | 178 | .not-exist { 179 | box-shadow: inset 0px 0px 4px 1px #d9534f; 180 | } 181 | 182 | .action-description { 183 | display: none; 184 | position: absolute; 185 | left: 100%; 186 | margin-top: -24px; 187 | margin-left: 3px; 188 | border-radius: 3px; 189 | width: 300px; 190 | background: white; 191 | min-height: 50px; 192 | padding: 10px; 193 | border: 1px solid #ccc; 194 | } 195 | 196 | .autocomplete-selected .action-description { 197 | display: block 198 | } 199 | 200 | .code-block { 201 | border: 1px solid #ccc; 202 | display: inline-block; 203 | width: calc(100% - 50px); 204 | border-radius: 3px; 205 | } 206 | 207 | .code-block .CodeMirror { 208 | height: auto; 209 | border-radius: 3px; 210 | } 211 | 212 | .code-block .CodeMirror-scroll { 213 | min-height: 50px 214 | } 215 | 216 | .CodeMirror-lines { 217 | padding: 6px 0; /* Vertical padding around content */ 218 | } 219 | .CodeMirror pre { 220 | padding: 0 6px; /* Horizontal padding of content */ 221 | } 222 | 223 | .step[step-type='code-block'] { 224 | margin-bottom: 1px; 225 | } -------------------------------------------------------------------------------- /golem/gui/static/css/test_case_code.css: -------------------------------------------------------------------------------- 1 | 2 | #codeEditorContainer { 3 | border-bottom-left-radius: 0px; 4 | border-bottom-right-radius: 0px 5 | } 6 | 7 | #dataContainerContainer { 8 | border-left: 1px solid #e0e0e0; 9 | border-right: 1px solid #e0e0e0; 10 | border-bottom: 1px solid #e0e0e0; 11 | border-bottom-left-radius: 3px; 12 | border-bottom-right-radius: 3px 13 | } 14 | 15 | .light-gray-block { 16 | background-color: #F8F8F8; 17 | } 18 | 19 | .light-gray-block:hover { 20 | background-color: white; 21 | } 22 | 23 | .input-group { 24 | margin-bottom: 5px; 25 | width: 100%; 26 | } 27 | 28 | .add-new-icon { 29 | width: 25px; 30 | vertical-align: inherit; 31 | } 32 | 33 | .input-middle-btn button{ 34 | border-radius: 0px; 35 | border-left: 0px; 36 | border-right: 0px; 37 | } -------------------------------------------------------------------------------- /golem/gui/static/css/test_case_common.css: -------------------------------------------------------------------------------- 1 | 2 | #testNameInput { 3 | display: inline; 4 | } 5 | 6 | #testNameInput input { 7 | width: calc(100% - 20px); 8 | } 9 | 10 | .inline-remove-icon { 11 | display: inline-block; 12 | margin-top: 10px; 13 | margin-left: 5px; 14 | } 15 | 16 | .inline-remove-icon a { 17 | color: black; 18 | } 19 | 20 | /* data table */ 21 | 22 | #dataContainerContainer { 23 | padding-bottom: 10px; 24 | } 25 | 26 | #dataTableContainer { 27 | display: table-cell; 28 | width: 100%; 29 | position: relative; 30 | } 31 | 32 | #dataTable { 33 | background-color: white; 34 | margin-bottom: 5px; 35 | } 36 | 37 | #dataTable td, #dataTable th { 38 | padding: 0px; 39 | } 40 | 41 | #dataTable th.index { 42 | width: 30px; 43 | text-align: center; 44 | } 45 | 46 | #dataTable .input-group { 47 | margin-bottom: 0px; 48 | } 49 | 50 | #dataTable input { 51 | border: 0px; 52 | padding: 4px 4px; 53 | height: 25px; 54 | } 55 | 56 | 57 | /* CODE EDITOR CODEMIRROR */ 58 | 59 | .CodeMirror { 60 | font-size:13px; 61 | height: auto; 62 | border-radius: 3px; 63 | border: 1px solid #e0e0e0; 64 | } 65 | 66 | .CodeMirror-scroll { 67 | min-height: 150px 68 | } -------------------------------------------------------------------------------- /golem/gui/static/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/golem/gui/static/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /golem/gui/static/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/golem/gui/static/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /golem/gui/static/img/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/golem/gui/static/img/icons/favicon.ico -------------------------------------------------------------------------------- /golem/gui/static/img/plus_sign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/golem/gui/static/img/plus_sign.png -------------------------------------------------------------------------------- /golem/gui/static/img/plus_sign2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/golem/gui/static/img/plus_sign2.png -------------------------------------------------------------------------------- /golem/gui/static/js/drivers.js: -------------------------------------------------------------------------------- 1 | 2 | $(document).ready(function() { 3 | Drivers.getDriverFiles() 4 | }); 5 | 6 | 7 | const Drivers = new function(){ 8 | 9 | this.getDriverFiles = function(){ 10 | xhr.get('/api/drivers/files', {}, driverFiles => { 11 | if(driverFiles.length) { 12 | driverFiles.forEach(filename => { 13 | let li = $(` 14 |
  • 15 | ${filename} 16 | 17 | 18 | `); 20 | $('#rootFolderContent').append(li); 21 | }) 22 | } else { 23 | $('#rootFolderContent').append('
  • no drivers
  • '); 24 | } 25 | }) 26 | } 27 | 28 | this.deleteFileConfirm = function(elem) { 29 | let filename = $(elem).closest('.file').attr('filename'); 30 | Main.Utils.displayConfirmModal('Delete File', `Delete file ${filename}?`, () => { 31 | xhr.delete('/api/drivers/delete', { filename }, errors => { 32 | if(errors.length) { 33 | errors.forEach(error => { 34 | Main.Utils.toast('error', `Error: ${error}`, 4000) 35 | }) 36 | } else { 37 | location.reload(); 38 | } 39 | }) 40 | }); 41 | } 42 | 43 | this.update = function(driverName) { 44 | xhr.post('/api/drivers/update', { driverName }, errors => { 45 | if(errors.length) { 46 | errors.forEach(error => { 47 | Main.Utils.toast('error', `Error: ${error}`, 4000) 48 | }) 49 | } else { 50 | location.reload(); 51 | } 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /golem/gui/static/js/environments.js: -------------------------------------------------------------------------------- 1 | let environmentsEditor = null; 2 | 3 | 4 | $(document).ready(function() { 5 | environmentsEditor = CodeMirror($("#environmentsContainer")[0], { 6 | value: environmentData, 7 | mode: "application/ld+json", 8 | lineNumbers: true, 9 | styleActiveLine: true, 10 | matchBrackets: true, 11 | autoCloseBrackets: true, 12 | lineWrapping: true 13 | }); 14 | 15 | if(Global.user.projectWeight < Main.PermissionWeightsEnum.admin){ 16 | environmentsEditor.setOption('readOnly', 'nocursor') 17 | } 18 | 19 | // set unsaved changes watcher 20 | watchForUnsavedChanges(); 21 | }); 22 | 23 | 24 | function saveEnvironments() { 25 | let environments = environmentsEditor.getValue(); 26 | xhr.put('/api/project/environments/save', { 27 | 'project': Global.project, 28 | 'environmentData': environments 29 | }, result => { 30 | if(result.error.length) { 31 | Main.Utils.toast('error', result.error, 2000); 32 | } else { 33 | Main.Utils.toast('success', "Environments saved", 2000); 34 | environmentsEditor.markClean(); 35 | } 36 | }) 37 | } 38 | 39 | 40 | function watchForUnsavedChanges(){ 41 | window.addEventListener("beforeunload", function (e) { 42 | if(!environmentsEditor.isClean()){ 43 | var confirmationMessage = 'There are unsaved changes'; 44 | (e || window.event).returnValue = confirmationMessage; //Gecko + IE 45 | return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc. 46 | } 47 | }); 48 | } -------------------------------------------------------------------------------- /golem/gui/static/js/external/code_mirror/addon/hint/python-hint.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | function forEach(arr, f) { 3 | for (var i = 0, e = arr.length; i < e; ++i) f(arr[i]); 4 | } 5 | 6 | function arrayContains(arr, item) { 7 | if (!Array.prototype.indexOf) { 8 | var i = arr.length; 9 | while (i--) { 10 | if (arr[i] === item) { 11 | return true; 12 | } 13 | } 14 | return false; 15 | } 16 | return arr.indexOf(item) != -1; 17 | } 18 | function scriptHint(editor, _keywords, getToken) { 19 | // Find the token at the cursor 20 | var cur = editor.getCursor(), token = getToken(editor, cur), tprop = token; 21 | // If it's not a 'word-style' token, ignore the token. 22 | 23 | if (!/^[\w$_]*$/.test(token.string)) { 24 | token = tprop = {start: cur.ch, end: cur.ch, string: "", state: token.state, 25 | className: token.string == ":" ? "python-type" : null}; 26 | } 27 | 28 | if (!context) var context = []; 29 | context.push(tprop); 30 | 31 | var completionList = getCompletions(token, context); 32 | completionList = completionList.sort(); 33 | //prevent autocomplete for last word, instead show dropdown with one word 34 | if(completionList.length == 1) { 35 | completionList.push(" "); 36 | } 37 | 38 | return {list: completionList, 39 | from: CodeMirror.Pos(cur.line, token.start), 40 | to: CodeMirror.Pos(cur.line, token.end)}; 41 | } 42 | 43 | CodeMirror.pythonHint = function(editor) { 44 | return scriptHint(editor, pythonKeywordsU, function (e, cur) {return e.getTokenAt(cur);}); 45 | }; 46 | var pythonKeywords = "and del from not while as elif global or with assert else if pass yield " 47 | + "break except import print class exec in raise continue finally is return def for lambda try"; 48 | var pythonKeywordsL = pythonKeywords.split(" "); 49 | var pythonKeywordsU = pythonKeywords.toUpperCase().split(" "); 50 | 51 | var pythonBuiltins = "abs divmod input open staticmethod all enumerate int ord str " 52 | + "any eval isinstance pow sum basestring execfile issubclass print super " 53 | + "bin file iter property tuple bool filter len range type " 54 | + "bytearray float list raw_input unichr callable format locals reduce unicode " 55 | + "chr frozenset long reload vars classmethod getattr map repr xrange " 56 | + "cmp globals max reversed zip compile hasattr memoryview round __import__ " 57 | + "complex hash min set apply delattr help next setattr buffer " 58 | + "dict hex object slice coerce dir id oct sorted intern"; 59 | var pythonBuiltinsL = pythonBuiltins.split(" ").join("() ").split(" "); 60 | var pythonBuiltinsU = pythonBuiltins.toUpperCase().split(" ").join("() ").split(" "); 61 | //console.log(actiongatherlist) 62 | 63 | function getCompletions(token, context) { 64 | var found = [], start = token.string; 65 | function maybeAdd(str) { 66 | if (str.indexOf(start) == 0 && !arrayContains(found, str)) found.push(str); 67 | } 68 | 69 | function gatherCompletions(_obj) { 70 | forEach(pythonBuiltinsL, maybeAdd); 71 | forEach(pythonBuiltinsU, maybeAdd); 72 | forEach(pythonKeywordsL, maybeAdd); 73 | forEach(pythonKeywordsU, maybeAdd); 74 | var pythonactionBuiltinsL = TestCode.golemActions.join("() ").split(" "); 75 | var pythonpageKeywordsL = TestCode.importedPages.join(" ").split(" "); 76 | var pythonDataKeywordsL = TestCode.test_data.join(" ").split(" "); 77 | forEach(pythonactionBuiltinsL, maybeAdd); 78 | forEach(pythonpageKeywordsL, maybeAdd); 79 | forEach(pythonDataKeywordsL, maybeAdd); 80 | } 81 | 82 | if (context) { 83 | // If this is a property, see if it belongs to some object we can 84 | // find in the current environment. 85 | var obj = context.pop(), base; 86 | 87 | if (obj.type == "variable") 88 | base = obj.string; 89 | else if(obj.type == "variable-3") 90 | base = ":" + obj.string; 91 | 92 | while (base != null && context.length) 93 | base = base[context.pop().string]; 94 | if (base != null) gatherCompletions(base); 95 | } 96 | return found; 97 | } 98 | })(); 99 | -------------------------------------------------------------------------------- /golem/gui/static/js/external/code_mirror/addon/hint/simple-hint.css: -------------------------------------------------------------------------------- 1 | .CodeMirror-completions { 2 | position: absolute; 3 | z-index: 10; 4 | overflow: hidden; 5 | -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2); 6 | -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2); 7 | box-shadow: 2px 3px 5px rgba(0,0,0,.2); 8 | } 9 | .CodeMirror-completions select { 10 | background: #fafafa; 11 | outline: none; 12 | border: none; 13 | padding: 0; 14 | margin: 0; 15 | font-family: monospace; 16 | } 17 | -------------------------------------------------------------------------------- /golem/gui/static/js/external/code_mirror/addon/hint/simple-hint.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | CodeMirror.simpleHint = function(editor, getHints, givenOptions) { 3 | // Determine effective options based on given values and defaults. 4 | var options = {}, defaults = CodeMirror.simpleHint.defaults; 5 | for (var opt in defaults) 6 | if (defaults.hasOwnProperty(opt)) 7 | options[opt] = (givenOptions && givenOptions.hasOwnProperty(opt) ? givenOptions : defaults)[opt]; 8 | 9 | function collectHints(previousToken) { 10 | // We want a single cursor position. 11 | if (editor.somethingSelected()) return; 12 | 13 | var tempToken = editor.getTokenAt(editor.getCursor()); 14 | 15 | // Don't show completions if token has changed and the option is set. 16 | if (options.closeOnTokenChange && previousToken != null && 17 | (tempToken.start != previousToken.start || tempToken.type != previousToken.type)) { 18 | return; 19 | } 20 | 21 | var result = getHints(editor, givenOptions); 22 | if (!result || !result.list.length) return; 23 | var completions = result.list; 24 | function insert(str) { 25 | editor.replaceRange(str, result.from, result.to); 26 | } 27 | // When there is only one completion, use it directly. 28 | if (options.completeSingle && completions.length == 1) { 29 | insert(completions[0]); 30 | return true; 31 | } 32 | 33 | // Build the select widget 34 | var complete = document.createElement("div"); 35 | complete.className = "CodeMirror-completions"; 36 | var sel = complete.appendChild(document.createElement("select")); 37 | // Opera doesn't move the selection when pressing up/down in a 38 | // multi-select, but it does properly support the size property on 39 | // single-selects, so no multi-select is necessary. 40 | if (!window.opera) sel.multiple = true; 41 | for (var i = 0; i < completions.length; ++i) { 42 | var opt = sel.appendChild(document.createElement("option")); 43 | opt.appendChild(document.createTextNode(completions[i])); 44 | } 45 | sel.firstChild.selected = true; 46 | sel.size = Math.min(10, completions.length); 47 | var pos = editor.cursorCoords(options.alignWithWord ? result.from : null); 48 | complete.style.left = pos.left + "px"; 49 | complete.style.top = pos.bottom + "px"; 50 | document.body.appendChild(complete); 51 | // If we're at the edge of the screen, then we want the menu to appear on the left of the cursor. 52 | var winW = window.innerWidth || Math.max(document.body.offsetWidth, document.documentElement.offsetWidth); 53 | if(winW - pos.left < sel.clientWidth) 54 | complete.style.left = (pos.left - sel.clientWidth) + "px"; 55 | // Hack to hide the scrollbar. 56 | if (completions.length <= 10) 57 | complete.style.width = (sel.clientWidth - 1) + "px"; 58 | 59 | var done = false; 60 | function close() { 61 | if (done) return; 62 | done = true; 63 | complete.parentNode.removeChild(complete); 64 | } 65 | function pick() { 66 | insert(completions[sel.selectedIndex]); 67 | close(); 68 | setTimeout(function(){editor.focus();}, 50); 69 | } 70 | CodeMirror.on(sel, "blur", close); 71 | CodeMirror.on(sel, "keydown", function(event) { 72 | var code = event.keyCode; 73 | // Enter 74 | if (code == 13) {CodeMirror.e_stop(event); pick();} 75 | // Escape 76 | else if (code == 27) {CodeMirror.e_stop(event); close(); editor.focus();} 77 | else if (code != 38 && code != 40 && code != 33 && code != 34 && !CodeMirror.isModifierKey(event)) { 78 | close(); editor.focus(); 79 | // Pass the event to the CodeMirror instance so that it can handle things like backspace properly. 80 | editor.triggerOnKeyDown(event); 81 | // Don't show completions if the code is backspace and the option is set. 82 | if (!options.closeOnBackspace || code != 8) { 83 | setTimeout(function(){collectHints(tempToken);}, 50); 84 | } 85 | } 86 | }); 87 | CodeMirror.on(sel, "dblclick", pick); 88 | 89 | sel.focus(); 90 | // Opera sometimes ignores focusing a freshly created node 91 | if (window.opera) setTimeout(function(){if (!done) sel.focus();}, 100); 92 | return true; 93 | } 94 | return collectHints(); 95 | }; 96 | CodeMirror.simpleHint.defaults = { 97 | closeOnBackspace: true, 98 | closeOnTokenChange: false, 99 | completeSingle: true, 100 | alignWithWord: true 101 | }; 102 | })(); 103 | -------------------------------------------------------------------------------- /golem/gui/static/js/external/treeview.js: -------------------------------------------------------------------------------- 1 | //http://bootsnipp.com/snippets/featured/bootstrap-30-treeview 2 | //http://jsfiddle.net/SeanWessell/roc0cqzc/ 3 | 4 | 5 | var openedClass = 'glyphicon-folder-open'; 6 | var closedClass = 'glyphicon-folder-close'; 7 | 8 | $.fn.extend({ 9 | treed: function (o) { 10 | 11 | //initialize each of the top levels 12 | var tree = $(this); 13 | tree.addClass("tree"); 14 | tree.find('li').has("ul").each(function () { 15 | var branch = $(this); //li with children ul 16 | branch.prepend(""); 17 | branch.addClass('branch'); 18 | branch.on('click', function (e) { 19 | if (this == e.target) { 20 | var icon = $(this).children('i:first'); 21 | icon.toggleClass(openedClass + " " + closedClass); 22 | $(this).children().children().toggle(); 23 | } 24 | }) 25 | branch.children().children().toggle(); 26 | }); 27 | //fire event from the dynamically added icon 28 | tree.find('.branch .indicator').each(function(){ 29 | $(this).on('click', function () { 30 | $(this).closest('li').click(); 31 | }); 32 | }); 33 | //fire event to open branch if the li contains an anchor instead of text 34 | tree.find('.branch>a').each(function () { 35 | $(this).on('click', function (e) { 36 | $(this).closest('li').click(); 37 | e.preventDefault(); 38 | }); 39 | }); 40 | //fire event to open branch if the li contains a button instead of text 41 | tree.find('.branch>button').each(function () { 42 | $(this).on('click', function (e) { 43 | $(this).closest('li').click(); 44 | e.preventDefault(); 45 | }); 46 | }); 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /golem/gui/static/js/file.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | class File { 4 | 5 | constructor(type, project, name, fullName, isCodeView) { 6 | this.type = type; 7 | this.project = project; 8 | this.name = name; 9 | this.fullName = fullName; 10 | this.isCodeView = isCodeView || false; 11 | } 12 | 13 | startInlineNameEdition() { 14 | Main.Utils.startGenericInlineName($("#filenameInput"), $("#fileName"), this.fullName, () => this.saveInlineNameEdition()) 15 | }; 16 | 17 | saveInlineNameEdition() { 18 | let newNameValue = $("#filenameInput input").val().trim(); 19 | if(newNameValue == this.fullName){ 20 | $("#filenameInput").hide(); 21 | $("#fileName").show(); 22 | return 23 | } 24 | let renameUrl = this.renameUrl(this.type); 25 | xhr.post(renameUrl, { 26 | project: this.project, 27 | fullFilename: this.fullName, 28 | newFullFilename: newNameValue, 29 | }, (result) => { 30 | if(result.errors.length == 0){ 31 | document.title = document.title.replace(this.fullName, newNameValue); 32 | this.fullName = newNameValue; 33 | $("#filenameInput input").val(''); 34 | $("#filenameInput").hide(); 35 | $("#fileName").html(newNameValue).show(); 36 | window.history.pushState("object or string", "", this.fileUrl()); 37 | Main.Utils.toast('success', 'File was renamed', 2000); 38 | } else { 39 | result.errors.forEach(function(error){ 40 | Main.Utils.toast('error', error, 3000); 41 | }); 42 | $("#filenameInput").hide(); 43 | $("#fileName").show(); 44 | } 45 | }) 46 | } 47 | 48 | renameUrl(fileType) { 49 | if(fileType == Main.FILE_TYPES.test) 50 | return '/api/test/rename' 51 | else if(fileType == Main.FILE_TYPES.suite) 52 | return '/api/suite/rename' 53 | else if(fileType == Main.FILE_TYPES.page) 54 | return '/api/page/rename' 55 | } 56 | 57 | fileUrl(fileType, project, fullName) { 58 | let url = ''; 59 | if(this.type == Main.FILE_TYPES.test) 60 | url = `/project/${this.project}/test/${this.fullName}/` 61 | else if(this.type == Main.FILE_TYPES.page) 62 | url = `/project/${this.project}/page/${this.fullName}/` 63 | else if(this.type == Main.FILE_TYPES.suite) 64 | url = `/project/${this.project}/suite/${this.fullName}/` 65 | if(this.isCodeView) 66 | url += 'code/' 67 | return url 68 | } 69 | } -------------------------------------------------------------------------------- /golem/gui/static/js/index.js: -------------------------------------------------------------------------------- 1 | 2 | $(document).ready(function() { 3 | $("#projectCreationButton").click(function(){ 4 | $("#projectCreationButton").hide(); 5 | $("#projectCreationForm").show(); 6 | $("#newProjectName").focus(); 7 | }); 8 | 9 | $("#createProjectCancel").click(function(){ 10 | $("#projectCreationForm").hide(); 11 | $("#projectCreationButton").show(); 12 | $("#newProjectName").val(''); 13 | }); 14 | }); 15 | 16 | 17 | function createProject() { 18 | let input = $("#newProjectName"); 19 | let projectName = input.val(); 20 | projectName = projectName.trim(); 21 | 22 | if(projectName.length < 3) { 23 | Main.Utils.displayErrorModal(['Project name is too short']); 24 | return 25 | } 26 | if(!/^[\w\s]+$/i.test(projectName)) { 27 | Main.Utils.displayErrorModal(['Only letters, numbers and underscores are allowed']); 28 | return 29 | } 30 | if(projectName.length > 50) { 31 | Main.Utils.displayErrorModal(['Maximum length is 50 characters']); 32 | return 33 | } 34 | xhr.post('/api/project', { 35 | 'project': projectName 36 | }, data => { 37 | if(data.errors.length == 0) { 38 | $("#projectList").append(`${data.project_name}`); 39 | $("#projectCreationForm").hide(); 40 | $("#projectCreationButton").show(); 41 | $("#newProjectName").val(''); 42 | } else { 43 | Main.Utils.displayErrorModal(data.errors); 44 | } 45 | }) 46 | } -------------------------------------------------------------------------------- /golem/gui/static/js/list/page_list.js: -------------------------------------------------------------------------------- 1 | 2 | $(document).ready(function() { 3 | PageList.getPages(Global.project) 4 | }); 5 | 6 | 7 | const PageList = new function(){ 8 | 9 | this.getPages = function(projectName) { 10 | xhr.get('/api/project/page-tree', {'project': projectName}, pages => { 11 | FileExplorer.initialize(pages, 'page', $('#fileExporerContainer')[0]); 12 | }) 13 | } 14 | } -------------------------------------------------------------------------------- /golem/gui/static/js/list/test_list.js: -------------------------------------------------------------------------------- 1 | 2 | $(document).ready(function() { 3 | TestList.getTests(Global.project) 4 | }); 5 | 6 | 7 | const TestList = new function() { 8 | 9 | this.getTests = function(projectName) { 10 | xhr.get('/api/project/test-tree', {'project': projectName}, tests => { 11 | FileExplorer.initialize(tests, 'test', $('#fileExporerContainer')[0]); 12 | TestList.getTestsTags(); 13 | }) 14 | } 15 | 16 | this.getTestsTags = function() { 17 | xhr.get('/api/project/test-tags', {'project': Global.project}, testsTags => { 18 | TestList.displayTags(testsTags) 19 | }) 20 | } 21 | 22 | this.displayTags = function(testsTags) { 23 | Object.keys(testsTags).forEach(test => { 24 | let file = FileExplorer.getFile(test); 25 | if(file) { 26 | FileExplorer.getFile(test).addTags(testsTags[test]) 27 | } 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /golem/gui/static/js/page_code.js: -------------------------------------------------------------------------------- 1 | 2 | let codeEditor; 3 | 4 | const PageCode = new function() { 5 | 6 | this.file; 7 | this.codeEditor; 8 | 9 | this.initialize = function(file, pageCode, codeError) { 10 | this.file = file; 11 | 12 | if(codeError !== null) { 13 | $(".error-container").show(); 14 | $(".error-container pre").html(codeError); 15 | } 16 | this.codeEditor = CodeMirror($("#codeEditorContainer")[0], { 17 | value: pageCode, 18 | mode: "python", 19 | lineNumbers: true, 20 | styleActiveLine: true, 21 | matchBrackets: true, 22 | indentUnit: 4, 23 | indentWithTabs: false, 24 | extraKeys: { 25 | Tab: this.convertTabToSpaces 26 | } 27 | }); 28 | codeEditor = this.codeEditor; 29 | 30 | if(Global.user.projectWeight < Main.PermissionWeightsEnum.standard) 31 | this.codeEditor.setOption('readOnly', 'nocursor') 32 | 33 | // set unsaved changes watcher 34 | this.watchForUnsavedChanges() 35 | } 36 | 37 | this.convertTabToSpaces = function(cm) { 38 | if (cm.somethingSelected()) { 39 | cm.indentSelection("add"); 40 | } else { 41 | cm.replaceSelection(cm.getOption("indentWithTabs")? "\t": 42 | Array(cm.getOption("indentUnit") + 1).join(" "), "end", "+input"); 43 | } 44 | } 45 | 46 | this.loadGuiView = function() { 47 | if(!this.codeEditor.isClean()) 48 | this.save() 49 | this.codeEditor.markClean(); 50 | // redirect to gui view 51 | let pathname = window.location.pathname; 52 | window.location.replace(pathname.replace('/code/', '/')); 53 | } 54 | 55 | this.save = function() { 56 | let content = this.codeEditor.getValue(); 57 | xhr.put('/api/page/code/save', { 58 | project: this.file.project, 59 | pageName: this.file.fullName, 60 | content: content 61 | }, result => { 62 | this.codeEditor.markClean(); 63 | Main.Utils.toast('success', `Page ${this.file.name} saved`, 3000); 64 | if(result.error != null) { 65 | $(".error-container").show(); 66 | $(".error-container pre").html(result.error); 67 | Main.Utils.toast('info', 'There are errors in the code', 3000) 68 | } else { 69 | $(".error-container").hide(); 70 | $(".error-container pre").html(''); 71 | } 72 | }) 73 | } 74 | 75 | this.watchForUnsavedChanges = function() { 76 | window.addEventListener("beforeunload", e => { 77 | if(this.hasUnsavedChanges()) { 78 | var confirmationMessage = 'There are unsaved changes'; 79 | (e || window.event).returnValue = confirmationMessage; //Gecko + IE 80 | return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc. 81 | } 82 | }) 83 | } 84 | 85 | this.hasUnsavedChanges = function(){ 86 | return !this.codeEditor.isClean() 87 | } 88 | } -------------------------------------------------------------------------------- /golem/gui/static/js/report_test.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | window.onload = function () { 4 | let resultDiv = $("[data='test-result']"); 5 | resultDiv.append(' ' + Main.Utils.getResultIcon(resultDiv.html())); 6 | } 7 | 8 | function setLogLevel(logLevel){ 9 | if(logLevel == 'debug'){ 10 | $("#debugLogLines").show(); 11 | $("#infoLogLines").hide(); 12 | } 13 | else if(logLevel == 'info'){ 14 | $("#infoLogLines").show(); 15 | $("#debugLogLines").hide(); 16 | } 17 | } -------------------------------------------------------------------------------- /golem/gui/static/js/settings.js: -------------------------------------------------------------------------------- 1 | let settingsEditor = null; 2 | 3 | 4 | $(document).ready(function() { 5 | settingsEditor = CodeMirror($("#settingsContainer")[0], { 6 | value: settingsJson, 7 | mode: "application/ld+json", 8 | lineNumbers: true, 9 | styleActiveLine: true, 10 | matchBrackets: true, 11 | autoCloseBrackets: true, 12 | lineWrapping: true 13 | }) 14 | 15 | if(Global.user.projectWeight < Main.PermissionWeightsEnum.admin){ 16 | settingsEditor.setOption('readOnly', 'nocursor') 17 | } 18 | 19 | // set unsaved changes watcher 20 | watchForUnsavedChanges(); 21 | }); 22 | 23 | 24 | function saveSettings() { 25 | let settings = settingsEditor.getValue(); 26 | xhr.put('/api/settings/project/save', { 27 | 'project': Global.project, 28 | settings 29 | }, data => { 30 | Main.Utils.toast('success', "Settings saved", 2000); 31 | settingsEditor.markClean(); 32 | }) 33 | } 34 | 35 | function saveGlobalSettings() { 36 | let settings = settingsEditor.getValue(); 37 | xhr.put('/api/settings/global/save', {settings}, data => { 38 | Main.Utils.toast('success', "Settings saved", 2000); 39 | settingsEditor.markClean(); 40 | }) 41 | } 42 | 43 | 44 | function watchForUnsavedChanges() { 45 | window.addEventListener("beforeunload", function (e) { 46 | let settingsIsClean = settingsEditor.isClean(); 47 | if(!settingsIsClean) { 48 | let confirmationMessage = 'There are unsaved changes'; 49 | (e || window.event).returnValue = confirmationMessage; 50 | return confirmationMessage 51 | } 52 | }); 53 | } -------------------------------------------------------------------------------- /golem/gui/static/js/suite_code.js: -------------------------------------------------------------------------------- 1 | let codeEditor; 2 | 3 | const SuiteCode = new function() { 4 | 5 | this.file; 6 | this.codeEditor; 7 | 8 | this.initialize = function(file, code, codeError) { 9 | this.file = file; 10 | if(codeError !== null) { 11 | $(".error-container").show(); 12 | $(".error-container pre").html(codeError); 13 | } 14 | this.codeEditor = CodeMirror($("#codeEditorContainer")[0], { 15 | value: code, 16 | mode: "python", 17 | lineNumbers: true, 18 | styleActiveLine: true, 19 | matchBrackets: true, 20 | indentUnit: 4, 21 | indentWithTabs: false, 22 | extraKeys: { 23 | Tab: this.convertTabToSpaces 24 | } 25 | }); 26 | codeEditor = this.codeEditor; 27 | if(Global.user.projectWeight < Main.PermissionWeightsEnum.standard) { 28 | codeEditor.setOption('readOnly', 'nocursor') 29 | } 30 | // set unsaved changes watcher 31 | this.watchForUnsavedChanges(); 32 | } 33 | 34 | this.convertTabToSpaces = function(cm) { 35 | if (cm.somethingSelected()) { 36 | cm.indentSelection("add"); 37 | } else { 38 | cm.replaceSelection(cm.getOption("indentWithTabs")? "\t": 39 | Array(cm.getOption("indentUnit") + 1).join(" "), "end", "+input"); 40 | } 41 | } 42 | 43 | this.loadGuiView = function() { 44 | if(!this.codeEditor.isClean()) 45 | this.save() 46 | this.codeEditor.markClean(); 47 | // redirect to gui view 48 | let pathname = window.location.pathname; 49 | window.location.replace(pathname.replace('/code/', '/')); 50 | } 51 | 52 | this.save = function() { 53 | xhr.put('/api/suite/code/save', { 54 | project: this.file.project, 55 | suiteName: this.file.fullName, 56 | content: codeEditor.getValue() 57 | }, result => { 58 | codeEditor.markClean(); 59 | Main.Utils.toast('success', `Suite ${this.file.fullName} saved`, 3000); 60 | if(result.error != null) { 61 | $(".error-container").show(); 62 | $(".error-container pre").html(result.error); 63 | Main.Utils.toast('info', "There are errors in the code", 3000) 64 | } else { 65 | $(".error-container").hide(); 66 | $(".error-container pre").html(''); 67 | } 68 | }) 69 | } 70 | 71 | // TODO, defined also in suite.js 72 | this.run = function() { 73 | let project = this.file.project; 74 | let fullName = this.file.fullName; 75 | 76 | function _runSuite() { 77 | xhr.post('/api/suite/run', { 78 | project: project, 79 | suite: fullName, 80 | }, timestamp => { 81 | let url = `/report/project/${project}/suite/${fullName}/${timestamp}/`; 82 | let msg = `Running suite ${fullName} - open`; 83 | Main.Utils.toast('info', msg, 15000) 84 | }) 85 | } 86 | 87 | if(this.unsavedChanges) 88 | this.save(_runSuite()) 89 | else 90 | _runSuite() 91 | } 92 | 93 | this.watchForUnsavedChanges = function() { 94 | window.addEventListener("beforeunload", e => { 95 | if(!this.codeEditor.isClean()){ 96 | let confirmationMessage = 'There are unsaved changes'; 97 | (e || window.event).returnValue = confirmationMessage; //Gecko + IE 98 | return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc. 99 | } 100 | }); 101 | } 102 | } -------------------------------------------------------------------------------- /golem/gui/static/js/users.js: -------------------------------------------------------------------------------- 1 | 2 | $(document).ready(function() { 3 | getUsers() 4 | }); 5 | 6 | 7 | function getUsers(){ 8 | 9 | xhr.get('/api/users', {}, users => { 10 | users.forEach(user => { 11 | let isSuperuser = ''; 12 | if(user.is_superuser) { 13 | isSuperuser = 'yes' 14 | } 15 | let email = ''; 16 | if(user.email) { 17 | email = user.email 18 | } 19 | let projects = $('
    '); 20 | Object.keys(user.projects).forEach(project => { 21 | let permission = user.projects[project]; 22 | if(project == '*') { 23 | project = 'all projects' 24 | } 25 | projects.append(`${project} - ${permission}`) 26 | }); 27 | let tr = ` 28 | ${user.username} 29 | ${email} 30 | ${isSuperuser} 31 | ${projects.html()} 32 | 33 | Edit 34 | 35 | 36 | ` 37 | $("#userTable>tbody").append(tr) 38 | }); 39 | $("#usersTableLoadingIconContainer").hide() 40 | }) 41 | } 42 | 43 | 44 | function deleteUser(username){ 45 | if(username == Global.user.username){ 46 | Main.Utils.toast('error', 'Cannot delete current user', 3000) 47 | return 48 | } 49 | let callback = function(){ 50 | deleteUserConfirmed(username); 51 | } 52 | Main.Utils.displayConfirmModal('Delete', `Are you sure you want to delete user ${username}?`, callback); 53 | } 54 | 55 | function deleteUserConfirmed(username) { 56 | xhr.delete('/api/users/delete', { username }, result => { 57 | if(result.errors.length) { 58 | result.errors.forEach(error => Main.Utils.toast('error', error, 3000)) 59 | } else { 60 | Main.Utils.toast('success', 'User deleted', 2000); 61 | $("#userTable>tbody").html(''); 62 | getUsers() 63 | } 64 | }) 65 | } -------------------------------------------------------------------------------- /golem/gui/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}Golem{% endblock %} 4 | 5 | {% block content %} 6 |
    7 |
    8 |
    9 |

    {{message}}

    10 |
    11 | 12 |
    13 |
    14 | {% endblock %} -------------------------------------------------------------------------------- /golem/gui/templates/common_element_error.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}Golem : {{project|capitalize}} : {{item_name}}{% endblock %} 4 | 5 | {% block content %} 6 |
    7 |

    {{item_name}}

    8 |
    9 |
    {{content|safe}}
    10 | {% endblock %} -------------------------------------------------------------------------------- /golem/gui/templates/drivers.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}Golem : Drivers{% endblock %} 4 | 5 | {% block head %} 6 | 7 | {% endblock %} 8 | 9 | {% block content %} 10 |
    11 |
    12 |

    Drivers

    13 |
    14 |
    15 | 16 | 23 |
    24 | 25 |
    26 |
    27 | 28 |
    29 | 30 |
      31 | 32 |
      33 |
      34 | 35 | 36 | 37 | 38 | 39 | {% endblock %} -------------------------------------------------------------------------------- /golem/gui/templates/environments.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}Golem : {{project|capitalize}} : Environments{% endblock %} 4 | 5 | {% block head %} 6 | 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
      12 |
      13 |

      {% if project %}{{project|capitalize|replace("_", " ")}} - {% endif %} environments

      14 |
      15 |
      16 | {% if g.user.project_weight(project) >= 40 %} 17 | 18 | {% endif %} 19 |
      20 |
      21 |
      22 |
      23 |
      24 |
      25 |
      26 |
      Must be valid JSON
      27 | Example 28 |
      29 | {
      30 |     "test": {
      31 |         "url": "http://test-url:5000/"
      32 |     },
      33 |     "staging": {
      34 |         "url": "http://staging-url:5000/"
      35 |     }
      36 | }
      37 |
      38 |
      39 |
      40 | 41 | 44 | 45 | 46 | 47 | {% endblock %} -------------------------------------------------------------------------------- /golem/gui/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}Golem{% endblock %} 4 | 5 | {% block head %} 6 | 11 | {% endblock %} 12 | 13 | {% block content %} 14 |
      15 |
      16 |

      Select a Project

      17 |
      18 |
      19 | {% for project in projects %} 20 | {{ project }} 21 | {% endfor %} 22 |
      23 | {% if current_user.is_superuser %} 24 |
      25 | 26 |
      27 | 36 | {% endif %} 37 |
      38 |
      39 | 40 | {% endblock %} -------------------------------------------------------------------------------- /golem/gui/templates/list/page_list.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}Golem : {{project|capitalize}} : Pages{% endblock %} 4 | 5 | {% block head %} 6 | 7 | {% endblock %} 8 | 9 | {% block content %} 10 |
      11 |
      12 |

      {{project|capitalize|replace("_", " ")}} - Pages

      13 |
      14 |
      15 | 16 |
        17 |
        18 |
        19 |
        20 |
        21 | 22 | 23 | {% endblock %} -------------------------------------------------------------------------------- /golem/gui/templates/list/suite_list.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}Golem : {{project|capitalize}} : Suites{% endblock %} 4 | 5 | {% block head %} 6 | 7 | 20 | {% endblock %} 21 | 22 | {% block content %} 23 |
        24 |
        25 |

        {{project|capitalize|replace("_", " ")}} - Suites

        26 |
        27 |
        28 |
        29 |
        30 | 31 |
          32 |
          33 |
          34 |
          35 |
          36 | 37 |
          38 |
          39 |
          40 |
          41 |
          42 |
          43 | 44 | 45 | 46 | 47 | {% endblock %} -------------------------------------------------------------------------------- /golem/gui/templates/list/test_list.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}Golem : {{project|capitalize}} : Tests{% endblock %} 4 | 5 | {% block head %} 6 | 7 | {% endblock %} 8 | 9 | {% block content %} 10 |
          11 |
          12 |

          {{project|capitalize|replace("_", " ")}} - Tests

          13 |
          14 |
          15 | 16 |
            17 |
            18 |
            19 |
            20 |
            21 | 22 | 23 | 24 | {% endblock %} -------------------------------------------------------------------------------- /golem/gui/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}Golem : Login{% endblock %} 4 | 5 | {% block content %} 6 |
            7 |
            8 |

            Login

            9 |
            10 | {% if errors %} 11 |
              12 | {% for error in errors %} 13 |
            • {{error}}
            • 14 | {% endfor %} 15 |
            16 | {% endif %} 17 | 18 |
            19 |
            20 | 21 |
            22 |
            23 | 24 |
            25 | 26 |
            27 | 28 |
            29 |
            30 |
            31 |
            32 | {% endblock %} -------------------------------------------------------------------------------- /golem/gui/templates/not_permission.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}Golem{% endblock %} 4 | 5 | {% block content %} 6 |
            7 |
            8 |
            9 |

            You do not have permissions to view this page.

            10 |
            11 | Go back 12 |
            13 |
            14 | {% endblock %} -------------------------------------------------------------------------------- /golem/gui/templates/page_builder/page_code.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}Golem : {{project|capitalize}} : {{page_name}}{% endblock %} 4 | 5 | {% block head %} 6 | 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
            12 |
            13 |

            14 | {{page_name}} 15 | 18 |

            19 |
            20 |
            21 | 22 | {% if g.user.project_weight(project) >= 30 %} 23 | 24 | {% endif %} 25 |
            26 |
            27 |
            28 | 31 |
            32 |
            33 |
            34 | 35 | 36 | 37 | 38 | 39 | {% endblock %} 40 | 41 | {% block footer_declarations %} 42 | 51 | {% endblock %} -------------------------------------------------------------------------------- /golem/gui/templates/report/report_dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}{% if project %}Golem : {{project|capitalize}} : Reports{% else %}Golem : Reports{% endif %}{% endblock %} 4 | 5 | {% block head %} 6 | 7 | 24 | {% endblock %} 25 | 26 | {% block content %} 27 |
            28 |

            Reports

            29 |
            30 | 31 |
            32 | 39 |
            40 |
            41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
            ExecutionBrowsersEnvironmentsStartedDurationTestsResult
            57 | 58 |
            59 | {% endblock %} 60 | 61 | {% block footer_declarations %} 62 | 66 | 67 | 68 | {% endblock %} -------------------------------------------------------------------------------- /golem/gui/templates/report/report_dashboard_old.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}{% if project %}Golem : {{project|capitalize}} : Reports{% else %}Golem : Reports{% endif %}{% endblock %} 4 | 5 | {% block head %} 6 | 7 | {% endblock %} 8 | 9 | {% block content %} 10 |
            11 |

            {% if project %}{{project|replace("_", " ")|capitalize}} - {% endif %}{% if execution %}{{execution|replace("_", " ")|capitalize}} - {% endif %}Reports

            12 |
            13 | {% endblock %} 14 | 15 | {% block footer_declarations %} 16 | 20 | 21 | 22 | {% endblock %} -------------------------------------------------------------------------------- /golem/gui/templates/settings/global_settings.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}Golem : Settings{% endblock %} 4 | 5 | {% block head %} 6 | 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
            12 |
            13 |

            Global Settings

            14 |
            15 |
            16 | 17 |
            18 |
            19 |
            20 |
            21 |
            22 | 23 | 24 | 25 | 28 | {% endblock %} -------------------------------------------------------------------------------- /golem/gui/templates/settings/project_settings.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}Golem : Settings{% endblock %} 4 | 5 | {% block head %} 6 | 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
            12 |
            13 |

            {{project|capitalize|replace("_", " ")}} - Settings

            14 |
            15 |
            16 | {% if g.user.project_weight(project) >= 40 %} 17 | 18 | {% endif %} 19 |
            20 |
            21 |
            22 |
            Project settings override global settings.
            23 |
            24 |
            25 | 26 | 27 | 28 | 31 | {% endblock %} -------------------------------------------------------------------------------- /golem/gui/templates/suite_code.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}Golem : {{project|capitalize}} : {{suite_name}}{% endblock %} 4 | 5 | {% block head %} 6 | 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
            12 |
            13 |

            14 | 15 | {{suite_name}} 16 | 17 | 20 |

            21 |
            22 |
            23 | {% if g.user.project_weight(project) >= 30 %} 24 | 25 | {% endif %} 26 | 27 | {% if g.user.project_weight(project) >= 30 %} 28 | 29 | {% endif %} 30 |
            31 |
            32 |
            33 | 36 |
            37 |
            38 |
            39 | 40 | 41 | 42 | 43 | 44 | {% endblock %} 45 | 46 | {% block footer_declarations %} 47 | 56 | {% endblock %} -------------------------------------------------------------------------------- /golem/gui/templates/users/reset_password.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /golem/gui/templates/users/user_form.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}Golem : User{% endblock %} 4 | 5 | {% block head %} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |
            10 |
            11 |

            New User

            12 |
            13 | {% if edition_mode %} 14 | 15 | {% else %} 16 | 17 | {% endif %} 18 |
            19 |
            20 |
            21 |
            22 |
            23 | 24 | 25 |
            26 |
            27 | 28 | 29 |
            30 | {% if not edit_user %} 31 |
            32 | 33 | 34 |
            35 | {% endif %} 36 |
            37 | 38 |
            39 | {% if edition_mode %} 40 |
            41 | 42 |
            43 |
            44 | {% endif %} 45 |
            46 | 47 |
            48 |
            49 | 50 |
            51 |
            52 | 53 |
            54 | 55 |
            56 |
            57 | 58 | 59 |
            60 |
            61 |
            62 |
            63 | 64 | {% include "users/reset_password.html" %} 65 | 66 | 67 | 68 | 78 | {% endblock %} -------------------------------------------------------------------------------- /golem/gui/templates/users/user_profile.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}Golem : User Profile{% endblock %} 4 | 5 | {% block head %} 6 | 20 | {% endblock %} 21 | 22 | {% block content %} 23 |
            24 |
            25 |

            {{g.user.username}} - profile

            26 |
            27 |
            28 |
            29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 42 | 43 | 44 |
            Superuser{% if g.user.is_superuser %}Yes{% else %}No{% endif %}
            Projects 38 | {% for project in g.user.projects %} 39 | {{project}} - {{g.user.projects[project]}} 40 | {% endfor %} 41 |
            45 |
            46 |
            47 |
            48 | 49 |
            50 |
            51 |
            52 | 53 | {% include "users/reset_password.html" %} 54 | 55 | 89 | 90 | {% endblock %} -------------------------------------------------------------------------------- /golem/gui/templates/users/users.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %}Golem : Users{% endblock %} 4 | 5 | {% block head %} 6 | 14 | {% endblock %} 15 | 16 | {% block content %} 17 |
            18 |
            19 |

            Users

            20 |
            21 |
            22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
            UsernameEmailSuperuserProjectsActions
            34 |
            35 | 36 |
            37 | Create User 38 |
            39 |
            40 | 41 | 42 | {% endblock %} -------------------------------------------------------------------------------- /golem/helpers.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | 4 | 5 | def random_float(min=1.0, max=100.0, decimals=None): 6 | """Generate a random float between min and max. 7 | 8 | `decimals` is the maximum amount of decimal places 9 | the generated float should have. 10 | """ 11 | randomfloat = random.uniform(min, max) 12 | if decimals is not None: 13 | randomfloat = round(randomfloat, decimals) 14 | return randomfloat 15 | 16 | 17 | def random_int(min=1, max=100): 18 | """Generate a random integer between min and max""" 19 | return random.randint(min, max) 20 | 21 | 22 | def random_str(length=10, sample=None, prefix='', suffix=''): 23 | """Generate a random string 24 | 25 | Sample should be a string or a list of strings/characters to 26 | choose from. The default sample is lowercase ascii letters. 27 | A few presets can be used: 28 | - 'LOWERCASE': lower case ascii letters 29 | - 'UPPERCASE': uppercase ascii letters 30 | - 'DIGITS': digit characters 31 | - 'SPECIAL': Special characters 32 | Example: 33 | random_str(sample=['LOWERCASE', '!@#$%']) 34 | 35 | prefix: A string to be prepended to the generated string 36 | 37 | suffix: A string to be appended to the generated string 38 | """ 39 | sample_match = { 40 | 'LOWERCASE': string.ascii_lowercase, 41 | 'UPPERCASE': string.ascii_uppercase, 42 | 'DIGITS': string.digits, 43 | 'SPECIAL': string.punctuation 44 | } 45 | 46 | sample_ = '' 47 | if sample is None: 48 | sample_ = string.ascii_lowercase 49 | else: 50 | if isinstance(sample, list): 51 | for s in sample: 52 | sample_ += sample_match.get(s, str(s)) 53 | elif isinstance(sample, str): 54 | sample_ += sample_match.get(sample, str(sample)) 55 | 56 | random_string = ''.join(random.choice(sample_) for _ in range(length)) 57 | random_string = prefix + random_string + suffix 58 | return random_string 59 | -------------------------------------------------------------------------------- /golem/main.py: -------------------------------------------------------------------------------- 1 | """Main point of entrance to the application""" 2 | import sys 3 | 4 | from .cli import argument_parser, commands 5 | 6 | 7 | def execute_from_command_line(testdir): 8 | # deactivate .pyc extention file generation 9 | sys.dont_write_bytecode = True 10 | 11 | args = argument_parser.get_parser().parse_args() 12 | 13 | commands.command_dispatcher(args, testdir) 14 | -------------------------------------------------------------------------------- /golem/report/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/golem/report/__init__.py -------------------------------------------------------------------------------- /golem/report/cli_report.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | import colorama 4 | 5 | from golem.test_runner.conf import ResultsEnum 6 | 7 | 8 | def _get_symbol_and_color(result): 9 | if result == ResultsEnum.SUCCESS: 10 | symbol = '.' # '✓' 11 | color = colorama.Fore.GREEN 12 | elif result in [ResultsEnum.CODE_ERROR, ResultsEnum.ERROR]: 13 | symbol = 'E' 14 | color = colorama.Fore.RED 15 | elif result == ResultsEnum.FAILURE: 16 | symbol = 'F' 17 | color = colorama.Fore.RED 18 | else: 19 | symbol = '?' 20 | color = colorama.Fore.YELLOW 21 | return symbol, color 22 | 23 | 24 | def _print_test_file_section(test_file_tests): 25 | print('------------------------------------------------------------------') 26 | first_test = test_file_tests[0] 27 | # if the test file only has one test, the test name is 'test' and the result is 28 | # success then the test file section is simplified to a single line 29 | if len(test_file_tests) == 1 and first_test['test'] == 'test' and \ 30 | first_test['result'] == ResultsEnum.SUCCESS: 31 | symbol, color = _get_symbol_and_color(first_test['result']) 32 | print(first_test['test_file'], first_test['set_name'], 33 | color + symbol + colorama.Fore.RESET) 34 | else: 35 | print(first_test['test_file'], first_test['set_name']) 36 | not_success = [] 37 | for test in test_file_tests: 38 | if test['result'] != ResultsEnum.SUCCESS: 39 | not_success.append(test) 40 | symbol, color = _get_symbol_and_color(test['result']) 41 | print(' ' + color + symbol + colorama.Fore.RESET + ' ' + test['test']) 42 | # list each test in this test file that was not a success 43 | for i, test in enumerate(not_success): 44 | print() 45 | print(f'{i + 1}) {test["test"]}') 46 | for error in test['errors']: 47 | print(error['message']) 48 | if error['description']: 49 | print(error['description']) 50 | # if test does not have errors show custom message 51 | if not len(test['errors']): 52 | print(test['result']) 53 | 54 | 55 | def report_to_cli(report): 56 | # group each test by test_file.set_name 57 | unique_test_files = {} 58 | for test in report['tests']: 59 | test_file_id = f"{test['test_file']}.{test['set_name']}" 60 | if test_file_id not in unique_test_files: 61 | unique_test_files[test_file_id] = [] 62 | unique_test_files[test_file_id].append(test) 63 | print() 64 | print('Result:') 65 | for test_file in unique_test_files: 66 | _print_test_file_section(unique_test_files[test_file]) 67 | 68 | 69 | def print_totals(report): 70 | if report['total_tests'] > 0: 71 | result_string = '' 72 | for result, number in OrderedDict(report['totals_by_result']).items(): 73 | result_string += f' {number} {result},' 74 | elapsed_time = report['net_elapsed_time'] 75 | if elapsed_time > 60: 76 | in_elapsed_time = f'in {round(elapsed_time / 60, 2)} minutes' 77 | else: 78 | in_elapsed_time = f'in {elapsed_time} seconds' 79 | output = f"Total: {report['total_tests']} tests,{result_string[:-1]} {in_elapsed_time}" 80 | print() 81 | print(output) 82 | -------------------------------------------------------------------------------- /golem/report/report.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | from datetime import datetime, timedelta 4 | 5 | from golem.core import test_directory 6 | from golem.core.project import Project 7 | from golem.report.execution_report import execution_report_path 8 | from golem.core import utils 9 | 10 | 11 | def get_last_execution_timestamps(projects=None, execution=None, limit=None, last_days=None): 12 | """Get the last n execution timestamps from all the executions of 13 | a list of projects. 14 | 15 | If projects is not provided, all projects will be selected. 16 | If execution is provided, projects should be a list of one. 17 | 18 | Timestamps are in descending order. 19 | """ 20 | start_timestamp = None 21 | if last_days is not None and last_days != 0: 22 | start_datetime = datetime.today() - timedelta(days=last_days) 23 | start_timestamp = utils.get_timestamp(start_datetime) 24 | 25 | last_timestamps = {} 26 | # if no projects provided, select every project 27 | if not projects: 28 | projects = test_directory.get_projects() 29 | for project in projects: 30 | last_timestamps[project] = {} 31 | report_path = Project(project).report_directory_path 32 | # if execution is not provided, select all executions 33 | if execution and os.path.isdir(os.path.join(report_path, execution)): 34 | executions = [execution] 35 | else: 36 | executions = next(os.walk(report_path))[1] 37 | for e in executions: 38 | exec_path = os.path.join(report_path, e) 39 | timestamps = next(os.walk(exec_path))[1] 40 | timestamps = sorted(timestamps, reverse=True) 41 | if limit is not None: 42 | limit = int(limit) 43 | timestamps = timestamps[:limit] 44 | if start_timestamp is not None: 45 | timestamps = [t for t in timestamps if t >= start_timestamp] 46 | if len(timestamps): 47 | last_timestamps[project][e] = timestamps 48 | else: 49 | last_timestamps[project][e] = [] 50 | 51 | return last_timestamps 52 | 53 | 54 | def delete_execution(project, execution): 55 | errors = [] 56 | path = os.path.join(Project(project).report_directory_path, execution) 57 | if os.path.isdir(path): 58 | try: 59 | shutil.rmtree(path) 60 | except Exception as e: 61 | errors.append(repr(e)) 62 | else: 63 | errors.append(f'Execution {execution} of project {project} does not exist') 64 | return errors 65 | 66 | 67 | def delete_execution_timestamp(project, execution, timestamp): 68 | errors = [] 69 | path = execution_report_path(project, execution, timestamp) 70 | if os.path.isdir(path): 71 | try: 72 | shutil.rmtree(path) 73 | except Exception as e: 74 | errors.append(repr(e)) 75 | else: 76 | errors.append(f'Execution for {project} {execution} {timestamp} does not exist') 77 | return errors 78 | -------------------------------------------------------------------------------- /golem/report/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | from io import BytesIO 3 | 4 | from golem import execution 5 | from golem.browser import get_browser 6 | 7 | 8 | def save_screenshot(reportdir, image_name, format='PNG', quality=None, width=None, 9 | height=None, resize=None): 10 | """Modify screenshot format, size and quality before saving. 11 | Pillow must be installed. 12 | 13 | - format must be 'PNG' or 'JPEG' 14 | - quality must be an int in 1..95 range. 15 | Default is 75. Only applies to JPEG. 16 | - width and height must be int greater than 0 17 | - resize must be an int greater than 0. 18 | Str in the format '55' or '55%' is also allowed. 19 | """ 20 | try: 21 | from PIL import Image 22 | except ModuleNotFoundError: 23 | execution.logger.warning('Pillow must be installed in order to modify' 24 | ' screenshot format, size or quality') 25 | return 26 | 27 | extension = 'png' 28 | resample_filter = Image.BOX # for PNG 29 | 30 | # validate format 31 | if format not in ['JPEG', 'PNG']: 32 | raise ValueError("settings screenshots format should be 'jpg' or 'png'") 33 | # validate quality 34 | if quality is not None: 35 | try: 36 | quality = int(quality) 37 | except ValueError: 38 | raise ValueError('settings screenshots quality should be int') 39 | if format == 'JPEG' and not 1 <= quality <= 95: 40 | raise ValueError('settings screenshots quality should be in 1..95 range for jpg files') 41 | # validate width 42 | if width is not None: 43 | try: 44 | width = int(width) 45 | except ValueError: 46 | raise ValueError('settings screenshots width should be int') 47 | if width < 0: 48 | raise ValueError('settings screenshots width should be greater than 0') 49 | # validate height 50 | if height is not None: 51 | try: 52 | height = int(height) 53 | except ValueError: 54 | raise ValueError('settings screenshots height should be int') 55 | if height < 0: 56 | raise ValueError('settings screenshots height should be greater than 0') 57 | # validate resize 58 | if resize is not None: 59 | if resize is str: 60 | resize = resize.replace('%', '') 61 | try: 62 | resize = int(resize) 63 | except ValueError: 64 | raise ValueError('settings screenshots resize should be int') 65 | if resize < 0: 66 | raise ValueError('settings screenshots resize should be greater than 0') 67 | 68 | base_png = get_browser().get_screenshot_as_png() 69 | pil_image = Image.open(BytesIO(base_png)) 70 | 71 | if format == 'JPEG': 72 | pil_image = pil_image.convert('RGB') 73 | extension = 'jpg' 74 | resample_filter = Image.BICUBIC 75 | 76 | if any([width, height, resize]): 77 | img_width, img_height = pil_image.size 78 | if width and height: 79 | new_width = width 80 | new_height = height 81 | elif width: 82 | new_width = width 83 | # maintain aspect ratio 84 | new_height = round(new_width * img_height / img_width) 85 | elif height: 86 | new_height = height 87 | # maintain aspect ratio 88 | new_width = round(new_height * img_width / img_height) 89 | else: # resize by % 90 | new_width = round(pil_image.size[0] * resize / 100) 91 | new_height = round(pil_image.size[1] * resize / 100) 92 | pil_image = pil_image.resize((new_width, new_height), resample=resample_filter) 93 | 94 | screenshot_filename = f'{image_name}.{extension}' 95 | screenshot_path = os.path.join(reportdir, screenshot_filename) 96 | if format == 'PNG': 97 | pil_image.save(screenshot_path, format=format, optimize=True) 98 | elif format == 'JPEG': 99 | if quality is None: 100 | pil_image.save(screenshot_path, format=format, optimize=True) 101 | else: 102 | pil_image.save(screenshot_path, format=format, optimize=True, 103 | quality=quality) 104 | 105 | return screenshot_filename 106 | -------------------------------------------------------------------------------- /golem/test_runner/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/golem/test_runner/__init__.py -------------------------------------------------------------------------------- /golem/test_runner/conf.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class ResultsEnum: 4 | PENDING = 'pending' 5 | RUNNING = 'running' 6 | FAILURE = 'failure' 7 | ERROR = 'error' 8 | CODE_ERROR = 'code error' 9 | SUCCESS = 'success' 10 | STOPPED = 'stopped' 11 | NOT_RUN = 'not run' 12 | SKIPPED = 'skipped' 13 | -------------------------------------------------------------------------------- /golem/test_runner/test_runner_utils.py: -------------------------------------------------------------------------------- 1 | """Utils for the test_runner module.""" 2 | import os 3 | import types 4 | 5 | from golem.core import utils 6 | 7 | 8 | def import_page_into_test(base_path, parent_module, page_path_list): 9 | """Import a page module into a (test) module provided 10 | the relative dot path to the page. 11 | """ 12 | if len(page_path_list) > 1: 13 | new_node_name = page_path_list.pop(0) 14 | if not hasattr(parent_module, new_node_name): 15 | new_module = types.ModuleType(new_node_name) 16 | setattr(parent_module, new_node_name, new_module) 17 | else: 18 | new_module = getattr(parent_module, new_node_name) 19 | base_path = os.path.join(base_path, new_node_name) 20 | new_module = import_page_into_test(base_path, new_module, page_path_list) 21 | setattr(parent_module, new_node_name, new_module) 22 | else: 23 | path = os.path.join(base_path, page_path_list[0] + '.py') 24 | imported_module, error = utils.import_module(path) 25 | if error: 26 | raise ImportError(error) 27 | setattr(parent_module, page_path_list[0], imported_module) 28 | return parent_module 29 | 30 | -------------------------------------------------------------------------------- /golem/webdriver/__init__.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver import Chrome as SeleniumChromeDriver 2 | from selenium.webdriver import Edge as SeleniumEdgeDriver 3 | from selenium.webdriver import Firefox as SeleniumGeckoDriver 4 | from selenium.webdriver import Ie as SeleniumIeDriver 5 | from selenium.webdriver import Opera as SeleniumOperaDriver 6 | from selenium.webdriver import Remote as SeleniumRemoteDriver 7 | 8 | from golem.webdriver.extended_driver import GolemExtendedDriver 9 | 10 | 11 | class GolemChromeDriver(SeleniumChromeDriver, GolemExtendedDriver): 12 | pass 13 | 14 | 15 | class GolemEdgeDriver(SeleniumEdgeDriver, GolemExtendedDriver): 16 | pass 17 | 18 | 19 | class GolemGeckoDriver(SeleniumGeckoDriver, GolemExtendedDriver): 20 | pass 21 | 22 | 23 | class GolemIeDriver(SeleniumIeDriver, GolemExtendedDriver): 24 | pass 25 | 26 | 27 | class GolemOperaDriver(SeleniumOperaDriver, GolemExtendedDriver): 28 | pass 29 | 30 | 31 | class GolemRemoteDriver(SeleniumRemoteDriver, GolemExtendedDriver): 32 | pass 33 | -------------------------------------------------------------------------------- /golem/webdriver/golem_expected_conditions.py: -------------------------------------------------------------------------------- 1 | 2 | class element_to_be_enabled(object): 3 | """An Expectation for checking an element is enabled""" 4 | def __init__(self, element): 5 | self.element = element 6 | 7 | def __call__(self, driver): 8 | return self.element.is_enabled() 9 | 10 | 11 | class text_to_be_present_in_page(object): 12 | """An Expectation for checking page contains text""" 13 | def __init__(self, text): 14 | self.text = text 15 | 16 | def __call__(self, driver): 17 | return self.text in driver.page_source 18 | 19 | 20 | class element_text_to_be(object): 21 | """An expectation for checking the given text matches element text""" 22 | def __init__(self, element, text): 23 | self.element = element 24 | self.text = text 25 | 26 | def __call__(self, driver): 27 | return self.element.text == self.text 28 | 29 | 30 | class element_text_to_contain(object): 31 | """An expectation for checking element contains the given text""" 32 | def __init__(self, element, text): 33 | self.element = element 34 | self.text = text 35 | 36 | def __call__(self, driver): 37 | return self.text in self.element.text 38 | 39 | 40 | class element_to_have_attribute(object): 41 | """An expectation for checking element has attribute""" 42 | def __init__(self, element, attribute): 43 | self.element = element 44 | self.attribute = attribute 45 | 46 | def __call__(self, driver): 47 | return self.element.get_attribute(self.attribute) is not None 48 | 49 | 50 | class window_present_by_partial_title(object): 51 | """An expectation for checking a window is present by partial title""" 52 | def __init__(self, partial_title): 53 | self.partial_title = partial_title 54 | 55 | def __call__(self, driver): 56 | return any(self.partial_title in t for t in driver.get_window_titles()) 57 | 58 | 59 | class window_present_by_partial_url(object): 60 | """An expectation for checking a window is present by partial url""" 61 | def __init__(self, partial_url): 62 | self.partial_url = partial_url 63 | 64 | def __call__(self, driver): 65 | return any(self.partial_url in u for u in driver.get_window_urls()) 66 | 67 | 68 | class window_present_by_title(object): 69 | """An expectation for checking a window is present by title""" 70 | def __init__(self, title): 71 | self.title = title 72 | 73 | def __call__(self, driver): 74 | return self.title in driver.get_window_titles() 75 | 76 | 77 | class window_present_by_url(object): 78 | """An expectation for checking a window is present by url""" 79 | def __init__(self, url): 80 | self.url = url 81 | 82 | def __call__(self, driver): 83 | return self.url in driver.get_window_urls() -------------------------------------------------------------------------------- /images/example-test-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/images/example-test-code.png -------------------------------------------------------------------------------- /images/execution-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/images/execution-report.png -------------------------------------------------------------------------------- /images/report-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/images/report-dashboard.png -------------------------------------------------------------------------------- /images/test-case.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/images/test-case.png -------------------------------------------------------------------------------- /images/test-execution-detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golemhq/golem/84f51478b169cdeab73fc7e2a22a64d0a2a29263/images/test-execution-detail.png -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | slow: marks tests as slow (deselect with '-m "not slow"') -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | Flask-Login 3 | selenium 4 | pytest 5 | requests 6 | colorama 7 | py-webdriver-manager -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | flask 2 | Flask-Login 3 | selenium 4 | pytest 5 | requests 6 | py-webdriver-manager 7 | sphinx==2.4.4 8 | recommonmark 9 | tox -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | license_file = LICENSE -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from setuptools.command.test import test as TestCommand 3 | import os 4 | import sys 5 | 6 | import golem 7 | 8 | 9 | here = os.path.abspath(os.path.dirname(__file__)) 10 | 11 | 12 | class PyTest(TestCommand): 13 | def finalize_options(self): 14 | TestCommand.finalize_options(self) 15 | self.test_args = ['tests'] 16 | self.test_suite = True 17 | 18 | def run_tests(self): 19 | import pytest 20 | errcode = pytest.main(self.test_args) 21 | sys.exit(errcode) 22 | 23 | 24 | setup( 25 | name='golem-framework', 26 | version=golem.__version__, 27 | description='Test automation framework for functional tests using Selenium', 28 | # long_description=long_description, 29 | url='https://github.com/golemhq/golem', 30 | author='Luciano Renzi', 31 | author_email='luciano@lucianorenzi.com', 32 | license='MIT', 33 | classifiers=[ 34 | 'Development Status :: 4 - Beta', 35 | 'Programming Language :: Python :: 3.5', 36 | 'Programming Language :: Python :: 3.6', 37 | 'Programming Language :: Python :: 3.8', 38 | 'Programming Language :: Python :: 3.9', 39 | 'Natural Language :: English', 40 | 'Intended Audience :: Developers', 41 | 'License :: OSI Approved :: MIT License', 42 | 'Operating System :: OS Independent', 43 | 'Topic :: Software Development :: Quality Assurance', 44 | 'Topic :: Software Development :: Testing', 45 | ], 46 | keywords='test automation framework selenium webdriver', 47 | packages=find_packages(), 48 | setup_requires=['setuptools-pep8'], 49 | install_requires=['Flask>=0.12.2', 50 | 'Flask-login>=0.4.0', 51 | 'selenium>=3.6.0, <4.0.0a1', 52 | 'requests>=2.18.4', 53 | 'py-webdriver-manager', 54 | 'colorama' 55 | ], 56 | tests_require=['pytest'], 57 | entry_points={ 58 | 'console_scripts': [ 59 | 'golem-admin = golem.bin.golem_admin:main', 60 | 'golem = golem.bin.golem_init:main' 61 | ] 62 | }, 63 | cmdclass={'test': PyTest}, 64 | include_package_data=True, 65 | platforms='any', 66 | test_suite='', 67 | extras_require={ 68 | 'testing': ['pytest'], 69 | } 70 | ) 71 | -------------------------------------------------------------------------------- /tests/browser_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from golem.gui import gui_utils 4 | from golem import browser, execution 5 | from golem.core import settings_manager 6 | from golem.execution_runner import execution_runner 7 | from golem.test_runner import test_logger 8 | 9 | 10 | class TestGetBrowser: 11 | 12 | def test_driver_path_is_not_defined(self): 13 | execution.settings = settings_manager.assign_settings_default_values({}) 14 | execution.logger = test_logger.get_logger() 15 | default_browsers = gui_utils.get_supported_browsers_suggestions() 16 | drivers = [ 17 | ('chromedriver_path', 'chrome'), 18 | ('chromedriver_path', 'chrome-headless'), 19 | ('edgedriver_path', 'edge'), 20 | ('geckodriver_path', 'firefox'), 21 | ('iedriver_path', 'ie'), 22 | ('operadriver_path', 'opera'), 23 | ] 24 | for setting_path, browser_name in drivers: 25 | execution.browser_definition = execution_runner.define_browsers( 26 | [browser_name], [], default_browsers, [])[0] 27 | with pytest.raises(Exception) as excinfo: 28 | browser.open_browser() 29 | expected = 'Exception: {setting_path} setting is not defined' 30 | assert expected in str(excinfo.value) 31 | 32 | def test_executable_not_present(self): 33 | execution.settings = settings_manager.assign_settings_default_values({}) 34 | execution.logger = test_logger.get_logger() 35 | default_browsers = gui_utils.get_supported_browsers_suggestions() 36 | drivers = [ 37 | ('chromedriver_path', './drivers/chromedriver*', 'chrome'), 38 | ('chromedriver_path', './drivers/chromedriver*', 'chrome-headless'), 39 | ('edgedriver_path', './drivers/edgedriver*', 'edge'), 40 | ('geckodriver_path', './drivers/geckodriver*', 'firefox'), 41 | ('iedriver_path', './drivers/iedriver*', 'ie'), 42 | ('operadriver_path', './drivers/operadriver*', 'opera'), 43 | ] 44 | for setting_key, setting_path, browser_name in drivers: 45 | execution.browser_definition = execution_runner.define_browsers( 46 | [browser_name], [], default_browsers, [])[0] 47 | execution.settings[setting_key] = setting_path 48 | with pytest.raises(Exception) as excinfo: 49 | browser.open_browser() 50 | expected = f'No executable file found using path {setting_path}' 51 | assert expected in str(excinfo.value) 52 | -------------------------------------------------------------------------------- /tests/core/parsing_utils_test.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | from golem.core import parsing_utils 4 | 5 | 6 | class TestAstParseFile: 7 | 8 | def test_ast_parse_file(self, dir_function, test_utils): 9 | filepath = test_utils.create_file(dir_function.path, 'test_file.py', content='foo = 2\n') 10 | ast_node = parsing_utils.ast_parse_file(filepath) 11 | assert isinstance(ast_node, ast.Module) 12 | 13 | 14 | class TestTopLevelFunctions: 15 | 16 | def test_top_level_functions(self, dir_function, test_utils): 17 | path = dir_function.path 18 | 19 | # empty file 20 | filepath = test_utils.create_file(path, 'test_one.py', content='') 21 | ast_node = parsing_utils.ast_parse_file(filepath) 22 | functions = parsing_utils.top_level_functions(ast_node) 23 | assert functions == [] 24 | 25 | # a python module with local functions and imported functions 26 | content = ('from os import listdir\n' 27 | 'from sys import *\n' 28 | 'foo = 2\n' 29 | 'def f1():\n' 30 | ' pass\n' 31 | 'def f2():\n' 32 | ' pass') 33 | filepath = test_utils.create_file(path, 'test_two.py', content=content) 34 | ast_node = parsing_utils.ast_parse_file(filepath) 35 | functions = parsing_utils.top_level_functions(ast_node) 36 | assert functions == ['f1', 'f2'] 37 | 38 | 39 | class TestTopLevelAssignments: 40 | 41 | def test_top_level_assignments(self, dir_function, test_utils): 42 | path = dir_function.path 43 | 44 | # empty file 45 | filepath = test_utils.create_file(path, 'test_one.py', content='') 46 | ast_node = parsing_utils.ast_parse_file(filepath) 47 | assignments = parsing_utils.top_level_assignments(ast_node) 48 | assert assignments == [] 49 | 50 | # a python module with local functions and imported functions 51 | content = ('from os import *\n' 52 | 'from sys import platform\n' 53 | 'foo = 2\n' 54 | 'def f1():\n' 55 | ' pass\n' 56 | 'bar = (1, 2, 3)\n' 57 | 'for n in bar:\n' 58 | ' print(x)\n' 59 | 'if True:\n' 60 | ' baz = 3') 61 | filepath = test_utils.create_file(path, 'test_two.py', content=content) 62 | ast_node = parsing_utils.ast_parse_file(filepath) 63 | assignments = parsing_utils.top_level_assignments(ast_node) 64 | assert assignments == ['foo', 'bar'] 65 | -------------------------------------------------------------------------------- /tests/core/secrets_manager_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | from golem.core import secrets_manager 5 | 6 | SECRETS = { 7 | "db_host": "db-server-01.local", 8 | "schema": "public", 9 | "users": { 10 | "user01": "Mike" 11 | } 12 | } 13 | 14 | 15 | class TestGetSecrets: 16 | 17 | def test_get_secrets_in_case_secrets_file_does_not_exists(self, project_session): 18 | _, project = project_session.activate() 19 | secrets = secrets_manager.get_secrets(project) 20 | assert len(secrets) == 0 21 | assert secrets == {} 22 | 23 | def test_get_secrets_in_case_secrets_file_exists(self, project_session): 24 | _, project = project_session.activate() 25 | secrets_path = os.path.join(project_session.path, 'secrets.json') 26 | with open(secrets_path, 'w') as secrets_file: 27 | secrets_file.write(json.dumps(SECRETS, indent=True)) 28 | secrets = secrets_manager.get_secrets(project) 29 | assert len(secrets) == 3 30 | assert 'db_host' in secrets 31 | assert 'schema' in secrets 32 | assert secrets['users']['user01'] == 'Mike' 33 | -------------------------------------------------------------------------------- /tests/core/test_directory_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from golem.core import test_directory 4 | from golem.core.project import create_project 5 | 6 | 7 | class TestCreateTestDirectory: 8 | 9 | def test_new_directory_contents(self, dir_function, test_utils): 10 | name = test_utils.random_string(10) 11 | os.chdir(dir_function.path) 12 | testdir = os.path.join(dir_function.path, name) 13 | test_directory.create_test_directory(testdir) 14 | listdir = os.listdir(testdir) 15 | files = [name for name in listdir if os.path.isfile(os.path.join(testdir, name))] 16 | dirs = [name for name in listdir if os.path.isdir(os.path.join(testdir, name))] 17 | if '.DS_Store' in files: 18 | files.remove('.DS_Store') 19 | assert len(files) == 4 20 | # verify files 21 | assert '__init__.py' in files 22 | assert 'settings.json' in files 23 | assert 'users.json' in files 24 | assert '.golem' in files 25 | # verify directories 26 | assert len(dirs) == 2 27 | # verify the test dir contains the correct directories 28 | assert 'projects' in dirs 29 | assert 'drivers' in dirs 30 | 31 | 32 | class TestCreateTestdirGolemFile: 33 | 34 | def test_create_testdir_golem_file(self, dir_function): 35 | testdir = dir_function.path 36 | test_directory.create_testdir_golem_file(testdir) 37 | golem_file_path = os.path.join(testdir, '.golem') 38 | assert os.path.isfile(golem_file_path) 39 | with open(golem_file_path) as f: 40 | lines = f.readlines() 41 | assert lines[0] == '[gui]\n' 42 | assert lines[1].startswith('secret_key = ') 43 | 44 | 45 | class TestGetProjects: 46 | 47 | def test_get_projects(self, testdir_function): 48 | testdir_function.activate() 49 | create_project('project1') 50 | create_project('project2') 51 | projects = test_directory.get_projects() 52 | assert projects.sort() == ['project1', 'project2'].sort() 53 | 54 | def test_get_projects_no_project(self, testdir_function): 55 | testdir_function.activate() 56 | projects = test_directory.get_projects() 57 | assert projects == [] 58 | 59 | 60 | class TestProjectExists: 61 | 62 | def test_project_exists(self, testdir_session, test_utils): 63 | testdir_session.activate() 64 | project = test_utils.random_string(10) 65 | assert not test_directory.project_exists(project) 66 | create_project(project) 67 | assert test_directory.project_exists(project) 68 | 69 | 70 | class TestIsValidTestDirectory: 71 | 72 | def test_is_valid_test_directory(self, dir_function): 73 | path = dir_function.path 74 | assert not test_directory.is_valid_test_directory(path) 75 | test_directory.create_testdir_golem_file(path) 76 | assert test_directory.is_valid_test_directory(path) 77 | 78 | 79 | class TestGetDriverFolderFiles: 80 | 81 | def test_get_driver_folder_files(self, testdir_function, test_utils): 82 | testdir_function.activate() 83 | drivers_path = test_directory.drivers_path() 84 | open(os.path.join(drivers_path, 'file1'), 'w+').close() 85 | open(os.path.join(drivers_path, 'file2'), 'w+').close() 86 | os.mkdir(os.path.join(drivers_path, 'folder1')) 87 | assert len(os.listdir(drivers_path)) == 3 88 | files = test_directory.get_driver_folder_files() 89 | assert len(files) == 2 90 | assert 'file1' in files 91 | assert 'file2' in files 92 | 93 | 94 | class TestDeleteDriverFile: 95 | 96 | def test_delete_driver_file(self, testdir_class, test_utils): 97 | testdir_class.activate() 98 | drivers_path = test_directory.drivers_path() 99 | filepath = os.path.join(drivers_path, 'file1') 100 | open(filepath, 'w+').close() 101 | assert os.path.isfile(filepath) 102 | errors = test_directory.delete_driver_file('file1') 103 | assert errors == [] 104 | assert not os.path.isfile('file11') 105 | 106 | def test_delete_driver_file_does_not_exist(self, testdir_class, test_utils): 107 | testdir_class.activate() 108 | errors = test_directory.delete_driver_file('file2') 109 | assert errors == ['File file2 does not exist'] 110 | -------------------------------------------------------------------------------- /tests/golem_admin_cli_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from golem.core import test_directory 6 | from golem.cli import messages 7 | 8 | 9 | class TestGolemAdmin: 10 | 11 | run_commands = [ 12 | ('golem-admin', messages.ADMIN_USAGE_MSG), 13 | ('golem-admin -h', messages.ADMIN_USAGE_MSG), 14 | ('golem-admin --help', messages.ADMIN_USAGE_MSG), 15 | ('golem-admin -h createdirectory', messages.ADMIN_USAGE_MSG) 16 | ] 17 | 18 | @pytest.mark.slow 19 | @pytest.mark.parametrize('command,expected', run_commands,) 20 | def test_golem_admin_command_output(self, command, expected, test_utils): 21 | result = test_utils.run_command(command) 22 | assert result == expected 23 | 24 | @pytest.mark.slow 25 | def test_createdirectory_whitout_args(self, test_utils): 26 | result = test_utils.run_command('golem-admin createdirectory') 27 | expected = ('usage: golem-admin createdirectory [-y] [-h] name\n' 28 | 'golem-admin createdirectory: error: the following ' 29 | 'arguments are required: name') 30 | assert result == expected 31 | 32 | @pytest.mark.slow 33 | def test_createdirectory(self, dir_function, test_utils): 34 | name = 'testdir_test_002' 35 | cmd = f'golem-admin createdirectory {name}' 36 | result = test_utils.run_command(cmd) 37 | full_path = os.path.join(dir_function.path, name) 38 | assert os.path.exists(full_path) 39 | expected = (f'New golem test directory created at {full_path}\n' 40 | 'Use these credentials to access the GUI module:\n' 41 | ' user: admin\n' 42 | ' password: admin\n' 43 | 'Would you like to download ChromeDriver now? [Y/n]') 44 | assert expected in result 45 | 46 | @pytest.mark.slow 47 | def test_createdirectory_existing_testdirectory(self, dir_function, test_utils): 48 | """A test directory can be created if the destination is not empty 49 | by confirming the operation 50 | """ 51 | os.chdir(dir_function.path) 52 | name = 'testdir_test_006' 53 | full_path = os.path.join(dir_function.path, name) 54 | test_directory.create_test_directory(full_path) 55 | cmd = f'golem-admin createdirectory {full_path} -y' 56 | result = test_utils.run_command(cmd) 57 | expected = 'Error: target directory is already an existing Golem test directory' 58 | assert result == expected 59 | -------------------------------------------------------------------------------- /tests/helpers_test.py: -------------------------------------------------------------------------------- 1 | import string 2 | 3 | from golem import helpers 4 | 5 | 6 | class TestRandomFloat: 7 | 8 | def test_random_float(self): 9 | randfloat = helpers.random_float() 10 | assert isinstance(randfloat, float) 11 | assert 1.0 <= randfloat <= 100.0 12 | 13 | randfloat = helpers.random_float(-5.5, 5.5) 14 | assert -5.5 <= randfloat <= 5.5 15 | 16 | randfloat = helpers.random_float(5, 5) 17 | assert isinstance(randfloat, float) 18 | 19 | randfloat = helpers.random_float(min=1, max=2, decimals=3) 20 | assert len(str(randfloat)) <= 5 21 | 22 | randfloat = helpers.random_float(min=1, max=2, decimals=0) 23 | assert len(str(randfloat)) == 3 # either '1.0' or '2.0' 24 | 25 | 26 | class TestRandomInt: 27 | 28 | def test_random_int(self): 29 | randint = helpers.random_int() 30 | assert 1 <= randint <= 100 31 | 32 | randint = helpers.random_int(-5, 5) 33 | assert -5 <= randint <= 5 34 | 35 | randint = helpers.random_int(-5.0, 5.0) 36 | assert isinstance(randint, int) 37 | 38 | 39 | class TestRandomStr: 40 | 41 | def test_random_str(self): 42 | random_string = helpers.random_str() 43 | assert len(random_string) == 10 44 | assert all(c in string.ascii_lowercase for c in random_string) 45 | 46 | def test_random_str_length(self): 47 | random_string = helpers.random_str(length=0) 48 | assert len(random_string) == 0 49 | assert random_string == '' 50 | random_string = helpers.random_str(length=5) 51 | assert len(random_string) == 5 52 | 53 | def test_random_str_prefix_suffix(self): 54 | random_string = helpers.random_str(prefix='pre') 55 | assert random_string.startswith('pre') 56 | random_string = helpers.random_str(suffix='suf') 57 | assert random_string.endswith('suf') 58 | 59 | def test_random_str_sample(self): 60 | sample = ['a', 'b', 'c'] 61 | random_string = helpers.random_str(sample=sample) 62 | assert all(c in sample for c in random_string) 63 | 64 | sample = ['LOWERCASE'] 65 | random_string = helpers.random_str(sample=sample) 66 | assert all(c in string.ascii_lowercase for c in random_string) 67 | 68 | sample = ['UPPERCASE'] 69 | random_string = helpers.random_str(sample=sample) 70 | assert all(c in string.ascii_uppercase for c in random_string) 71 | 72 | sample = ['DIGITS'] 73 | random_string = helpers.random_str(sample=sample) 74 | assert all(c in string.digits for c in random_string) 75 | 76 | sample = ['SPECIAL'] 77 | random_string = helpers.random_str(sample=sample) 78 | assert all(c in string.punctuation for c in random_string) 79 | 80 | sample = 'this is a string sample' 81 | random_string = helpers.random_str(sample=sample) 82 | assert all(c in sample for c in random_string) 83 | 84 | sample = ['LOWERCASE', 'DIGITS'] 85 | random_string = helpers.random_str(sample=sample) 86 | assert all(c in string.ascii_lowercase or c in string.digits for c in random_string) 87 | 88 | sample = ['DIGITS', 'a', 'b', 'c'] 89 | random_string = helpers.random_str(sample=sample) 90 | assert all(c in string.digits or c in ['a', 'b', 'c'] for c in random_string) 91 | 92 | sample = [1, 2, 3] 93 | random_string = helpers.random_str(sample=sample) 94 | assert all(c in ['1', '2', '3'] for c in random_string) 95 | -------------------------------------------------------------------------------- /tests/report/cli_report_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | SUCCESS_MESSAGE = 'Test Result: SUCCESS' 5 | 6 | 7 | class TestReportToCli: 8 | 9 | def test_report_to_cli_one_test_file(self, project_module, test_utils, caplog, capsys): 10 | _, project = project_module.activate() 11 | test_file = test_utils.create_random_test(project) 12 | suite_name = test_utils.create_suite(project, tests=[test_file]) 13 | test_utils.run_suite(project, suite_name) 14 | out, err = capsys.readouterr() 15 | lines = out.splitlines() 16 | assert lines[-5] == 'Result:' 17 | assert lines[-4] == '------------------------------------------------------------------' 18 | assert test_file in lines[-3] # cannot capture colored char 19 | assert lines[-2] == '' 20 | assert lines[-1].startswith('Total: 1 tests, 1 success in') 21 | 22 | def test_report_to_cli_multiple_tests(self, project_module, test_utils, caplog, capsys): 23 | _, project = project_module.activate() 24 | test_file_one = test_utils.random_string() 25 | code = 'def test_foo(data):\n' \ 26 | ' assert 2 == 2\n' \ 27 | 'def test_bar(data):\n' \ 28 | ' assert False' 29 | test_utils.create_test(project, test_file_one, content=code) 30 | suite_name = test_utils.create_suite(project, tests=[test_file_one]) 31 | with pytest.raises(SystemExit) as w: 32 | test_utils.run_suite(project, suite_name) 33 | out, err = capsys.readouterr() 34 | lines = out.splitlines() 35 | assert lines[-16] == 'Result:' 36 | assert lines[-15] == '------------------------------------------------------------------' 37 | assert lines[-14] == test_file_one + ' ' 38 | assert 'test_foo' in lines[-13] # cannot capture colored char 39 | assert 'test_bar' in lines[-12] 40 | assert lines[-11] == '' 41 | assert lines[-10] == '1) test_bar' 42 | assert lines[-9] == 'AssertionError: ' 43 | assert lines[-4] == ' assert False' 44 | assert lines[-3] == 'AssertionError' 45 | assert lines[-2] == '' 46 | assert 'Total: 2 tests, 1 success, 1 failure in' in lines[-1] 47 | -------------------------------------------------------------------------------- /tests/report/html_report_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from golem.report import html_report 6 | 7 | 8 | class TestGenerateHTMLReport: 9 | 10 | def test_generate_html_report(self, project_class, test_utils): 11 | _, project = project_class.activate() 12 | execution = test_utils.execute_random_suite(project) 13 | 14 | html = html_report.generate_html_report( 15 | project, execution['suite_name'], execution['timestamp']) 16 | 17 | html_path = os.path.join(execution['exec_dir'], 'report.html') 18 | assert os.path.isfile(html_path) 19 | with open(html_path, encoding='utf-8') as f: 20 | assert f.read() == html 21 | -------------------------------------------------------------------------------- /tests/report/report_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from golem.report import report 4 | from golem.core.project import Project 5 | 6 | 7 | class TestGetLastExecutionTimestamps: 8 | 9 | def test_get_last_execution_timestamps(self, project_function, test_utils): 10 | _, project = project_function.activate() 11 | 12 | # suite does not exist 13 | last_exec = report.get_last_execution_timestamps([project], 'suite_does_not_exist') 14 | assert last_exec[project] == {} 15 | 16 | # suite with no executions 17 | suite_name = 'suite1' 18 | test_utils.create_test(project, name='test1') 19 | test_utils.create_suite(project, name=suite_name, tests=['test1']) 20 | 21 | assert last_exec[project] == {} 22 | 23 | # suite with one execution 24 | timestamp = test_utils.run_suite(project, suite_name) 25 | last_exec = report.get_last_execution_timestamps([project], suite_name) 26 | assert last_exec[project] == {suite_name: [timestamp]} 27 | 28 | # multiple executions 29 | timestamp_first = test_utils.run_suite(project, suite_name) 30 | timestamp_second = test_utils.run_suite(project, suite_name) 31 | last_exec = report.get_last_execution_timestamps([project], suite_name, limit=2) 32 | assert len(last_exec[project][suite_name]) == 2 33 | assert last_exec[project][suite_name][0] == timestamp_second 34 | assert last_exec[project][suite_name][1] == timestamp_first 35 | 36 | 37 | class TestDeleteExecution: 38 | 39 | def test_delete_execution(self, project_class, test_utils): 40 | _, project = project_class.activate() 41 | execution = test_utils.execute_random_suite(project) 42 | execpath = os.path.join(Project(project).report_directory_path, execution['suite_name']) 43 | assert os.path.isdir(execpath) 44 | assert os.path.isdir(execution['exec_dir']) 45 | 46 | errors = report.delete_execution(project, execution['suite_name']) 47 | 48 | assert errors == [] 49 | assert not os.path.isdir(execpath) 50 | 51 | 52 | class TestDeleteExecutionTimestamp: 53 | 54 | def test_delete_execution_timestamp(self, project_class, test_utils): 55 | _, project = project_class.activate() 56 | execution = test_utils.execute_random_suite(project) 57 | execpath = os.path.join(Project(project).report_directory_path, execution['suite_name']) 58 | assert os.path.isdir(execution['exec_dir']) 59 | 60 | errors = report.delete_execution_timestamp(project, execution['suite_name'], execution['timestamp']) 61 | 62 | assert errors == [] 63 | assert not os.path.isdir(execution['exec_dir']) 64 | assert os.path.isdir(execpath) # folder for execution name still exists 65 | 66 | def test_delete_execution_timestamp_does_not_exist(self, project_class, test_utils): 67 | _, project = project_class.activate() 68 | execution = test_utils.random_string() 69 | timestamp = test_utils.random_string() 70 | errors = report.delete_execution_timestamp(project, execution, timestamp) 71 | assert errors == [f'Execution for {project} {execution} {timestamp} does not exist'] 72 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py35, py36 3 | 4 | [testenv] 5 | usedevelop = false 6 | deps = 7 | pytest>=3 8 | commands = pytest --------------------------------------------------------------------------------