Repository: TheSoftwareHouse/Kakunin Branch: master Commit: 9348aef24a6f Files: 409 Total size: 2.5 MB Directory structure: gitextract_s20x1q0_/ ├── .dockerignore ├── .editorconfig ├── .gitignore ├── .npmignore ├── .prettierrc ├── .travis.yml ├── CHANGELOG.MD ├── CONTRIBUTING.MD ├── Dockerfile ├── LICENSE ├── MIGRATION-2.0.0.MD ├── MIGRATION-2.2.0.MD ├── MIGRATION-3.0.0.MD ├── ROADMAP.MD ├── build.sh ├── docker-compose.yml ├── docs/ │ ├── browserstack.md │ ├── configuration.md │ ├── cross-browser.md │ ├── docker.md │ ├── extending.md │ ├── headless.md │ ├── hooks.md │ ├── how-it-works.md │ ├── index.md │ ├── matchers.md │ ├── parallel-testing.md │ ├── performance-testing.md │ ├── quickstart.md │ ├── steps-debug.md │ ├── steps-elements.md │ ├── steps-files.md │ ├── steps-forms.md │ ├── steps-generators.md │ ├── steps-navigation.md │ ├── steps-rest.md │ ├── testing-rest-api.md │ └── transformers.md ├── functional-tests/ │ ├── dictionaries/ │ │ └── test-dictionary.js │ ├── downloads/ │ │ ├── .gitkeep │ │ └── example.xlsx │ ├── features/ │ │ ├── content/ │ │ │ ├── operations_on_stored_variables.feature │ │ │ ├── validate_tabular_data.feature │ │ │ ├── validate_tabular_data_css.feature │ │ │ ├── wait_for_element_dissapear.feature │ │ │ └── wait_for_element_dissapear_css.feature │ │ ├── drag-and-drop/ │ │ │ ├── operations_on_elements.feature │ │ │ └── operations_on_elements_css.feature │ │ ├── forms/ │ │ │ ├── fill_and_check_form.feature │ │ │ ├── fill_and_check_form_css.feature │ │ │ └── fill_select.feature │ │ ├── matchers/ │ │ │ ├── match_current_date.feature │ │ │ ├── match_current_date_css.feature │ │ │ ├── matchers.feature │ │ │ └── matchers_css.feature │ │ ├── navigation/ │ │ │ ├── navigate_to_given_page.feature │ │ │ ├── navigate_to_given_page_css.feature │ │ │ └── switch-between-tabs.feature │ │ ├── pages/ │ │ │ └── verify_displayed_page.feature │ │ ├── testing-api/ │ │ │ ├── testing_delete_request.feature │ │ │ ├── testing_get_response.feature │ │ │ ├── testing_headers_setting.feature │ │ │ ├── testing_patch_request.feature │ │ │ ├── testing_post_form_data.feature │ │ │ └── testing_post_json.feature │ │ └── wait-for-elements/ │ │ ├── wait_for_form.feature │ │ ├── wait_for_form_css.feature │ │ └── wait_for_table.feature │ ├── kakunin.conf.js │ ├── package.json │ ├── pages/ │ │ ├── absolutePage.js │ │ ├── additionalParams.js │ │ ├── appearSimpleForm.js │ │ ├── appearSimpleFormPost.js │ │ ├── appearTabularData.js │ │ ├── buttonForm.js │ │ ├── dragAndDrop.js │ │ ├── google.js │ │ ├── main.js │ │ ├── matchers.js │ │ ├── navigationPages.js │ │ ├── simpleForm.js │ │ ├── simpleFormPost.js │ │ ├── simpleSelectForm.js │ │ └── tabularData.js │ ├── regexes/ │ │ └── index.js │ ├── step_definitions/ │ │ └── custom_json_parser.js │ └── www/ │ ├── index.js │ ├── jsonData/ │ │ └── xlsxData.router.js │ └── views/ │ ├── absolute/ │ │ └── index.njs │ ├── drag-and-drop/ │ │ └── index.njs │ ├── form/ │ │ ├── disappear.njs │ │ ├── select.njs │ │ └── simple.njs │ ├── index.njs │ ├── layout/ │ │ └── default.njs │ ├── matchers/ │ │ └── matchers.njs │ ├── navigation/ │ │ └── page.njs │ ├── table/ │ │ └── tabular-data.njs │ └── wait-for-appear/ │ ├── form.njs │ └── table.njs ├── package.json ├── readme.md ├── src/ │ ├── cli.ts │ ├── comparators/ │ │ ├── comparator/ │ │ │ ├── date.comparator.spec.ts │ │ │ ├── date.comparator.ts │ │ │ ├── index.ts │ │ │ ├── number.comparator.spec.ts │ │ │ └── number.comparator.ts │ │ ├── comparator.interface.ts │ │ ├── comparators.spec.ts │ │ ├── comparators.ts │ │ └── index.ts │ ├── core/ │ │ ├── cli/ │ │ │ ├── cli.helper.spec.ts │ │ │ ├── cli.helper.ts │ │ │ └── initializer.ts │ │ ├── config.helper.ts │ │ ├── fs/ │ │ │ ├── delete-files.helper.ts │ │ │ └── prepare-catalogs.helper.ts │ │ ├── modules-loader.helper.ts │ │ └── prototypes.ts │ ├── dictionaries/ │ │ ├── base.ts │ │ ├── dictionaries.spec.ts │ │ ├── dictionaries.ts │ │ └── index.ts │ ├── emails/ │ │ ├── adapter/ │ │ │ ├── mailtrap.client.spec.ts │ │ │ └── mailtrap.client.ts │ │ ├── email.service.spec.ts │ │ ├── email.service.ts │ │ ├── filter/ │ │ │ ├── current-user.filter.ts │ │ │ ├── current-user.spec.ts │ │ │ ├── index.ts │ │ │ ├── minimal-email-size.filter.spec.ts │ │ │ ├── minimal-email-size.filter.ts │ │ │ ├── text-fields.filter.spec.ts │ │ │ └── text-fields.filter.ts │ │ ├── filters.spec.ts │ │ ├── filters.ts │ │ └── index.ts │ ├── form-handlers/ │ │ ├── form-handler.interface.ts │ │ ├── handler/ │ │ │ ├── checkbox.handler.ts │ │ │ ├── ckeditor.handler.ts │ │ │ ├── custom-angular-select.handler.ts │ │ │ ├── default.handler.ts │ │ │ ├── file.handler.ts │ │ │ ├── index.ts │ │ │ ├── radio.handler.ts │ │ │ ├── select.handler.ts │ │ │ └── uploaded-file.handler.ts │ │ ├── handlers.ts │ │ └── index.ts │ ├── generators/ │ │ ├── generator/ │ │ │ ├── index.ts │ │ │ ├── personalData.generator.spec.ts │ │ │ ├── personalData.generator.ts │ │ │ ├── string-with-length.generator.spec.ts │ │ │ └── string-with-length.generator.ts │ │ ├── generator.interface.ts │ │ ├── generators.spec.ts │ │ ├── generators.ts │ │ └── index.ts │ ├── index.ts │ ├── kakunin.d.ts │ ├── matchers/ │ │ ├── index.ts │ │ ├── matcher/ │ │ │ ├── attribute.matcher.spec.ts │ │ │ ├── attribute.matcher.ts │ │ │ ├── clickable.matcher.spec.ts │ │ │ ├── clickable.matcher.ts │ │ │ ├── currentDate.matcher.spec.ts │ │ │ ├── currentDate.matcher.ts │ │ │ ├── index.ts │ │ │ ├── invisible.matcher.spec.ts │ │ │ ├── invisible.matcher.ts │ │ │ ├── not-clickable.matcher.spec.ts │ │ │ ├── not-clickable.matcher.ts │ │ │ ├── present.matcher.spec.ts │ │ │ ├── present.matcher.ts │ │ │ ├── regex-matcher/ │ │ │ │ ├── index.spec.ts │ │ │ │ ├── index.ts │ │ │ │ ├── regex-builder.spec.ts │ │ │ │ ├── regex-builder.ts │ │ │ │ ├── regex.ts │ │ │ │ └── regexes/ │ │ │ │ └── default.ts │ │ │ ├── text.matcher.spec.ts │ │ │ ├── text.matcher.ts │ │ │ ├── visible.matcher.spec.ts │ │ │ └── visible.matcher.ts │ │ ├── matcher.interface.ts │ │ ├── matchers.spec.ts │ │ └── matchers.ts │ ├── pages/ │ │ ├── base.ts │ │ ├── form.ts │ │ └── index.ts │ ├── protractor.conf.ts │ ├── rest/ │ │ ├── api-request.ts │ │ ├── api-response.spec.ts │ │ ├── api-response.ts │ │ └── rest-api-service.ts │ ├── step_definitions/ │ │ ├── api.ts │ │ ├── debug.ts │ │ ├── elements.ts │ │ ├── email.ts │ │ ├── file.ts │ │ ├── form.ts │ │ ├── generators.ts │ │ ├── navigation.ts │ │ ├── performance.ts │ │ └── tabs.ts │ ├── tests/ │ │ ├── dictionaries/ │ │ │ └── fake-dictionary.ts │ │ └── init.ts │ ├── transformers/ │ │ ├── index.ts │ │ ├── transformer/ │ │ │ ├── dictionary.transformer.spec.ts │ │ │ ├── dictionary.transformer.ts │ │ │ ├── generator.transformer.spec.ts │ │ │ ├── generator.transformer.ts │ │ │ ├── variable-store.transformer.spec.ts │ │ │ └── variable-store.transformer.ts │ │ ├── transformer.interface.ts │ │ ├── transformers.spec.ts │ │ └── transformers.ts │ └── web/ │ ├── browsers/ │ │ ├── browsers-config.helper.ts │ │ ├── browserstack-config.helper.ts │ │ ├── create-firefox-profile.helper.ts │ │ ├── get-browser-drivers.helper.ts │ │ └── safari-browser-configurator.helper.ts │ ├── cucumber/ │ │ ├── config.ts │ │ ├── hooks/ │ │ │ ├── clear-download.hook.ts │ │ │ ├── clear-variables.hook.ts │ │ │ ├── hook.interface.ts │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── reload-fixtures.hook.ts │ │ │ ├── reload-user.hook.ts │ │ │ └── take-screenshots.hook.ts │ │ ├── hooks.ts │ │ └── wait-for-condition.helper.ts │ ├── fixtures/ │ │ └── fixtures-loader.helper.ts │ ├── fs/ │ │ ├── download-checker.helper.ts │ │ └── file-manager.helper.ts │ ├── parallel/ │ │ ├── chunk-specs.helper.spec.ts │ │ ├── chunk-specs.helper.ts │ │ └── prepare-browser-instance-specs.helper.ts │ ├── parameters.ts │ ├── performance/ │ │ ├── JSON-performance-report-parser.helper.spec.ts │ │ ├── JSON-performance-report-parser.helper.ts │ │ ├── time-to-first-byte-analyser.helper.spec.ts │ │ └── time-to-first-byte-analyser.helper.ts │ ├── url-parser.helper.spec.ts │ ├── url-parser.helper.ts │ ├── user-provider.helper.ts │ ├── variable-store.helper.spec.ts │ └── variable-store.helper.ts ├── templates/ │ ├── example.feature │ ├── generator.js │ ├── hook.js │ ├── login.js │ ├── matcher.js │ ├── page.js │ ├── regex.js │ └── steps.js ├── tsconfig.json ├── tsconfig.test.json ├── tslint.json └── website/ ├── README.md ├── build/ │ └── Kakunin/ │ ├── css/ │ │ ├── main.css │ │ └── prism.css │ ├── docs/ │ │ ├── 2.4.0/ │ │ │ ├── configuration/ │ │ │ │ └── index.html │ │ │ ├── configuration.html │ │ │ ├── cross-browser/ │ │ │ │ └── index.html │ │ │ ├── cross-browser.html │ │ │ ├── docker/ │ │ │ │ └── index.html │ │ │ ├── docker.html │ │ │ ├── extending/ │ │ │ │ └── index.html │ │ │ ├── extending.html │ │ │ ├── how-it-works/ │ │ │ │ └── index.html │ │ │ ├── how-it-works.html │ │ │ ├── index.html │ │ │ ├── matchers/ │ │ │ │ └── index.html │ │ │ ├── matchers.html │ │ │ ├── parallel-testing/ │ │ │ │ └── index.html │ │ │ ├── parallel-testing.html │ │ │ ├── performance-testing/ │ │ │ │ └── index.html │ │ │ ├── performance-testing.html │ │ │ ├── quickstart/ │ │ │ │ └── index.html │ │ │ ├── quickstart.html │ │ │ ├── steps-debug/ │ │ │ │ └── index.html │ │ │ ├── steps-debug.html │ │ │ ├── steps-elements/ │ │ │ │ └── index.html │ │ │ ├── steps-elements.html │ │ │ ├── steps-files/ │ │ │ │ └── index.html │ │ │ ├── steps-files.html │ │ │ ├── steps-forms/ │ │ │ │ └── index.html │ │ │ ├── steps-forms.html │ │ │ ├── steps-generators/ │ │ │ │ └── index.html │ │ │ ├── steps-generators.html │ │ │ ├── steps-navigation/ │ │ │ │ └── index.html │ │ │ ├── steps-navigation.html │ │ │ ├── transformers/ │ │ │ │ └── index.html │ │ │ └── transformers.html │ │ ├── configuration/ │ │ │ └── index.html │ │ ├── configuration.html │ │ ├── cross-browser/ │ │ │ └── index.html │ │ ├── cross-browser.html │ │ ├── docker/ │ │ │ └── index.html │ │ ├── docker.html │ │ ├── extending/ │ │ │ └── index.html │ │ ├── extending.html │ │ ├── how-it-works/ │ │ │ └── index.html │ │ ├── how-it-works.html │ │ ├── index.html │ │ ├── matchers/ │ │ │ └── index.html │ │ ├── matchers.html │ │ ├── next/ │ │ │ ├── browserstack/ │ │ │ │ └── index.html │ │ │ ├── browserstack.html │ │ │ ├── configuration/ │ │ │ │ └── index.html │ │ │ ├── configuration.html │ │ │ ├── cross-browser/ │ │ │ │ └── index.html │ │ │ ├── cross-browser.html │ │ │ ├── docker/ │ │ │ │ └── index.html │ │ │ ├── docker.html │ │ │ ├── extending/ │ │ │ │ └── index.html │ │ │ ├── extending.html │ │ │ ├── headless/ │ │ │ │ └── index.html │ │ │ ├── headless.html │ │ │ ├── hooks/ │ │ │ │ └── index.html │ │ │ ├── hooks.html │ │ │ ├── how-it-works/ │ │ │ │ └── index.html │ │ │ ├── how-it-works.html │ │ │ ├── index.html │ │ │ ├── matchers/ │ │ │ │ └── index.html │ │ │ ├── matchers.html │ │ │ ├── parallel-testing/ │ │ │ │ └── index.html │ │ │ ├── parallel-testing.html │ │ │ ├── performance-testing/ │ │ │ │ └── index.html │ │ │ ├── performance-testing.html │ │ │ ├── quickstart/ │ │ │ │ └── index.html │ │ │ ├── quickstart.html │ │ │ ├── steps-debug/ │ │ │ │ └── index.html │ │ │ ├── steps-debug.html │ │ │ ├── steps-elements/ │ │ │ │ └── index.html │ │ │ ├── steps-elements.html │ │ │ ├── steps-files/ │ │ │ │ └── index.html │ │ │ ├── steps-files.html │ │ │ ├── steps-forms/ │ │ │ │ └── index.html │ │ │ ├── steps-forms.html │ │ │ ├── steps-generators/ │ │ │ │ └── index.html │ │ │ ├── steps-generators.html │ │ │ ├── steps-navigation/ │ │ │ │ └── index.html │ │ │ ├── steps-navigation.html │ │ │ ├── steps-rest/ │ │ │ │ └── index.html │ │ │ ├── steps-rest.html │ │ │ ├── testing-rest-api/ │ │ │ │ └── index.html │ │ │ ├── testing-rest-api.html │ │ │ ├── transformers/ │ │ │ │ └── index.html │ │ │ └── transformers.html │ │ ├── parallel-testing/ │ │ │ └── index.html │ │ ├── parallel-testing.html │ │ ├── performance-testing/ │ │ │ └── index.html │ │ ├── performance-testing.html │ │ ├── quickstart/ │ │ │ └── index.html │ │ ├── quickstart.html │ │ ├── steps-debug/ │ │ │ └── index.html │ │ ├── steps-debug.html │ │ ├── steps-elements/ │ │ │ └── index.html │ │ ├── steps-elements.html │ │ ├── steps-files/ │ │ │ └── index.html │ │ ├── steps-files.html │ │ ├── steps-forms/ │ │ │ └── index.html │ │ ├── steps-forms.html │ │ ├── steps-generators/ │ │ │ └── index.html │ │ ├── steps-generators.html │ │ ├── steps-navigation/ │ │ │ └── index.html │ │ ├── steps-navigation.html │ │ ├── transformers/ │ │ │ └── index.html │ │ └── transformers.html │ ├── en/ │ │ ├── versions/ │ │ │ └── index.html │ │ └── versions.html │ ├── index.html │ ├── js/ │ │ ├── codetabs.js │ │ └── scrollSpy.js │ ├── sitemap.xml │ ├── versions/ │ │ └── index.html │ └── versions.html ├── core/ │ └── Footer.js ├── package.json ├── pages/ │ └── en/ │ └── versions.js ├── sidebars.json ├── siteConfig.js ├── static/ │ ├── css/ │ │ └── custom.css │ └── index.html ├── versioned_docs/ │ ├── version-2.4.0/ │ │ ├── configuration.md │ │ ├── cross-browser.md │ │ ├── docker.md │ │ ├── extending.md │ │ ├── how-it-works.md │ │ ├── index.md │ │ ├── matchers.md │ │ ├── parallel-testing.md │ │ ├── performance-testing.md │ │ ├── quickstart.md │ │ ├── steps-debug.md │ │ ├── steps-elements.md │ │ ├── steps-files.md │ │ ├── steps-forms.md │ │ ├── steps-generators.md │ │ ├── steps-navigation.md │ │ └── transformers.md │ └── version-2.5.0/ │ └── steps-elements.md ├── versioned_sidebars/ │ └── version-2.4.0-sidebars.json └── versions.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ */node_modules *.log ================================================ FILE: .editorconfig ================================================ root=true [*] charset=utf-8 end_of_line=lf insert_final_newline=true indent_style=space indent_size=2 [Makefile] indent_style = tab [*.feature] indent_style=space indent_size=4 ================================================ FILE: .gitignore ================================================ #build files dist # dependencies node_modules #tests selenium-debug.log reports screenshots testium tmp-* npm-debug.log .idea downloads/* !functional-tests/downloads/.gitkeep !functional-tests/downloads/example.xlsx docker-compose.override.yml !downloads/.gitkeep !functional-tests/reports/.gitkeep !functional-tests/reports/report/.gitkeep !functional-tests/reports/report/features/.gitkeep !functional-tests/performance/.gitkeep !example/reports/.gitkeep !reports/.gitkeep .DS_Store functional-tests/package-lock.json website/yarn.lock website/node_modules website/i18n/* local.log browserstack.err ================================================ FILE: .npmignore ================================================ website functional-tests docs ================================================ FILE: .prettierrc ================================================ { "useTabs": false, "printWidth": 120, "tabWidth": 2, "singleQuote": true, "trailingComma": "es5", "bracketSpacing": true } ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - "lts/*" addons: chrome: stable script: - npm run test-ci - npm run lint ================================================ FILE: CHANGELOG.MD ================================================ #### v3.0.0 - added support for Browserstack - updated external libraries - locked selenium-standalone-server to `3.14.0` to prevent bugs caused due to updated external libraries (we'll update it manually) - coverted Kakunin from Javascript to Typescript - added possibility to set priority of hooks Breaking change! Take a look to the MIGRATION-3.0.0.MD #### v2.6.0 - added possibility to control `headless` by CLI by a command `npm run kakunin -- --headless` - added a possibility to visit URL w query params - added support for Internet Explorer #### v2.5.0 - added prepublish script to run tests before publish on NPM - improved logging errors in the console - added docusaurus documentation - added possibility to test REST API #### v2.4.0 - `When I pause` step has been deleted as the `browser.pause()` method is not supported since Node 8.x.x - added `how to debug` section to the documentation - added example how to configure docker for Kakunin projects - configured Travis CI - unit and functional tests - added support for Safari and Firefox browsers (take a look at the `cross-browser` section in the documentation) - added parallel functionality (take a look at the `parallel` section in the documentation) - configured prettier and eslint on `git commit` action - added a possibility to use `When I store` and `There is element` steps on `input`/`textarea` fields - added `faker` generator (generate random names, cities etc.) ##### v2.3.0 - added a new step: `success if email not found` - updated `emails section` in the documentation - added a possibility to answer prompts by a bash command in the `init process` - fixed `file.js` step and added functional tests covering `compare xlsx with stored data` - updated dependencies ##### v2.2.0 - added migration rules to version `v2.2.0` - fixed `wait for url change` error - improved CLI scripts and added tests - improved `When I click step` - added an extra wait until the element is clickable - fixed the cache problem in the `local storage` (wait until the cache is cleared) - improved how the matchers errors are displayed - changed Mocca into Jest - changed Chai for Jest syntax - moved `maxEmailRepeats` to the configuration - updated Mailtrap adapter due to changed API (take a look at the `MIGRATION-2.2.0.MD`) - updated dependencies ##### v2.1.0 - multiple code refactors - improved url comparing - fixed bug: missing "reports/report/features" catalogs required to execute tests - added functionality which deletes all report files before a test run - added new matcher - `f:currentDate:YYYY-MM-DD` to generate a current date - added a possibility to test performance with browserMob (save .har files) and compare TTFB timing values - improvements for clicking element - every element step waits for visibilityOf element (waiting before step is no longer required in common cases) - added wait method to helpers, also is exported outside of kakunin - added deprecated warning for is Present steps - added matcher for currentDate - code refactor for kakunin.conf.js - replaced generator step with real generator supporting multiple params - code cleanup (unused imports etc.), - documentation update and test update (added useful undocumented and untested steps, removal of unused steps) ##### v2.0.0 - updated documentation - added more functional tests - added support for Windows - added support for `relative` and `absolute` urls in Page Objects - added `BaseDictionary` functionality - added new step (support drag and drop) `I drag "elementName" element and drop over "dropOnElementName" element` - fixed reports - fixed step `I wait for the "elementName" element to disappear` - updated libs (e.g. cucumber js) - `isExternal` is no longer required in Page Objects (Angular) - locators are no longer supported in Page Objects - export `module.exports` has been changed in Page Objects - removed `| element | value |` headers from first row in a steps - `.gitkeep` is automatically created in reports catalog - `RELOAD_FIXTURES_URL` has been moved to advanced configuration - step `the "arrayElementName" element is visible` can be used now for an array element ##### v1.0.0 - updated documentation - added license - added example ##### v0.16.4 - updated documentation and readme ##### v0.16.3 - changed `There are "equal 4" following elements for element "rows":` error message to be more descriptive - added express app to handle form submit tests - added tests form html default field types and tabular content validation ##### v0.16.2 - added new step `I visit the "pageName" page with parameters:` which replaces wildcards with a values given in the table - fixed step `I wait for "condition" of the "element" element`, currently timeout is set properly to `elementsVisibilityTimeout` key which is placed in kakunin.config.js - improved step `I wait for "condition" of the "element" element`, currently singleElement and arrayElements can be checked - change step implementation: `I click the "keyName" key` to `I press the "keyName" key` ##### v0.16.1 - added changelog - added directory for mailing service adapters [`emails`] and connect it to modules loading system - fixed a bug where exported mailing service and the one used internally where a different instances ================================================ FILE: CONTRIBUTING.MD ================================================ # Contributing to Kakunin This section will guide you through the contribution process. ### Step 1: Fork Fork the project [on GitHub](https://github.com/TheSoftwareHouse/Kakunin) and clone your fork locally. ```bash git clone git@github.com:TheSoftwareHouse/Kakunin.git cd Kakunin git remote add upstream https://github.com/TheSoftwareHouse/Kakunin.git ``` #### Which branch? For developing new features and bug fixes, the `master` branch should be pulled and built upon. ### Step 2: Branch Create a branch and start hacking: ```bash git checkout -b my-branch -t origin/master ``` ### Step 3: Commit Make sure git knows your name and email address: ```bash git config --global user.name "Jan Kowalski" git config --global user.email "jan@kowalski.com" ``` Add and commit: ```bash $ git add my/changed/files $ git commit ``` ### Commit message guidelines The commit message should describe what changed and why. We don't put any constraints on message format although it should clearly describe the change. 4. If your patch fixes an open issue, you can add a reference to it at the end of the log. Use the `Fixes:` prefix and the full issue URL. For other references use `Refs:`. Examples: - `Fixes: https://github.com/TheSoftwareHouse/Kakunin/issues/1337` - `Refs: http://eslint.org/docs/rules/space-in-parens.html` - `Refs: https://github.com/TheSoftwareHouse/Kakunin/pull/1234` ### Step 4: Rebase Use `git rebase` (not `git merge`) to synchronize your work with the main repository. ```bash $ git fetch upstream $ git rebase upstream/master ``` ### Step 5: Test Bug fixes and features should come with tests. Looking at other tests to see how they should be structured can help. To run the tests just run the default command: ```bash npm test ``` Make sure the linter does not report any issues and that all tests pass. Please do not submit patches that fail either check. ### Step 6: Push ```bash git push origin my-branch ``` Pull requests are usually reviewed within a few days. ### Step 7: Discuss and update You will probably get feedback or requests for changes to your Pull Request. This is a big part of the submission process so don't be discouraged! To make changes to an existing Pull Request, make the changes to your branch. When you push that branch to your fork, GitHub will automatically update the Pull Request. Feel free to post a comment in the Pull Request to ping reviewers if you are awaiting an answer on something. ### Step 8: Landing In order to land, a Pull Request needs to be reviewed and approved by at least one Kakunin Collaborator and pass all test. After that, as long as there are no objections from a Collaborator, the Pull Request can be merged. After you push new changes to your branch, you need to get approval for these new changes again, even if GitHub shows "Approved" because the reviewers have hit the buttons before. ## Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: * (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or * (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or * (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. * (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. ================================================ FILE: Dockerfile ================================================ FROM node:8.11.4 WORKDIR /app/website EXPOSE 3000 35729 COPY kakunin/docs /app/docs COPY ./website /app/website RUN yarn install CMD ["yarn", "start"] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 The Software House Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MIGRATION-2.0.0.MD ================================================ # Kakunin - automated testing framework #### Migration to 2.0.0 version What needs to be done: 1. Remove all locators from Page Objects ```javascript this.exampleLocator = by.css('.unique-class'); ``` needs to be changed to: ```javascript this.exampleLocator = $('.unique-class'); ``` 2. Remove headers `| element | value |` from all steps ```gherkin And there are "equal 1" following elements for element "rows": | element | value | | firstName | t:John | | lastName | t:Doe | ``` needs to be changed to: ```gherkin And there are "equal 1" following elements for element "rows": | firstName | t:John | | lastName | t:Doe | ``` 3. Delete `isExternal` from all Page Objects, if you are using Angular application ```javascript class ExamplePage extends BasePage { constructor() { super(); this.isExternal = true; this.url = '/'; this.selector = $('.some-class'); } ``` needs to be changed to: ```javascript class ExamplePage extends BasePage { constructor() { super(); this.url = '/'; this.selector = $('.some-class'); } ``` 4. Change `exports` in Page Objects: The last line in each of the page objects ```javascript module.exports = new ExamplePage(); ``` needs to be changed to: ```javascript module.exports = ExamplePage; ``` 5. Change `dictionaries`, example: ```javascript const { dictionaries } = require('kakunin'); class TestDictionary { constructor() { this.values = { 'test-name': 'Janek', 'test-value': 'lux' }; this.name = 'test-dictionary'; } isSatisfiedBy(name) { return this.name === name; } getMappedValue(key) { return this.values[key]; } } dictionaries.addDictionary(new TestDictionary()); ``` needs to be changed to: ```javascript const { dictionaries } = require('kakunin'); const { BaseDictionary } = require('kakunin'); class TestDictionary extends BaseDictionary { constructor() { super('test-dictionary', { 'test-name': 'Janek', 'test-value': 'lux' }); } } dictionaries.addDictionary(new TestDictionary()); ``` ================================================ FILE: MIGRATION-2.2.0.MD ================================================ # Kakunin - automated testing framework #### Migration to 2.2.0 version What needs to be done: 1. If you are using MailTrap client to tests emails. Open `kakunin.conf.js` file and edit it: ```javascript email: { type: 'mailtrap', config: { apiKey: '', inboxId: '', url: 'https://mailtrap.io/api/v1', }, }, ``` needs to be changed to: ```javascript email: { type: 'mailtrap', config: { apiKey: '', inboxId: '', url: 'https://mailtrap.io', }, }, ``` This change is required due to API changes in MailTrap. More details available under the link: https://mailtrap.io/blog/2018-06-06-mailtraps-upcoming-api-changes Please note that, that the steps in scenarios do not require any changes! You can still use `html_body` to check the email content. This is a breaking change for users using MailTrap in tests but we did not want to change version to "3.0.0". Just "minor" version has been increased by one. 2. Remove node_modules 3. Update protractor to newest version - `npm install protractor@latest --save` 4. If you have some custom matchers, make sure to make them return rejected promise on fail. This is done to improve error readability. ================================================ FILE: MIGRATION-3.0.0.MD ================================================ # Kakunin - automated testing framework #### Migration to 3.0.0 version What needs to be done: 1. Change Hooks In the version before 3.0.0 we had a single file located in the `./hook` directory. Example of `hook.js`: ```javascript const { Before, After } = require('kakunin'); Before(() => { console.log('If you can see this in console then hook is working properly.'); }); After(() => { console.log('Console log after the scenario'); }); ``` Currently, the interface to set priorities has been added. So we can control if the hook `FirstExample` will be executed before `SecondExample`. Example of `first-example.hook.js`: ```javascript const { hookHandlers, Before } = require('kakunin'); class FirstExampleHook { initializeHook() { Before(() => { console.log('First example hook'); }); } getPriority() { return 990; } } hookHandlers.addHook(new FirstExampleHook()); ``` ================================================ FILE: ROADMAP.MD ================================================ ## Roadmap ### v2.1.0 * Allow to test REST endpoints against expected JSON Schema ### v2.0.0 * full support for Windows OS, * we are going to drop the need for strict first row in gherkin tables. For example, instead of: ``` gherkin Given there are "at least 5" following elements for element "items": | element | value | | viewButton | f:isVisible | ``` you will be able to write it like this: ``` gherkin Given there are "at least 5" following elements for element "items": | viewButton | f:isVisible | ``` this change will be introduced to all tables related steps. * we are going to drop the need for defining `locators` instead of `selectors` for some table steps. For example: ``` gherkin Given there are "at least 5" following elements for element "items": | element | value | | viewButton | f:isVisible | ``` Currently the `viewButton` must be defined as `this.viewButtonLocator = by.css('.viewButton')`. In case you wish to click on such locator, you have to create a selector for it - `this.viewButton = element(this.viewButtonLocator)`. We are aware of code duplication issue here and after investigation this will be fixed in upcoming major version. From `v2.0.0` you'll be able to use `this.viewButton = element(by.css('.viewButton'))` (and even the `$()` shortcut) in every step. ================================================ FILE: build.sh ================================================ #!/bin/sh git subtree push --prefix website/build/Kakunin origin gh-pages ================================================ FILE: docker-compose.yml ================================================ version: "3" services: docusaurus: build: . ports: - 3000:3000 - 35729:35729 volumes: - ./docs:/app/docs - ./website/blog:/app/website/blog - ./website/core:/app/website/core - ./website/i18n:/app/website/i18n - ./website/pages:/app/website/pages - ./website/static:/app/website/static - ./website/sidebars.json:/app/website/sidebars.json - ./website/siteConfig.js:/app/website/siteConfig.js working_dir: /app/website ================================================ FILE: docs/browserstack.md ================================================ --- id: browserstack title: Browserstack integration --- ## Browserstack project configuration 1. Create a new account in Browserstack: https://www.browserstack.com/ 2. Login and visit the website https://www.browserstack.com/accounts/settings 3. Scroll down to the `Automation` section and copy: - `Username` - `Access Key` 4. Add a new `browserstack` section to the `kakunin.conf.js` file in the repository. This is an example of a configuration for IE8 on Windows 7. ```javascript browserstack: { seleniumAddress: 'http://hub-cloud.browserstack.com/wd/hub', defaultPort: 45691, capabilities: { 'browserstack.user': 'example-user', 'browserstack.key': 'example-key', 'browserstack.local': true, nativeEvents: true, 'browserstack.ie.driver': '3.14.0', 'browserstack.selenium_version': '3.14.0', browserName: 'IE', browser_version: '8.0', } }, ``` 5. Set `'browserstack.user'` to the `Username` value that you copied from the `Automation` section 6. Set `'browserstack.key'` to the `Access Key` value that you copied from the `Automation` section 7. Visit the link https://www.browserstack.com/automate/capabilities if you want to find more capabilities for your project!. ## Run tests in Browserstack Runs the application with the capabilities set in `kakunin.conf.js` file through the command line: - `npm run kakunin -- --browserstack` Keep in mind that all capabilities that you set via CLI will be ignored! For example, `npm run kakunin -- --safari --browserstack` will ignore the `safari` part. Only `--browserstack` matters in case of running tests in Browserstack. ## Example kakunin.conf.js configuration file This is an example configuration for Internet Explorer 8 on Windows 7. ```javascript module.exports = { browserWidth: 1600, browserHeight: 900, timeout: 60, elementsVisibilityTimeout: 5, waitForPageTimeout: 5, downloadTimeout: 30, reports: '/reports', downloads: '/downloads', data: '/data', features: ['/features'], pages: ['/pages'], matchers: ['/matchers'], generators: ['/generators'], form_handlers: ['/form_handlers'], step_definitions: ['/step_definitions'], comparators: ['/comparators'], dictionaries: ['/dictionaries'], transformers: ['/transformers'], regexes: ['/regexes'], hooks: ['/hooks'], clearEmailInboxBeforeTests: false, clearCookiesAfterScenario: true, clearLocalStorageAfterScenario: true, email: null, headless: true, noGpu: true, type: 'otherWeb', baseUrl: 'http://localhost:8080', apiUrl: 'http://localhost:8080/', browserstack: { seleniumAddress: 'http://hub-cloud.browserstack.com/wd/hub', defaultPort: 45691, capabilities: { 'browserstack.user': 'example-user', 'browserstack.key': 'example-key', 'browserstack.local': true, nativeEvents: true, 'browserstack.ie.driver': '3.14.0', 'browserstack.selenium_version': '3.14.0', browserName: 'IE', browser_version: '8.0', } }, }; ``` ================================================ FILE: docs/configuration.md ================================================ --- id: configuration title: Configuration --- ## Kakunin config ``` module.exports = { "browserWidth": 1600, "browserHeight": 900, "timeout": 60, "maxEmailRepeats": 5, "intervalEmail": 5, "elementsVisibilityTimeout": 5, "waitForPageTimeout": 5, "downloadTimeout": 30, "reports": "/reports", "downloads": "/downloads", "data": "/data", "features": [ "/features" ], "pages": [ "/pages" ], "matchers": [ "/matchers" ], "generators": [ "/generators" ], "form_handlers": [ "/form_handlers" ], "step_definitions": [ "/step_definitions" ], "comparators": [ "/comparators" ], "dictionaries": [ "/dictionaries" ], "transformers": [ "/transformers" ], "regexes": [ "/regexes" ], "hooks": [ "/hooks" ], "clearEmailInboxBeforeTests": false, "clearCookiesAfterScenario": true, "clearLocalStorageAfterScenario": true, "email": null, "headless": false, "noGpu": false, "type": "otherWeb", "baseUrl": "http://localhost:8080", "accounts": { "someAccount": { "accounts": [ { "email": "", "password": "" } ] } } } ``` ## Configuration options `browserWidth` - width of browser window `default: 1600` `browserheight` - height of browser window `default: 900` `timeout` - global timeout for a single step execution in seconds `default: 60` `maxEmailRepeats` - maximum email repeats to catch email used in the email step `intervalEmail` - interval for email checking step `default: 5` in seconds `elementsVisibilityTimeout` - maximum wait timeout for element visibility `default: 5` seconds `waitForPageTimeout` - maximum wait timeout for page visibility `default: 5` seconds `downloadTimeout` - maximum wait timeout for file to be downloaded `default: 30` seconds `emails` - array of paths to store emails related custom code `reports` - path to store reports `downloads` - path to store downloaded files `data` - path to store test related files (for example files to be downloaded) `feature` - array of paths to store features `pages` - array of paths to store page objects `matchers` - array of paths to store custom matchers `generators` - array of paths to store custom generators `form_handlers` - array of paths to store custom form handlers `step_definitions` - array of paths to store custom steps `comparators` - array of paths to store custom comparators `dictionaries` - array of paths to store custom dictionaries `transformers` - array of paths to store custom transformers `regexes` - array of paths to store custom regexes `hooks` - array of paths to store custom hooks `clearEmailInboxBeforeTests` - flag to active clearing email inbox before tests are executed `default: false | true for apps with email checking functionality activated ` `clearCookiesAfterScenario` - flag to activate clearing cookies after every scenario `default: true` `clearLocalStorageAfterScenario` - flag to activate clearing local storage after every scenario `default: true` `email` - email configuration `default: null` for mailtrap email checking system: ```javascript "type": "mailtrap", "config": { "apiKey": "your-mailtrap-api-key", "inboxId": "your-mailtrap-inbox", "url": "https://mailtrap.io/api/v1" } ``` for custom email checking system only type is required: ``` "type": "custom-type" ``` `headless` - flag to activate chrome headless browser `default: false`. Keep in mind that CLI command `-- --headless=false/true` has higher priority than the config file. `noGpu` - flag to activate cpu only mode `default: false` `type` - type of application either `ng1 | ng2 | otherWeb` `baseUrl` - url of tested application `accounts` - object to store accounts information. This is bound to `userProvider` and allows to use advanced email checking options like recipient checking. ```javascript "someAccount": { "accounts": [ { "email": "", "password": "" } ] } ``` ## Environment variables Kakunin uses a single `.env` file to load ENV variables. By default there is only one: `FIXTURES_RELOAD_HOST` - allows you to specify host for fixtures reloading. This allows you to use `@reloadFixtures` tag on scenarios that should restore database to starting state, before the test is running ================================================ FILE: docs/cross-browser.md ================================================ --- id: cross-browser title: Cross-browser testing --- ## To run tests with specified browser There is a possibility to run Kakunin in various browsers: - Google Chrome (by default) `npm run kakunin` or `npm run kakunin -- --chrome` - Firefox `npm run kakunin -- --firefox` - Safari `npm run kakunin -- --safari` - Internet Explorer `npm run kakunin -- --ie` (supported versions IE8, IE9, IE10, IE11) ## To run tests in different browsers at once There is a possibility to run more than one instance of WebDriver by giving an extra parameter to a command line: - `npm run kakunin --chrome --safari` Currently, there is a problem with running more than one instance of Firefox! ## Safari ### Run tests 1. Open Safari's preferences 2. Enable "Show Develop menu in menu bar" 3. Open "Develop" tab 4. Enable "Allow Remote Automation" ## Internet Explorer ### Configure the browser 1. Open Internet options and set: - IE browser zoom level to 100 procent - IE Security level: keep all of the tabs either checked / unchecked (Itnernet, Local internet, Trusted sites, Restricted sites) ### Troubleshooting Safari version 12.0: - drag & drop actions in Kakunin impossible (more details https://github.com/angular/protractor/issues/1526) ================================================ FILE: docs/docker.md ================================================ --- id: docker title: Docker --- # Docker for Kakunin tests This section explains how to run kakunin tests inside docker, below examples of Dockerfile and docker-compose.yml files let you build your first docker image and run tests. ## Dockerfile: This file is responsible for building the whole environment for our e2e tests. It will allow you to run tests on local and CI environments, by configuring and copying the whole project inside the container. Just simply place it inside your e2e project root. Below an example of Dockerfile ### Example of Dockerfile: ```bash # Downloading selenium image and setting privileges FROM selenium/standalone-chrome:3.14.0 USER root # Setting test directory WORKDIR /app # Install openjdk-8-jdk-headless RUN apt-get update -qqy \ && apt-get -qqy --no-install-recommends install \ xvfb \ openjdk-8-jdk-headless \ curl \ make \ && rm -rf /var/lib/apt/lists/* /var/cache/apt/* # Installing node 8 globally and setting paths RUN set -x \ && curl -sL https://deb.nodesource.com/setup_8.x | bash - \ && apt-get install -y \ nodejs \ && npm install -g npm@latest RUN PATH=/usr/bin/node:$PATH # Copy tests directory with ignored files from .dockerignore COPY --chown=seluser:seluser . . # Removing node_modules in case of existence or lack of .dockerignore and installing from package.json RUN rm -rf ./node_modules \ && npm install # Setting Xvfb RUN export DISPLAY=:99.0 USER seluser ``` ##docker-compose.yml Compose is a tool for defining and running multi-container Docker applications, which we use for running our tests. Running command: ``docker-compose up -d`` will start Dockerfile script, as a result, it builds the container. Running command: ``docker-compose build `` or ``docker-compose up --build`` will rebuild container, if there were made any changes. Running command: ``docker-compose run --rm e2e`` will start running tests inside the container Composition below allows you to run e2e tests inside the container and configure it locally or in CI environments. ### Example of docker-compose.yml: ```bash e2e: build: . working_dir: /app command: sh -c "Xvfb -ac :99 -screen 0 1280x1024x16 & npm run kakunin" ``` ### How to run step by step 1. Install docker (e.g Docker for Mac), 2. Create Dockerfile and docker-compose.yml in the root of your e2e project, 3. Run in command line `docker-compose up -d ` which will start docker and build image if it's not build 4. Run in command line `docker-compose run --rm e2e` to run your tests ================================================ FILE: docs/extending.md ================================================ --- id: extending title: Extending Kakunin --- Kakunin allows you to easily add a custom code in order to extend it's functionality. ## Internal services ### Regex builder Regex builder is a special builder for creating `RegExp` objects based on regexp name. Internally it has access to not only to all built-in regular expression files, but also custom ones specified by user. ```javascript const { regexBuilder } = require('kakunin'); const myRegex = regexBuilder.buildRegex('r:number'); //myRegex will contain RegExp object that matches regular expression under the name "number" in regexes file. ``` ### Variable store Variable store allows you to store and read some values to be used during given scenario. ```javascript const { variableStore } = require('kakunin'); variableStore.storeVariable('some-name', 'some-value'); const myValue = variableStore.getVariableValue('some-name'); //contains 'some-value' ``` ### User provider Kakunin comes with functionality that allows you to easily load credentials for a given account type - `UserProvider`. In `kakunin.conf.js` you can find a section `accounts`. The structure it has is very simple: ```json "accounts": { "someAccount": { "accounts": [ { "email": "", "password": "" } ] } } ``` `someAccount` - the name of accounts group `accounts` - an array of account credentials (in order to be able to check if a `currentUser` got an email, this has to have an `email` key, otherwise account can have any kind of properties) Use provider is accessible inside any kind of a step by calling `this.userProvider`. It comes with a single method: `this.userProvider.getUser(groupName)` - returns an account credentials for a given user group. It is a good practice to save a current user in `this.currentUser` variable for a email checking service. ## Adding custom code ### Custom step In order to add a custom step, you have to create inside of a directory specified as `step_definitions` in kakunin configuration file `default: /step_definitions`. We're using `cucumber-js 4.X` so in order to add custom step you have to use `defineSupportCode` method like this: ```javascript const { defineSupportCode } = require('kakunin'); defineSupportCode(({ When }) => { When(/^I use kakunin$/, function() { expect(true).to.equal(true); }); }); ``` ### Page objects Kakunin comes with some built-in page objects, that should be used as a base for your page objects. In order to create a custom one, create a file inside the `pages` directory and extend the `BasePage` from kakunin package. ```javascript const { BasePage } = require('kakunin'); class MyPageObject extends BasePage { constructor() { this.myElement = element(by.css('.some-elemnt')); } } module.exports = MyPageObject; ``` ### Matchers Matchers are used to compare if given value is matching our expectation. For example if a value in table is a number. You can add your own matcher as below: ```javascript const { matchers } = require('kakunin'); class MyMatcher { isSatisfiedBy(prefix, name) { return prefix === 'm:' && name === 'pending'; } match(protractorElement, matcherName) { return protractorElement.getText().then((value) => { if (value === 'pending') { return true; } return Promise.reject(`Matcher "MyMatcher" could not match value on element "${protractorElement.locator()}". Expected: "pending", given: "${value}"`); }); } } matchers.addMatcher(new MyMatcher()); ``` ### Dictionaries Dictionaries allows you to present complicated values in much more readable way. For example if an element must be in a form of IRI `/some-resource/123-123-123-23` and you wish to use `pending-resource` as it's alias. You can add your own dictionary: ```javascript const { dictionaries } = require('kakunin'); const { BaseDictionary } = require('kakunin'); class TestDictionary extends BaseDictionary { constructor() { super('name-of-dictionary', { 'pending-resource': '/some-resource/123-123-123-23', 'test-value': 'some other value' }); } } dictionaries.addDictionary(new TestDictionary()); ``` ### Generators Generators allows you to create random values You can add your own generator: ```javascript const { generators } = require('kakunin'); class MyGeneerator{ isSatisfiedBy(name) { return name === 'my-generator'; } generate(params) { return Promise.resolve('some-random-value'); } } generators.addGenerator(new MyGeneerator()); ``` ### Comparators Comparators allows you to check if a set of values has an expected order You can add your own comparators: ```javascript const { comparators } = require('kakunin'); class MyComparator { isSatisfiedBy(values) { for(let i=0; iKeep in mind that CLI has greater prority than the cofig file (overides settings on runtime). ================================================ FILE: docs/hooks.md ================================================ --- id: hooks title: Hooks --- # Hooks for Kakunin tests This section explains how to add priority hooks for kakunin tests based on the built-in adapter. Hooks allow you to perform actions before and after scenario. For example, it lets you clear all files from the downloads folder. ## How to add hook with priority: ##### initializeHook() - this method is provided to execute hook logic. ##### getPriority() - this method returns numeric value and then it's sorted in order. ```text Remember that new Hook must contain these 2 methods to fulfill interface. ``` After your hook is ready to use method `hookHandlers.addHook(Hook object)` ### Example of example.hook.js: ```typescript const { hookHandlers, Before } = require('kakunin'); class ExampleHook { initializeHook() { Before(() => { console.log('This hook is going to be 5th in order'); }); } getPriority() { return 5; } } hookHandlers.addHook(new ExampleHook()); ``` ### Build in hooks: #### Clear download: - Clears download folder before or/and after scenario. To use them add `@downloadClearBefore` or `@downloadClearAfter` tag. Its priority is set to 1. #### Reload fixtures: - allows you to reload fixtures before the desired scenario, via URL provided in `.env` file. Its priority is set to 2. #### Take a screenshot and clear variables: - These hooks are used by kakunin mechanism to clear variable store and take screenshots after scenarios. Their priority is set to 1. ================================================ FILE: docs/how-it-works.md ================================================ --- id: how-it-works title: How it works --- Kakunin is built with `no-js` experience in mind. Because of that you're able to test even complicated apps just by knowing Kakunin (Gherkin) steps and a few good practices. ## Concepts Kakunin uses `cucumber-js` internally, because of that all tests (or rather scenarios) are using `Gherkin` as a "programming" language. A simple scenario could look like this: ```gherkin Feature: Scenario: Display user profile for logged user Given I am logged in as a "user" When the "dashboard" page is displayed And I click the "profileButton" element Then the "myProfile" page is displayed And the "myName" element is visible ``` This is how most of Kakunin test scenarios look like. There are a few concepts to be explained. ## Page objects Page object is a code representation of a page displayed in browser. Kakunin has built-in `BasePage` page object, that you should extend. Page object contains information about page url, its elements, locators, but can also have some custom methods if necessary. A very simple example of Kakunin's Page Object could look like the following: ```javascript const { BasePage } = require('kakunin'); class DashboardPage extends BasePage { constructor() { super(); this.url = '/dashboard'; } } module.exports = DashboardPage; ``` As you can see a basic Page Object must extend one of the Kakunin's Objects and needs to have url field defined (`this.url`). This code should be saved inside `pages` directory in a file with `js` extension. Note that a file name is very important, because we're going to use it as parameter for steps. For example, the following step: ```gherkin When the "dashboard" page is displayed ``` expects that there is a file named `dashboard.js` inside the `pages` directory. Every step that we are using is somehow connected to an object called `currentPage`. This object value is set to a page object that we expect to be on. This is done by two kinds of steps: * `Then the "dashboard" page is displayed` - this one checks if current url in browser is the same as the one inside Page Object and changes a value of the `currentPage` field to this page object * `When I visit the "dashboard" page` - this one goes to the url specified in Page Object and attaches the Page Object to the `currentPage` field as above This concept is a very simple and allows you to easily debug the framework. You can be sure that each subsequent step that declared below the ones above will be executed in context of a page object specified in those methods. For example, having the following code: ```gherkin Feature: Scenario: Display user profile for logged user Given I am logged in as a "user" When the "dashboard" page is displayed And I click the "profileButton" element Then the "myProfile" page is displayed And the "myName" element is visible ``` The step named `And I click the "profileButton" element` is executed in context of `dashboard` Page Object, thus we can assume that `profileButton` should be defined inside the `pages/dashboard.js` file. At the same time the step `And the "myName" element is visible` is executed in context of `myProfile`, so `myName` should be defined in `pages/myProfile.js` file. ## Elements and locators The second concept that you have to understand are elements and locators. Every element that you see on website can be represented as a element inside the page object. This allows us to use it as a parameter for a step, as we did in: `And the "myName" element is visible`. Defining elements is very simple. Let's say we have such page object: ``` const { BasePage } = require('kakunin'); class DashboardPage extends BasePage { constructor() { super(); this.url = '/dashboard'; } } module.exports = DashboardPage; ``` Elements should be defined inside `constructor` method. Let's add element for `myName`: ``` const { BasePage } = require('kakunin'); class DashboardPage extends BasePage { constructor() { super(); this.url = '/dashboard'; this.myName = element(by.css('.myName')); } } module.exports = DashboardPage; ``` As you see we added a single line `this.myName = element(by.css('.myName'));`. `by.css('.myName')` - is a locator, this is a standard protractor syntax, you can read more on protractors documentation By joining `element` method with a locator, we created element to be used by our steps. ## Compare URLs examples: | Page Object URL | Current Browser URL | Base URL - config file | Results | | ----------------------------------------------------------- | ------------------------------------------------- | ------------------------- | --------- | | http://localhost:8080/incorrect-data | http://localhost:8080/tabular-data | https://example-url.com | FALSE | | http://localhost:8080/incorrect-data/ | http://localhost:8080/tabular-data | https://example-url.com | FALSE | | http://google/incorrect-data | http://localhost:8080/tabular-data | https://example-url.com | FALSE | | http://google/tabular-data | http://localhost:8080/tabular-data | https://example-url.com | FALSE | | http://google/incorrect-data/ | http://localhost:8080/tabular-data | https://example-url.com | FALSE | | /incorrect-data | http://website.com/tabular-data | https://example-url.com | FALSE | | /incorrect-data/ | http://website.com/tabular-data | http://incorrect.com | FALSE | | http://localhost:8080/tabular-data | http://localhost:8080/tabular-data | https://example-url.com | TRUE | | http://localhost:8080/tabular-data/ | http://localhost:8080/tabular-data | http://localhost:8080 | TRUE | | /tabular-data | http://localhost:8080/tabular-data | http://localhost:8080 | TRUE | | /tabular-data/ | http://localhost:8080/tabular-data | http://localhost:8080 | TRUE | | /tabular-data | http://localhost:8080/tabular-data | https://google.pl | FALSE | | /tabular-data/ | http://localhost:8080/tabular-data | https://google.pl | FALSE | | / | https://google.pl/new | https://google.pl | FALSE | | | https://google.pl/new | https://google.pl | FALSE | | | http://localhost:8080 | http://localhost:8080 | TRUE | | / | https://google.pl | https://google.com | FALSE | | https://google.com/:example/:name | https://google.com/example/janek | https://example-url.com | TRUE | | https://google.com/:name | https://google.com/janek | https://example-url.com | TRUE | | https://google.com/account/:username/settings/display | https://google.com/account/janek/settings/display | https://example-url.com | TRUE | | /account/settings/:userType | https://incorrect-host/account/settings/admin | https://google.com | FALSE | | /account/settings/:userType/something | https://incorrect-host/account/settings/admin | https://example-url.com | FALSE | | https://incorrect-host/account/settings/:userType/something | https://incorrect-host/account/settings/admin | https://example-url.com | FALSE | | /account/settings/:userType | https://google.com/account/settings/user | https://google.com | TRUE | ================================================ FILE: docs/index.md ================================================ --- id: index title: Getting started --- ## About Kakunin Kakunin is a Protractor extension created by The Software House sp. z o.o. and Takamol Holding. It allows you to write e2e test scenarios with a help of Gherkin language and JavaScript for all kind of applications - Angular, React and others. ## Installation In order to install Kakunin you have to make sure that you have installed: ```text node.js - v7.8.0 min JDK Chrome ``` Create directory for your project ```bash mkdir my_project ``` Go to project directory ```bash cd my_project ``` Initialize JavaScript project ```bash npm init ``` Install dependencies ```bash npm install cross-env kakunin --save ``` Inside `package.json` file; add new script in `scripts` section: ```json "kakunin": "cross-env NODE_ENV=prod kakunin" ``` ## Configuration * Create kakunin project ```bash npm run kakunin init ``` The above command will run Kakunin's init script. * Answer what kind of app you're going to test (`default: AngularJS`) * Enter URL where your tested app will be running (`default: http://localhost:3000`) * Choose if you plan to use some emails checking service (`default: none`) Also, there is a possibility to answer these question by a command line. ```text npm run kakunin init -- --baseUrl https://google.com --type otherWeb --emailType none ``` Available parameters: `baseUrl`, `type`, `emailType`, `emailApiKey`, `emailInboxId`. You will not be asked about question that you already answered by a command. After the init process, a project files should be automatically created in your directory. This is an example of a console output after the init process is completed: ```text Created file at path /Users/example-user/projects/test/kakunin.conf.js Created directory at path /Users//TSHProjects/test/reports Created directory at path /Users//TSHProjects/test/reports/report Created directory at path /Users//TSHProjects/test/reports/report/features Created directory at path /Users//TSHProjects/test/reports/performance Created directory at path /Users//TSHProjects/test/downloads Created directory at path /Users/example-user/projects/test/data Created directory at path /Users/example-user/projects/test/features Created directory at path /Users/example-user/projects/test/pages Created directory at path /Users/example-user/projects/test/matchers Created directory at path /Users/example-user/projects/test/generators Created directory at path /Users/example-user/projects/test/form_handlers Created directory at path /Users/example-user/projects/test/step_definitions Created directory at path /Users/example-user/projects/test/comparators Created directory at path /Users/example-user/projects/test/dictionaries Created directory at path /Users/example-user/projects/test/regexes Created directory at path /Users/example-user/projects/test/hooks Created directory at path /Users/example-user/projects/test/transformers Created directory at path /Users/example-user/projects/test/emails Created file at path /Users/example-user/projects/test/downloads/.gitkeep Created file at path /Users/example-user/projects/test/reports/report/.gitkeep Created file at path /Users/example-user/projects/test/reports/report/features/.gitkeep Created file at path /Users/example-user/projects/test/reports/performance/.gitkeep Created file at path /Users/example-user/projects/test/features/example.feature Created file at path /Users/example-user/projects/test/pages/page.js Created file at path /Users/example-user/projects/test/matchers/matcher.js Created file at path /Users/example-user/projects/test/generators/generator.js Created file at path /Users/example-user/projects/test/step_definitions/steps.js Created file at path /Users/example-user/projects/test/regexes/regex.js Created file at path /Users/example-user/projects/test/hooks/hook.js ``` And you're set! Now you can run the tests using Kakunin: ```bash npm run kakunin ``` ## Commands * Create a new project by answering few simple questions (you can pass additional parameter to enter advanced mode where you can configure all Kakunin options by yourself) ```bash npm run kakunin init [-- --advanced] ``` * Run test scenarios ```bash npm run kakunin ``` * Run only scenarios tagged by `@someTag` ```bash npm run kakunin -- --tags @someTag ``` * Run only scenarios tagged by `@someTag` and `@otherTag` at the same time ```bash npm run kakunin -- --tags "@someTag and @otherTag" ``` * Run only scenarios tagged by `@someTag` or `@otherTag` ```bash npm run kakunin -- --tags "@someTag or @otherTag" ``` * Run only scenarios not tagged by `@someTag` ```bash npm run kakunin -- --tags "not @someTag" ``` ## Troubleshooting & Tips In order to make cucumber steps autosuggestion work properly in JetBrains tools, make sure your project is `ECMAScript 6` compatible and you have `cucumberjs` plugin installed. Due to non-resolved issue in Jetbrains editors ([see here](https://youtrack.jetbrains.com/issue/WEB-11505)) we'll have to do one more step: Go to `step_definitions` directory ```bash cd step_definitions ``` Paste this code into terminal and restart your IDE: For Linux/MacOs: ```bash ln -s ../node_modules/kakunin/src/step_definitions/elements.ts kakunin-elements.ts ln -s ../node_modules/kakunin/src/step_definitions/debug.ts kakunin-debug.ts ln -s ../node_modules/kakunin/src/step_definitions/file.ts kakunin-file.ts ln -s ../node_modules/kakunin/src/step_definitions/form.ts kakunin-form.ts ln -s ../node_modules/kakunin/src/step_definitions/email.ts kakunin-email.ts ln -s ../node_modules/kakunin/src/step_definitions/generators.ts kakunin-generators.ts ln -s ../node_modules/kakunin/src/step_definitions/navigation.ts kakunin-navigation.ts ln -s ../node_modules/kakunin/src/step_definitions/performance.ts kakunin-performance.ts ``` For Windows 8+: (you have to do this as administrator) ```bash mklink kakunin-elements.ts ../node_modules/kakunin/src/step_definitions/elements.ts mklink kakunin-debug.ts ../node_modules/kakunin/src/step_definitions/debug.ts mklink kakunin-file.ts ../node_modules/kakunin/src/step_definitions/file.ts mklink kakunin-form.ts ../node_modules/kakunin/src/step_definitions/form.ts mklink kakunin-email.ts ../node_modules/kakunin/src/step_definitions/email.ts mklink kakunin-generators.ts ../node_modules/kakunin/src/step_definitions/generators.ts mklink kakunin-navigation.ts ../node_modules/kakunin/src/step_definitions/navigation.ts mklink kakunin-performance.ts ../node_modules/kakunin/src/step_definitions/performance.ts ``` Keep in mind that `mklink` is not available in older Windows distributions. This will create symlinks inside `step_definitions` directory and make `cucumberjs` plugin recognize kakunin built-in steps. ================================================ FILE: docs/matchers.md ================================================ --- id: matchers title: Matchers --- Matchers allows you to check if a element content matches your expectation. For example you can check if a value has a specified pattern or if a button is clickable. Using matcher is very straightforward, for example: `f:isClickable`. Matchers can be used in most of the steps related to checking content (with exception of checking form values). Kakunin comes with a set of built in matchers: ## Visibility matcher `f:isVisible` - checks if element is visible (must be in viewport and cannot be hidden behind any other element) ## Invisibility matcher `f:isNotVisible` - checks if element is not visible ## Present matcher `f:isPresent` - checks if element is in html code (does not have to be visible) ## Clickable matcher `f:isClickable` - checks if element is clickable ## Not clickable matcher `f:isNotClickable` - checks if element is not clickable ## Attribute matcher `attribute:attributeName:regexName` - allows to check if element has attribute with a name specified by `attributeName` and it has to have a format passing `regexName` For example, if there is an element: `

some value

` you can check if attribute is an number by running: `attribute:custom-attribute:number` ## Regex matcher `r:regexName` - allows you to run a `regexName` against a text value of element Regexes have to be specified inside `regex` directory or be a kakunin built ones: `notEmpty` - there must be a value `number` - must be a number You can add your own matchers. In order to do so please read `Extending Kakunin` section. ## Text matcher `t:text you are looking for` - allows you to check if an element contains a expected text ## Current date matcher `f:currentDate:{format}` - allows you to generate current date, `{format}` is optional, by default `DD-MM-YYYY` ================================================ FILE: docs/parallel-testing.md ================================================ --- id: parallel-testing title: Parallel testing --- There is a possibility to run tests in parallel. ## How to execute Use a command `npm run kakunin -- --parallel ` where `number of instances` is a number. Example: - `npm run kakunin -- --chrome --parallel 2` Keep in mind that the merged report is available in the `reports/report/index.html` file. text ## Specify pattern per each instance - `npm run kakunin -- --parallel --pattern --pattern ` Keep in mind that: - the number given in `parallel` must be equal to passed `patterns` - `` is a number of instances of the specified browser - `` is a pattern that is used to specify the list of specs that will be executed in each of the instances ----------------------------------------------------------------------------------- ## Troubleshooting 1. Running more than one instance in `Firefox` is not possible now (fix in-progress). ================================================ FILE: docs/performance-testing.md ================================================ --- id: performance-testing title: Performance testing --- Performance testing is possible thanks to `browsermob-proxy`. It saves all data from network tab (Google Chrome console) which is generated during the test. There is a possibility to compare `TTFB` value with a maximum given one. `TTFB` (Time to first byte) measures the duration from the client making an HTTP request to the first byte of a response being received by the client's browser. More details can be found in documentation - `Built-in steps` section. # What needs to be done? ## Get started 1. Download `browsermob-proxy` from `https://github.com/lightbody/browsermob-proxy` 2. Navigate in terminal to the catalog 3. Use following command to start the REST API ``` ./browsermob-proxy -port 8887 ``` ## Configuration 1. Add `browsermob-proxy` configuration to `kakunin.conf.js` You can use one of the following methods to configure browsermob-proxy: - `npm run kakunin init -- --advanced` and go through the process - or add it manually to the config file: ```javascript "browserMob": { "serverPort": 8887, "port": 8888, "host": "localhost" } ``` ## Run tests 1. `performance steps` must be used in the scenario where you are testing performance 2. Scenario must have a tag `@performance` 3. Run tests with special parameter: ``` npm run kakunin -- --performance ``` ## Results 1. `.har` files are saved in catalog `reports/performance/*.har` ================================================ FILE: docs/quickstart.md ================================================ --- id: quickstart title: Quick start --- As a quick demonstration of the framework let's test the [React variant of TodoMVC](http://todomvc.com/examples/react/#/) project. Of course other testing other frameworks is possible, you can try it by yourself! ## Install packages In order to install Kakunin you have to make sure that you have installed: ```text node.js - v7.8.0 min JDK Chrome ``` Create directory for your project and enter it ```bash $mkdir my_project cd my_project ``` Initialize JavaScript project ```bash npm init ``` Install dependencies ```bash npm install cross-env kakunin --save ``` Inside `package.json` file add new script in `scripts` section: ```js ... "scripts": { "kakunin": "cross-env NODE_ENV=prod kakunin" }, ... ``` ## Configure Kakunin Run initialization command ```bash npm run kakunin init ``` Answer literally few questions: ```text What kind of application would you like to test? : otherWeb What is base url? [http://localhost:3000]: http://todomvc.com What kind of email service would you like to use?: none ``` And you're set! Now let's write some test! ## Test the app Create a page object that will contain instructions on how to locate elements in the projects. Create a file `pages/main.js`: ```javascript const { BasePage } = require('kakunin'); class MainPage extends BasePage { constructor() { super(); // define the main url for the page this.url = '/examples/react/#/'; // whole form tag this.addTodoForm = $('.todoapp'); // input field this.todoInput = $('input.new-todo'); // list of currently added todos this.todos = $$('.todo-list .view'); this.todoLabel = by.css('label'); // first todo item in a list this.firstTodoItem = this.todos.get(0); } } module.exports = MainPage; ``` Now that we have prepared the locators, we can start writing our test. Let's test adding new todo item. Create a file named: `features/adding_todo.feature` with the following contents: ```gherkin Feature: Scenario: Adding todo Given I visit the "main" page And I wait for "visibilityOf" of the "addTodoForm" element And the "addTodoForm" element is visible When I fill the "addTodoForm" form with: | todoInput | My new todo | And I press the "enter" key Then there are "equal 1" "todos" elements ``` And that's it! All you have to do now is to run the test and watch the magic happens ;) ```bash npm run kakunin ``` The tests may run quite fast so you might not been able to see that it really works as expected. To check if the todo items has been really added to the list, let's use a simple hack - let's pause the running test right after the todo has been added. To do that, let's upgrade our Scenario. Update the file: ```gherkin Feature: Scenario: Adding todo Given I visit the "main" page And I wait for "visibilityOf" of the "addTodoForm" element And the "addTodoForm" element is visible When I fill the "addTodoForm" form with: | todoInput | My new todo | And I wait for "1" seconds And I press the "enter" key When I fill the "addTodoForm" form with: | todoInput | Another todo item! | And I wait for "1" seconds And I press the "enter" key Then there are "equal 2" "todos" elements Then I wait for "5" seconds ``` As you can see, we've added 1 new step that waits for a second before "pressing" the `enter` key. We've also added a second todo item with a short pause at the end of the test so you can see the changes. If you want to see what can we do more with the TodoMVC project, take a look at the `example` dir, where you'll find a complete set of test for the project. ================================================ FILE: docs/steps-debug.md ================================================ --- id: steps-debug title: Debug --- # Steps for debugging application: ## `I pause` Pauses tests execution and allows to continue manually by pressing combination of `ctrl+c` inside terminal. --- ## `I wait for ":seconds" seconds` Waits with execution of next step for an amount provided by parameter `:seconds`. --- ## `I start performance monitor mode` It starts performance monitor mode. Keep in mind that REST API must be started on the port which must configured in `kakunin.conf.js` - `serverPort: 8887`. More details can be found in documentation file `performance-testing.md`. --- ## `I save performance report file as "fileName"` It saves `.har` file with a name `fileName` in `reports/performance` catalog. For example: `exampleReport-1511470954552.har` Data is generated during the test - network tab in Chrome Chrome console. Keep in mind: * `I start performance monitor mode` must be used before this step * `browserMob.port` must be configured in `kakunin.conf.js` * `browserMob.host` must be configured in `kakunin.conf.js` More details can be found in documentation file `performance-testing.md`. --- ## `the requests should take a maximum of "maxTiming" milliseconds` It compares every `TTFB` timing value from previously saved `.har` report with a `maxTiming` value. Slow requests are listed in your terminal in red colour. Keep in mind that `I start performance monitor mode` and `I save performance report file as "fileName"` steps must be executed before this one! --- ================================================ FILE: docs/steps-elements.md ================================================ --- id: steps-elements title: Elements --- # Steps used to interact with elements: ## `I infinitely scroll to the ":elementName" element` Allows to scroll through infinite scroll mechanism. The `:elementName` is a name of a selector for loading trigger. --- ## `I wait for ":expectedConditionName" of the ":elementName" element` Waits till element `:elementName` from `this.currentPage` meets criteria specified by `:expectedConditionName`. You can use any of the Protractor's expected condition: * `visibilityOf` * `invisibilityOf` etc. Read more in Protractor's API documentation. --- ## `I wait for the ":elementName" element to disappear` Waits till element `:elementName` disappears. --- ## `I scroll to the ":elementName" element` Scrolls to element `:elementName` of `this.currentPage`. The element will be on bottom of the page. --- ## `I infinitely scroll to the ":elementName" element` Allows to scroll till `:elementName` is visible. Useful for infinite scrolling functionality. --- ## `I press the ":keyName" key` Performs a key press operation on `:keyName` key. --- ## `I click the ":elementName" element` Performs a click action on element `:elementName` from `this.currentPage' The child element must be specified by `:elementName` and must be available in `this.currentPage`. --- ## `I store the ":elementName" element text as ":variableName" variable` Stores the text from element `:elementName` of `this.currentPage` under the `:variableName` so you can use it later. --- ## `I update the ":elementName" element text as ":variableName" variable` Updates the variable `:variableName` value by value from element `:elementName` of `this.currentPage`. --- ## `I store the ":elementName" element text matched by ":matchingRegex" as ":variableName" variable` Stores the part of the element `:elementName` text, that matches the `:matchingRegex` under the `:variableName` for later use. --- ## `the ":elementName"" element is visible` Checks if element `:elementName` is visible and clickable --- ## `the ":elementName"" element is not visible` Checks if element `:elementName` is available in HTML DOM but is not visible and clickable --- ## `the ":elementName" element is disabled` Checks if element is disabled --- ## `I store table ":tableRow" rows as ":variableName" with columns:` Allows to store a row specified columns from a table `:tableRow` and save it under `:variableName` as an array of objects. This step requires a table of columns elements, for example: ```gherkin I store table "someRow" rows as "someVariable" with columns: | firstName | | lastName | | id | ``` In order to make it work there must be not only array element `this.someRow = $$('.rows')` in `this.currentPage`, but also element `this.firstName = $('.firstName');` and so on. The result of this step is an array of: ```javascript [ [ 'firsRowFirstNameValue', 'firsRowLastNameValue' 'firsRowIdValue' ] ... ] ``` --- ## `there are following elements in table ":elementName":` Allows to check if a child elements of `:elementName` have a specified content. This steps allows you to specify an array of child elements that will be checked against expected values. For example: ```gherkin there are following elements in table "myTable": | id | firstName | lastName | | t:1 | t:Adam | t:Doe | | t:2 | t:John | t:Doe | ``` First row must specify columns elements. Starting from second row we must provide a matchers for each row that must be displayed. This step checks exact match, so if the table has 5 rows, there must be a 5 rows in this table. We can specify only a set of columns (for example if a table has 5 columns, we can specify only 1). --- ## `there are "numberExpression" following elements for element ":elementName":` Allows to check if a child elements of `:elementName` have a specified content. Element should be an array, for example: ```html
1
2
``` for this case the `:elementName` should be specified as `$$('table tr')`. Allows to check if a number of elements is the one that we expect. `numberExpression` is a supported expression from `chai.js` library: * `equal N` where N is a number * `at least N` where N is a number * `above N` where N is a number * `below N` where N is a number * `within N M` where N and M are a numbers and so on. You can check expressions on `chai.js` API dock for BDD. This step requires an array of elements to be checked. For example: ```gherkin there are "equal 5" following elements for element "myList": | viewButton | f:isClickable | | id | r:idRegex | ``` The child elements must be an elements, for example `this.viewButton = $('button.viewButton');`. You can use all kind of matchers here. --- ## `there are ":elementName" dropdown list elements with following options:` Allows to check if there is exact match to options provided in table for option selector. ```html ``` For example: ```gherkin there are "personOption" dropdown list elements with following options: | Person 1 | | Person 2 | | Person 3 | | Person 4 | ``` The element must be for example: `this.personOption = this.personForm.$$('option');`. --- ## `there is element ":elementName" with value ":matcher"` Allows to check if `:elementName` has a value that matches the `:matcher`. --- ## `there is element ":elementName" containing ":matcher" text` Allows to check if `:elementName` contains a text that matches the `:matcher`. --- ## `there is element ":elementName" matching ":matcher" matcher` Allows to check if `:elementName` matches the given type of `:matcher`. For example: ```gherkin there is element "button" matching "isClickable" matcher ``` --- ## `there is element ":elementName" with regex ":matcher"` Allows to check if `:elementName` matches given type of regex. For example: ```gherkin there is element "input" with regex "notEmpty" ``` --- ## `there is no element ":elementName" with value ":matcherName"` Allows to check if there is no `:elementName` that matches the `:matcher`. --- ## `there is no element ":elementName" containing ":matcher" text` Allows to check if `:elementName` doesn't contain a text that matches the `:matcher`. --- ## `there is no element ":elementName" matching ":matcher" matcher` Allows to check if `:elementName` is not matching the given type of `:matcher`. --- ## `there is no element ":elementName" with regex ":matcher"` Allows to check if `:elementName` is not matching given type of regex. --- ## `there are "numberExpression" ":elementName" elements` Allows to check if a number of `:elementName` elements is the same as we expect. `numberExpression` is a supported expression from `chai.js` library: * `equal N` where N is a number * `at least N` where N is a number * `above N` where N is a number * `below N` where N is a number * `within N M` where N and M are a numbers and so on. You can check expressions on `chai.js` API dock for BDD. `:elementName` should be specified as an array, for example: ```html
1
2
``` for this case the `:elementName` should be specified as `$$('table tr')`. --- ## `every ":elementName" element should have the same value for element ":columnElementName"` Allows to check if every row defined by `:elementName` has the same value for a column `:columnElementName`. `:elementName` must be an array of elements `:columnElementName` must be an element, for example: ```html
1
1
``` for this case the `:elementName` should be specified as `$$('table tr')` and we can specify column element `this.myColumn = $('td');`. This allows us to write: `every "myElement" element should have the same value for element "myColumn"` --- ## `the element ":elementName" should have an item with values:` Allows to check if any of the child elements of `:elementName` have a specified content (one matching element is enough). Element should be an array, for example: ```html
1
2
``` for this case the `:elementName` should be specified as `$$('table tr')`. This step requires an array of elements to be checked. For example: ```gherkin the element "myList" should have an item with values: | id | t:1 | ``` The child elements must be an elements, for example `this.id = $('td');`. You can use all kind of matchers here. --- ## `the element ":elementName" should not have an item with values:` Allows to check if the child elements of `:elementName` have a different content than that given in the table. Element should be an array, for example: ```html
1
2
``` for this case the `:elementName` should be specified as `$$('table tr')`. This step requires an array of elements to be checked. For example: ```gherkin the element "myList" should have an item with values: | id | t:does-not-exist | ``` The child elements must be an elements, for example `this.id = $('td');`. You can use all kind of matchers here. --- ## `I drag ":elementDrag" element and drop over ":elementDrop" element` Clicks on `:elementDrag` and moves it onto `:elementDrop` while left mouse button is pressed, and then release it. Note: This step is not working on HTML5! --- ================================================ FILE: docs/steps-files.md ================================================ --- id: steps-files title: Files --- # Steps used to interact with files: ## `the file ":fileName" should be downloaded` Checks if a file with name `:fileName` was downloaded. This step does not support matchers or regular expressions, so the name must be exact match. However you can use variable store here. Let's assume there is a variable `myFile` with a value `super-file` in variable store. You can write `the file "v:myFile.zip" should be downloaded` to check if a file `super-file.zip` was downloaded. --- ## `the file ":fileName" contains table data stored under ":variableName" variable` This step allows you to compare an xls/xlsx file `:fileName` with an existing data stored under `:variableName` variable. The data under `:variableName` must be an array of objects representing each row of file. --- ================================================ FILE: docs/steps-forms.md ================================================ --- id: steps-forms title: Forms --- # Steps used to fill forms: ## `I fill the ":formName" form with:` Allows to fill the form with the name `:formName` and values provided as an array of inputs and values. The element with name `:formName` must be defined inside the `currentPage` page object. Input and values should be provided as an array for example: ```gherkin I fill the "myForm" form with: | inputElement | value to be typed into field | | textareaElement | value to be typed into textarea | | radioElement | radio value to be selected | | checkboxElement | checkbox label value to be selected | ``` By default we support all basic HTML field types (text inputs, checkboxes, radios, selects, files and textareas) In order to use the default handlers the elements you use as input must follow pattern: For inputs: `this.element = $('input')` - element should point at input you want to fill For textareas: `this.element = $('textarea')` - element should point at textarea you want to fill For file input: `this.element = $('input')` - element should point at input you want to fill and value should a filename of file from `data` directory For selects: `this.element = $('select')` - element should point at select and value should be an value of expected option For radios: `this.element = $$('radio[name="name-of-radio"]')` - element should be an array of all radio input of given name and value should be an value of radio you wish to select For checkboxes: Checkbox should have a html like: ```html ``` `this.element = $$('checkbox[name="name-of-radio"]')` - element should be an array of all checkboxes of given name and value should be a text from label of checkbox you want to fill You can use all kind of transformers to as a values for fields. --- ## `the ":formName" form is filled with:` The same as `I fill the ":formName" form with:` but allows to check if a form is filled with a given set of values. You can use all kind of transformers to as a expected values for fields. The only difference is for file fields. You cannot check uploaded files just like that, however we prepared a special type of handler that allow to check for some information related to a specific file. Let's assume that after upload we display an information with a file name of a uploaded file. You can use a special handler that requires to set a element with a postfix `Uploaded`. This will check if a value of that element is the same as you expected. For example you can write a step like this: ```gherkin the "myform" form is filled with: | myFileUploaded | file.txt | ``` Keep in mind that the element name must end with `Uploaded` for example: `this.myFileUploaded = $('p.some-file')` --- ## `the error messages should be displayed:` Allows you to specify the error messages that should be displayed for a specific elements. This step requires an array of format: ```gherkin the error messages should be displayed: | myElement | my error message | ``` You can use dictionaries in this step as follows: ```gherkin the error messages should be displayed: | myElement | d:dictionaryName:dictionaryKey | ``` --- ================================================ FILE: docs/steps-generators.md ================================================ --- id: steps-generators title: Generators --- # Steps used to generate values: ## `I generate random ":generator:param:param" as ":variableName"` Allows to generate a random value using the generator specified by `:generator:param:param`. The generator must be defined inside the any of the `generators` directories specified in `kakunin.conf.js` file `default: generators`. If the generator exists, then the value will be saved under the `:variableName` and can be accessed by: * steps using variable store * by calling `variableStore.getVariableValue(:variableName)` * by using variable store transformer on supported steps `v:variableName` --- ================================================ FILE: docs/steps-navigation.md ================================================ --- id: steps-navigation title: Navigation --- # Steps used for navigation on page: ## `I visit the ":pageFileName" page` Visits the url of the page object with `:pageFileName` name. In order to make it work we create a page object file with a name of `:pageFileName`. For example in case of: `I visit the "myPage" page` there should be a file `myPage.js` inside the `pages` directory. If we have a page object with a name `somePageObject.js` defined inside `pages` directory then: `Given I visit the "somePageObject" page` will set `this.currentPage` variable to `somePageObject` page and we should end up on `somePageObject` url. --- ## `I visit the ":pageFileName" page with parameters:` The same as `I visit the ":pageFileName" page` except allows to pass url parameters. If url of `myPage` is defined as `this.url = /orders/:orderId/products/:productId` then we can use this step to visit this page by: ```gherkin I visit the "myPage" page with parameters: | orderId | 1 | | productId | 2 | ``` this will result in visiting the `/orders/1/product/2` page. --- ## `the ":pageFileName" page is displayed` Checks if current browser url matches url of `pageFileName` page object. If the url matches expected pattern then `this.currentPage` variable is set to `pageFileName` page object. --- ================================================ FILE: docs/steps-rest.md ================================================ --- id: steps-rest title: Rest api --- # Steps used for testing REST api: In order to configure url for api, please change `apiUrl` field in `functional-tests/kakunin.conf.js`. This will set url of application api. ## `I send ":methodName" request on ":endpoint" endpoint` Sends to the given request method to given website endpoint. For example, in case of GET request for /posts endpoint it should look like: ```gherkin I send "GET" request on "posts" endpoint ``` ## `^I send ":methodName" request on ":endpoint" endpoint with JSON body:` Sends request method to website endpoint requiring JSON body. ```gherkin I send "POST" request on "posts" endpoint with JSON body: """ { "title": "user", "body": "test" } """ ``` ## `^I send "methodName" request on ":endpoint" endpoint using form data:` Sends request method to website endpoint using form data. ```gherkin I send "POST" request on "posts" endpoint using form data: | title | user | ``` ## `the response code should be ":statusCode"` Verifies if the server response code has match to given one. ## `the response should exact match to body:` Verifies if the server response body has exact match to given one. ```gherkin the response should exact match to body: """ { "userId": 1, "id": 1, "title": "user", "body": "test" } """ ``` ## `the response should match JSON schema:` Verifies if the server response body has exact match to given JSON schema. ```gherkin the response should exact match JSON schema: """ { "title": "Test schema", "type": "object", "properties": { "id": { "type": "integer" } }, "required": ["id"] } """ ``` ## `I set request headers:` Sets the request headers to given one till creating new request. ```gherkin I set request headers: | Content-type | application/json | | accept | */* | ``` ================================================ FILE: docs/testing-rest-api.md ================================================ --- id: testing-rest-api title: REST API examples --- # Testing REST API of your application In this section examples of using steps provided for testing, REST API will be provided. All examples can be checked on site https://reqres.in/ which is simple REST API service # Available methods At this moment Kakunin supports methods for REST API: - GET - POST - DELETE - PATCH Also, You can set the headers for the request. # Making GET request In order to create get request and verify if the response is ok you need to create scenario step: ```gherkin Given I send "GET" request on "/api/users/2" endpoint Then the response code should be "200" ``` This scenario will create a get request to the application and verify if the response was 200. The response is stored till creating another request. So if We want to test the response body of a server we can create a scenario like: ```gherkin Given I send "GET" request on "/api/users/2" endpoint Then the response code should be "200" And the response should exact match to body: """ { "data": { "id": 2, "first_name": "Janet", "last_name": "Weaver", "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/josephstein/128.jpg" } } """ ``` Based on this We can also check if the response matches schema that we have provided by using step: ```gherkin Then the response should exact match JSON schema: ``` # Making POST request In order to create post request and attach the JSON body to it You need to create a scenario: ```gherkin Given I send "POST" request on "/api/users" endpoint with body: """ { "name": "morpheus", "job": "leader" } """ Then the response code should be "201" ``` or you can create post request and attach the form data to it: ```gherkin Given I send "POST" request on "/api/users" endpoint using form data: | name | morpheus | Then the response code should be "201" ``` This scenario will create a post request to the application and verify if response was 201 (created). The response is stored till creating another request. So if We want to test the response body of a server we can create scenarios like before: ```gherkin Given I send "POST" request on "/api/users" endpoint with body: """ { "name": "morpheus", "job": "leader" } """ Then the response code should be "201" And the response should match JSON schema: """ { "title": "Post schema", "type": "object", "properties": { "name": { "type": "string" }, "job": { "type": "string" } }, "required": ["name", "job"] } """ ``` Scenario like that will verify if the post request was executed and response schema matches given one. # Making DELETE request Delete request works similarly to get request. Example of delete scenario: ```gherkin Given I send "DELETE" request on "/api/users/2" endpoint Then the response code should be "204" ``` # Making PATCH request Patch request works similarly to post request. Example of patch scenario: ```gherkin Given I send "PATCH" request on "/api/users/2" endpoint with JSON body: """ { "name": "morpheus", "job": "zion resident" } """ Then the response code should be "200" And the response should exact match to body: """ { "name": "morpheus", "job": "zion resident", "updatedAt": "2019-02-12T18:25:06.001Z" } """ ``` # Setting headers for request Sometimes We want to set the headers for next request. In order to achieve this, We can create scenario like: ```gherkin Given I set request headers: | User-Agent | Mozilla | When I send "POST" request on "postTestEndpoint" endpoint with JSON body: """ { "title": "adam", "body": "test" } """ Then the response code should be "403" ``` This scenario will set "User-Agent" header of next request to "Mozilla". ================================================ FILE: docs/transformers.md ================================================ --- id: transformers title: Transformers --- Transformers allow you to transform values passed to form steps. For example a select requires to pass a value `/options/1b30f17e-e445-4d28-a30c-dedad95829ab`. This one is quite unreadable, but with the help of transformers you are able to write it like this: `d:options:someOptionName`. In real-life example it will look similar to: ```gherkin I fill the "myForm" form with: | inputElement | d:someDictionary:someKey | | textareaElement | g:someGenerator | | radioElement | v:someVariableName | | checkboxElement | standard value | ``` There are 3 types of built-in transformers: ## Dictionaries Dictionaries allows you to transform a value A to value B using a simple key->value transformation. You can run a dictionary transformer by providing dictionary prefix `d:`, specifying the dictionary name and key that should be used as a value provider. For example: `d:myDictionaryName:myDictionaryKey` this example assumes that there is a dictionary that supports name `myDictionaryName` and it has `myDictionarKey` key. You can read about dictionaries in `Extending Kakunin` section. ## Generators Generators allows you to generate a value by using a specified generator. This can be done by: `g:generatorName`. If a generator supports parameters then you can specify them by: `g:generatorName:param1:param2:...:paramN` You can read more about generators in `Extending Kakunin` section. ## Variable store Variable store allows you to fill the form with a value that was saved in previous steps of current running scenario. This can be done by: `v:variableName` You can read more about variable store in `Extending Kakunin` section ================================================ FILE: functional-tests/dictionaries/test-dictionary.js ================================================ const { dictionaries } = require('kakunin'); const { BaseDictionary } = require('kakunin'); class TestDictionary extends BaseDictionary { constructor() { super('test-dictionary', { 'test-name': 'Janek', 'test-value': 'lux', }); } } dictionaries.addDictionary(new TestDictionary()); ================================================ FILE: functional-tests/downloads/.gitkeep ================================================ ================================================ FILE: functional-tests/features/content/operations_on_stored_variables.feature ================================================ Feature: Store table and compare jsons As a kakunin user I want to store values as variables Scenario: Store table and compare jsons Given I visit the "main" page When I click the "tabularDataLink" element Then the "tabularData" page is displayed When I store table "rows" rows as "tableValue" with columns: | indexLocator | | descendingIndex | | viewButton | Then compare given JSON string with stored "tableValue" JSON: """ [ ["1", "4", "View"], ["2", "3", "View"], ["3", "2", "View"], ["4", "1", "View"] ] """ Scenario: Compare stored values with the content from a xlsx file - equal rows and four stored values Given I store the content from "http://localhost:8080/xlsx/data-9rows" endpoint as "storedTable" variable Then the file "example.xlsx" contains table data stored under "storedTable" variable Scenario: Compare stored values with the content from a xlsx file - equal rows and one stored value Given I store the content from "http://localhost:8080/xlsx/data-9rows-part" endpoint as "storedTable" variable Then the file "example.xlsx" contains table data stored under "storedTable" variable Scenario: Compare stored values with the content from a xlsx file - three rows and four stored values Given I store the content from "http://localhost:8080/xlsx/data-3rows" endpoint as "storedTable" variable Then the file "example.xlsx" contains table data stored under "storedTable" variable Scenario: Compare stored values with the content from a xlsx file - three rows and two stored values Given I store the content from "http://localhost:8080/xlsx/data-3rows-part" endpoint as "storedTable" variable Then the file "example.xlsx" contains table data stored under "storedTable" variable Scenario: Compare stored values with the content from a xlsx file - one row and four stored values Given I store the content from "http://localhost:8080/xlsx/data-1row" endpoint as "storedTable" variable Then the file "example.xlsx" contains table data stored under "storedTable" variable Scenario: Compare stored values with the content from a xlsx file - one row and two stored values Given I store the content from "http://localhost:8080/xlsx/data-1row-part" endpoint as "storedTable" variable Then the file "example.xlsx" contains table data stored under "storedTable" variable ================================================ FILE: functional-tests/features/content/validate_tabular_data.feature ================================================ Feature: Tabular data As a kakunin user I want validate tabular data Scenario: Validate tabular data count Given I visit the "main" page When I click the "tabularDataLink" element Then the "tabularData" page is displayed And there are "at least 1" "rows" elements And there are "above 3" "rows" elements And there are "below 5" "rows" elements And there are "within 3 5" "rows" elements And there are "equal 4" "rows" elements Scenario: Validate tabular data count and content, also check sorting Given I visit the "main" page When I click the "tabularDataLink" element Then the "tabularData" page is displayed And the "rows" element is visible And there are "at least 4" following elements for element "rows": | indexLocator | r:validNumber | And there are "above 3" following elements for element "rows": | indexLocator | r:validNumber | And there are "equal 4" following elements for element "rows": | indexLocator | r:validNumber | And there are "below 5" following elements for element "rows": | indexLocator | r:validNumber | And there are "within 3-5" following elements for element "rows": | indexLocator | r:validNumber | And there are "equal 4" following elements for element "rows": | indexLocator | r:validNumber | | idLocator | t:MY_CUSTOM_ID_ | | nameLocator | r:notEmpty | | viewButton | f:isVisible | | viewButton | f:isClickable | And every "rows" element should have the same value for element "viewButton" And "indexLocator" value on the "rows" list is sorted in "ascending" order And "descendingIndex" value on the "rows" list is sorted in "descending" order Scenario: Validate exact tabular data by columns Given I visit the "main" page When I click the "tabularDataLink" element Then the "tabularData" page is displayed And there are following elements in table "rows": | indexLocator | nameLocator | | t:1 | t:Some custom name 1 | | t:2 | t:Some custom name 2 | | t:3 | t:Some custom name 3 | | t:4 | t:Some custom name 4 | And the element "rows" should have an item with values: | indexLocator | t:1 | | indexLocator | f:isVisible | And the element "rows" should not have an item with values: | indexLocator | t:incorrect-number-value | Scenario: Navigate to pages by using click steps Given I visit the "main" page When I click the "valueToClick" element Then the "tabularData" page is displayed ================================================ FILE: functional-tests/features/content/validate_tabular_data_css.feature ================================================ Feature: Tabular data As a kakunin user I want validate tabular data Scenario: Validate tabular data count Given I visit the "main" page When I click the "a[href='/tabular-data']" element Then the "tabularData" page is displayed And there are "at least 1" "table tr" elements And there are "above 3" "table tr" elements And there are "below 5" "table tr" elements And there are "within 3 5" "table tr" elements And there are "equal 4" "table tr" elements Scenario: Validate tabular data count and content, also check sorting Given I visit the "main" page When I click the "a[href='/tabular-data']" element Then the "tabularData" page is displayed And the "table tr" element is visible And there are "at least 4" following elements for element "table tr": | .index | r:validNumber | And there are "above 3" following elements for element "table tr": | .index | r:validNumber | And there are "equal 4" following elements for element "table tr": | .index | r:validNumber | And there are "below 5" following elements for element "table tr": | .index | r:validNumber | And there are "within 3-5" following elements for element "table tr": | .index | r:validNumber | And there are "equal 4" following elements for element "table tr": | .index | r:validNumber | | .id | t:MY_CUSTOM_ID_ | | .name | r:notEmpty | | button.view | f:isVisible | | button.view | f:isClickable | And every "table tr" element should have the same value for element "button.view" And ".index" value on the "table tr" list is sorted in "ascending" order And ".descending-sort" value on the "table tr" list is sorted in "descending" order Scenario: Validate exact tabular data by columns Given I visit the "main" page When I click the "a[href='/tabular-data']" element Then the "tabularData" page is displayed And there are following elements in table "table tr": | .index | nameLocator | | t:1 | t:Some custom name 1 | | t:2 | t:Some custom name 2 | | t:3 | t:Some custom name 3 | | t:4 | t:Some custom name 4 | And the element "table tr" should have an item with values: | .index | t:1 | | .index | f:isVisible | And the element "table tr" should not have an item with values: | .index | t:incorrect-number-value | Scenario: Navigate to pages by using click steps Given I visit the "main" page When I click the ".valueForClickStep" element Then the "tabularData" page is displayed ================================================ FILE: functional-tests/features/content/wait_for_element_dissapear.feature ================================================ Feature: Element visibility As a kakunin user I want to wait for element to disappear Scenario: Check visibility - disappear step Given I visit the "main" page When I click the "buttonLink" element Then the "buttonForm" page is displayed When I click the "disappearBtn" element Then I wait for the "disappearBtn" element to disappear Scenario: Check visibility with - wait for condition step Given I visit the "main" page When I click the "buttonLink" element Then the "buttonForm" page is displayed When I click the "disappearBtn" element And I wait for "invisibilityOf" of the "disappearBtn" element Then the "disappearBtn" element is not visible ================================================ FILE: functional-tests/features/content/wait_for_element_dissapear_css.feature ================================================ Feature: Element visibility As a kakunin user I want to wait for element to disappear Scenario: Check visibility - disappear step Given I visit the "main" page When I click the "a[href='/form/disappear']" element Then the "buttonForm" page is displayed When I click the "#button" element Then I wait for the "#button" element to disappear Scenario: Check visibility with - wait for condition step Given I visit the "main" page When I click the "buttonLink" element Then the "buttonForm" page is displayed When I click the "#button" element And I wait for "invisibilityOf" of the "#button" element Then the "#button" element is not visible ================================================ FILE: functional-tests/features/drag-and-drop/operations_on_elements.feature ================================================ Feature: Drag and drop As a kakunin user I want to be able to make operations on elements Scenario: Drag element and drop on the other one Given I visit the "dragAndDrop" page When I drag "kittens" element and drop over "target" element Then the "kittensInsideTarget" element is visible ================================================ FILE: functional-tests/features/drag-and-drop/operations_on_elements_css.feature ================================================ Feature: Drag and drop As a kakunin user I want to be able to make operations on elements Scenario: Drag element and drop on the other one Given I visit the "dragAndDrop" page When I drag "#draggable" element and drop over "#droppable" element Then the ".ui-state-highlight" element is visible ================================================ FILE: functional-tests/features/forms/fill_and_check_form.feature ================================================ Feature: Forms As a kakunin user I want fill and check form fields Scenario: Fill and check form fields Given I visit the "main" page When I click the "formLink" element Then the "simpleForm" page is displayed When I generate random "stringWithLength:10" as "storedStringWithLength" And I fill the "form" form with: | nameInput | v:storedStringWithLength | Then the "form" form is filled with: | nameInput | v:storedStringWithLength | When I fill the "form" form with: | nameInput | d:test-dictionary:test-name | | descriptionTextarea | some description | | optionCheckboxes | Checkbox Option 2 | | optionCheckboxes | Checkbox Option 3 | | optionRadios | third-radio-option | | statusSelect | unknown | And I click the "submitButton" element Then the "simpleFormPost" page is displayed And the "form" form is filled with: | nameInput | d:test-dictionary:test-name | | descriptionTextarea | some description | | optionCheckboxes | Checkbox Option 2 | | optionCheckboxes | Checkbox Option 3 | | optionRadios | third-radio-option | | statusSelect | unknown | Scenario: Fill input and textarea fields and then store the values and check if the form was filled with expected data Given I visit the "main" page When I click the "formLink" element Then the "simpleForm" page is displayed When I generate random "stringWithLength:6" as "storedStringWithLength" And I fill the "form" form with: | nameInput | v:storedStringWithLength | And I store the "nameInput" element text as "storedInputValue" variable Then the "form" form is filled with: | nameInput | v:storedStringWithLength | | nameInput | v:storedInputValue | And there is element "nameInput" with value "t:v:storedInputValue" And there is element "nameInput" with value "t:v:storedStringWithLength" When I fill the "form" form with: | descriptionTextarea | g:personalData:email | And I store the "descriptionTextarea" element text as "storedTextareaValue" variable Then the "form" form is filled with: | descriptionTextarea | v:storedTextareaValue | And there is element "descriptionTextarea" with value "t:v:storedTextareaValue" And there is element "descriptionTextarea" with value "r:email" ================================================ FILE: functional-tests/features/forms/fill_and_check_form_css.feature ================================================ Feature: Forms As a kakunin user I want fill and check form fields Scenario: Fill and check form fields Given I visit the "main" page When I click the "a[href='/form/simple']" element Then the "simpleForm" page is displayed When I generate random "stringWithLength:10" as "storedStringWithLength" And I fill the "form" form with: | nameInput | v:storedStringWithLength | Then the "form" form is filled with: | nameInput | v:storedStringWithLength | When I fill the "form" form with: | input[name="name"] | d:test-dictionary:test-name | | textarea[name="description"] | some description | | input[type="checkbox"] | Checkbox Option 2 | | input[type="checkbox"] | Checkbox Option 3 | | input[type="radio"] | third-radio-option | | select[name="status"] | unknown | And I click the "submitButton" element Then the "simpleFormPost" page is displayed And the "form" form is filled with: | input[name="name"] | d:test-dictionary:test-name | | textarea[name="description"] | some description | | input[type="checkbox"] | Checkbox Option 2 | | input[type="checkbox"] | Checkbox Option 3 | | input[type="radio"] | third-radio-option | | select[name="status"] | unknown | ================================================ FILE: functional-tests/features/forms/fill_select.feature ================================================ Feature: Forms As a kakunin user I want to check options Scenario: Fill and check form fields Given I visit the "main" page When I click the "formSelectLink" element Then the "simpleSelectForm" page is displayed And there are "personOption" dropdown list elements with following options: | Person3 | | Person2 | | Person1 | | Person4 | ================================================ FILE: functional-tests/features/matchers/match_current_date.feature ================================================ Feature: Matchers As a kakunin user I want to navigate to matcher page and match current date Scenario: I want to match current date with format Given I visit the "main" page When I click the "matchersLink" element Then the "matchers" page is displayed And there is element "dateElement" with value "f:isVisible" And there is element "dateMatcherText" with value "t:Date/Time:" And there is element "dateElement" with value "f:isClickable" And there is element "dateElement" with value "f:isPresent" And there is element "dateElement" with value "r:notEmpty" And there is element "dateElement" with value "f:currentDate:YYYY-MM-DD" Scenario: I want to match current date without additional parameters Given I visit the "main" page When I click the "matchersLink" element Then the "matchers" page is displayed And there is element "dateElement" with value "f:currentDate" ================================================ FILE: functional-tests/features/matchers/match_current_date_css.feature ================================================ Feature: Matchers As a kakunin user I want to navigate to matcher page and match current date Scenario: I want to match current date with format Given I visit the "main" page When I click the ".matchers" element Then the "matchers" page is displayed And there is element "span.current_date" with value "f:isVisible" And there is element "p.date-matcher" with value "t:Date/Time:" And there is element "span.current_date" with value "f:isClickable" And there is element "span.current_date" with value "f:isPresent" And there is element "span.current_date" with value "r:notEmpty" And there is element "span.current_date" with value "f:currentDate:YYYY-MM-DD" Scenario: I want to match current date without additional parameters Given I visit the "main" page When I click the ".matchers" element Then the "matchers" page is displayed And there is element "span.current_date" with value "f:currentDate" ================================================ FILE: functional-tests/features/matchers/matchers.feature ================================================ Feature: Matchers As a Kakunin user I want to fill input and then check if the value matches the expected result Scenario: Fill the input and check value Given I visit the "main" page When I click the "formLink" element Then the "simpleForm" page is displayed And I fill the "form" form with: | nameInput | test | And there is element "nameInput" with value "t:test" And there is element "nameInput" containing "test" text And there is no element "nameInput" with value "t:hello" And there is no element "nameInput" containing "hello" text And there is element "nameInput" matching "isVisible" matcher And there is no element "nameInput" matching "isNotVisible" matcher And there is element "nameInput" with "notEmpty" regex And there is no element "nameInput" with "number" regex ================================================ FILE: functional-tests/features/matchers/matchers_css.feature ================================================ Feature: Matchers As a Kakunin user I want to fill input and then check if the value matches the expected result Scenario: Fill the input and check value Given I visit the "main" page When I click the "a[href='/form/simple']" element Then the "simpleForm" page is displayed And I fill the "form" form with: | input[name="name"] | test | And there is element "input[name='name']" with value "t:test" And there is element "input[name='name']" containing "test" text And there is no element "input[name='name']" with value "t:hello" And there is no element "input[name='name']" containing "hello" text And there is element "input[name='name']" matching "isVisible" matcher And there is no element "input[name='name']" matching "isNotVisible" matcher And there is element "input[name='name']" with "notEmpty" regex And there is no element "input[name='name']" with "number" regex ================================================ FILE: functional-tests/features/navigation/navigate_to_given_page.feature ================================================ Feature: Navigation As a kakunin user I want to navigate to selected page Scenario: Navigate by link click Given I visit the "main" page When I click the "formLink" element Then the "simpleForm" page is displayed And the "form" element is visible Scenario: Navigate to parametrized url Given I visit the "navigationPages" page with parameters: | pageId | myPageId | | title | myPageTitle | Then there is element "pageId" with value "t:myPageId" And there is element "title" with value "t:myPageTitle" Scenario: Navigate to parametrized url with additional params Given I visit the "navigationPages" page with parameters: | pageId | myPageId | | additionalParam1 | value1 | | title | myPageTitle | | additionalParam2 | value2 | Then the "additionalParams" page is displayed # check again Then I visit the "navigationPages" page with parameters: | pageId | myPageId | | additionalParam1 | value1 | | title | myPageTitle | | additionalParam2 | value2 | Then the "additionalParams" page is displayed ================================================ FILE: functional-tests/features/navigation/navigate_to_given_page_css.feature ================================================ Feature: Navigation As a kakunin user I want to navigate to selected page Scenario: Navigate by link click Given I visit the "main" page When I click the "a[href='/form/simple']" element Then the "simpleForm" page is displayed And the "form" element is visible Scenario: Navigate to parametrized url Given I visit the "navigationPages" page with parameters: | pageId | myPageId | | title | myPageTitle | Then there is element "p.pageId" with value "t:myPageId" And there is element "p.title" with value "t:myPageTitle" ================================================ FILE: functional-tests/features/navigation/switch-between-tabs.feature ================================================ Feature: Navigation As a kakunin user I want to switch between browser tabs Scenario: Navigate by link click Given I visit the "main" page When I click the "matchersInNewTabLink" element And I switch to window number "2" of a browser Then the "matchers" page is displayed And there is element "dateElement" with value "f:isVisible" When I close the current browser tab Then the "main" page is displayed When I click the "matchersLink" element Then the "matchers" page is displayed ================================================ FILE: functional-tests/features/pages/verify_displayed_page.feature ================================================ Feature: Verify displayed pge As a kakunin user I want to make sure I am on expected page Scenario: Verify relative page Given I visit the "main" page When I click the "formLink" element Then the "simpleForm" page is displayed Scenario: Verify absolute url page Given I visit the "main" page When I click the "absolutePageLink" element Then the "absolutePage" page is displayed Scenario: Verify external url page Given I visit the "main" page When I click the "googleLink" element Then the "google" page is displayed ================================================ FILE: functional-tests/features/testing-api/testing_delete_request.feature ================================================ Feature: Test server delete request As a kakunin user I want to test restApi delete request Scenario: REST get example test Given I send "delete" request on "deleteTestEndpoint" endpoint Then the response code should be "200" ================================================ FILE: functional-tests/features/testing-api/testing_get_response.feature ================================================ Feature: Test server get response As a kakunin user I want to test restApi get response Scenario: REST get example test Given I send "GET" request on "getTestEndpoint" endpoint Then the response code should be "200" And the response should exact match to body: """ { "id": 1, "title": "Kaunin", "body": "test" } """ ================================================ FILE: functional-tests/features/testing-api/testing_headers_setting.feature ================================================ Feature: Test setting headers As a kakunin user I want to set the headers Scenario: Setting http headers Given I set request headers: | User-Agent | Mozilla | When I send "POST" request on "postTestEndpoint" endpoint with JSON body: """ { "title": "adam", "body": "test" } """ Then the response code should be "403" ================================================ FILE: functional-tests/features/testing-api/testing_patch_request.feature ================================================ Feature: Test patch endpoint As a kakunin user I want to set test the patch endpoint Scenario: REST patch example test Given I send "PATCH" request on "patchTestEndpoint" endpoint with JSON body: """ { "first_name": "adam" } """ Then the response code should be "200" ================================================ FILE: functional-tests/features/testing-api/testing_post_form_data.feature ================================================ Feature: Test server post request using form data As a kakunin user I want to test restApi post request Scenario: REST get example test Given I send "POST" request on "postFormDataEndpoint" endpoint using form data: | name | adam1 | Then the response code should be "201" ================================================ FILE: functional-tests/features/testing-api/testing_post_json.feature ================================================ Feature: Test server post response As a kakunin user I want to test restApi post request Scenario: REST post example test Given I send "POST" request on "postTestEndpoint" endpoint with JSON body: """ { "name": "adam", "title": "test" } """ Then the response code should be "201" Then the response should match JSON schema: """ { "title": "Posts schema", "type": "object", "properties": { "code": { "type": "string" }, "name": { "type": "string" }, "title": { "type": "string" } }, "required": ["code", "name", "title"] } """ ================================================ FILE: functional-tests/features/wait-for-elements/wait_for_form.feature ================================================ Feature: Wait for forms As a kakunin user I want fill and check form fields Scenario: Fill and check form fields Given I visit the "main" page When I click the "appearForm" element Then the "appearSimpleForm" page is displayed When I click the "formAppearBtn" element And I fill the "form" form with: | nameInput | d:test-dictionary:test-name | | descriptionTextarea | some description | | optionCheckboxes | Checkbox Option 2 | | optionCheckboxes | Checkbox Option 3 | | optionRadios | third-radio-option | | statusSelect | unknown | Then the "form" form is filled with: | nameInput | d:test-dictionary:test-name | | descriptionTextarea | some description | | optionCheckboxes | Checkbox Option 2 | | optionCheckboxes | Checkbox Option 3 | | optionRadios | third-radio-option | | statusSelect | unknown | When I click the "submitButton" element Then the "appearSimpleFormPost" page is displayed ================================================ FILE: functional-tests/features/wait-for-elements/wait_for_form_css.feature ================================================ Feature: Wait for forms As a kakunin user I want fill and check form fields Scenario: Fill and check form fields Given I visit the "main" page When I click the ".appearForm" element Then the "appearSimpleForm" page is displayed When I click the ".colored" element And I fill the "form" form with: | input[name="name"] | d:test-dictionary:test-name | | textarea[name="description"] | some description | | input[type="checkbox"] | Checkbox Option 2 | | input[type="checkbox"] | Checkbox Option 3 | | input[type="radio"] | third-radio-option | | select[name="status"] | unknown | Then the "form" form is filled with: | input[name="name"] | d:test-dictionary:test-name | | textarea[name="description"] | some description | | input[type="checkbox"] | Checkbox Option 2 | | input[type="checkbox"] | Checkbox Option 3 | | input[type="radio"] | third-radio-option | | select[name="status"] | unknown | When I click the "input[type='submit']" element Then the "appearSimpleFormPost" page is displayed ================================================ FILE: functional-tests/features/wait-for-elements/wait_for_table.feature ================================================ Feature: Wait for Tabular data As a kakunin user I want validate tabular data which will appear in future Scenario: Validate tabular data count and content, also check sorting Given I visit the "main" page When I click the "appearTable" element Then the "appearTabularData" page is displayed When I click the "tableAppearBtn" element Then there are "equal 4" following elements for element "rows": | indexLocator | r:validNumber | | idLocator | t:MY_CUSTOM_ID_ | | nameLocator | r:notEmpty | | viewButton | f:isVisible | | viewButton | f:isClickable | And every "rows" element should have the same value for element "viewButton" And "indexLocator" value on the "rows" list is sorted in "ascending" order And "descendingIndex" value on the "rows" list is sorted in "descending" order Scenario: Validate tabular data count and content, also check sorting Given I visit the "main" page When I click the "appearTable" element Then the "appearTabularData" page is displayed When I click the "tableAppearBtn" element And I store table "rows" rows as "tableValue" with columns: | indexLocator | | idLocator | | nameLocator | Then compare given JSON string with stored "tableValue" JSON: """ [ ["1","MY_CUSTOM_ID_1","Some custom name 1"], ["2","MY_CUSTOM_ID_2","Some custom name 2"], ["3","MY_CUSTOM_ID_3","Some custom name 3"], ["4","MY_CUSTOM_ID_4","Some custom name 4"] ] """ ================================================ FILE: functional-tests/kakunin.conf.js ================================================ module.exports = { browserWidth: 1600, browserHeight: 900, timeout: 60, elementsVisibilityTimeout: 5, waitForPageTimeout: 5, downloadTimeout: 30, reports: '/reports', downloads: '/downloads', data: '/data', features: ['/features'], pages: ['/pages'], matchers: ['/matchers'], generators: ['/generators'], form_handlers: ['/form_handlers'], step_definitions: ['/step_definitions'], comparators: ['/comparators'], dictionaries: ['/dictionaries'], transformers: ['/transformers'], regexes: ['/regexes'], hooks: ['/hooks'], clearEmailInboxBeforeTests: false, clearCookiesAfterScenario: true, clearLocalStorageAfterScenario: true, email: null, headless: true, noGpu: true, type: 'otherWeb', baseUrl: 'http://localhost:8080', apiUrl: 'http://localhost:8080/', browserMob: { serverPort: 8887, port: 8888, host: 'localhost', }, browserstack: { seleniumAddress: 'http://hub-cloud.browserstack.com/wd/hub', defaultPort: 45691, capabilities: { 'browserstack.user': '', 'browserstack.key': '', 'browserstack.local': true, browserName: 'chrome', } }, accounts: { someAccount: { accounts: [ { email: '', password: '', }, ], }, }, }; ================================================ FILE: functional-tests/package.json ================================================ { "name": "kakunin-functional-tests", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "kakunin": "cross-env NODE_ENV=prod kakunin", "start": "pm2 update && pm2 start www", "stop": "pm2 delete www", "test-ci": "npm run start && npm run kakunin -- --parallel 4 && npm run stop", "test": "npm run start && npm run kakunin && npm run stop" }, "author": "", "dependencies": { "body-parser": "1.19.0", "cross-env": "7.0.3", "express": "4.17.1", "kakunin": "file:../", "node-fetch": "2.6.1", "nunjucks": "3.2.3", "protractor": "7.0.0" }, "license": "ISC", "devDependencies": { "pm2": "4.5.5" } } ================================================ FILE: functional-tests/pages/absolutePage.js ================================================ 'use strict'; const { BasePage } = require('kakunin'); class AbsolutePage extends BasePage { constructor() { super(); this.url = 'http://localhost:8080/absolute-page'; } } module.exports = AbsolutePage; ================================================ FILE: functional-tests/pages/additionalParams.js ================================================ 'use strict'; const { BasePage } = require('kakunin'); class AdditionalParamsPage extends BasePage { constructor() { super(); this.url = '/navigation/pages/:id/titles/:title?additionalParam1=:value1&additionalParam2=:value2'; } } module.exports = AdditionalParamsPage; ================================================ FILE: functional-tests/pages/appearSimpleForm.js ================================================ 'use strict'; const { BasePage } = require('kakunin'); class AppearSimpleForm extends BasePage { constructor() { super(); this.url = '/wait-for-appear/form'; this.formAppearBtn = $('.colored'); this.form = $('form'); this.nameInput = this.form.$('input[name="name"]'); this.descriptionTextarea = this.form.$('textarea[name="description"]'); this.optionCheckboxes = this.form.$$('input[type="checkbox"]'); this.optionRadios = this.form.$$('input[type="radio"]'); this.statusSelect = this.form.$('select[name="status"]'); this.submitButton = this.form.$('input[type="submit"]'); } } module.exports = AppearSimpleForm; ================================================ FILE: functional-tests/pages/appearSimpleFormPost.js ================================================ 'use strict'; const { BasePage } = require('kakunin'); class AppearSimpleFormPost extends BasePage { constructor() { super(); this.url = '/wait-for-appear/form/post'; this.form = $('form'); this.nameInput = this.form.$('input[name="name"]'); this.descriptionTextarea = this.form.$('textarea[name="description"]'); this.optionCheckboxes = this.form.$$('input[type="checkbox"]'); this.optionRadios = this.form.$$('input[type="radio"]'); this.statusSelect = this.form.$('select[name="status"]'); } } module.exports = AppearSimpleFormPost; ================================================ FILE: functional-tests/pages/appearTabularData.js ================================================ 'use strict'; const { BasePage } = require('kakunin'); class AppearTabularData extends BasePage { constructor() { super(); this.url = '/wait-for-appear/table'; this.tableAppearBtn = $('.colored'); this.rows = $$('table tr'); this.indexLocator = $('.index'); this.descendingIndex = $('.descending-sort'); this.idLocator = $('.id'); this.nameLocator = $('.name'); this.viewButton = $('button.view'); } } module.exports = AppearTabularData; ================================================ FILE: functional-tests/pages/buttonForm.js ================================================ 'use strict'; const { BasePage } = require('kakunin'); class ButtonForm extends BasePage { constructor() { super(); this.url = '/form/disappear'; this.disappearBtn = $('#button'); } } module.exports = ButtonForm; ================================================ FILE: functional-tests/pages/dragAndDrop.js ================================================ 'use strict'; const { BasePage } = require('kakunin'); class DragAndDropPage extends BasePage { constructor() { super(); this.url = '/drag-and-drop'; this.kittens = $('#draggable'); this.target = $('#droppable'); this.kittensInsideTarget = $('.ui-state-highlight'); } } module.exports = DragAndDropPage; ================================================ FILE: functional-tests/pages/google.js ================================================ 'use strict'; const { BasePage } = require('kakunin'); class GooglePage extends BasePage { constructor() { super(); this.url = 'https://www.google.pl'; } } module.exports = GooglePage; ================================================ FILE: functional-tests/pages/main.js ================================================ 'use strict'; const { BasePage } = require('kakunin'); class MainPage extends BasePage { constructor() { super(); this.url = '/'; this.linkDivs = $$('.available-examples-links'); this.formLink = $('a[href="/form/simple"]'); this.formSelectLink = $('a[href="/form/select"]'); this.absolutePageLink = $('a[href="/absolute-page"]'); this.googleLink = $('a[href="https://www.google.pl"]'); this.tabularDataLink = $('a[href="/tabular-data"]'); this.buttonLink = $('a[href="/form/disappear"]'); this.valueToClick = $('.valueForClickStep'); this.appearTable = $('.appearTable'); this.appearForm = $('.appearForm'); this.matchersLink = $('.matchers'); this.matchersInNewTabLink = $('.matchersInNewTab'); } } module.exports = MainPage; ================================================ FILE: functional-tests/pages/matchers.js ================================================ 'use strict'; const { BasePage } = require('kakunin'); class MatchersPage extends BasePage { constructor() { super(); this.url = '/matchers'; this.dateMatcherText = $('p.date-matcher '); this.dateElement = $('span.current_date '); } } module.exports = MatchersPage; ================================================ FILE: functional-tests/pages/navigationPages.js ================================================ 'use strict'; const { BasePage } = require('kakunin'); class NavigationPagesPage extends BasePage { constructor() { super(); this.url = '/navigation/pages/:pageId/titles/:title'; this.pageId = $('p.pageId'); this.title = $('p.title'); this.queryParam1 = $('p.queryParam1'); this.queryParam2 = $('p.queryParam2'); } } module.exports = NavigationPagesPage; ================================================ FILE: functional-tests/pages/simpleForm.js ================================================ 'use strict'; const { BasePage } = require('kakunin'); class SimpleForm extends BasePage { constructor() { super(); this.url = '/form/simple'; this.form = $('form'); this.nameInput = this.form.$('input[name="name"]'); this.descriptionTextarea = this.form.$('textarea[name="description"]'); this.optionCheckboxes = this.form.$$('input[type="checkbox"]'); this.optionRadios = this.form.$$('input[type="radio"]'); this.statusSelect = this.form.$('select[name="status"]'); this.submitButton = this.form.$('input[type="submit"]'); } } module.exports = SimpleForm; ================================================ FILE: functional-tests/pages/simpleFormPost.js ================================================ 'use strict'; const { BasePage } = require('kakunin'); class SimpleFormPost extends BasePage { constructor() { super(); this.url = '/form/simple/post'; this.form = $('form'); this.nameInput = this.form.$('input[name="name"]'); this.descriptionTextarea = this.form.$('textarea[name="description"]'); this.optionCheckboxes = this.form.$$('input[type="checkbox"]'); this.optionRadios = this.form.$$('input[type="radio"]'); this.statusSelect = this.form.$('select[name="status"]'); } } module.exports = SimpleFormPost; ================================================ FILE: functional-tests/pages/simpleSelectForm.js ================================================ 'use strict'; const { BasePage } = require('kakunin'); class SimpleForm extends BasePage { constructor() { super(); this.url = '/form/select'; this.form = $('form'); this.selectPerson = $('#personlist'); this.personOption = this.selectPerson.$$('option'); this.submitButton = this.form.$('input[type="submit"]'); } } module.exports = SimpleForm; ================================================ FILE: functional-tests/pages/tabularData.js ================================================ 'use strict'; const { BasePage } = require('kakunin'); class TabularData extends BasePage { constructor() { super(); this.url = '/tabular-data'; this.rows = $$('table tr'); this.indexLocator = $('.index'); this.descendingIndex = $('.descending-sort'); this.idLocator = $('.id'); this.nameLocator = $('.name'); this.viewButton = $('button.view'); } } module.exports = TabularData; ================================================ FILE: functional-tests/regexes/index.js ================================================ module.exports = { validNumber: '\\d+', }; ================================================ FILE: functional-tests/step_definitions/custom_json_parser.js ================================================ const variableStore = require('kakunin').variableStore; const fetch = require('node-fetch'); const { When } = require('kakunin'); When(/^compare given JSON string with stored "([^"]*)" JSON:$/, function(storedJsonArray, json) { const removeNewLines = str => str.replace(/(\r\n|\n|\r)/gm, ''); const storedJsonString = JSON.stringify(variableStore.getVariableValue(storedJsonArray)); const expectedJsonString = JSON.stringify(JSON.parse(removeNewLines(json))); if (storedJsonString === expectedJsonString) { return Promise.resolve(); } return Promise.reject('JSON strings are not the same!'); }); When(/^I store the content from "([^"]*)" endpoint as "([^"]*)" variable/, function(url, variableName) { return fetch(url) .then(res => res.json()) .then(data => variableStore.storeVariable(variableName, JSON.parse(data.content))); }); ================================================ FILE: functional-tests/www/index.js ================================================ const express = require('express'); const nunjucks = require('nunjucks'); const path = require('path'); const app = express(); const { xlsxDataRouting } = require('./jsonData/xlsxData.router'); app.set('views', path.join(__dirname, 'views')); app.use('/assets', express.static(path.join(__dirname, 'assets'))); nunjucks.configure(app.get('views'), { autoescape: true, express: app, }); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.get('/', function(req, res) { res.render('index.njs'); }); app.get('/drag-and-drop', function(req, res) { res.render('drag-and-drop/index.njs'); }); app.get('/tabular-data', function(req, res) { res.render('table/tabular-data.njs'); }); app.get('/absolute-page', function(req, res) { res.render('absolute/index.njs'); }); app.get('/form/simple', function(req, res) { res.render('form/simple.njs'); }); app.get('/form/disappear', function(req, res) { res.render('form/disappear.njs'); }); app.post('/form/simple/post', function(req, res) { res.render('form/simple.njs', { form: req.body, }); }); app.get('/navigation/pages/:pageId/titles/:title', function(req, res) { res.render('navigation/page.njs', { pageId: req.params.pageId, title: req.params.title, queryParam1: req.query.queryParam1, queryParam2: req.query.queryParam2 }); }); app.get('/wait-for-appear/table', function(req, res) { res.render('wait-for-appear/table.njs'); }); app.get('/wait-for-appear/form', function(req, res) { res.render('wait-for-appear/form.njs'); }); app.post('/wait-for-appear/form/post', function(req, res) { res.render('wait-for-appear/form.njs', { form: req.body, }); }); app.get('/matchers', function(req, res) { res.render('matchers/matchers.njs'); }); app.get('/form/select', function(req, res) { res.render('form/select.njs'); }); app.post('/form/select/post', function(req, res) { res.render('form/select.njs', { form: req.body, }); }); app.delete('/deleteTestEndpoint',function(req, res, next){ res.status(200); return res.end(); }); app.get('/getTestEndpoint', function (req, res) { res.setHeader('Content-Type', 'application/json'); const header = req.header('host'); if (header === 'localhost:8080') { return res.send(JSON.stringify( { id: 1, title: 'Kaunin', body: 'test' } )); } res.status(403); return res.end(); }); app.patch('/patchTestEndpoint', function (req, res) { if(req.body.hasOwnProperty('first_name') === true) { res.status(200); return res.end(); } res.status(400) return res.end(); }); app.post('/postTestEndpoint', function (req, res) { const name = req.body.name; const title = req.body.title; const header = req.header('User-Agent'); const object = { code: 'created', name: name, title: title }; if (header === 'Mozilla') { res.status(403); return res.end(); } res.status(201); return res.json(object); }); app.post('/postFormDataEndpoint', function (req, res) { const contentType = req.header('Content-Type'); if (contentType !== 'multipart/form-data') { res.status(403); return res.end(); } res.status(201); return res.end(); }); app.use('/xlsx', xlsxDataRouting()); app.listen(8080, function() { console.log('Example app listening on port 8080!'); }); ================================================ FILE: functional-tests/www/jsonData/xlsxData.router.js ================================================ const express = require('express'); function xlsxDataRouting() { const router = express.Router(); router.get('/data-9rows', (req, res) => { res.json({ content: '[["Schamberger PLC","33333003158","25","Food & Consumable supply, IT, Medical Supplies and Equipment, Motors & Vehicles, Office Furniture, Office Supplies"],["JANEK","9912396963","2","Security Guards, Printing & Advertising, Food & Consumable supply, IT, Medical Supplies and Equipment, Motors & Vehicles, Office Furniture, Office Supplies"],["شركة تكامل القابضة","9912396963","2","Contracting & Maintenance, Food & Consumable supply, IT, Medical Supplies and Equipment, Motors & Vehicles, Office Furniture, Office Supplies"],["شركة تكامل القابضة","9912396963","2","Contracting & Maintenance, Food & Consumable supply, IT, Medical Supplies and Equipment, Motors & Vehicles, Office Furniture, Office Supplies"],["Bosco, Marks and Walker","9910412435","7","Contracting & Maintenance, Food & Consumable supply, IT, Medical Supplies and Equipment, Motors & Vehicles, Office Furniture, Office Supplies"],["Rippin-Torp","9912038835","7","Contracting & Maintenance, Food & Consumable supply, IT, Medical Supplies and Equipment, Motors & Vehicles, Office Furniture, Office Supplies"],["Kling-Bogan","9910412335","25","Contracting & Maintenance, Security Guards, Printing & Advertising, Food & Consumable supply, IT, Medical Supplies and Equipment, Motors & Vehicles, Office Furniture, Office Supplies"],["Hane, Bartoletti and Mitchell","9911038835","7","Contracting & Maintenance, Food & Consumable supply, IT, Medical Supplies and Equipment, Motors & Vehicles, Office Furniture, Office Supplies"],["Zemlak, Stiedemann and Green","9912496963","7","Contracting & Maintenance, Food & Consumable supply, IT, Medical Supplies and Equipment, Motors & Vehicles, Office Furniture, Office Supplies"]]', }); }); router.get('/data-9rows-part', (req, res) => { res.json({ content: '[["Schamberger PLC"],["JANEK"],["شركة تكامل القابضة"],["شركة تكامل القابضة"],["Bosco, Marks and Walker"],["Rippin-Torp"],["Kling-Bogan"],["Hane, Bartoletti and Mitchell"],["Zemlak, Stiedemann and Green"]]', }); }); router.get('/data-3rows', (req, res) => { res.json({ content: '[["Schamberger PLC","33333003158","25","Food & Consumable supply, IT, Medical Supplies and Equipment, Motors & Vehicles, Office Furniture, Office Supplies"],["JANEK","9912396963","2","Security Guards, Printing & Advertising, Food & Consumable supply, IT, Medical Supplies and Equipment, Motors & Vehicles, Office Furniture, Office Supplies"],["شركة تكامل القابضة","9912396963","2","Contracting & Maintenance, Food & Consumable supply, IT, Medical Supplies and Equipment, Motors & Vehicles, Office Furniture, Office Supplies"]]', }); }); router.get('/data-3rows-part', (req, res) => { res.json({ content: '[["Rippin-Torp","9912038835"],["Kling-Bogan","9910412335"],["Hane, Bartoletti and Mitchell","9911038835"]]', }); }); router.get('/data-1row', (req, res) => { res.json({ content: '[["Bosco, Marks and Walker","9910412435","7","Contracting & Maintenance, Food & Consumable supply, IT, Medical Supplies and Equipment, Motors & Vehicles, Office Furniture, Office Supplies"]]', }); }); router.get('/data-1row-part', (req, res) => { res.json({ content: '[["Bosco, Marks and Walker","9910412435"]]', }); }); return router; } module.exports = { xlsxDataRouting, }; ================================================ FILE: functional-tests/www/views/absolute/index.njs ================================================ {% extends 'layout/default.njs' %} {% block content %}

Absolute page

{% endblock %} ================================================ FILE: functional-tests/www/views/drag-and-drop/index.njs ================================================ {% extends 'layout/default.njs' %} {% block content %}
{% endblock %} ================================================ FILE: functional-tests/www/views/form/disappear.njs ================================================ {% block content %} {% endblock %} ================================================ FILE: functional-tests/www/views/form/select.njs ================================================ {% extends 'layout/default.njs' %} {% block content %}
{% endblock %} ================================================ FILE: functional-tests/www/views/form/simple.njs ================================================ {% extends 'layout/default.njs' %} {% block content %}
{% endblock %} ================================================ FILE: functional-tests/www/views/index.njs ================================================ {% extends 'layout/default.njs' %} {% block content %} {% endblock %} ================================================ FILE: functional-tests/www/views/layout/default.njs ================================================ Example app {% block content %} {% endblock %} ================================================ FILE: functional-tests/www/views/matchers/matchers.njs ================================================ {% block content %}

Date/Time:

{% endblock %} {% block javascript %} {% endblock javascript %} ================================================ FILE: functional-tests/www/views/navigation/page.njs ================================================ {% extends 'layout/default.njs' %} {% block content %}

{{ pageId }}

{{ title }}

{{ queryParam1 }}

{{ queryParam2 }}

{% endblock %} ================================================ FILE: functional-tests/www/views/table/tabular-data.njs ================================================ {% extends 'layout/default.njs' %} {% block content %}
1 4 MY_CUSTOM_ID_1 Some custom name 1
2 3 MY_CUSTOM_ID_2 Some custom name 2
3 2 MY_CUSTOM_ID_3 Some custom name 3
4 1 MY_CUSTOM_ID_4 Some custom name 4
{% endblock %} ================================================ FILE: functional-tests/www/views/wait-for-appear/form.njs ================================================ {% block content %}
{% endblock %} {% block javascript %} {% endblock javascript %} ================================================ FILE: functional-tests/www/views/wait-for-appear/table.njs ================================================ {% block content %}
{% endblock %} {% block javascript %} {% endblock javascript %} ================================================ FILE: package.json ================================================ { "name": "kakunin", "version": "3.0.3", "description": "End-to-end testing framework", "homepage": "https://thesoftwarehouse.github.io/Kakunin/", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/TheSoftwareHouse/Kakunin.git" }, "author": { "name": "The Software House", "url": "http://tsh.io" }, "contributors": [ { "name": "Adam Polak" }, { "name": "Mariusz Richtscheid" }, { "name": "Tomasz Górski" }, { "name": "Jakub Paluch" }, { "name": "Szymon Stanisz" }, { "name": "Wojciech Wójcik" }, { "name": "Józef Szymala" }, { "name": "Adam Nowrot" } ], "bin": { "kakunin": "./dist/cli.js" }, "main": "./dist/index", "scripts": { "units": "cross-env jest --setupFiles ./src/tests/init.ts --no-cache", "functional": "npm run build && cd functional-tests && rm -rf package-lock.json && rm -rf node_modules && npm i && npm run test", "test": "npm run units && npm run functional", "test-ci": "npm run units && npm run build && cd functional-tests && npm i && npm run test-ci", "lint": "tslint -c tslint.json 'src/**/*.ts'", "lint-fix": "tslint -c tslint.json 'src/**/*.ts' --fix", "prettier": "prettier --write 'src/**/*.ts'", "type-check": "tsc --noEmit", "build": "tsc", "prepublishOnly": "npm run build && npm run test" }, "engines": { "node": ">=7.7.3", "npm": ">=4.1.2" }, "devDependencies": { "@types/extend": "3.0.1", "@types/faker": "5.1.7", "@types/glob": "7.1.3", "@types/inquirer": "7.3.1", "@types/jest": "26.0.22", "@types/lodash": "4.14.168", "@types/minimist": "1.2.1", "@types/mkdirp": "1.0.1", "@types/moment": "2.13.0", "@types/node": "14.14.37", "@types/node-fetch": "2.5.8", "@types/protractor": "4.0.0", "@types/shelljs": "0.8.8", "@types/cucumber": "6.0.1", "fetch-mock": "9.11.0", "husky": "6.0.0", "lint-staged": "10.5.4", "prettier": "2.2.1", "ts-jest": "26.5.4", "tslint": "6.1.3", "tslint-config-prettier": "1.18.0", "tslint-sonarts": "1.9.0", "typescript": "4.2.3" }, "dependencies": { "ajv": "8.0.1", "browsermob-proxy": "1.0.10", "browserstack-local": "1.4.8", "chai": "4.3.4", "chalk": "4.1.0", "child_process": "1.0.2", "cross-env": "7.0.3", "cucumber": "6.0.5", "extend": "3.0.2", "faker": "5.5.1", "glob": "7.1.6", "inquirer": "8.0.0", "jasmine": "3.7.0", "jasmine-reporters": "2.4.0", "jasmine-spec-reporter": "6.0.0", "jest": "26.6.3", "lodash": "4.17.21", "minimist": "1.2.5", "mkdirp": "1.0.4", "moment": "2.29.1", "node-env-file": "0.1.8", "node-fetch": "2.6.1", "node-xlsx": "0.16.1", "path": "0.12.7", "protractor": "7.0.0", "protractor-cucumber-framework": "8.0.1", "protractor-multiple-cucumber-html-reporter-plugin": "1.8.1", "shelljs": "0.8.4", "sugar-date": "2.0.6", "webdriver-manager": "12.1.8" }, "jest": { "collectCoverageFrom": [ "src/**/*.ts", "!src/**/index.ts", "!src/**/*.d.ts" ], "transform": { "^.+\\.(t|j)sx?$": "ts-jest" }, "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", "moduleFileExtensions": [ "ts", "tsx", "js", "jsx", "json", "node" ], "globals": { "ts-jest": { "tsConfig": "tsconfig.test.json" } } }, "lint-staged": { "src/**/*.ts": [ "prettier --write", "npm run lint", "git add" ] }, "husky": { "hooks": { "pre-commit": "lint-staged" } } } ================================================ FILE: readme.md ================================================

kakunin.png

travis.png Current travis build: build statusnpm.png Current npm version: npm version

Key FeaturesDocumentationContributingIssuesRelatedAbout us

pageObjectFeature.gif

### **Automated testing framework** Kakunin is a Protractor extension created by The Software House sp. z o.o. and Takamol Holding. It allows you to write e2e test scenarios with a help of Gherkin language and JavaScript for all kind of applications - Angular, React and others. --- ### **Key Features:** 1. E2E testing 2. Performance testing 3. Parallel testing 4. Cross browser testing 5. Reports --- ### **Documentation:** You can find documentation on the **[official page](https://kakunin.io)**. --- ### **Contributing:** Feel free to contribute to this project! Just fork the code, make any updated and let us know! --- ### **Issues:** If you notice any issues while using, let as know on **[github](https://github.com/TheSoftwareHouse/Kakunin/issues)**. Security issues, please sent on email --- ### **You may also like our other projects:** - **[Babelsheet-js](https://github.com/TheSoftwareHouse/babelsheet-js)** - **[Fogger](https://github.com/TheSoftwareHouse/fogger)** --- ### **About us:**

The Software Housetsh.png

================================================ FILE: src/cli.ts ================================================ #!/usr/bin/env node import * as path from 'path'; import * as fs from 'fs'; import * as os from 'os'; import * as childProcess from 'child_process'; import * as envfile from 'node-env-file'; import { createTagsCLIArgument, filterCLIArguments, getConfigPath, isInitCommand } from './core/cli/cli.helper'; import initializer from './core/cli/initializer'; const commandArgs = require('minimist')(process.argv.slice(2)); envfile(process.cwd() + '/.env', { raise: false, overwrite: false }); if (isInitCommand(process.argv)) { (async () => { await initializer.initConfig(commandArgs); await initializer.generateProjectStructure(); })(); } else { const optionsToFilter = ['config', 'projectPath', 'disableChecks', 'tags']; const argv = [ './node_modules/kakunin/dist/protractor.conf.js', `--config=${getConfigPath('kakunin.conf.js', commandArgs.config, process.cwd())}`, `--projectPath=${process.cwd()}`, '--disableChecks', ...createTagsCLIArgument(commandArgs), ...filterCLIArguments(optionsToFilter)(commandArgs), ]; const protractorExecutable = os.platform() === 'win32' ? 'protractor.cmd' : 'protractor'; if ( !fs.existsSync( path.join( process.cwd(), 'node_modules', 'protractor', 'node_modules', 'webdriver-manager', 'selenium', 'update-config.json' ) ) ) { childProcess.execSync( path.join(process.cwd(), 'node_modules', '.bin', 'webdriver-manager update --ie --versions.standalone=3.14.0') ); } childProcess .spawn(path.join('node_modules', '.bin', protractorExecutable), argv, { stdio: 'inherit', cwd: process.cwd(), }) .once('exit', code => { console.log('Protractor has finished'); process.exit(code); }); } ================================================ FILE: src/comparators/comparator/date.comparator.spec.ts ================================================ import { DateComparator, supportedFormats } from './date.comparator'; describe('Date comparator', () => { it('is satisfied by dates', () => { const dates = ['20-12-2017', '20-12-17', '20/12/2017', '20/12/17']; expect(DateComparator.isSatisfiedBy(dates)).toEqual(true); }); it('supports only specified date formats', () => { const dates = ['12-20-2017', '20/20/17', 'not-a-date', 31337]; dates.forEach(date => expect(DateComparator.isSatisfiedBy([date])).toEqual(false)); }); it('is pluggable', () => { supportedFormats.push('MM-DD-YYYY'); expect(DateComparator.isSatisfiedBy(['12-20-2017'])).toEqual(true); }); it('it returns resolved promise if dates are in ascending order', done => { const ascendingDates = ['10-12-2017', '11-12-2017', '01-01-2018']; DateComparator.compare(ascendingDates, 'ascending').then(() => done()); }); it('it returns resolved promise if dates are in descending order', done => { const descendingDates = ['10-12-2019', '11-12-2018', '01-01-2017']; DateComparator.compare(descendingDates, 'descending').then(() => done()); }); it('it returns rejected promise if dates are not in ascending order for ascending order expectation', done => { const descendingDates = ['10-12-2019', '11-12-2018', '01-01-2017']; DateComparator.compare(descendingDates, 'ascending').catch(() => done()); }); it('it returns rejected promise if dates are not in descending order for descending order expectation', done => { const ascendingDates = ['10-12-2017', '11-12-2017', '01-01-2018']; DateComparator.compare(ascendingDates, 'descending').catch(() => done()); }); }); ================================================ FILE: src/comparators/comparator/date.comparator.ts ================================================ import * as moment from 'moment'; import { Comparator } from '../comparator.interface'; export const supportedFormats = ['DD-MM-YYYY', 'DD-MM-YY', 'DD/MM/YYYY', 'DD/MM/YY']; const isValidDate = date => { for (const format of supportedFormats) { if (moment(date, format).isValid()) { return true; } } return false; }; export const DateComparator: Comparator = { isSatisfiedBy: values => { for (const date of values) { const found = isValidDate(date); if (!found) { return false; } } return true; }, compare: (values, order) => { for (let i = 1; i < values.length; i++) { const datePrevious = values[i - 1]; const date = values[i]; const foundPrevious = moment( datePrevious, supportedFormats.find(format => moment(datePrevious, format).isValid()) ); const found = moment(date, supportedFormats.find(format => moment(date, format).isValid())); const previousTimestamp = foundPrevious.unix(); const currentTimestamp = found.unix(); if (order === 'ascending') { if (currentTimestamp < previousTimestamp) { return Promise.reject(`Date ${foundPrevious[1]} should be before ${found[1]}.`); } } else if (currentTimestamp > previousTimestamp) { return Promise.reject(`Date ${found[1]} should be after ${foundPrevious[1]}.`); } } return Promise.resolve(); }, }; ================================================ FILE: src/comparators/comparator/index.ts ================================================ export * from './date.comparator'; export * from './number.comparator'; ================================================ FILE: src/comparators/comparator/number.comparator.spec.ts ================================================ import { NumberComparator } from './number.comparator'; describe('Number comparator', () => { it('is satisfied by numbers', () => { const numbers = ['10', '10.09', '0.3']; expect(NumberComparator.isSatisfiedBy(numbers)).toEqual(true); }); it('supports only specified number records', () => { const numbers = ['string', '12-12-2017']; numbers.forEach(number => expect(NumberComparator.isSatisfiedBy([number])).toEqual(false)); }); it('it returns resolved promise if numbers are in ascending order', done => { const ascendingNumbers = ['0.25', '10', '102', '105', '125.32']; NumberComparator.compare(ascendingNumbers, 'ascending').then(() => done()); }); it('it returns resolved promise if numbers are in descending order', done => { const descendingNumbers = ['102', '100', '10.03']; NumberComparator.compare(descendingNumbers, 'descending').then(() => done()); }); it('it returns rejected promise if numbers are not in ascending order for ascending order expectation', done => { const descendingNumbers = [331, 223.03, '123', '11.04', 0.1]; NumberComparator.compare(descendingNumbers, 'ascending').catch(() => done()); }); it('it returns rejected promise if numbers are not in descending order for descending order expectation', done => { const ascendingNumbers = ['312', '324.43', '1323.320', 1321, 0.5]; NumberComparator.compare(ascendingNumbers, 'descending').catch(() => done()); }); it('it returns rejected promise if numbers are in incorrect format', done => { const containsIncorrectNumber = ['10', '999,32', '8dsa', '20/12/2018']; NumberComparator.compare(containsIncorrectNumber, 'ascending').catch(() => done()); }); }); ================================================ FILE: src/comparators/comparator/number.comparator.ts ================================================ import { Comparator } from '../comparator.interface'; export const NumberComparator: Comparator = { isSatisfiedBy: values => { for (const value of values) { const numberValue = Number(value); if (Number.isNaN(numberValue)) { return false; } } return true; }, compare: (values, order) => { for (let i = 1; i < values.length; i++) { const previousValue = Number(values[i - 1]); const currentValue = Number(values[i]); if (Number.isNaN(previousValue) || Number.isNaN(currentValue)) { return Promise.reject(`${values[i - 1]} and ${values[i]} cannot be NaN after conversion to Number`); } if (order === 'ascending') { if (previousValue > currentValue) { return Promise.reject(`${previousValue} should be lower than ${currentValue}`); } } else if (previousValue < currentValue) { return Promise.reject(`${previousValue} should be higher than ${currentValue}`); } } return Promise.resolve(); }, }; ================================================ FILE: src/comparators/comparator.interface.ts ================================================ export interface Comparator { isSatisfiedBy(value: any[]): boolean; compare(values: any[], order: string): Promise; } ================================================ FILE: src/comparators/comparators.spec.ts ================================================ import { create } from './comparators'; const comparators = create(); describe('Comparators', () => { it('throws an error when no comparator was found', () => { const values = ['unspecified-value', 'other-value']; expect(() => comparators.compare(values, 'ascending')).toThrow(`Could not find comparator for ${values}.`); }); it('compares values using comparator that can handle given set of values', done => { const numbers = [2, 3, 4]; comparators.compare(numbers, 'ascending').then(() => done()); }); it('add new comparator', done => { const myComparator = { isSatisfiedBy: values => { for (let i = 0; i < values.length; i++) { if (values[i] !== 'foo' && values[i] !== 'bar') { return false; } } return true; }, compare: (values, order) => { for (let i = 1; i < values.length; i++) { const previousValue = values[i - 1]; const currentValue = values[i]; if (previousValue === currentValue) { return Promise.reject('Wrong order'); } } return Promise.resolve('Foo bar!'); }, }; comparators.addComparator(myComparator); const myValues = ['foo', 'bar', 'foo', 'bar', 'foo', 'bar']; comparators.compare(myValues, 'any').then(value => { expect(value).toEqual('Foo bar!'); done(); }); }); }); ================================================ FILE: src/comparators/comparators.ts ================================================ import * as comparators from './comparator'; import { Comparator } from './comparator.interface'; class Comparators { constructor( private availableComparators: Comparator[] = [comparators.DateComparator, comparators.NumberComparator] ) {} public compare(values: any[], order: string): Promise { const comparator = this.findComparator(values); if (comparator === undefined) { throw new Error(`Could not find comparator for ${values}.`); } return comparator.compare(values, order); } public findComparator(values: any[]): Comparator { return this.availableComparators.find(comparator => comparator.isSatisfiedBy(values)); } public addComparator(comparator: Comparator): void { this.availableComparators.push(comparator); } } export const create = () => new Comparators(); ================================================ FILE: src/comparators/index.ts ================================================ import { create } from './comparators'; export const comparators = create(); ================================================ FILE: src/core/cli/cli.helper.spec.ts ================================================ import { isInitCommand, createTagsCLIArgument, getConfigPath, filterCLIArguments } from './cli.helper'; describe('Cli helpers', () => { it('returns false if missing parameters for init command', () => { expect(isInitCommand()).toEqual(false); }); it('returns false if args is not an array', () => { expect(isInitCommand('some-args')).toEqual(false); }); it('returns false when args length is lower than 2', () => { expect(isInitCommand(['arg1'])).toEqual(false); }); it('returns false when third argument is not init', () => { expect(isInitCommand(['arg1', 'arg2', 'arg3'])).toEqual(false); }); it('returns true when third argument is init', () => { expect(isInitCommand(['arg1', 'arg2', 'init'])).toEqual(true); }); it('returns default config path', () => { expect(getConfigPath('some-file.config.js', undefined, '/my/path')).toEqual('/my/path/some-file.config.js'); }); it('returns config path by config file', () => { expect(getConfigPath('some-file.config.js', 'other-config.ts', '/my/path')).toEqual('/my/path/other-config.ts'); }); it('creates empty tags cli argument if neither performance nor tags param is defined', () => { expect(createTagsCLIArgument({})).toEqual([]); }); it('creates cli argument without performance flag if only cucumber tags passed', () => { expect( createTagsCLIArgument({ tags: '@some-tag and @other-tag', }) ).toEqual(['--cucumberOpts.tags', '@some-tag and @other-tag']); }); it('creates cli argument with performance flag but without performance tag and without cucumber tags', () => { expect( createTagsCLIArgument({ performance: true, }) ).toEqual(['--cucumberOpts.tags', '@performance']); }); it('creates cli argument with performance flag but without performance tag and with cucumber tags', () => { expect( createTagsCLIArgument({ performance: true, tags: '@some-tag and @other-tag', }) ).toEqual(['--cucumberOpts.tags', '@some-tag and @other-tag and @performance']); }); it('creates cli argument with performance flag but with performance tag and with cucumber tags', () => { expect( createTagsCLIArgument({ performance: true, tags: '@some-tag and @other-tag and @performance', }) ).toEqual(['--cucumberOpts.tags', '@some-tag and @other-tag and @performance']); }); it('adds only not black listed arguments', () => { expect( filterCLIArguments(['myArg'])({ myArg: true, otherArgument: false, }) ).toEqual(['--otherArgument']); }); it('adds bool not black listed arguments without parameters', () => { expect( filterCLIArguments(['myArg'])({ myArg: true, otherArgument: false, }) ).toEqual(['--otherArgument']); }); it('adds string not black listed arguments with parameters', () => { expect( filterCLIArguments(['myArg'])({ myArg: 'some-value', otherArgument: 'some-value', }) ).toEqual(['--otherArgument=some-value']); }); }); ================================================ FILE: src/core/cli/cli.helper.ts ================================================ import * as path from 'path'; export const isInitCommand = (args?: any[] | string) => { if (Array.isArray(args)) { return args.length > 2 && args[2] === 'init'; } return false; }; export const getConfigPath = (configFile, argsConfig, basePath) => { return argsConfig ? path.join(basePath, argsConfig) : path.join(basePath, configFile); }; export const createTagsCLIArgument = commandArgs => { const tags = []; if (commandArgs.performance) { if (commandArgs.tags !== undefined && commandArgs.tags.indexOf('@performance') < 0) { tags.push('--cucumberOpts.tags'); tags.push(`${commandArgs.tags} and @performance`); } else if (commandArgs.tags === undefined) { tags.push('--cucumberOpts.tags'); tags.push('@performance'); } else { tags.push('--cucumberOpts.tags'); tags.push(commandArgs.tags); } } else if (commandArgs.tags !== undefined) { tags.push('--cucumberOpts.tags'); tags.push(commandArgs.tags); } return tags; }; export const filterCLIArguments = blackList => commandArgs => { const commandLineArgs = []; for (const prop in commandArgs) { if (prop !== '_' && !blackList.includes(prop)) { if (commandArgs[prop] === true || commandArgs[prop] === false) { commandLineArgs.push(`--${prop}`); } else { commandLineArgs.push(`--${prop}=${commandArgs[prop]}`); } } } return commandLineArgs; }; ================================================ FILE: src/core/cli/initializer.ts ================================================ import * as fs from 'fs'; import * as inquirer from 'inquirer'; import * as mkdirp from 'mkdirp'; import * as path from 'path'; class Initializer { public createProjectDirectory(dirPath) { const projectPath = process.cwd() + dirPath; mkdirp(projectPath, null); console.log(`Created directory at path ${projectPath}`); } public createTemplateFile(templatePath, content) { const filePath = process.cwd() + templatePath; fs.writeFileSync(filePath, content); console.log(`Created file at path ${filePath}`); } public createTemplateFileWithContentFrom(contentPath, file) { const content = fs.readFileSync(path.join(__dirname, `../../../templates/${file}`)); this.createTemplateFile(contentPath, content); } public async promptFolders(message, defaultValue, type = 'input') { let fullMessage = message; if (defaultValue !== '') { fullMessage += ` [${defaultValue}]`; } return inquirer .prompt([ { type, name: 'input', message: fullMessage, }, ]) .then((answer: any): any => (answer.input === '' ? defaultValue : answer.input)); } public async initConfig(commandArgs) { const conf = { baseUrl: '', type: 'otherWeb', browserWidth: 1600, browserHeight: 900, timeout: 60, intervalEmail: 5, maxEmailRepeats: 5, elementsVisibilityTimeout: 5, waitForPageTimeout: 5, downloadTimeout: 30, emails: ['/emails'], reports: '/reports', downloads: '/downloads', data: '/data', features: ['/features'], pages: ['/pages'], matchers: ['/matchers'], generators: ['/generators'], form_handlers: ['/form_handlers'], step_definitions: ['/step_definitions'], comparators: ['/comparators'], dictionaries: ['/dictionaries'], transformers: ['/transformers'], regexes: ['/regexes'], hooks: ['/hooks'], clearEmailInboxBeforeTests: false, clearCookiesAfterScenario: true, clearLocalStorageAfterScenario: true, email: null, headless: false, noGpu: false, browserMob: null, accounts: null, }; if (typeof commandArgs.baseUrl === 'undefined') { conf.baseUrl = await this.promptFolders('What is base url?', 'http://localhost:3000'); } else { conf.baseUrl = commandArgs.baseUrl; } if (typeof commandArgs.emailType === 'undefined') { await inquirer .prompt([ { type: 'rawlist', name: 'type', message: 'What kind of email service would you like to use?', choices: [ { name: 'None', value: 'none' }, { name: 'Custom (you will have to fill configuration on your own)', value: 'custom' }, { name: 'MailTrap', value: 'mailtrap' }, ], }, ]) .then((answer: any) => { if (answer.type !== 'none') { conf.email = { type: answer.type, }; } }); } else { conf.email = { type: commandArgs.emailType, }; } if (conf.email && conf.email.type === 'mailtrap') { conf.email = { ...conf.email, config: { url: 'https://mailtrap.io', apiKey: '', inboxId: '', }, }; if (typeof commandArgs.emailApiKey === 'undefined') { conf.email.config.apiKey = await this.promptFolders('Type in your mailtrap apikey:', conf.email.config.apiKey); } else { conf.email.config.apiKey = commandArgs.emailApiKey; } if (typeof commandArgs.emailInboxId === 'undefined') { conf.email.config.inboxId = await this.promptFolders( 'Type in your mailtrap inboxId:', conf.email.config.inboxId ); } else { conf.email.config.inboxId = commandArgs.emailInboxId; } } if (commandArgs.advanced) { await this.initEnv(); conf.browserWidth = parseInt(await this.promptFolders('What is desired browser width?', conf.browserWidth)); conf.browserHeight = parseInt(await this.promptFolders('What is desired browser height?', conf.browserHeight)); conf.timeout = parseInt(await this.promptFolders('What is desired step timeout in seconds?', conf.timeout)); conf.intervalEmail = parseInt( await this.promptFolders('What is desired step email interval in seconds?', conf.intervalEmail) ); conf.maxEmailRepeats = parseInt( await this.promptFolders('How many times emails should be checked - maximum repeats?', conf.maxEmailRepeats) ); conf.elementsVisibilityTimeout = parseInt( await this.promptFolders( 'What is desired elements visibility timeout in seconds?', conf.elementsVisibilityTimeout ) ); conf.waitForPageTimeout = parseInt( await this.promptFolders('How long should I wait for page to load in seconds?', conf.waitForPageTimeout) ); conf.downloadTimeout = parseInt( await this.promptFolders('How long should I wait for files to download in seconds?', conf.downloadTimeout) ); conf.reports = await this.promptFolders('Where are your reports stored?', conf.reports); conf.downloads = await this.promptFolders('Where are your downloads stored?', conf.downloads); conf.data = await this.promptFolders('Where is your data stored?', conf.data); conf.features = [await this.promptFolders('Where are your features stored?', conf.features[0])]; conf.pages = [await this.promptFolders('Where are your pages stored?', conf.pages[0])]; conf.matchers = [await this.promptFolders('Where are your matchers stored?', conf.matchers[0])]; conf.generators = [await this.promptFolders('Where are your generators stored?', conf.generators[0])]; conf.form_handlers = [await this.promptFolders('Where are your form handlers stored?', conf.form_handlers[0])]; conf.step_definitions = [ await this.promptFolders('Where are your step definitions stored?', conf.step_definitions[0]), ]; conf.comparators = [await this.promptFolders('Where are your comparators stored?', conf.comparators[0])]; conf.dictionaries = [await this.promptFolders('Where are your dictionaries stored?', conf.dictionaries[0])]; conf.regexes = [await this.promptFolders('Where are your regexes stored?', conf.regexes[0])]; conf.hooks = [await this.promptFolders('Where are your hooks stored?', conf.hooks[0])]; conf.transformers = [await this.promptFolders('Where are your transformers stored?', conf.transformers[0])]; conf.clearEmailInboxBeforeTests = await this.promptFolders( 'Should email inbox be cleared before tests?', conf.clearEmailInboxBeforeTests, 'confirm' ); conf.clearCookiesAfterScenario = await this.promptFolders( 'Should cookies be cleared after scenario?', conf.clearCookiesAfterScenario, 'confirm' ); conf.clearLocalStorageAfterScenario = await this.promptFolders( 'Should local storage be cleared after scenario?', conf.clearLocalStorageAfterScenario, 'confirm' ); conf.browserMob = { serverPort: parseInt(await this.promptFolders('Define port where browsermob-proxy is running!', 8887)), port: parseInt(await this.promptFolders('Define port where browsermob-proxy should be listening!', 8888)), host: await this.promptFolders('Define host where browsermob-proxy is running!', 'localhost'), }; } conf.accounts = { someAccount: { accounts: [ { email: '', password: '', }, ], }, }; this.createTemplateFile('/kakunin.conf.js', 'module.exports = ' + JSON.stringify(conf, null, 4)); } public async initEnv() { const envs = []; envs.push('FIXTURES_RELOAD_HOST=' + (await this.promptFolders('Define FIXTURES_RELOAD_HOST', ''))); this.createTemplateFile('/.env', envs.join('\n')); } public async generateProjectStructure() { const config = require(process.cwd() + '/kakunin.conf.js'); this.createProjectDirectory(config.reports); this.createProjectDirectory(path.join(config.reports, 'report')); this.createProjectDirectory(path.join(config.reports, 'json-output-folder')); this.createProjectDirectory(path.join(config.reports, 'report', 'features')); this.createProjectDirectory(path.join(config.reports, 'performance')); this.createProjectDirectory(config.downloads); this.createProjectDirectory(config.data); this.createProjectDirectory(config.features[0]); this.createProjectDirectory(config.pages[0]); this.createProjectDirectory(config.matchers[0]); this.createProjectDirectory(config.generators[0]); this.createProjectDirectory(config.form_handlers[0]); this.createProjectDirectory(config.step_definitions[0]); this.createProjectDirectory(config.comparators[0]); this.createProjectDirectory(config.dictionaries[0]); this.createProjectDirectory(config.regexes[0]); this.createProjectDirectory(config.hooks[0]); this.createProjectDirectory(config.transformers[0]); this.createProjectDirectory(config.emails[0]); this.createTemplateFile(path.join(config.downloads, '.gitkeep'), ''); this.createTemplateFile(path.join(config.reports, 'json-output-folder', '.gitkeep'), ''); this.createTemplateFile(path.join(config.reports, 'report', '.gitkeep'), ''); this.createTemplateFile(path.join(config.reports, 'report', 'features', '.gitkeep'), ''); this.createTemplateFile(path.join(config.reports, 'performance', '.gitkeep'), ''); this.createTemplateFileWithContentFrom(config.features[0] + '/example.feature', 'example.feature'); this.createTemplateFileWithContentFrom(config.pages[0] + '/page.js', 'page.js'); this.createTemplateFileWithContentFrom(config.matchers[0] + '/matcher.js', 'matcher.js'); this.createTemplateFileWithContentFrom(config.generators[0] + '/generator.js', 'generator.js'); this.createTemplateFileWithContentFrom(config.step_definitions[0] + '/steps.js', 'steps.js'); this.createTemplateFileWithContentFrom(config.regexes[0] + '/regex.js', 'regex.js'); this.createTemplateFileWithContentFrom(config.hooks[0] + '/hook.js', 'hook.js'); } } export default new Initializer(); ================================================ FILE: src/core/config.helper.ts ================================================ const commandArgs = require('minimist')(process.argv.slice(2)); let config; if (process.env.NODE_ENV === 'test') { config = { projectPath: process.cwd(), email: { config: {}, }, }; } else { const configFile = process.argv.find(name => name.indexOf('--config') >= 0); const configFilePath = configFile.substr(configFile.indexOf('=') + 1); const project = process.argv.find(name => name.indexOf('--projectPath') >= 0); const projectPath = project.substr(project.indexOf('=') + 1); config = require(configFilePath); config.projectPath = projectPath; config.performance = commandArgs.performance || false; } export default config; ================================================ FILE: src/core/fs/delete-files.helper.ts ================================================ import * as fs from 'fs'; import * as path from 'path'; export const deleteReports = directory => { return fs .readdirSync(directory) .filter(file => fs.statSync(path.join(directory, file)).isFile() && file !== '.gitkeep') .forEach(file => fs.unlinkSync(path.join(directory, file))); }; ================================================ FILE: src/core/fs/prepare-catalogs.helper.ts ================================================ import * as fs from 'fs'; import * as mkdirp from 'mkdirp'; export const prepareCatalogs = directory => { if (fs.existsSync(directory)) { return Promise.resolve(); } mkdirp(directory, null); console.log(`${directory} has been added!`); fs.writeFileSync(`${directory}/.gitkeep`, ''); }; ================================================ FILE: src/core/modules-loader.helper.ts ================================================ import * as fs from 'fs'; import * as path from 'path'; import config from './config.helper'; class ModulesLoader { private paths: any; constructor(configuration) { this.paths = { comparators: [], dictionaries: [], form_handlers: [], generators: [], matchers: [], regexes: [], transformers: [], emails: [], hooks: [], }; Object.keys(this.paths).forEach(group => { if (typeof config[group] !== 'undefined') { configuration[group].forEach(groupPath => { this.paths[group].push(path.join(configuration.projectPath + groupPath)); }); } }); } public getModules(group) { return this.getFilePaths(this.paths[group]).map(file => require(file[1])); } public getModulesAsObject(projectFolders) { const modules = {}; const filePaths = this.getFilePaths(projectFolders); filePaths.forEach(file => { modules[file[0]] = require(file[1]); }); return modules; } public getFilePaths(folders) { let files = []; folders.forEach(folder => { if (fs.existsSync(folder)) { files = files.concat( fs .readdirSync(folder) .filter(file => file !== '.gitkeep' && file.indexOf('.spec.js') < 0) .map(file => [file.substr(0, file.indexOf('.')), `${folder}/${file}`]) ); } else { console.log(`Directory ${folder} does not exist.`); } }); return files; } } export const create = (configuration = config) => new ModulesLoader(configuration); ================================================ FILE: src/core/prototypes.ts ================================================ RegExp.escape = (text): string => { return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); }; ================================================ FILE: src/dictionaries/base.ts ================================================ class BaseDictionary { public readonly name: string; public readonly values: object; constructor(name, values) { this.name = name; this.values = values; } public isSatisfiedBy(name: string): boolean { return this.name === name; } public getMappedValue(key: string): string { return this.values[key]; } } export default BaseDictionary; ================================================ FILE: src/dictionaries/dictionaries.spec.ts ================================================ import { createDictionary as create, Base as BaseDictionary } from './'; import fakeDictionary = require('../tests/dictionaries/fake-dictionary'); const dictionaries = create(); dictionaries.addDictionary(fakeDictionary); describe('Dictionaries', () => { it('throws an error when no dictionary was found', () => { expect(() => dictionaries.getMappedValue('unknown-dictionary', 'unknown-key')).toThrow( 'Could not find dictionary for unknown-dictionary.' ); }); it('returns mapped value for given key', () => { expect(dictionaries.getMappedValue('fake-dictionary', 'some-key')).toEqual('some-value'); }); it('returns mapped value for given key - directly from dictionaries', () => { expect(dictionaries.findMappedValueByPhrase('d:fake-dictionary:some-key')).toEqual('some-value'); }); it('adds a dictionary', () => { const customDictionary = new BaseDictionary('my-dictionary', { 'some-key': 'mapped-some-key' }); dictionaries.addDictionary(customDictionary); expect(dictionaries.getMappedValue('my-dictionary', 'some-key')).toEqual('mapped-some-key'); }); }); ================================================ FILE: src/dictionaries/dictionaries.ts ================================================ import Dictionary from './base'; export class Dictionaries { constructor(private availableDictionaries: Dictionary[] = []) {} public getMappedValue(dictionaryName: string, key: string): string { const dic = this.findDictionary(dictionaryName); if (dic === undefined) { throw new Error(`Could not find dictionary for ${dictionaryName}.`); } return dic.getMappedValue(key); } public findDictionary(name: string): Dictionary { return this.availableDictionaries.find(dic => dic.isSatisfiedBy(name)); } public findMappedValueByPhrase(phrase: string): string { const parameters = phrase.split(':'); if (parameters[0] === 'd') { const dictionary = this.findDictionary(parameters[1]); if (dictionary) { return this.getMappedValue(parameters[1], parameters[2]); } } return phrase; } public addDictionary(dictionary: Dictionary): void { this.availableDictionaries.push(dictionary); } } export const create = () => new Dictionaries(); ================================================ FILE: src/dictionaries/index.ts ================================================ import BaseDictionary from './base'; import { create } from './dictionaries'; export { Dictionaries } from './dictionaries'; export const createDictionary = create; export const dictionaries = create(); export const Base = BaseDictionary; ================================================ FILE: src/emails/adapter/mailtrap.client.spec.ts ================================================ import { create } from './mailtrap.client'; import * as fetchMock from 'fetch-mock'; describe('Mailtrap client', () => { it('it returns mailtrap config', () => { const mailtrapClient = create(undefined, { config: { apiKey: 'fake-api-key', inboxId: 'fake-inbox-id', url: 'http://fake-url.com', }, }); expect(mailtrapClient.getMailtrapConfig()).toEqual({ apiKey: 'fake-api-key', inboxId: 'fake-inbox-id', endpoint: 'http://fake-url.com', }); }); it('clears inbox', done => { const apiKey = 'fake-api-key'; const inbox = 'fake-inbox-id'; const url = 'http://fake-url.com'; const requestMock: any = fetchMock .sandbox() .mock(`${url}/api/v1/inboxes/${inbox}/clean?api_token=${apiKey}`, { data: 'cleared' }, { method: 'PATCH' }); const mailtrapClient = create(requestMock, { config: { apiKey: apiKey, inboxId: inbox, url: url, }, }); mailtrapClient.clearInbox().then(res => { expect(res).toEqual({ data: 'cleared' }); done(); }); }); it('returns not read emails', done => { const apiKey = 'fake-api-key'; const inbox = 'fake-inbox-id'; const url = 'http://fake-url.com'; const emails = [ { subject: 's2', is_read: false, raw_path: `/api/v1/inboxes/${inbox}/messages/000000001/body.raw` }, { subject: 's3', is_read: true, raw_path: `/api/v1/inboxes/${inbox}/messages/000000002/body.raw` }, { subject: 's1', is_read: false, raw_path: `/api/v1/inboxes/${inbox}/messages/000000003/body.raw` }, ]; const requestMock: any = fetchMock .sandbox() .mock(`${url}/api/v1/inboxes/${inbox}/messages?api_token=${apiKey}`, emails, { method: 'GET' }) .mock(`${url}${emails[0].raw_path}?api_token=${apiKey}`, { method: 'GET' }) .mock(`${url}${emails[1].raw_path}?api_token=${apiKey}`, { method: 'GET' }) .mock(`${url}${emails[2].raw_path}?api_token=${apiKey}`, { method: 'GET' }); const mailtrapClient = create(requestMock, { config: { apiKey: apiKey, inboxId: inbox, url: url, }, }); mailtrapClient.getEmails().then(res => { expect(res.length).toEqual(2); res.forEach(email => expect(email.is_read).toEqual(false)); done(); }); }); it('returns email attachments', done => { const apiKey = 'fake-api-key'; const inbox = 'fake-inbox-id'; const url = 'http://fake-url.com'; const emailId = 'some-id'; const requestMock: any = fetchMock .sandbox() .mock( `${url}/api/v1/inboxes/${inbox}/messages/${emailId}/attachments?api_token=${apiKey}`, [{ id: 1, name: 'some-file', content: 'some-content' }], { method: 'GET', } ); const mailtrapClient = create(requestMock, { config: { apiKey: apiKey, inboxId: inbox, url: url, }, }); mailtrapClient.getAttachments({ id: emailId }).then(res => { expect(res).toEqual([{ id: 1, name: 'some-file', content: 'some-content' }]); done(); }); }); it('marks email as read', done => { const apiKey = 'fake-api-key'; const inbox = 'fake-inbox-id'; const url = 'http://fake-url.com'; const emailId = 'some-id'; const requestMock: any = fetchMock.sandbox().mock( (reqUrl, opts) => { return ( reqUrl === `${url}/api/v1/inboxes/${inbox}/messages/${emailId}?api_token=${apiKey}` && opts.body === JSON.stringify({ message: { is_read: true } }) ); }, { data: 'marked-as-read' }, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, } ); const mailtrapClient = create(requestMock, { config: { apiKey: apiKey, inboxId: inbox, url: url, }, }); mailtrapClient.markAsRead({ id: emailId }).then(res => { expect(res).toEqual({ data: 'marked-as-read' }); done(); }); }); }); ================================================ FILE: src/emails/adapter/mailtrap.client.ts ================================================ import * as fetch from 'node-fetch'; import configuration from '../../core/config.helper'; class MailTrapClient { private requestClient: any; private config: any; constructor(requestClient, config) { this.requestClient = requestClient; this.config = config; } public isSatisfiedBy(emailConfiguration) { return ( emailConfiguration.type === 'mailtrap' && emailConfiguration.config.hasOwnProperty('apiKey') && emailConfiguration.config.hasOwnProperty('inboxId') && emailConfiguration.config.hasOwnProperty('url') ); } public getMailtrapConfig() { return { apiKey: this.config.config.apiKey, inboxId: this.config.config.inboxId, endpoint: this.config.config.url, }; } public clearInbox() { const config = this.getMailtrapConfig(); const url = `${config.endpoint}/api/v1/inboxes/${config.inboxId}/clean?api_token=${config.apiKey}`; return this.requestClient(url, { method: 'PATCH', }).then(res => { if (res.status !== 200) { throw new Error(res); } return res.json(); }); } public async getEmails() { const config = this.getMailtrapConfig(); const url = `${config.endpoint}/api/v1/inboxes/${config.inboxId}/messages?api_token=${config.apiKey}`; const messages = await this.requestClient(url).then(res => { if (res.status !== 200) { throw new Error(res); } return res.json(); }); const messagesWithBody = []; for (const message of messages) { const rawBody = await this.requestClient(`${config.endpoint}${message.raw_path}?api_token=${config.apiKey}`).then( res => res.text() ); messagesWithBody.push({ ...message, html_body: rawBody, }); } return messagesWithBody.filter(message => !message.is_read); } public getAttachments(email) { const config = this.getMailtrapConfig(); const url = `${config.endpoint}/api/v1/inboxes/${config.inboxId}/messages/${email.id}/attachments?api_token=${ config.apiKey }`; return this.requestClient(url).then(res => { if (res.status !== 200) { throw new Error(res); } return res.json(); }); } public markAsRead(email) { const config = this.getMailtrapConfig(); const url = `${config.endpoint}/api/v1/inboxes/${config.inboxId}/messages/${email.id}?api_token=${config.apiKey}`; return this.requestClient(url, { method: 'PATCH', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ message: { is_read: true, }, }), }).then(res => { if (res.status !== 200) { throw new Error(res); } return res.json(); }); } } export const create = (requestClient = fetch, config = configuration.email) => { return new MailTrapClient(requestClient, config); }; ================================================ FILE: src/emails/email.service.spec.ts ================================================ import { create } from './email.service'; describe('Email service', () => { it('it returns adapter if found', () => { const fakeAdapter = { isSatisfiedBy: (config: any) => true, }; const fakeConfig = { email: { type: 'some-service-type', }, }; const emailService = create([fakeAdapter], fakeConfig); expect(emailService.getAdapter()).toBe(fakeAdapter); }); it('it throws error if adapter not found', () => { const fakeConfig = { email: { type: 'some-service-type', }, }; const emailService = create([], fakeConfig); expect(() => emailService.getAdapter()).toThrow('Could not find email adapter for given configuration.'); }); it('it adds adapter', () => { const fakeAdapter = { isSatisfiedBy: config => true, }; const fakeConfig = { email: { type: 'some-service-type', }, }; const emailService = create([], fakeConfig); expect(() => emailService.getAdapter()).toThrow('Could not find email adapter for given configuration.'); emailService.addAdapter(fakeAdapter); expect(emailService.getAdapter()).toEqual(fakeAdapter); }); it('it calls adapter clearInbox method', done => { const fakeAdapter = { isSatisfiedBy: config => true, clearInbox: () => Promise.resolve(), }; const fakeConfig = { email: { type: 'some-service-type', }, }; const emailService = create([fakeAdapter], fakeConfig); emailService.clearInbox().then(() => done()); }); it('it calls adapter getEmails method', done => { const fakeAdapter = { isSatisfiedBy: config => true, getEmails: () => Promise.resolve(), }; const fakeConfig = { email: { type: 'some-service-type', }, }; const emailService = create([fakeAdapter], fakeConfig); emailService.getEmails().then(() => done()); }); it('it calls adapter markAsRead method', done => { const fakeAdapter = { isSatisfiedBy: config => true, markAsRead: email => Promise.resolve(email), }; const fakeConfig = { email: { type: 'some-service-type', }, }; const emailService = create([fakeAdapter], fakeConfig); emailService.markAsRead('some@email.com').then(email => { expect(email).toEqual('some@email.com'); done(); }); }); it('it calls adapter getAttachments method', done => { const fakeAdapter = { isSatisfiedBy: config => true, getAttachments: email => Promise.resolve('some-attachment'), }; const fakeConfig = { email: { type: 'some-service-type', }, }; const emailService = create([fakeAdapter], fakeConfig); emailService.getAttachments('some@email.com').then(attachment => { expect(attachment).toEqual('some-attachment'); done(); }); }); }); ================================================ FILE: src/emails/email.service.ts ================================================ import configuration from '../core/config.helper'; import { create as createMailtrapAdapter } from './adapter/mailtrap.client'; class EmailService { private config: any; private availableAdapters: any; constructor(config, defaultAdapters) { this.config = config; this.availableAdapters = defaultAdapters; } public clearInbox() { const adapter = this.getAdapter(); return adapter.clearInbox(); } public getEmails() { const adapter = this.getAdapter(); return adapter.getEmails(); } public getAttachments(email) { const adapter = this.getAdapter(); return adapter.getAttachments(email); } public markAsRead(email) { const adapter = this.getAdapter(); return adapter.markAsRead(email); } public addAdapter(adapter) { this.availableAdapters.push(adapter); } public getAdapter() { const emailAdapter = this.availableAdapters.find(adapter => adapter.isSatisfiedBy(this.config.email)); if (emailAdapter === undefined) { throw new Error('Could not find email adapter for given configuration.'); } return emailAdapter; } } const mailtrapAdapter = createMailtrapAdapter(); export const create = (defaultAdapters: any = [mailtrapAdapter], config = configuration) => { return new EmailService(config, defaultAdapters); }; ================================================ FILE: src/emails/filter/current-user.filter.ts ================================================ // TODO: stop injecting world here and use some kind of a user provider class CurrentUserFilter { public isSatisfiedBy(type) { return type === 'currentUser'; } public filter(emails, type, value, world) { return emails.filter(email => email.to_email === world.currentUser.account.email); } } export const currentUserFilter = new CurrentUserFilter(); ================================================ FILE: src/emails/filter/current-user.spec.ts ================================================ const { currentUserFilter } = require('./current-user.filter'); const fakeWorld = { currentUser: { account: { email: 'some@email.com', }, }, }; describe('Current user filter', () => { it('returns true when can be use', () => { expect(currentUserFilter.isSatisfiedBy('currentUser')).toEqual(true); }); it('returns false when cannot be use', () => { expect(currentUserFilter.isSatisfiedBy('unsupportedName')).toEqual(false); }); it('returns only emails for current logged user', () => { const fakeEmails = [ { to_email: 'unknown@user.com' }, { to_email: 'some@email.com' }, { to_email: 'unknown@user.com' }, { to_email: 'some@email.com' }, ]; const filteredEmails = currentUserFilter.filter(fakeEmails, 'currentUser', undefined, fakeWorld); expect(filteredEmails.length).toEqual(2); filteredEmails.forEach(email => expect(email.to_email).toEqual(fakeWorld.currentUser.account.email)); }); }); ================================================ FILE: src/emails/filter/index.ts ================================================ export * from './current-user.filter'; export * from './minimal-email-size.filter'; export * from './text-fields.filter'; ================================================ FILE: src/emails/filter/minimal-email-size.filter.spec.ts ================================================ const { minimalEmailSizeFilter } = require('./minimal-email-size.filter.ts'); describe('Minimal email size filter', () => { it('returns true when can be use', () => { expect(minimalEmailSizeFilter.isSatisfiedBy('minimalEmailSize')).toEqual(true); }); it('returns false when cannot be use', () => { expect(minimalEmailSizeFilter.isSatisfiedBy('unsupportedName')).toEqual(false); }); it('returns only emails with a minimum size', () => { const fakeEmails = [{ email_size: 400 }, { email_size: 500 }, { email_size: 666 }, { email_size: 30 }]; const filteredEmails = minimalEmailSizeFilter.filter(fakeEmails, 'minimalEmailSize', 500); expect(filteredEmails.length).toEqual(2); filteredEmails.forEach(email => expect(email.email_size).toBeGreaterThanOrEqual(500)); }); }); ================================================ FILE: src/emails/filter/minimal-email-size.filter.ts ================================================ class MinimalEmailSizeFilter { public isSatisfiedBy(type) { return type === 'minimalEmailSize'; } public filter(emails, type, value) { return emails.filter(email => email.email_size >= parseInt(value)); } } export const minimalEmailSizeFilter = new MinimalEmailSizeFilter(); ================================================ FILE: src/emails/filter/text-fields.filter.spec.ts ================================================ import { textFieldFilter } from './text-fields.filter'; import variableStore from '../../web/variable-store.helper'; describe('Text fields filter', () => { it('returns true when supported typ passed', () => { const supportedTypes = ['subject', 'from_email', 'from_name', 'to_email', 'to_name', 'html_body', 'text_body']; supportedTypes.forEach(type => expect(textFieldFilter.isSatisfiedBy(type)).toEqual(true)); }); it('returns false when not supported type passed', () => { expect(textFieldFilter.isSatisfiedBy('unsupportedName')).toEqual(false); }); it('throws an error when value is not a regex or text matcher', () => { expect(() => textFieldFilter.filter(['some email content'], 'subject', 'some-value')).toThrow( 'Comparison type not specified. Please use r: for regex and t: for text' ); }); it('returns only emails matching given value using t:v:variableName', () => { const fakeEmails = [ { subject: 'some-subject', from_email: 'some@user.com', from_name: 'Username', to_email: 'other@user.com', to_name: 'ToUsername', html_body: 'Body 123', text_body: 'Body 123', }, { subject: 'other-subject', from_email: 'my@user.com', from_name: 'My Username', to_email: 'to@user.com', to_name: 'Username123', html_body: 'Complicated body 12.12.2016', text_body: 'Complicated body 12.12.2016', }, ]; variableStore.storeVariable('someVariable', 'some-subject'); const filteredEmails = textFieldFilter.filter(fakeEmails, 'subject', 't:v:someVariable'); expect(filteredEmails.length).toEqual(1); filteredEmails.forEach(email => expect(email.subject === 'some-subject').toEqual(true)); }); it('returns only emails matching given value using r:regexpName', () => { const fakeEmails = [ { subject: 'some-subject', from_email: 'some@user.com', from_name: 'Username', to_email: 'other@user.com', to_name: 'ToUsername', html_body: 'Body 123', text_body: 'Body 123', }, { subject: 'other-subject', from_email: 'my@user.com', from_name: 'My Username', to_email: 'to@user.com', to_name: 'Username123', html_body: 'Complicated body', text_body: 'Complicated body', }, ]; const filteredEmails = textFieldFilter.filter(fakeEmails, 'text_body', 'r:number'); expect(filteredEmails.length).toEqual(1); filteredEmails.forEach(email => expect(email.text_body === 'Body 123').toEqual(true)); }); }); ================================================ FILE: src/emails/filter/text-fields.filter.ts ================================================ import { regexBuilder } from '../../matchers'; import variableStore from '../../web/variable-store.helper'; class TextFieldFilter { public isSatisfiedBy(type) { return ['subject', 'from_email', 'from_name', 'to_email', 'to_name', 'html_body', 'text_body'].indexOf(type) !== -1; } public filter(emails, type, value) { return emails.filter(email => { if (value.startsWith('r:')) { return regexBuilder.buildRegex(value).test(email[type]); } if (value.startsWith('t:')) { return new RegExp(RegExp.escape(variableStore.replaceTextVariables(value.substr(2)))).test(email[type]); } throw new Error('Comparison type not specified. Please use r: for regex and t: for text'); }); } } export const textFieldFilter = new TextFieldFilter(); ================================================ FILE: src/emails/filters.spec.ts ================================================ import { filters } from './filters'; const world = { currentUser: { account: { email: 'some@email.com', }, }, }; describe('Email filters', () => { it('throws an error when no filter was found', () => { expect(() => filters.filter(['some email content'], 'unknown-filter-type', 'some-value', world)).toThrow( 'Could not find filter for unknown-filter-type.' ); }); it('return emails matching given filter', () => { const emails = [ { to_email: 'other@email.com' }, { to_email: 'some@email.com' }, { to_email: 'other@email.com' }, { to_email: 'some@email.com' }, { to_email: 'other@email.com' }, { to_email: 'some@email.com' }, ]; const filteredEmails = filters.filter(emails, 'currentUser', undefined, world); expect(filteredEmails.length).toEqual(3); filteredEmails.forEach(email => expect(email.to_email).toEqual('some@email.com')); }); }); ================================================ FILE: src/emails/filters.ts ================================================ import * as defaultFilters from './filter'; class Filters { private availableFilters: any; constructor() { this.availableFilters = [ defaultFilters.currentUserFilter, defaultFilters.minimalEmailSizeFilter, defaultFilters.textFieldFilter, ]; } public filter(emails, type, value, world) { const filter = this.findFilter(type); if (typeof filter === 'undefined') { throw new Error(`Could not find filter for ${type}.`); } return filter.filter(emails, type, value, world); } public findFilter(type) { return this.availableFilters.find(filter => filter.isSatisfiedBy(type)); } } export const filters = new Filters(); ================================================ FILE: src/emails/index.ts ================================================ import { create } from './email.service'; export const emailService = create(); ================================================ FILE: src/form-handlers/form-handler.interface.ts ================================================ export interface FormHandler { isSatisfiedBy(element?: object, elementName?: string): Promise; handleFill(page: object, elementName: string, desiredValue: string): Promise; handleCheck(page: object, elementName: string, desiredValue: string): Promise; getPriority(): number; } ================================================ FILE: src/form-handlers/handler/checkbox.handler.ts ================================================ import { FormHandler } from '../form-handler.interface'; class CheckboxHandler implements FormHandler { public isSatisfiedBy(element) { return element.getTagName().then(tagName => { if (tagName === 'input') { return element.getAttribute('type').then(inputType => inputType === 'checkbox'); } if (tagName instanceof Array) { return element .first() .getAttribute('type') .then(inputType => inputType === 'checkbox'); } return false; }); } public handleFill(page, elementName, desiredValue) { return page .getElements(elementName) .filter(elem => { return elem .element(by.xpath('..')) .getText() .then(text => { return text.trim() === desiredValue; }); }) .first() .click(); } public handleCheck(page, elementName, desiredValue) { const filteredElements = page.getElements(elementName).filter(element => element.isSelected()); return filteredElements.count().then(count => { if (desiredValue === '') { if (count === 0) { return Promise.resolve(); } return Promise.reject(`Expected count to be 0 got ${count}`); } return page .getElements(elementName) .filter(element => { return element .element(by.xpath('..')) .getText() .then(text => { return text.trim() === desiredValue; }); }) .first() .isSelected() .then(selected => { if (selected) { return Promise.resolve(); } return Promise.reject(`Expected element ${elementName} to be selected`); }); }); } public getPriority() { return 998; } } export const checkboxHandler = new CheckboxHandler(); ================================================ FILE: src/form-handlers/handler/ckeditor.handler.ts ================================================ import { FormHandler } from '../form-handler.interface'; class CKEditorHandler implements FormHandler { public isSatisfiedBy(element, elementName) { return Promise.resolve(elementName.endsWith('CKEditor')); } public handleFill(page, elementName, desiredValue) { browser.switchTo().frame(page[elementName].getWebElement()); browser.driver.findElement(by.tagName('body')).sendKeys(desiredValue); browser.switchTo().defaultContent(); return browser.waitForAngular(); } public handleCheck(page, elementName, desiredValue) { return Promise.reject('Checking CKEditor is not supported'); } public getPriority() { return 998; } } export const ckEditorHandler = new CKEditorHandler(); ================================================ FILE: src/form-handlers/handler/custom-angular-select.handler.ts ================================================ import { FormHandler } from '../form-handler.interface'; class CustomAngularSelectHandler implements FormHandler { private selectedOptionSelector: object; private optionsSelector: object; constructor() { this.optionsSelector = by.css('ul.ui-select-choices li a.ui-select-choices-row-inner'); this.selectedOptionSelector = by.css('div.ui-select-match .ui-select-match-text'); } public isSatisfiedBy(element, elementName) { return Promise.resolve(elementName.endsWith('CustomAngularSelect')); } public handleFill(page, elementName, desiredValue) { return browser .executeScript('arguments[0].scrollIntoView(false);', page[elementName].getWebElement()) .then(() => page[elementName].click()) .then(() => { const filtered = page[elementName] .all(this.optionsSelector) .filter(elem => elem.getText().then(text => text === desiredValue)); return filtered.count().then(count => { if (count === 0) { return page[elementName] .all(this.optionsSelector) .first() .click(); } return filtered.first().click(); }); }); } public handleCheck(page, elementName, desiredValue) { return page[elementName] .element(this.selectedOptionSelector) .getText() .then(text => { if (text === desiredValue) { return Promise.resolve(); } return Promise.reject(`Expected ${desiredValue} got ${text} for select element ${elementName}`); }); } public getPriority() { return 998; } } export const customAngularSelectHandler = new CustomAngularSelectHandler(); ================================================ FILE: src/form-handlers/handler/default.handler.ts ================================================ import { FormHandler } from '../form-handler.interface'; class DefaultHandler implements FormHandler { public isSatisfiedBy() { return Promise.resolve(true); } public handleFill(page, elementName, desiredValue) { return page .getElement(elementName) .isDisplayed() .then(() => page.getElement(elementName).clear()) .then(() => page.getElement(elementName).sendKeys(desiredValue)); } public handleCheck(page, elementName, desiredValue) { return page .getElement(elementName) .isDisplayed() .then(() => { return page .getElement(elementName) .getAttribute('value') .then(value => { if (value === desiredValue) { return Promise.resolve(); } return Promise.reject(`Expected ${desiredValue} got ${value} for text input element ${elementName}`); }); }); } public getPriority() { return 999; } } export const defaultHandler = new DefaultHandler(); ================================================ FILE: src/form-handlers/handler/file.handler.ts ================================================ import * as path from 'path'; import config from '../../core/config.helper'; import { FormHandler } from '../form-handler.interface'; class FileHandler implements FormHandler { public isSatisfiedBy(element) { return element.getTagName().then(tagName => { if (tagName === 'input') { return element.getAttribute('type').then(inputType => inputType === 'file'); } if (tagName instanceof Array) { return element .first() .getAttribute('type') .then(inputType => inputType === 'file'); } return false; }); } public handleFill(page, elementName, desiredValue) { const fileToUpload = path.resolve(path.join(config.projectPath, config.data, desiredValue)); return page.getElements(elementName).sendKeys(fileToUpload); } public handleCheck(page, elementName, desiredValue) { return page .getElements(elementName) .getText() .then(text => { if (text === desiredValue) { return Promise.resolve(); } return Promise.reject(`Expected ${desiredValue} got ${text} for file element ${elementName}`); }); } public getPriority() { return 998; } } export const fileHandler = new FileHandler(); ================================================ FILE: src/form-handlers/handler/index.ts ================================================ export * from './checkbox.handler'; export * from './ckeditor.handler'; export * from './custom-angular-select.handler'; export * from './default.handler'; export * from './file.handler'; export * from './uploaded-file.handler'; export * from './radio.handler'; export * from './select.handler'; ================================================ FILE: src/form-handlers/handler/radio.handler.ts ================================================ import { FormHandler } from '../form-handler.interface'; class RadioHandler implements FormHandler { public isSatisfiedBy(element) { return element.getTagName().then(tagName => { if (tagName === 'input') { return element.getAttribute('type').then(inputType => inputType === 'radio'); } if (tagName instanceof Array) { return element .first() .getAttribute('type') .then(inputType => inputType === 'radio'); } return false; }); } public handleFill(page, elementName, desiredValue) { const firstRadio = page .getElements(elementName) .filter(elem => elem.getAttribute('value').then(elemValue => elemValue === desiredValue)) .first(); return firstRadio.isDisplayed().then(isDisplayed => { if (isDisplayed) { return firstRadio.click(); } return firstRadio.element(by.xpath('..')).click(); }); } public handleCheck(page, elementName, desiredValue) { const filteredElements = page.getElements(elementName).filter(element => element.isSelected()); return filteredElements.count().then(count => { if (desiredValue === '') { if (count === 0) { return Promise.resolve(); } return Promise.reject(`Expected count to be 0 got ${count}`); } return filteredElements .first() .getAttribute('value') .then(value => { if (value === desiredValue) { return Promise.resolve(); } return Promise.reject(`Expected ${desiredValue} got ${value} for radio element ${elementName}`); }); }); } public getPriority() { return 998; } } export const radioHandler = new RadioHandler(); ================================================ FILE: src/form-handlers/handler/select.handler.ts ================================================ import { FormHandler } from '../form-handler.interface'; class SelectHandler implements FormHandler { private optionsSelector: object; constructor() { this.optionsSelector = by.css('option'); } public isSatisfiedBy(element) { return element.getTagName().then(tagName => tagName === 'select'); } public handleFill(page, elementName, desiredValue) { const self = this; const filteredByText = page .getElements(elementName) .all(this.optionsSelector) .filter(elem => elem.getText().then(text => text.trim() === desiredValue)); return filteredByText.count().then(filteredByTextCount => { if (filteredByTextCount === 0) { const filteredByValue = page .getElements(elementName) .all(by.css('option')) .filter(elem => elem.getAttribute('value').then(elemValue => elemValue === desiredValue)); return filteredByValue.count().then(filteredByValueCount => { if (filteredByValueCount === 0) { return page[elementName] .all(self.optionsSelector) .first() .click(); } return filteredByValue.first().click(); }); } return filteredByText.first().click(); }); } public handleCheck(page, elementName, desiredValue) { return page .getElement(elementName) .all(this.optionsSelector) .filter(element => element.getAttribute('value').then(elemValue => elemValue === desiredValue)) .count() .then(count => { if (count === 1) { return Promise.resolve(); } return Promise.reject('Option not found for select element.'); }); } public getPriority() { return 998; } } export const selectHandler = new SelectHandler(); ================================================ FILE: src/form-handlers/handler/uploaded-file.handler.ts ================================================ import { FormHandler } from '../form-handler.interface'; class UploadedFileHandler implements FormHandler { public isSatisfiedBy(element, elementName) { return Promise.resolve(elementName.endsWith('Uploaded')); } public handleFill(page, elementName, desiredValue) { return Promise.reject('Not supported for this field type'); } public handleCheck(page, elementName, desiredValue) { return page[elementName].getText().then(text => { if (text.indexOf(desiredValue) >= 0) { return Promise.resolve(); } return Promise.reject(`Expected ${desiredValue} got ${text} for file element ${elementName}`); }); } public getPriority() { return 998; } } export const uploadedFileHandler = new UploadedFileHandler(); ================================================ FILE: src/form-handlers/handlers.ts ================================================ import * as formHandler from './handler'; import { FormHandler } from './form-handler.interface'; import Base from '../pages/base'; class FormHandlers { constructor( private availableHandlers: FormHandler[] = [ formHandler.checkboxHandler, formHandler.ckEditorHandler, formHandler.customAngularSelectHandler, formHandler.defaultHandler, formHandler.fileHandler, formHandler.radioHandler, formHandler.selectHandler, formHandler.uploadedFileHandler, ] ) {} public addHandler(handler: FormHandler): void { this.availableHandlers.push(handler); } public async handleFill(page: Base, elementName: string, desiredValue: string): Promise { const handlers = this.getHandlers(); for (const handler of handlers) { const isSatisfied = await handler.isSatisfiedBy(page.getElement(elementName), elementName); if (isSatisfied) { return handler.handleFill(page, elementName, desiredValue); } } return Promise.reject('Could not find matching handler.'); } public async handleCheck(page: Base, elementName: string, desiredValue: string): Promise { const handlers = this.getHandlers(); for (const handler of handlers) { const isSatisfied = await handler.isSatisfiedBy(page.getElement(elementName), elementName); if (isSatisfied) { return handler.handleCheck(page, elementName, desiredValue); } } return Promise.reject('Could not find matching handler.'); } public getHandlers(): FormHandler[] { return this.availableHandlers.sort((handler, otherHandler) => handler.getPriority() - otherHandler.getPriority()); } } export default new FormHandlers(); ================================================ FILE: src/form-handlers/index.ts ================================================ export { default as fromHandlers } from './handlers'; ================================================ FILE: src/generators/generator/index.ts ================================================ export * from './string-with-length.generator'; export * from './personalData.generator'; ================================================ FILE: src/generators/generator/personalData.generator.spec.ts ================================================ import { personalDataGenerator } from './personalData.generator'; describe('Personal data', () => { it('returns true if is satisfied by', () => { expect(personalDataGenerator.isSatisfiedBy('personalData')).toEqual(true); }); it('returns false if is not satisfied by', () => { expect(personalDataGenerator.isSatisfiedBy('not-supported-name')).toEqual(false); }); it('generates a random firstName', done => { personalDataGenerator.generate(['firstName']).then(result => { expect(result.length > 1 && /[A-Z].*/.test(result)).toEqual(true); done(); }); }); it('generates a random lastName', done => { personalDataGenerator.generate(['lastName']).then(result => { expect(result.length > 1 && /[A-Z].*/.test(result)).toEqual(true); done(); }); }); it('generates a random jobTitle', done => { personalDataGenerator.generate(['jobTitle']).then(result => { expect(result.length > 1).toEqual(true); done(); }); }); it('generates a random email', done => { personalDataGenerator.generate(['email']).then(result => { const minEmailLength = 5; expect(result.includes('@') && result.length > minEmailLength).toEqual(true); done(); }); }); it('return error message - generator is not available', done => { personalDataGenerator.generate(['incorrect']).catch(error => { expect(error === 'Option not available in "personalData" generator!').toEqual(true); done(); }); }); }); ================================================ FILE: src/generators/generator/personalData.generator.ts ================================================ import * as faker from 'faker'; import { Generator } from '../generator.interface'; export const personalDataGenerator: Generator = { isSatisfiedBy(name) { return name === 'personalData'; }, generate(options) { switch (options[0]) { case 'firstName': return Promise.resolve(faker.name.firstName()); case 'lastName': return Promise.resolve(faker.name.lastName()); case 'jobTitle': return Promise.resolve(faker.name.jobTitle()); case 'email': return Promise.resolve(faker.internet.email(null, null, options[1])); default: return Promise.reject('Option not available in "personalData" generator!'); } }, }; ================================================ FILE: src/generators/generator/string-with-length.generator.spec.ts ================================================ import { stringWithLengthGenerator } from './string-with-length.generator'; describe('String with length', () => { it('returns true if is satisfied by', () => { expect(stringWithLengthGenerator.isSatisfiedBy('stringWithLength')).toEqual(true); }); it('returns false if is not satisfied by', () => { expect(stringWithLengthGenerator.isSatisfiedBy('not-supported-name')).toEqual(false); }); it('generates string with given length', done => { stringWithLengthGenerator.generate(100).then(result => { expect(result.length).toEqual(100); done(); }); }); }); ================================================ FILE: src/generators/generator/string-with-length.generator.ts ================================================ import { Generator } from '../generator.interface'; export const stringWithLengthGenerator: Generator = { isSatisfiedBy(name) { return name === 'stringWithLength'; }, generate(generatorParam) { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const length = parseInt(generatorParam); let result = ''; for (let i = length; i > 0; i--) { result += chars[Math.floor(Math.random() * chars.length)]; } return Promise.resolve(result); }, }; ================================================ FILE: src/generators/generator.interface.ts ================================================ export interface Generator { isSatisfiedBy(name: string): boolean; generate(...params: any): Promise; } ================================================ FILE: src/generators/generators.spec.ts ================================================ import { create } from './generators'; const generators = create(); describe('Generators', () => { it('throws an error when no generator was found', () => { expect(() => generators.generate('unknown-generator')).toThrow('Could not find generator for unknown-generator.'); }); it('return generated value', done => { generators.generate('stringWithLength', 100).then(result => { expect(result.length).toEqual(100); done(); }); }); it('adds new generator', done => { const customGenerator = { isSatisfiedBy: name => name === 'customGenerator', generate: params => { return Promise.resolve(params); }, }; generators.addGenerator(customGenerator); generators.generate('customGenerator', 'params').then(result => { expect(result).toEqual('params'); done(); }); }); }); ================================================ FILE: src/generators/generators.ts ================================================ import * as generators from './generator'; import { Generator } from './generator.interface'; export class Generators { constructor( private availableGenerators: Generator[] = [generators.personalDataGenerator, generators.stringWithLengthGenerator] ) {} public generate(generatorName: string, ...params: any): Promise { const gen: Generator = this.findGenerator(generatorName); if (gen === undefined) { throw new Error(`Could not find generator for ${generatorName}.`); } return gen.generate(...params); } public addGenerator(generator: Generator): void { this.availableGenerators.push(generator); } public findGenerator(name: string): Generator { return this.availableGenerators.find(gen => gen.isSatisfiedBy(name)); } } export const create = () => new Generators(); ================================================ FILE: src/generators/index.ts ================================================ import { create } from './generators'; export { Generators } from './generators'; export const generators = create(); ================================================ FILE: src/index.ts ================================================ // entry file import * as dictionaries from './dictionaries'; import * as pages from './pages'; export { matchers, regexBuilder } from './matchers'; export { defineSupportCode, Given, When, Then, After, Before, AfterAll, BeforeAll } from 'cucumber'; export { dictionaries } from './dictionaries'; export { transformers } from './transformers'; export { generators } from './generators'; export { default as variableStore } from './web/variable-store.helper'; export { default as handlers } from './form-handlers/handlers'; export { comparators } from './comparators'; export { emailService } from './emails'; export { waitForVisibilityOf, waitForInvisibilityOf, waitForCondition } from './web/cucumber/wait-for-condition.helper'; export { hookHandlers } from './web/cucumber/hooks/hooks'; export const BasePage = pages.Form; export const BaseDictionary = dictionaries.Base; ================================================ FILE: src/kakunin.d.ts ================================================ declare let browser: any; declare let by: any; declare let protractor: any; declare module NodeJS { interface Global { by: any; expect(value: any): any; } } interface Console { err: any; } declare interface RegExpConstructor { escape(text: string): any; } ================================================ FILE: src/matchers/index.ts ================================================ import { regexBuilder as builder } from './matcher/regex-matcher/regex-builder'; import { create as createMatchers } from './matchers'; export const matchers = createMatchers(); export const regexBuilder = builder; ================================================ FILE: src/matchers/matcher/attribute.matcher.spec.ts ================================================ import { attributeMatcher } from './attribute.matcher'; describe('Attribute matcher', () => { it('is satisfied when the prefix and name are correct', () => { expect(attributeMatcher.isSatisfiedBy('attribute')).toEqual(true); }); it('is not satisfied when the prefix and name are incorrect', () => { const incorrectParameters = [ { prefix: 'f', name: 'incorrect' }, { prefix: 'incorrect', name: 'incorrect' }, { prefix: 'g', name: 'isVisible' }, ]; incorrectParameters.forEach(parameters => expect(attributeMatcher.isSatisfiedBy(parameters.prefix)).toEqual(false)); }); it('returns true when the attribute is matched', done => { const elementMocked = { getAttribute: () => Promise.resolve('http://some-random-link.com'), }; attributeMatcher.match(elementMocked, 'href', 'someRandomLinkRegex').then(result => { expect(result).toEqual(true); done(); }); }); it('returns rejected promise when the attribute is not matched', done => { const elementMocked = { getAttribute: () => Promise.resolve('some-random-link'), locator: () => 'some-locator', }; attributeMatcher.match(elementMocked, 'href', 'someRandomLinkRegex').catch(err => { done(); }); }); }); ================================================ FILE: src/matchers/matcher/attribute.matcher.ts ================================================ import { regexBuilder } from './regex-matcher/regex-builder'; import { Matcher } from '../matcher.interface'; class AttributeMatcher implements Matcher { public isSatisfiedBy(prefix) { return prefix === 'attribute'; } public match(element, attributeName, regexName) { return element.getAttribute(attributeName).then(value => { if (regexBuilder.buildRegex(`r:${regexName}`).test(value)) { return true; } const transformedRegexName = `r:${regexName}`; return Promise.reject(` Matcher "AttributeMatcher" could not match regex on element "${element.locator()}" on attribute "${attributeName}". Expected to match: "${regexBuilder.buildRegex(transformedRegexName).toString()}", Given: "${value}" `); }); } } export const attributeMatcher = new AttributeMatcher(); ================================================ FILE: src/matchers/matcher/clickable.matcher.spec.ts ================================================ import { clickableMatcher } from './clickable.matcher'; describe('Clickable matcher', () => { it('is satisfied when the prefix and the name are correct', () => { expect(clickableMatcher.isSatisfiedBy('f', 'isClickable')).toEqual(true); }); it('is not satisfied when unsupported parameters are given', () => { const incorrectParameters = [{ prefix: 'r', name: 'isClickable' }, { prefix: 'f', name: 'isNotClickable' }]; incorrectParameters.forEach(parameter => expect(clickableMatcher.isSatisfiedBy(parameter.prefix, parameter.name)).toEqual(false) ); }); it('returns true when the element is clickable', done => { const mockedElement = { getAttribute: attribute => Promise.resolve(null), locator: () => 'some-locator', }; clickableMatcher.match(mockedElement).then(result => { expect(result).toEqual(true); done(); }); }); it('returns rejected promise when the element is not clickable - disabled with disabled parameter', done => { const mockedElement = { getAttribute: attribute => Promise.resolve('disabled'), locator: () => 'some-locator', }; clickableMatcher.match(mockedElement).catch(err => { done(); }); }); it('returns rejected promise when the element is not clickable - disabled with true parameter', done => { const mockedElement = { getAttribute: attribute => Promise.resolve(true), locator: () => 'some-locator', }; clickableMatcher.match(mockedElement).catch(err => { done(); }); }); it('returns rejected promise when the element is not clickable - disabled with true string parameter', done => { const mockedElement = { getAttribute: attribute => Promise.resolve('true'), locator: () => 'some-locator', }; clickableMatcher.match(mockedElement).catch(err => { done(); }); }); }); ================================================ FILE: src/matchers/matcher/clickable.matcher.ts ================================================ import { Matcher } from '../matcher.interface'; class ClickableMatcher implements Matcher { public isSatisfiedBy(prefix, name) { return prefix === 'f' && name === 'isClickable'; } public match(element) { return element .getAttribute('disabled') .then(disabled => ['disabled', true, 'true'].indexOf(disabled) === -1) .then(result => { if (result) { return true; } return Promise.reject(` Matcher "ClickableMatcher" could find attribute disabled on element "${element.locator()}". `); }); } } export const clickableMatcher = new ClickableMatcher(); ================================================ FILE: src/matchers/matcher/currentDate.matcher.spec.ts ================================================ import { currentDateMatcher } from './currentDate.matcher'; import * as moment from 'moment'; describe('Current Date matcher', () => { it('is satisfied when the prefix and the name are correct', () => { expect(currentDateMatcher.isSatisfiedBy('f', 'currentDate')).toEqual(true); }); it('is not satisfied when unsupported parameters are given', () => { const incorrectParameters = [{ prefix: 'm', name: 'date' }, { prefix: 't', name: 'currentDate' }]; incorrectParameters.forEach(parameter => expect(currentDateMatcher.isSatisfiedBy(parameter.prefix, parameter.name)).toEqual(false) ); }); it('returns true when the date is matched', done => { const elementMocked = { getText: () => Promise.resolve(moment(new Date())), locator: () => 'some-locator', }; currentDateMatcher.match(elementMocked).then(result => { expect(result).toEqual(true); done(); }); }); it('returns true when the date with slashes is matched', done => { const elementMocked = { getText: () => Promise.resolve(moment(new Date()).format('MM/DD/YYYY')), locator: () => 'some-locator', }; currentDateMatcher.match(elementMocked, null, 'MM/DD/YYYY').then(result => { expect(result).toEqual(true); done(); }); }); it('returns rejected promise when the text date is not matched', done => { const elementMocked = { getText: () => Promise.resolve('Yesterday'), locator: () => 'some-locator', }; currentDateMatcher.match(elementMocked).catch(err => { done(); }); }); it('returns rejected promise when the date is not matched', done => { const elementMocked = { getText: () => Promise.resolve('1900-01-01'), locator: () => 'some-locator', }; currentDateMatcher.match(elementMocked).catch(err => { done(); }); }); it('returns rejected promise when the date is incorrect', done => { const elementMocked = { getText: () => Promise.resolve('1900-01-1900'), locator: () => 'some-locator', }; currentDateMatcher.match(elementMocked).catch(err => { done(); }); }); }); ================================================ FILE: src/matchers/matcher/currentDate.matcher.ts ================================================ import * as moment from 'moment'; import { Matcher } from '../matcher.interface'; class CurrentDateMatcher implements Matcher { public isSatisfiedBy(prefix, name) { return prefix === 'f' && name === 'currentDate'; } public match(element, name = null, params = 'DD-MM-YYYY') { const currentDate = moment(new Date()).format(params); return element.getText().then(text => { const compareDate = moment(new Date(text)).format(params); if (compareDate === currentDate) { return true; } return Promise.reject(` Matcher "CurrentDate" could not match date for element "${element.locator()}". Expected: "${compareDate}", given: "${currentDate}". `); }); } } export const currentDateMatcher = new CurrentDateMatcher(); ================================================ FILE: src/matchers/matcher/index.ts ================================================ export { clickableMatcher } from './clickable.matcher'; export { invisibleMatcher } from './invisible.matcher'; export { notClickableMatcher } from './not-clickable.matcher'; export { presentMatcher } from './present.matcher'; export { textMatcher } from './text.matcher'; export { visibleMatcher } from './visible.matcher'; export { regexMatcher } from './regex-matcher'; export { attributeMatcher } from './attribute.matcher'; export { currentDateMatcher } from './currentDate.matcher'; ================================================ FILE: src/matchers/matcher/invisible.matcher.spec.ts ================================================ import { invisibleMatcher } from './invisible.matcher'; describe('Invisible matcher', () => { it('is satisfied when the prefix and the name are correct', () => { expect(invisibleMatcher.isSatisfiedBy('f', 'isNotVisible')).toEqual(true); }); it('is not satisfied when unsupported parameters are given', () => { const incorrectParameters = [{ prefix: 'f', name: 'isVisible' }, { prefix: 't', name: 'isNotVisible' }]; incorrectParameters.forEach(parameter => expect(invisibleMatcher.isSatisfiedBy(parameter.prefix, parameter.name)).toEqual(false) ); }); it('returns rejected promise when the element is visible', done => { const mockedElement = { isDisplayed: () => Promise.resolve(true), locator: () => 'some-locator', }; invisibleMatcher.match(mockedElement).catch(() => { done(); }); }); it('returns true when the element is not visible', done => { const mockedElement = { isDisplayed: () => Promise.reject(), locator: () => 'some-locator', }; invisibleMatcher.match(mockedElement).then(result => { expect(result).toEqual(true); done(); }); }); }); ================================================ FILE: src/matchers/matcher/invisible.matcher.ts ================================================ import { Matcher } from '../matcher.interface'; class InvisibleMatcher implements Matcher { public isSatisfiedBy(prefix, name) { return prefix === 'f' && name === 'isNotVisible'; } public async match(element) { try { await element.isDisplayed(); return Promise.reject(` Matcher "InvisibleMatcher" could find element "${element.locator()}". Expected element to be invisible. `); } catch (err) { return true; } } } export const invisibleMatcher = new InvisibleMatcher(); ================================================ FILE: src/matchers/matcher/not-clickable.matcher.spec.ts ================================================ import { notClickableMatcher } from './not-clickable.matcher'; describe('Not clickable matcher', () => { it('is satisfied when the prefix and the name are correct', () => { expect(notClickableMatcher.isSatisfiedBy('f', 'isNotClickable')).toEqual(true); }); it('is not satisfied when unsupported parameters are given', () => { const incorrectParameters = [{ prefix: 'r', name: 'isNotClickable' }, { prefix: 'f', name: 'isClickable' }]; incorrectParameters.forEach(parameter => expect(notClickableMatcher.isSatisfiedBy(parameter.prefix, parameter.name)).toEqual(false) ); }); it('returns rejected promise when the element is clickable', done => { const mockedElement = { getAttribute: attribute => Promise.resolve(null), locator: () => 'some-locator', }; notClickableMatcher.match(mockedElement).catch(err => { done(); }); }); it('returns true when the element is not clickable - disabled with disabled parameter', done => { const mockedElement = { getAttribute: attribute => Promise.resolve('disabled'), locator: () => 'some-locator', }; notClickableMatcher.match(mockedElement).then(result => { expect(result).toEqual(true); done(); }); }); it('returns true when the element is not clickable - disabled with true parameter', done => { const mockedElement = { getAttribute: attribute => Promise.resolve(true), locator: () => 'some-locator', }; notClickableMatcher.match(mockedElement).then(result => { expect(result).toEqual(true); done(); }); }); it('returns true when the element is not clickable - disabled with true string parameter', done => { const mockedElement = { getAttribute: attribute => Promise.resolve('true'), locator: () => 'some-locator', }; notClickableMatcher.match(mockedElement).then(result => { expect(result).toEqual(true); done(); }); }); }); ================================================ FILE: src/matchers/matcher/not-clickable.matcher.ts ================================================ import { Matcher } from '../matcher.interface'; class NotClickableMatcher implements Matcher { public isSatisfiedBy(prefix, name) { return prefix === 'f' && name === 'isNotClickable'; } public match(element) { return element .getAttribute('disabled') .then(disabled => ['disabled', true, 'true'].indexOf(disabled) !== -1) .then(result => { if (result) { return true; } return Promise.reject(` Matcher "NotClickable" could not find attribute disabled on element "${element.locator()}". `); }); } } export const notClickableMatcher = new NotClickableMatcher(); ================================================ FILE: src/matchers/matcher/present.matcher.spec.ts ================================================ import { presentMatcher } from './present.matcher'; describe('Present matcher', () => { it('is satisfied when the prefix and the name are correct', () => { expect(presentMatcher.isSatisfiedBy('f', 'isPresent')).toEqual(true); }); it('is not satisfied when unsupported parameters are given', () => { const incorrectParameters = [{ prefix: 'f', name: 'isVisible' }, { prefix: 'r', name: 'isPresent' }]; incorrectParameters.forEach(parameter => expect(presentMatcher.isSatisfiedBy(parameter.prefix, parameter.name)).toEqual(false) ); }); it('returns true when the element is present', done => { const mockedElement = { isPresent: () => Promise.resolve(), locator: () => 'some-locator', }; presentMatcher.match(mockedElement).then(result => { expect(result).toEqual(true); done(); }); }); it('returns rejected promise when the element is not present', done => { const mockedElement = { isPresent: () => Promise.reject(), locator: () => 'some-locator', }; presentMatcher.match(mockedElement).catch(err => { done(); }); }); }); ================================================ FILE: src/matchers/matcher/present.matcher.ts ================================================ import { Matcher } from '../matcher.interface'; class PresentMatcher implements Matcher { public isSatisfiedBy(prefix, name) { return prefix === 'f' && name === 'isPresent'; } public match(element) { return element .isPresent() .then(() => true) .catch(() => Promise.reject(`Matcher "PresentMatcher" could not find element "${element.locator()}".`)); } } export const presentMatcher = new PresentMatcher(); ================================================ FILE: src/matchers/matcher/regex-matcher/index.spec.ts ================================================ import { regexMatcher } from '.'; describe('Regex matcher', () => { it('is satisfied when the prefix is correct and regex exists', () => { expect(regexMatcher.isSatisfiedBy('r', 'number')).toEqual(true); }); it('is not satisfied when the prefix is incorrect', () => { expect(regexMatcher.isSatisfiedBy('f', 'number')).toEqual(false); }); it('is not satisfied when the name is not incorrect', () => { expect(regexMatcher.isSatisfiedBy('r', 'unknown')).toEqual(false); }); it('returns matches text of element', done => { const elementMocked = { getText: () => Promise.resolve('12345'), getAttribute: name => (name === 'value' ? Promise.resolve('') : Promise.resolve(null)), locator: () => 'some-locator', }; regexMatcher.match(elementMocked, 'number').then(result => { expect(result).toEqual(true); done(); }); }); it('matches value attribute if text is empty', done => { const elementMocked = { getText: () => Promise.resolve(''), getAttribute: name => (name === 'value' ? Promise.resolve('12345') : Promise.resolve(null)), locator: () => 'some-locator', }; regexMatcher.match(elementMocked, 'number').then(result => { expect(result).toEqual(true); done(); }); }); it('returns rejected promise when the text is not matching', done => { const elementMocked = { getText: () => Promise.resolve('not-a-number'), getAttribute: name => (name === 'value' ? Promise.resolve('') : Promise.resolve(null)), locator: () => 'some-locator', }; regexMatcher.match(elementMocked, 'number').catch(err => { done(); }); }); it('returns rejected promise when the value attribute is not matched', done => { const elementMocked = { getText: () => Promise.resolve(''), getAttribute: name => (name === 'value' ? Promise.resolve('not-a-number') : Promise.resolve(null)), locator: () => 'some-locator', }; regexMatcher.match(elementMocked, 'number').catch(err => { done(); }); }); it('returns rejected promise when the text and attribute are empty', done => { const elementMocked = { getText: () => Promise.resolve(''), getAttribute: () => Promise.resolve(null), locator: () => 'some-locator', }; regexMatcher.match(elementMocked, 'notEmpty').catch(err => { done(); }); }); }); ================================================ FILE: src/matchers/matcher/regex-matcher/index.ts ================================================ import regex from './regex'; import { regexBuilder } from './regex-builder'; import { Matcher } from '../../matcher.interface'; class RegexMatcher implements Matcher { public isSatisfiedBy(prefix, name) { return prefix === 'r' && typeof regex[name] !== 'undefined'; } public match(element, regexName) { return element.getText().then(text => { return element.getAttribute('value').then(value => { const regularExpression = regexBuilder.buildRegex(`r:${regexName}`); if (text === '') { if (value === null) { return Promise.reject(` Matcher "RegexMatcher" could not match value for element "${element.locator()}". Both text and attribute value are empty. `); } if (regularExpression.test(value)) { return true; } return Promise.reject(` Matcher "RegexMatcher" could not match regex on element "${element.locator()}" on value "${value}". Expected to match: "${regularExpression.toString()}", Given: "${value}" `); } if (regularExpression.test(text)) { return true; } return Promise.reject(` Matcher "RegexMatcher" could not match regex on element "${element.locator()}" on text "${text}". Expected to match: "${regularExpression.toString()}", Given: "${text}" `); }); }); } } export const regexMatcher = new RegexMatcher(); ================================================ FILE: src/matchers/matcher/regex-matcher/regex-builder.spec.ts ================================================ import { regexBuilder } from './regex-builder'; describe('Regex builder', () => { it('throws an error when could not build a regexp', () => { expect(() => regexBuilder.buildRegex('r:unknown-regexp')).toThrow( 'Regex with template r:unknown-regexp was not found' ); }); it('returns regular expression object', () => { expect(regexBuilder.buildRegex('r:number')).toEqual(new RegExp('[0-9]+')); }); }); ================================================ FILE: src/matchers/matcher/regex-matcher/regex-builder.ts ================================================ import regex from './regex'; class RegexBuilder { public buildRegex(regexTemplate: string): RegExp { for (const property in regex) { if (regex.hasOwnProperty(property) && regexTemplate === 'r:' + property) { return new RegExp(regex[property]); } } throw new Error('Regex with template ' + regexTemplate + ' was not found'); } } export const regexBuilder = new RegexBuilder(); ================================================ FILE: src/matchers/matcher/regex-matcher/regex.ts ================================================ import { create } from '../../../core/modules-loader.helper'; import { regex } from './regexes/default'; const modulesLoader = create(); const availableRegexes = modulesLoader.getModules('regexes'); const regularExpressions = availableRegexes.reduce((regexes, newRegexes) => ({ ...regexes, ...newRegexes }), { ...regex, }); export default regularExpressions; ================================================ FILE: src/matchers/matcher/regex-matcher/regexes/default.ts ================================================ export const regex = { arabianCharacters: '\u0621-\u064A', arabianNumbers: '\u0660-\u0669', standardCharacters: 'a-zA-Z', standardNumbers: '0-9', notEmpty: '.+', number: '[0-9]+', pdfFile: '[\\w]+.pdf', pdfFileType: 'application/pdf', email: "[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+(?:.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", // todo: following regex should be moved to tests/regexes someRandomLinkRegex: '(http(s)?://some-random-link.com)', }; ================================================ FILE: src/matchers/matcher/text.matcher.spec.ts ================================================ import { textMatcher } from './text.matcher'; describe('Text matcher', () => { it('is satisfied when the prefix is correct', () => { expect(textMatcher.isSatisfiedBy('t')).toEqual(true); }); it('is not satisfied when the prefix is incorrect', () => { expect(textMatcher.isSatisfiedBy('r')).toEqual(false); }); it('returns true when the text is matched - different tag than textarea and input', done => { const elementMocked = { getTagName: () => Promise.resolve('div'), getText: () => Promise.resolve('this string contains message'), locator: () => 'some-locator', }; textMatcher.match(elementMocked, 'message').then(result => { expect(result).toEqual(true); done(); }); }); it('returns true when the text from textarea is matched', done => { const elementMocked = { getTagName: () => Promise.resolve('textarea'), getAttribute: () => Promise.resolve('this string contains message from textarea'), locator: () => 'some-locator', }; textMatcher.match(elementMocked, 'message from textarea').then(result => { expect(result).toEqual(true); done(); }); }); it('returns true when the text from input is matched', done => { const elementMocked = { getTagName: () => Promise.resolve('textarea'), getAttribute: () => Promise.resolve('this string contains message from input'), locator: () => 'some-locator', }; textMatcher.match(elementMocked, 'message from input').then(result => { expect(result).toEqual(true); done(); }); }); it('returns rejected promise when the text is not matched - different tag than textarea and input', done => { const elementMocked = { getTagName: () => Promise.resolve('h4'), getText: () => Promise.resolve('missing expected value in string'), locator: () => 'some-locator', }; textMatcher.match(elementMocked, 'message').catch(err => { expect(err).toEqual(` Matcher "TextMatcher" could not match value on element "some-locator". Expected: "message", Given: "missing expected value in string" `); done(); }); }); it('returns rejected promise when the text is not matched in input field', done => { const elementMocked = { getTagName: () => Promise.resolve('input'), getAttribute: () => Promise.resolve('missing expected value in input string'), locator: () => 'some-locator', }; textMatcher.match(elementMocked, 'message').catch(err => { expect(err).toContain('Expected: "message", Given: "missing expected value in input string"'); done(); }); }); it('returns rejected promise when the text is not matched in textarea field', done => { const elementMocked = { getTagName: () => Promise.resolve('textarea'), getAttribute: () => Promise.resolve('missing expected value in textarea string'), locator: () => 'some-locator', }; textMatcher.match(elementMocked, 'message').catch(err => { expect(err).toContain('Matcher "TextMatcher" could not match value on element "some-locator".'); done(); }); }); }); ================================================ FILE: src/matchers/matcher/text.matcher.ts ================================================ import { separator } from '../matchers'; import { Matcher } from '../matcher.interface'; class TextMatcher implements Matcher { public isSatisfiedBy(prefix) { return prefix === 't'; } public match(element, ...params) { const expectedValue = params.join(separator); return element.getTagName().then(tag => { if (tag === 'input' || tag === 'textarea') { return element.getAttribute('value').then(value => { if (new RegExp(RegExp.escape(expectedValue)).test(value)) { return true; } return Promise.reject(` Matcher "TextMatcher" could not match value on element "${element.locator()}". Expected: "${expectedValue}", Given: "${value}" `); }); } return element.getText().then(text => { if (new RegExp(RegExp.escape(expectedValue)).test(text)) { return true; } return Promise.reject(` Matcher "TextMatcher" could not match value on element "${element.locator()}". Expected: "${expectedValue}", Given: "${text}" `); }); }); } } export const textMatcher = new TextMatcher(); ================================================ FILE: src/matchers/matcher/visible.matcher.spec.ts ================================================ import { visibleMatcher } from './visible.matcher'; describe('Visible matcher', () => { it('is satisfied when the prefix and the name are correct', () => { expect(visibleMatcher.isSatisfiedBy('f', 'isVisible')).toEqual(true); }); it('is not satisfied when unsupported parameters are given', () => { const incorrectParameters = [{ prefix: 'f', name: 'isNotVisible' }, { prefix: 'r', name: 'isVisible' }]; incorrectParameters.forEach(parameter => expect(visibleMatcher.isSatisfiedBy(parameter.prefix, parameter.name)).toEqual(false) ); }); it('returns true when the element is visible', done => { const mockedElement = { isDisplayed: () => Promise.resolve(), locator: () => 'some-locator', }; visibleMatcher.match(mockedElement).then(result => { expect(result).toEqual(true); done(); }); }); it('returns rejected promise when the element is not visible', done => { const mockedElement = { isDisplayed: () => Promise.reject(), locator: () => 'some-locator', }; visibleMatcher.match(mockedElement).catch(err => { done(); }); }); }); ================================================ FILE: src/matchers/matcher/visible.matcher.ts ================================================ import { Matcher } from '../matcher.interface'; class VisibleMatcher implements Matcher { public isSatisfiedBy(prefix, name) { return prefix === 'f' && name === 'isVisible'; } public match(element) { return element .isDisplayed() .then(() => true) .catch(() => { return Promise.reject(`Matcher "VisibleMatcher" could not find element "${element.locator()}".`); }); } } export const visibleMatcher = new VisibleMatcher(); ================================================ FILE: src/matchers/matcher.interface.ts ================================================ export interface Matcher { isSatisfiedBy(prefix: string, name?: string): boolean; match(element: object, param1?: string, param2?: string): Promise; } ================================================ FILE: src/matchers/matchers.spec.ts ================================================ import { create } from './matchers'; const matchers = create(); describe('Matchers', () => { it('throws an error when no matcher was found', () => { const mockedElement = {}; expect(() => matchers.match(mockedElement, 'incorrect:unknown-matcher')).toThrow( 'Could not find matcher for incorrect:unknown-matcher.' ); }); it('returns true when found a matcher and element value is correct', done => { const mockedElement = { getTagName: () => Promise.resolve('div'), getText: () => Promise.resolve('my message'), }; matchers.match(mockedElement, 't:message').then(result => { expect(result).toEqual(true); done(); }); }); it('returns true when found a matcher and element value in input field is correct', done => { const mockedElement = { getTagName: () => Promise.resolve('input'), getAttribute: () => Promise.resolve('my message'), }; matchers.match(mockedElement, 't:message').then(result => { expect(result).toEqual(true); done(); }); }); it('returns true when found a matcher and element value in textarea field is correct', done => { const mockedElement = { getTagName: () => Promise.resolve('textarea'), getAttribute: () => Promise.resolve('my message'), }; matchers.match(mockedElement, 't:message').then(result => { expect(result).toEqual(true); done(); }); }); it('returns rejected promise when found a matcher and element value is not correct', done => { const mockedElement = { getTagName: () => Promise.resolve('h2'), getText: () => Promise.resolve('my message'), locator: () => 'some-locator', }; matchers.match(mockedElement, 't:not-existing').catch(err => { done(); }); }); it('returns rejected promise when found a matcher and element value in input field is not correct', done => { const mockedElement = { getTagName: () => Promise.resolve('input'), getAttribute: () => Promise.resolve('my message'), locator: () => 'some-locator', }; matchers.match(mockedElement, 't:not-existing').catch(err => { done(); }); }); it('returns rejected promise when found a matcher and element value in textarea field is not correct', done => { const mockedElement = { getTagName: () => Promise.resolve('textarea'), getAttribute: () => Promise.resolve('my message'), locator: () => 'some-locator', }; matchers.match(mockedElement, 't:not-existing').catch(err => { done(); }); }); it('returns true when found a matcher and element value after a first colon sign is correct', done => { const mockedElement = { getTagName: () => Promise.resolve('p'), getText: () => Promise.resolve('my message: contains :colons'), locator: () => 'some-locator', }; matchers.match(mockedElement, 't:contains :colons').then(result => { expect(result).toEqual(true); done(); }); }); it('returns true when found a matcher and element value after a first colon sign is correct - input field of the element', done => { const mockedElement = { getTagName: () => Promise.resolve('input'), getAttribute: () => Promise.resolve('my message: contains :colons'), locator: () => 'some-locator', }; matchers.match(mockedElement, 't:contains :colons').then(result => { expect(result).toEqual(true); done(); }); }); it('returns true when found a matcher and element value after a first colon sign is correct - textarea field of the element', done => { const mockedElement = { getTagName: () => Promise.resolve('textarea'), getAttribute: () => Promise.resolve('my message: contains :colons'), locator: () => 'some-locator', }; matchers.match(mockedElement, 't:contains :colons').then(result => { expect(result).toEqual(true); done(); }); }); it('returns rejected promise when found a matcher but a text after colon sign is incorrect', done => { const mockedElement = { getTagName: () => Promise.resolve('td'), getText: () => Promise.resolve('my message: contains :colons'), locator: () => 'some-locator', }; matchers.match(mockedElement, 't:my message: contains :incorrect').catch(err => { done(); }); }); it('returns rejected promise when found a matcher but a text after colon sign is incorrect - input field of the element', done => { const mockedElement = { getTagName: () => Promise.resolve('input'), getAttribute: () => Promise.resolve('my message: contains :colons'), locator: () => 'some-locator', }; matchers.match(mockedElement, 't:my message: contains :incorrect').catch(err => { done(); }); }); it('returns rejected promise when found a matcher but a text after colon sign is incorrect - textarea field of the element', done => { const mockedElement = { getTagName: () => Promise.resolve('textarea'), getAttribute: () => Promise.resolve('my message: contains :colons'), locator: () => 'some-locator', }; matchers.match(mockedElement, 't:my message: contains :incorrect').catch(err => { done(); }); }); }); ================================================ FILE: src/matchers/matchers.ts ================================================ import * as matchers from './matcher'; import { Matcher } from './matcher.interface'; export const separator = ':'; class Matchers { constructor( private availableMatchers: Matcher[] = [ matchers.regexMatcher, matchers.clickableMatcher, matchers.invisibleMatcher, matchers.notClickableMatcher, matchers.presentMatcher, matchers.textMatcher, matchers.visibleMatcher, matchers.attributeMatcher, matchers.currentDateMatcher, ] ) {} public addMatcher(matcher: Matcher): void { this.availableMatchers.push(matcher); } public match(element: object, matcherName: string): Promise { const splittedValue = matcherName.split(separator); const matcher = this.findMatcher(splittedValue[0], splittedValue[1]); if (matcher === undefined) { throw new Error(`Could not find matcher for ${matcherName}.`); } return matcher.match(element, ...splittedValue.slice(1)); } public findMatcher(prefix: string, param: string): Matcher { return this.availableMatchers.find(matcher => matcher.isSatisfiedBy(prefix, param)); } } export const create = () => new Matchers(); ================================================ FILE: src/pages/base.ts ================================================ import config from '../core/config.helper'; import { waitForInvisibilityOf, waitForVisibilityOf } from '../web/cucumber/wait-for-condition.helper'; import { isRelativePage, waitForUrlChangeTo } from '../web/url-parser.helper'; import { stringify } from 'querystring'; import { element } from 'protractor'; class Page { private url: string; public visit() { if (config.type === 'otherWeb' || !isRelativePage(this.url)) { protractor.browser.ignoreSynchronization = true; return protractor.browser.get(this.url); } return protractor.browser.get(this.url).then(() => protractor.browser.waitForAngular()); } public visitWithParameters(data) { const additionalParams: any = []; const url = data.raw().reduce((prev, item) => { if (prev.indexOf(`:${item[0]}`) === -1) { additionalParams[item[0]] = item[1]; return prev; } return prev.replace(`:${item[0]}`, item[1]); }, this.url) + (Object.entries(additionalParams).length > 0 ? '?' + stringify(additionalParams) : ''); if (config.type === 'otherWeb' || !isRelativePage(url)) { protractor.browser.ignoreSynchronization = true; return protractor.browser.get(url); } return protractor.browser.get(url).then(() => protractor.browser.waitForAngular()); } public async isOn() { if (isRelativePage(this.url) && config.type !== 'otherWeb') { protractor.browser.ignoreSynchronization = false; } return browser.wait(async () => { const currentUrl = await browser.getCurrentUrl().then(url => url); return waitForUrlChangeTo(this.url, currentUrl)(config.baseUrl); }, config.waitForPageTimeout * 1000); } public click(elementName: string) { return this.getElement(elementName).click(); } public isDisabled(elementName: string) { return this.getElement(elementName) .getAttribute('disabled') .then(disabled => ['disabled', true, 'true'].indexOf(disabled) !== -1); } public isVisible(elementName: string) { return this.getElement(elementName).isDisplayed(); } public isPresent(elementName: string) { return this.getElement(elementName).isPresent(); } public getNumberOfElements(elementName: string) { return this.getElements(elementName).count(); } public scrollIntoElement(elementName: string, elementIndex?: string) { if (elementIndex !== undefined) { return browser.executeScript( 'arguments[0].scrollIntoView(false);', this.getElement(elementName) .get(elementIndex) .getWebElement() ); } return browser.executeScript('arguments[0].scrollIntoView(false);', this.getElement(elementName).getWebElement()); } public waitForVisibilityOf(elementName: string) { return waitForVisibilityOf(this.getElement(elementName)); } public waitForInvisibilityOf(elementName: string) { return waitForInvisibilityOf(this.getElement(elementName)); } public getElement(elementName: string) { if (!this[elementName]) { return element(by.css(elementName)); } return this[elementName]; } public getElements(elementName: string) { if (!this[elementName]) { return element.all(by.css(elementName)); } return this[elementName]; } } export default Page; ================================================ FILE: src/pages/form.ts ================================================ import { fromHandlers } from '../form-handlers'; import { transformers } from '../transformers'; import Base from './base'; class FormPage extends Base { public async fillForm(formData) { for (const item of formData) { await this.fillField(item[0], item[1]); } return Promise.resolve(); } public async checkForm(formData) { for (const item of formData) { await this.checkField(item[0], item[1]); } return Promise.resolve(); } public fillField(name, value) { return fromHandlers.handleFill(this, name, transformers.transform(value)); } public checkField(name, value) { return fromHandlers.handleCheck(this, name, transformers.transform(value)); } } export default FormPage; ================================================ FILE: src/pages/index.ts ================================================ import BasePage from './base'; import FormPage from './form'; export const Base = BasePage; export const Form = FormPage; ================================================ FILE: src/protractor.conf.ts ================================================ require('./core/prototypes'); import * as jestExpect from 'expect'; import * as path from 'path'; import config from './core/config.helper'; import { deleteReports } from './core/fs/delete-files.helper'; import { prepareCatalogs } from './core/fs/prepare-catalogs.helper'; import { browsersConfiguration, setSeleniumAddress } from './web/browsers/browsers-config.helper'; import { getBrowsersDrivers } from './web/browsers/get-browser-drivers.helper'; import { connectBrowserstack, disconnectBrowserstack } from './web/browsers/browserstack-config.helper'; import { emailService } from './emails'; const commandArgs = require('minimist')(process.argv.slice(2)); const modulesLoader = require('./core/modules-loader.helper.js').create(); const reportsDirectory = path.join(config.projectPath, config.reports); const jsonOutputDirectory = path.join(reportsDirectory, 'json-output-folder'); const generatedReportsDirectory = path.join(reportsDirectory, 'report'); const featureReportsDirectory = path.join(generatedReportsDirectory, 'features'); const performanceReportsDirectory = path.join(reportsDirectory, 'performance'); const prepareReportCatalogs = () => { prepareCatalogs(reportsDirectory); prepareCatalogs(jsonOutputDirectory); prepareCatalogs(generatedReportsDirectory); prepareCatalogs(featureReportsDirectory); prepareCatalogs(performanceReportsDirectory); }; const deleteReportFiles = () => { deleteReports(reportsDirectory); deleteReports(jsonOutputDirectory); deleteReports(generatedReportsDirectory); deleteReports(featureReportsDirectory); deleteReports(performanceReportsDirectory); console.log('All reports have been deleted!'); }; const configureMultiCapabilities = () => browsersConfiguration(config, commandArgs); exports.config = { seleniumAddress: setSeleniumAddress(commandArgs, config), getMultiCapabilities: configureMultiCapabilities(), jvmArgs: getBrowsersDrivers(commandArgs), useAllAngular2AppRoots: config.type === 'ng2', getPageTimeout: parseInt(config.timeout) * 1000, allScriptsTimeout: parseInt(config.timeout) * 1000, framework: 'custom', frameworkPath: require.resolve('protractor-cucumber-framework'), specs: [], cucumberOpts: { require: [ './web/cucumber/config.js', './step_definitions/**/*.js', './web/cucumber/hooks.js', ...config.step_definitions.map(file => path.join(config.projectPath, file, '**/*.js')), ], format: [`json:${process.cwd()}/${config.reports}/features-report.json`], profile: false, 'no-source': true, }, plugins: [ { package: 'protractor-multiple-cucumber-html-reporter-plugin', options: { removeExistingJsonReportFile: true, removeOriginalJsonReportFile: true, automaticallyGenerateReport: true, saveCollectedJSON: true, }, }, ], async beforeLaunch() { prepareReportCatalogs(); deleteReportFiles(); if (commandArgs.browserstack) { await connectBrowserstack((await configureMultiCapabilities()())[0]['browserstack.key']); } }, async afterLaunch() { await disconnectBrowserstack(commandArgs.browserstack); }, onPrepare() { if (!config.headless) { browser.driver .manage() .window() .setSize(parseInt(config.browserWidth), parseInt(config.browserHeight)); } modulesLoader.getModules('matchers'); modulesLoader.getModules('dictionaries'); modulesLoader.getModules('generators'); modulesLoader.getModules('comparators'); modulesLoader.getModules('form_handlers'); modulesLoader.getModules('transformers'); modulesLoader.getModules('emails'); modulesLoader.getModules('hooks'); const modules = modulesLoader.getModulesAsObject(config.pages.map(page => path.join(config.projectPath, page))); browser.page = Object.keys(modules).reduce( (pages, moduleName) => ({ ...pages, [moduleName]: new modules[moduleName]() }), {} ); global.expect = jestExpect; if (config.clearEmailInboxBeforeTests) { return emailService.clearInbox(); } }, baseUrl: config.baseUrl, }; ================================================ FILE: src/rest/api-request.ts ================================================ import { Headers } from 'node-fetch'; import FormData = require('form-data'); interface HeaderList { [name: string]: string; } export class ApiRequest { public method: string; public endpoint: string; private payload: string | object; private headers: Headers; private formData: FormData; constructor() { this.payload = null; this.headers = new Headers(); this.formData = new FormData(); } public addHeaders(headers: HeaderList) { for (const [key, value] of Object.entries(headers)) { this.headers.append(key, value); } } public addFormData(payload) { for (const table of payload) { this.formData.append(table[0], table[1]); } return this.formData; } get body() { return this.payload; } set body(payload) { if (payload instanceof FormData) { this.payload = payload; } else { this.payload = payload ? JSON.stringify(payload) : undefined; } } } ================================================ FILE: src/rest/api-response.spec.ts ================================================ import { ApiResponse } from './api-response'; const response = new ApiResponse(200, { type: 'Fiat', model: '500', color: 'white' }); describe('apiResponse', () => { it('returns true when statuses are the same', () => { expect(response.hasStatus(200)).toEqual(true); }); it('returns false when statuses are different', () => { expect(response.hasStatus(100)).toEqual(false); }); it('returns true when response match given object', () => { expect( response.hasBodyMatch({ type: 'Fiat', model: '500', color: 'white', }) ).toEqual(true); }); it('returns false when response doesnt match given object', () => { expect( response.hasBodyMatch({ model: '500', color: 'white', }) ).toEqual(false); }); }); ================================================ FILE: src/rest/api-response.ts ================================================ import * as _ from 'lodash'; import Ajv from 'ajv'; const ajv = new Ajv({ allErrors: true }); export class ApiResponse { private readonly body: object; private readonly status: number; constructor(responseStatus, body) { this.body = body; this.status = responseStatus; } public hasStatus(status) { return this.status === status; } public hasBodyMatch(body) { if (Object.keys(this.body).length === 0) { return Error('Response from server was empty'); } return _.isEqual(this.body, body); } public hasMatchingSchema(schema) { const test = ajv.compile(schema); const isValid = test(this.body); if (isValid === false) { throw Error('Response doesnt match schema'); } } } ================================================ FILE: src/rest/rest-api-service.ts ================================================ import fetch from 'node-fetch'; import { ApiResponse } from './api-response'; export class RestApiService { private readonly baseUrl: string; constructor(baseUrl) { this.baseUrl = baseUrl; } public fetch(request) { const url = this.resolveUrl(request.endpoint); return fetch(url, { method: request.method, body: request.body, headers: request.headers }).then(response => { const contentType = response.headers.get('content-type'); if (contentType && contentType.startsWith('application/json')) { return response.json().then(requestBody => { return new ApiResponse(response.status, requestBody); }); } return new ApiResponse(response.status, {}); }); } private resolveUrl(endpoint) { return `${this.baseUrl}${endpoint}`; } } ================================================ FILE: src/step_definitions/api.ts ================================================ import { defineSupportCode } from 'cucumber'; import config from '../core/config.helper'; import { RestApiService } from '../rest/rest-api-service'; import { ApiRequest } from '../rest/api-request'; const service = new RestApiService(config.apiUrl); let apiRequest = new ApiRequest(); defineSupportCode(({ When, Then }) => { let fetchResult; When(/^I send "([^"]*)" request on "([^"]*)" endpoint$/, (method, endpoint) => { apiRequest.method = method; apiRequest.endpoint = endpoint; return service .fetch(apiRequest) .then(response => { fetchResult = response; return response; }) .finally(() => { apiRequest = new ApiRequest(); return apiRequest; }); }); When(/^I send "([^"]*)" request on "([^"]*)" endpoint with JSON body:$/, (method, endpoint, payload) => { apiRequest.method = method; apiRequest.endpoint = endpoint; apiRequest.body = JSON.parse(payload); apiRequest.addHeaders({ 'Content-Type': 'application/json' }); return service .fetch(apiRequest) .then(response => { fetchResult = response; return response; }) .finally(() => { apiRequest = new ApiRequest(); return apiRequest; }); }); When(/^I send "([^"]*)" request on "([^"]*)" endpoint using form data:$/, (method, endpoint, payload) => { apiRequest.method = method; apiRequest.endpoint = endpoint; apiRequest.body = apiRequest.addFormData(payload.raw()); apiRequest.addHeaders({ 'Content-Type': 'multipart/form-data' }); return service .fetch(apiRequest) .then(response => { fetchResult = response; return response; }) .finally(() => { apiRequest = new ApiRequest(); return apiRequest; }); }); When(/^I set request headers:$/, headers => { return apiRequest.addHeaders(headers.rowsHash()); }); Then(/^the response code should be "([^"]*)"$/, status => { return expect(fetchResult.hasStatus(parseInt(status))).toBe(true); }); Then(/^the response should exact match to body:$/, body => { return expect(fetchResult.hasBodyMatch(JSON.parse(body))).toBe(true); }); Then(/^the response should match JSON schema:$/, schema => { try { fetchResult.hasMatchingSchema(JSON.parse(schema)); } catch (error) { return Promise.reject(error); } }); }); ================================================ FILE: src/step_definitions/debug.ts ================================================ import { Then } from 'cucumber'; Then(/^I wait for "([^"]*)" seconds$/, seconds => { return browser.sleep(Number(seconds) * 1000); }); ================================================ FILE: src/step_definitions/elements.ts ================================================ import * as chai from 'chai'; import { When, Then } from 'cucumber'; import { comparators } from '../comparators'; import config from '../core/config.helper'; import { matchers, regexBuilder } from '../matchers'; import { waitForCondition } from '../web/cucumber/wait-for-condition.helper'; import variableStore from '../web/variable-store.helper'; const timeout = parseInt(config.elementsVisibilityTimeout) * 1000; const handlePromises = (hashedData, onSuccess, onReject) => resolvedPromises => { for (let i = 0; i < resolvedPromises.length; i += hashedData.length) { let allFieldsMatching = true; for (let j = i; j < i + hashedData.length; j++) { if (resolvedPromises[j] === false) { allFieldsMatching = false; break; } } if (allFieldsMatching) { return onSuccess(); } } return onReject(); }; function checkNumberOfElements(numberExpression, element) { const self = this; const numberPattern = /\d+/g; const numbers = numberExpression.match(numberPattern).map(item => parseInt(item)); const expectFunction = async (words, num) => { const numberOfElements = await self.currentPage.getNumberOfElements(element); return chai.expect(numberOfElements).to.be[words.pop()](...num); }; return expectFunction(numberExpression.substr(0, numberExpression.indexOf(numbers[0]) - 1).split(' '), numbers); } When(/^I wait for "([^"]*)" of the "([^"]*)" element$/, function(condition, elementName) { if (this.currentPage.getElement(elementName) instanceof protractor.ElementArrayFinder) { return waitForCondition(condition, timeout)(this.currentPage.getElement(elementName).first()); } return waitForCondition(condition, timeout)(this.currentPage.getElement(elementName)); }); When(/^I scroll to the "([^"]*)" element$/, function(elementName) { return this.currentPage.scrollIntoElement(elementName); }); When(/^I click the "([^"]*)" element$/, function(elementName) { return this.currentPage .scrollIntoElement(elementName) .catch(() => Promise.resolve()) .then(() => this.currentPage.waitForVisibilityOf(elementName)) .then(() => this.currentPage.scrollIntoElement(elementName)) .then(() => this.currentPage.click(elementName)) .catch(() => { return waitForCondition('elementToBeClickable', timeout)(this.currentPage.getElement(elementName)).then(() => { return this.currentPage.click(elementName); }); }) .catch(() => { console.warn('Warning! Element was not clickable. We need to scroll it down.'); return browser .executeScript('window.scrollBy(0,50);') .then(() => this.currentPage.waitForVisibilityOf(elementName)) .then(() => this.currentPage.click(elementName)); }) .catch(() => { console.warn('Warning! Element was not clickable. We need use the WebDriver method to perform the click action.'); return browser .actions() .mouseMove(this.currentPage.getElement(elementName)) .mouseMove({ x: 5, y: 0 }) .click() .perform(); }) .catch(() => { return Promise.reject(`Error, after scrolling the element "${elementName}" is still not clickable.`); }); }); When(/^I store the "([^"]*)" element text as "([^"]*)" variable$/, function(elementName, variable) { return this.currentPage.waitForVisibilityOf(elementName).then(async () => { const elementTag = await this.currentPage[elementName].getTagName(tag => tag); if (elementTag === 'input' || elementTag === 'textarea') { return this.currentPage .getElement(elementName) .getAttribute('value') .then(value => { variableStore.storeVariable(variable, value); }); } return this.currentPage .getElement(elementName) .getText() .then(text => { variableStore.storeVariable(variable, text); }); }); }); When(/^I update the "([^"]*)" element text as "([^"]*)" variable$/, function(elementName, variable) { return this.currentPage.waitForVisibilityOf(elementName).then(() => { this.currentPage .getElement(elementName) .getText() .then(text => { variableStore.updateVariable(variable, text); }); }); }); When(/^I store the "([^"]*)" element text matched by "([^"]*)" as "([^"]*)" variable$/, function( elementName, matcher, variable ) { const regex = regexBuilder.buildRegex(matcher); return this.currentPage.waitForVisibilityOf(elementName).then(() => { return this.currentPage .getElement(elementName) .getText() .then(text => { const matchedText = text.match(regex); if (matchedText === null) { return Promise.reject(`Could not match text ${text} with matcher ${matcher}`); } if (matchedText.length <= 1) { return Promise.reject(`Matcher ${matcher} does not contain capturing brackets`); } variableStore.storeVariable(variable, matchedText[1]); }); }); }); When(/^I wait for the "([^"]*)" element to disappear$/, function(elementName, sync) { const self = this; let maxRepeats = 10; const interval = setInterval(() => { console.log('Waiting for element to disappear...'); return self.currentPage.isPresent(elementName).then(isPresent => { if (!isPresent) { clearInterval(interval); sync(); return; } maxRepeats--; if (maxRepeats === 0) { clearInterval(interval); sync('Element is still visible'); } }); }, 1500); }); Then(/^the "([^"]*)" element is visible$/, function(elementName) { return this.currentPage.isVisible(elementName); }); Then(/^the "([^"]*)" element is not visible$/, function(elementName) { return this.currentPage .isVisible(elementName) .then(isVisible => Promise.reject(isVisible)) .catch(isVisible => { if (isVisible === true) { return Promise.reject(`Element '${elementName}' should not be visible.`); } return Promise.resolve(); }); }); Then(/^the "([^"]*)" element is disabled$/, async function(elementName) { return await expect(this.currentPage.isDisabled(elementName)).resolves.toBe(true); }); When(/^I store table "([^"]*)" rows as "([^"]*)" with columns:$/, function(table, variableName, data) { const self = this; const columns = data.raw().map(element => element[0]); const promises = []; return this.currentPage.waitForVisibilityOf(table).then(() => { return this.currentPage .getElement(table) .each(element => { const rowPromises = []; for (const columnIndex in columns) { if (columns.hasOwnProperty(columnIndex)) { rowPromises.push(element.element(self.currentPage.getElement(columns[columnIndex]).locator()).getText()); } } promises.push(Promise.all(rowPromises)); }) .then(() => Promise.all(promises).then(resolvedPromises => { variableStore.storeVariable(variableName, resolvedPromises); }) ); }); }); Then(/^there are following elements in table "([^"]*)":$/, function(table, data) { const self = this; const allElements = this.currentPage.getElements(table); const hashes = data.hashes(); return this.currentPage.waitForVisibilityOf(table).then(() => { return checkNumberOfElements.call(this, `equal ${hashes.length}`, table).then(() => { const promises = []; return allElements .each((element, index) => { const hash = hashes[index]; for (const prop in hash) { if (hash.hasOwnProperty(prop)) { const propValue = hash[prop]; promises.push( matchers.match( element.element(self.currentPage.getElement(prop).locator()), variableStore.replaceTextVariables(propValue) ) ); } } }) .then(() => Promise.all(promises)); }); }); }); Then(/^there are "([^"]*)" following elements for element "([^"]*)":$/, function(numberExpression, elementName, data) { const self = this; const allElements = this.currentPage.getElements(elementName); const hashedData = data.raw(); if (hashedData.length === 0) { return Promise.reject('Missing table under the step.'); } return this.currentPage.waitForVisibilityOf(elementName).then(() => { return checkNumberOfElements.call(this, numberExpression, elementName).then(() => { const promises = []; return allElements .each(element => { hashedData.forEach(hash => { promises.push( matchers.match( element.element(self.currentPage.getElement(hash[0]).locator()), variableStore.replaceTextVariables(hash[1]) ) ); }); }) .then(() => Promise.all(promises)); }); }); }); Then(/^there are "([^"]*)" dropdown list elements with following options:$/, function(elementName, data) { const allOptionElements = this.currentPage.getElement(elementName); const hashedData = data.raw(); if (hashedData.length === 0) { return Promise.reject('Missing table under the step.'); } return this.currentPage.waitForVisibilityOf(elementName).then(() => { allOptionElements.getText().then(textArray => { if (textArray.length === hashedData.length) { hashedData.forEach(hash => { textArray.splice(textArray.indexOf(hash), 1); }); } else { return Promise.reject("Number of options doesn't match the number of asked"); } expect(textArray.length).toEqual(0); }); }); }); Then(/^there is element "([^"]*)" with value "([^"]*)"$/, function(elementName, value) { const pageElement = this.currentPage.getElement(elementName); return this.currentPage.waitForVisibilityOf(elementName).then(() => { return matchers .match(pageElement, variableStore.replaceTextVariables(value)) .then(matcherResult => expect(matcherResult).toBe(true)); }); }); Then(/^there is no element "([^"]*)" with value "([^"]*)"$/, function(elementName, value) { const pageElement = this.currentPage.getElement(elementName); return matchers .match(pageElement, variableStore.replaceTextVariables(value)) .catch(() => Promise.resolve(false)) .then(result => (result ? Promise.reject() : Promise.resolve())); }); Then(/^there is element "([^"]*)" containing "([^"]*)" text$/, function(elementName, value) { const pageElement = this.currentPage.getElement(elementName); return this.currentPage.waitForVisibilityOf(elementName).then(() => { return matchers.match(pageElement, variableStore.replaceTextVariables(`t:${value}`)); }); }); Then(/^there is no element "([^"]*)" containing "([^"]*)" text$/, function(elementName, value) { const pageElement = this.currentPage.getElement(elementName); return matchers .match(pageElement, variableStore.replaceTextVariables(`t:${value}`)) .catch(() => Promise.resolve(false)) .then(result => (result ? Promise.reject() : Promise.resolve())); }); Then(/^there is element "([^"]*)" matching "([^"]*)" matcher$/, function(elementName, matcher) { const pageElement = this.currentPage.getElement(elementName); return this.currentPage.waitForVisibilityOf(elementName).then(() => { return matchers .match(pageElement, variableStore.replaceTextVariables(`f:${matcher}`)) .then(matcherResult => expect(matcherResult).toBe(true)); }); }); Then(/^there is no element "([^"]*)" matching "([^"]*)" matcher$/, function(elementName, matcher) { const pageElement = this.currentPage.getElement(elementName); return this.currentPage.waitForVisibilityOf(elementName).then(() => { return matchers .match(pageElement, variableStore.replaceTextVariables(`f:${matcher}`)) .catch(() => Promise.resolve(false)) .then(result => (result ? Promise.reject() : Promise.resolve())); }); }); Then(/^there is element "([^"]*)" with "([^"]*)" regex$/, function(elementName, matcher) { const pageElement = this.currentPage.getElement(elementName); return this.currentPage.waitForVisibilityOf(elementName).then(() => { return matchers .match(pageElement, variableStore.replaceTextVariables(`r:${matcher}`)) .then(matcherResult => expect(matcherResult).toBe(true)); }); }); Then(/^there is no element "([^"]*)" with "([^"]*)" regex$/, function(elementName, matcher) { const pageElement = this.currentPage.getElement(elementName); return this.currentPage.waitForVisibilityOf(elementName).then(() => { return matchers .match(pageElement, variableStore.replaceTextVariables(`r:${matcher}`)) .catch(() => Promise.resolve(false)) .then(result => (result ? Promise.reject() : Promise.resolve())); }); }); Then(/^there are "([^"]*)" "([^"]*)" elements$/, checkNumberOfElements); Then(/^every "([^"]*)" element should have the same value for element "([^"]*)"$/, function( containerName, elementName ) { const self = this; return this.currentPage.waitForVisibilityOf(containerName).then(() => { return this.currentPage .getElements(containerName) .first() .element(self.currentPage.getElement(elementName).locator()) .getText() .then(firstElementText => { return self.currentPage.getElements(containerName).each(containerElement => { containerElement .element(self.currentPage.getElement(elementName).locator()) .getText() .then(elementText => { expect(elementText).toEqual(firstElementText); }); }); }); }); }); Then(/^the element "([^"]*)" should have an item with values:$/, function(elementName, data) { const self = this; const allElements = this.currentPage.getElements(elementName); const hashedData = data.raw(); if (hashedData.length === 0) { return Promise.reject('Missing table under the step.'); } const promises = []; return this.currentPage .waitForVisibilityOf(elementName) .then(() => allElements.each(element => { hashedData.forEach(hash => { promises.push( matchers .match( element.element(self.currentPage.getElement(hash[0]).locator()), variableStore.replaceTextVariables(hash[1]) ) .catch(() => false) ); }); }) ) .then(() => Promise.all(promises).then( handlePromises(hashedData, () => Promise.resolve(), () => Promise.reject('No matching element has been found.')) ) ); }); Then(/^the element "([^"]*)" should not have an item with values:$/, function(elementName, data) { const self = this; const allElements = this.currentPage.getElements(elementName); const hashedData = data.raw(); if (hashedData.length === 0) { return Promise.reject('Missing table under the step.'); } const promises = []; return allElements .each(element => { hashedData.forEach(hash => { promises.push( matchers .match( element.element(self.currentPage.getElement(hash[0]).locator()), variableStore.replaceTextVariables(hash[1]) ) .catch(() => false) ); }); }) .then(() => Promise.all(promises).then( handlePromises(hashedData, () => Promise.reject('Matching element has been found'), () => Promise.resolve()) ) ); }); Then(/^"([^"]*)" value on the "([^"]*)" list is sorted in "([^"]*)" order$/, function( elementValue, elementList, dependency ) { const self = this; const promise = []; return this.currentPage.waitForVisibilityOf(elementList).then(() => { return self.currentPage .getElements(elementList) .each(singleElement => { promise.push(singleElement.element(self.currentPage.getElement(elementValue).locator()).getText()); }) .then(() => Promise.all(promise)) .then(elementsValues => comparators.compare(elementsValues, dependency)); }); }); When(/^I infinitely scroll to the "([^"]*)" element$/, function(elementName) { const self = this; const scrollToLoader = () => { return self.currentPage .isPresent(elementName) .then(isPresent => { if (isPresent) { return self.currentPage.scrollIntoElement(elementName); } return Promise.resolve(); }) .then(() => self.currentPage.isPresent(elementName)) .then(isPresent => { if (isPresent) { return browser.sleep(1000).then(() => scrollToLoader()); } return Promise.resolve(); }); }; return scrollToLoader(); }); When(/^I press the "([^"]*)" key$/, key => { const keyTransformed = key.toUpperCase(); return Promise.resolve( browser .actions() .sendKeys(protractor.Key[keyTransformed]) .perform() ); }); When(/^I drag "([^"]*)" element and drop over "([^"]*)" element$/, async function(elementDrag, elementDrop) { const wait = timeToWait => browser.sleep(timeToWait); await this.currentPage.waitForVisibilityOf(elementDrag); await browser .actions() .mouseMove(this.currentPage.getElement(elementDrag)) .perform(); await wait(200); await browser .actions() .mouseDown() .perform(); await wait(200); await browser .actions() .mouseMove(this.currentPage.getElement(elementDrop)) .perform(); await wait(200); await browser .actions() .mouseUp() .perform(); }); ================================================ FILE: src/step_definitions/email.ts ================================================ import { Then } from 'cucumber'; import * as sugar from 'sugar-date/index'; import config from '../core/config.helper'; import { filters } from '../emails/filters'; import { regexBuilder } from '../matchers'; import { emailService } from '../emails'; function stopInterval(interval, callback) { clearInterval(interval); callback(); } function checkAttachmentsInEmail(email, filesExtensions, attachments) { let fileAttachments = attachments.filter(attachment => attachment.attachment_type === 'attachment'); const missingFiles = filesExtensions.reduce((previous, current) => { const expectedFile = fileAttachments.find(attachment => { return ( regexBuilder.buildRegex(current.name).test(attachment.filename) && regexBuilder.buildRegex(current.type).test(attachment.content_type) && attachment.attachment_size >= current.minimalSize ); }); if (typeof expectedFile === 'undefined') { previous.push(current); return previous; } fileAttachments = fileAttachments.filter(attachment => attachment.id !== expectedFile.id); return previous; }, []); if (missingFiles.length === 0) { return emailService.markAsRead(email); } return Promise.reject('Some attachments not found: ' + missingFiles.map(file => file.name).join(', ')); } function filterEmails(emails, data) { let originalEmails = emails; const checks = data.raw().filter(elem => elem[0] !== 'file'); for (const check of checks) { const checkType = check[0]; const checkValue = check[1]; originalEmails = filters.filter(originalEmails, checkType, checkValue, this); } return originalEmails; } function getFilesExtensions(data) { return data .raw() .filter(elem => elem[0] === 'file') .map(elem => { return { name: elem[1], type: elem[2], minimalSize: elem[3] }; }); } function rejectIfMaxRepeatsReached(filteredEmails, maxRepeats) { if (filteredEmails.length === 0 && maxRepeats === 0) { return Promise.reject('No emails found and maximum repeats reached'); } return filteredEmails; } function rejectIfMoreThanOneEmailFound(filteredEmails) { if (filteredEmails.length > 1) { return Promise.reject('More than one email found'); } return filteredEmails; } function rejectIfEmailFound(filteredEmails) { if (filteredEmails.length > 0) { return Promise.reject('Email has been found!'); } return filteredEmails; } function validateEmailDate(filteredEmails) { if (filteredEmails.length === 1 && sugar.Date.minutesFromNow(sugar.Date.create(filteredEmails[0].created_at)) < -10) { return Promise.reject('Email was sent more than 10 minutes ago. This is probably not what you are looking for.'); } return filteredEmails; } function validateEmailContentAndAttachments(filteredEmails, data, interval, sync) { if (filteredEmails.length === 1) { const filesExtensions = getFilesExtensions(data); if (filesExtensions.length > 0) { return emailService .getAttachments(filteredEmails[0]) .then(checkAttachmentsInEmail.bind(null, filteredEmails[0], filesExtensions)) .then(stopInterval.bind(null, interval, sync)); } return emailService.markAsRead(filteredEmails[0]).then(stopInterval.bind(null, interval, sync)); } } Then(/^the email has been sent and contains:$/, function(data, sync) { const self = this; const timeout = parseInt(config.intervalEmail) * 1000; let maxRepeats = config.maxEmailRepeats === undefined ? 5 : parseInt(config.maxEmailRepeats); const interval = setInterval(() => { console.log('Checking mailbox for email...'); emailService .getEmails() .then(emails => filterEmails.call(self, emails, data)) .then(filteredEmails => rejectIfMaxRepeatsReached(filteredEmails, maxRepeats)) .then(filteredEmails => rejectIfMoreThanOneEmailFound(filteredEmails)) .then(filteredEmails => validateEmailDate(filteredEmails)) .then(filteredEmails => validateEmailContentAndAttachments(filteredEmails, data, interval, sync)) .then(() => maxRepeats--) .catch(err => stopInterval(interval, sync.bind(null, err))); }, timeout); }); Then(/^the email with the following data has not been sent:$/, function(data, sync) { const self = this; const timeout = parseInt(config.intervalEmail) * 1000; let maxRepeats = 5; const interval = setInterval(() => { console.log('Checking mailbox for email...'); emailService .getEmails() .then(emails => filterEmails.call(self, emails, data)) .then(filteredEmails => rejectIfEmailFound(filteredEmails)) .then(filteredEmails => rejectIfMaxRepeatsReached(filteredEmails, maxRepeats)) .then(() => maxRepeats--) .catch(err => { err === 'No emails found and maximum repeats reached' ? stopInterval(interval, sync) : stopInterval(interval, sync.bind(null, err)); }); }, timeout); }); ================================================ FILE: src/step_definitions/file.ts ================================================ import { Then } from 'cucumber'; import fileManager from '../web/fs/file-manager.helper'; import variableStore from '../web/variable-store.helper'; Then(/^the file "([^"]*)" should be downloaded$/, filename => { return fileManager.wasDownloaded(variableStore.replaceTextVariables(filename)); }); Then(/^the file "([^"]*)" contains table data stored under "([^"]*)" variable$/, (filename, variableName) => { const file = fileManager.parseXLS(variableStore.replaceTextVariables(filename)); const storedData = variableStore.getVariableValue(variableName); const rows = file.filter((row, index) => row.length > 0 && index > 0); const findIndexes = () => { const allFoundIndexesInRows = []; storedData.forEach(storedItems => { const foundIndexesInRow = []; let previousFoundIndex = null; storedItems.forEach(storedValue => { for (const index in rows) { if (rows.hasOwnProperty(index)) { if (storedValue.match(/^\d+$/)) { if (previousFoundIndex !== null) { foundIndexesInRow.push(rows[previousFoundIndex].indexOf(parseInt(storedValue))); break; } if (rows[index].includes(parseInt(storedValue))) { previousFoundIndex = index; foundIndexesInRow.push(rows[index].indexOf(parseInt(storedValue))); break; } } if (previousFoundIndex !== null) { foundIndexesInRow.push(rows[previousFoundIndex].indexOf(storedValue)); break; } if (rows[index].includes(storedValue)) { previousFoundIndex = index; foundIndexesInRow.push(rows[index].indexOf(storedValue)); break; } } } }); allFoundIndexesInRows.push(foundIndexesInRow); }); return Promise.resolve(allFoundIndexesInRows); }; return findIndexes().then(allFoundIndexes => { if (allFoundIndexes[0].length !== storedData[0].length) { return Promise.reject('Values not found!'); } if (allFoundIndexes.length === 1) { return Promise.resolve(); } for (let index = 1; index < allFoundIndexes.length; index++) { if (JSON.stringify(allFoundIndexes[index]) !== JSON.stringify(allFoundIndexes[index - 1])) { return Promise.reject('Arrays are different!'); } } }); }); ================================================ FILE: src/step_definitions/form.ts ================================================ import { When, Then } from 'cucumber'; import { dictionaries } from '../dictionaries'; When(/^I fill the "([^"]*)" form with:$/, function(formName, data) { const self = this; return this.currentPage.waitForVisibilityOf(formName).then(() => self.currentPage.fillForm(data.raw())); }); Then(/^the "([^"]*)" form is filled with:$/, function(formName, data) { const self = this; return this.currentPage.waitForVisibilityOf(formName).then(() => self.currentPage.checkForm(data.raw())); }); Then(/^the error messages should be displayed:$/, function(data) { const self = this; const table = data.rows(); const promise = []; table.forEach(item => { promise.push( self.currentPage .waitForVisibilityOf(item[0]) .then(() => self.currentPage[item[0]].getText()) .then(text => { if (text.indexOf(dictionaries.findMappedValueByPhrase(item[1])) >= 0) { return Promise.resolve(); } return Promise.reject( `Error "${dictionaries.findMappedValueByPhrase(item[1])}" for element "${item[0]}" was not found.` ); }) ); }); return Promise.all(promise); }); ================================================ FILE: src/step_definitions/generators.ts ================================================ import { When } from 'cucumber'; import { transformers } from '../transformers'; import variableStore from '../web/variable-store.helper'; When(/^I generate random "([^"]*)" as "([^"]*)"$/, (generator, variableName) => { return transformers.transform(`g:${generator}`).then(result => variableStore.storeVariable(variableName, result)); }); ================================================ FILE: src/step_definitions/navigation.ts ================================================ import { Then, Given } from 'cucumber'; Given(/^I visit the "([^"]*)" page$/, function(pageName) { expect(browser.page[pageName]).toBeDefined(); this.currentPage = browser.page[pageName]; return this.currentPage.visit(); }); Given(/^I visit the "([^"]*)" page with parameters:$/, function(pageName, data) { expect(browser.page[pageName]).toBeDefined(); this.currentPage = browser.page[pageName]; return this.currentPage.visitWithParameters(data); }); Then(/^the "([^"]*)" page is displayed$/, function(pageName) { const self = this; return browser.page[pageName].isOn().then(checkResult => { if (typeof checkResult !== 'object') { return Promise.reject('Check result must be an object!!!'); } self.currentPage = browser.page[pageName]; self.urlParameters = checkResult.parameters; }); }); ================================================ FILE: src/step_definitions/performance.ts ================================================ import * as chalk from 'chalk'; import { When, Then } from 'cucumber'; import config from '../core/config.helper'; import { create as createAnalyser } from '../web/performance/time-to-first-byte-analyser.helper'; import * as fs from 'fs'; import { Proxy as Browsermob } from 'browsermob-proxy'; const analyser = createAnalyser(); let proxy; When(/^I start performance monitor mode$/, () => { proxy = new Browsermob({ port: config.browserMob.serverPort, }); let proxyReady = false; proxy.start(config.browserMob.port, (err) => { if (!err) { proxy.startHAR(config.browserMob.port, 'test', true, true, () => { proxyReady = true; }); } else { console.error(err); } }); browser.driver.wait(() => { return proxyReady; }); }); When(/^I save performance report file as "([^"]*)"$/, function (fileName) { const uniqueFileName = `${fileName}-${Date.now()}.har`; let proxyDone = false; proxy.getHAR(config.browserMob.port, (err, resp) => { if (!err) { console.log(`har saved at ${uniqueFileName}`); fs.writeFileSync(`reports/performance/${uniqueFileName}`, resp, 'utf8'); } else { console.err('Error getting HAR file: ' + err); } proxy.stop(config.browserMob.port, () => { proxyDone = true; }); }); return browser.driver.wait(() => { this.performanceReportFile = uniqueFileName; return proxyDone; }); }); Then(/^the requests should take a maximum of "([^"]*)" milliseconds$/, function (maxTiming) { try { const slowRequests = analyser.checkTiming(this.performanceReportFile, parseFloat(maxTiming)); if (slowRequests.length > 0) { slowRequests.forEach(({ url, ttfb }) => { console.log( chalk.white.bgRed( '\r\n', 'Slow request:', '\r\n', `URL: ${url}`, '\r\n', `TTFB: ${ttfb.toFixed(2)} ms`, '\r\n' ) ); }); return Promise.reject('TTFB value is too big! Details available above.'); } return Promise.resolve(); } catch (err) { return Promise.reject(err); } }); ================================================ FILE: src/step_definitions/tabs.ts ================================================ import { When } from 'cucumber'; When(/^I switch to window number "([^"]*)" of a browser$/, tabNumber => { return browser.getAllWindowHandles().then(handles => browser.switchTo().window(handles[tabNumber - 1])); }); When(/^I close the current browser tab$/, () => { return browser .close() .then(() => browser.getAllWindowHandles()) .then(tabs => browser.switchTo().window(tabs[0])); }); ================================================ FILE: src/tests/dictionaries/fake-dictionary.ts ================================================ import BaseDictionary from '../../dictionaries/base'; class FakeDictionary extends BaseDictionary { constructor() { super('fake-dictionary', { 'some-key': 'some-value', }); } } export = new FakeDictionary(); ================================================ FILE: src/tests/init.ts ================================================ import * as protractor from 'protractor'; // set global by to allow recursive call on src directory global.by = new protractor.ProtractorBy(); require('../core/prototypes'); ================================================ FILE: src/transformers/index.ts ================================================ import { create } from './transformers'; export const transformers = create(); ================================================ FILE: src/transformers/transformer/dictionary.transformer.spec.ts ================================================ import { create as createDictionaries } from '../../dictionaries/dictionaries'; import { createDictionaryTransformer } from './dictionary.transformer'; import fakeDictionary = require('../../tests/dictionaries/fake-dictionary'); const dictionaries = createDictionaries(); dictionaries.addDictionary(fakeDictionary); describe('Dictionary transformer', () => { it('returns found position in the dictionary', () => { const transformer = createDictionaryTransformer(dictionaries); expect(transformer.transform('fake-dictionary:some-key')).toEqual('some-value'); }); it('returns true when the prefix is correct', () => { const transformer = createDictionaryTransformer(); expect(transformer.isSatisfiedBy('d:')).toEqual(true); }); it('returns false when the prefix is incorrect', () => { const transformer = createDictionaryTransformer(); expect(transformer.isSatisfiedBy('v:')).toEqual(false); }); }); ================================================ FILE: src/transformers/transformer/dictionary.transformer.ts ================================================ import { dictionaries as dicts, Dictionaries } from '../../dictionaries'; import { Transformer } from '../transformer.interface'; class DictionaryTransformer implements Transformer { constructor(private dictionaries: Dictionaries) {} public isSatisfiedBy(prefix) { return prefix === 'd:'; } public transform(value) { const splittedValue = value.split(':'); return this.dictionaries.getMappedValue(splittedValue[0], splittedValue[1]); } } export const createDictionaryTransformer = (dictionaries = dicts) => new DictionaryTransformer(dictionaries); ================================================ FILE: src/transformers/transformer/generator.transformer.spec.ts ================================================ import { createGeneratorTransformer } from './generator.transformer'; describe('Generator transformer', () => { it('returns true when the prefix is correct', () => { const transformer = createGeneratorTransformer(); expect(transformer.isSatisfiedBy('g:')).toEqual(true); }); it('returns false when the prefix is incorrect', () => { const transformer = createGeneratorTransformer(); expect(transformer.isSatisfiedBy('d:')).toEqual(false); }); it('returns generated value ', () => { const mockedGenerators: any = { generate: value => 'my-generated-value', }; const transformer = createGeneratorTransformer(mockedGenerators); expect(transformer.transform('generator:generate')).toEqual('my-generated-value'); }); }); ================================================ FILE: src/transformers/transformer/generator.transformer.ts ================================================ import { generators, Generators } from '../../generators'; import { Transformer } from '../transformer.interface'; class GeneratorTransformer implements Transformer { constructor(public generator: Generators) {} public isSatisfiedBy(prefix) { return prefix === 'g:'; } public transform(value) { const splittedValues = value.split(':'); const generatorName = splittedValues[0]; return this.generator.generate(generatorName, splittedValues.slice(1)); } } export const createGeneratorTransformer = (geners = generators) => new GeneratorTransformer(geners); ================================================ FILE: src/transformers/transformer/variable-store.transformer.spec.ts ================================================ import { createVariableStoreTransformer } from './variable-store.transformer'; describe('Variable store transformer', () => { it('returns replaced text', () => { const fakeValue: any = { getVariableValue: () => 'expected value', }; const transformer = createVariableStoreTransformer(fakeValue); expect(transformer.transform('given value')).toEqual('expected value'); }); it('returns true when the prefix is correct', () => { const transformer = createVariableStoreTransformer(); expect(transformer.isSatisfiedBy('v:')).toEqual(true); }); it('returns false when the prefix is incorrect', () => { const transformer = createVariableStoreTransformer(); expect(transformer.isSatisfiedBy('t:')).toEqual(false); }); }); ================================================ FILE: src/transformers/transformer/variable-store.transformer.ts ================================================ import store from '../../web/variable-store.helper'; import { VariableStore } from '../../web/variable-store.helper'; import { Transformer } from '../transformer.interface'; class VariableStoreTransformer implements Transformer { constructor(private variableStore: VariableStore) {} public isSatisfiedBy(prefix) { return prefix === 'v:'; } public transform(value) { return this.variableStore.getVariableValue(value); } } export const createVariableStoreTransformer = (variableStore = store) => new VariableStoreTransformer(variableStore); ================================================ FILE: src/transformers/transformer.interface.ts ================================================ export interface Transformer { isSatisfiedBy(prefix: string): boolean; transform(transform: string): any; } ================================================ FILE: src/transformers/transformers.spec.ts ================================================ import { create } from './transformers'; const transformers = create(); describe('Value transformers', () => { it('returns the same value if transformer has not been found', () => { const emptyTransformer = create([]); expect(emptyTransformer.transform('some value')).toEqual('some value'); }); it('returns transformed value when expected transformer has been found', () => { const fakeTransformer: any = { isSatisfiedBy: prefix => prefix === 'v:', transform: value => { expect(value).toEqual('value'); return 'expected value'; }, }; const transformers = create([fakeTransformer]); expect(transformers.transform('v:value')).toEqual('expected value'); }); it('adds a transformer', () => { const customTransformer = { isSatisfiedBy: prefix => prefix === 'j:', transform: () => 'custom-transformer', }; transformers.addTransformer(customTransformer); expect(transformers.transform('j:custom-transformer')).toEqual('custom-transformer'); }); }); ================================================ FILE: src/transformers/transformers.ts ================================================ import { createDictionaryTransformer } from './transformer/dictionary.transformer'; import { createGeneratorTransformer } from './transformer/generator.transformer'; import { createVariableStoreTransformer } from './transformer/variable-store.transformer'; import { Transformer } from './transformer.interface'; class Transformers { constructor(private availableTransformers: Transformer[]) {} public transform(value: string): any { const transformer = this.findTransformer(value.substr(0, 2)); if (transformer === undefined) { return value; } return transformer.transform(value.substr(2)); } public findTransformer(prefix: string): Transformer { return this.availableTransformers.find(transformer => transformer.isSatisfiedBy(prefix)); } public addTransformer(transformer: Transformer): void { this.availableTransformers.push(transformer); } } const transformers = [createVariableStoreTransformer(), createDictionaryTransformer(), createGeneratorTransformer()]; export const create = (transf = transformers) => new Transformers(transf); ================================================ FILE: src/web/browsers/browsers-config.helper.ts ================================================ import * as glob from 'glob'; import * as path from 'path'; import { createFirefoxProfile } from './create-firefox-profile.helper'; import { safariBrowserConfigurator } from './safari-browser-configurator.helper'; import { prepareBrowserInstance } from '../parallel/prepare-browser-instance-specs.helper'; import { chunkSpecs } from '../parallel/chunk-specs.helper'; const getDefaultBrowsersConfigs = (config): any => { const chromeConfig = { browserName: 'chrome', chromeOptions: { args: [], prefs: { credentials_enable_service: false, profile: { password_manager_enabled: false, }, download: { prompt_for_download: false, default_directory: config.projectPath + config.downloads, directory_upgrade: true, }, }, }, }; const firefoxConfig = { browserName: 'firefox', marionette: true, 'moz:firefoxOptions': { args: [], }, }; const safariConfig = { browserName: 'safari', }; const ieConfig = { browserName: 'internet explorer', }; return { chromeConfig, firefoxConfig, safariConfig, ieConfig, }; }; const getExtendedBrowsersConfigs = (config, commandArgs): any => { const configs = getDefaultBrowsersConfigs(config); if (config.performance) { configs.chromeConfig.proxy = { proxyType: 'manual', httpProxy: `${config.browserMob.host}:${config.browserMob.port}`, sslProxy: `${config.browserMob.host}:${config.browserMob.port}`, }; } if (config.noGpu) { configs.chromeConfig.chromeOptions.args = [ ...configs.chromeConfig.chromeOptions.args, '--disable-gpu', '--disable-impl-side-painting', '--disable-gpu-sandbox', '--disable-accelerated-2d-canvas', '--disable-accelerated-jpeg-decoding', '--no-sandbox', ]; } if ( (config.headless && commandArgs.headless === undefined) || (commandArgs.headless && commandArgs.headless !== 'false') ) { configs.chromeConfig.chromeOptions.args = [ ...configs.chromeConfig.chromeOptions.args, '--headless', `--window-size=${config.browserWidth}x${config.browserHeight}`, ]; configs.firefoxConfig['moz:firefoxOptions'].args = [ ...configs.firefoxConfig['moz:firefoxOptions'].args, '-headless', `--window-size=${config.browserWidth}x${config.browserHeight}`, ]; } return configs; }; export const browsersConfiguration = (config, commandArgs): any => { return () => { const browsersSettings = []; const browserConfigs = getExtendedBrowsersConfigs(config, commandArgs); const allSpecs = glob.sync(config.features.map(file => path.join(config.projectPath, file, '**/*.feature'))[0]); const isParallel = commandArgs.parallel !== undefined && Number.isInteger(commandArgs.parallel) && commandArgs.parallel !== 0; const numberOfInstances = isParallel ? commandArgs.parallel >= allSpecs.length ? allSpecs.length : commandArgs.parallel : 1; const expectedArrayLength = Math.ceil(allSpecs.length / numberOfInstances); const chunkedSpecs = chunkSpecs(commandArgs, allSpecs, expectedArrayLength, numberOfInstances); if (allSpecs.length === 0) { throw new Error('Could not find any files matching regex in the directory!'); } const pushPreparedBrowserInstance = browserType => { for (let i = 0; i < numberOfInstances; i++) { browsersSettings.push(prepareBrowserInstance(browserConfigs[browserType], chunkedSpecs[i])); } }; if (commandArgs.browserstack) { return Promise.resolve([prepareBrowserInstance(config.browserstack.capabilities, allSpecs)]); } if (commandArgs.firefox) { browserConfigs.firefoxConfig.firefox_profile = createFirefoxProfile(config); pushPreparedBrowserInstance('firefoxConfig'); } if (commandArgs.safari) { safariBrowserConfigurator(config); pushPreparedBrowserInstance('safariConfig'); } if (commandArgs.ie) { pushPreparedBrowserInstance('ieConfig'); } if ( commandArgs.chrome || (commandArgs.firefox === undefined && commandArgs.safari === undefined && commandArgs.ie === undefined) ) { pushPreparedBrowserInstance('chromeConfig'); } return Promise.resolve(browsersSettings); }; }; export const setSeleniumAddress = (commandArgs, config): string => { return commandArgs.browserstack ? config.browserstack.seleniumAddress : ''; }; ================================================ FILE: src/web/browsers/browserstack-config.helper.ts ================================================ import * as browserstack from 'browserstack-local'; import * as chalk from 'chalk'; import * as shell from 'shelljs'; import config from '../../core/config.helper'; export const disconnectBrowserstack = (browserstackEnabled: boolean) => { if (browserstackEnabled && config.browserstack) { const browserstackPid = shell.exec(`lsof -t -i :${config.browserstack.defaultPort}`).stdout; if (browserstackPid.length > 0) { return shell.exec(`kill -9 ${browserstackPid}`); } } return Promise.resolve(); }; export const connectBrowserstack = (browserstackKey: string) => { console.log( chalk.black.bgYellow( 'Keep in mind that Browserstack capabilities cannot be used with the local. Check the documentation for more information!' ) ); disconnectBrowserstack(true); return new Promise((resolve, reject) => { const bsLocal = new browserstack.Local(); bsLocal.start({ key: browserstackKey }, (error) => { if (error) { return reject(error); } console.log('Connected to the Browsertack Selenium server! Now testing...'); resolve(true); }); }); }; ================================================ FILE: src/web/browsers/create-firefox-profile.helper.ts ================================================ import * as firefox from 'selenium-webdriver/firefox'; import * as path from 'path'; export const createFirefoxProfile = config => { const profile = new firefox.Profile(); profile.setPreference('browser.download.dir', path.join(config.projectPath, config.downloads)); profile.setPreference('browser.download.folderList', 2); profile.setPreference('browser.download.panel.shown', true); profile.setPreference('browser.safebrowsing.downloads.enabled', false); profile.setPreference('browser.helperApps.alwaysAsk.force', false); profile.setPreference('browser.download.manager.showWhenStarting', false); profile.setPreference('browser.download.manager.showAlertOnComplete', false); profile.setPreference( 'browser.helperApps.neverAsk.saveToDisk', 'application/vnd.hzn-3d-crossword;video/3gpp;video/3gpp2;application/vnd.mseq;application/vnd.3m.post-it-notes;application/vnd.3gpp.pic-bw-large;application/vnd.3gpp.pic-bw-small;application/vnd.3gpp.pic-bw-var;application/vnd.3gp2.tcap;application/x-7z-compressed;application/x-abiword;application/x-ace-compressed;application/vnd.americandynamics.acc;application/vnd.acucobol;application/vnd.acucorp;audio/adpcm;application/x-authorware-bin;application/x-athorware-map;application/x-authorware-seg;application/vnd.adobe.air-application-installer-package+zip;application/x-shockwave-flash;application/vnd.adobe.fxp;application/pdf;application/vnd.cups-ppd;application/x-director;applicaion/vnd.adobe.xdp+xml;application/vnd.adobe.xfdf;audio/x-aac;application/vnd.ahead.space;application/vnd.airzip.filesecure.azf;application/vnd.airzip.filesecure.azs;application/vnd.amazon.ebook;application/vnd.amiga.ami;applicatin/andrew-inset;application/vnd.android.package-archive;application/vnd.anser-web-certificate-issue-initiation;application/vnd.anser-web-funds-transfer-initiation;application/vnd.antix.game-component;application/vnd.apple.installe+xml;application/applixware;application/vnd.hhe.lesson-player;application/vnd.aristanetworks.swi;text/x-asm;application/atomcat+xml;application/atomsvc+xml;application/atom+xml;application/pkix-attr-cert;audio/x-aiff;video/x-msvieo;application/vnd.audiograph;image/vnd.dxf;model/vnd.dwf;text/plain-bas;application/x-bcpio;application/octet-stream;image/bmp;application/x-bittorrent;application/vnd.rim.cod;application/vnd.blueice.multipass;application/vnd.bm;application/x-sh;image/prs.btif;application/vnd.businessobjects;application/x-bzip;application/x-bzip2;application/x-csh;text/x-c;application/vnd.chemdraw+xml;text/css;chemical/x-cdx;chemical/x-cml;chemical/x-csml;application/vn.contact.cmsg;application/vnd.claymore;application/vnd.clonk.c4group;image/vnd.dvb.subtitle;application/cdmi-capability;application/cdmi-container;application/cdmi-domain;application/cdmi-object;application/cdmi-queue;applicationvnd.cluetrust.cartomobile-config;application/vnd.cluetrust.cartomobile-config-pkg;image/x-cmu-raster;model/vnd.collada+xml;text/csv;application/mac-compactpro;application/vnd.wap.wmlc;image/cgm;x-conference/x-cooltalk;image/x-cmx;application/vnd.xara;application/vnd.cosmocaller;application/x-cpio;application/vnd.crick.clicker;application/vnd.crick.clicker.keyboard;application/vnd.crick.clicker.palette;application/vnd.crick.clicker.template;application/vn.crick.clicker.wordbank;application/vnd.criticaltools.wbs+xml;application/vnd.rig.cryptonote;chemical/x-cif;chemical/x-cmdf;application/cu-seeme;application/prs.cww;text/vnd.curl;text/vnd.curl.dcurl;text/vnd.curl.mcurl;text/vnd.crl.scurl;application/vnd.curl.car;application/vnd.curl.pcurl;application/vnd.yellowriver-custom-menu;application/dssc+der;application/dssc+xml;application/x-debian-package;audio/vnd.dece.audio;image/vnd.dece.graphic;video/vnd.dec.hd;video/vnd.dece.mobile;video/vnd.uvvu.mp4;video/vnd.dece.pd;video/vnd.dece.sd;video/vnd.dece.video;application/x-dvi;application/vnd.fdsn.seed;application/x-dtbook+xml;application/x-dtbresource+xml;application/vnd.dvb.ait;applcation/vnd.dvb.service;audio/vnd.digital-winds;image/vnd.djvu;application/xml-dtd;application/vnd.dolby.mlp;application/x-doom;application/vnd.dpgraph;audio/vnd.dra;application/vnd.dreamfactory;audio/vnd.dts;audio/vnd.dts.hd;imag/vnd.dwg;application/vnd.dynageo;application/ecmascript;application/vnd.ecowin.chart;image/vnd.fujixerox.edmics-mmr;image/vnd.fujixerox.edmics-rlc;application/exi;application/vnd.proteus.magazine;application/epub+zip;message/rfc82;application/vnd.enliven;application/vnd.is-xpr;image/vnd.xiff;application/vnd.xfdl;application/emma+xml;application/vnd.ezpix-album;application/vnd.ezpix-package;image/vnd.fst;video/vnd.fvt;image/vnd.fastbidsheet;application/vn.denovo.fcselayout-link;video/x-f4v;video/x-flv;image/vnd.fpx;image/vnd.net-fpx;text/vnd.fmi.flexstor;video/x-fli;application/vnd.fluxtime.clip;application/vnd.fdf;text/x-fortran;application/vnd.mif;application/vnd.framemaker;imae/x-freehand;application/vnd.fsc.weblaunch;application/vnd.frogans.fnc;application/vnd.frogans.ltf;application/vnd.fujixerox.ddd;application/vnd.fujixerox.docuworks;application/vnd.fujixerox.docuworks.binder;application/vnd.fujitu.oasys;application/vnd.fujitsu.oasys2;application/vnd.fujitsu.oasys3;application/vnd.fujitsu.oasysgp;application/vnd.fujitsu.oasysprs;application/x-futuresplash;application/vnd.fuzzysheet;image/g3fax;application/vnd.gmx;model/vn.gtw;application/vnd.genomatix.tuxedo;application/vnd.geogebra.file;application/vnd.geogebra.tool;model/vnd.gdl;application/vnd.geometry-explorer;application/vnd.geonext;application/vnd.geoplan;application/vnd.geospace;applicatio/x-font-ghostscript;application/x-font-bdf;application/x-gtar;application/x-texinfo;application/x-gnumeric;application/vnd.google-earth.kml+xml;application/vnd.google-earth.kmz;application/vnd.grafeq;image/gif;text/vnd.graphviz;aplication/vnd.groove-account;application/vnd.groove-help;application/vnd.groove-identity-message;application/vnd.groove-injector;application/vnd.groove-tool-message;application/vnd.groove-tool-template;application/vnd.groove-vcar;video/h261;video/h263;video/h264;application/vnd.hp-hpid;application/vnd.hp-hps;application/x-hdf;audio/vnd.rip;application/vnd.hbci;application/vnd.hp-jlyt;application/vnd.hp-pcl;application/vnd.hp-hpgl;application/vnd.yamaha.h-script;application/vnd.yamaha.hv-dic;application/vnd.yamaha.hv-voice;application/vnd.hydrostatix.sof-data;application/hyperstudio;application/vnd.hal+xml;text/html;application/vnd.ibm.rights-management;application/vnd.ibm.securecontainer;text/calendar;application/vnd.iccprofile;image/x-icon;application/vnd.igloader;image/ief;application/vnd.immervision-ivp;application/vnd.immervision-ivu;application/reginfo+xml;text/vnd.in3d.3dml;text/vnd.in3d.spot;mode/iges;application/vnd.intergeo;application/vnd.cinderella;application/vnd.intercon.formnet;application/vnd.isac.fcs;application/ipfix;application/pkix-cert;application/pkixcmp;application/pkix-crl;application/pkix-pkipath;applicaion/vnd.insors.igm;application/vnd.ipunplugged.rcprofile;application/vnd.irepository.package+xml;text/vnd.sun.j2me.app-descriptor;application/java-archive;application/java-vm;application/x-java-jnlp-file;application/java-serializd-object;text/x-java-source,java;application/javascript;application/json;application/vnd.joost.joda-archive;video/jpm;image/jpeg;video/jpeg;application/vnd.kahootz;application/vnd.chipnuts.karaoke-mmd;application/vnd.kde.karbon;aplication/vnd.kde.kchart;application/vnd.kde.kformula;application/vnd.kde.kivio;application/vnd.kde.kontour;application/vnd.kde.kpresenter;application/vnd.kde.kspread;application/vnd.kde.kword;application/vnd.kenameaapp;applicatin/vnd.kidspiration;application/vnd.kinar;application/vnd.kodak-descriptor;application/vnd.las.las+xml;application/x-latex;application/vnd.llamagraphics.life-balance.desktop;application/vnd.llamagraphics.life-balance.exchange+xml;application/vnd.jam;application/vnd.lotus-1-2-3;application/vnd.lotus-approach;application/vnd.lotus-freelance;application/vnd.lotus-notes;application/vnd.lotus-organizer;application/vnd.lotus-screencam;application/vnd.lotus-wordro;audio/vnd.lucent.voice;audio/x-mpegurl;video/x-m4v;application/mac-binhex40;application/vnd.macports.portpkg;application/vnd.osgeo.mapguide.package;application/marc;application/marcxml+xml;application/mxf;application/vnd.wolfrm.player;application/mathematica;application/mathml+xml;application/mbox;application/vnd.medcalcdata;application/mediaservercontrol+xml;application/vnd.mediastation.cdkey;application/vnd.mfer;application/vnd.mfmp;model/mesh;appliation/mads+xml;application/mets+xml;application/mods+xml;application/metalink4+xml;application/vnd.ms-powerpoint.template.macroenabled.12;application/vnd.ms-word.document.macroenabled.12;application/vnd.ms-word.template.macroenabed.12;application/vnd.mcd;application/vnd.micrografx.flo;application/vnd.micrografx.igx;application/vnd.eszigno3+xml;application/x-msaccess;video/x-ms-asf;application/x-msdownload;application/vnd.ms-artgalry;application/vnd.ms-ca-compressed;application/vnd.ms-ims;application/x-ms-application;application/x-msclip;image/vnd.ms-modi;application/vnd.ms-fontobject;application/vnd.ms-excel;application/vnd.ms-excel.addin.macroenabled.12;application/vnd.ms-excelsheet.binary.macroenabled.12;application/vnd.ms-excel.template.macroenabled.12;application/vnd.ms-excel.sheet.macroenabled.12;application/vnd.ms-htmlhelp;application/x-mscardfile;application/vnd.ms-lrm;application/x-msmediaview;aplication/x-msmoney;application/vnd.openxmlformats-officedocument.presentationml.presentation;application/vnd.openxmlformats-officedocument.presentationml.slide;application/vnd.openxmlformats-officedocument.presentationml.slideshw;application/vnd.openxmlformats-officedocument.presentationml.template;application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;application/vnd.openxmlformats-officedocument.spreadsheetml.template;application/vnd.openxmformats-officedocument.wordprocessingml.document;application/vnd.openxmlformats-officedocument.wordprocessingml.template;application/x-msbinder;application/vnd.ms-officetheme;application/onenote;audio/vnd.ms-playready.media.pya;vdeo/vnd.ms-playready.media.pyv;application/vnd.ms-powerpoint;application/vnd.ms-powerpoint.addin.macroenabled.12;application/vnd.ms-powerpoint.slide.macroenabled.12;application/vnd.ms-powerpoint.presentation.macroenabled.12;appliation/vnd.ms-powerpoint.slideshow.macroenabled.12;application/vnd.ms-project;application/x-mspublisher;application/x-msschedule;application/x-silverlight-app;application/vnd.ms-pki.stl;application/vnd.ms-pki.seccat;application/vn.visio;video/x-ms-wm;audio/x-ms-wma;audio/x-ms-wax;video/x-ms-wmx;application/x-ms-wmd;application/vnd.ms-wpl;application/x-ms-wmz;video/x-ms-wmv;video/x-ms-wvx;application/x-msmetafile;application/x-msterminal;application/msword;application/x-mswrite;application/vnd.ms-works;application/x-ms-xbap;application/vnd.ms-xpsdocument;audio/midi;application/vnd.ibm.minipay;application/vnd.ibm.modcap;application/vnd.jcp.javame.midlet-rms;application/vnd.tmobile-ivetv;application/x-mobipocket-ebook;application/vnd.mobius.mbk;application/vnd.mobius.dis;application/vnd.mobius.plc;application/vnd.mobius.mqy;application/vnd.mobius.msl;application/vnd.mobius.txf;application/vnd.mobius.daf;tex/vnd.fly;application/vnd.mophun.certificate;application/vnd.mophun.application;video/mj2;audio/mpeg;video/vnd.mpegurl;video/mpeg;application/mp21;audio/mp4;video/mp4;application/mp4;application/vnd.apple.mpegurl;application/vnd.msician;application/vnd.muvee.style;application/xv+xml;application/vnd.nokia.n-gage.data;application/vnd.nokia.n-gage.symbian.install;application/x-dtbncx+xml;application/x-netcdf;application/vnd.neurolanguage.nlu;application/vnd.na;application/vnd.noblenet-directory;application/vnd.noblenet-sealer;application/vnd.noblenet-web;application/vnd.nokia.radio-preset;application/vnd.nokia.radio-presets;text/n3;application/vnd.novadigm.edm;application/vnd.novadim.edx;application/vnd.novadigm.ext;application/vnd.flographit;audio/vnd.nuera.ecelp4800;audio/vnd.nuera.ecelp7470;audio/vnd.nuera.ecelp9600;application/oda;application/ogg;audio/ogg;video/ogg;application/vnd.oma.dd2+xml;applicatin/vnd.oasis.opendocument.text-web;application/oebps-package+xml;application/vnd.intu.qbo;application/vnd.openofficeorg.extension;application/vnd.yamaha.openscoreformat;audio/webm;video/webm;application/vnd.oasis.opendocument.char;application/vnd.oasis.opendocument.chart-template;application/vnd.oasis.opendocument.database;application/vnd.oasis.opendocument.formula;application/vnd.oasis.opendocument.formula-template;application/vnd.oasis.opendocument.grapics;application/vnd.oasis.opendocument.graphics-template;application/vnd.oasis.opendocument.image;application/vnd.oasis.opendocument.image-template;application/vnd.oasis.opendocument.presentation;application/vnd.oasis.opendocumen.presentation-template;application/vnd.oasis.opendocument.spreadsheet;application/vnd.oasis.opendocument.spreadsheet-template;application/vnd.oasis.opendocument.text;application/vnd.oasis.opendocument.text-master;application/vnd.asis.opendocument.text-template;image/ktx;application/vnd.sun.xml.calc;application/vnd.sun.xml.calc.template;application/vnd.sun.xml.draw;application/vnd.sun.xml.draw.template;application/vnd.sun.xml.impress;application/vnd.sun.xl.impress.template;application/vnd.sun.xml.math;application/vnd.sun.xml.writer;application/vnd.sun.xml.writer.global;application/vnd.sun.xml.writer.template;application/x-font-otf;application/vnd.yamaha.openscoreformat.osfpvg+xml;application/vnd.osgi.dp;application/vnd.palm;text/x-pascal;application/vnd.pawaafile;application/vnd.hp-pclxl;application/vnd.picsel;image/x-pcx;image/vnd.adobe.photoshop;application/pics-rules;image/x-pict;application/x-chat;aplication/pkcs10;application/x-pkcs12;application/pkcs7-mime;application/pkcs7-signature;application/x-pkcs7-certreqresp;application/x-pkcs7-certificates;application/pkcs8;application/vnd.pocketlearn;image/x-portable-anymap;image/-portable-bitmap;application/x-font-pcf;application/font-tdpfr;application/x-chess-pgn;image/x-portable-graymap;image/png;image/x-portable-pixmap;application/pskc+xml;application/vnd.ctc-posml;application/postscript;application/xfont-type1;application/vnd.powerbuilder6;application/pgp-encrypted;application/pgp-signature;application/vnd.previewsystems.box;application/vnd.pvi.ptid1;application/pls+xml;application/vnd.pg.format;application/vnd.pg.osasli;tex/prs.lines.tag;application/x-font-linux-psf;application/vnd.publishare-delta-tree;application/vnd.pmi.widget;application/vnd.quark.quarkxpress;application/vnd.epson.esf;application/vnd.epson.msf;application/vnd.epson.ssf;applicaton/vnd.epson.quickanime;application/vnd.intu.qfx;video/quicktime;application/x-rar-compressed;audio/x-pn-realaudio;audio/x-pn-realaudio-plugin;application/rsd+xml;application/vnd.rn-realmedia;application/vnd.realvnc.bed;applicatin/vnd.recordare.musicxml;application/vnd.recordare.musicxml+xml;application/relax-ng-compact-syntax;application/vnd.data-vision.rdz;application/rdf+xml;application/vnd.cloanto.rp9;application/vnd.jisp;application/rtf;text/richtex;application/vnd.route66.link66+xml;application/rss+xml;application/shf+xml;application/vnd.sailingtracker.track;image/svg+xml;application/vnd.sus-calendar;application/sru+xml;application/set-payment-initiation;application/set-reistration-initiation;application/vnd.sema;application/vnd.semd;application/vnd.semf;application/vnd.seemail;application/x-font-snf;application/scvp-vp-request;application/scvp-vp-response;application/scvp-cv-request;application/svp-cv-response;application/sdp;text/x-setext;video/x-sgi-movie;application/vnd.shana.informed.formdata;application/vnd.shana.informed.formtemplate;application/vnd.shana.informed.interchange;application/vnd.shana.informed.package;application/thraud+xml;application/x-shar;image/x-rgb;application/vnd.epson.salt;application/vnd.accpac.simply.aso;application/vnd.accpac.simply.imp;application/vnd.simtech-mindmapper;application/vnd.commonspace;application/vnd.ymaha.smaf-audio;application/vnd.smaf;application/vnd.yamaha.smaf-phrase;application/vnd.smart.teacher;application/vnd.svd;application/sparql-query;application/sparql-results+xml;application/srgs;application/srgs+xml;application/sml+xml;application/vnd.koan;text/sgml;application/vnd.stardivision.calc;application/vnd.stardivision.draw;application/vnd.stardivision.impress;application/vnd.stardivision.math;application/vnd.stardivision.writer;application/vnd.tardivision.writer-global;application/vnd.stepmania.stepchart;application/x-stuffit;application/x-stuffitx;application/vnd.solent.sdkm+xml;application/vnd.olpc-sugar;audio/basic;application/vnd.wqd;application/vnd.symbian.install;application/smil+xml;application/vnd.syncml+xml;application/vnd.syncml.dm+wbxml;application/vnd.syncml.dm+xml;application/x-sv4cpio;application/x-sv4crc;application/sbml+xml;text/tab-separated-values;image/tiff;application/vnd.to.intent-module-archive;application/x-tar;application/x-tcl;application/x-tex;application/x-tex-tfm;application/tei+xml;text/plain;application/vnd.spotfire.dxp;application/vnd.spotfire.sfs;application/timestamped-data;applicationvnd.trid.tpt;application/vnd.triscape.mxs;text/troff;application/vnd.trueapp;application/x-font-ttf;text/turtle;application/vnd.umajin;application/vnd.uoml+xml;application/vnd.unity;application/vnd.ufdl;text/uri-list;application/nd.uiq.theme;application/x-ustar;text/x-uuencode;text/x-vcalendar;text/x-vcard;application/x-cdlink;application/vnd.vsf;model/vrml;application/vnd.vcx;model/vnd.mts;model/vnd.vtu;application/vnd.visionary;video/vnd.vivo;applicatin/ccxml+xml,;application/voicexml+xml;application/x-wais-source;application/vnd.wap.wbxml;image/vnd.wap.wbmp;audio/x-wav;application/davmount+xml;application/x-font-woff;application/wspolicy+xml;image/webp;application/vnd.webturb;application/widget;application/winhlp;text/vnd.wap.wml;text/vnd.wap.wmlscript;application/vnd.wap.wmlscriptc;application/vnd.wordperfect;application/vnd.wt.stf;application/wsdl+xml;image/x-xbitmap;image/x-xpixmap;image/x-xwindowump;application/x-x509-ca-cert;application/x-xfig;application/xhtml+xml;application/xml;application/xcap-diff+xml;application/xenc+xml;application/patch-ops-error+xml;application/resource-lists+xml;application/rls-services+xml;aplication/resource-lists-diff+xml;application/xslt+xml;application/xop+xml;application/x-xpinstall;application/xspf+xml;application/vnd.mozilla.xul+xml;chemical/x-xyz;text/yaml;application/yang;application/yin+xml;application/vnd.ul;application/zip;application/vnd.handheld-entertainment+xml;application/vnd.zzazz.deck+xml' ); profile.setPreference('pdfjs.disabled', true); return profile; }; ================================================ FILE: src/web/browsers/get-browser-drivers.helper.ts ================================================ import * as fs from 'fs'; export const getBrowsersDrivers = (commandArgs): string[] => { const drivers: string[] = []; const pathToDrivers = './node_modules/protractor/node_modules/webdriver-manager/selenium'; if (commandArgs.ie) { // This is required as Protractor cannot find IEDriverServer. The other drivers does not require any additional configuration. const availableDrivers = fs.readdirSync(pathToDrivers); const IEDriver = availableDrivers.filter(item => item.match('IEDriverServer([0-9].[0-9]{3}.[0-9]).exe'))[0]; drivers.push(`-Dwebdriver.ie.driver=${pathToDrivers}/${IEDriver}`); } return drivers; }; ================================================ FILE: src/web/browsers/safari-browser-configurator.helper.ts ================================================ import * as path from 'path'; import * as shell from 'shelljs'; export const safariBrowserConfigurator = config => { shell.exec(`defaults write -app Safari DownloadsPath ${path.join(config.projectPath, config.downloads)}`); }; ================================================ FILE: src/web/cucumber/config.ts ================================================ import config from '../../core/config.helper'; import { setDefaultTimeout } from 'cucumber'; setDefaultTimeout(Number(config.timeout) * 1000); ================================================ FILE: src/web/cucumber/hooks/clear-download.hook.ts ================================================ import { After, Before } from 'cucumber'; import { HookHandler } from './hook.interface'; import * as fs from 'fs'; import * as path from 'path'; import config from '../../../core/config.helper'; const clearDownload = callback => { const files = fs.readdirSync(path.join(config.projectPath, config.downloads)).filter(file => file !== '.gitkeep'); for (const file of files) { fs.unlinkSync(path.join(config.projectPath, config.downloads, file)); } callback(); }; class ClearDownloadHook implements HookHandler { public initializeHook() { Before('@downloadClearBefore', (scenario, callback) => { clearDownload(callback); }); After('@downloadClearAfter', (scenario, callback) => { clearDownload(callback); }); } public getPriority() { return 1; } } export const clearDownloadHook = new ClearDownloadHook(); ================================================ FILE: src/web/cucumber/hooks/clear-variables.hook.ts ================================================ import { Before } from 'cucumber'; import { HookHandler } from './hook.interface'; import userProvider from '../../user-provider.helper'; import variableStore from '../../variable-store.helper'; class ClearVariablesHook implements HookHandler { public initializeHook() { Before(function(scenario, callback) { this.currentUser = null; if (typeof this.userProvider === 'undefined') { this.userProvider = userProvider; } variableStore.clearVariables(); callback(); }); } public getPriority() { return 1; } } export const clearVariablesHook = new ClearVariablesHook(); ================================================ FILE: src/web/cucumber/hooks/hook.interface.ts ================================================ export interface HookHandler { initializeHook(): void; getPriority(): number; } ================================================ FILE: src/web/cucumber/hooks/hooks.ts ================================================ import * as hookHandler from './index'; import { HookHandler } from './hook.interface'; class HookHandlers { constructor( private availableHandlers: HookHandler[] = [ hookHandler.takeScreenshotHook, hookHandler.reloadFixturesHook, hookHandler.clearDownloadHook, hookHandler.reloadUserHook, hookHandler.clearVariablesHook, ] ) {} public addHook(handler: HookHandler): void { this.availableHandlers.push(handler); } public initializeHook(): void { const handlers = this.getHooks(); for (const handler of handlers) { handler.initializeHook(); } } public getHooks(): HookHandler[] { return this.availableHandlers.sort((handler, otherHandler) => handler.getPriority() - otherHandler.getPriority()); } } export const hookHandlers = new HookHandlers(); ================================================ FILE: src/web/cucumber/hooks/index.ts ================================================ export * from './take-screenshots.hook'; export * from './reload-fixtures.hook'; export * from './reload-user.hook'; export * from './clear-download.hook'; export * from './clear-variables.hook'; ================================================ FILE: src/web/cucumber/hooks/reload-fixtures.hook.ts ================================================ import { Before } from 'cucumber'; import { HookHandler } from './hook.interface'; import * as chalk from 'chalk'; import parameters from '../../parameters'; import fixturesLoader from '../../fixtures/fixtures-loader.helper'; const logRequestTime = (timeStart) => { const timeDiff = process.hrtime(timeStart); console.log(chalk.black.bgYellow('Request took ' + (timeDiff[0] + timeDiff[1] / 1000000000) + ' seconds')); }; class ReloadFixturesHook implements HookHandler { public initializeHook() { Before('@reloadFixtures', (scenario, callback) => { console.log(chalk.black.bgYellow('Reloading fixtures')); const timeStart = process.hrtime(); fixturesLoader .reloadFixtures(parameters.getReloadFixturesEndpoint()) .then((response) => { if (response.status === 200) { console.log(chalk.black.bgGreen('Fixtures reloaded')); } else { console.log(chalk.black.bgRed('There was a problem with fixtures reloading. The response is: '), response); } logRequestTime(timeStart); callback(); }) .catch((error) => { console.log(chalk.black.bgRed('An error occurred during fixtures reloading: '), error); logRequestTime(timeStart); callback(); }); }); } public getPriority() { return 2; } } export const reloadFixturesHook = new ReloadFixturesHook(); ================================================ FILE: src/web/cucumber/hooks/reload-user.hook.ts ================================================ import { After } from 'cucumber'; import { HookHandler } from './hook.interface'; class ReloadUserHook implements HookHandler { public initializeHook() { After('@reloadUsers', function(scenario, callback) { if (this.currentUser !== null) { this.userProvider.lockUser(this.currentUser.account, this.currentUser.type); } callback(); }); } public getPriority() { return 1; } } export const reloadUserHook = new ReloadUserHook(); ================================================ FILE: src/web/cucumber/hooks/take-screenshots.hook.ts ================================================ import { HookHandler } from './hook.interface'; import config from '../../../core/config.helper'; import { After } from 'cucumber'; const takeScreenshot = scenario => { return browser.takeScreenshot().then( base64png => { scenario.attach(new Buffer(base64png, 'base64'), 'image/png'); return Promise.resolve(); }, () => Promise.resolve() ); }; const clearCookiesAndLocalStorage = callback => { let cookiesFunc = () => Promise.resolve(true); if (config.clearCookiesAfterScenario) { cookiesFunc = () => protractor.browser.manage().deleteAllCookies(); } let localStorageFunc = () => Promise.resolve(true); if (config.clearLocalStorageAfterScenario) { localStorageFunc = () => protractor.browser.executeScript('window.localStorage.clear();'); } browser .wait( cookiesFunc() .then(localStorageFunc) .catch(() => false), config.waitForPageTimeout * 1000 ) .then(() => { protractor.browser.ignoreSynchronization = config.type === 'otherWeb'; callback(); }); }; class TakeScreenshotHook implements HookHandler { public initializeHook() { return After(function(scenario, callback) { if (scenario.result.status !== 'passed') { takeScreenshot(this).then(() => { clearCookiesAndLocalStorage(callback); }); } else { clearCookiesAndLocalStorage(callback); } }); } public getPriority() { return 1; } } export const takeScreenshotHook = new TakeScreenshotHook(); ================================================ FILE: src/web/cucumber/hooks.ts ================================================ import { hookHandlers } from './hooks/hooks'; hookHandlers.initializeHook(); ================================================ FILE: src/web/cucumber/wait-for-condition.helper.ts ================================================ import config from '../../core/config.helper'; const globalTimeout = parseInt(config.elementsVisibilityTimeout) * 1000; export const waitForCondition = (condition, timeout) => { return element => { if (element instanceof protractor.ElementArrayFinder) { return browser.wait(protractor.ExpectedConditions[condition](element.first()), timeout); } return browser.wait(protractor.ExpectedConditions[condition](element), timeout); }; }; export const waitForVisibilityOf = element => { return waitForCondition('visibilityOf', globalTimeout)(element); }; export const waitForInvisibilityOf = element => { return waitForCondition('invisibilityOf', globalTimeout)(element); }; ================================================ FILE: src/web/fixtures/fixtures-loader.helper.ts ================================================ import fetch from 'node-fetch'; const FixturesLoader = { reloadFixtures(endpoint) { return fetch(endpoint); }, }; export default FixturesLoader; ================================================ FILE: src/web/fs/download-checker.helper.ts ================================================ import * as fs from 'fs'; import * as path from 'path'; import config from '../../core/config.helper'; const DownloadChecker = { wasDownloaded(expectedFileName) { return browser.driver.wait(() => { return fs.existsSync(path.join(config.projectPath, config.downloads, expectedFileName)); }, config.downloadTimeout * 1000); }, }; export default DownloadChecker; ================================================ FILE: src/web/fs/file-manager.helper.ts ================================================ import * as fs from 'fs'; import * as xlsx from 'node-xlsx'; import * as path from 'path'; import config from '../../core/config.helper'; const FileManager = { wasDownloaded(expectedFileName) { return browser.driver.wait(() => { return fs.existsSync(path.join(config.projectPath, config.downloads, expectedFileName)); }, config.downloadTimeout * 1000); }, parseXLS(expectedFileName) { return xlsx.parse(path.join(config.projectPath, config.downloads, expectedFileName))[0].data; }, }; export default FileManager; ================================================ FILE: src/web/parallel/chunk-specs.helper.spec.ts ================================================ import { chunkSpecs } from './chunk-specs.helper'; const allSpecs = [ '/Users/user/Kakunin/functional-tests/features/content/operations_on_stored_variables.feature', '/Users/user/Kakunin/functional-tests/features/content/validate_tabular_data.feature', '/Users/user/Kakunin/functional-tests/features/content/wait_for_element_dissapear.feature', '/Users/user/Kakunin/functional-tests/features/drag-and-drop/operations_on_elements.feature', '/Users/user/Kakunin/functional-tests/features/forms/fill_and_check_form.feature', '/Users/user/Kakunin/functional-tests/features/matchers/match_current_date.feature', '/Users/user/Kakunin/functional-tests/features/navigation/navigate_to_given_page.feature', '/Users/user/Kakunin/functional-tests/features/pages/verify_displayed_page.feature', '/Users/user/Kakunin/functional-tests/features/wait-for-elements/wait_for_form.feature', '/Users/user/Kakunin/functional-tests/features/wait-for-elements/wait_for_table.feature', ]; describe('Chunk specs', () => { it('returns all specs as one list as the pattern was not specified', () => { expect(chunkSpecs({}, allSpecs, 1, 1)).toEqual([ ['/Users/user/Kakunin/functional-tests/features/content/operations_on_stored_variables.feature'], ['/Users/user/Kakunin/functional-tests/features/content/validate_tabular_data.feature'], ['/Users/user/Kakunin/functional-tests/features/content/wait_for_element_dissapear.feature'], ['/Users/user/Kakunin/functional-tests/features/drag-and-drop/operations_on_elements.feature'], ['/Users/user/Kakunin/functional-tests/features/forms/fill_and_check_form.feature'], ['/Users/user/Kakunin/functional-tests/features/matchers/match_current_date.feature'], ['/Users/user/Kakunin/functional-tests/features/navigation/navigate_to_given_page.feature'], ['/Users/user/Kakunin/functional-tests/features/pages/verify_displayed_page.feature'], ['/Users/user/Kakunin/functional-tests/features/wait-for-elements/wait_for_form.feature'], ['/Users/user/Kakunin/functional-tests/features/wait-for-elements/wait_for_table.feature'], ]); }); it('returns chucked specs for one pattern', () => { expect(chunkSpecs({ pattern: 'features/content' }, allSpecs, 1, 1)).toEqual([ [ '/Users/user/Kakunin/functional-tests/features/content/operations_on_stored_variables.feature', '/Users/user/Kakunin/functional-tests/features/content/validate_tabular_data.feature', '/Users/user/Kakunin/functional-tests/features/content/wait_for_element_dissapear.feature', ], ]); }); it('returns chucked specs by default because the pattern is boolean - true', () => { expect(chunkSpecs({ pattern: true }, allSpecs, 1, 1)).toEqual([ ['/Users/user/Kakunin/functional-tests/features/content/operations_on_stored_variables.feature'], ['/Users/user/Kakunin/functional-tests/features/content/validate_tabular_data.feature'], ['/Users/user/Kakunin/functional-tests/features/content/wait_for_element_dissapear.feature'], ['/Users/user/Kakunin/functional-tests/features/drag-and-drop/operations_on_elements.feature'], ['/Users/user/Kakunin/functional-tests/features/forms/fill_and_check_form.feature'], ['/Users/user/Kakunin/functional-tests/features/matchers/match_current_date.feature'], ['/Users/user/Kakunin/functional-tests/features/navigation/navigate_to_given_page.feature'], ['/Users/user/Kakunin/functional-tests/features/pages/verify_displayed_page.feature'], ['/Users/user/Kakunin/functional-tests/features/wait-for-elements/wait_for_form.feature'], ['/Users/user/Kakunin/functional-tests/features/wait-for-elements/wait_for_table.feature'], ]); }); it('returns chucked specs by default because the pattern is boolean - false', () => { expect(chunkSpecs({ pattern: false }, allSpecs, 1, 1)).toEqual([ ['/Users/user/Kakunin/functional-tests/features/content/operations_on_stored_variables.feature'], ['/Users/user/Kakunin/functional-tests/features/content/validate_tabular_data.feature'], ['/Users/user/Kakunin/functional-tests/features/content/wait_for_element_dissapear.feature'], ['/Users/user/Kakunin/functional-tests/features/drag-and-drop/operations_on_elements.feature'], ['/Users/user/Kakunin/functional-tests/features/forms/fill_and_check_form.feature'], ['/Users/user/Kakunin/functional-tests/features/matchers/match_current_date.feature'], ['/Users/user/Kakunin/functional-tests/features/navigation/navigate_to_given_page.feature'], ['/Users/user/Kakunin/functional-tests/features/pages/verify_displayed_page.feature'], ['/Users/user/Kakunin/functional-tests/features/wait-for-elements/wait_for_form.feature'], ['/Users/user/Kakunin/functional-tests/features/wait-for-elements/wait_for_table.feature'], ]); }); it('returns chunked specs by default as the pattern was not specified', () => { expect(chunkSpecs({}, allSpecs, 2, 2)).toEqual([ [ '/Users/user/Kakunin/functional-tests/features/content/operations_on_stored_variables.feature', '/Users/user/Kakunin/functional-tests/features/content/validate_tabular_data.feature', ], [ '/Users/user/Kakunin/functional-tests/features/content/wait_for_element_dissapear.feature', '/Users/user/Kakunin/functional-tests/features/drag-and-drop/operations_on_elements.feature', ], [ '/Users/user/Kakunin/functional-tests/features/forms/fill_and_check_form.feature', '/Users/user/Kakunin/functional-tests/features/matchers/match_current_date.feature', ], [ '/Users/user/Kakunin/functional-tests/features/navigation/navigate_to_given_page.feature', '/Users/user/Kakunin/functional-tests/features/pages/verify_displayed_page.feature', ], [ '/Users/user/Kakunin/functional-tests/features/wait-for-elements/wait_for_form.feature', '/Users/user/Kakunin/functional-tests/features/wait-for-elements/wait_for_table.feature', ], ]); }); it('returns chunked specs by two pattern', () => { expect(chunkSpecs({ pattern: 'features/content,features/forms' }, allSpecs, 2, 2)).toEqual( expect.arrayContaining([ [ '/Users/user/Kakunin/functional-tests/features/content/operations_on_stored_variables.feature', '/Users/user/Kakunin/functional-tests/features/content/validate_tabular_data.feature', '/Users/user/Kakunin/functional-tests/features/content/wait_for_element_dissapear.feature', ], ['/Users/user/Kakunin/functional-tests/features/forms/fill_and_check_form.feature'], ]) ); }); it('returns error as the number of instances is higher than given patterns', () => { expect(() => { chunkSpecs({ pattern: 'features/content' }, allSpecs, 4, 3); }).toThrow('Number of the specified patterns is different than number of instances!'); }); it('returns error as the number of instances is lower than given patterns', () => { expect(() => { chunkSpecs({ pattern: 'features/forms,features/navigation,features/wait-for-elements' }, allSpecs, 2, 2); }).toThrow('Number of the specified patterns is different than number of instances!'); }); }); ================================================ FILE: src/web/parallel/chunk-specs.helper.ts ================================================ import * as _ from 'lodash'; export const chunkSpecs = (commandArgs, allSpecs, expectedArrayLength, numberOfInstances) => { if (commandArgs.pattern !== undefined && typeof commandArgs.pattern !== 'boolean') { const patterns = commandArgs.pattern.split(','); const chunkedSpecs = []; if (patterns.length !== numberOfInstances) { throw new Error('Number of the specified patterns is different than number of instances!'); } for (const pattern of patterns) { chunkedSpecs.push(allSpecs.filter(spec => spec.match(new RegExp(pattern)))); } return chunkedSpecs; } return _.chunk(allSpecs, expectedArrayLength); }; ================================================ FILE: src/web/parallel/prepare-browser-instance-specs.helper.ts ================================================ import * as _ from 'lodash'; export const prepareBrowserInstance = (browserConfig, specs) => { const instance = _.cloneDeep(browserConfig); instance.specs = specs; return instance; }; ================================================ FILE: src/web/parameters.ts ================================================ const Parameters = { getReloadFixturesEndpoint() { const config = this.getConfig(); return config.fixturesReloadHost; }, getConfig() { if (typeof process.env.FIXTURES_RELOAD_HOST === 'undefined') { throw new Error('Missing fixtures reload url. Use export FIXTURES_RELOAD_HOST=valid-host for setup.'); } return { fixturesReloadHost: process.env.FIXTURES_RELOAD_HOST, }; }, }; export default Parameters; ================================================ FILE: src/web/performance/JSON-performance-report-parser.helper.spec.ts ================================================ import JSONPerformanceReportParser from './JSON-performance-report-parser.helper'; const parser = new JSONPerformanceReportParser('src/tests/reports/performance'); describe('JSON performance report parser', () => { it('returns found objects with TTFB and URL values', () => { const fileName = 'performance-report.har'; expect(parser.parse(fileName)).toContainEqual({ ttfb: 0, url: 'http://localhost:8080/' }); }); it('returns error message - file contains incorrect data', () => { const fileName = 'incorrect-performance-report.har'; expect(() => parser.parse(fileName)).toThrow(`${fileName} contains incorrect data!`); }); }); ================================================ FILE: src/web/performance/JSON-performance-report-parser.helper.ts ================================================ import * as fs from 'fs'; const getReport = (fileName, path) => { return JSON.parse(fs.readFileSync(`${path}/${fileName}`, 'utf8')); }; class JSONPerformanceReportParser { private readonly path: string; constructor(path = 'reports/performance') { this.path = path; } public parse(fileName) { const reportFile = getReport(fileName, this.path); const requests = reportFile.log.entries.map(item => ({ ttfb: item.timings.wait, url: item.request.url, })); if (requests.length > 0) { return requests; } throw Error(`${fileName} contains incorrect data!`); } } export default JSONPerformanceReportParser; ================================================ FILE: src/web/performance/time-to-first-byte-analyser.helper.spec.ts ================================================ import { create as createAnalyser } from './time-to-first-byte-analyser.helper'; const myFakeParser: any = { parse: () => { return [ { ttfb: 0, url: 'http://localhost:8080/' }, { ttfb: 2, url: 'http://localhost:8080/assets/kittens.jpg' }, { ttfb: 1, url: 'http://localhost:8080/favicon.ico' }, { ttfb: 2, url: 'http://localhost:8080/tabular-data' }, { ttfb: 1, url: 'http://localhost:8080/assets/kittens.jpg' }, ]; }, }; const analyser = createAnalyser(myFakeParser); describe('Time to first byte analyser', () => { it('returns found slow request', () => { const maxTiming = 1; // value to be compared with TTFB expect(analyser.checkTiming(myFakeParser, maxTiming)).toEqual([ { ttfb: 2, url: 'http://localhost:8080/assets/kittens.jpg' }, { ttfb: 2, url: 'http://localhost:8080/tabular-data' }, ]); }); it('returns empty array - no slow requests', () => { const maxTiming = 1000; // bigger than the highest request's TTFB value expect(analyser.checkTiming(myFakeParser, maxTiming)).toEqual([]); }); }); ================================================ FILE: src/web/performance/time-to-first-byte-analyser.helper.ts ================================================ import JSONPerformanceReportParser from './JSON-performance-report-parser.helper'; class TimeToFirstByteAnalyser { private reader: any; constructor(jsonPerformanceReportParser) { this.reader = jsonPerformanceReportParser; } public checkTiming(fileName, maxTiming) { const parsedReport = this.reader.parse(fileName); return parsedReport.filter(report => report.ttfb > maxTiming); } } export const create = (reportParser = new JSONPerformanceReportParser()) => new TimeToFirstByteAnalyser(reportParser); ================================================ FILE: src/web/url-parser.helper.spec.ts ================================================ import { isRelativePage, waitForUrlChangeTo } from './url-parser.helper'; const exampleBaseUrl = 'https://example-base-url.com'; describe('URL parser', () => { it('returns false if a path in absolute URL is incorrect - without slash', () => { expect( waitForUrlChangeTo('http://localhost:8080/incorrect-data', 'http://localhost:8080/tabular-data').bind( null, exampleBaseUrl )() ).toEqual(false); }); it('returns false if a path in absolute URL is incorrect - with slash', () => { expect( waitForUrlChangeTo('http://localhost:8080/incorrect-data/', 'http://localhost:8080/tabular-data').bind( null, exampleBaseUrl )() ).toEqual(false); }); it('returns false if a host in absolute URL is incorrect - without slash', () => { expect( waitForUrlChangeTo('http://google/incorrect-data', 'http://localhost:8080/tabular-data').bind( null, exampleBaseUrl )() ).toEqual(false); }); it('returns false if a host path in absolute URL is incorrect - without slash', () => { expect( waitForUrlChangeTo('http://google/tabular-data', 'http://localhost:8080/tabular-data').bind( null, exampleBaseUrl )() ).toEqual(false); }); it('returns false if a host in absolute URL is incorrect - with slash', () => { expect( waitForUrlChangeTo('http://google/incorrect-data/', 'http://localhost:8080/tabular-data').bind( null, exampleBaseUrl )() ).toEqual(false); }); it('returns false if a path in relative URL is incorrect - without slash', () => { expect(waitForUrlChangeTo('/incorrect-data', 'http://website/tabular-data').bind(null, exampleBaseUrl)()).toEqual( false ); }); it('returns false if a path in relative URL is incorrect - with slash', () => { expect( waitForUrlChangeTo('/incorrect-data/', 'http://website.com/tabular-data').bind(null, 'http://incorrect.com')() ).toEqual(false); }); it('returns empty object if a page URL and the current URL are the same - absolute URL without slash', () => { expect( waitForUrlChangeTo('http://localhost:8080/tabular-data', 'http://localhost:8080/tabular-data').bind( null, exampleBaseUrl )() ).toEqual({}); }); it('returns empty object if a page URL and the current URL are the same - absolute URL with slash', () => { expect( waitForUrlChangeTo('http://localhost:8080/tabular-data/', 'http://localhost:8080/tabular-data').bind( null, exampleBaseUrl )() ).toEqual({}); }); it('returns empty object if a page URL and the current URL paths are the same - relative URL without slash', () => { expect( waitForUrlChangeTo('/tabular-data', 'http://localhost:8080/tabular-data').bind(null, 'http://localhost:8080')() ).toEqual({}); }); it('returns empty object if a page URL and the current URL paths are the same - relative URL with slash', () => { expect( waitForUrlChangeTo('/tabular-data/', 'http://localhost:8080/tabular-data').bind(null, 'http://localhost:8080')() ).toEqual({}); }); it('returns false if a page URL and the current URL paths are the same but hosts are different - relative URL without slash', () => { expect( waitForUrlChangeTo('/tabular-data', 'http://localhost:8080/tabular-data').bind(null, 'http://google.pl')() ).toEqual(false); }); it('returns false if a page URL and the current URL paths are the same but hosts are different - relative URL with slash', () => { expect( waitForUrlChangeTo('/tabular-data/', 'http://localhost:8080/tabular-data').bind(null, 'http://google.pl')() ).toEqual(false); }); it('returns false if a baseUrl is different than the current one - page URL with slash', () => { expect(waitForUrlChangeTo('/', 'https://google.pl/new').bind(null, 'https://google.pl')()).toEqual(false); }); it('returns false if a baseUrl path is different than the current one - empty URL in page', () => { expect(waitForUrlChangeTo('', 'https://google.pl/new').bind(null, 'https://google.pl')()).toEqual(false); }); it('returns empty object if a baseUrl and the current one are the same - empty URL in page', () => { expect(waitForUrlChangeTo('', 'http://localhost:8080').bind(null, 'http://localhost:8080')()).toEqual({}); }); it('returns false if a baseUrl is different than the current one - empty URL in page', () => { expect(waitForUrlChangeTo('/', 'https://google.pl').bind(null, 'https://google.com')()).toEqual(false); }); it('returns object with properties defined in a page URL - one wildcard', () => { expect( waitForUrlChangeTo('https://google.com/:name', 'https://google.com/janek').bind(null, exampleBaseUrl)() ).toEqual({ name: 'janek' }); }); it('returns object with properties defined in a page URL - one wildcard inside a long URL', () => { expect( waitForUrlChangeTo( 'https://google.com/account/:username/settings/display', 'https://google.com/account/janek/settings/display' ).bind(null, exampleBaseUrl)() ).toEqual({ username: 'janek' }); }); it('returns false if a host is incorrect - one wildcard in relative path', () => { expect( waitForUrlChangeTo('/account/settings/:userType', 'https://incorrect-host/account/settings/admin').bind( null, 'https://google.com' )() ).toEqual(false); }); it('returns false if a URL is incorrect - one wildcard in relative path', () => { expect( waitForUrlChangeTo('/account/settings/:userType/something', 'https://incorrect-host/account/settings/admin').bind( null, 'https://google.com' )() ).toEqual(false); }); it('returns false if a URL is incorrect - one wildcard in absolute path', () => { expect( waitForUrlChangeTo( 'https://incorrect-host/account/settings/:userType/something', 'https://incorrect-host/account/settings/admin' ).bind(null, exampleBaseUrl)() ).toEqual(false); }); it('returns object with properties defined in a page URL - one wildcard in relative path', () => { expect( waitForUrlChangeTo('/account/settings/:userType', 'https://google.com/account/settings/user').bind( null, 'https://google.com' )() ).toEqual({ userType: 'user' }); }); it('returns false if the URL is absolute - http without path', () => { expect(isRelativePage('http://google.com')).toEqual(false); }); it('returns false if the URL is absolute - https without path', () => { expect(isRelativePage('https://google.com')).toEqual(false); }); it('returns false if the URL is absolute - http with path', () => { expect(isRelativePage('http://google.com/with-path')).toEqual(false); }); it('returns false if the URL is absolute - https with parameter', () => { expect(isRelativePage('https://google.com/:param/with-path')).toEqual(false); }); it('returns true if the URL is relative', () => { expect(isRelativePage('/with-path')).toEqual(true); }); it('returns true if the URL is relative - with parameter', () => { expect(isRelativePage('/:path-with-parameter')).toEqual(true); }); it('returns true if the URL contains only slash', () => { expect(isRelativePage('/')).toEqual(true); }); it('returns true if the URL is empty', () => { expect(isRelativePage('')).toEqual(true); }); }); ================================================ FILE: src/web/url-parser.helper.ts ================================================ import * as Url from 'url'; const extractDomain = url => Url.parse(url).host; const extractUrl = url => Url.parse(url).pathname; const normalizeUrl = url => { if (url.length === 0) { return extractUrl('/'); } if (url[url.length - 1] === '/' && url.length > 1) { return extractUrl(url.substr(0, url.length - 1)); } return extractUrl(url); }; const compareUrls = (urlSplit, baseUrlSplit) => { const resultParameters = {}; for (const i in urlSplit) { if (urlSplit.hasOwnProperty(i) && baseUrlSplit.hasOwnProperty(i)) { const template = baseUrlSplit[i]; const actual = urlSplit[i]; if (template.startsWith(':')) { resultParameters[template.substr(1)] = actual; } else if (template !== actual) { return false; } } } return resultParameters; }; export const isRelativePage = url => { return url === '' || url[0] === '/'; }; export const waitForUrlChangeTo = (newUrl, currentUrl) => { return baseUrl => { const pageUrl = Url.resolve(baseUrl, newUrl); const pageDomain = extractDomain(pageUrl); const currentUrlDomain = extractDomain(currentUrl); if (pageDomain !== currentUrlDomain) { return false; } const urlSplit = normalizeUrl(currentUrl).split('/'); const pageUrlSplit = normalizeUrl(pageUrl).split('/'); if (urlSplit.length !== pageUrlSplit.length) { return false; } return compareUrls(urlSplit, pageUrlSplit); }; }; ================================================ FILE: src/web/user-provider.helper.ts ================================================ import config from '../core/config.helper'; const accounts = config.accounts; const userProvider = { getUser(userType) { const user = accounts[userType]; if (user.accounts.length > 1) { const usedAccounts = user.accounts.filter(account => account.used); if (usedAccounts.length === user.accounts.length) { user.accounts.map(account => ({ ...account, used: false, })); } return user.accounts.find(account => !account.used); } return user.accounts[0]; }, lockUser(user, userType) { if (accounts[userType].accounts.length > 1) { accounts[userType].accounts.forEach((account, index) => { if (account.email === user.email) { accounts[userType].accounts[index].used = true; } }); } }, }; export default userProvider; ================================================ FILE: src/web/variable-store.helper.spec.ts ================================================ import VariableStore from './variable-store.helper'; describe('Variable store', () => { it('returns empty list of stored variables after clear', () => { VariableStore.variables = [ { name: 'newStoredVariable', value: 'example-variable-content', }, { name: 'newStoredVariableTwo', value: 'example-variable-content-two', }, ]; VariableStore.clearVariables(); expect(VariableStore.variables).toEqual([]); }); it('returns array which contains newly stored variable', () => { VariableStore.variables = []; VariableStore.storeVariable('newStoredVariable', 'example-variable-content'); expect(VariableStore.variables).toEqual( expect.arrayContaining([ { name: 'newStoredVariable', value: 'example-variable-content', }, ]) ); }); it('throws error that the variable already exists', () => { VariableStore.variables = [ { name: 'newStoredVariable', value: 'example-variable-content', }, ]; expect(() => { VariableStore.storeVariable('newStoredVariable', 'example-variable-content'); }).toThrow('Variable newStoredVariable is stored already'); }); it('returns updated array of stored variables', () => { VariableStore.variables = [ { name: 'newStoredVariable', value: 'example-variable-content', }, ]; VariableStore.updateVariable('newStoredVariable', 'updated variable'); expect(VariableStore.variables).toEqual( expect.arrayContaining([ { name: 'newStoredVariable', value: 'updated variable', }, ]) ); }); it('throws error that the variable cannot be updated because does not exist', () => { VariableStore.variables = []; expect(() => { VariableStore.updateVariable('newStoredVariable', 'updated variable'); }).toThrow('Variable newStoredVariable does not exist'); }); it('returns value from the stored variable', () => { VariableStore.variables = [ { name: 'newStoredVariable', value: 'example-variable-content', }, ]; expect(VariableStore.getVariableValue('newStoredVariable')).toEqual('example-variable-content'); }); it('throws error that the value cannot be displayed because does not exist', () => { VariableStore.variables = [ { name: 'newStoredVariable', value: 'example-variable-content', }, ]; expect(() => { VariableStore.getVariableValue('unavailableVariable'); }).toThrow('Variable unavailableVariable was not stored'); }); it('returns false when variable was not stored', () => { VariableStore.variables = []; expect(VariableStore.isStored('newStoredVariable')).toEqual(false); }); it('returns true when variable was stored', () => { VariableStore.variables = [ { name: 'newStoredVariable', value: 'example-variable-content', }, ]; expect(VariableStore.isStored('newStoredVariable')).toEqual(true); }); it('returns value with replaced variable with text matcher prefix', () => { VariableStore.variables = [ { name: 'newStoredVariable', value: 'example-variable-content', }, ]; expect(VariableStore.replaceTextVariables('t:v:newStoredVariable')).toEqual('t:example-variable-content'); }); it('returns value with replaced variable with only variableStore prefix', () => { VariableStore.variables = [ { name: 'newStoredVariable', value: 'example-variable-content', }, ]; expect(VariableStore.replaceTextVariables('v:newStoredVariable')).toEqual('example-variable-content'); }); it('returns the same value if there is no variable prefix - text matcher', () => { VariableStore.variables = []; expect(VariableStore.replaceTextVariables('t:some random text')).toEqual('t:some random text'); }); it('returns the same value if there is no variable prefix - regex matcher', () => { VariableStore.variables = []; expect(VariableStore.replaceTextVariables('r:notEmpty')).toEqual('r:notEmpty'); }); it('returns full text if there is no stored variable', () => { VariableStore.variables = []; expect(VariableStore.replaceTextVariables('t:v:unavailableVariable')).toEqual('t:v:unavailableVariable'); }); }); ================================================ FILE: src/web/variable-store.helper.ts ================================================ export class VariableStore { constructor(public variables: any[] = []) {} public storeVariable(name: string, value: any): void { const foundVariable = this.variables.find(variable => variable.name === name); if (typeof foundVariable !== 'undefined') { throw new Error(`Variable ${name} is stored already`); } this.variables.push({ name, value }); } public updateVariable(name: string, value: any): void { const foundVariable = this.variables.find(variable => variable.name === name); if (typeof foundVariable === 'undefined') { throw new Error(`Variable ${name} does not exist.`); } this.variables.push({ name, value }); } public getVariableValue(name: string): any { const foundVariable = this.variables.find(variable => variable.name === name); if (typeof foundVariable === 'undefined') { throw new Error(`Variable ${name} was not stored`); } return foundVariable.value; } public isStored(name: string): boolean { const foundVariable = this.variables.find(variable => variable.name === name); return typeof foundVariable !== 'undefined'; } public clearVariables(): void { this.variables = []; } public replaceTextVariables(text: string): any { let newText = text; const variableNames = this.variables.map(variable => variable.name); for (const variableNameIndex in variableNames) { if (variableNames.hasOwnProperty(variableNameIndex)) { const variableName = variableNames[variableNameIndex]; if (newText.indexOf(variableName) > -1) { newText = text.replace(`v:${variableName}`, this.getVariableValue(variableName)); break; } } } return newText; } } export default new VariableStore(); ================================================ FILE: templates/example.feature ================================================ Feature: Example feature file Scenario: Example scenario When I visit the "page" page And I generate random "name" as "myName" Then my matcher "e:name" matches "v:myName" And my matcher "e:name" matches "Bob" ================================================ FILE: templates/generator.js ================================================ const { generators } = require('kakunin'); class Generator { isSatisfiedBy(name) { return name === 'name'; } generate() { const names = [ 'Bob', 'John', 'Paul' ]; return Promise.resolve(names[Math.floor(Math.random() * names.length)]); } } generators.addGenerator(new Generator()); ================================================ FILE: templates/hook.js ================================================ const { hookHandlers, Before } = require('kakunin'); class TestHook { initializeHook() { Before(() => { console.log('Standard hook'); }); } getPriority() { return 990; } } hookHandlers.addHook(new TestHook()); ================================================ FILE: templates/login.js ================================================ const { Given } = require('kakunin'); Given(/^I am logged in as a "([^"]*)$/, async function(user) { this.currentUser = { account: this.userProvider.getUser(user), type: user, }; const mainPage = browser.page.main; const loginPage = browser.page.login; await mainPage.visit(); await mainPage.waitForVisibilityOf('login'); await mainPage.click('login'); await loginPage.login(this.currentUser.account.email, this.currentUser.account.password); await mainPage.waitForInvisibilityOf('login'); this.currentPage = mainPage; }); ================================================ FILE: templates/matcher.js ================================================ const { regexBuilder, matchers } = require('kakunin'); const ExampleMatcher = { isSatisfiedBy: function (prefix, name) { return prefix === 'e'; }, match: function (element, regexName) { const regex = regexBuilder.buildRegex(`r:${regexName}`); return regex.test(element); } }; matchers.addMatcher(ExampleMatcher); ================================================ FILE: templates/page.js ================================================ const { BasePage } = require('kakunin'); class ExamplePage extends BasePage { constructor() { super(); this.url = '/'; } } module.exports = ExamplePage; ================================================ FILE: templates/regex.js ================================================ module.exports = { name: '(Bob|John|Paul)' }; ================================================ FILE: templates/steps.js ================================================ const { matchers, variableStore, Then } = require('kakunin'); Then(/^my matcher "([^"]*)" matches "([^"]*)"$/, function (matcher, text) { return expect(matchers.match(variableStore.replaceTextVariables(text), matcher)).toBe(true); }); ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "esnext", "module": "commonjs", "allowSyntheticDefaultImports": false, "outDir": "dist", "noUnusedLocals": true, "noImplicitAny": false, "removeComments": false }, "exclude": [ "node_modules", "**/*.spec.ts", "dist/**" ], "include": [ "./src/kakunin.d.ts", "./src/**/*.ts" ] } ================================================ FILE: tsconfig.test.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "target": "es6" } } ================================================ FILE: tslint.json ================================================ { "defaultSeverity": "error", "extends": [ "tslint:recommended", "tslint-config-prettier", "tslint-sonarts" ], "linterOptions": { "exclude": [ "**/*.spec.ts", "**/*.d.ts" ] }, "jsRules": {}, "rules": { "no-extra-semicolon": false, "no-empty-interface": false, "object-literal-sort-keys": false, "radix": false, "interface-name": [true, "never-prefix"], "ordered-imports": false, "no-duplicate-string": false, "variable-name": [true, "ban-keywords", "allow-leading-underscore", "allow-pascal-case"], "interface-over-type-literal": false, "no-angle-bracket-type-assertion": false, "max-union-size": false, "no-identical-functions": false, "no-console": false, "no-var-requires": false, "cognitive-complexity": false, "no-big-function": false }, "rulesDirectory": [] } ================================================ FILE: website/README.md ================================================

kakunin.png

travis.png Current travis build: build statusnpm.png Current npm version: npm version

Getting startedContent mangmentVersifyingBuilding applicationPublishingDocs

pageObjectFeature.gif

# Getting started with Docusaurus 1. Make sure all the dependencies for the website are installed: ```sh # Install dependencies $ yarn ``` 2. Run documentation in local environment. Change directory into `website` folder: ```sh # Start the site $ yarn start ``` # Directory Structure Project file structure look like this: ``` Kakunin/ docs/ configuration.md extending.md ... website/ core/ node_modules/ pages/ static/ css/ img/ package.json sidebars.json siteConfig.js ``` # Content management ## Adding new content to documentation 1. In `docs/` folder add new markdown file `docs/new-markdown-file.md` 2. In created file on top of the document add: ```markdown --- id: new-markdown-file title: New Markdown File --- New Content... ``` Title should be in uppercase - it is displayed in browser tab titile. Id should be in lowercase - it is used in `website/sidebars.json`. 3. In `website/sidebar.json` add ID of the created markdowns file, into desired array. ```javascript { "docs": { "Kakunin": [ "quickstart", "index", "configuration", "how-it-works", "steps", "matchers", "transformers", "cross-browser", "parallel-testing", "performance-testing", "extending", "new-markdown-file" ] } } ``` 4. Test your site locally to confirm changes. ## Editing an existing docs page 1. To edit file open in `docs` folder open file which should be edited `docs/new-markdown-file.md` 2. Change content of file 3. Test your site locally to confirm changes. # Versifying Docusaurus support versifying, right now we started to using this feature from `2.4.0` Kakunin version. 1. To add new version of documentation open `website/` folder 2. Run `yarn run version [version]` example `yarn run version 2.4.0-1` 3. After that in `website/` will be created 2 folders `versioned_docs/` & `versioned_sidebars/` 4. In `versioned_docs/` will be created folder `version-2.4.0-1` 5. In `versioned_sidebars/` there will be created json file with changed sidebar `version-2.4.0-sidebars.json`. **In those folders there are only files which was added or changed!** 6. After that our latest version will be `2.4.0-1` # Building application 1. To build application in `website/` folder run `yarn build` 2. After that in `website/build/Kakunin` will be created application for latest version `2.4.0-1` # Publishing To publish changes run: `GIT_USER= CURRENT_BRANCH=next USE_SSH=true npm run publish-gh-pages` where is your github user. # Full Documentation Full documentation can be found on the [website](https://docusaurus.io/). ================================================ FILE: website/build/Kakunin/css/main.css ================================================ a,abbr,acronym,address,applet,article,aside,audio,b,big,blockquote,body,canvas,caption,center,cite,code,dd,del,details,dfn,div,dl,dt,em,embed,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,html,i,iframe,img,ins,kbd,label,legend,li,mark,menu,nav,object,ol,output,p,pre,q,ruby,s,samp,section,small,span,strike,strong,sub,summary,sup,table,tbody,td,tfoot,th,thead,time,tr,tt,u,ul,var,video{border:0;font:inherit;font-size:100%;margin:0;padding:0;vertical-align:baseline}body{color:#24292e;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;font-size:16px;line-height:1.5;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;word-wrap:break-word}*{box-sizing:border-box}b,strong{font-weight:600}em,i{font-style:italic}[type=checkbox]{box-sizing:border-box;padding:0}a,a:hover{color:#2f1666;text-decoration:none}a:active,a:hover{outline-width:0}a:not([href]){color:inherit;text-decoration:none}p{margin-bottom:1em;margin-top:0}h1,h2,h3,h4,h5,h6{color:inherit;font-weight:600;line-height:1.25;margin-bottom:16px;margin-top:1.5em}h1{font-size:32px}h2{font-size:24px}h3{font-size:20px}h4{font-size:16px}h5{font-size:14px}h6{font-size:13.6px}ol,ul{margin-bottom:1em;margin-top:0;padding-left:2em}ol ol,ul ol{list-style-type:lower-roman}ol ol,ol ul,ul ol,ul ul{margin-bottom:0;margin-top:0}ol ol ol,ol ul ol,ul ol ol,ul ul ol{list-style-type:lower-alpha}li{word-wrap:break-all}li>p{margin-top:1em}li+li{margin-top:.25em}img{border-style:none;box-sizing:content-box;max-width:100%}img[align=right]{padding-left:1.25em}img[align=left]{padding-right:1.25em}table{border-collapse:collapse;border-spacing:0;display:block;margin-bottom:16px;margin-top:0;overflow:auto;width:100%}table tr{background-color:transparent;border-top:1px solid #dfe2e5}table tr:nth-child(2n){background-color:#f6f8fa}table td,table th{border:1px solid #dfe2e5;padding:6px 13px}table th{background-color:inherit;font-weight:600}table td,table th{color:inherit}blockquote{color:#6a737d;font-size:16px;margin:0 0 16px;padding:0 1em}blockquote>:first-child{margin-top:0}blockquote>:last-child{margin-bottom:0}code{background-color:rgba(27,31,35,.05);border-radius:3px;color:inherit;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:85%;margin:0;padding:3.2px 6.4px}pre{margin-bottom:16px}pre code{background-color:transparent;border:0;display:inline;font-size:85%;line-height:inherit;margin:0;max-width:auto;overflow:visible;padding:0;white-space:pre;word-break:normal;word-wrap:normal}kbd{background-color:#fafbfc;border:1px solid #d1d5da;border-bottom-color:#c6cbd1;border-radius:3px;box-shadow:inset 0 -1px 0 #c6cbd1;color:#444d56;display:inline-block;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:68.75%;line-height:10px;padding:3px 5px;vertical-align:middle}hr{border:1px solid #dfe2e5;box-sizing:content-box;margin:1.5em 0;overflow:hidden;padding:0}hr:after,hr:before{content:"";display:table}hr:after{clear:both}body{background-color:#fff;min-height:100vh;text-rendering:optimizeLegibility}@media only screen and (min-width:736px){body{display:flex;flex-direction:column}}article:after,article:before{content:"";display:table}article:after{clear:both}article>:first-child{margin-top:0}article>:last-child{margin-bottom:0}article iframe,article p img{display:block;margin-left:auto;margin-right:auto;max-width:100%}.anchor{display:block;position:relative;top:-80px}.hash-link{line-height:1;margin-left:-20px;opacity:0;padding-right:4px;transition:opacity .3s}.hash-link:hover{opacity:1!important;transition:none}.hash-link .hash-link-icon{vertical-align:middle}.button{border:1px solid #2f1666;border-radius:3px;color:#2f1666;display:inline-block;font-size:14px;font-weight:400;line-height:1.2em;padding:10px;text-decoration:none!important;text-transform:uppercase;transition:background .3s,color .3s}.button:hover{background:#2f1666;color:#fff}h1:hover .hash-link,h2:hover .hash-link,h3:hover .hash-link,h4:hover .hash-link{opacity:.5;transition:none}blockquote{background-color:rgba(255,229,100,.3);border-left:8px solid #ffe564;padding:15px 30px 15px 15px}.wrapper{margin:0 auto;max-width:1100px;padding:0 20px}.wrapper blockquote>p:first-child{padding-top:0}.center{display:block}.center,.homeContainer{text-align:center}.homeContainer .homeWrapper{padding:2em 10px}.homeContainer .homeWrapper .wrapper{margin:0 auto;max-width:900px;padding:0 20px}.homeContainer .homeWrapper .projectLogo img{height:100px;margin-bottom:0}.homeContainer .homeWrapper #project_title{font-size:300%;letter-spacing:-.08em;line-height:1em;margin-bottom:80px}.homeContainer .homeWrapper #project_tagline{font-size:200%;letter-spacing:-.04em;line-height:1em}.projectLogo{display:none;pointer-events:none}.projectLogo img{height:100px;margin-bottom:0}.projectIntro{margin:40px 0}.projectTitle{color:#2f1666;font-size:250%;line-height:1em}.projectTitle>small{display:block;font-weight:400;font-size:50%;line-height:1em;margin:.7em 0 1.3em}@media only screen and (min-width:480px){.projectTitle{font-size:300%;margin:.3em 0}.projectLogo img{height:200px;margin-bottom:10px}.homeContainer .homeWrapper{padding-left:10px;padding-right:10px}}@media only screen and (min-width:736px){.homeContainer .homeWrapper{position:relative}.homeContainer .homeWrapper #inner{max-width:600px;padding-right:40px}}@media only screen and (min-width:1200px){.homeContainer .homeWrapper #inner{max-width:750px}.homeContainer .homeWrapper .projectLogo{align-items:center;bottom:0;display:flex;justify-content:flex-end;left:0;padding:2em 100px 4em;position:absolute;right:0;top:0}.homeContainer .homeWrapper .projectLogo img{height:100%;max-height:250px}}@media only screen and (min-width:1500px){.homeContainer .homeWrapper #inner{max-width:1100px;padding-bottom:40px;padding-top:40px}.wrapper{max-width:1400px}}.mainContainer{flex:1 1 0%;max-width:100%;padding:40px 0}.mainContainer .wrapper{text-align:left}.mainContainer .wrapper .allShareBlock{padding:10px 0}.mainContainer .wrapper .allShareBlock .pluginBlock{margin:12px 0;padding:0}.mainContainer .wrapper .post{position:relative}.mainContainer .wrapper .post.basicPost{margin-top:30px}.mainContainer .wrapper .post .postHeader{margin-bottom:16px}.mainContainer .wrapper .post .postHeaderTitle{margin-top:0;padding:0}.docsContainer .wrapper .post .postHeader:before,.docsContainer .wrapper .post .postHeaderTitle:before{content:"";display:block;height:90px;margin-top:-90px;visibility:hidden;pointer-events:none}.mainContainer .wrapper .post .postSocialPlugins{padding-top:1em}.mainContainer .wrapper .post .docPagination{background:#2f1666;bottom:0;left:0;position:absolute;right:0}.mainContainer .wrapper .post .docPagination .pager{display:inline-block;width:50%}.mainContainer .wrapper .post .docPagination .pagingNext{float:right;text-align:right}.mainContainer .wrapper .post .docPagination a{border:none;color:#fff;display:block;padding:4px 12px}.mainContainer .wrapper .post .docPagination a:hover{background-color:#f9f9f9;color:#393939}.mainContainer .wrapper .post .docPagination a .pagerLabel{display:inline}.mainContainer .wrapper .post .docPagination a .pagerTitle{display:none}@media only screen and (min-width:480px){.mainContainer .wrapper .post .docPagination a .pagerLabel{display:none}.mainContainer .wrapper .post .docPagination a .pagerTitle{display:inline}}@media only screen and (min-width:1024px){.mainContainer .wrapper .post{display:block}.mainContainer .wrapper .posts .post{width:100%}}@media only screen and (max-width:1023px){.docsContainer .wrapper .post .postHeader:before,.docsContainer .wrapper .post .postHeaderTitle:before{content:"";display:block;height:200px;margin-top:-200px;visibility:hidden;pointer-events:none}}.fixedHeaderContainer{background:#2f1666;color:#fff;min-height:50px;padding:8px 0;position:fixed;width:100%;z-index:9999;transform:translateZ(0)}@media only screen and (min-width:1024px){.fixedHeaderContainer{flex-shrink:0}}.fixedHeaderContainer a{align-items:center;border:0;color:#fff;display:flex;flex-flow:row nowrap;height:34px;z-index:10000}.fixedHeaderContainer header{display:flex;flex-flow:row nowrap;position:relative;text-align:left}.fixedHeaderContainer header img{height:100%;margin-right:10px}.fixedHeaderContainer header .headerTitle{font-size:1.25em;margin:0}.fixedHeaderContainer header .headerTitleWithLogo{display:block;font-size:1.25em;line-height:18px;margin:0;position:relative;z-index:9999}.fixedHeaderContainer header h3{color:#fff;font-size:16px;margin:0 0 0 10px;text-decoration:underline}@media (max-width:480px){.headerTitle{font-size:17px}.headerTitleWithLogo{display:none!important}}.promoSection{display:flex;flex-flow:column wrap;font-size:125%;line-height:1.6em;position:relative;z-index:99}.promoSection .promoRow{padding:10px 0}.promoSection .promoRow .pluginWrapper{display:block}.promoSection .promoRow .pluginWrapper.ghStarWrapper,.promoSection .promoRow .pluginWrapper.ghWatchWrapper{height:28px}.promoSection .promoRow .pluginRowBlock{display:flex;flex-wrap:wrap;justify-content:center;margin:0 -2px}.promoSection .promoRow .pluginRowBlock .pluginWrapper{padding:0 2px}.promoSection .promoRow .pluginRowBlock iframe{margin-left:2px;margin-top:5px}input[type=search]{-moz-appearance:none;-webkit-appearance:none}.navSearchWrapper{align-items:center;align-self:center;display:flex;justify-content:center;padding-left:10px;position:absolute;right:10px;top:10px}.navSearchWrapper:before{border:3px solid #e5e5e5;border-radius:50%;content:" ";display:block;height:6px;left:15px;position:absolute;top:50%;transform:translateY(-58%);width:6px;z-index:1}.navSearchWrapper:after{background:#e5e5e5;content:" ";height:7px;left:24px;position:absolute;top:55%;transform:rotate(-45deg);width:3px;z-index:1}.navSearchWrapper .aa-dropdown-menu{background:#f9f9f9;border:3px solid rgba(57,57,57,.25);color:#393939;font-size:14px;left:auto!important;line-height:1.2em;right:0!important}.navSearchWrapper .aa-dropdown-menu .algolia-docsearch-suggestion--category-header{background:#2f1666;color:#fff;font-size:14px;font-weight:400}.navSearchWrapper .aa-dropdown-menu .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--highlight{background-color:#2f1666;color:#fff}.navSearchWrapper .aa-dropdown-menu .algolia-docsearch-suggestion--subcategory-column .algolia-docsearch-suggestion--highlight,.navSearchWrapper .aa-dropdown-menu .algolia-docsearch-suggestion--title .algolia-docsearch-suggestion--highlight{color:#2f1666}.navSearchWrapper .aa-dropdown-menu .algolia-docsearch-suggestion--subcategory-column,.navSearchWrapper .aa-dropdown-menu .algolia-docsearch-suggestion__secondary{border-color:rgba(57,57,57,.3)}input#search_input_react{background-color:rgba(0,0,0,.2);border:none;border-radius:20px;color:#fff;font-size:14px;font-weight:300;line-height:20px;outline:none;padding-left:25px;position:relative;transition:width .5s ease;width:170px}.navSearchWrapper:before{left:24px}.navSearchWrapper:after{left:35px}input#search_input_react:active,input#search_input_react:focus{color:#fff;width:220px}.navigationSlider .slidingNav .navSearchWrapper .algolia-docsearch-footer a{height:auto}@media only screen and (max-width:735px){.navSearchWrapper{width:40%}}input::-moz-placeholder{color:#e5e5e5}input:-ms-input-placeholder{color:#e5e5e5}input::placeholder{color:#e5e5e5}.hljs{padding:1.25rem 1.5rem}.gridBlock{padding:0}.gridBlock>*{box-sizing:border-box}.gridBlock .fourByGridBlock img,.gridBlock .threeByGridBlock img,.gridBlock .twoByGridBlock img{max-width:100%}.gridBlock .gridClear{clear:both}@media only screen and (max-width:735px){.gridBlock .fourByGridBlock{flex:1 0 26%}}@media only screen and (min-width:736px){.gridBlock{display:flex;flex-direction:row;flex-wrap:wrap}.gridBlock>*{margin:0 12px}.gridBlock>:first-child{margin-left:0}.gridBlock>:last-child{margin-right:0}.gridBlock .twoByGridBlock{flex:1 0 40%}.gridBlock .threeByGridBlock{flex:1 0 26%}.gridBlock .fourByGridBlock{flex:1 0 20%}h2+.gridBlock{padding-top:20px}}@media only screen and (min-width:1400px){.gridBlock{display:flex;flex-direction:row;flex-wrap:wrap}}.alignCenter{text-align:center}.alignRight{text-align:right}.imageAlignSide{display:flex;flex-flow:row wrap}.blockImage{max-width:730px}.imageAlignSide .blockImage{flex:0 1 500px;max-width:500px}@media only screen and (max-width:735px){.imageAlignSide .blockImage{display:none}}.imageAlignSide .blockContent{flex:1 1}.imageAlignBottom .blockImage{margin:0 auto 20px;max-width:730px}.imageAlignBottom.alignCenter .blockImage{margin-left:auto;margin-right:auto}.imageAlignTop .blockImage{margin-bottom:20px;max-width:80px}.imageAlignTop.alignCenter .blockImage{margin-left:auto;margin-right:auto}.imageAlignRight .blockImage{margin-left:40px}.imageAlignLeft .blockImage{margin-right:40px}.container .gridBlock .blockContent p{padding:0}.container .wrapper .alignCenter h2{text-align:center}.container .wrapper .imageAlignSide h2{text-align:left}.container .wrapper .imageAlignSide p{margin:0 0 40px;max-width:560px}.highlightBackground{background:rgba(153,66,79,.7);color:#fff}.highlightBackground a{font-weight:800}.container.highlightBackground .wrapper h1,.container.highlightBackground .wrapper h2,.container.highlightBackground .wrapper h3,.container.highlightBackground .wrapper h4,.container.highlightBackground .wrapper h5,.highlightBackground a{border-color:#fff;color:#fff}.lightBackground{background:#f7f7f7}.darkBackground{background:grey;color:#fff}.darkBackground a,.darkBackground code{color:#d6b3b8}.container.darkBackground .wrapper h1,.container.darkBackground .wrapper h2,.container.darkBackground .wrapper h3,.container.darkBackground .wrapper h4,.container.darkBackground .wrapper h5{border-color:#fff;color:#fff}.container.paddingAll{padding:40px}.container.paddingBottom{padding-bottom:80px}.container.paddingLeft{padding-left:40px}.container.paddingRight{padding-right:40px}.container.paddingTop{padding-top:80px}@media only screen and (max-width:735px){.container.paddingBottom{padding-bottom:40px}.container.paddingTop{padding-top:20px}}@media only screen and (max-width:1023px){.responsiveList .blockContent{position:relative}.responsiveList .blockContent>div{padding-left:20px}.responsiveList .blockContent:before{content:"\2022";position:absolute}}.navigationSlider .navSlideout{cursor:pointer;padding-top:4px;position:absolute;right:10px;top:0;transition:top .3s;z-index:101}.navigationSlider .slidingNav{bottom:auto;box-sizing:border-box;left:0;position:fixed;right:0;top:0}.navigationSlider .slidingNav.slidingNavActive{height:auto;padding-top:42px;width:300px}.navigationSlider .slidingNav ul{background:#7731f6;box-sizing:border-box;color:#fff;display:flex;flex-wrap:nowrap;list-style:none;margin-top:50px;padding:0;width:100%}.navigationSlider .slidingNav.slidingNavActive ul{display:block}.navigationSlider .slidingNav ul li{flex:1 1 auto;margin:0;text-align:center;white-space:nowrap}.navigationSlider .slidingNav ul li a{align-items:center;box-sizing:border-box;color:#2f1666;color:inherit;display:flex;font-size:.9em;height:auto;height:50px;justify-content:center;margin:0;padding:10px;transition:background-color .3s}.navigationSlider .slidingNav ul li.siteNavGroupActive>a,.navigationSlider .slidingNav ul li.siteNavItemActive>a,.navigationSlider .slidingNav ul li>a:focus,.navigationSlider .slidingNav ul li>a:hover{background-color:#2f1666}.languages-icon{width:20px}#languages-dropdown{pointer-events:none;position:absolute;width:100%}#languages-dropdown.visible{display:flex}#languages-dropdown.hide{display:none}#languages-dropdown-items{background-color:#2f1666;display:flex;flex-direction:column;min-width:120px;pointer-events:all}#languages li{display:block}.navPusher{left:0;min-height:100%;padding-top:100px;position:relative;z-index:99}.singleRowMobileNav.navPusher{padding-top:50px}.navPusher:after{background:rgba(0,0,0,.4);content:"";height:0;opacity:0;position:absolute;right:0;top:0;transition:opacity .5s,width .1s .5s,height .1s .5s;width:0}@media screen and (min-width:1024px){.navPusher{display:flex;flex-direction:column;min-height:calc(100vh - 50px);padding-top:50px}.navPusher,.navPusher>:first-child{flex-grow:1}}.sliderActive .navPusher:after{height:100%;opacity:1;transition:opacity .5s;width:100%;z-index:100}@media only screen and (max-width:1024px){.reactNavSearchWrapper input#search_input_react{background-color:rgba(242,196,178,.25);border:none;border-radius:20px;box-sizing:border-box;color:#393939;font-size:14px;line-height:20px;outline:none;padding-left:38px;position:relative;transition:background-color .2s cubic-bezier(.68,-.55,.265,1.55),width .2s cubic-bezier(.68,-.55,.265,1.55),color .2s ease;width:100%;height:30px}.reactNavSearchWrapper input#search_input_react:active,.reactNavSearchWrapper input#search_input_react:focus{background-color:#2f1666;color:#fff}.reactNavSearchWrapper .algolia-docsearch-suggestion--subcategory-inline{display:none}.reactNavSearchWrapper>span{width:100%}.reactNavSearchWrapper .aa-dropdown-menu{font-size:12px;line-height:2em;padding:0;border-width:1px;min-width:500px}.reactNavSearchWrapper .algolia-docsearch-suggestion__secondary{border-top:none}.aa-suggestions{min-height:140px;max-height:60vh;-webkit-overflow-scrolling:touch;overflow-y:scroll}#languages-dropdown{left:0;top:50px}#languages-dropdown-items{background-color:#2f1666;display:flex;flex-direction:row}}@media only screen and (min-width:1024px){.navSearchWrapper{padding-left:10px;position:relative;right:auto;top:auto}.reactNavSearchWrapper input#search_input_react{height:100%;padding-top:8px;padding-bottom:8px;padding-left:38px}.navSearchWrapper .algolia-autocomplete{display:block}.navigationSlider{height:34px;margin-left:auto;position:relative}.navigationSlider .navSlideout{display:none}.navigationSlider nav.slidingNav{background:none;height:auto;position:relative;right:auto;top:auto;width:auto}.navigationSlider .slidingNav ul{background:none;display:flex;flex-flow:row nowrap;margin:0;padding:0;width:auto}.navigationSlider .slidingNav ul li a{border:0;color:hsla(0,0%,100%,.8);display:flex;font-size:16px;font-size:1em;font-weight:300;height:32px;line-height:1.2em;margin:0;padding:6px 10px}.navigationSlider .slidingNav ul li.siteNavGroupActive a,.navigationSlider .slidingNav ul li.siteNavItemActive a,.navigationSlider .slidingNav ul li a:hover{color:#fff}}@media only screen and (max-width:735px){.navigationSlider .slidingNav ul{overflow-x:auto}.navigationSlider .slidingNav ul::-webkit-scrollbar{display:none}.reactNavSearchWrapper .aa-dropdown-menu{min-width:400px}}@media only screen and (max-width:475px){.reactNavSearchWrapper .aa-dropdown-menu{min-width:300px}}.docMainWrapper .wrapper{padding-left:0;padding-right:0;padding-top:10px}@media only screen and (min-width:1024px){.docMainWrapper{width:100%}.docMainWrapper>*{margin:0 24px}.docMainWrapper>:first-child{margin-left:0}.docMainWrapper>:last-child{margin-right:0}.docMainWrapper .mainContainer{min-width:0}}.edit-page-link{float:right;font-size:10px;font-weight:400;margin-top:3px;text-decoration:none}@media only screen and (max-width:1023px){.edit-page-link{display:none}}.docLastUpdate{font-size:13px;font-style:italic;margin:20px 0;text-align:right}.docs-prevnext{margin:20px 0}.docs-prevnext:after{clear:both;content:" ";display:table}.docs-next{float:right}.docs-prev{float:left}@media only screen and (max-width:735px){.docs-next{clear:both;float:left}.docs-next,.docs-prev{margin:10px 0}.arrow-next{float:right;margin-left:10px}.arrow-prev{float:left;margin-right:10px}.function-name-prevnext{width:200px;display:inline-block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}}.hide{display:none}.collapsible{cursor:pointer}.collapsible .arrow{float:right;margin-right:8px;margin-top:-4px;transform:rotate(90deg);transition:transform .2s linear}.collapsible .arrow.rotate{transform:rotate(180deg)}@media only screen and (max-width:1023px){.docsNavContainer{background:#fff;left:0;position:fixed;width:100%;z-index:100}}@media only screen and (min-width:1024px){.docsNavContainer{flex:0 0 240px;height:calc(100vh - 50px);position:sticky;overflow-y:auto;top:50px}}.docsSliderActive.docsNavContainer{box-sizing:border-box;height:100%;-webkit-overflow-scrolling:touch;overflow-y:auto;-ms-scroll-chaining:none;overscroll-behavior:contain;padding-bottom:50px}.docsNavContainer .toc .navBreadcrumb{background-color:#f1f1f1;box-sizing:border-box;display:flex;flex-flow:row nowrap;font-size:12px;height:48px;overflow:hidden;padding:8px 20px}.docsNavContainer .toc .navWrapper{padding:0}@media only screen and (min-width:1024px){.docsNavContainer .toc .navBreadcrumb{display:none}.navBreadcrumb h2{padding:0 10px}.separateOnPageNav .docsNavContainer{flex:0 0 240px}}.navBreadcrumb a,.navBreadcrumb span{border:0;color:#393939}@media only screen and (max-width:735px){.anchor{top:-144px}}@media only screen and (min-width:1024px){.toc{padding:40px 0}}.toc section{padding:0;position:relative}.toc section .navGroups{display:none;padding:48px 20px 60px}.toc .toggleNav{color:#393939;position:relative}.toc .toggleNav .navToggle{cursor:pointer;height:32px;margin-right:10px;position:relative;text-align:left;width:18px}.hamburger-menu{position:absolute;top:6px;width:100%}.line1,.line2,.line3{width:100%;height:3px;background-color:#393939;margin:3px 0;transition:.4s;border-radius:10px}.docsSliderActive .hamburger-menu{top:12px}.docsSliderActive .line1{position:absolute;top:50%;transform:rotate(-45deg)}.docsSliderActive .line2{display:none}.docsSliderActive .line3{position:absolute;top:50%;transform:rotate(45deg)}.toggleNav h2 i{padding:0 4px}.toc .toggleNav .navGroup{margin-bottom:16px}.toc .toggleNav .subNavGroup{margin-bottom:0}.toc .toggleNav .navGroup .navGroupCategoryTitle{color:#393939;font-size:18px;font-weight:500;line-height:1.2em;margin-bottom:8px;margin-top:0}.toc .toggleNav .navGroup .navGroupSubcategoryTitle{color:#393939;font-size:14px;font-weight:500;line-height:1.5;margin-bottom:0;margin-top:0;padding:4px 0}.toc .toggleNav .navGroup .navListItem{margin:0}.toc .toggleNav .navGroup h3 i:not(:empty){box-sizing:border-box;color:rgba(57,57,57,.5);display:inline-block;height:16px;margin-right:10px;text-align:center;transition:color .2s;width:16px}.toc .toggleNav ul{padding:0 8px}.docsSliderActive .toc .toggleNav ul{padding-left:0}.toc .toggleNav ul li{list-style-type:none;padding:0}.toc .toggleNav ul li a{border:none;color:#717171;display:block;font-size:14px;padding:4px 0;transition:color .3s}.toc .toggleNav ul li.navListItemActive a,.toc .toggleNav ul li a:focus,.toc .toggleNav ul li a:hover{color:#2f1666}.docsSliderActive .toc .navBreadcrumb,.tocActive .navBreadcrumb{border-bottom:1px solid #ccc;margin-bottom:20px;position:fixed;width:100%}.toc .toggleNav .navBreadcrumb h2{border:0;flex-grow:1;font-size:16px;font-weight:600;line-height:32px;margin:0;padding:0}.docsSliderActive .toc section .navGroups{display:block;padding-top:60px}.tocToggler{cursor:pointer;height:32px;line-height:32px;margin-right:-10px;padding:0 10px}.icon-toc{box-sizing:border-box;display:inline-block;line-height:normal;position:relative;top:-1px;vertical-align:middle}.icon-toc,.icon-toc:after,.icon-toc:before{background-color:currentColor;border:1px solid;border-radius:50%;box-sizing:border-box;height:4px;width:4px}.icon-toc:after,.icon-toc:before{content:"";position:absolute}.icon-toc:before{left:-1px;top:-7px}.icon-toc:after{left:-1px;top:5px}.tocActive .icon-toc{border-radius:0;height:16px;transform:rotate(45deg);width:3px}.tocActive .icon-toc:before{border-radius:0;height:3px;left:50%;top:50%;transform:translate(-50%,-50%);width:16px}.tocActive .icon-toc:after{content:""}@media only screen and (min-width:1024px){.docMainWrapper{display:flex;flex-flow:row nowrap}.docMainWrapper .wrapper{padding-top:0;padding-left:0;padding-right:0}}.onPageNav{display:none;margin-bottom:40px}.onPageNav::-webkit-scrollbar{width:7px}.onPageNav::-webkit-scrollbar-track{background:#f1f1f1;border-radius:10px}.onPageNav::-webkit-scrollbar-thumb{background:#888;border-radius:10px}.onPageNav::-webkit-scrollbar-thumb:hover{background:#555}.onPageNav a{color:#717171}.onPageNav .toc-headings>li>a.active,.onPageNav .toc-headings>li>a.hover{font-weight:600;color:#2f1666}.onPageNav ul{list-style:none}.onPageNav ul li{font-size:12px;line-height:16px;padding-bottom:8px}.onPageNav ul ul{padding:8px 0 0 20px}.onPageNav ul ul li{padding-bottom:5px}@media only screen and (min-width:1024px){.toc section .navGroups{display:block;padding:8px 0 0}.navBreadcrumb h2{padding:0 10px}}@supports (position:sticky){@media only screen and (max-width:1023px){.tocActive .onPageNav{background:#fff;bottom:0;display:block;left:0;overflow-y:auto;-ms-scroll-chaining:none;overscroll-behavior:contain;padding:0 20px;position:fixed;right:0;top:148px;z-index:10;margin-bottom:0}.tocActive .singleRowMobileNav .onPageNav{top:98px}.tocActive .navBreadcrumb h2,.tocActive .navToggle{visibility:hidden}.tocActive .onPageNav>.toc-headings{padding:12px 0}}@media only screen and (min-width:1024px){.separateOnPageNav .headerWrapper.wrapper,.separateOnPageNav .wrapper{max-width:1400px}.separateOnPageNav .toc{width:auto}.separateOnPageNav.sideNavVisible .navPusher .mainContainer{flex:1 auto;max-width:100%;min-width:0}.onPageNav{align-self:flex-start;display:block;flex:0 0 240px;max-height:calc(100vh - 90px);overflow-y:auto;position:sticky;top:90px}.onPageNav>.toc-headings{border-left:1px solid #e0e0e0;padding:10px 0 2px 15px}.tocToggler{display:none}}}.blog .wrapper{max-width:1100px}.blogContainer .posts .post{border-bottom:1px solid #e0e0e0;border-radius:3px;margin-bottom:20px;padding-bottom:20px}.blogContainer .postHeader{margin-bottom:10px}.blogContainer .postHeaderTitle{margin-top:0}.blogContainer .postHeader p.post-meta{margin-bottom:10px;padding:0}.blogContainer .postHeader .authorBlock{display:flex}.blogContainer .postHeader .post-authorName{color:rgba(57,57,57,.7);display:flex;flex-direction:column;font-size:14px;font-weight:400;justify-content:center;margin-right:10px;margin-top:0;margin-bottom:0;padding:0}.blogContainer .postHeader .authorPhoto{border-radius:50%;height:30px;overflow:hidden;width:30px}.blogContainer .postHeader .authorPhoto.authorPhotoBig{height:50px;width:50px}.blog-recent{margin:20px 0}.blog-recent>a{float:left}@media only screen and (max-width:735px){.blog-recent{height:40px}}.blogSocialSection{display:block;padding:36px 0}.blogSocialSection .blogSocialSectionItem{padding-bottom:5px}.fb-like{display:block;margin-bottom:20px;width:100%}.more-users{margin:0 auto;max-width:560px;text-align:center}.productShowcaseSection{padding:0 20px;text-align:center}.productShowcaseSection.paddingTop{padding-top:20px}.productShowcaseSection.paddingBottom{padding-bottom:80px}.productShowcaseSection h2{color:#2f1666;font-size:30px;line-height:1em;margin-top:20px;padding:10px 0;text-align:center}.productShowcaseSection p{margin:0 auto;max-width:560px;padding:.8em 0}.productShowcaseSection .logos{align-items:center;display:flex;flex-flow:row wrap;justify-content:center;padding:20px}.productShowcaseSection .logos img{max-height:110px;padding:20px;width:110px}@media only screen and (max-width:735px){.productShowcaseSection .logos img{max-height:64px;padding:20px;width:64px}}.showcaseSection{margin:0 auto;max-width:900px}.showcaseSection,.showcaseSection .prose h1{text-align:center}.showcaseSection .prose{margin:0 auto;max-width:560px;text-align:center}.showcaseSection .logos{align-items:center;display:flex;flex-flow:row wrap;justify-content:center}.showcaseSection .logos img{max-height:128px;padding:20px;width:128px}@media only screen and (max-width:735px){.showcaseSection .logos img{max-height:64px;padding:20px;width:64px}}.nav-footer{background:#20232a;border:none;color:#202020;font-size:15px;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-weight:400;line-height:24px;padding-bottom:2em;padding-top:2em;position:relative}@media only screen and (min-width:1024px){.nav-footer{flex-shrink:0}}.nav-footer .sitemap{display:flex;justify-content:space-between;margin:0 auto 3em;max-width:1080px}.nav-footer .sitemap div{flex:1}.nav-footer .sitemap .nav-home{display:table;height:72px;margin:-12px 20px 0 0;opacity:.4;padding:10px;transition:opacity .15s ease-in-out;width:72px}.nav-footer .sitemap .nav-home:focus,.nav-footer .sitemap .nav-home:hover{opacity:1}@media only screen and (max-width:735px){.nav-footer .sitemap{display:flex;flex-direction:column;margin:0 2em 3em;width:calc(100% - 4em)}.nav-footer .sitemap>div{margin-bottom:18px}}.nav-footer .sitemap a{color:hsla(0,0%,100%,.6);display:block;margin:2px 0;padding:3px 0}.nav-footer .sitemap a:focus,.nav-footer .sitemap a:hover,.nav-footer .sitemap h5>a:focus,.nav-footer .sitemap h5>a:hover{color:#fff;text-decoration:none}.nav-footer .sitemap h5,.nav-footer .sitemap h6{margin:0 0 10px}.nav-footer .sitemap h5,.nav-footer .sitemap h5>a,.nav-footer .sitemap h6,.nav-footer .sitemap h6>a{color:#fff}.nav-footer .sitemap h5>a,.nav-footer .sitemap h6>a{margin:0 -10px}.nav-footer .fbOpenSource{display:block;margin:1em auto;opacity:.4;transition:opacity .15s ease-in-out;width:170px}.nav-footer .fbOpenSource:hover{opacity:1}.nav-footer .copyright{color:hsla(0,0%,100%,.4);text-align:center}.nav-footer .social{padding:5px 0}.tabs{border-top:1px solid #cfcfcf}.nav-tabs{display:flex;border-bottom:4px solid #e0e0e0;width:100%;padding:0;overflow-x:auto;white-space:nowrap;max-height:100%}.nav-tabs::-webkit-scrollbar{display:none}.tabs .tab-pane:focus{outline:none}.tabs .nav-tabs>div{font-size:14px;line-height:1.14286;padding:12px 16px;text-decoration:none;display:block;cursor:pointer}.tabs .nav-tabs>div.active{border-bottom:4px solid #2f1666}.tab-pane{display:none}.tab-pane.active{display:block}.tab-pane>pre{white-space:pre-wrap}.tab-pane>pre>code{margin-top:0;border-radius:0;box-shadow:none}.nav-footer{background-color:#2f1666}.nav-home.custom{height:auto!important;width:auto!important;padding:0 10px!important} ================================================ FILE: website/build/Kakunin/css/prism.css ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * Modified prism.js default theme for JavaScript, CSS and HTML * Based on dabblet (http://dabblet.com) * @author Lea Verou */ code[class*='language-'], pre[class*='language-'] { font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; text-align: left; white-space: pre; word-spacing: normal; word-break: normal; word-wrap: normal; line-height: 1.5; -moz-tab-size: 4; -o-tab-size: 4; tab-size: 4; -webkit-hyphens: none; -moz-hyphens: none; -ms-hyphens: none; hyphens: none; } /* Code blocks */ pre[class*='language-'] { padding: 1em; margin: 0.5em 0; overflow: auto; } /* Inline code */ :not(pre) > code[class*='language-'] { padding: 0.1em; border-radius: 0.3em; white-space: normal; } .token.comment, .token.prolog, .token.doctype, .token.cdata { color: slategray; } .token.punctuation { color: #999; } .namespace { opacity: 0.7; } .token.property, .token.tag, .token.boolean, .token.constant, .token.symbol, .token.deleted { color: #905; } .token.selector, .token.number, .token.attr-name, .token.string, .token.char, .token.builtin, .token.inserted { color: #690; } .token.operator, .token.entity, .token.url, .language-css .token.string, .style .token.string { color: #9a6e3a; } .token.atrule, .token.attr-value, .token.keyword { color: #07a; } .token.function, .token.class-name { color: #dd4a68; } .token.regex, .token.important, .token.variable { color: #e90; } .token.important, .token.bold { font-weight: bold; } .token.italic { font-style: italic; } .token.entity { cursor: help; } ================================================ FILE: website/build/Kakunin/docs/2.4.0/configuration/index.html ================================================ Configuration · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/configuration.html ================================================ Configuration · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/cross-browser/index.html ================================================ Cross-browser testing · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/cross-browser.html ================================================ Cross-browser testing · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/docker/index.html ================================================ Docker · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/docker.html ================================================ Docker · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/extending/index.html ================================================ Extending Kakunin · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/extending.html ================================================ Extending Kakunin · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/how-it-works/index.html ================================================ How it works · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/how-it-works.html ================================================ How it works · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/index.html ================================================ Getting started · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/matchers/index.html ================================================ Matchers · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/matchers.html ================================================ Matchers · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/parallel-testing/index.html ================================================ Parallel testing · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/parallel-testing.html ================================================ Parallel testing · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/performance-testing/index.html ================================================ Performance testing · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/performance-testing.html ================================================ Performance testing · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/quickstart/index.html ================================================ Quick start · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/quickstart.html ================================================ Quick start · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/steps-debug/index.html ================================================ Debug · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/steps-debug.html ================================================ Debug · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/steps-elements/index.html ================================================ Elements · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/steps-elements.html ================================================ Elements · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/steps-files/index.html ================================================ Files · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/steps-files.html ================================================ Files · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/steps-forms/index.html ================================================ Forms · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/steps-forms.html ================================================ Forms · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/steps-generators/index.html ================================================ Generators · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/steps-generators.html ================================================ Generators · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/steps-navigation/index.html ================================================ Navigation · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/steps-navigation.html ================================================ Navigation · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/transformers/index.html ================================================ Transformers · Kakunin ================================================ FILE: website/build/Kakunin/docs/2.4.0/transformers.html ================================================ Transformers · Kakunin ================================================ FILE: website/build/Kakunin/docs/configuration/index.html ================================================ Configuration · Kakunin ================================================ FILE: website/build/Kakunin/docs/configuration.html ================================================ Configuration · Kakunin ================================================ FILE: website/build/Kakunin/docs/cross-browser/index.html ================================================ Cross-browser testing · Kakunin ================================================ FILE: website/build/Kakunin/docs/cross-browser.html ================================================ Cross-browser testing · Kakunin ================================================ FILE: website/build/Kakunin/docs/docker/index.html ================================================ Docker · Kakunin ================================================ FILE: website/build/Kakunin/docs/docker.html ================================================ Docker · Kakunin ================================================ FILE: website/build/Kakunin/docs/extending/index.html ================================================ Extending Kakunin · Kakunin ================================================ FILE: website/build/Kakunin/docs/extending.html ================================================ Extending Kakunin · Kakunin ================================================ FILE: website/build/Kakunin/docs/how-it-works/index.html ================================================ How it works · Kakunin ================================================ FILE: website/build/Kakunin/docs/how-it-works.html ================================================ How it works · Kakunin ================================================ FILE: website/build/Kakunin/docs/index.html ================================================ Getting started · Kakunin ================================================ FILE: website/build/Kakunin/docs/matchers/index.html ================================================ Matchers · Kakunin ================================================ FILE: website/build/Kakunin/docs/matchers.html ================================================ Matchers · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/browserstack/index.html ================================================ Browserstack integration · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/browserstack.html ================================================ Browserstack integration · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/configuration/index.html ================================================ Configuration · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/configuration.html ================================================ Configuration · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/cross-browser/index.html ================================================ Cross-browser testing · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/cross-browser.html ================================================ Cross-browser testing · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/docker/index.html ================================================ Docker · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/docker.html ================================================ Docker · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/extending/index.html ================================================ Extending Kakunin · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/extending.html ================================================ Extending Kakunin · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/headless/index.html ================================================ Headless · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/headless.html ================================================ Headless · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/hooks/index.html ================================================ Hooks · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/hooks.html ================================================ Hooks · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/how-it-works/index.html ================================================ How it works · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/how-it-works.html ================================================ How it works · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/index.html ================================================ Getting started · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/matchers/index.html ================================================ Matchers · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/matchers.html ================================================ Matchers · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/parallel-testing/index.html ================================================ Parallel testing · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/parallel-testing.html ================================================ Parallel testing · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/performance-testing/index.html ================================================ Performance testing · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/performance-testing.html ================================================ Performance testing · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/quickstart/index.html ================================================ Quick start · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/quickstart.html ================================================ Quick start · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/steps-debug/index.html ================================================ Debug · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/steps-debug.html ================================================ Debug · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/steps-elements/index.html ================================================ Elements · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/steps-elements.html ================================================ Elements · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/steps-files/index.html ================================================ Files · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/steps-files.html ================================================ Files · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/steps-forms/index.html ================================================ Forms · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/steps-forms.html ================================================ Forms · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/steps-generators/index.html ================================================ Generators · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/steps-generators.html ================================================ Generators · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/steps-navigation/index.html ================================================ Navigation · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/steps-navigation.html ================================================ Navigation · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/steps-rest/index.html ================================================ Rest api · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/steps-rest.html ================================================ Rest api · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/testing-rest-api/index.html ================================================ REST API examples · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/testing-rest-api.html ================================================ REST API examples · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/transformers/index.html ================================================ Transformers · Kakunin ================================================ FILE: website/build/Kakunin/docs/next/transformers.html ================================================ Transformers · Kakunin ================================================ FILE: website/build/Kakunin/docs/parallel-testing/index.html ================================================ Parallel testing · Kakunin ================================================ FILE: website/build/Kakunin/docs/parallel-testing.html ================================================ Parallel testing · Kakunin ================================================ FILE: website/build/Kakunin/docs/performance-testing/index.html ================================================ Performance testing · Kakunin ================================================ FILE: website/build/Kakunin/docs/performance-testing.html ================================================ Performance testing · Kakunin ================================================ FILE: website/build/Kakunin/docs/quickstart/index.html ================================================ Quick start · Kakunin ================================================ FILE: website/build/Kakunin/docs/quickstart.html ================================================ Quick start · Kakunin ================================================ FILE: website/build/Kakunin/docs/steps-debug/index.html ================================================ Debug · Kakunin ================================================ FILE: website/build/Kakunin/docs/steps-debug.html ================================================ Debug · Kakunin ================================================ FILE: website/build/Kakunin/docs/steps-elements/index.html ================================================ Elements · Kakunin ================================================ FILE: website/build/Kakunin/docs/steps-elements.html ================================================ Elements · Kakunin ================================================ FILE: website/build/Kakunin/docs/steps-files/index.html ================================================ Files · Kakunin ================================================ FILE: website/build/Kakunin/docs/steps-files.html ================================================ Files · Kakunin ================================================ FILE: website/build/Kakunin/docs/steps-forms/index.html ================================================ Forms · Kakunin ================================================ FILE: website/build/Kakunin/docs/steps-forms.html ================================================ Forms · Kakunin ================================================ FILE: website/build/Kakunin/docs/steps-generators/index.html ================================================ Generators · Kakunin ================================================ FILE: website/build/Kakunin/docs/steps-generators.html ================================================ Generators · Kakunin ================================================ FILE: website/build/Kakunin/docs/steps-navigation/index.html ================================================ Navigation · Kakunin ================================================ FILE: website/build/Kakunin/docs/steps-navigation.html ================================================ Navigation · Kakunin ================================================ FILE: website/build/Kakunin/docs/transformers/index.html ================================================ Transformers · Kakunin ================================================ FILE: website/build/Kakunin/docs/transformers.html ================================================ Transformers · Kakunin ================================================ FILE: website/build/Kakunin/en/versions/index.html ================================================ Kakunin · Are you looking for an automation testing tool? Here's Kakunin – an open-source framework for end-to-end, automated software testing. ================================================ FILE: website/build/Kakunin/en/versions.html ================================================ Kakunin · Are you looking for an automation testing tool? Here's Kakunin – an open-source framework for end-to-end, automated software testing. ================================================ FILE: website/build/Kakunin/index.html ================================================ Kakunin If you are not redirected automatically, follow this link. ================================================ FILE: website/build/Kakunin/js/codetabs.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ // Turn off ESLint for this file because it's sent down to users as-is. /* eslint-disable */ window.addEventListener('load', function () { // add event listener for all tab document.querySelectorAll('.nav-link').forEach(function (el) { el.addEventListener('click', function (e) { var groupId = e.target.getAttribute('data-group'); document .querySelectorAll('.nav-link[data-group='.concat(groupId, ']')) .forEach(function (el) { el.classList.remove('active'); }); document .querySelectorAll('.tab-pane[data-group='.concat(groupId, ']')) .forEach(function (el) { el.classList.remove('active'); }); e.target.classList.add('active'); document .querySelector('#'.concat(e.target.getAttribute('data-tab'))) .classList.add('active'); }); }); }); ================================================ FILE: website/build/Kakunin/js/scrollSpy.js ================================================ /** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ /* eslint-disable */ (function scrollSpy() { var OFFSET = 10; var timer; var headingsCache; var findHeadings = function findHeadings() { return headingsCache || document.querySelectorAll('.toc-headings > li > a'); }; var onScroll = function onScroll() { if (timer) { // throttle return; } timer = setTimeout(function () { timer = null; var activeNavFound = false; var headings = findHeadings(); // toc nav anchors /** * On every call, try to find header right after <-- next header * the one whose content is on the current screen <-- highlight this */ for (var i = 0; i < headings.length; i++) { // headings[i] is current element // if an element is already active, then current element is not active // if no element is already active, then current element is active var currNavActive = !activeNavFound; /** * Enter the following check up only when an active nav header is not yet found * Then, check the bounding rectangle of the next header * The headers that are scrolled passed will have negative bounding rect top * So the first one with positive bounding rect top will be the nearest next header */ if (currNavActive && i < headings.length - 1) { var heading = headings[i + 1]; var next = decodeURIComponent(heading.href.split('#')[1]); var nextHeader = document.getElementById(next); if (nextHeader) { var top = nextHeader.getBoundingClientRect().top; currNavActive = top > OFFSET; } else { console.error('Can not find header element', { id: next, heading: heading, }); } } /** * Stop searching once a first such header is found, * this makes sure the highlighted header is the most current one */ if (currNavActive) { activeNavFound = true; headings[i].classList.add('active'); } else { headings[i].classList.remove('active'); } } }, 100); }; document.addEventListener('scroll', onScroll); document.addEventListener('resize', onScroll); document.addEventListener('DOMContentLoaded', function () { // Cache the headings once the page has fully loaded. headingsCache = findHeadings(); onScroll(); }); })(); ================================================ FILE: website/build/Kakunin/sitemap.xml ================================================ https://thesoftwarehouse.github.io/versionsweekly0.5https://thesoftwarehouse.github.io/Kakunin/docs/next/browserstackhourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/next/configurationhourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/next/cross-browserhourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/next/dockerhourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/next/extendinghourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/next/headlesshourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/next/hookshourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/next/how-it-workshourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/next/hourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/next/matchershourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/next/parallel-testinghourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/next/performance-testinghourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/next/quickstarthourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/next/steps-debughourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/next/steps-elementshourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/next/steps-fileshourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/next/steps-formshourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/next/steps-generatorshourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/next/steps-navigationhourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/next/steps-resthourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/next/testing-rest-apihourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/next/transformershourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/configurationhourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/cross-browserhourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/dockerhourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/extendinghourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/how-it-workshourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/hourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/matchershourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/parallel-testinghourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/performance-testinghourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/quickstarthourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/steps-debughourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/steps-elementshourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/steps-fileshourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/steps-formshourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/steps-generatorshourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/steps-navigationhourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/transformershourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/2.4.0/configurationhourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/2.4.0/cross-browserhourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/2.4.0/dockerhourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/2.4.0/extendinghourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/2.4.0/how-it-workshourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/2.4.0/hourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/2.4.0/matchershourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/2.4.0/parallel-testinghourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/2.4.0/performance-testinghourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/2.4.0/quickstarthourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/2.4.0/steps-debughourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/2.4.0/steps-elementshourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/2.4.0/steps-fileshourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/2.4.0/steps-formshourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/2.4.0/steps-generatorshourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/2.4.0/steps-navigationhourly1.0https://thesoftwarehouse.github.io/Kakunin/docs/2.4.0/transformershourly1.0 ================================================ FILE: website/build/Kakunin/versions/index.html ================================================ Kakunin · Are you looking for an automation testing tool? Here's Kakunin – an open-source framework for end-to-end, automated software testing. ================================================ FILE: website/build/Kakunin/versions.html ================================================ Kakunin · Are you looking for an automation testing tool? Here's Kakunin – an open-source framework for end-to-end, automated software testing. ================================================ FILE: website/core/Footer.js ================================================ /** * Copyright (c) 2017-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ const React = require('react'); class Footer extends React.Component { render() { return ( ); } } module.exports = Footer; ================================================ FILE: website/package.json ================================================ { "scripts": { "examples": "docusaurus-examples", "start": "docusaurus-start", "build": "docusaurus-build", "publish-gh-pages": "docusaurus-publish", "write-translations": "docusaurus-write-translations", "version": "docusaurus-version", "rename-version": "docusaurus-rename-version" }, "devDependencies": { "docusaurus": "1.14.7" } } ================================================ FILE: website/pages/en/versions.js ================================================ /** * Copyright (c) 2017-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ const React = require('react'); const CompLibrary = require('../../core/CompLibrary'); const Container = CompLibrary.Container; const CWD = process.cwd(); const siteConfig = require(`${CWD}/siteConfig.js`); const versions = require(`${CWD}/versions.json`); function Versions() { const latestVersion = versions[0]; const repoUrl = `https://github.com/${siteConfig.organizationName}/${ siteConfig.projectName }`; return (

{siteConfig.title} Versions

Current version (Stable)

Latest version of Kakunin.

{latestVersion} Documentation Release Notes

Pre-release versions

next Documentation

Past Versions

{versions.map( version => version !== latestVersion && ( ), )}
{version} Documentation Release Notes

You can find past versions of this project on{' '} GitHub.

); } module.exports = Versions; ================================================ FILE: website/sidebars.json ================================================ { "docs": { "Documentation": [ "quickstart", "index", "configuration", "how-it-works" ], "Built in mechanisms":[ "matchers", "transformers", "extending", "hooks" ], "Features": [ "cross-browser", "browserstack", "headless", "parallel-testing", "performance-testing", "docker", "testing-rest-api" ], "Steps": [ "steps-navigation", "steps-forms", "steps-elements", "steps-files", "steps-generators", "steps-debug", "steps-rest" ] } } ================================================ FILE: website/siteConfig.js ================================================ const siteConfig = { title: 'Kakunin', tagline: 'Are you looking for an automation testing tool? Here\'s Kakunin – an open-source framework for end-to-end, automated software testing.', ogImage: 'img/kakunin_ogImage.jpg', url: 'https://thesoftwarehouse.github.io', baseUrl: '/Kakunin/', projectName: 'Kakunin', organizationName: 'TheSoftwareHouse', headerLinks: [ {search: true}, {href: 'https://github.com/TheSoftwareHouse/Kakunin', label: 'GitHub'}, ], headerIcon: 'img/kakunin_white_logo.svg', footerIcon: 'img/kakunin_white_logo_text.svg', favicon: 'img/favicon/favicon.ico', algolia: { apiKey: '3cda5a3d6672f433f13a4a2cda6d2186', indexName: 'kakunin', algoliaOptions: {} // Optional, if provided by Algolia }, colors: { primaryColor: '#2F1666', secondaryColor: '#7731F6', }, copyright: `Copyright © ${new Date().getFullYear()} The Software House`, highlight: { theme: 'atom-one-dark', }, scripts: ['https://buttons.github.io/buttons.js'], onPageNav: 'separate', cleanUrl: true, }; module.exports = siteConfig; ================================================ FILE: website/static/css/custom.css ================================================ /* your custom css */ @media only screen and (min-device-width: 360px) and (max-device-width: 736px) { } @media only screen and (min-width: 1024px) { } @media only screen and (max-width: 1023px) { } @media only screen and (min-width: 1400px) { } @media only screen and (min-width: 1500px) { } .nav-footer { background-color: #2F1666; } .nav-home.custom { height: auto !important; width: auto !important; padding: 0 10px !important; } ================================================ FILE: website/static/index.html ================================================ Kakunin If you are not redirected automatically, follow this link. ================================================ FILE: website/versioned_docs/version-2.4.0/configuration.md ================================================ --- id: version-2.4.0-configuration title: Configuration original_id: configuration --- ## Kakunin config ``` module.exports = { "browserWidth": 1600, "browserHeight": 900, "timeout": 60, "maxEmailRepeats": 5, "intervalEmail": 5, "elementsVisibilityTimeout": 5, "waitForPageTimeout": 5, "downloadTimeout": 30, "reports": "/reports", "downloads": "/downloads", "data": "/data", "features": [ "/features" ], "pages": [ "/pages" ], "matchers": [ "/matchers" ], "generators": [ "/generators" ], "form_handlers": [ "/form_handlers" ], "step_definitions": [ "/step_definitions" ], "comparators": [ "/comparators" ], "dictionaries": [ "/dictionaries" ], "transformers": [ "/transformers" ], "regexes": [ "/regexes" ], "hooks": [ "/hooks" ], "clearEmailInboxBeforeTests": false, "clearCookiesAfterScenario": true, "clearLocalStorageAfterScenario": true, "email": null, "headless": false, "noGpu": false, "type": "otherWeb", "baseUrl": "http://localhost:8080", "accounts": { "someAccount": { "accounts": [ { "email": "", "password": "" } ] } } } ``` ## Configuration options `browserWidth` - width of browser window `default: 1600` `browserheight` - height of browser window `default: 900` `timeout` - global timeout for a single step execution in seconds `default: 60` `maxEmailRepeats` - maximum email repeats to catch email used in the email step `intervalEmail` - interval for email checking step `default: 5` in seconds `elementsVisibilityTimeout` - maximum wait timeout for element visibility `default: 5` seconds `waitForPageTimeout` - maximum wait timeout for page visibility `default: 5` seconds `downloadTimeout` - maximum wait timeout for file to be downloaded `default: 30` seconds `emails` - array of paths to store emails related custom code `reports` - path to store reports `downloads` - path to store downloaded files `data` - path to store test related files (for example files to be downloaded) `feature` - array of paths to store features `pages` - array of paths to store page objects `matchers` - array of paths to store custom matchers `generators` - array of paths to store custom generators `form_handlers` - array of paths to store custom form handlers `step_definitions` - array of paths to store custom steps `comparators` - array of paths to store custom comparators `dictionaries` - array of paths to store custom dictionaries `transformers` - array of paths to store custom transformers `regexes` - array of paths to store custom regexes `hooks` - array of paths to store custom hooks `clearEmailInboxBeforeTests` - flag to active clearing email inbox before tests are executed `default: false | true for apps with email checking functionality activated ` `clearCookiesAfterScenario` - flag to activate clearing cookies after every scenario `default: true` `clearLocalStorageAfterScenario` - flag to activate clearing local storage after every scenario `default: true` `email` - email configuration `default: null` for mailtrap email checking system: ```javascript "type": "mailtrap", "config": { "apiKey": "your-mailtrap-api-key", "inboxId": "your-mailtrap-inbox", "url": "https://mailtrap.io/api/v1" } ``` for custom email checking system only type is required: ``` "type": "custom-type" ``` `headless` - flag to activate chrome headless browser `default: false` `noGpu` - flag to activate cpu only mode `default: false` `type` - type of application either `ng1 | ng2 | otherWeb` `baseUrl` - url of tested application `accounts` - object to store accounts information. This is bound to `userProvider` and allows to use advanced email checking options like recipient checking. ```javascript "someAccount": { "accounts": [ { "email": "", "password": "" } ] } ``` ## Environment variables Kakunin uses a single `.env` file to load ENV variables. By default there is only one: `FIXTURES_RELOAD_HOST` - allows you to specify host for fixtures reloading. This allows you to use `@reloadFixtures` tag on scenarios that should restore database to starting state, before the test is running ================================================ FILE: website/versioned_docs/version-2.4.0/cross-browser.md ================================================ --- id: version-2.4.0-cross-browser title: Cross-browser testing original_id: cross-browser --- ## To run tests with specified browser There is a possibility to run Kakunin in various browsers: - Google Chrome (by default) `npm run kakunin` or `npm run kakunin -- --chrome` - Firefox `npm run kakunin -- --firefox` - Safari `npm run kakunin -- --safari` ## To run tests in different browsers at once There is a possibility to run more than one instance of WebDriver by giving an extra parameter to a command line: - `npm run kakunin --chrome --firefox` ## Safari ### Run tests 1. Open Safari's preferences 2. Enable "Show Develop menu in menu bar" 3. Open "Develop" tab 4. Enable "Allow Remote Automation" ### Troubleshooting Safari version 12.0: - drag & drop actions in Kakunin impossible (more details https://github.com/angular/protractor/issues/1526) ================================================ FILE: website/versioned_docs/version-2.4.0/docker.md ================================================ --- id: version-2.4.0-docker title: Docker original_id: docker --- # Docker for Kakunin tests This section explains how to run kakunin tests inside docker, below examples of Dockerfile and docker-compose.yml files let you build your first docker image and run tests. ## Dockerfile: This file is responsible for building the whole environment for our e2e tests. It will allow you to run tests on local and CI environments, by configuring and copying the whole project inside the container. Just simply place it inside your e2e project root. Below an example of Dockerfile ### Example of Dockerfile: ```bash # Downloading selenium image and setting privileges FROM selenium/standalone-chrome:3.14.0 USER root # Setting test directory WORKDIR /app # Install openjdk-8-jdk-headless RUN apt-get update -qqy \ && apt-get -qqy --no-install-recommends install \ xvfb \ openjdk-8-jdk-headless \ curl \ make \ && rm -rf /var/lib/apt/lists/* /var/cache/apt/* # Installing node 8 globally and setting paths RUN set -x \ && curl -sL https://deb.nodesource.com/setup_8.x | bash - \ && apt-get install -y \ nodejs \ && npm install -g npm@latest RUN PATH=/usr/bin/node:$PATH # Copy tests directory with ignored files from .dockerignore COPY --chown=seluser:seluser . . # Removing node_modules in case of existence or lack of .dockerignore and installing from package.json RUN rm -rf ./node_modules \ && npm install # Setting Xvfb RUN export DISPLAY=:99.0 USER seluser ``` ##docker-compose.yml Compose is a tool for defining and running multi-container Docker applications, which we use for running our tests. Running command: ``docker-compose up -d`` will start Dockerfile script, as a result, it builds the container. Running command: ``docker-compose build `` or ``docker-compose up --build`` will rebuild container, if there were made any changes. Running command: ``docker-compose run --rm e2e`` will start running tests inside the container Composition below allows you to run e2e tests inside the container and configure it locally or in CI environments. ### Example of docker-compose.yml: ```bash e2e: build: . working_dir: /app command: sh -c "Xvfb -ac :99 -screen 0 1280x1024x16 & npm run kakunin" ``` ### How to run step by step 1. Install docker (e.g Docker for Mac), 2. Create Dockerfile and docker-compose.yml in the root of your e2e project, 3. Run in command line `docker-compose up -d ` which will start docker and build image if it's not build 4. Run in command line `docker-compose run --rm e2e` to run your tests ================================================ FILE: website/versioned_docs/version-2.4.0/extending.md ================================================ --- id: version-2.4.0-extending title: Extending Kakunin original_id: extending --- Kakunin allows you to easily add a custom code in order to extend it's functionality. ## Internal services ### Regex builder Regex builder is a special builder for creating `RegExp` objects based on regexp name. Internally it has access to not only to all built-in regular expression files, but also custom ones specified by user. ```javascript const { regexBuilder } = require('kakunin'); const myRegex = regexBuilder.buildRegex('r:number'); //myRegex will contain RegExp object that matches regular expression under the name "number" in regexes file. ``` ### Variable store Variable store allows you to store and read some values to be used during given scenario. ```javascript const { variableStore } = require('kakunin'); variableStore.storeVariable('some-name', 'some-value'); const myValue = variableStore.getVariableValue('some-name'); //contains 'some-value' ``` ### User provider Kakunin comes with functionality that allows you to easily load credentials for a given account type - `UserProvider`. In `kakunin.conf.js` you can find a section `accounts`. The structure it has is very simple: ```json "accounts": { "someAccount": { "accounts": [ { "email": "", "password": "" } ] } } ``` `someAccount` - the name of accounts group `accounts` - an array of account credentials (in order to be able to check if a `currentUser` got an email, this has to have an `email` key, otherwise account can have any kind of properties) Use provider is accessible inside any kind of a step by calling `this.userProvider`. It comes with a single method: `this.userProvider.getUser(groupName)` - returns an account credentials for a given user group. It is a good practice to save a current user in `this.currentUser` variable for a email checking service. ## Adding custom code ### Custom step In order to add a custom step, you have to create inside of a directory specified as `step_definitions` in kakunin configuration file `default: /step_definitions`. We're using `cucumber-js 4.X` so in order to add custom step you have to use `defineSupportCode` method like this: ```javascript const { defineSupportCode } = require('kakunin'); defineSupportCode(({ When }) => { When(/^I use kakunin$/, function() { expect(true).to.equal(true); }); }); ``` ### Page objects Kakunin comes with some built-in page objects, that should be used as a base for your page objects. In order to create a custom one, create a file inside the `pages` directory and extend the `BasePage` from kakunin package. ```javascript const { BasePage } = require('kakunin'); class MyPageObject extends BasePage { constructor() { this.myElement = element(by.css('.some-elemnt')); } } module.exports = MyPageObject; ``` ### Matchers Matchers are used to compare if given value is matching our expectation. For example if a value in table is a number. You can add your own matcher as below: ```javascript const { matchers } = require('kakunin'); class MyMatcher { isSatisfiedBy(prefix, name) { return prefix === 'm:' && name === 'pending'; } match(protractorElement, matcherName) { return protractorElement.getText().then((value) => { if (value === 'pending') { return true; } return Promise.reject(`Matcher "MyMatcher" could not match value on element "${protractorElement.locator()}". Expected: "pending", given: "${value}"`); }); } } matchers.addMatcher(new MyMatcher()); ``` ### Dictionaries Dictionaries allows you to present complicated values in much more readable way. For example if an element must be in a form of IRI `/some-resource/123-123-123-23` and you wish to use `pending-resource` as it's alias. You can add your own dictionary: ```javascript const { dictionaries } = require('kakunin'); const { BaseDictionary } = require('kakunin'); class TestDictionary extends BaseDictionary { constructor() { super('name-of-dictionary', { 'pending-resource': '/some-resource/123-123-123-23', 'test-value': 'some other value' }); } } dictionaries.addDictionary(new TestDictionary()); ``` ### Generators Generators allows you to create random values You can add your own generator: ```javascript const { generators } = require('kakunin'); class MyGeneerator{ isSatisfiedBy(name) { return name === 'my-generator'; } generate(params) { return Promise.resolve('some-random-value'); } } generators.addGenerator(new MyGeneerator()); ``` ### Comparators Comparators allows you to check if a set of values has an expected order You can add your own comparators: ```javascript const { comparators } = require('kakunin'); class MyComparator { isSatisfiedBy(values) { for(let i=0; i/TSHProjects/test/reports Created directory at path /Users//TSHProjects/test/reports/report Created directory at path /Users//TSHProjects/test/reports/report/features Created directory at path /Users//TSHProjects/test/reports/performance Created directory at path /Users//TSHProjects/test/downloads Created directory at path /Users/example-user/projects/test/data Created directory at path /Users/example-user/projects/test/features Created directory at path /Users/example-user/projects/test/pages Created directory at path /Users/example-user/projects/test/matchers Created directory at path /Users/example-user/projects/test/generators Created directory at path /Users/example-user/projects/test/form_handlers Created directory at path /Users/example-user/projects/test/step_definitions Created directory at path /Users/example-user/projects/test/comparators Created directory at path /Users/example-user/projects/test/dictionaries Created directory at path /Users/example-user/projects/test/regexes Created directory at path /Users/example-user/projects/test/hooks Created directory at path /Users/example-user/projects/test/transformers Created directory at path /Users/example-user/projects/test/emails Created file at path /Users/example-user/projects/test/downloads/.gitkeep Created file at path /Users/example-user/projects/test/reports/report/.gitkeep Created file at path /Users/example-user/projects/test/reports/report/features/.gitkeep Created file at path /Users/example-user/projects/test/reports/performance/.gitkeep Created file at path /Users/example-user/projects/test/features/example.feature Created file at path /Users/example-user/projects/test/pages/page.js Created file at path /Users/example-user/projects/test/matchers/matcher.js Created file at path /Users/example-user/projects/test/generators/generator.js Created file at path /Users/example-user/projects/test/step_definitions/steps.js Created file at path /Users/example-user/projects/test/regexes/regex.js Created file at path /Users/example-user/projects/test/hooks/hook.js ``` And you're set! Now you can run the tests using Kakunin: ```bash npm run kakunin ``` ## Commands * Create a new project by answering few simple questions (you can pass additional parameter to enter advanced mode where you can configure all Kakunin options by yourself) ```bash npm run kakunin init [-- --advanced] ``` * Run test scenarios ```bash npm run kakunin ``` * Run only scenarios tagged by `@someTag` ```bash npm run kakunin -- --tags @someTag ``` * Run only scenarios tagged by `@someTag` and `@otherTag` at the same time ```bash npm run kakunin -- --tags "@someTag and @otherTag" ``` * Run only scenarios tagged by `@someTag` or `@otherTag` ```bash npm run kakunin -- --tags "@someTag or @otherTag" ``` * Run only scenarios not tagged by `@someTag` ```bash npm run kakunin -- --tags "not @someTag" ``` ## Troubleshooting & Tips In order to make cucumber steps autosuggestion work properly in JetBrains tools, make sure your project is `ECMAScript 6` compatible and you have `cucumberjs` plugin installed. Due to non-resolved issue in Jetbrains editors ([see here](https://youtrack.jetbrains.com/issue/WEB-11505)) we'll have to do one more step: Go to `step_definitions` directory ```bash cd step_definitions ``` Paste this code into terminal and restart your IDE: For Linux/MacOs: ```bash ln -s ../node_modules/kakunin/dist/step_definitions/elements.js kakunin-elements.js ln -s ../node_modules/kakunin/dist/step_definitions/debug.js kakunin-debug.js ln -s ../node_modules/kakunin/dist/step_definitions/file.js kakunin-file.js ln -s ../node_modules/kakunin/dist/step_definitions/form.js kakunin-form.js ln -s ../node_modules/kakunin/dist/step_definitions/email.js kakunin-email.js ln -s ../node_modules/kakunin/dist/step_definitions/generators.js kakunin-generators.js ln -s ../node_modules/kakunin/dist/step_definitions/navigation.js kakunin-navigation.js ln -s ../node_modules/kakunin/dist/step_definitions/performance.js kakunin-performance.js ``` For Windows 8+: (you have to do this as administrator) ```bash mklink kakunin-elements.js ../node_modules/kakunin/dist/step_definitions/elements.js mklink kakunin-debug.js ../node_modules/kakunin/dist/step_definitions/debug.js mklink kakunin-file.js ../node_modules/kakunin/dist/step_definitions/file.js mklink kakunin-form.js ../node_modules/kakunin/dist/step_definitions/form.js mklink kakunin-email.js ../node_modules/kakunin/dist/step_definitions/email.js mklink kakunin-generators.js ../node_modules/kakunin/dist/step_definitions/generators.js mklink kakunin-navigation.js ../node_modules/kakunin/dist/step_definitions/navigation.js mklink kakunin-performance.js ../node_modules/kakunin/dist/step_definitions/performance.js ``` Keep in mind that `mklink` is not available in older Windows distributions. This will create symlinks inside `step_definitions` directory and make `cucumberjs` plugin recognize kakunin built-in steps. ================================================ FILE: website/versioned_docs/version-2.4.0/matchers.md ================================================ --- id: version-2.4.0-matchers title: Matchers original_id: matchers --- Matchers allows you to check if a element content matches your expectation. For example you can check if a value has a specified pattern or if a button is clickable. Using matcher is very straightforward, for example: `f:isClickable`. Matchers can be used in most of the steps related to checking content (with exception of checking form values). Kakunin comes with a set of built in matchers: ## Visibility matcher `f:isVisible` - checks if element is visible (must be in viewport and cannot be hidden behind any other element) ## Invisibility matcher `f:isNotVisible` - checks if element is not visible ## Present matcher `f:isPresent` - checks if element is in html code (does not have to be visible) ## Clickable matcher `f:isClickable` - checks if element is clickable ## Not clickable matcher `f:isNotClickable` - checks if element is not clickable ## Attribute matcher `attribute:attributeName:regexName` - allows to check if element has attribute with a name specified by `attributeName` and it has to have a format passing `regexName` For example, if there is an element: `

some value

` you can check if attribute is an number by running: `attribute:custom-attribute:number` ## Regex matcher `r:regexName` - allows you to run a `regexName` against a text value of element Regexes have to be specified inside `regex` directory or be a kakunin built ones: `notEmpty` - there must be a value `number` - must be a number You can add your own matchers. In order to do so please read `Extending Kakunin` section. ## Text matcher `t:text you are looking for` - allows you to check if an element contains a expected text ## Current date matcher `f:currentDate:{format}` - allows you to generate current date, `{format}` is optional, by default `DD-MM-YYYY` ================================================ FILE: website/versioned_docs/version-2.4.0/parallel-testing.md ================================================ --- id: version-2.4.0-parallel-testing title: Parallel testing original_id: parallel-testing --- There is a possibility to run tests in parallel. ## How to execute Use a command `npm run kakunin -- --parallel ` where `number of instances` is a number. Example: - `npm run kakunin -- --chrome --parallel 2` Keep in mind that the merged report is available in the `reports/report/index.html` file. text ## Specify pattern per each instance - `npm run kakunin -- --parallel --pattern --pattern ` Keep in mind that: - the number given in `parallel` must be equal to passed `patterns` - `` is a number of instances of the specified browser - `` is a pattern that is used to specify the list of specs that will be executed in each of the instances ----------------------------------------------------------------------------------- ## Troubleshooting 1. Running more than one instance in `Firefox` is not possible now (fix in-progress). ================================================ FILE: website/versioned_docs/version-2.4.0/performance-testing.md ================================================ --- id: version-2.4.0-performance-testing title: Performance testing original_id: performance-testing --- Performance testing is possible thanks to `browsermob-proxy`. It saves all data from network tab (Google Chrome console) which is generated during the test. There is a possibility to compare `TTFB` value with a maximum given one. `TTFB` (Time to first byte) measures the duration from the client making an HTTP request to the first byte of a response being received by the client's browser. More details can be found in documentation - `Built-in steps` section. # What needs to be done? ## Get started 1. Download `browsermob-proxy` from `https://github.com/lightbody/browsermob-proxy` 2. Navigate in terminal to the catalog 3. Use following command to start the REST API ``` ./browsermob-proxy -port 8887 ``` ## Configuration 1. Add `browsermob-proxy` configuration to `kakunin.conf.js` You can use one of the following methods to configure browsermob-proxy: - `npm run kakunin init -- --advanced` and go through the process - or add it manually to the config file: ```javascript "browserMob": { "serverPort": 8887, "port": 8888, "host": "localhost" } ``` ## Run tests 1. `performance steps` must be used in the scenario where you are testing performance 2. Scenario must have a tag `@performance` 3. Run tests with special parameter: ``` npm run kakunin -- --performance ``` ## Results 1. `.har` files are saved in catalog `reports/performance/*.har` ================================================ FILE: website/versioned_docs/version-2.4.0/quickstart.md ================================================ --- id: version-2.4.0-quickstart title: Quick start original_id: quickstart --- As a quick demonstration of the framework let's test the [React variant of TodoMVC](http://todomvc.com/examples/react/#/) project. Of course other testing other frameworks is possible, you can try it by yourself! ## Install packages In order to install Kakunin you have to make sure that you have installed: ```text node.js - v7.8.0 min JDK Chrome ``` Create directory for your project and enter it ```bash $mkdir my_project cd my_project ``` Initialize JavaScript project ```bash npm init ``` Install dependencies ```bash npm install cross-env protractor webdriver-manager kakunin --save ``` Inside `package.json` file add new script in `scripts` section: ```js ... "scripts": { "kakunin": "cross-env NODE_ENV=prod kakunin" }, ... ``` ## Configure Kakunin Run initialization command ```bash npm run kakunin init ``` Answer literally few questions: ```text What kind of application would you like to test? : otherWeb What is base url? [http://localhost:3000]: http://todomvc.com What kind of email service would you like to use?: none ``` And you're set! Now let's write some test! ## Test the app Create a page object that will contain instructions on how to locate elements in the projects. Create a file `pages/main.js`: ```javascript const { BasePage } = require('kakunin'); class MainPage extends BasePage { constructor() { super(); // define the main url for the page this.url = '/examples/react/#/'; // whole form tag this.addTodoForm = $('.todoapp'); // input field this.todoInput = $('input.new-todo'); // list of currently added todos this.todos = $$('.todo-list .view'); this.todoLabel = by.css('label'); // first todo item in a list this.firstTodoItem = this.todos.get(0); } } module.exports = MainPage; ``` Now that we have prepared the locators, we can start writing our test. Let's test adding new todo item. Create a file named: `features/adding_todo.feature` with the following contents: ```gherkin Feature: Scenario: Adding todo Given I visit the "main" page And I wait for "visibilityOf" of the "addTodoForm" element And the "addTodoForm" element is visible When I fill the "addTodoForm" form with: | todoInput | My new todo | And I press the "enter" key Then there are "equal 1" "todos" elements ``` And that's it! All you have to do now is to run the test and watch the magic happens ;) ```bash npm run kakunin ``` The tests may run quite fast so you might not been able to see that it really works as expected. To check if the todo items has been really added to the list, let's use a simple hack - let's pause the running test right after the todo has been added. To do that, let's upgrade our Scenario. Update the file: ```gherkin Feature: Scenario: Adding todo Given I visit the "main" page And I wait for "visibilityOf" of the "addTodoForm" element And the "addTodoForm" element is visible When I fill the "addTodoForm" form with: | todoInput | My new todo | And I wait for "1" seconds And I press the "enter" key When I fill the "addTodoForm" form with: | todoInput | Another todo item! | And I wait for "1" seconds And I press the "enter" key Then there are "equal 2" "todos" elements Then I wait for "5" seconds ``` As you can see, we've added 1 new step that waits for a second before "pressing" the `enter` key. We've also added a second todo item with a short pause at the end of the test so you can see the changes. If you want to see what can we do more with the TodoMVC project, take a look at the `example` dir, where you'll find a complete set of test for the project. ================================================ FILE: website/versioned_docs/version-2.4.0/steps-debug.md ================================================ --- id: version-2.4.0-steps-debug title: Debug original_id: steps-debug --- # Steps for debugging application: ## `I pause` Pauses tests execution and allows to continue manually by pressing combination of `ctrl+c` inside terminal. --- ## `I wait for ":seconds" seconds` Waits with execution of next step for an amount provided by parameter `:seconds`. --- ## `I start performance monitor mode` It starts performance monitor mode. Keep in mind that REST API must be started on the port which must configured in `kakunin.conf.js` - `serverPort: 8887`. More details can be found in documentation file `performance-testing.md`. --- ## `I save performance report file as "fileName"` It saves `.har` file with a name `fileName` in `reports/performance` catalog. For example: `exampleReport-1511470954552.har` Data is generated during the test - network tab in Chrome Chrome console. Keep in mind: * `I start performance monitor mode` must be used before this step * `browserMob.port` must be configured in `kakunin.conf.js` * `browserMob.host` must be configured in `kakunin.conf.js` More details can be found in documentation file `performance-testing.md`. --- ## `the requests should take a maximum of "maxTiming" milliseconds` It compares every `TTFB` timing value from previously saved `.har` report with a `maxTiming` value. Slow requests are listed in your terminal in red colour. Keep in mind that `I start performance monitor mode` and `I save performance report file as "fileName"` steps must be executed before this one! --- ================================================ FILE: website/versioned_docs/version-2.4.0/steps-elements.md ================================================ --- id: version-2.4.0-steps-elements title: Elements original_id: steps-elements --- # Steps used to interact with elements: ## `I infinitely scroll to the ":elementName" element` Allows to scroll through infinite scroll mechanism. The `:elementName` is a name of a selector for loading trigger. --- ## `I wait for ":expectedConditionName" of the ":elementName" element` Waits till element `:elementName` from `this.currentPage` meets criteria specified by `:expectedConditionName`. You can use any of the Protractor's expected condition: * `visibilityOf` * `invisibilityOf` etc. Read more in Protractor's API documentation. --- ## `I wait for the ":elementName" element to disappear` Waits till element `:elementName` disappears. --- ## `I scroll to the ":elementName" element` Scrolls to element `:elementName` of `this.currentPage`. The element will be on bottom of the page. --- ## `I infinitely scroll to the ":elementName" element` Allows to scroll till `:elementName` is visible. Useful for infinite scrolling functionality. --- ## `I press the ":keyName" key` Performs a key press operation on `:keyName` key. --- ## `I click the ":elementName" element` Performs a click action on element `:elementName` from `this.currentPage' The child element must be specified by `:elementName` and must be available in `this.currentPage`. --- ## `I store the ":elementName" element text as ":variableName" variable` Stores the text from element `:elementName` of `this.currentPage` under the `:variableName` so you can use it later. --- ## `I update the ":elementName" element text as ":variableName" variable` Updates the variable `:variableName` value by value from element `:elementName` of `this.currentPage`. --- ## `I store the ":elementName" element text matched by ":matchingRegex" as ":variableName" variable` Stores the part of the element `:elementName` text, that matches the `:matchingRegex` under the `:variableName` for later use. --- ## `the ":elementName"" element is visible` Checks if element `:elementName` is visible and clickable --- ## `the ":elementName"" element is not visible` Checks if element `:elementName` is available in HTML DOM but is not visible and clickable --- ## `the ":elementName" element is disabled` Checks if element is disabled --- ## `I store table ":tableRow" rows as ":variableName" with columns:` Allows to store a row specified columns from a table `:tableRow` and save it under `:variableName` as an array of objects. This step requires a table of columns elements, for example: ```gherkin I store table "someRow" rows as "someVariable" with columns: | firstName | | lastName | | id | ``` In order to make it work there must be not only array element `this.someRow = $$('.rows')` in `this.currentPage`, but also element `this.firstName = $('.firstName');` and so on. The result of this step is an array of: ```javascript [ [ 'firsRowFirstNameValue', 'firsRowLastNameValue' 'firsRowIdValue' ] ... ] ``` --- ## `there are following elements in table ":elementName":` Allows to check if a child elements of `:elementName` have a specified content. This steps allows you to specify an array of child elements that will be checked against expected values. For example: ```gherkin there are following elements in table "myTable": | id | firstName | lastName | | t:1 | t:Adam | t:Doe | | t:2 | t:John | t:Doe | ``` First row must specify columns elements. Starting from second row we must provide a matchers for each row that must be displayed. This step checks exact match, so if the table has 5 rows, there must be a 5 rows in this table. We can specify only a set of columns (for example if a table has 5 columns, we can specify only 1). --- ## `there are "numberExpression" following elements for element ":elementName":` Allows to check if a child elements of `:elementName` have a specified content. Element should be an array, for example: ```html
1
2
``` for this case the `:elementName` should be specified as `$$('table tr')`. Allows to check if a number of elements is the one that we expect. `numberExpression` is a supported expression from `chai.js` library: * `equal N` where N is a number * `at least N` where N is a number * `above N` where N is a number * `below N` where N is a number * `within N M` where N and M are a numbers and so on. You can check expressions on `chai.js` API dock for BDD. This step requires an array of elements to be checked. For example: ```gherkin there are "equal 5" following elements for element "myList": | viewButton | f:isClickable | | id | r:idRegex | ``` The child elements must be an elements, for example `this.viewButton = $('button.viewButton');`. You can use all kind of matchers here. --- ## `there is element ":elementName" with value ":matcher"` Allows to check if `:elementName` has a value that matches the `:matcher`. --- ## `there is no element ":elementName" with value ":matcherName"` Allows to check if there is no `:elementName` that matches the `:matcher`. --- ## `there are "numberExpression" ":elementName" elements` Allows to check if a number of `:elementName` elements is the same as we expect. `numberExpression` is a supported expression from `chai.js` library: * `equal N` where N is a number * `at least N` where N is a number * `above N` where N is a number * `below N` where N is a number * `within N M` where N and M are a numbers and so on. You can check expressions on `chai.js` API dock for BDD. `:elementName` should be specified as an array, for example: ```html
1
2
``` for this case the `:elementName` should be specified as `$$('table tr')`. --- ## `every ":elementName" element should have the same value for element ":columnElementName"` Allows to check if every row defined by `:elementName` has the same value for a column `:columnElementName`. `:elementName` must be an array of elements `:columnElementName` must be an element, for example: ```html
1
1
``` for this case the `:elementName` should be specified as `$$('table tr')` and we can specify column element `this.myColumn = $('td');`. This allows us to write: `every "myElement" element should have the same value for element "myColumn"` --- ## `the element ":elementName" should have an item with values:` Allows to check if any of the child elements of `:elementName` have a specified content (one matching element is enough). Element should be an array, for example: ```html
1
2
``` for this case the `:elementName` should be specified as `$$('table tr')`. This step requires an array of elements to be checked. For example: ```gherkin the element "myList" should have an item with values: | id | t:1 | ``` The child elements must be an elements, for example `this.id = $('td');`. You can use all kind of matchers here. --- ## `the element ":elementName" should not have an item with values:` Allows to check if the child elements of `:elementName` have a different content than that given in the table. Element should be an array, for example: ```html
1
2
``` for this case the `:elementName` should be specified as `$$('table tr')`. This step requires an array of elements to be checked. For example: ```gherkin the element "myList" should have an item with values: | id | t:does-not-exist | ``` The child elements must be an elements, for example `this.id = $('td');`. You can use all kind of matchers here. --- ## `I drag ":elementDrag" element and drop over ":elementDrop" element` Clicks on `:elementDrag` and moves it onto `:elementDrop` while left mouse button is pressed, and then release it. Note: This step is not working on HTML5! --- ================================================ FILE: website/versioned_docs/version-2.4.0/steps-files.md ================================================ --- id: version-2.4.0-steps-files title: Files original_id: steps-files --- # Steps used to interact with files: ## `the file ":fileName" should be downloaded` Checks if a file with name `:fileName` was downloaded. This step does not support matchers or regular expressions, so the name must be exact match. However you can use variable store here. Let's assume there is a variable `myFile` with a value `super-file` in variable store. You can write `the file "v:myFile.zip" should be downloaded` to check if a file `super-file.zip` was downloaded. --- ## `the file ":fileName" contains table data stored under ":variableName" variable` This step allows you to compare an xls/xlsx file `:fileName` with an existing data stored under `:variableName` variable. The data under `:variableName` must be an array of objects representing each row of file. --- ================================================ FILE: website/versioned_docs/version-2.4.0/steps-forms.md ================================================ --- id: version-2.4.0-steps-forms title: Forms original_id: steps-forms --- # Steps used to fill forms: ## `I fill the ":formName" form with:` Allows to fill the form with the name `:formName` and values provided as an array of inputs and values. The element with name `:formName` must be defined inside the `currentPage` page object. Input and values should be provided as an array for example: ```gherkin I fill the "myForm" form with: | inputElement | value to be typed into field | | textareaElement | value to be typed into textarea | | radioElement | radio value to be selected | | checkboxElement | checkbox label value to be selected | ``` By default we support all basic HTML field types (text inputs, checkboxes, radios, selects, files and textareas) In order to use the default handlers the elements you use as input must follow pattern: For inputs: `this.element = $('input')` - element should point at input you want to fill For textareas: `this.element = $('textarea')` - element should point at textarea you want to fill For file input: `this.element = $('input')` - element should point at input you want to fill and value should a filename of file from `data` directory For selects: `this.element = $('select')` - element should point at select and value should be an value of expected option For radios: `this.element = $$('radio[name="name-of-radio"]')` - element should be an array of all radio input of given name and value should be an value of radio you wish to select For checkboxes: Checkbox should have a html like: ```html ``` `this.element = $$('checkbox[name="name-of-radio"]')` - element should be an array of all checkboxes of given name and value should be a text from label of checkbox you want to fill You can use all kind of transformers to as a values for fields. --- ## `the ":formName" form is filled with:` The same as `I fill the ":formName" form with:` but allows to check if a form is filled with a given set of values. You can use all kind of transformers to as a expected values for fields. The only difference is for file fields. You cannot check uploaded files just like that, however we prepared a special type of handler that allow to check for some information related to a specific file. Let's assume that after upload we display an information with a file name of a uploaded file. You can use a special handler that requires to set a element with a postfix `Uploaded`. This will check if a value of that element is the same as you expected. For example you can write a step like this: ```gherkin the "myform" form is filled with: | myFileUploaded | file.txt | ``` Keep in mind that the element name must end with `Uploaded` for example: `this.myFileUploaded = $('p.some-file')` --- ## `the error messages should be displayed:` Allows you to specify the error messages that should be displayed for a specific elements. This step requires an array of format: ```gherkin the error messages should be displayed: | myElement | my error message | ``` You can use dictionaries in this step as follows: ```gherkin the error messages should be displayed: | myElement | d:dictionaryName:dictionaryKey | ``` --- ================================================ FILE: website/versioned_docs/version-2.4.0/steps-generators.md ================================================ --- id: version-2.4.0-steps-generators title: Generators original_id: steps-generators --- # Steps used to generate values: ## `I generate random ":generator:param:param" as ":variableName"` Allows to generate a random value using the generator specified by `:generator:param:param`. The generator must be defined inside the any of the `generators` directories specified in `kakunin.conf.js` file `default: generators`. If the generator exists, then the value will be saved under the `:variableName` and can be accessed by: * steps using variable store * by calling `variableStore.getVariableValue(:variableName)` * by using variable store transformer on supported steps `v:variableName` --- ================================================ FILE: website/versioned_docs/version-2.4.0/steps-navigation.md ================================================ --- id: version-2.4.0-steps-navigation title: Navigation original_id: steps-navigation --- # Steps used for navigation on page: ## `I visit the ":pageFileName" page` Visits the url of the page object with `:pageFileName` name. In order to make it work we create a page object file with a name of `:pageFileName`. For example in case of: `I visit the "myPage" page` there should be a file `myPage.js` inside the `pages` directory. If we have a page object with a name `somePageObject.js` defined inside `pages` directory then: `Given I visit the "somePageObject" page` will set `this.currentPage` variable to `somePageObject` page and we should end up on `somePageObject` url. --- ## `I visit the ":pageFileName" page with parameters:` The same as `I visit the ":pageFileName" page` except allows to pass url parameters. If url of `myPage` is defined as `this.url = /orders/:orderId/products/:productId` then we can use this step to visit this page by: ```gherkin I visit the "myPage" page with parameters: | orderId | 1 | | productId | 2 | ``` this will result in visiting the `/orders/1/product/2` page. --- ## `the ":pageFileName" page is displayed` Checks if current browser url matches url of `pageFileName` page object. If the url matches expected pattern then `this.currentPage` variable is set to `pageFileName` page object. --- ================================================ FILE: website/versioned_docs/version-2.4.0/transformers.md ================================================ --- id: version-2.4.0-transformers title: Transformers original_id: transformers --- Transformers allow you to transform values passed to form steps. For example a select requires to pass a value `/options/1b30f17e-e445-4d28-a30c-dedad95829ab`. This one is quite unreadable, but with the help of transformers you are able to write it like this: `d:options:someOptionName`. In real-life example it will look similar to: ```gherkin I fill the "myForm" form with: | inputElement | d:someDictionary:someKey | | textareaElement | g:someGenerator | | radioElement | v:someVariableName | | checkboxElement | standard value | ``` There are 3 types of built-in transformers: ## Dictionaries Dictionaries allows you to transform a value A to value B using a simple key->value transformation. You can run a dictionary transformer by providing dictionary prefix `d:`, specifying the dictionary name and key that should be used as a value provider. For example: `d:myDictionaryName:myDictionaryKey` this example assumes that there is a dictionary that supports name `myDictionaryName` and it has `myDictionarKey` key. You can read about dictionaries in `Extending Kakunin` section. ## Generators Generators allows you to generate a value by using a specified generator. This can be done by: `g:generatorName`. If a generator supports parameters then you can specify them by: `g:generatorName:param1:param2:...:paramN` You can read more about generators in `Extending Kakunin` section. ## Variable store Variable store allows you to fill the form with a value that was saved in previous steps of current running scenario. This can be done by: `v:variableName` You can read more about variable store in `Extending Kakunin` section ================================================ FILE: website/versioned_docs/version-2.5.0/steps-elements.md ================================================ --- id: version-2.5.0-steps-elements title: Elements original_id: steps-elements --- # Steps used to interact with elements: ## `I infinitely scroll to the ":elementName" element` Allows to scroll through infinite scroll mechanism. The `:elementName` is a name of a selector for loading trigger. --- ## `I wait for ":expectedConditionName" of the ":elementName" element` Waits till element `:elementName` from `this.currentPage` meets criteria specified by `:expectedConditionName`. You can use any of the Protractor's expected condition: * `visibilityOf` * `invisibilityOf` etc. Read more in Protractor's API documentation. --- ## `I wait for the ":elementName" element to disappear` Waits till element `:elementName` disappears. --- ## `I scroll to the ":elementName" element` Scrolls to element `:elementName` of `this.currentPage`. The element will be on bottom of the page. --- ## `I infinitely scroll to the ":elementName" element` Allows to scroll till `:elementName` is visible. Useful for infinite scrolling functionality. --- ## `I press the ":keyName" key` Performs a key press operation on `:keyName` key. --- ## `I click the ":elementName" element` Performs a click action on element `:elementName` from `this.currentPage' The child element must be specified by `:elementName` and must be available in `this.currentPage`. --- ## `I store the ":elementName" element text as ":variableName" variable` Stores the text from element `:elementName` of `this.currentPage` under the `:variableName` so you can use it later. --- ## `I update the ":elementName" element text as ":variableName" variable` Updates the variable `:variableName` value by value from element `:elementName` of `this.currentPage`. --- ## `I store the ":elementName" element text matched by ":matchingRegex" as ":variableName" variable` Stores the part of the element `:elementName` text, that matches the `:matchingRegex` under the `:variableName` for later use. --- ## `the ":elementName"" element is visible` Checks if element `:elementName` is visible and clickable --- ## `the ":elementName"" element is not visible` Checks if element `:elementName` is available in HTML DOM but is not visible and clickable --- ## `the ":elementName" element is disabled` Checks if element is disabled --- ## `I store table ":tableRow" rows as ":variableName" with columns:` Allows to store a row specified columns from a table `:tableRow` and save it under `:variableName` as an array of objects. This step requires a table of columns elements, for example: ```gherkin I store table "someRow" rows as "someVariable" with columns: | firstName | | lastName | | id | ``` In order to make it work there must be not only array element `this.someRow = $$('.rows')` in `this.currentPage`, but also element `this.firstName = $('.firstName');` and so on. The result of this step is an array of: ```javascript [ [ 'firsRowFirstNameValue', 'firsRowLastNameValue' 'firsRowIdValue' ] ... ] ``` --- ## `there are following elements in table ":elementName":` Allows to check if a child elements of `:elementName` have a specified content. This steps allows you to specify an array of child elements that will be checked against expected values. For example: ```gherkin there are following elements in table "myTable": | id | firstName | lastName | | t:1 | t:Adam | t:Doe | | t:2 | t:John | t:Doe | ``` First row must specify columns elements. Starting from second row we must provide a matchers for each row that must be displayed. This step checks exact match, so if the table has 5 rows, there must be a 5 rows in this table. We can specify only a set of columns (for example if a table has 5 columns, we can specify only 1). --- ## `there are "numberExpression" following elements for element ":elementName":` Allows to check if a child elements of `:elementName` have a specified content. Element should be an array, for example: ```html
1
2
``` for this case the `:elementName` should be specified as `$$('table tr')`. Allows to check if a number of elements is the one that we expect. `numberExpression` is a supported expression from `chai.js` library: * `equal N` where N is a number * `at least N` where N is a number * `above N` where N is a number * `below N` where N is a number * `within N M` where N and M are a numbers and so on. You can check expressions on `chai.js` API dock for BDD. This step requires an array of elements to be checked. For example: ```gherkin there are "equal 5" following elements for element "myList": | viewButton | f:isClickable | | id | r:idRegex | ``` The child elements must be an elements, for example `this.viewButton = $('button.viewButton');`. You can use all kind of matchers here. --- ## `there is element ":elementName" with value ":matcher"` Allows to check if `:elementName` has a value that matches the `:matcher`. --- ## `there is element ":elementName" containing ":matcher" text` Allows to check if `:elementName` contains a text that matches the `:matcher`. --- ## `there is element ":elementName" matching ":matcher" matcher` Allows to check if `:elementName` matches the given type of `:matcher`. For example: ```gherkin there is element "button" matching "isClickable" matcher ``` --- ## `there is element ":elementName" with regex ":matcher"` Allows to check if `:elementName` matches given type of regex. For example: ```gherkin there is element "input" with regex "notEmpty" ``` --- ## `there is no element ":elementName" with value ":matcherName"` Allows to check if there is no `:elementName` that matches the `:matcher`. --- ## `there is no element ":elementName" containing ":matcher" text` Allows to check if `:elementName` doesn't contain a text that matches the `:matcher`. --- ## `there is no element ":elementName" matching ":matcher" matcher` Allows to check if `:elementName` is not matching the given type of `:matcher`. --- ## `there is no element ":elementName" with regex ":matcher"` Allows to check if `:elementName` is not matching given type of regex. --- ## `there are "numberExpression" ":elementName" elements` Allows to check if a number of `:elementName` elements is the same as we expect. `numberExpression` is a supported expression from `chai.js` library: * `equal N` where N is a number * `at least N` where N is a number * `above N` where N is a number * `below N` where N is a number * `within N M` where N and M are a numbers and so on. You can check expressions on `chai.js` API dock for BDD. `:elementName` should be specified as an array, for example: ```html
1
2
``` for this case the `:elementName` should be specified as `$$('table tr')`. --- ## `every ":elementName" element should have the same value for element ":columnElementName"` Allows to check if every row defined by `:elementName` has the same value for a column `:columnElementName`. `:elementName` must be an array of elements `:columnElementName` must be an element, for example: ```html
1
1
``` for this case the `:elementName` should be specified as `$$('table tr')` and we can specify column element `this.myColumn = $('td');`. This allows us to write: `every "myElement" element should have the same value for element "myColumn"` --- ## `the element ":elementName" should have an item with values:` Allows to check if any of the child elements of `:elementName` have a specified content (one matching element is enough). Element should be an array, for example: ```html
1
2
``` for this case the `:elementName` should be specified as `$$('table tr')`. This step requires an array of elements to be checked. For example: ```gherkin the element "myList" should have an item with values: | id | t:1 | ``` The child elements must be an elements, for example `this.id = $('td');`. You can use all kind of matchers here. --- ## `the element ":elementName" should not have an item with values:` Allows to check if the child elements of `:elementName` have a different content than that given in the table. Element should be an array, for example: ```html
1
2
``` for this case the `:elementName` should be specified as `$$('table tr')`. This step requires an array of elements to be checked. For example: ```gherkin the element "myList" should have an item with values: | id | t:does-not-exist | ``` The child elements must be an elements, for example `this.id = $('td');`. You can use all kind of matchers here. --- ## `I drag ":elementDrag" element and drop over ":elementDrop" element` Clicks on `:elementDrag` and moves it onto `:elementDrop` while left mouse button is pressed, and then release it. Note: This step is not working on HTML5! --- ================================================ FILE: website/versioned_sidebars/version-2.4.0-sidebars.json ================================================ { "version-2.4.0-docs": { "Documentation": [ "version-2.4.0-quickstart", "version-2.4.0-index", "version-2.4.0-configuration", "version-2.4.0-how-it-works" ], "Built in mechanisms": [ "version-2.4.0-matchers", "version-2.4.0-transformers", "version-2.4.0-extending" ], "Features": [ "version-2.4.0-cross-browser", "version-2.4.0-parallel-testing", "version-2.4.0-performance-testing", "version-2.4.0-docker" ], "Steps": [ "version-2.4.0-steps-navigation", "version-2.4.0-steps-forms", "version-2.4.0-steps-elements", "version-2.4.0-steps-files", "version-2.4.0-steps-generators", "version-2.4.0-steps-debug" ] } } ================================================ FILE: website/versions.json ================================================ [ "2.5.0", "2.4.0" ]