diff --git a/.github/.metadata.json b/.github/.metadata.json new file mode 100644 index 000000000..f1f6926ab --- /dev/null +++ b/.github/.metadata.json @@ -0,0 +1,33 @@ +{ + "templateVersion": "0.2", + "product": { + "name": "Magento2 Functional Testing Framework (MFTF)", + "description": "MFTF is a framework to write and execute UI Functional tests for Magento 2 projects" + }, + "contacts": { + "team": { + "name": "Adobe Commerce Quality Engineering / Pangolin", + "DL": "GRP-Pangolin", + "slackChannel": "mftf" + } + }, + "ticketTracker": { + "functionalJiraQueue": { + "projectKey": "ACQE", + "component": "Framework - MFTF" + }, + "securityJiraQueue": { + "projectKey": "MAGREQ", + "component": "Test Infrastructure" + } + }, + "productionCodeBranches": ["master"], + "staticScan": { + "enable": true, + "frequency": "monthly", + "customName": "", + "branchesToScan": [ + "develop" + ] + } +} diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 271b66244..b037250ef 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -24,8 +24,16 @@ If there is no response from the contributor for two weeks, the issue is closed. Often when the MFTF team works on reviewing the suggested changes, we will add a label to the issue to indicate to our internal team certain information, like status or who is working the issue. If you’re ever curious what the different labels mean, see the [table][labels] below for an explanation of each one. -Refer to [Magento Contributor Agreement] for detailed information about the License Agreement. -All contributors are required to submit a click-through form to agree to the terms. +## Code Of Conduct + +This project adheres to the Adobe [Code Of Conduct](../CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. +Please report unacceptable behavior to [Grp-opensourceoffice@adobe.com](mailto:Grp-opensourceoffice@adobe.com). + +## Contributor License Agreement + +All third-party contributions to this project must be accompanied by a signed Contributor License Agreement (CLA). +This gives Adobe permission to redistribute your contributions as part of the project. +[Sign our CLA](https://opensource.adobe.com/cla.html). You only need to sign it once. ## Contribution requirements @@ -44,7 +52,7 @@ All contributors are required to submit a click-through form to agree to the ter 7. For large features or changes, [open an issue][issue] to discuss first. This may prevent duplicate or unnecessary effort, and it may gain you some additional contributors. 8. To report a bug, [open an issue][issue], and follow [guidelines about bugfix issues][issue reporting]. -9. All automated tests must pass successfully (all builds on [Travis CI] must be green). +9. All automated tests must pass successfully (all builds must be green). ## Fork a repository @@ -171,4 +179,3 @@ Label| Description [Magento Contributor Agreement]: http://www.magento.com/legaldocuments/mca [MFTF repository]: https://github.com/magento/magento2-functional-testing-framework [open new issue]: https://github.com/magento/magento2-functional-testing-framework/issues/new -[Travis CI]: https://travis-ci.com/magento/magento2-functional-testing-framework/pull_requests diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3ce27ce35..9bcc9ec2b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -12,5 +12,5 @@ - [ ] Pull request has a meaningful description of its purpose - [ ] All commits are accompanied by meaningful commit messages - [ ] All new or changed code is covered with unit/verification tests (if applicable) - - [ ] All automated tests passed successfully (all builds on Travis CI are green) + - [ ] All automated tests passed successfully (all builds are green) - [ ] Changes to Framework doesn't have backward incompatible changes for tests or have related Pull Request with fixes to tests \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 000000000..f860dd5ae --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,153 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +name: CI + +on: [pull_request] + +jobs: + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php-versions: ['8.2', '8.3', '8.4'] + steps: + - uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@master + with: + php-version: ${{ matrix.php-versions }} + extensions: curl, dom, intl, json, openssl + coverage: xdebug + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v2 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + + - name: Install dependencies + if: steps.composer-cache.outputs.cache-hit != 'true' + run: composer install --prefer-dist --no-progress --no-suggest + + - name: Run tests + run: vendor/bin/phpunit --configuration dev/tests/phpunit.xml --testsuite unit --coverage-clover clover.xml + +# - name: Monitor coverage +# if: github.event_name == 'pull_request' +# uses: slavcodev/coverage-monitor-action@1.2.0 +# with: +# github_token: ${{ secrets.GITHUB_TOKEN }} +# clover_file: "clover.xml" +# threshold_alert: 10 +# threshold_warning: 20 + + verification-tests: + name: Verification Tests + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php-versions: ['8.2', '8.3', '8.4'] + steps: + - uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@master + with: + php-version: ${{ matrix.php-versions }} + extensions: curl, dom, intl, json, openssl + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v2 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + + - name: Install dependencies + if: steps.composer-cache.outputs.cache-hit != 'true' + run: composer install --prefer-dist --no-progress --no-suggest + + - name: Run tests + run: vendor/bin/phpunit --configuration dev/tests/phpunit.xml --testsuite verification + + static-tests: + name: Static Tests + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php-versions: ['8.2', '8.3', '8.4'] + steps: + - uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@master + with: + php-version: ${{ matrix.php-versions }} + extensions: curl, dom, intl, json, openssl + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v2 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + + - name: Install dependencies + if: steps.composer-cache.outputs.cache-hit != 'true' + run: composer install --prefer-dist --no-progress --no-suggest + + - name: Run tests + run: bin/static-checks + + functional-tests: + name: Functional Tests + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php-versions: ['8.2', '8.3', '8.4'] + + services: + chrome: + image: selenium/standalone-chrome:3.141.59-zirconium + ports: + - 4444:4444 + steps: + - uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@master + with: + php-version: ${{ matrix.php-versions }} + extensions: curl, dom, intl, json, openssl + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v2 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + + - name: Install dependencies + if: steps.composer-cache.outputs.cache-hit != 'true' + run: composer install --prefer-dist --no-progress --no-suggest + + - name: Run tests + run: bin/functional + + diff --git a/.gitignore b/.gitignore index d6ad9e81c..419218ba4 100755 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,7 @@ dev/tests/mftf.log dev/tests/docs/* dev/tests/_output dev/tests/functional.suite.yml +/v2/ +dev/.credentials.example +dev/tests/.phpunit.result.cache +dev/tests/verification/TestModule/Test/testFile.xml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index fba3b46ae..000000000 --- a/.travis.yml +++ /dev/null @@ -1,18 +0,0 @@ -language: php -php: - - 7.2 - - 7.3 -services: - - docker -before_install: - - docker run -d -p 4444:4444 -v /dev/shm:/dev/shm selenium/standalone-chrome:3.141.59-zirconium -install: composer install --no-interaction --prefer-source -env: - matrix: - - VERIFICATION_TOOL=phpunit-checks - - VERIFICATION_TOOL=static-checks - - VERIFICATION_TOOL=functional -script: - - bin/$VERIFICATION_TOOL -after_success: - - travis_retry php vendor/bin/coveralls diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d1ae20a2..999dbd50f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,598 @@ Magento Functional Testing Framework Changelog ================================================ + +4.8.2 +--------- +### Enhancements +* Bumped brainmaestro/composer-git-hook to ^3.0 +* Bumped nikic/php-parser to 5.1.0 +* Bumped monolog/monolog to 3.7.0 +* Bumped guzzlehttp/guzzle to 7.9.2 + +4.8.1 +--------- +### Enhancements +* Bumped allure-codeception to ^2.4 +* Bumped squizlabs/php_codesniffer to 3.10.1 +* Bumped composer/composer to 2.7.7 +* Bumped monolog/monolog to 3.6.0 +* Bumped spomky-labs/otphp to 11.3.0 +* Bumped aws-sdk-php to 3.314.1 + +### Fixes +* Unneeded reports are shown when MFTF Static tests fail. + +4.8.0 +--------- +### Enhancements +* Bumped phpunit/phpunit to ^10.0 +* Bumped allure-framework/allure-phpunit to ^3 +* Bumped codeception/module-webdriver ^4.0 + +### Fixes +* Fixed Class "Magento\FunctionalTestingFramework\StaticCheck\ActionGroupArgumentsCheck" not found on running vendor/bin/mftf build:project --upgrade. + +4.7.2 +--------- +### Enhancements +* Fail static test when introduced filename does not equal the MFTF object name + contained within. + +4.7.1 +--------- +### Enhancements +* Bumped all symfony dependencies to `^6.0 +* Removed abandoned package codacy/coverage +* Removed abandoned package sebastian/phpcpd +* Bumped monolog/monolog to ^3.0 +* Bumped nikic/php-parser to ^5.0 + +4.7.0 +--------- +### Enhancements +* Bumped all symfony dependencies to `^6.0 +* Unit Test for PTS enabled doesn't apply filter fix + +4.6.1 +--------- +### Enhancements +* Supported setting custom timeout value for `magentoCLI` command via an environment variable `MAGENTO_CLI_WAIT_TIMEOUT`. + +4.6.0 +--------- +### Enhancements +* Added Support for PHP 8.3 and enabled PR checks for PHP 8.3. +* Bumped `symfony/console` dependency to `^6.0`. +* Bumped `laminas/laminas-diactoros` dependency to `^3.0`. +* Added no-ansi option to bin/mftf command. + +### Fixes +* Fixed 8.3 deprecation errors. +* Fixed The build with PTS enabled doesn't apply filter issue. +* Change MFTF command to maintain Magento CLI output. + +4.5.0 +--------- +### Enhancements +* Increase browser resolution to 1920x1080. +* Add metadata to ACQE Repositories. +* Add magento admin password to credentials example. + +### Fixes +* Fixed test failure while running any test suite with an argument. + +4.4.2 +--------- +### Fixes +* Fixed PHP 8.2 deprecation warnings. + +4.4.1 +--------- +* Same as previous release + +4.4.0 +--------- +### Enhancements +* Bumped `doctrine/annotations` dependency to `^2.0`. +* Bumped `squizlabs/php_codesniffer` dependency to `^3.7`. +* Bumped `php-webdriver/webdriver` dependency to `^1.14`. +* Bumped `symfony/string` dependency to `^6.3`. +* Bumped `symfony/dotenv` dependency to `^6.3`. +* Bumped `symfony/finder` dependency to `^6.3`. +* Bumped `symfony/http-foundation` dependency to `^6.3`. +* Bumped `symfony/mime` dependency to `^6.3`. +* Enhanced MFTF Modularity Test with "allow failure list". + +4.3.4 +--------- +### Fixes +* Resolving an issue when test is marked as failed due to Suite after section failure + +4.3.3 +--------- +### Enhancements +* Enhance the details in the testgroupmembership.txt file. + +### Fixes +* Fixed MFTF helpers & actionGroups allow duplicate argument names to be passed. + +4.3.2 +--------- +### Enhancements +* 'bootstrap' argument added to indicate that no additional background processes will be run and the jobs complete in the foreground process. + +### Fixes +* Fixed serialization of weakmap exception thrown for every internal exception after codeception upgrade. +* Fixed suites no longer separated by MFTF Suite. + +4.3.1 +--------- +### Fixes +* Fixed cannot bind closure to scope of internal class Exception. +* Fixed broken Mftf doctor command. + +4.3.0 +--------- +### Enhancements +* Bumped `allure-framework/allure-codeception` dependency to `^2.1`. +* Bumped `codeception/codeception` to `^5.0` and upgraded its dependent packages. +* Replaced Yandex methods with Qameta related methods. +* Created methods for modifying step name and for formatting allure. + +### Fixes +* Fixed all issues and exceptions thrown after codeception upgrade. +* Removed dependency of MagentoAllureAdapter in codeception.yml file. + +4.2.1 +--------- +### Fixes + +* Updated constraint for php-webdriver to restrict pulling versions above 1.14.0 + +4.2.0 +--------- +### Fixes + +* Bumped `allure-framework/allure-codeception` dependency to `^1.5` to fix downstream dependency issues in Magento. + + +4.1.0 +--------- +### Enhancements + +* Dropped Support for PHP 8.0 and disabled PR checks for PHP 8.0 +* Allow MFTF generate minimum possible groups runnable by codeception + +### Fixes + +* Fixed Allure report not generating issue +* MFTF displays an appropriate message for unable to connect to Selenium server + +4.0.1 +--------- +### Fixes + +* Fixed HTML files and images not attached to allure report issue + +4.0.0 +--------- +### Enhancements + +* Added Supported for PHP 8.2 and enabled PR checks for PHP 8.2 +* Dropped Support for PHP 7.4 and disabled PR checks for PHP 7.4 +* Upgraded allure-framework/allure-phpunit to its latest version + +### Fixes + +* MFTF deprecation errors fixes +* Composer downgraded from 2.4 to 2.2 due to lamina issue + +3.12.0 +--------- + +### Fixes +* Removed obsolete docs/directories + +3.11.1 +--------- + +### Fixes + +* Removed environment variable MAGENTO_ADMIN_PASSWORD +* Fixed WaitForElementClickable action cannot be used more than once + +3.11.0 +--------- +### Enhancements +* Composer updated to 2.4.2 version +* Static check for duplicate step keys in action group + + +### Fixes + +* Fixed incorrect MFTF test dependencies path +* Removed PHP 7.3 build check from MFTF PR build as PHP 7.3 is no longer supported +* Fixed fatal error when running generate:tests --config parallel -g + + +3.10.3 +--------- + +### Fixes + +* Chrome settings for potential cost reductions + +3.10.2 +--------- + +### Fixes + +* Fixed admin credentials being output to console in WebAPIAuth +* Fixed links in docs + + +3.10.1 +--------- + +### Fixes + +* Fixed allure reports not generating for composer builds. +* Fixed all MFTF scheduled build not generating allure report. + +3.10.0 +--------- + +### Enhancements +* Updated symfony/console and symfony/process constraints to support latest Symfony LTS (5.4v) +* Updated Symfony related code to support latest Symfony LTS (5.4v). +* Implement rapid times X clicks on UI element in MFTF +* Log MFTF test dependencies +* Unused entity static check +* Updated docs for new location of password +* Remove any remaining usages of Travis CI from MFTF Repo +* Unit tests for GenerateTestFailedCommandTest and RunTestFailedCommandTest + +### Fixes +* Hashicorp Vault PHP lib being instantiated with wrong params + +3.9.0 +--------- + +### Fixes + +* Fixed invalid UTF-8 chars returned from magentoCLI that breaks allure reporting +* Fixed MFTF tests failure without access to S3 +* Removed sub heading Parameters from allure report +* Removed truncation of parameters on running MFTF run:test + +### Enhancements +* MFTF group summary file +* Static check to detect unused entities +* Ability To Run MFTF JSON Configuration From File +* Test generation error on invalid entities +* Update MFTF to not pass NULL into non-nullable arguments +* Static check for created data outside action group +* Deleted the unused images +* CreateData's to allow uniqueness attribute +* Set proper weight for action for config parallel generation +* Test before/after comments in test/allure +* Throw error message if key value pair is not mapped properly in .credentials file + +3.8.0 +--------- + +### Updates: +* Allow MFTF Helpers to Return Data to MFTF Test +* Improve parallel grouping and fix an issue with unbalanced groups +* Added new action WaitForElementClickable + +3.7.3 +--------- + +### Updates: +* Fix encoding issue when secret key contains plus sign +* Adding pagebuilder file upload spinner to loadingMaskLocators + + +3.7.2 +--------- + +### Bug fix: +* Failed tests weren't logged correctly to `failed` file which caused a failure during run:failed command execution + + +3.7.1 +--------- + +### GitHub Pull Requests: +* [#873](https://github.com/magento/magento2-functional-testing-framework/pull/873) -- Add check for isBuiltin method (for PHP 8 compatibility) by @karyna-tsymbal-atwix + +### Updates +* Moved `hoa/console` to suggest section to avoid issues with PHP8.0 +* Update `vlucas/phpdotenv` to the latest versions +* `` encodes special character which caused test failed +* Add filter for groups, now we can generate tests with specific group annotation +* Seprated a `run:failed` command to `generate:failed` and `run:failed` + * `run:failed` command can execute failed tests without need to regenerate failed tests +* Deleting MagentoPwaWebDriver file and moving it to Pwa_tests repo + + +3.7.0 +--------- + +### GitHub Pull Requests: + +* [#842](https://github.com/magento/magento2-functional-testing-framework/pull/842) -- Eliminated AspectMock from FileStorageTest +* [#843](https://github.com/magento/magento2-functional-testing-framework/pull/843) -- Eliminated AspectMock from ObjectExtensionUtilTest +* [#844](https://github.com/magento/magento2-functional-testing-framework/pull/844) -- Eliminated AspectMock from TestObjectHandlerTest +* [#845](https://github.com/magento/magento2-functional-testing-framework/pull/845) -- Eliminated AspectMock from SuiteObjectHandlerTest +* [#846](https://github.com/magento/magento2-functional-testing-framework/pull/846) -- Eliminated AspectMock from ActionGroupObjectTest +* [#847](https://github.com/magento/magento2-functional-testing-framework/pull/847) -- Removed not used mocked object +* [#848](https://github.com/magento/magento2-functional-testing-framework/pull/848) -- Eliminated AspectMock usage from ActionObjectTest +* [#850](https://github.com/magento/magento2-functional-testing-framework/pull/850) -- Eliminated AspectMock from TestGeneratorTest +* [#852](https://github.com/magento/magento2-functional-testing-framework/pull/852) -- Eliminated AspectMock from ModuleResolverTest +* [#853](https://github.com/magento/magento2-functional-testing-framework/pull/853) -- Eliminated AspectMock from PersistedObjectHandlerTest +* [#855](https://github.com/magento/magento2-functional-testing-framework/pull/855) -- Eliminated AspectMock from OperationDataArrayResolverTest +* [#856](https://github.com/magento/magento2-functional-testing-framework/pull/856) -- Eliminated AspectMock from DataExtensionUtilTest +* [#857](https://github.com/magento/magento2-functional-testing-framework/pull/857) -- Eliminated AspectMock from ParallelGroupSorterTest +* [#859](https://github.com/magento/magento2-functional-testing-framework/pull/859) -- Eliminated AspectMock usage from SuiteGeneratorTest +* [#861](https://github.com/magento/magento2-functional-testing-framework/pull/861) -- Eliminated aspect mock from mock module resolver builder +* [#862](https://github.com/magento/magento2-functional-testing-framework/pull/862) -- Eliminated AspectMock where it was imported but never used +* [#863](https://github.com/magento/magento2-functional-testing-framework/pull/863) -- Eliminated AspectMock from MagentoTestCase +* [#864](https://github.com/magento/magento2-functional-testing-framework/pull/864) -- Eliminated AspectMock usage from TestLoggingUtil +* [#865](https://github.com/magento/magento2-functional-testing-framework/pull/865) -- Eliminated aspect mock from object handler uti +* [#866](https://github.com/magento/magento2-functional-testing-framework/pull/866) -- Added access/secret key config parameters +* [#867](https://github.com/magento/magento2-functional-testing-framework/pull/867) -- Added empty query and fragment testing to the UrlFormatterTest +* [#868](https://github.com/magento/magento2-functional-testing-framework/pull/868) -- PHP 8 support - fix code related to changes in CURL +* [#869](https://github.com/magento/magento2-functional-testing-framework/pull/869) -- The squizlabs/php_codesniffer composer dependency has been updated to version 3.6.0 +* [#870](https://github.com/magento/magento2-functional-testing-framework/pull/870) -- Removing the csharpru/vault-php-guzzle6-transport not needed dependency +* [#871](https://github.com/magento/magento2-functional-testing-framework/pull/871) -- Changed loose comparisons into strict +* [#872](https://github.com/magento/magento2-functional-testing-framework/pull/872) -- Fix broken MFTF tests + + 3.6.1 +--------- + +### Enhancements + +* Maintainability + * Updated allure dependencies to pull package from new repo `allure-framework/allure-php-api`. + +3.6.0 +--------- + +### Enhancements + +* Maintainability + * Updated composer dependencies to be PHP 8 compatible with the except of codeception/aspect-mock. + +### GitHub Pull Requests: + +* [#830](https://github.com/magento/magento2-functional-testing-framework/pull/830) -- Add ability to configure multiple OTPs +* [#832](https://github.com/magento/magento2-functional-testing-framework/pull/832) -- Updated monolog/monolog to ^2.2 +* [#833](https://github.com/magento/magento2-functional-testing-framework/pull/833) -- Removed usage of AspectMock in FilesystemTest +* [#834](https://github.com/magento/magento2-functional-testing-framework/pull/834) -- Removed usage of AspectMock in AnnotationsCheckTest +* [#838](https://github.com/magento/magento2-functional-testing-framework/pull/838) -- Removed usage of AspectMock in DeprecatedEntityUsageCheckTest +* [#841](https://github.com/magento/magento2-functional-testing-framework/pull/841) -- Removed usage of AspectMock in GenerationErrorHandlerTest +* [#854](https://github.com/magento/magento2-functional-testing-framework/pull/854) -- Updated "monolog" to the latest version 2.3.1 + +3.5.1 +--------- + +### GitHub Pull Requests: + +* [#825](https://github.com/magento/magento2-functional-testing-framework/pull/825) -- Update allure-codeception in order to support php8 + +3.5.0 +--------- + +### Enhancements + +* Customizability + * Added new `config:parallel --groups` option in `generate:tests` command to generate and split tests/suites into required number of execution time balanced groups. + +### Fixes + +* Added --no-sandbox chrome option in functional suite configuration. + +### GitHub Pull Requests: + +* [#824](https://github.com/magento/magento2-functional-testing-framework/pull/824) -- Fix typo in introduction.md +* [#816](https://github.com/magento/magento2-functional-testing-framework/pull/816) -- Update mftf.md +* [#812](https://github.com/magento/magento2-functional-testing-framework/pull/812) -- Added examples and modified url links in assertions.md + +3.4.0 +--------- + +### Enhancements + +* Maintainability + * Added support for composer 2. + +3.3.0 +--------- + +### Enhancements + +* Usability + * [#817](https://github.com/magento/magento2-functional-testing-framework/pull/817) -- Add support for admin WebAPI token refresh. + +* Maintainability + * [#814](https://github.com/magento/magento2-functional-testing-framework/pull/814) -- Update dependencies in order to make mftf php8 compatible, fix running phpcpd + * [#815](https://github.com/magento/magento2-functional-testing-framework/pull/815) -- Upgrade csharpru/vault-php to 4.1 + +### Fixes + +* Fixed test generation error in a split suite group (--config=parallel) to allow generation of subsequent groups. +* Fixed an issue where test extends from a skipped parent is not properly skipped. + +3.2.1 +--------- + +### Fixes + +* Fixed issue that causes Magento bin/magento to fail when xdebug 3 is used. [GitHub Issue #808](https://github.com/magento/magento2-functional-testing-framework/issues/808) + +### GitHub Pull Requests: + + * [#806](https://github.com/magento/magento2-functional-testing-framework/pull/806) -- Enable an extending entity to overwrite a requiredEntity binding + * [#809](https://github.com/magento/magento2-functional-testing-framework/pull/809) -- Add MFTF documentation for AWS S3 + +3.2.0 +--------- + +### Enhancements + +* Usability + * Introduced error tolerance during test and suite generation. See the [command page](./docs/commands/mftf.md#error-tolerance-during-generation) for details. + Addressed github issue [#276](https://github.com/magento/magento2-functional-testing-framework/issues/276). + +* Maintainability + * Updated annotation static-check to check all required annotations. + +### Fixes + +* Fixed issue where CUSTOM_MODULE_PATHS env variable does not use all paths. +* Fixed issue where run:test only records the last failed test in `failed` file. + +3.1.1 +--------- + +* Traceability + * Removed `travis.yml` and replaced with `.github/workflows/main.yml` + +### Fixes +Fixed issue with XPath locators for waits in MagentoPwaWebDriver. + +3.1.0 +--------- + +### Enhancements + +* Customizability + * Introduced the new `return` action that allows action groups to return a value. See the [actions page](./docs/test/actions.md#return) for details. + * Introduced new MFTF command that invokes `vendor/bin/codecept run`. See the [mftf page](./docs/commands/mftf.md#codeceptrun) for details. + +* Usability + * Introduced new action `pause`, to invoke codeception interactive pause for debugging during test execution. See the [Interactive Pause](./docs/interactive-pause.md) page for details. + * Introduced a new `.env` configuration option `ENABLE_PAUSE`, to enable the new pause feature. + +* Maintainability + * Added a new static check that checks for the usage of the `pause` action. See the [command page](./docs/commands/mftf.md#static-checks) for details. + +* Modularity + * MFTF now supports the use of actions from multiple modules within suites. + +* Traceability + * A deprecation notice is now added at test execution time for deprecated metadata usage. + +### Fixes + +* Fixed issue with suite precondition failure for `createData` with required entity. + +### GitHub Issues/Pull Requests: + + * [#547](https://github.com/magento/magento2-functional-testing-framework/pull/547) -- Fix invalid behavior of MAGENTO_BACKEND_BASE_URL + * [#742](https://github.com/magento/magento2-functional-testing-framework/pull/742) -- Fix Waits In MagentoPwaWebDriver + * [#750](https://github.com/magento/magento2-functional-testing-framework/pull/750) -- Docs: Outlining the difference between Allure severity levels + * [#683](https://github.com/magento/magento2-functional-testing-framework/pull/683) -- Docs: Renamed sample test name with the correct one + * [#691](https://github.com/magento/magento2-functional-testing-framework/pull/691) -- Docs: Branch name updates + * [#678](https://github.com/magento/magento2-functional-testing-framework/pull/678) -- Docs: Command added to modify the web server rewrites configuration + * [#745](https://github.com/magento/magento2-functional-testing-framework/pull/745) -- Docs: Remove invalid sample test name + 3.0.0 ------ +--------- + +### Enhancements + +* Customizability + * Introduced MFTF helpers `` to create custom actions outside of MFTF.[See custom-helpers page for details](./docs/custom-helpers.md) + * Removed deprecated actions `` and ``. + * `` no longer skips a test. Instead, the test is added to the `skip` group. + +* Maintainability + * Added support for PHP 7.4. + * Added support for PHPUnit 9. + * Dropped support for PHP 7.0, 7.1, 7.2. + * Schema updates for test entities to only allow single entity per file except Data and Metadata. + * Support for sub-folders in test modules. + * Removed support to read test entities from `dev/tests/acceptance/tests/functional/Magento/FunctionalTest`. + * Removed file attribute for `` in suiteSchema. + * Removed action `pauseExecution` and added `pause`. [See actions page for details](./docs/test/actions.md#pause) + * Removed action `formatMoney` and added `formatCurrency`. [See actions page for details](./docs/test/actions.md#formatcurrency) + * Improved assertion actions to support PHPUnit 9 changes. [See assertions page for details](./docs/test/assertions.md) + * Added new actions: `assertEqualsWithDelta`, `assertNotEqualsWithDelta`, `assertEqualsCanonicalizing`, `assertNotEqualsCanonicalizing`, `assertEqualsIgnoringCase`, `assertNotEqualsIgnoringCase`. + * Added new actions: `assertStringContainsString`, `assertStringNotContainsString`, `assertStringContainsStringIgnoringCase`, `assertStringNotContainsStringIgnoringCase` for string haystacks. + * Removed actions: `assertInternalType`, `assertNotInternalType`, `assertArraySubset`. + * Removed delta option from `assertEquals` and `assertNotEquals`. + * Added static check `deprecatedEntityUsage` that checks and reports references to deprecated test entities. + * Added static check `annotations` that checks and reports missing annotations in tests. + * Updated `bin/mftf static-checks` command to allow executing static-checks defined in `staticRuleSet.json` by default. [See command page for details](./docs/commands/mftf.md#static-checks) + * Added support for Two-Factor Authentication (2FA). [See configure-2fa page for details](./docs/configure-2fa.md) + * Added new upgrade script to remove unused arguments from action groups. + * `mftf.log` no longer includes notices and warnings at test execution time. + * Added unhandledPromptBehavior driver capability for Chrome 75+ support. + * Added the Chrome option `--ignore-certificate-errors` to `functional.suite.dist.yml`. + +* Traceability + * Removed `--debug` option `NONE` to disallow ability to turn off schema validation. + * Notices added for test entity naming convention violations. + * Metadata file names changed to `*Meta.xml`. + * Introduced new `.env` configuration `VERBOSE_ARTIFACTS` to toggle saving attachments in Allure. [See configuration page for details](./docs/configuration.md) + * Changed the `bin/mftf static-checks` error file directory from the current working directory to `TESTS_BP/tests/_output/static-results/`. + +* Readability + * Support only nested assertion syntax [See assertions page for details](./docs/test/assertions.md). + * Documented [3.0.0 Backward Incompatible Changes](./docs/backward-incompatible-changes.md). + * Removed blacklist/whitelist terminology in MFTF. + +* Upgrade scripts added to upgrade tests to MFTF major version requirements. See upgrade instructions below. +* Bumped dependencies to support PHP/PHPUnit upgrade. + ### Upgrade Instructions + * Run `bin/mftf reset --hard` to remove old generated configurations. * Run `bin/mftf build:project` to generate new configurations. * Run `bin/mftf upgrade:tests`. [See command page for details](./docs/commands/mftf.md#upgradetests). * After running the above command, some tests may need manually updates: - * Remove all occurrences of `` and `` - * Remove all occurrences of `` from any ``s + * Remove all occurrences of `` and ``. + * Remove all occurrences of `` from any ``s. * Ensure all `` actions in your tests have a valid schema. * Lastly, try to generate all tests. Tests should all be generated as a result of the upgrades. * If not, the most likely issue will be a changed XML schema. Check error messaging and search your codebase for the attributes listed. +### Fixes + +* Throw exception during generation when leaving out .url for `amOnPage`. +* `request_timeout` and `connection_timeout` added to functional.suite.yml.dist. +* Fixed `ModuleResolver` to resolve test modules moved out of deprecated path. +* Fixed issue of resolving arguments of type `entity` in action groups within a custom helper. +* Fixed reporting issue in output file for `testDependencies` static check. +* Fixed a bug in `actionGroupArguments` static check when action group filename is missing `ActionGroup`. +* Fixed issue of running suites under root `_suite` directory in Standalone MFTF. +* Fixed issue with custom helper usage in suites. +* Fixed issue with decryption of secrets during data entity creation. +* Fixed issue with merging of `array` items in data entity. +* Fixed issue where an extended data entity would not merge array items. Array items should merge properly now. +* Fixed issue where Chrome remains running after MFTF suite finishes. +* Fixed javascript error seen on Chrome 83 for dragAndDrop action. +* Fixed allure issue when `WebDriverCurlException` is encountered in `afterStep`. + +### GitHub Issues/Pull Requests + +* [#567](https://github.com/magento/magento2-functional-testing-framework/pull/567) -- log attachments for failed requests. + +### Demo Video links + +* [MFTF 3.0.0 RC1](https://www.youtube.com/watch?v=z0ZaZCmnw-A&t=2s) +* [MFTF 3.0.0 RC2](https://www.youtube.com/watch?v=BJOQAw6dX5o) +* [MFTF 3.0.0 RC3](https://www.youtube.com/watch?v=scLb7pi8pR0) + +2.6.4 +----- + +### Fixes +* added dependency to packages MFTF used but never specified in composer.json + 2.6.3 ----- @@ -52,7 +632,7 @@ Magento Functional Testing Framework Changelog * Page * Section * Section Element - * See DevDocs for details + * See DevDocs for details * Improved `mftf static-checks` command to allow executing all or specific static checks. * Added a new static check that checks and reports unused arguments in action groups. * Customizability @@ -77,9 +657,9 @@ Magento Functional Testing Framework Changelog * Command verifies and troubleshoots some configuration steps required for running tests * Please see DevDocs for more details * `<*Data>` actions now contain `API Endpoint` and `Request Header` artifacts. - * Introduced new `.env` configurations `ENABLE_BROWSER_LOG` and `BROWSER_LOG_BLACKLIST` + * Introduced new `.env` configurations `ENABLE_BROWSER_LOG` and `BROWSER_LOG_BLOCKLIST` * Configuration enables allure artifacts for browser log entries if they are present after the step. - * Blacklist filters out logs from specific sources. + * Blocklist filters out logs from specific sources. * Customizability * Introduced `timeout=""` to `magentoCLI` actions. @@ -104,12 +684,12 @@ Magento Functional Testing Framework Changelog ----- * Traceability - * Allure report enhanced to display file path of tests. + * Allure report enhanced to display file path of tests. * Maintainability * Added support to read MFTF test entities from `dev/tests/acceptance/tests/functional///*` * Removed path deprecation warning from `ModuleResolver`. * Refactored problem methods to reduce cyclomatic complexity. - + ### Fixes * Fixed issue with builds due to absence of AcceptanceTester class. @@ -176,7 +756,7 @@ Magento Functional Testing Framework Changelog * Customizability * Use of `_CREDS` has been extended to `` and `` actions * Traceability - * A Test step is now generated and injected in the allure report when a test is reported as `BROKEN`. + * A Test step is now generated and injected in the allure report when a test is reported as `BROKEN`. ### Fixes * `static-checks` command now properly returns `1` if any static check failed. @@ -278,7 +858,7 @@ Magento Functional Testing Framework Changelog ### Enhancements * Maintainability * Added new `mftf run:failed` commands, which reruns all failed tests from last run configuration. - + ### Fixes * Fixed an issue where mftf would fail to parse test materials for extensions installed under `vendor`. * Fixed a Windows compatibility issue around the use of Magento's `ComponentRegistrar` to aggregate paths. @@ -367,15 +947,15 @@ Magento Functional Testing Framework Changelog 2.3.1 ----- -### Enhancements +### Enhancements * Maintainability * `mftf build:project` now copies over the `command.php` file into the parent Magento installation, if detected. 2.3.0 ----- -### Enhancements +### Enhancements * Traceability - * MFTF now outputs generation run-time information, warnings, and errors to an `mftf.log` file. + * MFTF now outputs generation run-time information, warnings, and errors to an `mftf.log` file. * Overall error messages for various generation errors have been improved. Usage of the `--debug` flag provides file-specific errors for all XML-related errors. * Allure Reports now require a unique `story` and `title` combination, to prevent collisions in Allure Report generation. * The `features` annotation now ignores user input and defaults to the module the test lives under (for clear Allure organization). @@ -426,7 +1006,7 @@ Magento Functional Testing Framework Changelog 2.2.0 ----- -### Enhancements +### Enhancements * Traceability * Javascript errors are now logged and reported in test output. * Test failures are no longer overwritten by failures in an `` hook. @@ -503,7 +1083,7 @@ Magento Functional Testing Framework Changelog ----- ### Enhancements * Traceability - * Severity in `` tags now properly reflect Magento severity values. + * Severity in `` tags now properly reflect Magento severity values. * Modularity * Added ability to pass in simple values to actionGroups via addition of `type` attribute in actionGroup `` declaration. * For examples, see devdocs article on actionGroups. @@ -539,7 +1119,7 @@ Magento Functional Testing Framework Changelog * Added the ability to refer to `custom_attribute` data in persisted entities via `key` instead of index. * ex. `url_key` in category entity: `$category.custom_attributes[3][value]$` and `$category.custom_attributes[url_key]$` are both valid. * Maintainability - * Added check for duplicate `stepKey` attributes at test generation. This check is scoped to `` tags within a single `` tag in a single file. + * Added check for duplicate `stepKey` attributes at test generation. This check is scoped to `` tags within a single `` tag in a single file. ### Fixes * Fixed inability to use `` with `` in test hooks. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..549b492a0 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Adobe Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language. +* Being respectful of differing viewpoints and experiences. +* Gracefully accepting constructive criticism. +* Focusing on what is best for the community. +* Showing empathy towards other community members. + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances. +* Trolling, insulting/derogatory comments, and personal or political attacks. +* Public or private harassment. +* Publishing others' private information, such as a physical or electronic + address, without explicit permission. +* Other conduct which could reasonably be considered inappropriate in a + professional setting. + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at Grp-opensourceoffice@adobe.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [https://contributor-covenant.org/version/1/4][version]. + +[homepage]: https://contributor-covenant.org +[version]: https://contributor-covenant.org/version/1/4/ \ No newline at end of file diff --git a/README.md b/README.md index fc42f0603..02a2b0cd5 100755 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # Magento Functional Testing Framework (MFTF) -[![Build Status](https://travis-ci.org/magento/magento2-functional-testing-framework.svg?branch=develop)](https://travis-ci.org/magento/magento2-functional-testing-framework) [![Coverage Status](https://coveralls.io/repos/github/magento/magento2-functional-testing-framework/badge.svg?branch=develop)](https://coveralls.io/github/magento/magento2-functional-testing-framework) - ---- ## Installation @@ -16,7 +14,7 @@ Learn more about contributing in our [Contribution Guidelines][]. If you want to participate in the documentation work, see [DevDocs Contributing][]. -### Labels applied by the MFTF team +### Labels Applied by the MFTF Team Refer to the tables with descriptions of each label below. These labels are applied by the MFTF development team to community contributed issues and pull requests, to communicate status, impact, or which team is working on it. @@ -53,12 +51,10 @@ These labels are applied by the MFTF development team to community contributed i | **bugfix** | The issue or pull request relates to bug fixing. | | **enhancement** | The issue or pull request that raises the MFTF to a higher degree (for example new features, optimization, refactoring, etc). | -## Reporting security issues +## Reporting Security Issues -To report security vulnerabilities and other security issues in the Magento software or web sites, send an email with the report at [security@magento.com][]. -Do not report security issues using GitHub. -Be sure to encrypt your e-mail with our [encryption key][] if it includes sensitive information. -Learn more about reporting security issues [here][]. +To report security vulnerabilities or learn more about reporting security issues in Magento software or web sites visit the [Magento Bug Bounty Program](https://hackerone.com/magento) on hackerone. +Please create a hackerone account [there](https://hackerone.com/magento) to submit and follow-up on your issue. Stay up-to-date on the latest security news and patches for Magento by signing up for [Security Alert Notifications][]. @@ -66,15 +62,12 @@ Stay up-to-date on the latest security news and patches for Magento by signing u Each Magento source file included in this distribution is licensed under AGPL 3.0. -See the license [here][] or contact [license@magentocommerce.com][] for a copy. +See the [license][] or contact [license@magentocommerce.com][] for a copy. [Getting Started]: docs/getting-started.md -[Contribution Guidelines]: https://github.com/magento/magento2-functional-testing-framework/blob/develop/.github/CONTRIBUTING.md +[Contribution Guidelines]: .github/CONTRIBUTING.md [DevDocs Contributing]: https://github.com/magento/devdocs/blob/master/.github/CONTRIBUTING.md -[security@magento.com]: mailto:security@magento.com -[encryption key]: https://info2.magento.com/rs/magentoenterprise/images/security_at_magento.asc -[here]: https://magento.com/security/reporting-magento-security-issue [Security Alert Notifications]: https://magento.com/security/sign-up -[here]: LICENSE_AGPL3.txt +[license]: LICENSE_AGPL3.txt [license@magentocommerce.com]: mailto:license@magentocommerce.com diff --git a/_config.yml b/_config.yml new file mode 100644 index 000000000..fc24e7a62 --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-hacker \ No newline at end of file diff --git a/bin/blacklist.txt b/bin/blocklist.txt similarity index 96% rename from bin/blacklist.txt rename to bin/blocklist.txt index 39b3a6700..6d9f6f4e2 100644 --- a/bin/blacklist.txt +++ b/bin/blocklist.txt @@ -3,7 +3,7 @@ # # # THIS FILE CANNOT CONTAIN BLANK LINES # ################################################################### -bin/blacklist.txt +bin/blocklist.txt dev/tests/static/Magento/Sniffs/Commenting/FunctionCommentSniff.php dev/tests/static/Magento/Sniffs/Commenting/VariableCommentSniff.php dev/tests/verification/_generated diff --git a/bin/copyright-check b/bin/copyright-check index 79fe8df06..dc9976d77 100755 --- a/bin/copyright-check +++ b/bin/copyright-check @@ -5,13 +5,13 @@ FILE_EXTENSIONS='.php\|.xml\|.xsd' -BLACKLIST='bin/blacklist.txt' +BLOCKLIST='bin/blocklist.txt' RESULT='' # Iterate through the list of tracked files # that have the expected extensions # that are not ignored -for i in `git ls-tree --full-tree -r --name-only HEAD | grep $FILE_EXTENSIONS | grep -v -f $BLACKLIST` +for i in `git ls-tree --full-tree -r --name-only HEAD | grep $FILE_EXTENSIONS | grep -v -f $BLOCKLIST` do if echo `cat $i` | grep -q -v "Copyright © Magento, Inc. All rights reserved."; then # Copyright is missing diff --git a/bin/copyright-check.bat b/bin/copyright-check.bat index 9f2e23bfb..0a7612f77 100644 --- a/bin/copyright-check.bat +++ b/bin/copyright-check.bat @@ -3,7 +3,7 @@ @echo off SETLOCAL EnableDelayedExpansion -SET BLACKLIST_FILE=bin/blacklist.txt +SET BLOCKLIST_FILE=bin/blocklist.txt SET i=0 FOR /F %%x IN ('git ls-tree --full-tree -r --name-only HEAD') DO ( @@ -12,14 +12,14 @@ FOR /F %%x IN ('git ls-tree --full-tree -r --name-only HEAD') DO ( if "%%~xx"==".xml" set GOOD_EXT=1 if "%%~xx"==".xsd" set GOOD_EXT=1 IF DEFINED GOOD_EXT ( - SET BLACKLISTED= - FOR /F "tokens=* skip=5" %%f IN (%BLACKLIST_FILE%) DO ( + SET BLOCKLISTED= + FOR /F "tokens=* skip=5" %%f IN (%BLOCKLIST_FILE%) DO ( SET LINE=%%x IF NOT "!LINE!"=="!LINE:%%f=!" ( - SET BLACKLISTED=1 + SET BLOCKLISTED=1 ) ) - IF NOT DEFINED BLACKLISTED ( + IF NOT DEFINED BLOCKLISTED ( FIND "Copyright © Magento, Inc. All rights reserved." %%x >nul IF ERRORLEVEL 1 ( SET /A i+=1 diff --git a/bin/functional b/bin/functional index 99377337e..63bb41197 100755 --- a/bin/functional +++ b/bin/functional @@ -4,7 +4,9 @@ set -e echo "===============================" -echo " EXECUTE DevDocsTest " +echo " EXECUTE Functional Tests " echo "===============================" bin/mftf build:project +bin/mftf run:test DeprecatedDevDocsTest -f bin/mftf run:test DevDocsTest -f +bin/mftf run:test FormatCurrencyTest -f \ No newline at end of file diff --git a/bin/static-checks b/bin/static-checks index c711726e4..b6733ba25 100755 --- a/bin/static-checks +++ b/bin/static-checks @@ -16,8 +16,6 @@ echo "" echo "===============================" echo " COPY PASTE DETECTOR" echo "===============================" -vendor/bin/phpcpd ./src -echo "" echo "===============================" echo " MESS DETECTOR" diff --git a/composer.json b/composer.json index a66ce80bb..f0ea832cb 100755 --- a/composer.json +++ b/composer.json @@ -2,63 +2,62 @@ "name": "magento/magento2-functional-testing-framework", "description": "Magento2 Functional Testing Framework", "type": "library", - "version": "3.0.0", + "version": "4.8.2", "license": "AGPL-3.0", "keywords": ["magento", "automation", "functional", "testing"], "config": { "sort-packages": true }, "require": { - "php": "~7.2.0||~7.3.0", + "allure-framework/allure-codeception": "^2.1", + "aws/aws-sdk-php": "^3.132", + "codeception/codeception": "^5.0", + "codeception/module-asserts": "^3.0", + "codeception/module-webdriver": "^4.0", + "composer/composer": "^1.9||^2.0,!=2.2.16", + "csharpru/vault-php": "^4.2.1", "ext-curl": "*", "ext-dom": "*", + "ext-iconv": "*", + "ext-intl": "*", "ext-json": "*", "ext-openssl": "*", - "allure-framework/allure-codeception": "~1.3.0", - "aws/aws-sdk-php": "^3.132", - "codeception/codeception": "~2.4.5", - "composer/composer": "^1.6", - "csharpru/vault-php": "~3.5.3", - "csharpru/vault-php-guzzle6-transport": "^2.0", - "monolog/monolog": "^1.0", + "guzzlehttp/guzzle": "^7.3.0", + "laminas/laminas-diactoros": "^3.0", + "monolog/monolog": "^2.3||^3.0", "mustache/mustache": "~2.5", - "php-webdriver/webdriver": "^1.8.0", - "symfony/console": "^4.4", - "symfony/finder": "^4.4", - "symfony/mime": "^5.0", - "symfony/process": "^4.4", - "vlucas/phpdotenv": "^2.4" + "nikic/php-parser": "^4.4||^5.0", + "php": ">=8.2", + "php-webdriver/webdriver": "^1.14.0", + "spomky-labs/otphp": "^10.0||^11.0", + "symfony/console": "^6.4", + "symfony/dotenv": "^6.4", + "symfony/finder": "^6.4", + "symfony/mime": "^6.4", + "symfony/process": "^6.4", + "weew/helpers-array": "^1.3" }, "require-dev": { - "squizlabs/php_codesniffer": "~3.2", - "sebastian/phpcpd": "~3.0 || ~4.0", - "brainmaestro/composer-git-hooks": "^2.3.1", - "doctrine/cache": "<1.7.0", - "codeception/aspect-mock": "^3.0", - "goaop/framework": "2.2.0", - "codacy/coverage": "^1.4", - "phpmd/phpmd": "^2.6.0", - "phpunit/phpunit": "~6.5.0 || ~7.0.0", - "rregeer/phpunit-coverage-check": "^0.1.4", - "php-coveralls/php-coveralls": "^1.0", - "symfony/stopwatch": "~3.4.6" + "brainmaestro/composer-git-hooks": "^3.0", + "php-coveralls/php-coveralls": "^1.0||^2.2", + "phpmd/phpmd": "^2.8.0", + "phpunit/phpunit": "^10.0", + "squizlabs/php_codesniffer": "~3.10.1" }, "suggest": { - "epfremme/swagger-php": "^2.0" - }, - "replace": { - "facebook/webdriver": "^1.7.1" + "hoa/console": "Enables action and interactive console functionality" }, "autoload": { "files": ["src/Magento/FunctionalTestingFramework/_bootstrap.php"], "psr-4": { "Magento\\FunctionalTestingFramework\\": "src/Magento/FunctionalTestingFramework", - "MFTF\\": "dev/tests/functional/tests/MFTF" + "MFTF\\": "dev/tests/functional/tests/MFTF", + "Magento\\FunctionalTestingFramework\\Tests\\Verification\\": "dev/tests/verification/Tests" } }, "autoload-dev": { "psr-4": { - "tests\\unit\\": "dev/tests/unit" + "tests\\": "dev/tests" } }, "scripts": { diff --git a/composer.lock b/composer.lock index bbab8dbd4..40c23e003 100644 --- a/composer.lock +++ b/composer.lock @@ -4,33 +4,39 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8ad8319a9bd27934c8bdc036729d4fac", + "content-hash": "358c40222c9d8cc40ba6d26bfd485c5e", "packages": [ { "name": "allure-framework/allure-codeception", - "version": "1.3.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/allure-framework/allure-codeception.git", - "reference": "9d31d781b3622b028f1f6210bc76ba88438bd518" + "reference": "854320894b5e65952eb0cafd1555e9efb4543350" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/allure-framework/allure-codeception/zipball/9d31d781b3622b028f1f6210bc76ba88438bd518", - "reference": "9d31d781b3622b028f1f6210bc76ba88438bd518", + "url": "https://api.github.com/repos/allure-framework/allure-codeception/zipball/854320894b5e65952eb0cafd1555e9efb4543350", + "reference": "854320894b5e65952eb0cafd1555e9efb4543350", "shasum": "" }, "require": { - "allure-framework/allure-php-api": "~1.1.0", - "codeception/codeception": "~2.1", - "php": ">=5.4.0", - "symfony/filesystem": ">=2.6", - "symfony/finder": ">=2.6" + "allure-framework/allure-php-commons": "^2.3.1", + "codeception/codeception": "^5.0.3", + "ext-json": "*", + "php": "^8" + }, + "require-dev": { + "psalm/plugin-phpunit": "^0.19.0", + "remorhaz/php-json-data": "^0.5.3", + "remorhaz/php-json-path": "^0.7.7", + "squizlabs/php_codesniffer": "^3.7.2", + "vimeo/psalm": "^5.12" }, "type": "library", "autoload": { - "psr-0": { - "Yandex": "src/" + "psr-4": { + "Qameta\\Allure\\Codeception\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -40,12 +46,17 @@ "authors": [ { "name": "Ivan Krutov", - "email": "vania-pooh@yandex-team.ru", + "email": "vania-pooh@aerokube.com", + "role": "Developer" + }, + { + "name": "Edward Surov", + "email": "zoohie@gmail.com", "role": "Developer" } ], - "description": "A Codeception adapter for Allure report.", - "homepage": "http://allure.qatools.ru/", + "description": "Allure Codeception integration", + "homepage": "https://allurereport.org/", "keywords": [ "allure", "attachments", @@ -55,38 +66,49 @@ "steps", "testing" ], - "time": "2018-12-18T19:47:23+00:00" + "support": { + "email": "allure@qameta.io", + "issues": "https://github.com/allure-framework/allure-codeception/issues", + "source": "https://github.com/allure-framework/allure-codeception" + }, + "time": "2024-05-28T09:54:01+00:00" }, { - "name": "allure-framework/allure-php-api", - "version": "1.1.8", + "name": "allure-framework/allure-php-commons", + "version": "v2.3.1", "source": { "type": "git", - "url": "https://github.com/allure-framework/allure-php-commons.git", - "reference": "5ae2deac1c7e1b992cfa572167370de45bdd346d" + "url": "https://github.com/allure-framework/allure-php-commons2.git", + "reference": "5d7ed5ab510339652163ca1473eb499d4b7ec488" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/allure-framework/allure-php-commons/zipball/5ae2deac1c7e1b992cfa572167370de45bdd346d", - "reference": "5ae2deac1c7e1b992cfa572167370de45bdd346d", + "url": "https://api.github.com/repos/allure-framework/allure-php-commons2/zipball/5d7ed5ab510339652163ca1473eb499d4b7ec488", + "reference": "5d7ed5ab510339652163ca1473eb499d4b7ec488", "shasum": "" }, "require": { - "jms/serializer": "^0.16 || ^1.0", - "php": ">=5.4.0", - "ramsey/uuid": "^3.0", - "symfony/http-foundation": "^2.0 || ^3.0 || ^4.0 || ^5.0" + "doctrine/annotations": "^1.12 || ^2", + "ext-json": "*", + "php": "^8", + "psr/log": "^1 || ^2 || ^3", + "ramsey/uuid": "^3 || ^4" + }, + "conflict": { + "amphp/byte-stream": "<1.5.1" }, "require-dev": { - "phpunit/phpunit": "^4.0.0" + "jetbrains/phpstorm-attributes": "^1", + "phpunit/phpunit": "^9.6.8", + "psalm/plugin-phpunit": "^0.18.4", + "squizlabs/php_codesniffer": "^3.7.2", + "vimeo/psalm": "^5.12" }, "type": "library", "autoload": { - "psr-0": { - "Yandex": [ - "src/", - "test/" - ] + "psr-4": { + "Qameta\\Allure\\": "src", + "Yandex\\Allure\\Adapter\\": "src/Legacy" } }, "notification-url": "https://packagist.org/downloads/", @@ -95,59 +117,130 @@ ], "authors": [ { - "name": "Ivan Krutov", - "email": "vania-pooh@yandex-team.ru", + "name": "Dmitry Baev", + "email": "baev.dm@gmail.com", + "role": "Developer" + }, + { + "name": "Edward Surov", + "email": "zoohie@gmail.com", "role": "Developer" } ], - "description": "PHP API for Allure adapter", + "description": "Allure PHP commons", "homepage": "http://allure.qatools.ru/", "keywords": [ "allure", - "api", + "commons", "php", - "report" + "report", + "testing" + ], + "support": { + "email": "allure@qameta.io", + "issues": "https://github.com/allure-framework/allure-php-commons2/issues", + "source": "https://github.com/allure-framework/allure-php-commons" + }, + "time": "2023-05-30T10:55:43+00:00" + }, + { + "name": "aws/aws-crt-php", + "version": "v1.2.7", + "source": { + "type": "git", + "url": "https://github.com/awslabs/aws-crt-php.git", + "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/d71d9906c7bb63a28295447ba12e74723bd3730e", + "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35||^5.6.3||^9.5", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality." + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "AWS SDK Common Runtime Team", + "email": "aws-sdk-common-runtime@amazon.com" + } + ], + "description": "AWS Common Runtime for PHP", + "homepage": "https://github.com/awslabs/aws-crt-php", + "keywords": [ + "amazon", + "aws", + "crt", + "sdk" ], - "time": "2020-03-13T10:47:35+00:00" + "support": { + "issues": "https://github.com/awslabs/aws-crt-php/issues", + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.7" + }, + "time": "2024-10-18T22:15:13+00:00" }, { "name": "aws/aws-sdk-php", - "version": "3.133.38", + "version": "3.339.7", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "5ec9442162d83f94918bc17136a2b674a04784e5" + "reference": "7b7e48ce7970c0416c5fda045df7b93948fbf643" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/5ec9442162d83f94918bc17136a2b674a04784e5", - "reference": "5ec9442162d83f94918bc17136a2b674a04784e5", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7b7e48ce7970c0416c5fda045df7b93948fbf643", + "reference": "7b7e48ce7970c0416c5fda045df7b93948fbf643", "shasum": "" }, "require": { + "aws/aws-crt-php": "^1.2.3", "ext-json": "*", "ext-pcre": "*", "ext-simplexml": "*", - "guzzlehttp/guzzle": "^5.3.3|^6.2.1|^7.0", - "guzzlehttp/promises": "^1.0", - "guzzlehttp/psr7": "^1.4.1", - "mtdowling/jmespath.php": "^2.5", - "php": ">=5.5" + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/promises": "^2.0", + "guzzlehttp/psr7": "^2.4.5", + "mtdowling/jmespath.php": "^2.8.0", + "php": ">=8.1", + "psr/http-message": "^2.0" }, "require-dev": { "andrewsville/php-token-reflection": "^1.4", "aws/aws-php-sns-message-validator": "~1.0", "behat/behat": "~3.0", + "composer/composer": "^2.7.8", + "dms/phpunit-arraysubset-asserts": "^0.4.0", "doctrine/cache": "~1.4", "ext-dom": "*", "ext-openssl": "*", "ext-pcntl": "*", "ext-sockets": "*", - "nette/neon": "^2.3", - "phpunit/phpunit": "^4.8.35|^5.4.3", - "psr/cache": "^1.0", - "psr/simple-cache": "^1.0", - "sebastian/comparator": "^1.2.3" + "paragonie/random_compat": ">= 2", + "phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5", + "psr/cache": "^2.0 || ^3.0", + "psr/simple-cache": "^2.0 || ^3.0", + "sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0", + "symfony/filesystem": "^v6.4.0 || ^v7.1.0", + "yoast/phpunit-polyfills": "^2.0" }, "suggest": { "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", @@ -163,11 +256,14 @@ } }, "autoload": { + "files": [ + "src/functions.php" + ], "psr-4": { "Aws\\": "src/" }, - "files": [ - "src/functions.php" + "exclude-from-classmap": [ + "src/data/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -192,29 +288,34 @@ "s3", "sdk" ], - "time": "2020-03-17T18:16:01+00:00" + "support": { + "forum": "https://github.com/aws/aws-sdk-php/discussions", + "issues": "https://github.com/aws/aws-sdk-php/issues", + "source": "https://github.com/aws/aws-sdk-php/tree/3.339.7" + }, + "time": "2025-02-05T19:06:15+00:00" }, { "name": "behat/gherkin", - "version": "v4.6.2", + "version": "v4.11.0", "source": { "type": "git", "url": "https://github.com/Behat/Gherkin.git", - "reference": "51ac4500c4dc30cbaaabcd2f25694299df666a31" + "reference": "32821a17b12620951e755b5d49328a6421a5b5b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Gherkin/zipball/51ac4500c4dc30cbaaabcd2f25694299df666a31", - "reference": "51ac4500c4dc30cbaaabcd2f25694299df666a31", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/32821a17b12620951e755b5d49328a6421a5b5b5", + "reference": "32821a17b12620951e755b5d49328a6421a5b5b5", "shasum": "" }, "require": { - "php": ">=5.3.1" + "php": "8.1.* || 8.2.* || 8.3.* || 8.4.*" }, "require-dev": { - "phpunit/phpunit": "~4.5|~5", - "symfony/phpunit-bridge": "~2.7|~3|~4", - "symfony/yaml": "~2.3|~3|~4" + "cucumber/cucumber": "dev-gherkin-24.1.0", + "phpunit/phpunit": "^9.6", + "symfony/yaml": "^5.4 || ^6.4 || ^7.0" }, "suggest": { "symfony/yaml": "If you want to parse features, represented in YAML files" @@ -222,7 +323,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.4-dev" + "dev-master": "4.x-dev" } }, "autoload": { @@ -241,7 +342,7 @@ "homepage": "http://everzet.com" } ], - "description": "Gherkin DSL parser for PHP 5.3", + "description": "Gherkin DSL parser for PHP", "homepage": "http://behat.org/", "keywords": [ "BDD", @@ -251,169 +352,155 @@ "gherkin", "parser" ], - "time": "2020-03-17T14:03:26+00:00" + "support": { + "issues": "https://github.com/Behat/Gherkin/issues", + "source": "https://github.com/Behat/Gherkin/tree/v4.11.0" + }, + "time": "2024-12-06T10:07:25+00:00" }, { - "name": "cache/cache", - "version": "0.4.0", + "name": "brick/math", + "version": "0.12.1", "source": { "type": "git", - "url": "https://github.com/php-cache/cache.git", - "reference": "902b2e5b54ea57e3a801437748652228c4c58604" + "url": "https://github.com/brick/math.git", + "reference": "f510c0a40911935b77b86859eb5223d58d660df1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-cache/cache/zipball/902b2e5b54ea57e3a801437748652228c4c58604", - "reference": "902b2e5b54ea57e3a801437748652228c4c58604", + "url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1", + "reference": "f510c0a40911935b77b86859eb5223d58d660df1", "shasum": "" }, "require": { - "doctrine/cache": "^1.3", - "league/flysystem": "^1.0", - "php": "^5.6 || ^7.0", - "psr/cache": "^1.0", - "psr/log": "^1.0", - "psr/simple-cache": "^1.0" - }, - "conflict": { - "cache/adapter-common": "*", - "cache/apc-adapter": "*", - "cache/apcu-adapter": "*", - "cache/array-adapter": "*", - "cache/chain-adapter": "*", - "cache/doctrine-adapter": "*", - "cache/filesystem-adapter": "*", - "cache/hierarchical-cache": "*", - "cache/illuminate-adapter": "*", - "cache/memcache-adapter": "*", - "cache/memcached-adapter": "*", - "cache/mongodb-adapter": "*", - "cache/predis-adapter": "*", - "cache/psr-6-doctrine-bridge": "*", - "cache/redis-adapter": "*", - "cache/session-handler": "*", - "cache/taggable-cache": "*", - "cache/void-adapter": "*" + "php": "^8.1" }, "require-dev": { - "cache/integration-tests": "^0.16", - "defuse/php-encryption": "^2.0", - "illuminate/cache": "^5.4", - "mockery/mockery": "^0.9", - "phpunit/phpunit": "^4.0 || ^5.1", - "predis/predis": "^1.0", - "symfony/cache": "dev-master" - }, - "suggest": { - "ext-apc": "APC extension is required to use the APC Adapter", - "ext-apcu": "APCu extension is required to use the APCu Adapter", - "ext-memcache": "Memcache extension is required to use the Memcache Adapter", - "ext-memcached": "Memcached extension is required to use the Memcached Adapter", - "ext-mongodb": "Mongodb extension required to use the Mongodb adapter", - "ext-redis": "Redis extension is required to use the Redis adapter", - "mongodb/mongodb": "Mongodb lib required to use the Mongodb adapter" + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^10.1", + "vimeo/psalm": "5.16.0" }, "type": "library", "autoload": { "psr-4": { - "Cache\\": "src/" - }, - "exclude-from-classmap": [ - "**/Tests/" - ] + "Brick\\Math\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Aaron Scherer", - "email": "aequasi@gmail.com", - "homepage": "https://github.com/aequasi" - }, - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/Nyholm" - } - ], - "description": "Library of all the php-cache adapters", - "homepage": "http://www.php-cache.com/en/latest/", + "description": "Arbitrary-precision arithmetic library", "keywords": [ - "cache", - "psr6" - ], - "time": "2017-03-28T16:08:48+00:00" + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.12.1" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2023-11-29T23:19:16+00:00" }, { "name": "codeception/codeception", - "version": "2.4.5", + "version": "5.1.2", "source": { "type": "git", "url": "https://github.com/Codeception/Codeception.git", - "reference": "5fee32d5c82791548931cbc34806b4de6aa1abfc" + "reference": "3b2d7d1a88e7e1d9dc0acb6d3c8f0acda0a37374" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/5fee32d5c82791548931cbc34806b4de6aa1abfc", - "reference": "5fee32d5c82791548931cbc34806b4de6aa1abfc", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/3b2d7d1a88e7e1d9dc0acb6d3c8f0acda0a37374", + "reference": "3b2d7d1a88e7e1d9dc0acb6d3c8f0acda0a37374", "shasum": "" }, "require": { - "behat/gherkin": "^4.4.0", - "codeception/phpunit-wrapper": "^6.0.9|^7.0.6", - "codeception/stub": "^2.0", + "behat/gherkin": "^4.6.2", + "codeception/lib-asserts": "^2.0", + "codeception/stub": "^4.1", + "ext-curl": "*", "ext-json": "*", "ext-mbstring": "*", - "facebook/webdriver": ">=1.1.3 <2.0", - "guzzlehttp/guzzle": ">=4.1.4 <7.0", - "guzzlehttp/psr7": "~1.0", - "php": ">=5.6.0 <8.0", - "symfony/browser-kit": ">=2.7 <5.0", - "symfony/console": ">=2.7 <5.0", - "symfony/css-selector": ">=2.7 <5.0", - "symfony/dom-crawler": ">=2.7 <5.0", - "symfony/event-dispatcher": ">=2.7 <5.0", - "symfony/finder": ">=2.7 <5.0", - "symfony/yaml": ">=2.7 <5.0" + "php": "^8.0", + "phpunit/php-code-coverage": "^9.2 || ^10.0 || ^11.0", + "phpunit/php-text-template": "^2.0 || ^3.0 || ^4.0", + "phpunit/php-timer": "^5.0.3 || ^6.0 || ^7.0", + "phpunit/phpunit": "^9.5.20 || ^10.0 || ^11.0", + "psy/psysh": "^0.11.2 || ^0.12", + "sebastian/comparator": "^4.0.5 || ^5.0 || ^6.0", + "sebastian/diff": "^4.0.3 || ^5.0 || ^6.0", + "symfony/console": ">=4.4.24 <8.0", + "symfony/css-selector": ">=4.4.24 <8.0", + "symfony/event-dispatcher": ">=4.4.24 <8.0", + "symfony/finder": ">=4.4.24 <8.0", + "symfony/var-dumper": ">=4.4.24 <8.0", + "symfony/yaml": ">=4.4.24 <8.0" + }, + "conflict": { + "codeception/lib-innerbrowser": "<3.1.3", + "codeception/module-filesystem": "<3.0", + "codeception/module-phpbrowser": "<2.5" + }, + "replace": { + "codeception/phpunit-wrapper": "*" }, "require-dev": { - "codeception/specify": "~0.3", - "facebook/graph-sdk": "~5.3", - "flow/jsonpath": "~0.2", - "monolog/monolog": "~1.8", - "pda/pheanstalk": "~3.0", - "php-amqplib/php-amqplib": "~2.4", - "predis/predis": "^1.0", - "squizlabs/php_codesniffer": "~2.0", - "symfony/process": ">=2.7 <5.0", - "vlucas/phpdotenv": "^2.4.0" + "codeception/lib-innerbrowser": "*@dev", + "codeception/lib-web": "^1.0", + "codeception/module-asserts": "*@dev", + "codeception/module-cli": "*@dev", + "codeception/module-db": "*@dev", + "codeception/module-filesystem": "*@dev", + "codeception/module-phpbrowser": "*@dev", + "codeception/util-universalframework": "*@dev", + "ext-simplexml": "*", + "jetbrains/phpstorm-attributes": "^1.0", + "symfony/dotenv": ">=4.4.24 <8.0", + "symfony/process": ">=4.4.24 <8.0", + "vlucas/phpdotenv": "^5.1" }, "suggest": { - "aws/aws-sdk-php": "For using AWS Auth in REST module and Queue module", - "codeception/phpbuiltinserver": "Start and stop PHP built-in web server for your tests", "codeception/specify": "BDD-style code blocks", "codeception/verify": "BDD-style assertions", - "flow/jsonpath": "For using JSONPath in REST module", - "league/factory-muffin": "For DataFactory module", - "league/factory-muffin-faker": "For Faker support in DataFactory module", - "phpseclib/phpseclib": "for SFTP option in FTP Module", + "ext-simplexml": "For loading params from XML files", "stecman/symfony-console-completion": "For BASH autocompletion", - "symfony/phpunit-bridge": "For phpunit-bridge support" + "symfony/dotenv": "For loading params from .env files", + "symfony/phpunit-bridge": "For phpunit-bridge support", + "vlucas/phpdotenv": "For loading params from .env files" }, "bin": [ "codecept" ], "type": "library", - "extra": { - "branch-alias": [] - }, "autoload": { + "files": [ + "functions.php" + ], "psr-4": { - "Codeception\\": "src\\Codeception", + "Codeception\\": "src/Codeception", "Codeception\\Extension\\": "ext" - } + }, + "classmap": [ + "src/PHPUnit/TestCase.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -422,12 +509,12 @@ "authors": [ { "name": "Michael Bodnarchuk", - "email": "davert@mail.ua", - "homepage": "http://codegyre.com" + "email": "davert.ua@gmail.com", + "homepage": "https://codeception.com" } ], "description": "BDD-style testing framework", - "homepage": "http://codeception.com/", + "homepage": "https://codeception.com/", "keywords": [ "BDD", "TDD", @@ -435,37 +522,214 @@ "functional testing", "unit testing" ], - "time": "2018-08-01T07:21:49+00:00" + "support": { + "issues": "https://github.com/Codeception/Codeception/issues", + "source": "https://github.com/Codeception/Codeception/tree/5.1.2" + }, + "funding": [ + { + "url": "https://opencollective.com/codeception", + "type": "open_collective" + } + ], + "time": "2024-03-07T07:19:42+00:00" + }, + { + "name": "codeception/lib-asserts", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/Codeception/lib-asserts.git", + "reference": "b8c7dff552249e560879c682ba44a4b963af91bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Codeception/lib-asserts/zipball/b8c7dff552249e560879c682ba44a4b963af91bc", + "reference": "b8c7dff552249e560879c682ba44a4b963af91bc", + "shasum": "" + }, + "require": { + "codeception/phpunit-wrapper": "^7.7.1 | ^8.0.3 | ^9.0", + "ext-dom": "*", + "php": "^7.4 | ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Bodnarchuk", + "email": "davert@mail.ua", + "homepage": "http://codegyre.com" + }, + { + "name": "Gintautas Miselis" + }, + { + "name": "Gustavo Nieves", + "homepage": "https://medium.com/@ganieves" + } + ], + "description": "Assertion methods used by Codeception core and Asserts module", + "homepage": "https://codeception.com/", + "keywords": [ + "codeception" + ], + "support": { + "issues": "https://github.com/Codeception/lib-asserts/issues", + "source": "https://github.com/Codeception/lib-asserts/tree/2.1.0" + }, + "time": "2023-02-10T18:36:23+00:00" }, { - "name": "codeception/phpunit-wrapper", - "version": "7.0.6", + "name": "codeception/lib-web", + "version": "1.0.6", "source": { "type": "git", - "url": "https://github.com/Codeception/phpunit-wrapper.git", - "reference": "e8528cb777cf5a5ccea1cf57a3522b142625d1b5" + "url": "https://github.com/Codeception/lib-web.git", + "reference": "01ff7f9ed8760ba0b0805a0b3a843b4e74165a60" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/e8528cb777cf5a5ccea1cf57a3522b142625d1b5", - "reference": "e8528cb777cf5a5ccea1cf57a3522b142625d1b5", + "url": "https://api.github.com/repos/Codeception/lib-web/zipball/01ff7f9ed8760ba0b0805a0b3a843b4e74165a60", + "reference": "01ff7f9ed8760ba0b0805a0b3a843b4e74165a60", "shasum": "" }, "require": { - "phpunit/php-code-coverage": "^6.0", - "phpunit/phpunit": "^7.0", - "sebastian/comparator": "^2.0", - "sebastian/diff": "^3.0" + "ext-mbstring": "*", + "guzzlehttp/psr7": "^2.0", + "php": "^8.0", + "phpunit/phpunit": "^9.5 | ^10.0 | ^11.0", + "symfony/css-selector": ">=4.4.24 <8.0" + }, + "conflict": { + "codeception/codeception": "<5.0.0-alpha3" }, "require-dev": { - "codeception/specify": "*", - "vlucas/phpdotenv": "^2.4" + "php-webdriver/webdriver": "^1.12" }, "type": "library", "autoload": { - "psr-4": { - "Codeception\\PHPUnit\\": "src\\" + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gintautas Miselis" + } + ], + "description": "Library containing files used by module-webdriver and lib-innerbrowser or module-phpbrowser", + "homepage": "https://codeception.com/", + "keywords": [ + "codeception" + ], + "support": { + "issues": "https://github.com/Codeception/lib-web/issues", + "source": "https://github.com/Codeception/lib-web/tree/1.0.6" + }, + "time": "2024-02-06T20:50:08+00:00" + }, + { + "name": "codeception/module-asserts", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/Codeception/module-asserts.git", + "reference": "1b6b150b30586c3614e7e5761b31834ed7968603" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Codeception/module-asserts/zipball/1b6b150b30586c3614e7e5761b31834ed7968603", + "reference": "1b6b150b30586c3614e7e5761b31834ed7968603", + "shasum": "" + }, + "require": { + "codeception/codeception": "*@dev", + "codeception/lib-asserts": "^2.0", + "php": "^8.0" + }, + "conflict": { + "codeception/codeception": "<5.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Bodnarchuk" + }, + { + "name": "Gintautas Miselis" + }, + { + "name": "Gustavo Nieves", + "homepage": "https://medium.com/@ganieves" } + ], + "description": "Codeception module containing various assertions", + "homepage": "https://codeception.com/", + "keywords": [ + "assertions", + "asserts", + "codeception" + ], + "support": { + "issues": "https://github.com/Codeception/module-asserts/issues", + "source": "https://github.com/Codeception/module-asserts/tree/3.0.0" + }, + "time": "2022-02-16T19:48:08+00:00" + }, + { + "name": "codeception/module-webdriver", + "version": "4.0.2", + "source": { + "type": "git", + "url": "https://github.com/Codeception/module-webdriver.git", + "reference": "ef0ea8044eb01dc1e830df27fe431e71440a462f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Codeception/module-webdriver/zipball/ef0ea8044eb01dc1e830df27fe431e71440a462f", + "reference": "ef0ea8044eb01dc1e830df27fe431e71440a462f", + "shasum": "" + }, + "require": { + "codeception/codeception": "^5.0.8", + "codeception/lib-web": "^1.0.1", + "codeception/stub": "^4.0", + "ext-json": "*", + "ext-mbstring": "*", + "php": "^8.1", + "php-webdriver/webdriver": "^1.14.0", + "phpunit/phpunit": "^10.0 || ^11.0" + }, + "suggest": { + "codeception/phpbuiltinserver": "Start and stop PHP built-in web server for your tests" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -473,29 +737,51 @@ ], "authors": [ { - "name": "Davert", - "email": "davert.php@resend.cc" + "name": "Michael Bodnarchuk" + }, + { + "name": "Gintautas Miselis" + }, + { + "name": "Zaahid Bateson" } ], - "description": "PHPUnit classes used by Codeception", - "time": "2018-03-31T18:49:51+00:00" + "description": "WebDriver module for Codeception", + "homepage": "https://codeception.com/", + "keywords": [ + "acceptance-testing", + "browser-testing", + "codeception" + ], + "support": { + "issues": "https://github.com/Codeception/module-webdriver/issues", + "source": "https://github.com/Codeception/module-webdriver/tree/4.0.2" + }, + "time": "2024-08-10T00:18:42+00:00" }, { "name": "codeception/stub", - "version": "2.0.4", + "version": "4.1.3", "source": { "type": "git", "url": "https://github.com/Codeception/Stub.git", - "reference": "f50bc271f392a2836ff80690ce0c058efe1ae03e" + "reference": "4fcad2c165f365377486dc3fd8703b07f1f2fcae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Stub/zipball/f50bc271f392a2836ff80690ce0c058efe1ae03e", - "reference": "f50bc271f392a2836ff80690ce0c058efe1ae03e", + "url": "https://api.github.com/repos/Codeception/Stub/zipball/4fcad2c165f365377486dc3fd8703b07f1f2fcae", + "reference": "4fcad2c165f365377486dc3fd8703b07f1f2fcae", "shasum": "" }, "require": { - "phpunit/phpunit": ">=4.8 <8.0" + "php": "^7.4 | ^8.0", + "phpunit/phpunit": "^8.4 | ^9.0 | ^10.0 | ^11" + }, + "conflict": { + "codeception/codeception": "<5.0.6" + }, + "require-dev": { + "consolidation/robo": "^3.0" }, "type": "library", "autoload": { @@ -508,36 +794,41 @@ "MIT" ], "description": "Flexible Stub wrapper for PHPUnit's Mock Builder", - "time": "2018-07-26T11:55:37+00:00" + "support": { + "issues": "https://github.com/Codeception/Stub/issues", + "source": "https://github.com/Codeception/Stub/tree/4.1.3" + }, + "time": "2024-02-02T19:21:00+00:00" }, { "name": "composer/ca-bundle", - "version": "1.2.6", + "version": "1.5.5", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "47fe531de31fca4a1b997f87308e7d7804348f7e" + "reference": "08c50d5ec4c6ced7d0271d2862dec8c1033283e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/47fe531de31fca4a1b997f87308e7d7804348f7e", - "reference": "47fe531de31fca4a1b997f87308e7d7804348f7e", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/08c50d5ec4c6ced7d0271d2862dec8c1033283e6", + "reference": "08c50d5ec4c6ced7d0271d2862dec8c1033283e6", "shasum": "" }, "require": { "ext-openssl": "*", "ext-pcre": "*", - "php": "^5.3.2 || ^7.0 || ^8.0" + "php": "^7.2 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8", - "psr/log": "^1.0", - "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0" + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8 || ^9", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-main": "1.x-dev" } }, "autoload": { @@ -564,61 +855,63 @@ "ssl", "tls" ], - "time": "2020-01-13T10:02:55+00:00" + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/ca-bundle/issues", + "source": "https://github.com/composer/ca-bundle/tree/1.5.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2025-01-08T16:17:16+00:00" }, { - "name": "composer/composer", - "version": "1.10.1", + "name": "composer/class-map-generator", + "version": "1.6.0", "source": { "type": "git", - "url": "https://github.com/composer/composer.git", - "reference": "b912a45da3e2b22f5cb5a23e441b697a295ba011" + "url": "https://github.com/composer/class-map-generator.git", + "reference": "ffe442c5974c44a9343e37a0abcb1cc37319f5b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/b912a45da3e2b22f5cb5a23e441b697a295ba011", - "reference": "b912a45da3e2b22f5cb5a23e441b697a295ba011", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/ffe442c5974c44a9343e37a0abcb1cc37319f5b9", + "reference": "ffe442c5974c44a9343e37a0abcb1cc37319f5b9", "shasum": "" }, "require": { - "composer/ca-bundle": "^1.0", - "composer/semver": "^1.0", - "composer/spdx-licenses": "^1.2", - "composer/xdebug-handler": "^1.1", - "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0", - "php": "^5.3.2 || ^7.0", - "psr/log": "^1.0", - "seld/jsonlint": "^1.4", - "seld/phar-utils": "^1.0", - "symfony/console": "^2.7 || ^3.0 || ^4.0 || ^5.0", - "symfony/filesystem": "^2.7 || ^3.0 || ^4.0 || ^5.0", - "symfony/finder": "^2.7 || ^3.0 || ^4.0 || ^5.0", - "symfony/process": "^2.7 || ^3.0 || ^4.0 || ^5.0" - }, - "conflict": { - "symfony/console": "2.8.38" + "composer/pcre": "^2.1 || ^3.1", + "php": "^7.2 || ^8.0", + "symfony/finder": "^4.4 || ^5.3 || ^6 || ^7" }, "require-dev": { - "phpspec/prophecy": "^1.10", - "symfony/phpunit-bridge": "^3.4" - }, - "suggest": { - "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages", - "ext-zip": "Enabling the zip extension allows you to unzip archives", - "ext-zlib": "Allow gzip compression of HTTP requests" + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-deprecation-rules": "^1 || ^2", + "phpstan/phpstan-phpunit": "^1 || ^2", + "phpstan/phpstan-strict-rules": "^1.1 || ^2", + "phpunit/phpunit": "^8", + "symfony/filesystem": "^5.4 || ^6" }, - "bin": [ - "bin/composer" - ], "type": "library", "extra": { "branch-alias": { - "dev-master": "1.10-dev" + "dev-main": "1.x-dev" } }, "autoload": { "psr-4": { - "Composer\\": "src/Composer" + "Composer\\ClassMapGenerator\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -627,14 +920,118 @@ ], "authors": [ { - "name": "Nils Adermann", - "email": "naderman@naderman.de", - "homepage": "http://www.naderman.de" + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Utilities to scan PHP code and generate class maps.", + "keywords": [ + "classmap" + ], + "support": { + "issues": "https://github.com/composer/class-map-generator/issues", + "source": "https://github.com/composer/class-map-generator/tree/1.6.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2025-02-05T10:05:34+00:00" + }, + { + "name": "composer/composer", + "version": "2.8.5", + "source": { + "type": "git", + "url": "https://github.com/composer/composer.git", + "reference": "ae208dc1e182bd45d99fcecb956501da212454a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/composer/zipball/ae208dc1e182bd45d99fcecb956501da212454a1", + "reference": "ae208dc1e182bd45d99fcecb956501da212454a1", + "shasum": "" + }, + "require": { + "composer/ca-bundle": "^1.5", + "composer/class-map-generator": "^1.4.0", + "composer/metadata-minifier": "^1.0", + "composer/pcre": "^2.2 || ^3.2", + "composer/semver": "^3.3", + "composer/spdx-licenses": "^1.5.7", + "composer/xdebug-handler": "^2.0.2 || ^3.0.3", + "justinrainbow/json-schema": "^5.3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "react/promise": "^2.11 || ^3.2", + "seld/jsonlint": "^1.4", + "seld/phar-utils": "^1.2", + "seld/signal-handler": "^2.0", + "symfony/console": "^5.4.35 || ^6.3.12 || ^7.0.3", + "symfony/filesystem": "^5.4.35 || ^6.3.12 || ^7.0.3", + "symfony/finder": "^5.4.35 || ^6.3.12 || ^7.0.3", + "symfony/polyfill-php73": "^1.24", + "symfony/polyfill-php80": "^1.24", + "symfony/polyfill-php81": "^1.24", + "symfony/process": "^5.4.35 || ^6.3.12 || ^7.0.3" + }, + "require-dev": { + "phpstan/phpstan": "^1.11.8", + "phpstan/phpstan-deprecation-rules": "^1.2.0", + "phpstan/phpstan-phpunit": "^1.4.0", + "phpstan/phpstan-strict-rules": "^1.6.0", + "phpstan/phpstan-symfony": "^1.4.0", + "symfony/phpunit-bridge": "^6.4.3 || ^7.0.1" + }, + "suggest": { + "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages", + "ext-zip": "Enabling the zip extension allows you to unzip archives", + "ext-zlib": "Allow gzip compression of HTTP requests" + }, + "bin": [ + "bin/composer" + ], + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "phpstan/rules.neon" + ] + }, + "branch-alias": { + "dev-main": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\": "src/Composer/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "https://www.naderman.de" }, { "name": "Jordi Boggiano", "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" + "homepage": "https://seld.be" } ], "description": "Composer helps you declare, manage and install dependencies of PHP projects. It ensures you have the right stack everywhere.", @@ -644,32 +1041,201 @@ "dependency", "package" ], - "time": "2020-03-13T19:34:27+00:00" + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/composer/issues", + "security": "https://github.com/composer/composer/security/policy", + "source": "https://github.com/composer/composer/tree/2.8.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2025-01-21T14:23:40+00:00" + }, + { + "name": "composer/metadata-minifier", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/composer/metadata-minifier.git", + "reference": "c549d23829536f0d0e984aaabbf02af91f443207" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/metadata-minifier/zipball/c549d23829536f0d0e984aaabbf02af91f443207", + "reference": "c549d23829536f0d0e984aaabbf02af91f443207", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "composer/composer": "^2", + "phpstan/phpstan": "^0.12.55", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\MetadataMinifier\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Small utility library that handles metadata minification and expansion.", + "keywords": [ + "composer", + "compression" + ], + "support": { + "issues": "https://github.com/composer/metadata-minifier/issues", + "source": "https://github.com/composer/metadata-minifier/tree/1.0.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2021-04-07T13:37:33+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" }, { "name": "composer/semver", - "version": "1.5.1", + "version": "3.4.3", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de" + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/c6bea70230ef4dd483e6bbcab6005f682ed3a8de", - "reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de", + "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0" + "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^4.5 || ^5.0.5" + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-main": "3.x-dev" } }, "autoload": { @@ -705,32 +1271,52 @@ "validation", "versioning" ], - "time": "2020-01-13T12:06:48+00:00" + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-09-19T14:15:21+00:00" }, { "name": "composer/spdx-licenses", - "version": "1.5.3", + "version": "1.5.8", "source": { "type": "git", "url": "https://github.com/composer/spdx-licenses.git", - "reference": "0c3e51e1880ca149682332770e25977c70cf9dae" + "reference": "560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/0c3e51e1880ca149682332770e25977c70cf9dae", - "reference": "0c3e51e1880ca149682332770e25977c70cf9dae", + "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a", + "reference": "560bdcf8deb88ae5d611c80a2de8ea9d0358cc0a", "shasum": "" }, "require": { "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 7" + "phpstan/phpstan": "^0.12.55", + "symfony/phpunit-bridge": "^4.2 || ^5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-main": "1.x-dev" } }, "autoload": { @@ -765,28 +1351,50 @@ "spdx", "validator" ], - "time": "2020-02-14T07:44:31+00:00" + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/spdx-licenses/issues", + "source": "https://github.com/composer/spdx-licenses/tree/1.5.8" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-11-20T07:44:33+00:00" }, { "name": "composer/xdebug-handler", - "version": "1.4.1", + "version": "3.0.5", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "1ab9842d69e64fb3a01be6b656501032d1b78cb7" + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/1ab9842d69e64fb3a01be6b656501032d1b78cb7", - "reference": "1ab9842d69e64fb3a01be6b656501032d1b78cb7", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0 || ^8.0", - "psr/log": "^1.0" + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8" + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" }, "type": "library", "autoload": { @@ -809,36 +1417,61 @@ "Xdebug", "performance" ], - "time": "2020-03-01T12:26:26+00:00" + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" }, { "name": "csharpru/vault-php", - "version": "3.5.3", + "version": "4.4.0", "source": { "type": "git", "url": "https://github.com/CSharpRU/vault-php.git", - "reference": "04be9776310fe7d1afb97795645f95c21e6b4fcf" + "reference": "ba4cbd7b55f1ce10bc72a7e4c36cfd87a42d3573" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CSharpRU/vault-php/zipball/04be9776310fe7d1afb97795645f95c21e6b4fcf", - "reference": "04be9776310fe7d1afb97795645f95c21e6b4fcf", + "url": "https://api.github.com/repos/CSharpRU/vault-php/zipball/ba4cbd7b55f1ce10bc72a7e4c36cfd87a42d3573", + "reference": "ba4cbd7b55f1ce10bc72a7e4c36cfd87a42d3573", "shasum": "" }, "require": { - "cache/cache": "^0.4.0", - "doctrine/inflector": "~1.1.0", - "guzzlehttp/promises": "^1.3", - "guzzlehttp/psr7": "^1.4", - "psr/cache": "^1.0", - "psr/log": "^1.0", - "weew/helpers-array": "^1.3" + "aws/aws-sdk-php": "^3.0", + "ext-json": "*", + "php": "^7.2 || ^8.0", + "psr/cache": "^1.0|^2.0|^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/log": "^1.0|^2.0|^3.0" }, "require-dev": { - "codacy/coverage": "^1.1", - "codeception/codeception": "^2.2", - "csharpru/vault-php-guzzle6-transport": "~2.0", - "php-vcr/php-vcr": "^1.3" + "alextartan/guzzle-psr18-adapter": "^1.2 || ^2.0", + "cache/array-adapter": "^1.0", + "codeception/codeception": "^4.1", + "codeception/module-asserts": "^1.3", + "laminas/laminas-diactoros": "^2.3", + "php-vcr/php-vcr": "^1.5", + "symfony/event-dispatcher": "<5.0" + }, + "suggest": { + "cache/array-adapter": "For usage with CachedClient class" }, "type": "library", "autoload": { @@ -857,74 +1490,49 @@ } ], "description": "Best Vault client for PHP that you can find", - "time": "2018-04-28T04:52:17+00:00" - }, - { - "name": "csharpru/vault-php-guzzle6-transport", - "version": "2.0.4", - "source": { - "type": "git", - "url": "https://github.com/CSharpRU/vault-php-guzzle6-transport.git", - "reference": "33c392120ac9f253b62b034e0e8ffbbdb3513bd8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/CSharpRU/vault-php-guzzle6-transport/zipball/33c392120ac9f253b62b034e0e8ffbbdb3513bd8", - "reference": "33c392120ac9f253b62b034e0e8ffbbdb3513bd8", - "shasum": "" - }, - "require": { - "guzzlehttp/guzzle": "~6.2", - "guzzlehttp/promises": "^1.3", - "guzzlehttp/psr7": "^1.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "VaultTransports\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Yaroslav Lukyanov", - "email": "c_sharp@mail.ru" - } + "keywords": [ + "hashicorp", + "secrets", + "vault" ], - "description": "Guzzle6 transport for Vault PHP client", - "time": "2019-03-10T06:17:37+00:00" + "support": { + "issues": "https://github.com/CSharpRU/vault-php/issues", + "source": "https://github.com/CSharpRU/vault-php/tree/4.4.0" + }, + "time": "2023-11-22T11:38:41+00:00" }, { "name": "doctrine/annotations", - "version": "v1.8.0", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "904dca4eb10715b92569fbcd79e201d5c349b6bc" + "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/904dca4eb10715b92569fbcd79e201d5c349b6bc", - "reference": "904dca4eb10715b92569fbcd79e201d5c349b6bc", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/901c2ee5d26eb64ff43c47976e114bf00843acf7", + "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7", "shasum": "" }, "require": { - "doctrine/lexer": "1.*", - "php": "^7.1" + "doctrine/lexer": "^2 || ^3", + "ext-tokenizer": "*", + "php": "^7.2 || ^8.0", + "psr/cache": "^1 || ^2 || ^3" }, "require-dev": { - "doctrine/cache": "1.*", - "phpunit/phpunit": "^7.5" + "doctrine/cache": "^2.0", + "doctrine/coding-standard": "^10", + "phpstan/phpstan": "^1.10.28", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "symfony/cache": "^5.4 || ^6.4 || ^7", + "vimeo/psalm": "^4.30 || ^5.14" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.7.x-dev" - } + "suggest": { + "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" }, + "type": "library", "autoload": { "psr-4": { "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" @@ -957,48 +1565,46 @@ } ], "description": "Docblock Annotations Parser", - "homepage": "http://www.doctrine-project.org", + "homepage": "https://www.doctrine-project.org/projects/annotations.html", "keywords": [ "annotations", "docblock", "parser" ], - "time": "2019-10-01T18:55:10+00:00" + "support": { + "issues": "https://github.com/doctrine/annotations/issues", + "source": "https://github.com/doctrine/annotations/tree/2.0.2" + }, + "time": "2024-09-05T10:17:24+00:00" }, { - "name": "doctrine/cache", - "version": "v1.6.2", + "name": "doctrine/lexer", + "version": "3.0.1", "source": { "type": "git", - "url": "https://github.com/doctrine/cache.git", - "reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b" + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/eb152c5100571c7a45470ff2a35095ab3f3b900b", - "reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", "shasum": "" }, "require": { - "php": "~5.5|~7.0" - }, - "conflict": { - "doctrine/common": ">2.2,<2.4" + "php": "^8.1" }, "require-dev": { - "phpunit/phpunit": "~4.8|~5.0", - "predis/predis": "~1.0", - "satooshi/php-coveralls": "~0.6" + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.6.x-dev" - } - }, "autoload": { "psr-4": { - "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" + "Doctrine\\Common\\Lexer\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1006,394 +1612,340 @@ "MIT" ], "authors": [ - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, { "name": "Guilherme Blanco", "email": "guilhermeblanco@gmail.com" }, { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" + "name": "Roman Borschel", + "email": "roman@code-factory.org" }, { "name": "Johannes Schmitt", "email": "schmittjoh@gmail.com" } ], - "description": "Caching library offering an object-oriented API for many cache backends", - "homepage": "http://www.doctrine-project.org", + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", "keywords": [ - "cache", - "caching" + "annotations", + "docblock", + "lexer", + "parser", + "php" ], - "time": "2017-07-22T12:49:21+00:00" - }, - { - "name": "doctrine/inflector", - "version": "v1.1.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/inflector.git", - "reference": "90b2128806bfde671b6952ab8bea493942c1fdae" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/90b2128806bfde671b6952ab8bea493942c1fdae", - "reference": "90b2128806bfde671b6952ab8bea493942c1fdae", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "require-dev": { - "phpunit/phpunit": "4.*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1.x-dev" - } - }, - "autoload": { - "psr-0": { - "Doctrine\\Common\\Inflector\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "Common String Manipulations with regard to casing and singular/plural rules.", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "inflection", - "pluralize", - "singularize", - "string" - ], - "time": "2015-11-06T14:35:42+00:00" - }, - { - "name": "doctrine/instantiator", - "version": "1.3.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "ae466f726242e637cebdd526a7d991b9433bacf1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/ae466f726242e637cebdd526a7d991b9433bacf1", - "reference": "ae466f726242e637cebdd526a7d991b9433bacf1", - "shasum": "" - }, - "require": { - "php": "^7.1" - }, - "require-dev": { - "doctrine/coding-standard": "^6.0", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^0.13", - "phpstan/phpstan-phpunit": "^0.11", - "phpstan/phpstan-shim": "^0.11", - "phpunit/phpunit": "^7.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "http://ocramius.github.com/" - } - ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://www.doctrine-project.org/projects/instantiator.html", - "keywords": [ - "constructor", - "instantiate" - ], - "time": "2019-10-21T16:45:58+00:00" - }, - { - "name": "doctrine/lexer", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/lexer.git", - "reference": "5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6", - "reference": "5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6", - "shasum": "" - }, - "require": { - "php": "^7.2" - }, - "require-dev": { - "doctrine/coding-standard": "^6.0", - "phpstan/phpstan": "^0.11.8", - "phpunit/phpunit": "^8.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" - } + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ + "funding": [ { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" }, { - "name": "Roman Borschel", - "email": "roman@code-factory.org" + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" }, { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" } ], - "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", - "homepage": "https://www.doctrine-project.org/projects/lexer.html", - "keywords": [ - "annotations", - "docblock", - "lexer", - "parser", - "php" - ], - "time": "2019-10-30T14:39:59+00:00" + "time": "2024-02-05T11:56:58+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "6.5.2", + "version": "7.9.2", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "43ece0e75098b7ecd8d13918293029e555a50f82" + "reference": "d281ed313b989f213357e3be1a179f02196ac99b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/43ece0e75098b7ecd8d13918293029e555a50f82", - "reference": "43ece0e75098b7ecd8d13918293029e555a50f82", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^1.0", - "guzzlehttp/psr7": "^1.6.1", - "php": ">=5.5" + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" }, "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", - "psr/log": "^1.1" + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { + "ext-curl": "Required for CURL handler support", "ext-intl": "Required for Internationalized Domain Name (IDN) support", "psr/log": "Required for using the Log middleware" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "6.5-dev" + "bamarni-bin": { + "bin-links": true, + "forward-command": false } }, "autoload": { - "psr-4": { - "GuzzleHttp\\": "src/" - }, "files": [ "src/functions_include.php" - ] + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" } ], "description": "Guzzle is a PHP HTTP client library", - "homepage": "http://guzzlephp.org/", "keywords": [ "client", "curl", "framework", "http", "http client", + "psr-18", + "psr-7", "rest", "web service" ], - "time": "2019-12-23T11:57:10+00:00" + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.9.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2024-07-24T11:22:20+00:00" }, { "name": "guzzlehttp/promises", - "version": "v1.3.1", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", - "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", "shasum": "" }, "require": { - "php": ">=5.5.0" + "php": "^7.2.5 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^4.0" + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.4-dev" + "bamarni-bin": { + "bin-links": true, + "forward-command": false } }, "autoload": { "psr-4": { "GuzzleHttp\\Promise\\": "src/" - }, - "files": [ - "src/functions_include.php" - ] + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" } ], "description": "Guzzle promises library", "keywords": [ "promise" ], - "time": "2016-12-20T10:07:11+00:00" + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2024-10-17T10:06:22+00:00" }, { "name": "guzzlehttp/psr7", - "version": "1.6.1", + "version": "2.7.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "239400de7a173fe9901b9ac7c06497751f00727a" + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a", - "reference": "239400de7a173fe9901b9ac7c06497751f00727a", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", "shasum": "" }, "require": { - "php": ">=5.4.0", - "psr/http-message": "~1.0", - "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" }, "provide": { + "psr/http-factory-implementation": "1.0", "psr/http-message-implementation": "1.0" }, "require-dev": { - "ext-zlib": "*", - "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" }, "suggest": { - "zendframework/zend-httphandlerrunner": "Emit PSR-7 responses" + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.6-dev" + "bamarni-bin": { + "bin-links": true, + "forward-command": false } }, "autoload": { "psr-4": { "GuzzleHttp\\Psr7\\": "src/" - }, - "files": [ - "src/functions_include.php" - ] + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, { "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" } ], "description": "PSR-7 message implementation that also provides common utility methods", @@ -1407,38 +1959,55 @@ "uri", "url" ], - "time": "2019-07-01T23:21:34+00:00" + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.7.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2024-07-18T11:15:46+00:00" }, { - "name": "jms/metadata", - "version": "1.7.0", + "name": "justinrainbow/json-schema", + "version": "5.3.0", "source": { "type": "git", - "url": "https://github.com/schmittjoh/metadata.git", - "reference": "e5854ab1aa643623dc64adde718a8eec32b957a8" + "url": "https://github.com/jsonrainbow/json-schema.git", + "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/e5854ab1aa643623dc64adde718a8eec32b957a8", - "reference": "e5854ab1aa643623dc64adde718a8eec32b957a8", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", + "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=7.1" }, "require-dev": { - "doctrine/cache": "~1.0", - "symfony/cache": "~3.1" + "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", + "json-schema/json-schema-test-suite": "1.2.0", + "phpunit/phpunit": "^4.8.35" }, + "bin": [ + "bin/validate-json" + ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.5.x-dev" - } - }, "autoload": { - "psr-0": { - "Metadata\\": "src/" + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1447,343 +2016,184 @@ ], "authors": [ { - "name": "Asmir Mustafic", - "email": "goetas@gmail.com" + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" }, { - "name": "Johannes M. Schmitt", - "email": "schmittjoh@gmail.com" + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" } ], - "description": "Class/method/property metadata management in PHP", + "description": "A library to validate a json schema.", + "homepage": "https://github.com/justinrainbow/json-schema", "keywords": [ - "annotations", - "metadata", - "xml", - "yaml" + "json", + "schema" ], - "time": "2018-10-26T12:40:10+00:00" + "support": { + "issues": "https://github.com/jsonrainbow/json-schema/issues", + "source": "https://github.com/jsonrainbow/json-schema/tree/5.3.0" + }, + "time": "2024-07-06T21:00:26+00:00" }, { - "name": "jms/parser-lib", - "version": "1.0.0", + "name": "laminas/laminas-diactoros", + "version": "3.5.0", "source": { "type": "git", - "url": "https://github.com/schmittjoh/parser-lib.git", - "reference": "c509473bc1b4866415627af0e1c6cc8ac97fa51d" + "url": "https://github.com/laminas/laminas-diactoros.git", + "reference": "143a16306602ce56b8b092a7914fef03c37f9ed2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/parser-lib/zipball/c509473bc1b4866415627af0e1c6cc8ac97fa51d", - "reference": "c509473bc1b4866415627af0e1c6cc8ac97fa51d", + "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/143a16306602ce56b8b092a7914fef03c37f9ed2", + "reference": "143a16306602ce56b8b092a7914fef03c37f9ed2", "shasum": "" }, "require": { - "phpoption/phpoption": ">=0.9,<2.0-dev" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "psr-0": { - "JMS\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache2" - ], - "description": "A library for easily creating recursive-descent parsers.", - "time": "2012-11-18T18:08:43+00:00" - }, - { - "name": "jms/serializer", - "version": "1.14.1", - "source": { - "type": "git", - "url": "https://github.com/schmittjoh/serializer.git", - "reference": "ba908d278fff27ec01fb4349f372634ffcd697c0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/ba908d278fff27ec01fb4349f372634ffcd697c0", - "reference": "ba908d278fff27ec01fb4349f372634ffcd697c0", - "shasum": "" - }, - "require": { - "doctrine/annotations": "^1.0", - "doctrine/instantiator": "^1.0.3", - "jms/metadata": "^1.3", - "jms/parser-lib": "1.*", - "php": "^5.5|^7.0", - "phpcollection/phpcollection": "~0.1", - "phpoption/phpoption": "^1.1" + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.1 || ^2.0" }, "conflict": { - "twig/twig": "<1.12" - }, - "require-dev": { - "doctrine/orm": "~2.1", - "doctrine/phpcr-odm": "^1.3|^2.0", - "ext-pdo_sqlite": "*", - "jackalope/jackalope-doctrine-dbal": "^1.1.5", - "phpunit/phpunit": "^4.8|^5.0", - "propel/propel1": "~1.7", - "psr/container": "^1.0", - "symfony/dependency-injection": "^2.7|^3.3|^4.0", - "symfony/expression-language": "^2.6|^3.0", - "symfony/filesystem": "^2.1", - "symfony/form": "~2.1|^3.0", - "symfony/translation": "^2.1|^3.0", - "symfony/validator": "^2.2|^3.0", - "symfony/yaml": "^2.1|^3.0", - "twig/twig": "~1.12|~2.0" - }, - "suggest": { - "doctrine/cache": "Required if you like to use cache functionality.", - "doctrine/collections": "Required if you like to use doctrine collection types as ArrayCollection.", - "symfony/yaml": "Required if you'd like to serialize data to YAML format." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-1.x": "1.14-dev" - } - }, - "autoload": { - "psr-0": { - "JMS\\Serializer": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Johannes M. Schmitt", - "email": "schmittjoh@gmail.com" - }, - { - "name": "Asmir Mustafic", - "email": "goetas@gmail.com" - } - ], - "description": "Library for (de-)serializing data of any complexity; supports XML, JSON, and YAML.", - "homepage": "http://jmsyst.com/libs/serializer", - "keywords": [ - "deserialization", - "jaxb", - "json", - "serialization", - "xml" - ], - "time": "2020-02-22T20:59:37+00:00" - }, - { - "name": "justinrainbow/json-schema", - "version": "5.2.9", - "source": { - "type": "git", - "url": "https://github.com/justinrainbow/json-schema.git", - "reference": "44c6787311242a979fa15c704327c20e7221a0e4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/44c6787311242a979fa15c704327c20e7221a0e4", - "reference": "44c6787311242a979fa15c704327c20e7221a0e4", - "shasum": "" + "amphp/amp": "<2.6.4" }, - "require": { - "php": ">=5.3.3" + "provide": { + "psr/http-factory-implementation": "^1.0", + "psr/http-message-implementation": "^1.1 || ^2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", - "json-schema/json-schema-test-suite": "1.2.0", - "phpunit/phpunit": "^4.8.35" + "ext-curl": "*", + "ext-dom": "*", + "ext-gd": "*", + "ext-libxml": "*", + "http-interop/http-factory-tests": "^2.2.0", + "laminas/laminas-coding-standard": "~2.5.0", + "php-http/psr7-integration-tests": "^1.4.0", + "phpunit/phpunit": "^10.5.36", + "psalm/plugin-phpunit": "^0.19.0", + "vimeo/psalm": "^5.26.1" }, - "bin": [ - "bin/validate-json" - ], "type": "library", "extra": { - "branch-alias": { - "dev-master": "5.0.x-dev" + "laminas": { + "module": "Laminas\\Diactoros", + "config-provider": "Laminas\\Diactoros\\ConfigProvider" } }, "autoload": { + "files": [ + "src/functions/create_uploaded_file.php", + "src/functions/marshal_headers_from_sapi.php", + "src/functions/marshal_method_from_sapi.php", + "src/functions/marshal_protocol_version_from_sapi.php", + "src/functions/normalize_server.php", + "src/functions/normalize_uploaded_files.php", + "src/functions/parse_cookie_header.php" + ], "psr-4": { - "JsonSchema\\": "src/JsonSchema/" + "Laminas\\Diactoros\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bruno Prieto Reis", - "email": "bruno.p.reis@gmail.com" - }, - { - "name": "Justin Rainbow", - "email": "justin.rainbow@gmail.com" - }, - { - "name": "Igor Wiedler", - "email": "igor@wiedler.ch" - }, - { - "name": "Robert Schönthal", - "email": "seroscho@googlemail.com" - } + "BSD-3-Clause" ], - "description": "A library to validate a json schema.", - "homepage": "https://github.com/justinrainbow/json-schema", + "description": "PSR HTTP Message implementations", + "homepage": "https://laminas.dev", "keywords": [ - "json", - "schema" + "http", + "laminas", + "psr", + "psr-17", + "psr-7" ], - "time": "2019-09-25T14:49:45+00:00" - }, - { - "name": "league/flysystem", - "version": "1.0.66", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/flysystem.git", - "reference": "021569195e15f8209b1c4bebb78bd66aa4f08c21" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/021569195e15f8209b1c4bebb78bd66aa4f08c21", - "reference": "021569195e15f8209b1c4bebb78bd66aa4f08c21", - "shasum": "" - }, - "require": { - "ext-fileinfo": "*", - "php": ">=5.5.9" - }, - "conflict": { - "league/flysystem-sftp": "<1.0.6" - }, - "require-dev": { - "phpspec/phpspec": "^3.4", - "phpunit/phpunit": "^5.7.26" + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-diactoros/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-diactoros/issues", + "rss": "https://github.com/laminas/laminas-diactoros/releases.atom", + "source": "https://github.com/laminas/laminas-diactoros" }, - "suggest": { - "ext-fileinfo": "Required for MimeType", - "ext-ftp": "Allows you to use FTP server storage", - "ext-openssl": "Allows you to use FTPS server storage", - "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2", - "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", - "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", - "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", - "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", - "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", - "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", - "league/flysystem-webdav": "Allows you to use WebDAV storage", - "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter", - "spatie/flysystem-dropbox": "Allows you to use Dropbox storage", - "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - } - }, - "autoload": { - "psr-4": { - "League\\Flysystem\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ + "funding": [ { - "name": "Frank de Jonge", - "email": "info@frenky.net" + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" } ], - "description": "Filesystem abstraction: Many filesystems, one API.", - "keywords": [ - "Cloud Files", - "WebDAV", - "abstraction", - "aws", - "cloud", - "copy.com", - "dropbox", - "file systems", - "files", - "filesystem", - "filesystems", - "ftp", - "rackspace", - "remote", - "s3", - "sftp", - "storage" - ], - "time": "2020-03-17T18:58:12+00:00" + "time": "2024-10-14T11:59:49+00:00" }, { "name": "monolog/monolog", - "version": "1.25.3", + "version": "3.8.1", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "fa82921994db851a8becaf3787a9e73c5976b6f1" + "reference": "aef6ee73a77a66e404dd6540934a9ef1b3c855b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/fa82921994db851a8becaf3787a9e73c5976b6f1", - "reference": "fa82921994db851a8becaf3787a9e73c5976b6f1", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/aef6ee73a77a66e404dd6540934a9ef1b3c855b4", + "reference": "aef6ee73a77a66e404dd6540934a9ef1b3c855b4", "shasum": "" }, "require": { - "php": ">=5.3.0", - "psr/log": "~1.0" + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" }, "provide": { - "psr/log-implementation": "1.0.0" + "psr/log-implementation": "3.0.0" }, "require-dev": { - "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "aws/aws-sdk-php": "^3.0", "doctrine/couchdb": "~1.0@dev", - "graylog2/gelf-php": "~1.0", - "jakub-onderka/php-parallel-lint": "0.9", - "php-amqplib/php-amqplib": "~2.4", - "php-console/php-console": "^3.1.3", - "phpunit/phpunit": "~4.5", - "phpunit/phpunit-mock-objects": "2.3.0", - "ruflin/elastica": ">=0.90 <3.0", - "sentry/sentry": "^0.13", - "swiftmailer/swiftmailer": "^5.3|^6.0" + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" }, "suggest": { "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", - "ext-mongo": "Allow sending log messages to a MongoDB server", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", - "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", - "php-console/php-console": "Allow sending log messages to Google Chrome", "rollbar/rollbar": "Allow sending log messages to Rollbar", - "ruflin/elastica": "Allow sending log messages to an Elastic Search server", - "sentry/sentry": "Allow sending log messages to a Sentry server" + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-main": "3.x-dev" } }, "autoload": { @@ -1799,39 +2209,53 @@ { "name": "Jordi Boggiano", "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" + "homepage": "https://seld.be" } ], "description": "Sends your logs to files, sockets, inboxes, databases and various web services", - "homepage": "http://github.com/Seldaek/monolog", + "homepage": "https://github.com/Seldaek/monolog", "keywords": [ "log", "logging", "psr-3" ], - "time": "2019-12-20T14:15:16+00:00" + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.8.1" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2024-12-05T17:15:07+00:00" }, { "name": "mtdowling/jmespath.php", - "version": "2.5.0", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/jmespath/jmespath.php.git", - "reference": "52168cb9472de06979613d365c7f1ab8798be895" + "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/52168cb9472de06979613d365c7f1ab8798be895", - "reference": "52168cb9472de06979613d365c7f1ab8798be895", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/a2a865e05d5f420b50cc2f85bb78d565db12a6bc", + "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc", "shasum": "" }, "require": { - "php": ">=5.4.0", - "symfony/polyfill-mbstring": "^1.4" + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-mbstring": "^1.17" }, "require-dev": { - "composer/xdebug-handler": "^1.2", - "phpunit/phpunit": "^4.8.36|^7.5.15" + "composer/xdebug-handler": "^3.0.3", + "phpunit/phpunit": "^8.5.33" }, "bin": [ "bin/jp.php" @@ -1839,22 +2263,27 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.8-dev" } }, "autoload": { - "psr-4": { - "JmesPath\\": "src/" - }, "files": [ "src/JmesPath.php" - ] + ], + "psr-4": { + "JmesPath\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", @@ -1866,20 +2295,24 @@ "json", "jsonpath" ], - "time": "2019-12-30T18:03:34+00:00" + "support": { + "issues": "https://github.com/jmespath/jmespath.php/issues", + "source": "https://github.com/jmespath/jmespath.php/tree/2.8.0" + }, + "time": "2024-09-04T18:46:31+00:00" }, { "name": "mustache/mustache", - "version": "v2.13.0", + "version": "v2.14.2", "source": { "type": "git", "url": "https://github.com/bobthecow/mustache.php.git", - "reference": "e95c5a008c23d3151d59ea72484d4f72049ab7f4" + "reference": "e62b7c3849d22ec55f3ec425507bf7968193a6cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/mustache.php/zipball/e95c5a008c23d3151d59ea72484d4f72049ab7f4", - "reference": "e95c5a008c23d3151d59ea72484d4f72049ab7f4", + "url": "https://api.github.com/repos/bobthecow/mustache.php/zipball/e62b7c3849d22ec55f3ec425507bf7968193a6cb", + "reference": "e62b7c3849d22ec55f3ec425507bf7968193a6cb", "shasum": "" }, "require": { @@ -1912,41 +2345,47 @@ "mustache", "templating" ], - "time": "2019-11-23T21:40:31+00:00" + "support": { + "issues": "https://github.com/bobthecow/mustache.php/issues", + "source": "https://github.com/bobthecow/mustache.php/tree/v2.14.2" + }, + "time": "2022-08-23T13:07:01+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.9.5", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "b2c28789e80a97badd14145fda39b545d83ca3ef" + "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/b2c28789e80a97badd14145fda39b545d83ca3ef", - "reference": "b2c28789e80a97badd14145fda39b545d83ca3ef", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", + "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", "shasum": "" }, "require": { - "php": "^7.1" + "php": "^7.1 || ^8.0" }, - "replace": { - "myclabs/deep-copy": "self.version" + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { - "doctrine/collections": "^1.0", - "doctrine/common": "^2.6", - "phpunit/phpunit": "^7.1" + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", "autoload": { - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - }, "files": [ "src/DeepCopy/deep_copy.php" - ] + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1960,33 +2399,103 @@ "object", "object graph" ], - "time": "2020-01-17T21:11:47+00:00" + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2024-11-08T17:47:46+00:00" }, { - "name": "paragonie/random_compat", - "version": "v9.99.99", + "name": "nikic/php-parser", + "version": "v5.4.0", "source": { "type": "git", - "url": "https://github.com/paragonie/random_compat.git", - "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95" + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", - "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", "shasum": "" }, "require": { - "php": "^7" + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" }, "require-dev": { - "phpunit/phpunit": "4.*|5.*", - "vimeo/psalm": "^1" + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" }, - "suggest": { - "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + }, + "time": "2024-12-30T11:07:19+00:00" + }, + { + "name": "paragonie/constant_time_encoding", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512", + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "phpunit/phpunit": "^9", + "vimeo/psalm": "^4|^5" }, "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" @@ -1995,42 +2504,64 @@ { "name": "Paragon Initiative Enterprises", "email": "security@paragonie.com", - "homepage": "https://paragonie.com" + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" } ], - "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", "keywords": [ - "csprng", - "polyfill", - "pseudorandom", - "random" - ], - "time": "2018-07-02T15:55:56+00:00" + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2024-05-08T12:36:18+00:00" }, { "name": "phar-io/manifest", - "version": "1.0.1", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0" + "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0", - "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "" }, "require": { "ext-dom": "*", + "ext-libxml": "*", "ext-phar": "*", - "phar-io/version": "^1.0.1", - "php": "^5.6 || ^7.0" + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -2060,24 +2591,34 @@ } ], "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", - "time": "2017-03-05T18:14:27+00:00" + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" }, { "name": "phar-io/version", - "version": "1.0.1", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/phar-io/version.git", - "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df" + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df", - "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": "^7.2 || ^8.0" }, "type": "library", "autoload": { @@ -2107,57 +2648,58 @@ } ], "description": "Library for handling version information and constraints", - "time": "2017-03-05T17:38:23+00:00" + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" }, { "name": "php-webdriver/webdriver", - "version": "1.8.2", + "version": "1.15.2", "source": { "type": "git", "url": "https://github.com/php-webdriver/php-webdriver.git", - "reference": "3308a70be084d6d7fd1ee5787b4c2e6eb4b70aab" + "reference": "998e499b786805568deaf8cbf06f4044f05d91bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/3308a70be084d6d7fd1ee5787b4c2e6eb4b70aab", - "reference": "3308a70be084d6d7fd1ee5787b4c2e6eb4b70aab", + "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/998e499b786805568deaf8cbf06f4044f05d91bf", + "reference": "998e499b786805568deaf8cbf06f4044f05d91bf", "shasum": "" }, "require": { "ext-curl": "*", "ext-json": "*", "ext-zip": "*", - "php": "^5.6 || ~7.0", + "php": "^7.3 || ^8.0", "symfony/polyfill-mbstring": "^1.12", - "symfony/process": "^2.8 || ^3.1 || ^4.0 || ^5.0" + "symfony/process": "^5.0 || ^6.0 || ^7.0" + }, + "replace": { + "facebook/webdriver": "*" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.0", - "jakub-onderka/php-parallel-lint": "^1.0", - "php-coveralls/php-coveralls": "^2.0", - "php-mock/php-mock-phpunit": "^1.1", - "phpunit/phpunit": "^5.7", - "sebastian/environment": "^1.3.4 || ^2.0 || ^3.0", - "sminnee/phpunit-mock-objects": "^3.4", + "ergebnis/composer-normalize": "^2.20.0", + "ondram/ci-detector": "^4.0", + "php-coveralls/php-coveralls": "^2.4", + "php-mock/php-mock-phpunit": "^2.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpunit/phpunit": "^9.3", "squizlabs/php_codesniffer": "^3.5", - "symfony/var-dumper": "^3.3 || ^4.0 || ^5.0" + "symfony/var-dumper": "^5.0 || ^6.0 || ^7.0" }, "suggest": { "ext-SimpleXML": "For Firefox profile creation" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.8.x-dev" - } - }, "autoload": { - "psr-4": { - "Facebook\\WebDriver\\": "lib/" - }, "files": [ "lib/Exception/TimeoutException.php" - ] + ], + "psr-4": { + "Facebook\\WebDriver\\": "lib/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2172,364 +2714,391 @@ "selenium", "webdriver" ], - "time": "2020-03-04T14:40:12+00:00" + "support": { + "issues": "https://github.com/php-webdriver/php-webdriver/issues", + "source": "https://github.com/php-webdriver/php-webdriver/tree/1.15.2" + }, + "time": "2024-11-21T15:12:59+00:00" }, { - "name": "phpcollection/phpcollection", - "version": "0.5.0", + "name": "phpunit/php-code-coverage", + "version": "10.1.16", "source": { "type": "git", - "url": "https://github.com/schmittjoh/php-collection.git", - "reference": "f2bcff45c0da7c27991bbc1f90f47c4b7fb434a6" + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-collection/zipball/f2bcff45c0da7c27991bbc1f90f47c4b7fb434a6", - "reference": "f2bcff45c0da7c27991bbc1f90f47c4b7fb434a6", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", "shasum": "" }, "require": { - "phpoption/phpoption": "1.*" + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" }, - "type": "library", + "require-dev": { + "phpunit/phpunit": "^10.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", "extra": { "branch-alias": { - "dev-master": "0.4-dev" + "dev-main": "10.1.x-dev" } }, "autoload": { - "psr-0": { - "PhpCollection": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "Apache2" + "BSD-3-Clause" ], "authors": [ { - "name": "Johannes M. Schmitt", - "email": "schmittjoh@gmail.com" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "General-Purpose Collection Library for PHP", + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", "keywords": [ - "collection", - "list", - "map", - "sequence", - "set" + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } ], - "time": "2015-05-17T12:39:23+00:00" + "time": "2024-08-22T04:31:57+00:00" }, { - "name": "phpdocumentor/reflection-common", - "version": "2.0.0", + "name": "phpunit/php-file-iterator", + "version": "4.1.0", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a" + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a", - "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "~6" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.x-dev" + "dev-main": "4.0-dev" } }, "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" + "filesystem", + "iterator" ], - "time": "2018-08-07T13:53:10+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "5.1.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e", - "reference": "cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e", - "shasum": "" - }, - "require": { - "ext-filter": "^7.1", - "php": "^7.2", - "phpdocumentor/reflection-common": "^2.0", - "phpdocumentor/type-resolver": "^1.0", - "webmozart/assert": "^1" - }, - "require-dev": { - "doctrine/instantiator": "^1", - "mockery/mockery": "^1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - }, + "funding": [ { - "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" + "url": "https://github.com/sebastianbergmann", + "type": "github" } ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2020-02-22T12:28:44+00:00" + "time": "2023-08-31T06:24:48+00:00" }, { - "name": "phpdocumentor/type-resolver", - "version": "1.1.0", + "name": "phpunit/php-invoker", + "version": "4.0.0", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "7462d5f123dfc080dfdf26897032a6513644fc95" + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/7462d5f123dfc080dfdf26897032a6513644fc95", - "reference": "7462d5f123dfc080dfdf26897032a6513644fc95", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", "shasum": "" }, "require": { - "php": "^7.2", - "phpdocumentor/reflection-common": "^2.0" + "php": ">=8.1" }, "require-dev": { - "ext-tokenizer": "^7.2", - "mockery/mockery": "~1" + "ext-pcntl": "*", + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-pcntl": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-main": "4.0-dev" } }, "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" } ], - "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "time": "2020-02-18T18:59:58+00:00" + "time": "2023-02-03T06:56:09+00:00" }, { - "name": "phpoption/phpoption", - "version": "1.7.2", + "name": "phpunit/php-text-template", + "version": "3.0.1", "source": { "type": "git", - "url": "https://github.com/schmittjoh/php-option.git", - "reference": "77f7c4d2e65413aff5b5a8cc8b3caf7a28d81959" + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/77f7c4d2e65413aff5b5a8cc8b3caf7a28d81959", - "reference": "77f7c4d2e65413aff5b5a8cc8b3caf7a28d81959", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", "shasum": "" }, "require": { - "php": "^5.5.9 || ^7.0" + "php": ">=8.1" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.3", - "phpunit/phpunit": "^4.8.35 || ^5.0 || ^6.0 || ^7.0" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.7-dev" + "dev-main": "3.0-dev" } }, "autoload": { - "psr-4": { - "PhpOption\\": "src/PhpOption/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "Apache-2.0" + "BSD-3-Clause" ], "authors": [ { - "name": "Johannes M. Schmitt", - "email": "schmittjoh@gmail.com" - }, - { - "name": "Graham Campbell", - "email": "graham@alt-three.com" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Option Type for PHP", + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", "keywords": [ - "language", - "option", - "php", - "type" + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } ], - "time": "2019-12-15T19:35:24+00:00" + "time": "2023-08-31T14:07:24+00:00" }, { - "name": "phpspec/prophecy", - "version": "v1.10.3", + "name": "phpunit/php-timer", + "version": "6.0.0", "source": { "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "451c3cd1418cf640de218914901e51b064abb093" + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093", - "reference": "451c3cd1418cf640de218914901e51b064abb093", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", - "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0", - "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0" + "php": ">=8.1" }, "require-dev": { - "phpspec/phpspec": "^2.5 || ^3.2", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.10.x-dev" + "dev-main": "6.0-dev" } }, "autoload": { - "psr-4": { - "Prophecy\\": "src/Prophecy" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "time": "2020-03-05T15:02:03+00:00" + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:57:52+00:00" }, { - "name": "phpunit/php-code-coverage", - "version": "6.0.5", + "name": "phpunit/phpunit", + "version": "10.5.44", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "4cab20a326d14de7575a8e235c70d879b569a57a" + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "1381c62769be4bb88fa4c5aec1366c7c66ca4f36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4cab20a326d14de7575a8e235c70d879b569a57a", - "reference": "4cab20a326d14de7575a8e235c70d879b569a57a", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1381c62769be4bb88fa4c5aec1366c7c66ca4f36", + "reference": "1381c62769be4bb88fa4c5aec1366c7c66ca4f36", "shasum": "" }, "require": { "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", "ext-xmlwriter": "*", - "php": "^7.1", - "phpunit/php-file-iterator": "^1.4.2", - "phpunit/php-text-template": "^1.2.1", - "phpunit/php-token-stream": "^3.0", - "sebastian/code-unit-reverse-lookup": "^1.0.1", - "sebastian/environment": "^3.1", - "sebastian/version": "^2.0.1", - "theseer/tokenizer": "^1.1" - }, - "require-dev": { - "phpunit/phpunit": "^7.0" + "myclabs/deep-copy": "^1.12.1", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.3", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.2", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.0", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" }, "suggest": { - "ext-xdebug": "^2.6.0" + "ext-soap": "To be able to generate mocks based on WSDL files" }, + "bin": [ + "phpunit" + ], "type": "library", "extra": { "branch-alias": { - "dev-master": "6.0-dev" + "dev-main": "10.5-dev" } }, "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], "classmap": [ "src/" ] @@ -2545,354 +3114,251 @@ "role": "lead" } ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", "keywords": [ - "coverage", + "phpunit", "testing", "xunit" ], - "time": "2018-05-28T11:49:20+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.44" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2025-01-31T07:00:38+00:00" }, { - "name": "phpunit/php-file-iterator", - "version": "1.4.5", + "name": "psr/cache", + "version": "3.0.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", - "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=8.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4.x-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Psr\\Cache\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "description": "Common interface for caching libraries", "keywords": [ - "filesystem", - "iterator" + "cache", + "psr", + "psr-6" ], - "time": "2017-11-27T13:52:08+00:00" + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" }, { - "name": "phpunit/php-text-template", - "version": "1.2.1", + "name": "psr/clock", + "version": "1.0.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^7.0 || ^8.0" }, "type": "library", "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Psr\\Clock\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", "keywords": [ - "template" + "clock", + "now", + "psr", + "psr-20", + "time" ], - "time": "2015-06-21T13:50:34+00:00" + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" }, { - "name": "phpunit/php-timer", - "version": "2.1.2", + "name": "psr/container", + "version": "2.0.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "1038454804406b0b5f5f520358e78c1c2f71501e" + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e", - "reference": "1038454804406b0b5f5f520358e78c1c2f71501e", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", "shasum": "" }, "require": { - "php": "^7.1" - }, - "require-dev": { - "phpunit/phpunit": "^7.0" + "php": ">=7.4.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.1-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Psr\\Container\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", "keywords": [ - "timer" + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" ], - "time": "2019-06-07T04:22:29+00:00" + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" }, { - "name": "phpunit/php-token-stream", - "version": "3.1.1", + "name": "psr/event-dispatcher", + "version": "1.0.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff" + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/995192df77f63a59e47f025390d2d1fdf8f425ff", - "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", "shasum": "" }, "require": { - "ext-tokenizer": "*", - "php": "^7.1" - }, - "require-dev": { - "phpunit/phpunit": "^7.0" + "php": ">=7.2.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Wrapper around PHP's tokenizer extension.", - "homepage": "https://github.com/sebastianbergmann/php-token-stream/", - "keywords": [ - "tokenizer" - ], - "time": "2019-09-17T06:23:10+00:00" - }, - { - "name": "phpunit/phpunit", - "version": "7.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "536f4d853c12d8189963435088e8ff7c0daeab2e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/536f4d853c12d8189963435088e8ff7c0daeab2e", - "reference": "536f4d853c12d8189963435088e8ff7c0daeab2e", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "myclabs/deep-copy": "^1.6.1", - "phar-io/manifest": "^1.0.1", - "phar-io/version": "^1.0", - "php": "^7.1", - "phpspec/prophecy": "^1.7", - "phpunit/php-code-coverage": "^6.0.1", - "phpunit/php-file-iterator": "^1.4.3", - "phpunit/php-text-template": "^1.2.1", - "phpunit/php-timer": "^2.0", - "phpunit/phpunit-mock-objects": "^6.0", - "sebastian/comparator": "^2.1", - "sebastian/diff": "^3.0", - "sebastian/environment": "^3.1", - "sebastian/exporter": "^3.1", - "sebastian/global-state": "^2.0", - "sebastian/object-enumerator": "^3.0.3", - "sebastian/resource-operations": "^1.0", - "sebastian/version": "^2.0.1" - }, - "require-dev": { - "ext-pdo": "*" - }, - "suggest": { - "ext-xdebug": "*", - "phpunit/php-invoker": "^2.0" - }, - "bin": [ - "phpunit" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "7.0-dev" + "psr-4": { + "Psr\\EventDispatcher\\": "src/" } }, - "autoload": { - "classmap": [ - "src/" - ] - }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" } ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", + "description": "Standard interfaces for event handling.", "keywords": [ - "phpunit", - "testing", - "xunit" + "events", + "psr", + "psr-14" ], - "time": "2018-03-26T07:36:48+00:00" - }, - { - "name": "phpunit/phpunit-mock-objects", - "version": "6.1.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "f9756fd4f43f014cb2dca98deeaaa8ce5500a36e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/f9756fd4f43f014cb2dca98deeaaa8ce5500a36e", - "reference": "f9756fd4f43f014cb2dca98deeaaa8ce5500a36e", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.5", - "php": "^7.1", - "phpunit/php-text-template": "^1.2.1", - "sebastian/exporter": "^3.1" - }, - "require-dev": { - "phpunit/phpunit": "^7.0" - }, - "suggest": { - "ext-soap": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "6.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Mock Object library for PHPUnit", - "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", - "keywords": [ - "mock", - "xunit" - ], - "abandoned": true, - "time": "2018-05-29T13:54:20+00:00" + "time": "2019-01-08T18:20:26+00:00" }, { - "name": "psr/cache", - "version": "1.0.1", + "name": "psr/http-client", + "version": "1.0.3", "source": { "type": "git", - "url": "https://github.com/php-fig/cache.git", - "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", - "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { @@ -2902,7 +3368,7 @@ }, "autoload": { "psr-4": { - "Psr\\Cache\\": "src/" + "Psr\\Http\\Client\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2912,33 +3378,39 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], - "description": "Common interface for caching libraries", + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", "keywords": [ - "cache", + "http", + "http-client", "psr", - "psr-6" + "psr-18" ], - "time": "2016-08-06T20:24:11+00:00" + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" }, { - "name": "psr/container", - "version": "1.0.0", + "name": "psr/http-factory", + "version": "1.1.0", "source": { "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { @@ -2948,7 +3420,7 @@ }, "autoload": { "psr-4": { - "Psr\\Container\\": "src/" + "Psr\\Http\\Message\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2958,41 +3430,46 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" ], - "time": "2017-02-14T16:28:37+00:00" + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" }, { "name": "psr/http-message", - "version": "1.0.1", + "version": "2.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -3007,7 +3484,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for HTTP messages", @@ -3020,34 +3497,37 @@ "request", "response" ], - "time": "2016-08-06T14:39:51+00:00" + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" }, { "name": "psr/log", - "version": "1.1.2", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801" + "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/446d54b4cb6bf489fc9d75f55843658e6f25d801", - "reference": "446d54b4cb6bf489fc9d75f55843658e6f25d801", + "url": "https://api.github.com/repos/php-fig/log/zipball/ef29f6d262798707a9edd554e2b82517ef3a9376", + "reference": "ef29f6d262798707a9edd554e2b82517ef3a9376", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=8.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { "psr-4": { - "Psr\\Log\\": "Psr/Log/" + "Psr\\Log\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -3057,7 +3537,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for logging libraries", @@ -3067,34 +3547,63 @@ "psr", "psr-3" ], - "time": "2019-11-01T11:05:21+00:00" + "support": { + "source": "https://github.com/php-fig/log/tree/2.0.0" + }, + "time": "2021-07-14T16:41:46+00:00" }, { - "name": "psr/simple-cache", - "version": "1.0.1", + "name": "psy/psysh", + "version": "v0.12.7", "source": { "type": "git", - "url": "https://github.com/php-fig/simple-cache.git", - "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + "url": "https://github.com/bobthecow/psysh.git", + "reference": "d73fa3c74918ef4522bb8a3bf9cab39161c4b57c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", - "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/d73fa3c74918ef4522bb8a3bf9cab39161c4b57c", + "reference": "d73fa3c74918ef4522bb8a3bf9cab39161c4b57c", "shasum": "" }, "require": { - "php": ">=5.3.0" + "ext-json": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + }, + "conflict": { + "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2" + }, + "suggest": { + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-pdo-sqlite": "The doc command requires SQLite to work.", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." }, + "bin": [ + "bin/psysh" + ], "type": "library", "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-main": "0.12.x-dev" } }, "autoload": { + "files": [ + "src/functions.php" + ], "psr-4": { - "Psr\\SimpleCache\\": "src/" + "Psy\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -3103,19 +3612,24 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "name": "Justin Hileman", + "email": "justin@justinhileman.info", + "homepage": "http://justinhileman.com" } ], - "description": "Common interfaces for simple caching", + "description": "An interactive shell for modern PHP.", + "homepage": "http://psysh.org", "keywords": [ - "cache", - "caching", - "psr", - "psr-16", - "simple-cache" + "REPL", + "console", + "interactive", + "shell" ], - "time": "2017-10-23T01:57:42+00:00" + "support": { + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.7" + }, + "time": "2024-12-10T01:58:33+00:00" }, { "name": "ralouphie/getallheaders", @@ -3155,66 +3669,64 @@ } ], "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, "time": "2019-03-08T08:55:37+00:00" }, { - "name": "ramsey/uuid", - "version": "3.9.3", + "name": "ramsey/collection", + "version": "2.0.0", "source": { "type": "git", - "url": "https://github.com/ramsey/uuid.git", - "reference": "7e1633a6964b48589b142d60542f9ed31bd37a92" + "url": "https://github.com/ramsey/collection.git", + "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/7e1633a6964b48589b142d60542f9ed31bd37a92", - "reference": "7e1633a6964b48589b142d60542f9ed31bd37a92", + "url": "https://api.github.com/repos/ramsey/collection/zipball/a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", "shasum": "" }, "require": { - "ext-json": "*", - "paragonie/random_compat": "^1 | ^2 | 9.99.99", - "php": "^5.4 | ^7 | ^8", - "symfony/polyfill-ctype": "^1.8" - }, - "replace": { - "rhumsaa/uuid": "self.version" + "php": "^8.1" }, "require-dev": { - "codeception/aspect-mock": "^1 | ^2", - "doctrine/annotations": "^1.2", - "goaop/framework": "1.0.0-alpha.2 | ^1 | ^2.1", - "jakub-onderka/php-parallel-lint": "^1", - "mockery/mockery": "^0.9.11 | ^1", - "moontoast/math": "^1.1", - "paragonie/random-lib": "^2", - "php-mock/php-mock-phpunit": "^0.3 | ^1.1", - "phpunit/phpunit": "^4.8 | ^5.4 | ^6.5", - "squizlabs/php_codesniffer": "^3.5" - }, - "suggest": { - "ext-ctype": "Provides support for PHP Ctype functions", - "ext-libsodium": "Provides the PECL libsodium extension for use with the SodiumRandomGenerator", - "ext-openssl": "Provides the OpenSSL extension for use with the OpenSslGenerator", - "ext-uuid": "Provides the PECL UUID extension for use with the PeclUuidTimeGenerator and PeclUuidRandomGenerator", - "moontoast/math": "Provides support for converting UUID to 128-bit integer (in string form).", - "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", - "ramsey/uuid-console": "A console application for generating UUIDs with ramsey/uuid", - "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.28.3", + "fakerphp/faker": "^1.21", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^1.0", + "mockery/mockery": "^1.5", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpcsstandards/phpcsutils": "^1.0.0-rc1", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5", + "psalm/plugin-mockery": "^1.1", + "psalm/plugin-phpunit": "^0.18.4", + "ramsey/coding-standard": "^2.0.3", + "ramsey/conventional-commits": "^1.3", + "vimeo/psalm": "^5.4" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "3.x-dev" + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" } }, "autoload": { "psr-4": { - "Ramsey\\Uuid\\": "src/" - }, - "files": [ - "src/functions.php" - ] + "Ramsey\\Collection\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3225,159 +3737,222 @@ "name": "Ben Ramsey", "email": "ben@benramsey.com", "homepage": "https://benramsey.com" - }, + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.0.0" + }, + "funding": [ { - "name": "Marijn Huizendveld", - "email": "marijn.huizendveld@gmail.com" + "url": "https://github.com/ramsey", + "type": "github" }, { - "name": "Thibaud Fabre", - "email": "thibaud@aztech.io" + "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", + "type": "tidelift" } ], - "description": "Formerly rhumsaa/uuid. A PHP 5.4+ library for generating RFC 4122 version 1, 3, 4, and 5 universally unique identifiers (UUID).", - "homepage": "https://github.com/ramsey/uuid", - "keywords": [ - "guid", - "identifier", - "uuid" - ], - "time": "2020-02-21T04:36:14+00:00" + "time": "2022-12-31T21:50:55+00:00" }, { - "name": "sebastian/code-unit-reverse-lookup", - "version": "1.0.1", + "name": "ramsey/uuid", + "version": "4.7.6", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" + "url": "https://github.com/ramsey/uuid.git", + "reference": "91039bc1faa45ba123c4328958e620d382ec7088" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", - "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088", + "reference": "91039bc1faa45ba123c4328958e620d382ec7088", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12", + "ext-json": "*", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" }, "require-dev": { - "phpunit/phpunit": "^5.7 || ^6.0" + "captainhook/captainhook": "^5.10", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "doctrine/annotations": "^1.8", + "ergebnis/composer-normalize": "^2.15", + "mockery/mockery": "^1.3", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.2", + "php-mock/php-mock-mockery": "^1.3", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpbench/phpbench": "^1.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^8.5 || ^9", + "ramsey/composer-repl": "^1.4", + "slevomat/coding-standard": "^8.4", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.9" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" + "captainhook": { + "force-install": true } }, "autoload": { - "classmap": [ - "src/" - ] + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], - "authors": [ + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.7.6" + }, + "funding": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", + "type": "tidelift" } ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "time": "2017-03-04T06:30:41+00:00" + "time": "2024-04-27T21:32:50+00:00" }, { - "name": "sebastian/comparator", - "version": "2.1.3", + "name": "react/promise", + "version": "v3.2.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9" + "url": "https://github.com/reactphp/promise.git", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9", - "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", "shasum": "" }, "require": { - "php": "^7.0", - "sebastian/diff": "^2.0 || ^3.0", - "sebastian/exporter": "^3.1" + "php": ">=7.1.0" }, "require-dev": { - "phpunit/phpunit": "^6.4" + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.1.x-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" }, { - "name": "Volker Dusch", - "email": "github@wallbash.com" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" }, { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" }, { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "https://github.com/sebastianbergmann/comparator", + "description": "A lightweight implementation of CommonJS Promises/A for PHP", "keywords": [ - "comparator", - "compare", - "equality" + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } ], - "time": "2018-02-01T13:46:46+00:00" + "time": "2024-05-24T10:39:05+00:00" }, { - "name": "sebastian/diff", - "version": "3.0.2", + "name": "sebastian/cli-parser", + "version": "2.0.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29" + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29", - "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", "shasum": "" }, "require": { - "php": "^7.1" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^7.5 || ^8.0", - "symfony/process": "^2 || ^3.3 || ^4" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "2.0-dev" } }, "autoload": { @@ -3391,48 +3966,106 @@ ], "authors": [ { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - }, + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:12:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff", - "udiff", - "unidiff", - "unified diff" + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } ], - "time": "2019-02-04T06:01:07+00:00" + "time": "2023-02-03T06:58:43+00:00" }, { - "name": "sebastian/environment", - "version": "3.1.0", + "name": "sebastian/code-unit-reverse-lookup", + "version": "3.0.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5" + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/cd0871b3975fb7fc44d11314fd1ee20925fce4f5", - "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", "shasum": "" }, "require": { - "php": "^7.0" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^6.1" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1.x-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -3450,41 +4083,48 @@ "email": "sebastian@phpunit.de" } ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", - "keywords": [ - "Xdebug", - "environment", - "hhvm" + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } ], - "time": "2017-07-01T08:51:00+00:00" + "time": "2023-02-03T06:59:15+00:00" }, { - "name": "sebastian/exporter", - "version": "3.1.2", + "name": "sebastian/comparator", + "version": "5.0.3", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e" + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e", - "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", + "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", "shasum": "" }, "require": { - "php": "^7.0", - "sebastian/recursion-context": "^3.0" + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" }, "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1.x-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -3509,50 +4149,56 @@ "name": "Volker Dusch", "email": "github@wallbash.com" }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - }, { "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" + "email": "bschussek@2bepublished.at" } ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", "keywords": [ - "export", - "exporter" + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } ], - "time": "2019-09-14T09:02:43+00:00" + "time": "2024-10-18T14:56:07+00:00" }, { - "name": "sebastian/global-state", - "version": "2.0.0", + "name": "sebastian/complexity", + "version": "3.2.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "68ff824baeae169ec9f2137158ee529584553799" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", - "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", "shasum": "" }, "require": { - "php": "^7.0" + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^6.0" - }, - "suggest": { - "ext-uopz": "*" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "3.2-dev" } }, "autoload": { @@ -3567,42 +4213,50 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", - "keywords": [ - "global state" + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } ], - "time": "2017-04-27T15:39:26+00:00" + "time": "2023-12-21T08:37:17+00:00" }, { - "name": "sebastian/object-enumerator", - "version": "3.0.3", + "name": "sebastian/diff", + "version": "5.1.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", - "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", "shasum": "" }, "require": { - "php": "^7.0", - "sebastian/object-reflector": "^1.1.1", - "sebastian/recursion-context": "^3.0" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0.x-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -3618,36 +4272,60 @@ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" } ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "time": "2017-08-03T12:35:26+00:00" + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:15:17+00:00" }, { - "name": "sebastian/object-reflector", - "version": "1.1.1", + "name": "sebastian/environment", + "version": "6.1.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "773f97c67f28de00d397be301821b06708fca0be" + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", - "reference": "773f97c67f28de00d397be301821b06708fca0be", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", "shasum": "" }, "require": { - "php": "^7.0" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-posix": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1-dev" + "dev-main": "6.1-dev" } }, "autoload": { @@ -3665,34 +4343,52 @@ "email": "sebastian@phpunit.de" } ], - "description": "Allows reflection of object attributes, including inherited and non-public ones", - "homepage": "https://github.com/sebastianbergmann/object-reflector/", - "time": "2017-03-29T09:07:27+00:00" + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-23T08:47:14+00:00" }, { - "name": "sebastian/recursion-context", - "version": "3.0.0", + "name": "sebastian/exporter", + "version": "5.1.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "955288482d97c19a372d3f31006ab3f37da47adf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", - "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf", + "reference": "955288482d97c19a372d3f31006ab3f37da47adf", "shasum": "" }, "require": { - "php": "^7.0" + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0.x-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -3705,44 +4401,73 @@ "BSD-3-Clause" ], "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, { "name": "Jeff Welch", "email": "whatthejeff@gmail.com" }, { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Volker Dusch", + "email": "github@wallbash.com" }, { "name": "Adam Harvey", "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" } ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2017-03-03T06:23:57+00:00" + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:17:12+00:00" }, { - "name": "sebastian/resource-operations", - "version": "1.0.0", + "name": "sebastian/global-state", + "version": "6.0.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", "shasum": "" }, "require": { - "php": ">=5.6.0" + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -3760,31 +4485,49 @@ "email": "sebastian@phpunit.de" } ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "time": "2015-07-28T20:34:47+00:00" + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:19:19+00:00" }, { - "name": "sebastian/version", - "version": "2.0.1", + "name": "sebastian/lines-of-code", + "version": "2.0.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", "shasum": "" }, "require": { - "php": ">=5.6" + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-main": "2.0-dev" } }, "autoload": { @@ -3803,333 +4546,334 @@ "role": "lead" } ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", - "time": "2016-10-03T07:35:21+00:00" + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:38:20+00:00" }, { - "name": "seld/jsonlint", - "version": "1.7.2", + "name": "sebastian/object-enumerator", + "version": "5.0.0", "source": { "type": "git", - "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "e2e5d290e4d2a4f0eb449f510071392e00e10d19" + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/e2e5d290e4d2a4f0eb449f510071392e00e10d19", - "reference": "e2e5d290e4d2a4f0eb449f510071392e00e10d19", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", "shasum": "" }, "require": { - "php": "^5.3 || ^7.0" + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + "phpunit/phpunit": "^10.0" }, - "bin": [ - "bin/jsonlint" - ], "type": "library", - "autoload": { - "psr-4": { - "Seld\\JsonLint\\": "src/Seld/JsonLint/" + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" } ], - "description": "JSON Linter", - "keywords": [ - "json", - "linter", - "parser", - "validator" - ], - "time": "2019-10-24T14:27:39+00:00" + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:08:32+00:00" }, { - "name": "seld/phar-utils", - "version": "1.1.0", + "name": "sebastian/object-reflector", + "version": "3.0.0", "source": { "type": "git", - "url": "https://github.com/Seldaek/phar-utils.git", - "reference": "8800503d56b9867d43d9c303b9cbcc26016e82f0" + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/8800503d56b9867d43d9c303b9cbcc26016e82f0", - "reference": "8800503d56b9867d43d9c303b9cbcc26016e82f0", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", "shasum": "" }, "require": { - "php": ">=5.3" + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-main": "3.0-dev" } }, "autoload": { - "psr-4": { - "Seld\\PharUtils\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" } ], - "description": "PHAR file format utilities, for when PHP phars you up", - "keywords": [ - "phar" + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } ], - "time": "2020-02-14T15:25:33+00:00" + "time": "2023-02-03T07:06:18+00:00" }, { - "name": "symfony/browser-kit", - "version": "v4.4.5", + "name": "sebastian/recursion-context", + "version": "5.0.0", "source": { "type": "git", - "url": "https://github.com/symfony/browser-kit.git", - "reference": "090ce406505149d6852a7c03b0346dec3b8cf612" + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "05909fb5bc7df4c52992396d0116aed689f93712" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/090ce406505149d6852a7c03b0346dec3b8cf612", - "reference": "090ce406505149d6852a7c03b0346dec3b8cf612", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712", + "reference": "05909fb5bc7df4c52992396d0116aed689f93712", "shasum": "" }, "require": { - "php": "^7.1.3", - "symfony/dom-crawler": "^3.4|^4.0|^5.0" + "php": ">=8.1" }, "require-dev": { - "symfony/css-selector": "^3.4|^4.0|^5.0", - "symfony/http-client": "^4.3|^5.0", - "symfony/mime": "^4.3|^5.0", - "symfony/process": "^3.4|^4.0|^5.0" - }, - "suggest": { - "symfony/process": "" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.4-dev" + "dev-main": "5.0-dev" } }, "autoload": { - "psr-4": { - "Symfony\\Component\\BrowserKit\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" } ], - "description": "Symfony BrowserKit Component", - "homepage": "https://symfony.com", - "time": "2020-02-23T10:00:59+00:00" + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:05:40+00:00" }, { - "name": "symfony/console", - "version": "v4.4.5", + "name": "sebastian/type", + "version": "4.0.0", "source": { "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "4fa15ae7be74e53f6ec8c83ed403b97e23b665e9" + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/4fa15ae7be74e53f6ec8c83ed403b97e23b665e9", - "reference": "4fa15ae7be74e53f6ec8c83ed403b97e23b665e9", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", "shasum": "" }, "require": { - "php": "^7.1.3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php73": "^1.8", - "symfony/service-contracts": "^1.1|^2" - }, - "conflict": { - "symfony/dependency-injection": "<3.4", - "symfony/event-dispatcher": "<4.3|>=5", - "symfony/lock": "<4.4", - "symfony/process": "<3.3" - }, - "provide": { - "psr/log-implementation": "1.0" + "php": ">=8.1" }, "require-dev": { - "psr/log": "~1.0", - "symfony/config": "^3.4|^4.0|^5.0", - "symfony/dependency-injection": "^3.4|^4.0|^5.0", - "symfony/event-dispatcher": "^4.3", - "symfony/lock": "^4.4|^5.0", - "symfony/process": "^3.4|^4.0|^5.0", - "symfony/var-dumper": "^4.3|^5.0" - }, - "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/lock": "", - "symfony/process": "" + "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.4-dev" + "dev-main": "4.0-dev" } }, "autoload": { - "psr-4": { - "Symfony\\Component\\Console\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" + }, + "funding": [ { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "url": "https://github.com/sebastianbergmann", + "type": "github" } ], - "description": "Symfony Console Component", - "homepage": "https://symfony.com", - "time": "2020-02-24T13:10:00+00:00" + "time": "2023-02-03T07:10:45+00:00" }, { - "name": "symfony/css-selector", - "version": "v4.4.5", + "name": "sebastian/version", + "version": "4.0.1", "source": { "type": "git", - "url": "https://github.com/symfony/css-selector.git", - "reference": "d0a6dd288fa8848dcc3d1f58b94de6a7cc5d2d22" + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/d0a6dd288fa8848dcc3d1f58b94de6a7cc5d2d22", - "reference": "d0a6dd288fa8848dcc3d1f58b94de6a7cc5d2d22", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", "shasum": "" }, "require": { - "php": "^7.1.3" + "php": ">=8.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.4-dev" + "dev-main": "4.0-dev" } }, "autoload": { - "psr-4": { - "Symfony\\Component\\CssSelector\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Jean-François Simon", - "email": "jeanfrancois.simon@sensiolabs.com" - }, + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" + }, + "funding": [ { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "url": "https://github.com/sebastianbergmann", + "type": "github" } ], - "description": "Symfony CssSelector Component", - "homepage": "https://symfony.com", - "time": "2020-02-04T09:01:01+00:00" + "time": "2023-02-07T11:34:05+00:00" }, { - "name": "symfony/dom-crawler", - "version": "v4.4.5", + "name": "seld/jsonlint", + "version": "1.11.0", "source": { "type": "git", - "url": "https://github.com/symfony/dom-crawler.git", - "reference": "11dcf08f12f29981bf770f097a5d64d65bce5929" + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/11dcf08f12f29981bf770f097a5d64d65bce5929", - "reference": "11dcf08f12f29981bf770f097a5d64d65bce5929", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/1748aaf847fc731cfad7725aec413ee46f0cc3a2", + "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2", "shasum": "" }, "require": { - "php": "^7.1.3", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.0" - }, - "conflict": { - "masterminds/html5": "<2.6" + "php": "^5.3 || ^7.0 || ^8.0" }, "require-dev": { - "masterminds/html5": "^2.6", - "symfony/css-selector": "^3.4|^4.0|^5.0" - }, - "suggest": { - "symfony/css-selector": "" + "phpstan/phpstan": "^1.11", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13" }, + "bin": [ + "bin/jsonlint" + ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { - "Symfony\\Component\\DomCrawler\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Seld\\JsonLint\\": "src/Seld/JsonLint/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4137,69 +4881,61 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "JSON Linter", + "keywords": [ + "json", + "linter", + "parser", + "validator" + ], + "support": { + "issues": "https://github.com/Seldaek/jsonlint/issues", + "source": "https://github.com/Seldaek/jsonlint/tree/1.11.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", + "type": "tidelift" } ], - "description": "Symfony DomCrawler Component", - "homepage": "https://symfony.com", - "time": "2020-02-29T10:05:28+00:00" + "time": "2024-07-11T14:55:45+00:00" }, { - "name": "symfony/event-dispatcher", - "version": "v4.4.5", + "name": "seld/phar-utils", + "version": "1.2.1", "source": { "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "4ad8e149799d3128621a3a1f70e92b9897a8930d" + "url": "https://github.com/Seldaek/phar-utils.git", + "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/4ad8e149799d3128621a3a1f70e92b9897a8930d", - "reference": "4ad8e149799d3128621a3a1f70e92b9897a8930d", + "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/ea2f4014f163c1be4c601b9b7bd6af81ba8d701c", + "reference": "ea2f4014f163c1be4c601b9b7bd6af81ba8d701c", "shasum": "" }, "require": { - "php": "^7.1.3", - "symfony/event-dispatcher-contracts": "^1.1" - }, - "conflict": { - "symfony/dependency-injection": "<3.4" - }, - "provide": { - "psr/event-dispatcher-implementation": "1.0", - "symfony/event-dispatcher-implementation": "1.1" - }, - "require-dev": { - "psr/log": "~1.0", - "symfony/config": "^3.4|^4.0|^5.0", - "symfony/dependency-injection": "^3.4|^4.0|^5.0", - "symfony/expression-language": "^3.4|^4.0|^5.0", - "symfony/http-foundation": "^3.4|^4.0|^5.0", - "symfony/service-contracts": "^1.1|^2", - "symfony/stopwatch": "^3.4|^4.0|^5.0" - }, - "suggest": { - "symfony/dependency-injection": "", - "symfony/http-kernel": "" + "php": ">=5.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.4-dev" + "dev-master": "1.x-dev" } }, "autoload": { "psr-4": { - "Symfony\\Component\\EventDispatcher\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Seld\\PharUtils\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4207,48 +4943,54 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" } ], - "description": "Symfony EventDispatcher Component", - "homepage": "https://symfony.com", - "time": "2020-02-04T09:32:40+00:00" + "description": "PHAR file format utilities, for when PHP phars you up", + "keywords": [ + "phar" + ], + "support": { + "issues": "https://github.com/Seldaek/phar-utils/issues", + "source": "https://github.com/Seldaek/phar-utils/tree/1.2.1" + }, + "time": "2022-08-31T10:31:18+00:00" }, { - "name": "symfony/event-dispatcher-contracts", - "version": "v1.1.7", + "name": "seld/signal-handler", + "version": "2.0.2", "source": { "type": "git", - "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "c43ab685673fb6c8d84220c77897b1d6cdbe1d18" + "url": "https://github.com/Seldaek/signal-handler.git", + "reference": "04a6112e883ad76c0ada8e4a9f7520bbfdb6bb98" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c43ab685673fb6c8d84220c77897b1d6cdbe1d18", - "reference": "c43ab685673fb6c8d84220c77897b1d6cdbe1d18", + "url": "https://api.github.com/repos/Seldaek/signal-handler/zipball/04a6112e883ad76c0ada8e4a9f7520bbfdb6bb98", + "reference": "04a6112e883ad76c0ada8e4a9f7520bbfdb6bb98", "shasum": "" }, "require": { - "php": "^7.1.3" + "php": ">=7.2.0" }, - "suggest": { - "psr/event-dispatcher": "", - "symfony/event-dispatcher-implementation": "" + "require-dev": { + "phpstan/phpstan": "^1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^7.5.20 || ^8.5.23", + "psr/log": "^1 || ^2 || ^3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1-dev" + "dev-main": "2.x-dev" } }, "autoload": { "psr-4": { - "Symfony\\Contracts\\EventDispatcher\\": "" + "Seld\\Signal\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -4257,57 +4999,65 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" } ], - "description": "Generic abstractions related to dispatching event", - "homepage": "https://symfony.com", + "description": "Simple unix signal handler that silently fails where signals are not supported for easy cross-platform development", "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" + "posix", + "sigint", + "signal", + "sigterm", + "unix" ], - "time": "2019-09-17T09:54:03+00:00" + "support": { + "issues": "https://github.com/Seldaek/signal-handler/issues", + "source": "https://github.com/Seldaek/signal-handler/tree/2.0.2" + }, + "time": "2023-09-03T09:24:00+00:00" }, { - "name": "symfony/filesystem", - "version": "v5.0.5", + "name": "spomky-labs/otphp", + "version": "11.3.0", "source": { "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "3afadc0f57cd74f86379d073e694b0f2cda2a88c" + "url": "https://github.com/Spomky-Labs/otphp.git", + "reference": "2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/3afadc0f57cd74f86379d073e694b0f2cda2a88c", - "reference": "3afadc0f57cd74f86379d073e694b0f2cda2a88c", + "url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33", + "reference": "2d8ccb5fc992b9cc65ef321fa4f00fefdb3f4b33", "shasum": "" }, "require": { - "php": "^7.2.5", - "symfony/polyfill-ctype": "~1.8" + "ext-mbstring": "*", + "paragonie/constant_time_encoding": "^2.0 || ^3.0", + "php": ">=8.1", + "psr/clock": "^1.0", + "symfony/deprecation-contracts": "^3.2" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0-dev" - } + "require-dev": { + "ekino/phpstan-banned-code": "^1.0", + "infection/infection": "^0.26|^0.27|^0.28|^0.29", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5.26|^10.0|^11.0", + "qossmic/deptrac-shim": "^1.0", + "rector/rector": "^1.0", + "symfony/phpunit-bridge": "^6.1|^7.0", + "symplify/easy-coding-standard": "^12.0" }, + "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Filesystem\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "OTPHP\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4315,44 +5065,89 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "All contributors", + "homepage": "https://github.com/Spomky-Labs/otphp/contributors" } ], - "description": "Symfony Filesystem Component", - "homepage": "https://symfony.com", - "time": "2020-01-21T08:40:24+00:00" + "description": "A PHP library for generating one time passwords according to RFC 4226 (HOTP Algorithm) and the RFC 6238 (TOTP Algorithm) and compatible with Google Authenticator", + "homepage": "https://github.com/Spomky-Labs/otphp", + "keywords": [ + "FreeOTP", + "RFC 4226", + "RFC 6238", + "google authenticator", + "hotp", + "otp", + "totp" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/otphp/issues", + "source": "https://github.com/Spomky-Labs/otphp/tree/11.3.0" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2024-06-12T11:22:32+00:00" }, { - "name": "symfony/finder", - "version": "v4.4.5", + "name": "symfony/console", + "version": "v6.4.17", "source": { "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "ea69c129aed9fdeca781d4b77eb20b62cf5d5357" + "url": "https://github.com/symfony/console.git", + "reference": "799445db3f15768ecc382ac5699e6da0520a0a04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/ea69c129aed9fdeca781d4b77eb20b62cf5d5357", - "reference": "ea69c129aed9fdeca781d4b77eb20b62cf5d5357", + "url": "https://api.github.com/repos/symfony/console/zipball/799445db3f15768ecc382ac5699e6da0520a0a04", + "reference": "799445db3f15768ecc382ac5699e6da0520a0a04", "shasum": "" }, "require": { - "php": "^7.1.3" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" }, + "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Finder\\": "" + "Symfony\\Component\\Console\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -4372,42 +5167,54 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Finder Component", + "description": "Eases the creation of beautiful and testable command line interfaces", "homepage": "https://symfony.com", - "time": "2020-02-14T07:42:58+00:00" + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-07T12:07:30+00:00" }, { - "name": "symfony/http-foundation", - "version": "v5.0.5", + "name": "symfony/css-selector", + "version": "v7.2.0", "source": { "type": "git", - "url": "https://github.com/symfony/http-foundation.git", - "reference": "6f9c2ba72f4295d7ce6cf9f79dbb18036291d335" + "url": "https://github.com/symfony/css-selector.git", + "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6f9c2ba72f4295d7ce6cf9f79dbb18036291d335", - "reference": "6f9c2ba72f4295d7ce6cf9f79dbb18036291d335", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2", "shasum": "" }, "require": { - "php": "^7.2.5", - "symfony/mime": "^4.4|^5.0", - "symfony/polyfill-mbstring": "~1.1" - }, - "require-dev": { - "predis/predis": "~1.0", - "symfony/expression-language": "^4.4|^5.0" + "php": ">=8.2" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0-dev" - } - }, "autoload": { "psr-4": { - "Symfony\\Component\\HttpFoundation\\": "" + "Symfony\\Component\\CssSelector\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -4422,53 +5229,66 @@ "name": "Fabien Potencier", "email": "fabien@symfony.com" }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony HttpFoundation Component", + "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", - "time": "2020-02-14T07:43:07+00:00" + "support": { + "source": "https://github.com/symfony/css-selector/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" }, { - "name": "symfony/mime", - "version": "v5.0.5", + "name": "symfony/deprecation-contracts", + "version": "v3.5.1", "source": { "type": "git", - "url": "https://github.com/symfony/mime.git", - "reference": "9b3e5b5e58c56bbd76628c952d2b78556d305f3c" + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/9b3e5b5e58c56bbd76628c952d2b78556d305f3c", - "reference": "9b3e5b5e58c56bbd76628c952d2b78556d305f3c", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", "shasum": "" }, "require": { - "php": "^7.2.5", - "symfony/polyfill-intl-idn": "^1.10", - "symfony/polyfill-mbstring": "^1.0" - }, - "conflict": { - "symfony/mailer": "<4.4" - }, - "require-dev": { - "egulias/email-validator": "^2.1.10", - "symfony/dependency-injection": "^4.4|^5.0" + "php": ">=8.1" }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { - "dev-master": "5.0-dev" + "dev-main": "3.5-dev" } }, "autoload": { - "psr-4": { - "Symfony\\Component\\Mime\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" + "files": [ + "function.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -4477,54 +5297,67 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "A library to manipulate MIME messages", + "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", - "keywords": [ - "mime", - "mime-type" + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } ], - "time": "2020-02-04T09:41:09+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { - "name": "symfony/polyfill-ctype", - "version": "v1.14.0", + "name": "symfony/dotenv", + "version": "v6.4.16", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "fbdeaec0df06cf3d51c93de80c7eb76e271f5a38" + "url": "https://github.com/symfony/dotenv.git", + "reference": "1ac5e7e7e862d4d574258daf08bd569ba926e4a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/fbdeaec0df06cf3d51c93de80c7eb76e271f5a38", - "reference": "fbdeaec0df06cf3d51c93de80c7eb76e271f5a38", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/1ac5e7e7e862d4d574258daf08bd569ba926e4a5", + "reference": "1ac5e7e7e862d4d574258daf08bd569ba926e4a5", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=8.1" }, - "suggest": { - "ext-ctype": "For best performance" + "conflict": { + "symfony/console": "<5.4", + "symfony/process": "<5.4" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.14-dev" - } + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0" }, + "type": "library", "autoload": { "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" + "Symfony\\Component\\Dotenv\\": "" }, - "files": [ - "bootstrap.php" + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -4533,58 +5366,83 @@ ], "authors": [ { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for ctype functions", + "description": "Registers environment variables from a .env file", "homepage": "https://symfony.com", "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" + "dotenv", + "env", + "environment" + ], + "support": { + "source": "https://github.com/symfony/dotenv/tree/v6.4.16" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } ], - "time": "2020-01-13T11:15:53+00:00" + "time": "2024-11-27T11:08:19+00:00" }, { - "name": "symfony/polyfill-intl-idn", - "version": "v1.14.0", + "name": "symfony/event-dispatcher", + "version": "v7.2.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "6842f1a39cf7d580655688069a03dd7cd83d244a" + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/6842f1a39cf7d580655688069a03dd7cd83d244a", - "reference": "6842f1a39cf7d580655688069a03dd7cd83d244a", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/910c5db85a5356d0fea57680defec4e99eb9c8c1", + "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1", "shasum": "" }, "require": { - "php": ">=5.3.3", - "symfony/polyfill-mbstring": "^1.3", - "symfony/polyfill-php72": "^1.10" + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" }, - "suggest": { - "ext-intl": "For best performance" + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.14-dev" - } + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0" }, + "type": "library", "autoload": { "psr-4": { - "Symfony\\Polyfill\\Intl\\Idn\\": "" + "Symfony\\Component\\EventDispatcher\\": "" }, - "files": [ - "bootstrap.php" + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -4593,59 +5451,67 @@ ], "authors": [ { - "name": "Laurent Bassin", - "email": "laurent@bassin.info" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "idn", - "intl", - "polyfill", - "portable", - "shim" + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } ], - "time": "2020-01-17T12:01:36+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { - "name": "symfony/polyfill-mbstring", - "version": "v1.14.0", + "name": "symfony/event-dispatcher-contracts", + "version": "v3.5.1", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "34094cfa9abe1f0f14f48f490772db7a775559f2" + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/34094cfa9abe1f0f14f48f490772db7a775559f2", - "reference": "34094cfa9abe1f0f14f48f490772db7a775559f2", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", "shasum": "" }, "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-mbstring": "For best performance" + "php": ">=8.1", + "psr/event-dispatcher": "^1" }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { - "dev-master": "1.14-dev" + "dev-main": "3.5-dev" } }, "autoload": { "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - }, - "files": [ - "bootstrap.php" - ] + "Symfony\\Contracts\\EventDispatcher\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4661,46 +5527,64 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for the Mbstring extension", + "description": "Generic abstractions related to dispatching event", "homepage": "https://symfony.com", "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } ], - "time": "2020-01-13T11:15:53+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { - "name": "symfony/polyfill-php72", - "version": "v1.14.0", + "name": "symfony/filesystem", + "version": "v7.2.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "46ecacf4751dd0dc81e4f6bf01dbf9da1dc1dadf" + "url": "https://github.com/symfony/filesystem.git", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/46ecacf4751dd0dc81e4f6bf01dbf9da1dc1dadf", - "reference": "46ecacf4751dd0dc81e4f6bf01dbf9da1dc1dadf", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.14-dev" - } + "require-dev": { + "symfony/process": "^6.4|^7.0" }, + "type": "library", "autoload": { "psr-4": { - "Symfony\\Polyfill\\Php72\\": "" + "Symfony\\Component\\Filesystem\\": "" }, - "files": [ - "bootstrap.php" + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -4709,56 +5593,62 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } ], - "time": "2020-01-13T11:15:53+00:00" + "time": "2024-10-25T15:15:23+00:00" }, { - "name": "symfony/polyfill-php73", - "version": "v1.14.0", + "name": "symfony/finder", + "version": "v6.4.17", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "5e66a0fa1070bf46bec4bea7962d285108edd675" + "url": "https://github.com/symfony/finder.git", + "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/5e66a0fa1070bf46bec4bea7962d285108edd675", - "reference": "5e66a0fa1070bf46bec4bea7962d285108edd675", + "url": "https://api.github.com/repos/symfony/finder/zipball/1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7", + "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=8.1" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.14-dev" - } + "require-dev": { + "symfony/filesystem": "^6.0|^7.0" }, + "type": "library", "autoload": { "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" + "Symfony\\Component\\Finder\\": "" }, - "files": [ - "bootstrap.php" - ], - "classmap": [ - "Resources/stubs" + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -4767,50 +5657,76 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" + "support": { + "source": "https://github.com/symfony/finder/tree/v6.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } ], - "time": "2020-01-13T11:15:53+00:00" + "time": "2024-12-29T13:51:37+00:00" }, { - "name": "symfony/process", - "version": "v4.4.5", + "name": "symfony/mime", + "version": "v6.4.18", "source": { "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "bf9166bac906c9e69fb7a11d94875e7ced97bcd7" + "url": "https://github.com/symfony/mime.git", + "reference": "917d77981eb1ea963608d5cda4d9c0cf72eaa68e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/bf9166bac906c9e69fb7a11d94875e7ced97bcd7", - "reference": "bf9166bac906c9e69fb7a11d94875e7ced97bcd7", + "url": "https://api.github.com/repos/symfony/mime/zipball/917d77981eb1ea963608d5cda4d9c0cf72eaa68e", + "reference": "917d77981eb1ea963608d5cda4d9c0cf72eaa68e", "shasum": "" }, "require": { - "php": "^7.1.3" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<5.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.4|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3" }, + "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Process\\": "" + "Symfony\\Component\\Mime\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -4830,40 +5746,67 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Process Component", + "description": "Allows manipulating MIME messages", "homepage": "https://symfony.com", - "time": "2020-02-07T20:06:44+00:00" + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v6.4.18" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-23T13:10:52+00:00" }, { - "name": "symfony/service-contracts", - "version": "v2.0.1", + "name": "symfony/polyfill-ctype", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "144c5e51266b281231e947b51223ba14acf1a749" + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/144c5e51266b281231e947b51223ba14acf1a749", - "reference": "144c5e51266b281231e947b51223ba14acf1a749", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": "^7.2.5", - "psr/container": "^1.0" + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" }, "suggest": { - "symfony/service-implementation": "" + "ext-ctype": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.0-dev" + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Contracts\\Service\\": "" + "Symfony\\Polyfill\\Ctype\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -4872,66 +5815,75 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Generic abstractions related to writing services", + "description": "Symfony polyfill for ctype functions", "homepage": "https://symfony.com", "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } ], - "time": "2019-11-18T17:27:11+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/yaml", - "version": "v4.4.5", + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "94d005c176db2080e98825d98e01e8b311a97a88" + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/94d005c176db2080e98825d98e01e8b311a97a88", - "reference": "94d005c176db2080e98825d98e01e8b311a97a88", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", "shasum": "" }, "require": { - "php": "^7.1.3", - "symfony/polyfill-ctype": "~1.8" - }, - "conflict": { - "symfony/console": "<3.4" - }, - "require-dev": { - "symfony/console": "^3.4|^4.0|^5.0" + "php": ">=7.2" }, "suggest": { - "symfony/console": "For validating YAML files using the lint command" + "ext-intl": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "4.4-dev" + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Component\\Yaml\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4939,137 +5891,243 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Yaml Component", + "description": "Symfony polyfill for intl's grapheme_* functions", "homepage": "https://symfony.com", - "time": "2020-02-03T10:46:43+00:00" + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "theseer/tokenizer", - "version": "1.1.3", + "name": "symfony/polyfill-intl-idn", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/theseer/tokenizer.git", - "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9" + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9", - "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773", + "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": "^7.0" + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" }, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { - "classmap": [ - "src/" - ] + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "time": "2019-06-13T22:48:21+00:00" + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "vlucas/phpdotenv", - "version": "v2.6.1", + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "2a7dcf7e3e02dc5e701004e51a6f304b713107d5" + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/2a7dcf7e3e02dc5e701004e51a6f304b713107d5", - "reference": "2a7dcf7e3e02dc5e701004e51a6f304b713107d5", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "php": ">=5.3.9", - "symfony/polyfill-ctype": "^1.9" + "php": ">=7.2" }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.0" + "suggest": { + "ext-intl": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.6-dev" + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Dotenv\\": "src/" - } + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Vance Lucas", - "email": "vance@vancelucas.com", - "homepage": "http://www.vancelucas.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", "keywords": [ - "dotenv", - "env", - "environment" + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } ], - "time": "2019-01-29T11:11:52+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "webmozart/assert", - "version": "1.7.0", + "name": "symfony/polyfill-mbstring", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/webmozart/assert.git", - "reference": "aed98a490f9a8f78468232db345ab9cf606cf598" + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/aed98a490f9a8f78468232db345ab9cf606cf598", - "reference": "aed98a490f9a8f78468232db345ab9cf606cf598", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0", - "symfony/polyfill-ctype": "^1.8" + "php": ">=7.2" }, - "conflict": { - "vimeo/psalm": "<3.6.0" + "provide": { + "ext-mbstring": "*" }, - "require-dev": { - "phpunit/phpunit": "^4.8.36 || ^7.5.13" + "suggest": { + "ext-mbstring": "For best performance" }, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Webmozart\\Assert\\": "src/" + "Symfony\\Polyfill\\Mbstring\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -5078,40 +6136,75 @@ ], "authors": [ { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Assertions to validate method input/output with nice error messages.", + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", "keywords": [ - "assert", - "check", - "validate" + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } ], - "time": "2020-02-14T12:15:55+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "weew/helpers-array", - "version": "v1.3.1", + "name": "symfony/polyfill-php73", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/weew/helpers-array.git", - "reference": "9bff63111f9765b4277750db8d276d92b3e16ed0" + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/weew/helpers-array/zipball/9bff63111f9765b4277750db8d276d92b3e16ed0", - "reference": "9bff63111f9765b4277750db8d276d92b3e16ed0", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", "shasum": "" }, - "require-dev": { - "phpunit/phpunit": "^4.7", - "satooshi/php-coveralls": "^0.6.1" + "require": { + "php": ">=7.2" }, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { "files": [ - "src/array.php" + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -5120,64 +6213,74 @@ ], "authors": [ { - "name": "Maxim Kott", - "email": "maximkott@gmail.com" - } + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } ], - "description": "Useful collection of php array helpers.", - "time": "2016-07-21T11:18:01+00:00" - } - ], - "packages-dev": [ + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, { - "name": "brainmaestro/composer-git-hooks", - "version": "v2.8.3", + "name": "symfony/polyfill-php80", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/BrainMaestro/composer-git-hooks.git", - "reference": "97888dd34e900931117747cd34a42fdfcf271142" + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/BrainMaestro/composer-git-hooks/zipball/97888dd34e900931117747cd34a42fdfcf271142", - "reference": "97888dd34e900931117747cd34a42fdfcf271142", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", "shasum": "" }, "require": { - "php": "^5.6 || >=7.0", - "symfony/console": "^3.2 || ^4.0 || ^5.0" + "php": ">=7.2" }, - "require-dev": { - "ext-json": "*", - "friendsofphp/php-cs-fixer": "^2.9", - "phpunit/phpunit": "^5.7 || ^7.0" - }, - "bin": [ - "cghooks" - ], "type": "library", "extra": { - "hooks": { - "pre-commit": "composer check-style", - "pre-push": [ - "composer test", - "appver=$(grep -o -E '\\d.\\d.\\d' cghooks)", - "tag=$(git describe --tags --abbrev=0)", - "if [ \"$tag\" != \"v$appver\" ]; then", - "echo \"The most recent tag $tag does not match the application version $appver\\n\"", - "tag=${tag#v}", - "sed -i -E \"s/$appver/$tag/\" cghooks", - "exit 1", - "fi" - ] + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "BrainMaestro\\GitHooks\\": "src/" + "Symfony\\Polyfill\\Php80\\": "" }, - "files": [ - "src/helpers.php" + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -5186,48 +6289,78 @@ ], "authors": [ { - "name": "Ezinwa Okpoechi", - "email": "brainmaestro@outlook.com" + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Easily manage git hooks in your composer config", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", "keywords": [ - "HOOK", - "composer", - "git" + "compatibility", + "polyfill", + "portable", + "shim" ], - "time": "2019-12-09T09:49:20+00:00" + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "codacy/coverage", - "version": "1.4.3", + "name": "symfony/polyfill-php81", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/codacy/php-codacy-coverage.git", - "reference": "1852ca987c91ef466ebcfdbdd4e1788b653eaf1d" + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/codacy/php-codacy-coverage/zipball/1852ca987c91ef466ebcfdbdd4e1788b653eaf1d", - "reference": "1852ca987c91ef466ebcfdbdd4e1788b653eaf1d", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", "shasum": "" }, "require": { - "gitonomy/gitlib": ">=1.0", - "php": ">=5.3.3", - "symfony/console": "~2.5|~3.0|~4.0|~5.0" - }, - "require-dev": { - "clue/phar-composer": "^1.1", - "phpunit/phpunit": "~6.5" + "php": ">=7.2" }, - "bin": [ - "bin/codacycoverage" - ], "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, "classmap": [ - "src/" + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -5236,45 +6369,66 @@ ], "authors": [ { - "name": "Jakob Pupke", - "email": "jakob.pupke@gmail.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "description": "Sends PHP test coverage information to Codacy.", - "homepage": "https://github.com/codacy/php-codacy-coverage", - "abandoned": true, - "time": "2020-01-10T10:52:12+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "codeception/aspect-mock", - "version": "3.1.0", + "name": "symfony/process", + "version": "v6.4.15", "source": { "type": "git", - "url": "https://github.com/Codeception/AspectMock.git", - "reference": "72fc3d6877ede7ff255ee2c96b7b57d4f87d0f30" + "url": "https://github.com/symfony/process.git", + "reference": "3cb242f059c14ae08591c5c4087d1fe443564392" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/AspectMock/zipball/72fc3d6877ede7ff255ee2c96b7b57d4f87d0f30", - "reference": "72fc3d6877ede7ff255ee2c96b7b57d4f87d0f30", + "url": "https://api.github.com/repos/symfony/process/zipball/3cb242f059c14ae08591c5c4087d1fe443564392", + "reference": "3cb242f059c14ae08591c5c4087d1fe443564392", "shasum": "" }, "require": { - "goaop/framework": "^2.2.0 | ^3.0.0-RC2", - "php": ">=7.0.0", - "phpunit/phpunit": "> 6.0.0", - "symfony/finder": ">=2.4 <6.0" - }, - "require-dev": { - "codeception/codeception": "^4.0", - "codeception/specify": "^1.0", - "codeception/verify": "^1.2" + "php": ">=8.1" }, "type": "library", "autoload": { - "psr-0": { - "AspectMock": "src/" - } + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -5282,48 +6436,74 @@ ], "authors": [ { - "name": "Michael Bodnarchuk", - "email": "davert@codeception.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Experimental Mocking Framework powered by Aspects", - "time": "2020-02-29T15:39:49+00:00" + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v6.4.15" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-06T14:19:14+00:00" }, { - "name": "gitonomy/gitlib", - "version": "v1.2.0", + "name": "symfony/service-contracts", + "version": "v3.5.1", "source": { "type": "git", - "url": "https://github.com/gitonomy/gitlib.git", - "reference": "a0bea921266ad1c9626d712e7f8687dcc08ca528" + "url": "https://github.com/symfony/service-contracts.git", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/gitonomy/gitlib/zipball/a0bea921266ad1c9626d712e7f8687dcc08ca528", - "reference": "a0bea921266ad1c9626d712e7f8687dcc08ca528", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0", - "symfony/process": "^3.4|^4.0|^5.0" + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" }, - "require-dev": { - "phpunit/phpunit": "^5.7|^6.5|^7.0", - "psr/log": "^1.0" - }, - "suggest": { - "psr/log": "Required to use loggers for reporting of execution" + "conflict": { + "ext-psr": "<1.1|>=2" }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { - "dev-master": "1.2-dev" + "dev-main": "3.5-dev" } }, "autoload": { "psr-4": { - "Gitonomy\\Git\\": "src/Gitonomy/Git/" - } + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -5331,71 +6511,86 @@ ], "authors": [ { - "name": "Graham Campbell", - "email": "graham@alt-three.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { - "name": "Julien Didier", - "email": "genzo.wm@gmail.com" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" }, { - "name": "Grégoire Pineau", - "email": "lyrixx@lyrixx.info" + "url": "https://github.com/fabpot", + "type": "github" }, { - "name": "Alexandre Salomé", - "email": "alexandre.salome@gmail.com" + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "description": "Library for accessing git", - "homepage": "http://gitonomy.com", - "time": "2019-12-08T12:42:25+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { - "name": "goaop/framework", - "version": "2.2.0", + "name": "symfony/string", + "version": "v7.2.0", "source": { "type": "git", - "url": "https://github.com/goaop/framework.git", - "reference": "152abbffffcba72d2d159b892deb40b0829d0f28" + "url": "https://github.com/symfony/string.git", + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/goaop/framework/zipball/152abbffffcba72d2d159b892deb40b0829d0f28", - "reference": "152abbffffcba72d2d159b892deb40b0829d0f28", + "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", + "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", "shasum": "" }, "require": { - "doctrine/annotations": "^1.2.3", - "doctrine/cache": "^1.5", - "goaop/parser-reflection": "~1.4", - "jakubledl/dissect": "~1.0", - "php": ">=5.6.0" + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" }, - "require-dev": { - "adlawson/vfs": "^0.12", - "doctrine/orm": "^2.5", - "phpunit/phpunit": "^5.7", - "symfony/console": "^2.7|^3.0", - "symfony/filesystem": "^3.3", - "symfony/process": "^3.3" + "conflict": { + "symfony/translation-contracts": "<2.5" }, - "suggest": { - "symfony/console": "Enables the usage of the command-line tool." + "require-dev": { + "symfony/emoji": "^7.1", + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" }, - "bin": [ - "bin/aspect" - ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, "autoload": { + "files": [ + "Resources/functions.php" + ], "psr-4": { - "Go\\": "src/" - } + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -5403,56 +6598,85 @@ ], "authors": [ { - "name": "Lisachenko Alexander", - "homepage": "https://github.com/lisachenko" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Framework for aspect-oriented programming in PHP.", - "homepage": "http://go.aopphp.com/", + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", "keywords": [ - "aop", - "aspect", - "library", - "php" + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" ], - "time": "2018-01-05T23:07:51+00:00" + "support": { + "source": "https://github.com/symfony/string/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-13T13:31:26+00:00" }, { - "name": "goaop/parser-reflection", - "version": "1.4.1", + "name": "symfony/var-dumper", + "version": "v7.2.3", "source": { "type": "git", - "url": "https://github.com/goaop/parser-reflection.git", - "reference": "d9c1dcc7ce4a5284fe3530e011faf9c9c10e1166" + "url": "https://github.com/symfony/var-dumper.git", + "reference": "82b478c69745d8878eb60f9a049a4d584996f73a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/goaop/parser-reflection/zipball/d9c1dcc7ce4a5284fe3530e011faf9c9c10e1166", - "reference": "d9c1dcc7ce4a5284fe3530e011faf9c9c10e1166", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/82b478c69745d8878eb60f9a049a4d584996f73a", + "reference": "82b478c69745d8878eb60f9a049a4d584996f73a", "shasum": "" }, "require": { - "nikic/php-parser": "^1.2|^2.0|^3.0", - "php": ">=5.6.0" + "php": ">=8.2", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" }, "require-dev": { - "phpunit/phpunit": "~4.0" + "ext-iconv": "*", + "symfony/console": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0", + "twig/twig": "^3.12" }, + "bin": [ + "Resources/bin/var-dump-server" + ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, "autoload": { - "psr-4": { - "Go\\ParserReflection\\": "src" - }, "files": [ - "src/bootstrap.php" + "Resources/functions/dump.php" ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, "exclude-from-classmap": [ - "/tests/" + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -5461,76 +6685,75 @@ ], "authors": [ { - "name": "Alexander Lisachenko", - "email": "lisachenko.it@gmail.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.2.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "description": "Provides reflection information, based on raw source", - "time": "2018-03-19T15:57:41+00:00" + "time": "2025-01-17T11:39:41+00:00" }, { - "name": "guzzle/guzzle", - "version": "v3.8.1", + "name": "symfony/yaml", + "version": "v7.2.3", "source": { "type": "git", - "url": "https://github.com/guzzle/guzzle.git", - "reference": "4de0618a01b34aa1c8c33a3f13f396dcd3882eba" + "url": "https://github.com/symfony/yaml.git", + "reference": "ac238f173df0c9c1120f862d0f599e17535a87ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/4de0618a01b34aa1c8c33a3f13f396dcd3882eba", - "reference": "4de0618a01b34aa1c8c33a3f13f396dcd3882eba", + "url": "https://api.github.com/repos/symfony/yaml/zipball/ac238f173df0c9c1120f862d0f599e17535a87ec", + "reference": "ac238f173df0c9c1120f862d0f599e17535a87ec", "shasum": "" }, "require": { - "ext-curl": "*", - "php": ">=5.3.3", - "symfony/event-dispatcher": ">=2.1" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "^1.8" }, - "replace": { - "guzzle/batch": "self.version", - "guzzle/cache": "self.version", - "guzzle/common": "self.version", - "guzzle/http": "self.version", - "guzzle/inflection": "self.version", - "guzzle/iterator": "self.version", - "guzzle/log": "self.version", - "guzzle/parser": "self.version", - "guzzle/plugin": "self.version", - "guzzle/plugin-async": "self.version", - "guzzle/plugin-backoff": "self.version", - "guzzle/plugin-cache": "self.version", - "guzzle/plugin-cookie": "self.version", - "guzzle/plugin-curlauth": "self.version", - "guzzle/plugin-error-response": "self.version", - "guzzle/plugin-history": "self.version", - "guzzle/plugin-log": "self.version", - "guzzle/plugin-md5": "self.version", - "guzzle/plugin-mock": "self.version", - "guzzle/plugin-oauth": "self.version", - "guzzle/service": "self.version", - "guzzle/stream": "self.version" + "conflict": { + "symfony/console": "<6.4" }, "require-dev": { - "doctrine/cache": "*", - "monolog/monolog": "1.*", - "phpunit/phpunit": "3.7.*", - "psr/log": "1.0.*", - "symfony/class-loader": "*", - "zendframework/zend-cache": "<2.3", - "zendframework/zend-log": "<2.3" + "symfony/console": "^6.4|^7.0" }, + "bin": [ + "Resources/bin/yaml-lint" + ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.8-dev" - } - }, "autoload": { - "psr-0": { - "Guzzle": "src/", - "Guzzle\\Tests": "tests/" - } + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -5538,159 +6761,225 @@ ], "authors": [ { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { - "name": "Guzzle Community", - "homepage": "https://github.com/guzzle/guzzle/contributors" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients", - "homepage": "http://guzzlephp.org/", - "keywords": [ - "client", - "curl", - "framework", - "http", - "http client", - "rest", - "web service" + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.2.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } ], - "abandoned": "guzzlehttp/guzzle", - "time": "2014-01-28T22:29:15+00:00" + "time": "2025-01-07T12:55:42+00:00" }, { - "name": "jakubledl/dissect", - "version": "v1.0.1", + "name": "theseer/tokenizer", + "version": "1.2.3", "source": { "type": "git", - "url": "https://github.com/jakubledl/dissect.git", - "reference": "d3a391de31e45a247e95cef6cf58a91c05af67c4" + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jakubledl/dissect/zipball/d3a391de31e45a247e95cef6cf58a91c05af67c4", - "reference": "d3a391de31e45a247e95cef6cf58a91c05af67c4", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "" }, "require": { - "php": ">=5.3.3" + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" }, - "require-dev": { - "symfony/console": "~2.1" + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] }, - "suggest": { - "symfony/console": "for the command-line tool" + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + }, + { + "name": "weew/helpers-array", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/weew/helpers-array.git", + "reference": "9bff63111f9765b4277750db8d276d92b3e16ed0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/weew/helpers-array/zipball/9bff63111f9765b4277750db8d276d92b3e16ed0", + "reference": "9bff63111f9765b4277750db8d276d92b3e16ed0", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "^4.7", + "satooshi/php-coveralls": "^0.6.1" }, - "bin": [ - "bin/dissect.php", - "bin/dissect" - ], "type": "library", "autoload": { - "psr-0": { - "Dissect": [ - "src/" - ] - } + "files": [ + "src/array.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "unlicense" + "MIT" ], "authors": [ { - "name": "Jakub Lédl", - "email": "jakubledl@gmail.com" + "name": "Maxim Kott", + "email": "maximkott@gmail.com" } ], - "description": "Lexing and parsing in pure PHP", - "homepage": "https://github.com/jakubledl/dissect", - "keywords": [ - "ast", - "lexing", - "parser", - "parsing" - ], - "time": "2013-01-29T21:29:14+00:00" - }, + "description": "Useful collection of php array helpers.", + "support": { + "issues": "https://github.com/weew/helpers-array/issues", + "source": "https://github.com/weew/helpers-array/tree/master" + }, + "time": "2016-07-21T11:18:01+00:00" + } + ], + "packages-dev": [ { - "name": "nikic/php-parser", - "version": "v3.1.5", + "name": "brainmaestro/composer-git-hooks", + "version": "v3.0.0", "source": { "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "bb87e28e7d7b8d9a7fda231d37457c9210faf6ce" + "url": "https://github.com/BrainMaestro/composer-git-hooks.git", + "reference": "684dc85f480268baf5e13f39a3cc494eeb2536e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/bb87e28e7d7b8d9a7fda231d37457c9210faf6ce", - "reference": "bb87e28e7d7b8d9a7fda231d37457c9210faf6ce", + "url": "https://api.github.com/repos/BrainMaestro/composer-git-hooks/zipball/684dc85f480268baf5e13f39a3cc494eeb2536e8", + "reference": "684dc85f480268baf5e13f39a3cc494eeb2536e8", "shasum": "" }, "require": { - "ext-tokenizer": "*", - "php": ">=5.5" + "php": "^8.0", + "symfony/console": "^5.0|^6.0|^7.0" }, "require-dev": { - "phpunit/phpunit": "~4.0|~5.0" + "ext-json": "*", + "friendsofphp/php-cs-fixer": "^3.0", + "phpunit/phpunit": "^9|^10|^11" }, "bin": [ - "bin/php-parse" + "cghooks" ], "type": "library", "extra": { - "branch-alias": { - "dev-master": "3.0-dev" + "hooks": { + "pre-push": [ + "composer test", + "appver=$(grep -o -E '[0-9]+\\.[0-9]+\\.[0-9]+(-alpha\\.[0-9]+)?' cghooks)", + "tag=$(git tag | tail -n 1)", + "tag=${tag#v}", + "if [ \"$tag\" != \"$appver\" ]; then", + "echo \"The most recent tag $tag does not match the application version $appver\\n\"", + "sed -i -E \"s/$appver/$tag/\" cghooks", + "exit 1", + "fi" + ], + "pre-commit": "composer check-style" } }, "autoload": { + "files": [ + "src/helpers.php" + ], "psr-4": { - "PhpParser\\": "lib/PhpParser" + "BrainMaestro\\GitHooks\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Nikita Popov" + "name": "Ezinwa Okpoechi", + "email": "brainmaestro@outlook.com" } ], - "description": "A PHP parser written in PHP", + "description": "Easily manage git hooks in your composer config", "keywords": [ - "parser", - "php" + "HOOK", + "composer", + "git" ], - "time": "2018-02-28T20:30:58+00:00" + "support": { + "issues": "https://github.com/BrainMaestro/composer-git-hooks/issues", + "source": "https://github.com/BrainMaestro/composer-git-hooks/tree/v3.0.0" + }, + "time": "2024-06-22T09:17:59+00:00" }, { "name": "pdepend/pdepend", - "version": "2.7.1", + "version": "2.16.2", "source": { "type": "git", "url": "https://github.com/pdepend/pdepend.git", - "reference": "daba1cf0a6edaf172fa02a17807ae29f4c1c7471" + "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pdepend/pdepend/zipball/daba1cf0a6edaf172fa02a17807ae29f4c1c7471", - "reference": "daba1cf0a6edaf172fa02a17807ae29f4c1c7471", + "url": "https://api.github.com/repos/pdepend/pdepend/zipball/f942b208dc2a0868454d01b29f0c75bbcfc6ed58", + "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58", "shasum": "" }, "require": { "php": ">=5.3.7", - "symfony/config": "^2.3.0|^3|^4|^5", - "symfony/dependency-injection": "^2.3.0|^3|^4|^5", - "symfony/filesystem": "^2.3.0|^3|^4|^5" + "symfony/config": "^2.3.0|^3|^4|^5|^6.0|^7.0", + "symfony/dependency-injection": "^2.3.0|^3|^4|^5|^6.0|^7.0", + "symfony/filesystem": "^2.3.0|^3|^4|^5|^6.0|^7.0", + "symfony/polyfill-mbstring": "^1.19" }, "require-dev": { - "easy-doc/easy-doc": "0.0.0 || ^1.2.3", + "easy-doc/easy-doc": "0.0.0|^1.2.3", "gregwar/rst": "^1.0", - "phpunit/phpunit": "^4.8.35|^5.7", "squizlabs/php_codesniffer": "^2.0.0" }, "bin": [ @@ -5712,46 +7001,63 @@ "BSD-3-Clause" ], "description": "Official version of pdepend to be handled with Composer", - "time": "2020-02-08T12:06:13+00:00" + "keywords": [ + "PHP Depend", + "PHP_Depend", + "dev", + "pdepend" + ], + "support": { + "issues": "https://github.com/pdepend/pdepend/issues", + "source": "https://github.com/pdepend/pdepend/tree/2.16.2" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/pdepend/pdepend", + "type": "tidelift" + } + ], + "time": "2023-12-17T18:09:59+00:00" }, { "name": "php-coveralls/php-coveralls", - "version": "v1.1.0", + "version": "v2.7.0", "source": { "type": "git", "url": "https://github.com/php-coveralls/php-coveralls.git", - "reference": "37f8f83fe22224eb9d9c6d593cdeb33eedd2a9ad" + "reference": "b36fa4394e519dafaddc04ae03976bc65a25ba15" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-coveralls/php-coveralls/zipball/37f8f83fe22224eb9d9c6d593cdeb33eedd2a9ad", - "reference": "37f8f83fe22224eb9d9c6d593cdeb33eedd2a9ad", + "url": "https://api.github.com/repos/php-coveralls/php-coveralls/zipball/b36fa4394e519dafaddc04ae03976bc65a25ba15", + "reference": "b36fa4394e519dafaddc04ae03976bc65a25ba15", "shasum": "" }, "require": { "ext-json": "*", "ext-simplexml": "*", - "guzzle/guzzle": "^2.8 || ^3.0", - "php": "^5.3.3 || ^7.0", - "psr/log": "^1.0", - "symfony/config": "^2.1 || ^3.0 || ^4.0", - "symfony/console": "^2.1 || ^3.0 || ^4.0", - "symfony/stopwatch": "^2.0 || ^3.0 || ^4.0", - "symfony/yaml": "^2.0 || ^3.0 || ^4.0" + "guzzlehttp/guzzle": "^6.0 || ^7.0", + "php": "^7.0 || ^8.0", + "psr/log": "^1.0 || ^2.0", + "symfony/config": "^2.1 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0", + "symfony/console": "^2.1 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0", + "symfony/stopwatch": "^2.0 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0", + "symfony/yaml": "^2.0.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.4.3 || ^6.0" + "phpunit/phpunit": "^4.8.35 || ^5.4.3 || ^6.0 || ^7.0 || >=8.0 <8.5.29 || >=9.0 <9.5.23", + "sanmai/phpunit-legacy-adapter": "^6.1 || ^8.0" }, "suggest": { "symfony/http-kernel": "Allows Symfony integration" }, "bin": [ - "bin/coveralls" + "bin/php-coveralls" ], "type": "library", "autoload": { "psr-4": { - "Satooshi\\": "src/Satooshi/" + "PhpCoveralls\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -5762,7 +7068,24 @@ { "name": "Kitamura Satoshi", "email": "with.no.parachute@gmail.com", - "homepage": "https://www.facebook.com/satooshi.jp" + "homepage": "https://www.facebook.com/satooshi.jp", + "role": "Original creator" + }, + { + "name": "Takashi Matsuo", + "email": "tmatsuo@google.com" + }, + { + "name": "Google Inc" + }, + { + "name": "Dariusz Ruminski", + "email": "dariusz.ruminski@gmail.com", + "homepage": "https://github.com/keradus" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-coveralls/php-coveralls/graphs/contributors" } ], "description": "PHP client library for Coveralls API", @@ -5773,34 +7096,39 @@ "github", "test" ], - "time": "2017-12-06T23:17:56+00:00" + "support": { + "issues": "https://github.com/php-coveralls/php-coveralls/issues", + "source": "https://github.com/php-coveralls/php-coveralls/tree/v2.7.0" + }, + "time": "2023-11-22T10:21:01+00:00" }, { "name": "phpmd/phpmd", - "version": "2.8.2", + "version": "2.15.0", "source": { "type": "git", "url": "https://github.com/phpmd/phpmd.git", - "reference": "714629ed782537f638fe23c4346637659b779a77" + "reference": "74a1f56e33afad4128b886e334093e98e1b5e7c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpmd/phpmd/zipball/714629ed782537f638fe23c4346637659b779a77", - "reference": "714629ed782537f638fe23c4346637659b779a77", + "url": "https://api.github.com/repos/phpmd/phpmd/zipball/74a1f56e33afad4128b886e334093e98e1b5e7c0", + "reference": "74a1f56e33afad4128b886e334093e98e1b5e7c0", "shasum": "" }, "require": { - "composer/xdebug-handler": "^1.0", + "composer/xdebug-handler": "^1.0 || ^2.0 || ^3.0", "ext-xml": "*", - "pdepend/pdepend": "^2.7.1", + "pdepend/pdepend": "^2.16.1", "php": ">=5.3.9" }, "require-dev": { "easy-doc/easy-doc": "0.0.0 || ^1.3.2", + "ext-json": "*", + "ext-simplexml": "*", "gregwar/rst": "^1.0", - "mikey179/vfsstream": "^1.6.4", - "phpunit/phpunit": "^4.8.36 || ^5.7.27", - "squizlabs/php_codesniffer": "^2.0" + "mikey179/vfsstream": "^1.6.8", + "squizlabs/php_codesniffer": "^2.9.2 || ^3.7.2" }, "bin": [ "src/bin/phpmd" @@ -5837,162 +7165,38 @@ "description": "PHPMD is a spin-off project of PHP Depend and aims to be a PHP equivalent of the well known Java tool PMD.", "homepage": "https://phpmd.org/", "keywords": [ + "dev", "mess detection", "mess detector", "pdepend", "phpmd", "pmd" ], - "time": "2020-02-16T20:15:50+00:00" - }, - { - "name": "rregeer/phpunit-coverage-check", - "version": "0.1.6", - "source": { - "type": "git", - "url": "https://github.com/richardregeer/phpunit-coverage-check.git", - "reference": "330ae9318e0e60960cfb97c4a0459e3e4a3080e0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/richardregeer/phpunit-coverage-check/zipball/330ae9318e0e60960cfb97c4a0459e3e4a3080e0", - "reference": "330ae9318e0e60960cfb97c4a0459e3e4a3080e0", - "shasum": "" - }, - "require": { - "php": ">=5.5.0" - }, - "bin": [ - "bin/coverage-check" - ], - "type": "library", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Richard Regeer", - "email": "rich2309@gmail.com" - } - ], - "description": "Check the code coverage using the clover report of phpunit", - "keywords": [ - "ci", - "code coverage", - "php", - "phpunit", - "testing", - "unittest" - ], - "time": "2018-07-29T13:27:58+00:00" - }, - { - "name": "sebastian/finder-facade", - "version": "1.2.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/finder-facade.git", - "reference": "167c45d131f7fc3d159f56f191a0a22228765e16" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/finder-facade/zipball/167c45d131f7fc3d159f56f191a0a22228765e16", - "reference": "167c45d131f7fc3d159f56f191a0a22228765e16", - "shasum": "" - }, - "require": { - "php": "^7.1", - "symfony/finder": "^2.3|^3.0|^4.0|^5.0", - "theseer/fdomdocument": "^1.6" - }, - "type": "library", - "extra": { - "branch-alias": [] - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "FinderFacade is a convenience wrapper for Symfony's Finder component.", - "homepage": "https://github.com/sebastianbergmann/finder-facade", - "time": "2020-01-16T08:08:45+00:00" - }, - { - "name": "sebastian/phpcpd", - "version": "4.1.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpcpd.git", - "reference": "0d9afa762f2400de077b2192f4a9d127de0bb78e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpcpd/zipball/0d9afa762f2400de077b2192f4a9d127de0bb78e", - "reference": "0d9afa762f2400de077b2192f4a9d127de0bb78e", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "php": "^7.1", - "phpunit/php-timer": "^2.0", - "sebastian/finder-facade": "^1.1", - "sebastian/version": "^1.0|^2.0", - "symfony/console": "^2.7|^3.0|^4.0" - }, - "bin": [ - "phpcpd" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] + "support": { + "irc": "irc://irc.freenode.org/phpmd", + "issues": "https://github.com/phpmd/phpmd/issues", + "source": "https://github.com/phpmd/phpmd/tree/2.15.0" }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ + "funding": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "url": "https://tidelift.com/funding/github/packagist/phpmd/phpmd", + "type": "tidelift" } ], - "description": "Copy/Paste Detector (CPD) for PHP code.", - "homepage": "https://github.com/sebastianbergmann/phpcpd", - "time": "2018-09-17T17:17:27+00:00" + "time": "2023-12-11T08:22:20+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.5.4", + "version": "3.10.3", "source": { "type": "git", - "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "dceec07328401de6211037abbb18bda423677e26" + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "62d32998e820bddc40f99f8251958aed187a5c9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/dceec07328401de6211037abbb18bda423677e26", - "reference": "dceec07328401de6211037abbb18bda423677e26", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/62d32998e820bddc40f99f8251958aed187a5c9c", + "reference": "62d32998e820bddc40f99f8251958aed187a5c9c", "shasum": "" }, "require": { @@ -6002,11 +7206,11 @@ "php": ">=5.4.0" }, "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" }, "bin": [ - "bin/phpcs", - "bin/phpcbf" + "bin/phpcbf", + "bin/phpcs" ], "type": "library", "extra": { @@ -6021,55 +7225,78 @@ "authors": [ { "name": "Greg Sherwood", - "role": "lead" + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" } ], "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", - "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", "keywords": [ "phpcs", - "standards" + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + } ], - "time": "2020-01-30T22:20:29+00:00" + "time": "2024-09-18T10:38:58+00:00" }, { "name": "symfony/config", - "version": "v4.4.5", + "version": "v7.2.3", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "cbfef5ae91ccd3b06621c18d58cd355c68c87ae9" + "reference": "7716594aaae91d9141be080240172a92ecca4d44" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/cbfef5ae91ccd3b06621c18d58cd355c68c87ae9", - "reference": "cbfef5ae91ccd3b06621c18d58cd355c68c87ae9", + "url": "https://api.github.com/repos/symfony/config/zipball/7716594aaae91d9141be080240172a92ecca4d44", + "reference": "7716594aaae91d9141be080240172a92ecca4d44", "shasum": "" }, "require": { - "php": "^7.1.3", - "symfony/filesystem": "^3.4|^4.0|^5.0", + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^7.1", "symfony/polyfill-ctype": "~1.8" }, "conflict": { - "symfony/finder": "<3.4" + "symfony/finder": "<6.4", + "symfony/service-contracts": "<2.5" }, "require-dev": { - "symfony/event-dispatcher": "^3.4|^4.0|^5.0", - "symfony/finder": "^3.4|^4.0|^5.0", - "symfony/messenger": "^4.1|^5.0", - "symfony/service-contracts": "^1.1|^2", - "symfony/yaml": "^3.4|^4.0|^5.0" - }, - "suggest": { - "symfony/yaml": "To use the yaml reference dumper" + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Config\\": "" @@ -6092,57 +7319,64 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Config Component", + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", - "time": "2020-02-04T09:32:40+00:00" + "support": { + "source": "https://github.com/symfony/config/tree/v7.2.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-22T12:07:01+00:00" }, { "name": "symfony/dependency-injection", - "version": "v4.4.5", + "version": "v7.2.3", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "ebb2e882e8c9e2eb990aa61ddcd389848466e342" + "reference": "1d321c4bc3fe926fd4c38999a4c9af4f5d61ddfc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/ebb2e882e8c9e2eb990aa61ddcd389848466e342", - "reference": "ebb2e882e8c9e2eb990aa61ddcd389848466e342", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/1d321c4bc3fe926fd4c38999a4c9af4f5d61ddfc", + "reference": "1d321c4bc3fe926fd4c38999a4c9af4f5d61ddfc", "shasum": "" }, "require": { - "php": "^7.1.3", - "psr/container": "^1.0", - "symfony/service-contracts": "^1.1.6|^2" + "php": ">=8.2", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^3.5", + "symfony/var-exporter": "^6.4|^7.0" }, "conflict": { - "symfony/config": "<4.3|>=5.0", - "symfony/finder": "<3.4", - "symfony/proxy-manager-bridge": "<3.4", - "symfony/yaml": "<3.4" + "ext-psr": "<1.1|>=2", + "symfony/config": "<6.4", + "symfony/finder": "<6.4", + "symfony/yaml": "<6.4" }, "provide": { - "psr/container-implementation": "1.0", - "symfony/service-implementation": "1.0" + "psr/container-implementation": "1.1|2.0", + "symfony/service-implementation": "1.1|2.0|3.0" }, "require-dev": { - "symfony/config": "^4.3", - "symfony/expression-language": "^3.4|^4.0|^5.0", - "symfony/yaml": "^3.4|^4.0|^5.0" - }, - "suggest": { - "symfony/config": "", - "symfony/expression-language": "For using expressions in service container configuration", - "symfony/finder": "For using double-star glob patterns or when GLOB_BRACE portability is required", - "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them", - "symfony/yaml": "" + "symfony/config": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\DependencyInjection\\": "" @@ -6165,33 +7399,46 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony DependencyInjection Component", + "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", - "time": "2020-02-29T09:50:10+00:00" + "support": { + "source": "https://github.com/symfony/dependency-injection/tree/v7.2.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-17T10:56:55+00:00" }, { "name": "symfony/stopwatch", - "version": "v3.4.38", + "version": "v7.2.2", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "e2d954156d4817c9a5c79f519a71516693a4a9c8" + "reference": "e46690d5b9d7164a6d061cab1e8d46141b9f49df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/e2d954156d4817c9a5c79f519a71516693a4a9c8", - "reference": "e2d954156d4817c9a5c79f519a71516693a4a9c8", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/e46690d5b9d7164a6d061cab1e8d46141b9f49df", + "reference": "e46690d5b9d7164a6d061cab1e8d46141b9f49df", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8" + "php": ">=8.2", + "symfony/service-contracts": "^2.5|^3" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Stopwatch\\": "" @@ -6214,62 +7461,118 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Stopwatch Component", + "description": "Provides a way to profile code", "homepage": "https://symfony.com", - "time": "2020-01-01T11:03:25+00:00" + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v7.2.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-18T14:28:33+00:00" }, { - "name": "theseer/fdomdocument", - "version": "1.6.6", + "name": "symfony/var-exporter", + "version": "v7.2.0", "source": { "type": "git", - "url": "https://github.com/theseer/fDOMDocument.git", - "reference": "6e8203e40a32a9c770bcb62fe37e68b948da6dca" + "url": "https://github.com/symfony/var-exporter.git", + "reference": "1a6a89f95a46af0f142874c9d650a6358d13070d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/fDOMDocument/zipball/6e8203e40a32a9c770bcb62fe37e68b948da6dca", - "reference": "6e8203e40a32a9c770bcb62fe37e68b948da6dca", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/1a6a89f95a46af0f142874c9d650a6358d13070d", + "reference": "1a6a89f95a46af0f142874c9d650a6358d13070d", "shasum": "" }, "require": { - "ext-dom": "*", - "lib-libxml": "*", - "php": ">=5.3.3" + "php": ">=8.2" + }, + "require-dev": { + "symfony/property-access": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" }, "type": "library", "autoload": { - "classmap": [ - "src/" + "psr-4": { + "Symfony\\Component\\VarExporter\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "lead" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "homepage": "https://symfony.com", + "keywords": [ + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" + ], + "support": { + "source": "https://github.com/symfony/var-exporter/tree/v7.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "description": "The classes contained within this repository extend the standard DOM to use exceptions at all occasions of errors instead of PHP warnings or notices. They also add various custom methods and shortcuts for convenience and to simplify the usage of DOM.", - "homepage": "https://github.com/theseer/fDOMDocument", - "time": "2017-06-30T11:53:12+00:00" + "time": "2024-10-18T07:58:17+00:00" } ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "~7.2.0||~7.3.0", "ext-curl": "*", "ext-dom": "*", + "ext-iconv": "*", + "ext-intl": "*", "ext-json": "*", - "ext-openssl": "*" + "ext-openssl": "*", + "php": ">=8.2" }, - "platform-dev": [] + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/dev/tests/_bootstrap.php b/dev/tests/_bootstrap.php index 66be4ee45..7da6cffa7 100644 --- a/dev/tests/_bootstrap.php +++ b/dev/tests/_bootstrap.php @@ -9,26 +9,11 @@ $vendorAutoloadPath = realpath(PROJECT_ROOT . '/vendor/autoload.php'); $mftfTestCasePath = realpath(PROJECT_ROOT . '/dev/tests/util/MftfTestCase.php'); +$mftfStaticTestCasePath = realpath(PROJECT_ROOT . '/dev/tests/util/MftfStaticTestCase.php'); require_once $vendorAutoloadPath; require_once $mftfTestCasePath; - -// Set up AspectMock -$kernel = \AspectMock\Kernel::getInstance(); -$kernel->init([ - 'debug' => true, - 'includePaths' => [ - PROJECT_ROOT . DIRECTORY_SEPARATOR . 'src', - PROJECT_ROOT . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'allure-framework' - ], - 'cacheDir' => PROJECT_ROOT . - DIRECTORY_SEPARATOR . - 'dev' . - DIRECTORY_SEPARATOR . - 'tests' . - DIRECTORY_SEPARATOR . - '.cache' -]); +require_once $mftfStaticTestCasePath; // set mftf appplication context \Magento\FunctionalTestingFramework\Config\MftfApplicationConfig::create( @@ -51,11 +36,11 @@ foreach ($TEST_ENVS as $key => $value) { $_ENV[$key] = $value; - putenv("{$key}=${value}"); + putenv("{$key}={$value}"); } -// Add our test module to the whitelist -putenv('MODULE_WHITELIST=Magento_TestModule'); +// Add our test module to the allowlist +putenv('MODULE_ALLOWLIST=Magento_TestModule'); // Define our own set of paths for the tests defined('FW_BP') || define('FW_BP', PROJECT_ROOT); diff --git a/dev/tests/functional/standalone_bootstrap.php b/dev/tests/functional/standalone_bootstrap.php index 34516236e..e048498c3 100755 --- a/dev/tests/functional/standalone_bootstrap.php +++ b/dev/tests/functional/standalone_bootstrap.php @@ -20,8 +20,12 @@ //Load constants from .env file if (file_exists(ENV_FILE_PATH . '.env')) { - $env = new \Dotenv\Loader(ENV_FILE_PATH . '.env'); - $env->load(); + $env = new \Symfony\Component\Dotenv\Dotenv(); + if (function_exists('putenv')) { + $env->usePutenv(); + } + $env->populate($env->parse(file_get_contents(ENV_FILE_PATH . '.env'), ENV_FILE_PATH . '.env'), true); + foreach ($_ENV as $key => $var) { defined($key) || define($key, $var); @@ -42,16 +46,20 @@ 'MAGENTO_CLI_COMMAND_PATH', 'dev/tests/acceptance/utils/command.php' ); - $env->setEnvironmentVariable('MAGENTO_CLI_COMMAND_PATH', MAGENTO_CLI_COMMAND_PATH); - defined('MAGENTO_CLI_COMMAND_PARAMETER') || define('MAGENTO_CLI_COMMAND_PARAMETER', 'command'); - $env->setEnvironmentVariable('MAGENTO_CLI_COMMAND_PARAMETER', MAGENTO_CLI_COMMAND_PARAMETER); - defined('DEFAULT_TIMEZONE') || define('DEFAULT_TIMEZONE', 'America/Los_Angeles'); - $env->setEnvironmentVariable('DEFAULT_TIMEZONE', DEFAULT_TIMEZONE); - defined('WAIT_TIMEOUT') || define('WAIT_TIMEOUT', 30); - $env->setEnvironmentVariable('WAIT_TIMEOUT', WAIT_TIMEOUT); + defined('VERBOSE_ARTIFACTS') || define('VERBOSE_ARTIFACTS', false); + $env->populate( + [ + 'MAGENTO_CLI_COMMAND_PATH' => MAGENTO_CLI_COMMAND_PATH, + 'MAGENTO_CLI_COMMAND_PARAMETER' => MAGENTO_CLI_COMMAND_PARAMETER, + 'DEFAULT_TIMEZONE' => DEFAULT_TIMEZONE, + 'WAIT_TIMEOUT' => WAIT_TIMEOUT, + 'VERBOSE_ARTIFACTS' => VERBOSE_ARTIFACTS, + ], + true + ); try { new DateTimeZone(DEFAULT_TIMEZONE); @@ -67,10 +75,3 @@ $RELATIVE_TESTS_MODULE_PATH = '/tests/functional/tests/MFTF'; defined('TESTS_MODULE_PATH') || define('TESTS_MODULE_PATH', realpath(TESTS_BP . $RELATIVE_TESTS_MODULE_PATH)); - - -// add the debug flag here -$debug_mode = $_ENV['MFTF_DEBUG'] ?? false; -if (!(bool)$debug_mode && extension_loaded('xdebug')) { - xdebug_disable(); -} diff --git a/dev/tests/functional/tests/MFTF/DevDocs/ActionGroup/HelperActionGroup.xml b/dev/tests/functional/tests/MFTF/DevDocs/ActionGroup/HelperActionGroup.xml index 4e39a2da1..42159fc66 100644 --- a/dev/tests/functional/tests/MFTF/DevDocs/ActionGroup/HelperActionGroup.xml +++ b/dev/tests/functional/tests/MFTF/DevDocs/ActionGroup/HelperActionGroup.xml @@ -10,11 +10,12 @@ + - {{contentSection.parametrizedSelector(test)}} - ['{{test}}', 'Bla'] + {{contentSection.parametrizedSelector(entityTest.entityField)}} + ['{{entityTest.entityField}}', 'Bla'] {{test}} true 4.400000000234234 diff --git a/dev/tests/functional/tests/MFTF/DevDocs/Data/DeprecatedMessageData.xml b/dev/tests/functional/tests/MFTF/DevDocs/Data/DeprecatedMessageData.xml index 1c2462c2f..8be85b447 100644 --- a/dev/tests/functional/tests/MFTF/DevDocs/Data/DeprecatedMessageData.xml +++ b/dev/tests/functional/tests/MFTF/DevDocs/Data/DeprecatedMessageData.xml @@ -9,6 +9,9 @@ - Introduction to the Magento Functional Testing Framework + Introduction to the Functional Testing Framework + + + Some data diff --git a/dev/tests/functional/tests/MFTF/DevDocs/Data/ExtendMessageData.xml b/dev/tests/functional/tests/MFTF/DevDocs/Data/ExtendMessageData.xml new file mode 100644 index 000000000..040b80597 --- /dev/null +++ b/dev/tests/functional/tests/MFTF/DevDocs/Data/ExtendMessageData.xml @@ -0,0 +1,16 @@ + + + + + + + Something New + + + diff --git a/dev/tests/functional/tests/MFTF/DevDocs/Data/MessageData.xml b/dev/tests/functional/tests/MFTF/DevDocs/Data/MessageData.xml index 9c34b5672..2a973ce53 100644 --- a/dev/tests/functional/tests/MFTF/DevDocs/Data/MessageData.xml +++ b/dev/tests/functional/tests/MFTF/DevDocs/Data/MessageData.xml @@ -9,6 +9,18 @@ - Introduction to the Magento Functional Testing Framework + Introduction to the Functional Testing Framework + + 0 + 1 + 2 + 3 + + + + + + TESTING CASE + diff --git a/dev/tests/functional/tests/MFTF/DevDocs/Helper/CustomHelper.php b/dev/tests/functional/tests/MFTF/DevDocs/Helper/CustomHelper.php index 46fba18d9..3705ec1d0 100644 --- a/dev/tests/functional/tests/MFTF/DevDocs/Helper/CustomHelper.php +++ b/dev/tests/functional/tests/MFTF/DevDocs/Helper/CustomHelper.php @@ -46,4 +46,15 @@ public function goTo( print('$bla = ' . $bla . PHP_EOL); print('array $arraysomething = [' . implode(', ', $arraysomething) . ']' . PHP_EOL); } + + /** + * Returns value of provided param $text + * + * @param string $text + * @return string + */ + public function getText(string $text): string + { + return $text; + } } diff --git a/dev/tests/functional/tests/MFTF/DevDocs/Section/ContentSection.xml b/dev/tests/functional/tests/MFTF/DevDocs/Section/ContentSection.xml index e0a133d51..219752bf5 100644 --- a/dev/tests/functional/tests/MFTF/DevDocs/Section/ContentSection.xml +++ b/dev/tests/functional/tests/MFTF/DevDocs/Section/ContentSection.xml @@ -6,10 +6,12 @@ */ --> + +
- +
diff --git a/dev/tests/functional/tests/MFTF/DevDocs/Section/DeprecatedContentSection.xml b/dev/tests/functional/tests/MFTF/DevDocs/Section/DeprecatedContentSection.xml index 8ed018146..63431e2a1 100644 --- a/dev/tests/functional/tests/MFTF/DevDocs/Section/DeprecatedContentSection.xml +++ b/dev/tests/functional/tests/MFTF/DevDocs/Section/DeprecatedContentSection.xml @@ -9,6 +9,6 @@
- +
diff --git a/dev/tests/functional/tests/MFTF/DevDocs/Test/DevDocsTest.xml b/dev/tests/functional/tests/MFTF/DevDocs/Test/DevDocsTest.xml index e7c3bed54..911bc65eb 100644 --- a/dev/tests/functional/tests/MFTF/DevDocs/Test/DevDocsTest.xml +++ b/dev/tests/functional/tests/MFTF/DevDocs/Test/DevDocsTest.xml @@ -42,8 +42,23 @@ 987
+ + some text + + + some text + getText + + + + + + {{ExtendedMessageData.numbers}} + ["Something New", "0", "1", "2", "3", "TESTING CASE"] + +
diff --git a/dev/tests/functional/tests/MFTF/DevDocs/Test/FormatCurrencyTest.xml b/dev/tests/functional/tests/MFTF/DevDocs/Test/FormatCurrencyTest.xml new file mode 100644 index 000000000..ab3d36c4e --- /dev/null +++ b/dev/tests/functional/tests/MFTF/DevDocs/Test/FormatCurrencyTest.xml @@ -0,0 +1,52 @@ + + + + + + + + + 1.234,57 € + $eurInDE + + + + 1.234,00 € + $eurInDEPos + + + + -1.234,56 € + $eurInDENeg + + + + + 1.234,57 $ + $usdInDE + + + + 1.234,00 $ + $usdInDEPos + + + + -1.234,56 $ + $usdInDENeg + + + + + + 10,50 € + $usingVariable + + + diff --git a/dev/tests/phpunit.xml b/dev/tests/phpunit.xml index 9648ab3fd..7512be49d 100644 --- a/dev/tests/phpunit.xml +++ b/dev/tests/phpunit.xml @@ -1,34 +1,32 @@ + - - - - - verification - - - unit - - - - - ../../src/Magento/FunctionalTestingFramework/DataGenerator - ../../src/Magento/FunctionalTestingFramework/Page - ../../src/Magento/FunctionalTestingFramework/Suite - ../../src/Magento/FunctionalTestingFramework/Test - ../../src/Magento/FunctionalTestingFramework/Util - - - - - + + + + + + + + + verification + + + unit + + + + + + ../../src/Magento/FunctionalTestingFramework/DataGenerator + ../../src/Magento/FunctionalTestingFramework/Page + ../../src/Magento/FunctionalTestingFramework/Suite + ../../src/Magento/FunctionalTestingFramework/Test + ../../src/Magento/FunctionalTestingFramework/Util + + diff --git a/dev/tests/static/Magento/Sniffs/Commenting/FunctionCommentSniff.php b/dev/tests/static/Magento/Sniffs/Commenting/FunctionCommentSniff.php index 5050d5f03..5e539aeaf 100644 --- a/dev/tests/static/Magento/Sniffs/Commenting/FunctionCommentSniff.php +++ b/dev/tests/static/Magento/Sniffs/Commenting/FunctionCommentSniff.php @@ -214,7 +214,7 @@ protected function processThrows(File $phpcsFile, $stackPtr, $commentStart) } // Starts with a capital letter and ends with a fullstop. - $firstChar = $comment{0}; + $firstChar = $comment[0]; if (strtoupper($firstChar) !== $firstChar) { $error = '@throws tag comment must start with a capital letter'; $phpcsFile->addError($error, ($tag + 2), 'ThrowsNotCapital'); @@ -422,7 +422,9 @@ protected function processParams(File $phpcsFile, $stackPtr, $commentStart) $typeHint, $param['var'], ); - $phpcsFile->addError($error, $stackPtr, 'IncorrectTypeHint', $data); + if ($suggestedTypeHint != "null" && (ltrim($typeHint, '?') !== $suggestedTypeHint)) { + $phpcsFile->addError($error, $stackPtr, 'IncorrectTypeHint', $data); + } }//end if } else if ($suggestedTypeHint === '' && isset($realParams[$pos]) === true) { $typeHint = $realParams[$pos]['type_hint']; @@ -437,7 +439,7 @@ protected function processParams(File $phpcsFile, $stackPtr, $commentStart) }//end if }//end foreach - $suggestedType = implode($suggestedTypeNames, '|'); + $suggestedType = implode('|', $suggestedTypeNames); if ($param['type'] !== $suggestedType) { $error = 'Expected "%s" but found "%s" for parameter type'; $data = array( diff --git a/dev/tests/static/Magento/Sniffs/MicroOptimizations/IsNullSniff.php b/dev/tests/static/Magento/Sniffs/MicroOptimizations/IsNullSniff.php index 928fc3a0d..9ec74cb4f 100644 --- a/dev/tests/static/Magento/Sniffs/MicroOptimizations/IsNullSniff.php +++ b/dev/tests/static/Magento/Sniffs/MicroOptimizations/IsNullSniff.php @@ -13,7 +13,7 @@ class IsNullSniff implements Sniff /** * @var string */ - protected $blacklist = 'is_null'; + protected $blocklist = 'is_null'; /** * @inheritdoc @@ -29,7 +29,7 @@ public function register() public function process(File $sourceFile, $stackPtr) { $tokens = $sourceFile->getTokens(); - if ($tokens[$stackPtr]['content'] === $this->blacklist) { + if ($tokens[$stackPtr]['content'] === $this->blocklist) { $sourceFile->addError( "is_null must be avoided. Use strict comparison instead.", $stackPtr, diff --git a/dev/tests/static/Magento/Sniffs/NamingConventions/InterfaceNameSniff.php b/dev/tests/static/Magento/Sniffs/NamingConventions/InterfaceNameSniff.php index 1618beb66..4c8461bea 100644 --- a/dev/tests/static/Magento/Sniffs/NamingConventions/InterfaceNameSniff.php +++ b/dev/tests/static/Magento/Sniffs/NamingConventions/InterfaceNameSniff.php @@ -29,9 +29,9 @@ public function process(File $sourceFile, $stackPtr) $declarationLine = $tokens[$stackPtr]['line']; $suffixLength = strlen(self::INTERFACE_SUFFIX); // Find first T_STRING after 'interface' keyword in the line and verify it - while ($tokens[$stackPtr]['line'] == $declarationLine) { - if ($tokens[$stackPtr]['type'] == 'T_STRING') { - if (substr($tokens[$stackPtr]['content'], 0 - $suffixLength) != self::INTERFACE_SUFFIX) { + while ($tokens[$stackPtr]['line'] === $declarationLine) { + if ($tokens[$stackPtr]['type'] === 'T_STRING') { + if (substr($tokens[$stackPtr]['content'], 0 - $suffixLength) !== self::INTERFACE_SUFFIX) { $sourceFile->addError( 'Interface should have name that ends with "Interface" suffix.', $stackPtr, diff --git a/dev/tests/static/Magento/Sniffs/NamingConventions/ReservedWordsSniff.php b/dev/tests/static/Magento/Sniffs/NamingConventions/ReservedWordsSniff.php index f41c235a6..7d1957195 100644 --- a/dev/tests/static/Magento/Sniffs/NamingConventions/ReservedWordsSniff.php +++ b/dev/tests/static/Magento/Sniffs/NamingConventions/ReservedWordsSniff.php @@ -32,6 +32,7 @@ class ReservedWordsSniff implements Sniff 'object' => '7', 'mixed' => '7', 'numeric' => '7', + 'match' => '8', ]; /** diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Allure/AllureHelperTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Allure/AllureHelperTest.php index b7eafaead..1e5abfc80 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Allure/AllureHelperTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Allure/AllureHelperTest.php @@ -3,132 +3,134 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Tests\unit\Magento\FunctionalTestingFramework\Allure; +declare(strict_types=1); + +namespace tests\unit\Magento\FunctionalTestFramework\Allure; use Magento\FunctionalTestingFramework\Allure\AllureHelper; -use Magento\FunctionalTestingFramework\Allure\Event\AddUniqueAttachmentEvent; -use Yandex\Allure\Adapter\Allure; -use Yandex\Allure\Adapter\Event\AddAttachmentEvent; -use Yandex\Allure\Adapter\Event\StepFinishedEvent; -use Yandex\Allure\Adapter\Event\StepStartedEvent; -use Yandex\Allure\Adapter\Model\Attachment; -use AspectMock\Test as AspectMock; use PHPUnit\Framework\TestCase; +use Qameta\Allure\Allure; +use Qameta\Allure\Io\DataSourceFactory; +use Qameta\Allure\Model\AttachmentResult; +use Qameta\Allure\Model\ResultFactoryInterface; +use Qameta\Allure\Setup\LifecycleBuilderInterface; +use const STDOUT; +/** + * @covers \Qameta\Allure\Allure + */ class AllureHelperTest extends TestCase { - const MOCK_FILENAME = 'filename'; - - /** - * Clear Allure Lifecycle - */ - public function tearDown() + public function setUp(): void { - Allure::setDefaultLifecycle(); - AspectMock::clean(); + Allure::reset(); } /** - * AddAtachmentToStep should add an attachment to the current step - * @throws \Yandex\Allure\Adapter\AllureException + * @dataProvider providerAttachmentProperties */ - public function testAddAttachmentToStep() - { - $this->mockAttachmentWriteEvent(); - $expectedData = "string"; - $expectedCaption = "caption"; - - //Prepare Allure lifecycle - Allure::lifecycle()->fire(new StepStartedEvent('firstStep')); - - //Call function - AllureHelper::addAttachmentToCurrentStep($expectedData, $expectedCaption); - - // Assert Attachment is created as expected - $step = Allure::lifecycle()->getStepStorage()->pollLast(); - $expectedAttachment = new Attachment($expectedCaption, self::MOCK_FILENAME, null); - $this->assertEquals($step->getAttachments()[0], $expectedAttachment); + public function testDoAddAttachmentMethod( + string $name, + $type, + ?string $fileExtension, + ): void { + $attachment = new AttachmentResult('a'); + Allure::setLifecycleBuilder( + $this->createLifecycleBuilder($this->createResultFactoryWithAttachment($attachment)), + ); + + AllureHelper::doAddAttachment( + DataSourceFactory::fromFile('test'), + 'nameOfTheFile', + 'typeOfTheFile', + $fileExtension + ); + self::assertSame('nameOfTheFile', $attachment->getName()); + self::assertSame('typeOfTheFile', $attachment->getType()); } /** - * AddAttachmentToLastStep should add an attachment only to the last step - * @throws \Yandex\Allure\Adapter\AllureException + * @dataProvider providerAttachmentProperties */ - public function testAddAttachmentToLastStep() - { - $this->mockAttachmentWriteEvent(); - $expectedData = "string"; - $expectedCaption = "caption"; - - //Prepare Allure lifecycle - Allure::lifecycle()->fire(new StepStartedEvent('firstStep')); - Allure::lifecycle()->fire(new StepFinishedEvent('firstStep')); - Allure::lifecycle()->fire(new StepStartedEvent('secondStep')); - Allure::lifecycle()->fire(new StepFinishedEvent('secondStep')); - - //Call function - AllureHelper::addAttachmentToLastStep($expectedData, $expectedCaption); - - //Continue Allure lifecycle - Allure::lifecycle()->fire(new StepStartedEvent('thirdStep')); - Allure::lifecycle()->fire(new StepFinishedEvent('thirdStep')); - - // Assert Attachment is created as expected on the right step - $rootStep = Allure::lifecycle()->getStepStorage()->pollLast(); - - $firstStep = $rootStep->getSteps()[0]; - $secondStep = $rootStep->getSteps()[1]; - $thirdStep = $rootStep->getSteps()[2]; - - $expectedAttachment = new Attachment($expectedCaption, self::MOCK_FILENAME, null); - $this->assertEmpty($firstStep->getAttachments()); - $this->assertEquals($secondStep->getAttachments()[0], $expectedAttachment); - $this->assertEmpty($thirdStep->getAttachments()); + public function testAddAttachmentToStep( + string $name, + ?string $type, + ?string $fileExtension, + ): void { + $attachment = new AttachmentResult('a'); + Allure::setLifecycleBuilder( + $this->createLifecycleBuilder($this->createResultFactoryWithAttachment($attachment)), + ); + + Allure::attachment($name, 'nameOfTheFile', $type, $fileExtension); + self::assertSame($name, $attachment->getName()); + self::assertSame($type, $attachment->getType()); + self::assertSame($fileExtension, $attachment->getFileExtension()); } /** - * AddAttachment actions should have files with different attachment names - * @throws \Yandex\Allure\Adapter\AllureException + * @dataProvider providerAttachmentProperties */ - public function testAddAttachementUniqueName() - { - $this->mockCopyFile(); - $expectedData = "string"; - $expectedCaption = "caption"; - - //Prepare Allure lifecycle - Allure::lifecycle()->fire(new StepStartedEvent('firstStep')); - - //Call function twice - AllureHelper::addAttachmentToCurrentStep($expectedData, $expectedCaption); - AllureHelper::addAttachmentToCurrentStep($expectedData, $expectedCaption); - - // Assert file names for both attachments are not the same. - $step = Allure::lifecycle()->getStepStorage()->pollLast(); - $attachmentOne = $step->getAttachments()[0]->getSource(); - $attachmentTwo = $step->getAttachments()[1]->getSource(); - $this->assertNotEquals($attachmentOne, $attachmentTwo); + public function testAddAttachmentFileToStep( + string $name, + ?string $type, + ?string $fileExtension, + ): void { + $attachment = new AttachmentResult('a'); + Allure::setLifecycleBuilder( + $this->createLifecycleBuilder($this->createResultFactoryWithAttachment($attachment)), + ); + + Allure::attachmentFile($name, 'b', $type, '.html'); + self::assertSame('c', $attachment->getName()); + self::assertSame('.html', $attachment->getFileExtension()); } /** - * Mock entire attachment writing mechanisms - * @throws \Exception + * @return iterable */ - public function mockAttachmentWriteEvent() + public static function providerAttachmentProperties(): iterable { - AspectMock::double(AddUniqueAttachmentEvent::class, [ - "getAttachmentFileName" => self::MOCK_FILENAME - ]); + return [ + 'Only name' => ['c', null, null], + 'Name and type' => ['c', 'd', null], + 'Name and file extension' => ['c', null, 'd'], + 'Name, type and file extension' => ['c', 'd', 'e'], + ]; } - /** - * Mock only file writing mechanism - * @throws \Exception - */ - public function mockCopyFile() + private function createResultFactoryWithAttachment(AttachmentResult $attachment): ResultFactoryInterface { - AspectMock::double(AddUniqueAttachmentEvent::class, [ - "copyFile" => true - ]); + $resultFactory = $this->createStub(ResultFactoryInterface::class); + $resultFactory + ->method('createAttachment') + ->willReturn($attachment); + + return $resultFactory; + } + + private function createLifecycleBuilder( + ?ResultFactoryInterface $resultFactory = null, + ?AllureLifecycleInterface $lifecycle = null, + ?StatusDetectorInterface $statusDetector = null, + ): LifecycleBuilderInterface { + $builder = $this->createStub(LifecycleBuilderInterface::class); + if (isset($resultFactory)) { + $builder + ->method('getResultFactory') + ->willReturn($resultFactory); + } + if (isset($lifecycle)) { + $builder + ->method('createLifecycle') + ->willReturn($lifecycle); + } + if (isset($statusDetector)) { + $builder + ->method('getStatusDetector') + ->willReturn($statusDetector); + } + + return $builder; } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Composer/ComposerInstallTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Composer/ComposerInstallTest.php index e4977184f..9b8327bd4 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Composer/ComposerInstallTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Composer/ComposerInstallTest.php @@ -4,10 +4,10 @@ * See COPYING.txt for license details. */ -namespace tests\unit\Magento\FunctionalTestFramework\Test\Util; +namespace tests\unit\Magento\FunctionalTestFramework\Composer; use Magento\FunctionalTestingFramework\Composer\ComposerInstall; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use tests\unit\Util\MagentoTestCase; class ComposerInstallTest extends MagentoTestCase { @@ -18,7 +18,7 @@ class ComposerInstallTest extends MagentoTestCase */ private $composer; - public function setUp() + public function setUp(): void { $composerJson = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Composer' . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'dir1' . DIRECTORY_SEPARATOR . 'dir2' . DIRECTORY_SEPARATOR . 'composer.json'; diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Composer/ComposerPackageTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Composer/ComposerPackageTest.php index 4802f0c33..7620f9c1c 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Composer/ComposerPackageTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Composer/ComposerPackageTest.php @@ -4,10 +4,10 @@ * See COPYING.txt for license details. */ -namespace tests\unit\Magento\FunctionalTestFramework\Test\Util; +namespace tests\unit\Magento\FunctionalTestFramework\Composer; use Magento\FunctionalTestingFramework\Composer\ComposerPackage; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use tests\unit\Util\MagentoTestCase; use Composer\Package\RootPackage; class ComposerPackageTest extends MagentoTestCase @@ -19,7 +19,7 @@ class ComposerPackageTest extends MagentoTestCase */ private $composer; - public function setUp() + public function setUp(): void { $composerJson = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Composer' . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'dir1' . DIRECTORY_SEPARATOR . 'dir2' . DIRECTORY_SEPARATOR . 'composer.json'; diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Config/Reader/FilesystemTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Config/Reader/FilesystemTest.php index b59858e7f..1fe5bb6b5 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Config/Reader/FilesystemTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Config/Reader/FilesystemTest.php @@ -3,104 +3,108 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Tests\unit\Magento\FunctionalTestFramework\Test\Config\Reader; +declare(strict_types=1); +namespace tests\unit\Magento\FunctionalTestFramework\Config\Reader; + +use Magento\FunctionalTestingFramework\Config\ConverterInterface; use Magento\FunctionalTestingFramework\Config\FileResolver\Module; use Magento\FunctionalTestingFramework\Config\Reader\Filesystem; +use Magento\FunctionalTestingFramework\Config\SchemaLocatorInterface; use Magento\FunctionalTestingFramework\Config\ValidationState; use Magento\FunctionalTestingFramework\Util\Iterator\File; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use AspectMock\Test as AspectMock; use tests\unit\Util\TestLoggingUtil; class FilesystemTest extends TestCase { /** - * Before test functionality * @return void */ - public function setUp() + public function setUp(): void { TestLoggingUtil::getInstance()->setMockLoggingUtil(); } /** - * Test Reading Empty Files * @throws \Exception */ public function testEmptyXmlFile() { - // create mocked items and read the file - $someFile = $this->setMockFile("somepath.xml", ""); - $filesystem = $this->createPseudoFileSystem($someFile); - $filesystem->read(); + $filesystem = $this->getFilesystem($this->getFileIterator('somepath.xml', '')); + $this->assertEquals([], $filesystem->read()); - // validate log statement TestLoggingUtil::getInstance()->validateMockLogStatement( - "warning", - "XML File is empty.", - ["File" => "somepath.xml"] + 'warning', + 'XML File is empty.', + ['File' => 'somepath.xml'] ); } /** - * Function used to set mock for File created in test + * Retrieve mocked file iterator * * @param string $fileName * @param string $content - * @return object + * @return File|MockObject * @throws \Exception */ - public function setMockFile($fileName, $content) + public function getFileIterator(string $fileName, string $content): File { - $file = AspectMock::double( - File::class, - [ - 'current' => "", - 'count' => 1, - 'getFilename' => $fileName - ] - )->make(); + $iterator = new \ArrayIterator([$content]); + + $file = $this->createMock(File::class); + + $file->method('current') + ->willReturn($content); + $file->method('getFilename') + ->willReturn($fileName); + $file->method('count') + ->willReturn(1); + + $file->method('next') + ->willReturnCallback(function () use ($iterator): void { + $iterator->next(); + }); - //set mocked data property for File - $property = new \ReflectionProperty(File::class, 'data'); - $property->setAccessible(true); - $property->setValue($file, [$fileName => $content]); + $file->method('valid') + ->willReturnCallback(function () use ($iterator): bool { + return $iterator->valid(); + }); return $file; } /** - * Function used to set mock for filesystem class during test + * Get real instance of Filesystem class with mocked dependencies * - * @param string $fileList - * @return object - * @throws \Exception + * @param File $fileIterator + * @return Filesystem */ - public function createPseudoFileSystem($fileList) + public function getFilesystem(File $fileIterator): Filesystem { - $filesystem = AspectMock::double(Filesystem::class)->make(); - - //set resolver to use mocked resolver - $mockFileResolver = AspectMock::double(Module::class, ['get' => $fileList])->make(); - $property = new \ReflectionProperty(Filesystem::class, 'fileResolver'); - $property->setAccessible(true); - $property->setValue($filesystem, $mockFileResolver); - - //set validator to use mocked validator - $mockValidation = AspectMock::double(ValidationState::class, ['isValidationRequired' => false])->make(); - $property = new \ReflectionProperty(Filesystem::class, 'validationState'); - $property->setAccessible(true); - $property->setValue($filesystem, $mockValidation); + $fileResolver = $this->createMock(Module::class); + $fileResolver->method('get') + ->willReturn($fileIterator); + $validationState = $this->createMock(ValidationState::class); + $validationState->method('isValidationRequired') + ->willReturn(false); + $filesystem = new Filesystem( + $fileResolver, + $this->createMock(ConverterInterface::class), + $this->createMock(SchemaLocatorInterface::class), + $validationState, + '' + ); return $filesystem; } /** - * After class functionality * @return void */ - public static function tearDownAfterClass() + public static function tearDownAfterClass(): void { TestLoggingUtil::getInstance()->clearMockLoggingUtil(); parent::tearDownAfterClass(); diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Console/BaseGenerateCommandTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Console/BaseGenerateCommandTest.php index 234f677e8..6ce83ca9a 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Console/BaseGenerateCommandTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Console/BaseGenerateCommandTest.php @@ -3,24 +3,46 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Tests\unit\Magento\FunctionalTestingFramework\Console; +declare(strict_types=1); -use AspectMock\Test as AspectMock; -use PHPUnit\Framework\TestCase; +namespace tests\unit\Magento\FunctionalTestFramework\Console; + +use Exception; use Magento\FunctionalTestingFramework\Console\BaseGenerateCommand; -use Magento\FunctionalTestingFramework\Suite\Objects\SuiteObject; use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; -use Magento\FunctionalTestingFramework\Test\Objects\TestObject; +use Magento\FunctionalTestingFramework\Suite\Objects\SuiteObject; use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; +use Magento\FunctionalTestingFramework\Test\Objects\TestObject; +use PHPUnit\Framework\TestCase; +use ReflectionClass; +use ReflectionException; +use ReflectionProperty; class BaseGenerateCommandTest extends TestCase { - public function tearDown() + /** + * @inheritDoc + */ + protected function tearDown(): void { - AspectMock::clean(); + $handler = TestObjectHandler::getInstance(); + $testsProperty = new ReflectionProperty(TestObjectHandler::class, 'tests'); + $testsProperty->setAccessible(true); + $testsProperty->setValue($handler, []); + $testObjectHandlerProperty = new ReflectionProperty(TestObjectHandler::class, 'testObjectHandler'); + $testObjectHandlerProperty->setAccessible(true); + $testObjectHandlerProperty->setValue(null, $handler); + + $handler = SuiteObjectHandler::getInstance(); + $suiteObjectsProperty = new ReflectionProperty(SuiteObjectHandler::class, 'suiteObjects'); + $suiteObjectsProperty->setAccessible(true); + $suiteObjectsProperty->setValue($handler, []); + $suiteObjectHandlerProperty = new ReflectionProperty(SuiteObjectHandler::class, 'instance'); + $suiteObjectHandlerProperty->setAccessible(true); + $suiteObjectHandlerProperty->setValue(null, $handler); } - public function testOneTestOneSuiteConfig() + public function testOneTestOneSuiteConfig(): void { $testOne = new TestObject('Test1', [], [], []); $suiteOne = new SuiteObject('Suite1', ['Test1' => $testOne], [], []); @@ -35,7 +57,7 @@ public function testOneTestOneSuiteConfig() $this->assertEquals($expected, $actual); } - public function testOneTestTwoSuitesConfig() + public function testOneTestTwoSuitesConfig(): void { $testOne = new TestObject('Test1', [], [], []); $suiteOne = new SuiteObject('Suite1', ['Test1' => $testOne], [], []); @@ -51,7 +73,7 @@ public function testOneTestTwoSuitesConfig() $this->assertEquals($expected, $actual); } - public function testOneTestOneGroup() + public function testOneTestOneGroup(): void { $testOne = new TestObject('Test1', [], ['group' => ['Group1']], []); @@ -65,7 +87,7 @@ public function testOneTestOneGroup() $this->assertEquals($expected, $actual); } - public function testThreeTestsTwoGroup() + public function testThreeTestsTwoGroup(): void { $testOne = new TestObject('Test1', [], ['group' => ['Group1']], []); $testTwo = new TestObject('Test2', [], ['group' => ['Group1']], []); @@ -81,7 +103,7 @@ public function testThreeTestsTwoGroup() $this->assertEquals($expected, $actual); } - public function testOneTestOneSuiteOneGroupConfig() + public function testOneTestOneSuiteOneGroupConfig(): void { $testOne = new TestObject('Test1', [], ['group' => ['Group1']], []); $suiteOne = new SuiteObject('Suite1', ['Test1' => $testOne], [], []); @@ -96,7 +118,7 @@ public function testOneTestOneSuiteOneGroupConfig() $this->assertEquals($expected, $actual); } - public function testTwoTestOneSuiteTwoGroupConfig() + public function testTwoTestOneSuiteTwoGroupConfig(): void { $testOne = new TestObject('Test1', [], ['group' => ['Group1']], []); $testTwo = new TestObject('Test2', [], ['group' => ['Group2']], []); @@ -112,7 +134,7 @@ public function testTwoTestOneSuiteTwoGroupConfig() $this->assertEquals($expected, $actual); } - public function testTwoTestTwoSuiteOneGroupConfig() + public function testTwoTestTwoSuiteOneGroupConfig(): void { $testOne = new TestObject('Test1', [], ['group' => ['Group1']], []); $testTwo = new TestObject('Test2', [], ['group' => ['Group1']], []); @@ -131,10 +153,12 @@ public function testTwoTestTwoSuiteOneGroupConfig() /** * Test specific usecase of a test that is in a group with the group being called along with the suite - * i.e. run:group Group1 Suite1 - * @throws \Exception + * i.e. run:group Group1 Suite1. + * + * @return void + * @throws Exception */ - public function testThreeTestOneSuiteOneGroupMix() + public function testThreeTestOneSuiteOneGroupMix(): void { $testOne = new TestObject('Test1', [], [], []); $testTwo = new TestObject('Test2', [], [], []); @@ -156,7 +180,7 @@ public function testThreeTestOneSuiteOneGroupMix() $this->assertEquals($expected, $actual); } - public function testSuiteToTestSyntax() + public function testSuiteToTestSyntax(): void { $testOne = new TestObject('Test1', [], [], []); $suiteOne = new SuiteObject( @@ -175,51 +199,82 @@ public function testSuiteToTestSyntax() } /** - * Mock handlers to skip parsing + * Mock handlers to skip parsing. + * * @param array $testArray * @param array $suiteArray - * @throws \Exception + * + * @return void + * @throws Exception */ - public function mockHandlers($testArray, $suiteArray) + public function mockHandlers(array $testArray, array $suiteArray): void { - AspectMock::double(TestObjectHandler::class, ['initTestData' => ''])->make(); + // bypass the initTestData method + $testObjectHandlerClass = new ReflectionClass(TestObjectHandler::class); + $constructor = $testObjectHandlerClass->getConstructor(); + $constructor->setAccessible(true); + $testObjectHandlerObject = $testObjectHandlerClass->newInstanceWithoutConstructor(); + $constructor->invoke($testObjectHandlerObject); + + $testObjectHandlerProperty = new ReflectionProperty(TestObjectHandler::class, 'testObjectHandler'); + $testObjectHandlerProperty->setAccessible(true); + $testObjectHandlerProperty->setValue(null, $testObjectHandlerObject); + $handler = TestObjectHandler::getInstance(); - $property = new \ReflectionProperty(TestObjectHandler::class, 'tests'); + $property = new ReflectionProperty(TestObjectHandler::class, 'tests'); $property->setAccessible(true); $property->setValue($handler, $testArray); - AspectMock::double(SuiteObjectHandler::class, ['initSuiteData' => ''])->make(); + // bypass the initTestData method + $suiteObjectHandlerClass = new ReflectionClass(SuiteObjectHandler::class); + $constructor = $suiteObjectHandlerClass->getConstructor(); + $constructor->setAccessible(true); + $suiteObjectHandlerObject = $suiteObjectHandlerClass->newInstanceWithoutConstructor(); + $constructor->invoke($suiteObjectHandlerObject); + + $suiteObjectHandlerProperty = new ReflectionProperty(SuiteObjectHandler::class, 'instance'); + $suiteObjectHandlerProperty->setAccessible(true); + $suiteObjectHandlerProperty->setValue(null, $suiteObjectHandlerObject); + $handler = SuiteObjectHandler::getInstance(); - $property = new \ReflectionProperty(SuiteObjectHandler::class, 'suiteObjects'); + $property = new ReflectionProperty(SuiteObjectHandler::class, 'suiteObjects'); $property->setAccessible(true); $property->setValue($handler, $suiteArray); } /** - * Changes visibility and runs getTestAndSuiteConfiguration + * Changes visibility and runs getTestAndSuiteConfiguration. + * * @param array $testArray + * * @return string + * @throws ReflectionException */ - public function callTestConfig($testArray) + public function callTestConfig(array $testArray): string { $command = new BaseGenerateCommand(); - $class = new \ReflectionClass($command); + $class = new ReflectionClass($command); $method = $class->getMethod('getTestAndSuiteConfiguration'); $method->setAccessible(true); + return $method->invokeArgs($command, [$testArray]); } /** - * Changes visibility and runs getGroupAndSuiteConfiguration + * Changes visibility and runs getGroupAndSuiteConfiguration. + * * @param array $groupArray + * * @return string + * @throws ReflectionException */ - public function callGroupConfig($groupArray) + public function callGroupConfig(array $groupArray): string { $command = new BaseGenerateCommand(); - $class = new \ReflectionClass($command); + $class = new ReflectionClass($command); $method = $class->getMethod('getGroupAndSuiteConfiguration'); $method->setAccessible(true); + return $method->invokeArgs($command, [$groupArray]); } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Console/GenerateTestFailedCommandTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Console/GenerateTestFailedCommandTest.php new file mode 100644 index 000000000..317868300 --- /dev/null +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Console/GenerateTestFailedCommandTest.php @@ -0,0 +1,166 @@ +getMockBuilder(GenerateTestFailedCommand::class) + ->onlyMethods(["readFailedTestFile", "writeFailedTestToFile"]) + ->getMock(); + // Configure the stub. + $stub + ->method('readFailedTestFile') + ->willReturn($testFileReturn); + $stub + ->method('writeFailedTestToFile') + ->willReturn(null); + + // Run the real code + $configuration = $stub->getFailedTestList("", ""); + $this->assertEquals($expectedConfiguration, $configuration); + } + + public function testMultipleTestsWithSuites(): void + { + $testFileReturn = [ + "tests/functional/tests/MFTF/_generated/SomeSpecificSuite/FirstTestSuiteTest.php:SingleTestSuiteTest", + "tests/functional/tests/MFTF/_generated/SomeSpecificSuite/SecondTestNoSuiteTest.php:SingleTestNoSuiteTest" + ]; + $expectedConfiguration = + '{"tests":null,"suites":{"SomeSpecificSuite":["SingleTestSuiteTest","SingleTestNoSuiteTest"]}}'; + + // Create a stub for the SomeClass class. + $stub = $this->getMockBuilder(GenerateTestFailedCommand::class) + ->onlyMethods(["readFailedTestFile", "writeFailedTestToFile"]) + ->getMock(); + // Configure the stub. + $stub + ->method('readFailedTestFile') + ->willReturn($testFileReturn); + $stub + ->method('writeFailedTestToFile') + ->willReturn(null); + + // Run the real code + $configuration = $stub->getFailedTestList("", ""); + $this->assertEquals($expectedConfiguration, $configuration); + } + + public function testMultipleTestFailureWithNoSuites(): void + { + $testFileReturn = [ + "tests/functional/tests/MFTF/_generated/default/SingleTestNoSuiteTest.php:SingleTestNoSuiteTest", + "tests/functional/tests/MFTF/_generated/default/FirstTestSuiteTest.php:SingleTestSuiteTest" + ]; + $expectedConfiguration = '{"tests":["SingleTestNoSuiteTest","SingleTestSuiteTest"],"suites":null}'; + + // Create a stub for the SomeClass class. + $stub = $this->getMockBuilder(GenerateTestFailedCommand::class) + ->onlyMethods(["readFailedTestFile", "writeFailedTestToFile"]) + ->getMock(); + // Configure the stub. + $stub + ->method('readFailedTestFile') + ->willReturn($testFileReturn); + $stub + ->method('writeFailedTestToFile') + ->willReturn(null); + + // Run the real code + $configuration = $stub->getFailedTestList("", ""); + $this->assertEquals($expectedConfiguration, $configuration); + } + + public function testSingleSuiteAndNoTest(): void + { + $testFileReturn = [ + "tests/functional/tests/MFTF/_generated/SomeSpecificSuite/", + ]; + $expectedConfiguration = '{"tests":null,"suites":{"SomeSpecificSuite":[[]]}}'; + + // Create a stub for the SomeClass class. + $stub = $this->getMockBuilder(GenerateTestFailedCommand::class) + ->onlyMethods(["readFailedTestFile", "writeFailedTestToFile"]) + ->getMock(); + // Configure the stub. + $stub + ->method('readFailedTestFile') + ->willReturn($testFileReturn); + $stub + ->method('writeFailedTestToFile') + ->willReturn(null); + + // Run the real code + $configuration = $stub->getFailedTestList("", ""); + $this->assertEquals($expectedConfiguration, $configuration); + } + + public function testSingleSuiteWithTest(): void + { + $testFileReturn = [ + "tests/functional/tests/MFTF/_generated/SomeSpecificSuite/FirstTestSuiteTest.php:SingleTestSuiteTest", + ]; + $expectedConfiguration = '{"tests":null,"suites":{"SomeSpecificSuite":["SingleTestSuiteTest"]}}'; + + // Create a stub for the SomeClass class. + $stub = $this->getMockBuilder(GenerateTestFailedCommand::class) + ->onlyMethods(["readFailedTestFile", "writeFailedTestToFile"]) + ->getMock(); + // Configure the stub. + $stub + ->method('readFailedTestFile') + ->willReturn($testFileReturn); + $stub + ->method('writeFailedTestToFile') + ->willReturn(null); + + // Run the real code + $configuration = $stub->getFailedTestList("", ""); + $this->assertEquals($expectedConfiguration, $configuration); + } + + public function testMultipleSuitesWithNoTests(): void + { + $testFileReturn = [ + "tests/functional/tests/MFTF/_generated/SomeSpecificSuite/", + "tests/functional/tests/MFTF/_generated/SomeSpecificSuite1/", + + ]; + $expectedConfiguration = '{"tests":null,"suites":{"SomeSpecificSuite":[[]],"SomeSpecificSuite1":[[]]}}'; + + // Create a stub for the SomeClass class. + $stub = $this->getMockBuilder(GenerateTestFailedCommand::class) + ->onlyMethods(["readFailedTestFile", "writeFailedTestToFile"]) + ->getMock(); + // Configure the stub. + $stub + ->method('readFailedTestFile') + ->willReturn($testFileReturn); + $stub + ->method('writeFailedTestToFile') + ->willReturn(null); + + // Run the real code + $configuration = $stub->getFailedTestList("", ""); + $this->assertEquals($expectedConfiguration, $configuration); + } +} diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Console/GenerateTestsCommandTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Console/GenerateTestsCommandTest.php new file mode 100644 index 000000000..3e00c34a5 --- /dev/null +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Console/GenerateTestsCommandTest.php @@ -0,0 +1,81 @@ +getMethod('parseConfigParallelOptions'); + $method->setAccessible(true); + + if (is_array($expected)) { + $actual = $method->invokeArgs($command, [$time, $groups]); + $this->assertEquals($expected, $actual); + } else { + $this->expectException(FastFailException::class); + $this->expectExceptionMessage($expected); + $method->invokeArgs($command, [$time, $groups]); + } + } + + /** + * Data provider for testParseConfigParallelOptions() + * + * @return array + */ + public static function configParallelOptions(): array + { + return [ + [null, null, ['parallelByTime', 600000]], /* #0 */ + ['20', null, ['parallelByTime', 1200000]], /* #1 */ + [5, null, ['parallelByTime', 300000]], /* #2 */ + [null, '300', ['parallelByGroup', 300]], /* #3 */ + [null, 1000, ['parallelByGroup', 1000]], /* #4 */ + [0.5, null, "'time' option must be an integer and greater than 0"], /* #5 */ + [0, null, "'time' option must be an integer and greater than 0"], /* #6 */ + ['0', null, "'time' option must be an integer and greater than 0"], /* #7 */ + [0.0, null, "'time' option must be an integer and greater than 0"], /* #8 */ + ['0.0', null, "'time' option must be an integer and greater than 0"], /* #9 */ + [-10, null, "'time' option must be an integer and greater than 0"], /* #10 */ + ['-10', null, "'time' option must be an integer and greater than 0"], /* #11 */ + ['12x', null, "'time' option must be an integer and greater than 0"], /* #12 */ + [null, 0.5, "'groups' option must be an integer and greater than 0"], /* #13 */ + [null, 0, "'groups' option must be an integer and greater than 0"], /* #14 */ + [null, 0.0, "'groups' option must be an integer and greater than 0"], /* #15 */ + [null, '0', "'groups' option must be an integer and greater than 0"], /* #16 */ + [null, '0.0', "'groups' option must be an integer and greater than 0"], /* #17 */ + [null, -10, "'groups' option must be an integer and greater than 0"], /* #18 */ + [null, '-10', "'groups' option must be an integer and greater than 0"], /* #19 */ + [null, '12x', "'groups' option must be an integer and greater than 0"], /* #20 */ + ['20', '300', "'time' and 'groups' options are mutually exclusive. " + . "Only one can be specified for 'config parallel'" + ], /* #21 */ + [20, 300, "'time' and 'groups' options are mutually exclusive. " + . "Only one can be specified for 'config parallel'" + ], /* #22 */ + ['0', 0, "'time' and 'groups' options are mutually exclusive. " + . "Only one can be specified for 'config parallel'" + ], /* #23 */ + [[1], null, "'time' option must be an integer and greater than 0"], /* #24 */ + [null, [-1], "'groups' option must be an integer and greater than 0"], /* #25 */ + ]; + } +} diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Console/RunTestFailedCommandTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Console/RunTestFailedCommandTest.php new file mode 100644 index 000000000..db0c63c61 --- /dev/null +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Console/RunTestFailedCommandTest.php @@ -0,0 +1,132 @@ +invokePrivateMethod($runFailed, 'filterTestsForExecution', [$testFailedFile]); + $this->assertEquals($expectedResult, $filter); + } + + /** + * Invoking private method to be able to test it. + * NOTE: Bad practice don't repeat it. + * + * @param $object + * @param $methodName + * @param array $parameters + * @return mixed + * @throws \ReflectionException + */ + private function invokePrivateMethod(&$object, $methodName, array $parameters = []) + { + $reflection = new \ReflectionClass(get_class($object)); + $method = $reflection->getMethod($methodName); + $method->setAccessible(true); + return $method->invokeArgs($object, $parameters); + } + + public function testSingleTestNoSuite(): void + { + $testFailedFile = [ + "tests/functional/tests/MFTF/_generated/default/SingleTestNoSuiteTest.php:SingleTestNoSuiteTest" + ]; + + $expectedResult = [ + 'tests/functional/tests/MFTF/_generated/default/SingleTestNoSuiteTest.php' + ]; + + $runFailed = new RunTestFailedCommand('run:failed'); + $filter = $this->invokePrivateMethod($runFailed, 'filterTestsForExecution', [$testFailedFile]); + $this->assertEquals($expectedResult, $filter); + } + + public function testMultipleTestNoSuite(): void + { + $testFailedFile = [ + "tests/functional/tests/MFTF/_generated/default/SingleTestNoSuiteTest.php:SingleTestNoSuiteTest", + "tests/functional/tests/MFTF/_generated/default/FirstTestSuiteTest.php:SingleTestSuiteTest" + ]; + + $expectedResult = [ + "tests/functional/tests/MFTF/_generated/default/SingleTestNoSuiteTest.php", + "tests/functional/tests/MFTF/_generated/default/FirstTestSuiteTest.php" + ]; + + $runFailed = new RunTestFailedCommand('run:failed'); + $filter = $this->invokePrivateMethod($runFailed, 'filterTestsForExecution', [$testFailedFile]); + $this->assertEquals($expectedResult, $filter); + } + + public function testSingleSuiteNoTest(): void + { + $testFailedFile = [ + "tests/functional/tests/MFTF/_generated/SomeSpecificSuite/", + ]; + + $expectedResult = [ + "-g SomeSpecificSuite" + ]; + + $runFailed = new RunTestFailedCommand('run:failed'); + $filter = $this->invokePrivateMethod($runFailed, 'filterTestsForExecution', [$testFailedFile]); + $this->assertEquals($expectedResult, $filter); + } + + public function testSingleSuiteAndTest(): void + { + $testFailedFile = [ + "tests/functional/tests/MFTF/_generated/SomeSpecificSuite/FirstTestSuiteTest.php:SingleTestSuiteTest", + ]; + $expectedResult = [ + "-g SomeSpecificSuite", + ]; + + $runFailed = new RunTestFailedCommand('run:failed'); + $filter = $this->invokePrivateMethod($runFailed, 'filterTestsForExecution', [$testFailedFile]); + $this->assertEquals($expectedResult, $filter); + } + + public function testMultipleSuitesWithNoTest(): void + { + $testFailedFile = [ + "tests/functional/tests/MFTF/_generated/SomeSpecificSuite/", + "tests/functional/tests/MFTF/_generated/SomeSpecificSuite1/", + "tests/functional/tests/MFTF/_generated/SomeSpecificSuite2/" + ]; + $expectedResult = [ + "-g SomeSpecificSuite", + "-g SomeSpecificSuite1", + "-g SomeSpecificSuite2", + ]; + + $runFailed = new RunTestFailedCommand('run:failed'); + $filter = $this->invokePrivateMethod($runFailed, 'filterTestsForExecution', [$testFailedFile]); + $this->assertEquals($expectedResult, $filter); + } +} diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/DataObjectHandlerTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/DataObjectHandlerTest.php index d55f35247..059a94057 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/DataObjectHandlerTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/DataObjectHandlerTest.php @@ -3,22 +3,34 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace tests\unit\Magento\FunctionalTestFramework\DataGenerator\Handlers; -use AspectMock\Test as AspectMock; +use Exception; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; use Magento\FunctionalTestingFramework\DataGenerator\Parsers\DataProfileSchemaParser; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; use Magento\FunctionalTestingFramework\ObjectManager; use Magento\FunctionalTestingFramework\ObjectManagerFactory; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use ReflectionProperty; +use tests\unit\Util\MagentoTestCase; +use tests\unit\Util\TestLoggingUtil; /** * Class DataObjectHandlerTest */ class DataObjectHandlerTest extends MagentoTestCase { + /** + * @inheritDoc + */ + protected function setUp(): void + { + TestLoggingUtil::getInstance()->setMockLoggingUtil(); + } + // All tests share this array, feel free to add but be careful modifying or removing const PARSER_OUTPUT = [ 'entity' => [ @@ -44,6 +56,22 @@ class DataObjectHandlerTest extends MagentoTestCase ] ]; + const PARSER_OUTPUT_DEPRECATED = [ + 'entity' => [ + 'EntityOne' => [ + 'type' => 'testType', + 'data' => [ + 0 => [ + 'key' => 'testKey', + 'value' => 'testValue' + ] + ], + 'deprecated' => "deprecation message", + 'filename' => "filename.xml" + ], + ] + ]; + const PARSER_OUTPUT_WITH_EXTEND = [ 'entity' => [ 'EntityOne' => [ @@ -119,11 +147,14 @@ class DataObjectHandlerTest extends MagentoTestCase ]; /** - * getAllObjects should contain the expected data object + * Validate getAllObjects should contain the expected data object. + * + * @return void + * @throws Exception */ - public function testGetAllObjects() + public function testGetAllObjects(): void { - $this->setUpMockDataObjectHander(self::PARSER_OUTPUT); + $this->mockDataObjectHandlerWithData(self::PARSER_OUTPUT); // Call the method under test $actual = DataObjectHandler::getInstance()->getAllObjects(); @@ -135,11 +166,35 @@ public function testGetAllObjects() } /** - * getObject should return the expected data object if it exists + * Validate test deprecated data object. + * + * @return void + * @throws Exception + */ + public function testDeprecatedDataObject(): void + { + $this->mockDataObjectHandlerWithData(self::PARSER_OUTPUT_DEPRECATED); + + // Call the method under test + DataObjectHandler::getInstance()->getAllObjects(); + + //validate deprecation warning + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'warning', + 'DEPRECATION: The data entity \'EntityOne\' is deprecated.', + ['fileName' => 'filename.xml', 'deprecatedMessage' => 'deprecation message'] + ); + } + + /** + * Validate getObject should return the expected data object if it exists. + * + * @return void + * @throws Exception */ - public function testGetObject() + public function testGetObject(): void { - $this->setUpMockDataObjectHander(self::PARSER_OUTPUT); + $this->mockDataObjectHandlerWithData(self::PARSER_OUTPUT); // Call the method under test $actual = DataObjectHandler::getInstance()->getObject('EntityOne'); @@ -150,22 +205,28 @@ public function testGetObject() } /** - * getAllObjects should return the expected data object if it exists + * Validate getAllObjects should return the expected data object if it exists. + * + * @return void + * @throws Exception */ - public function testGetObjectNull() + public function testGetObjectNull(): void { - $this->setUpMockDataObjectHander(self::PARSER_OUTPUT); + $this->mockDataObjectHandlerWithData(self::PARSER_OUTPUT); $actual = DataObjectHandler::getInstance()->getObject('h953u789h0g73t521'); // doesnt exist $this->assertNull($actual); } /** - * getAllObjects should contain the expected data object with extends + * Validate getAllObjects should contain the expected data object with extends. + * + * @return void + * @throws Exception */ - public function testGetAllObjectsWithDataExtends() + public function testGetAllObjectsWithDataExtends(): void { - $this->setUpMockDataObjectHander(self::PARSER_OUTPUT_WITH_EXTEND); + $this->mockDataObjectHandlerWithData(self::PARSER_OUTPUT_WITH_EXTEND); // Call the method under test $actual = DataObjectHandler::getInstance()->getAllObjects(); @@ -185,11 +246,14 @@ public function testGetAllObjectsWithDataExtends() } /** - * getObject should return the expected data object with extended data if it exists + * Validate getObject should return the expected data object with extended data if it exists. + * + * @return void + * @throws Exception */ - public function testGetObjectWithDataExtends() + public function testGetObjectWithDataExtends(): void { - $this->setUpMockDataObjectHander(self::PARSER_OUTPUT_WITH_EXTEND); + $this->mockDataObjectHandlerWithData(self::PARSER_OUTPUT_WITH_EXTEND); // Call the method under test $actual = DataObjectHandler::getInstance()->getObject('EntityTwo'); @@ -208,15 +272,18 @@ public function testGetObjectWithDataExtends() } /** - * getAllObjects should throw TestFrameworkException exception if some data extends itself + * Validate getAllObjects should throw TestFrameworkException exception if some data extends itself. + * + * @return void + * @throws Exception */ - public function testGetAllObjectsWithDataExtendsItself() + public function testGetAllObjectsWithDataExtendsItself(): void { - $this->setUpMockDataObjectHander(self::PARSER_OUTPUT_WITH_EXTEND_INVALID); + $this->mockDataObjectHandlerWithData(self::PARSER_OUTPUT_WITH_EXTEND_INVALID); - $this->expectException(\Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException::class); + $this->expectException(TestFrameworkException::class); $this->expectExceptionMessage( - "Mftf Data can not extend from itself: " + 'Mftf Data can not extend from itself: ' . self::PARSER_OUTPUT_WITH_EXTEND_INVALID['entity']['EntityOne']['name'] ); @@ -225,15 +292,18 @@ public function testGetAllObjectsWithDataExtendsItself() } /** - * getObject should throw TestFrameworkException exception if requested data extends itself + * Validate getObject should throw TestFrameworkException exception if requested data extends itself. + * + * @return void + * @throws Exception */ - public function testGetObjectWithDataExtendsItself() + public function testGetObjectWithDataExtendsItself(): void { - $this->setUpMockDataObjectHander(self::PARSER_OUTPUT_WITH_EXTEND_INVALID); + $this->mockDataObjectHandlerWithData(self::PARSER_OUTPUT_WITH_EXTEND_INVALID); - $this->expectException(\Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException::class); + $this->expectException(TestFrameworkException::class); $this->expectExceptionMessage( - "Mftf Data can not extend from itself: " + 'Mftf Data can not extend from itself: ' . self::PARSER_OUTPUT_WITH_EXTEND_INVALID['entity']['EntityOne']['name'] ); @@ -244,29 +314,65 @@ public function testGetObjectWithDataExtendsItself() } /** - * Set up everything required to mock DataObjectHander::getInstance() - * The first call to getInstance() uses these mocks to emulate the parser, initializing internal state - * according to the PARSER_OUTPUT value + * Create mock data object handler with data. + * + * @param array $mockData * - * @param array $entityDataArray + * @return void */ - private function setUpMockDataObjectHander($entityDataArray) + private function mockDataObjectHandlerWithData(array $mockData): void { - // Clear DataObjectHandler singleton if already set - $property = new \ReflectionProperty(DataObjectHandler::class, "INSTANCE"); + $dataObjectHandlerProperty = new ReflectionProperty(DataObjectHandler::class, "INSTANCE"); + $dataObjectHandlerProperty->setAccessible(true); + $dataObjectHandlerProperty->setValue(null, null); + + $mockDataProfileSchemaParser = $this->createMock(DataProfileSchemaParser::class); + $mockDataProfileSchemaParser + ->method('readDataProfiles') + ->willReturn($mockData); + + $objectManager = ObjectManagerFactory::getObjectManager(); + $mockObjectManagerInstance = $this->createMock(ObjectManager::class); + $mockObjectManagerInstance + ->method('create') + ->will( + $this->returnCallback( + function ( + string $class, + array $arguments = [] + ) use ( + $objectManager, + $mockDataProfileSchemaParser + ) { + if ($class === DataProfileSchemaParser::class) { + return $mockDataProfileSchemaParser; + } + + return $objectManager->create($class, $arguments); + } + ) + ); + + $property = new ReflectionProperty(ObjectManager::class, 'instance'); $property->setAccessible(true); - $property->setValue(null); + $property->setValue(null, $mockObjectManagerInstance); + } + + /** + * @inheritDoc + */ + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); - $mockDataProfileSchemaParser = AspectMock::double(DataProfileSchemaParser::class, [ - 'readDataProfiles' => $entityDataArray - ])->make(); + $dataObjectHandlerProperty = new ReflectionProperty(DataObjectHandler::class, "INSTANCE"); + $dataObjectHandlerProperty->setAccessible(true); + $dataObjectHandlerProperty->setValue(null, null); - $mockObjectManager = AspectMock::double(ObjectManager::class, [ - 'create' => $mockDataProfileSchemaParser - ])->make(); + $objectManagerProperty = new ReflectionProperty(ObjectManager::class, 'instance'); + $objectManagerProperty->setAccessible(true); + $objectManagerProperty->setValue(null, null); - AspectMock::double(ObjectManagerFactory::class, [ - 'getObjectManager' => $mockObjectManager - ]); + TestLoggingUtil::getInstance()->clearMockLoggingUtil(); } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/OperationDefinitionObjectHandlerTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/OperationDefinitionObjectHandlerTest.php index b54980314..23dbcacc1 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/OperationDefinitionObjectHandlerTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/OperationDefinitionObjectHandlerTest.php @@ -3,29 +3,46 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace tests\unit\Magento\FunctionalTestFramework\DataGenerator\Handlers; -use AspectMock\Test as AspectMock; +use Exception; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\OperationDefinitionObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\OperationDefinitionObject; use Magento\FunctionalTestingFramework\DataGenerator\Objects\OperationElement; +use Magento\FunctionalTestingFramework\DataGenerator\Parsers\OperationDefinitionParser; use Magento\FunctionalTestingFramework\ObjectManager; use Magento\FunctionalTestingFramework\ObjectManagerFactory; -use Magento\FunctionalTestingFramework\DataGenerator\Handlers\OperationDefinitionObjectHandler; -use Magento\FunctionalTestingFramework\DataGenerator\Parsers\OperationDefinitionParser; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use ReflectionProperty; +use tests\unit\Util\MagentoTestCase; +use tests\unit\Util\TestLoggingUtil; /** * Class OperationDefinitionObjectHandlerTest */ class OperationDefinitionObjectHandlerTest extends MagentoTestCase { - public function testGetMultipleObjects() + /** + * @inheritDoc + */ + protected function setUp(): void + { + TestLoggingUtil::getInstance()->setMockLoggingUtil(); + } + + /** + * Validate testGetMultipleObjects. + * + * @return void + * @throws Exception + */ + public function testGetMultipleObjects(): void { // Data Variables for Assertions - $dataType1 = "type1"; - $operationType1 = "create"; - $operationType2 = "update"; + $dataType1 = 'type1'; + $operationType1 = 'create'; + $operationType2 = 'update'; /** * Parser Output. Just two simple pieces of metadata with 1 field each @@ -37,33 +54,37 @@ public function testGetMultipleObjects() * has field * key=id, value=integer */ - $mockData = [OperationDefinitionObjectHandler::ENTITY_OPERATION_ROOT_TAG => [ - "testOperationName" => [ - OperationDefinitionObjectHandler::ENTITY_OPERATION_DATA_TYPE => $dataType1, - OperationDefinitionObjectHandler::ENTITY_OPERATION_TYPE => $operationType1, - OperationDefinitionObjectHandler::ENTITY_OPERATION_AUTH => "auth", - OperationDefinitionObjectHandler::ENTITY_OPERATION_URL => "V1/Type1", - OperationDefinitionObjectHandler::ENTITY_OPERATION_METHOD => "POST", - OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY => [ - 0 => [ - OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_KEY => "id", - OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_VALUE => "integer" - ], + $mockData = [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_ROOT_TAG => [ + 'testOperationName' => [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_DATA_TYPE => $dataType1, + OperationDefinitionObjectHandler::ENTITY_OPERATION_TYPE => $operationType1, + OperationDefinitionObjectHandler::ENTITY_OPERATION_AUTH => 'auth', + OperationDefinitionObjectHandler::ENTITY_OPERATION_URL => 'V1/Type1', + OperationDefinitionObjectHandler::ENTITY_OPERATION_METHOD => 'POST', + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY => [ + 0 => [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_KEY => 'id', + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_VALUE => 'integer' + ], + ] + ], + [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_DATA_TYPE => $dataType1, + OperationDefinitionObjectHandler::ENTITY_OPERATION_TYPE => $operationType2, + OperationDefinitionObjectHandler::ENTITY_OPERATION_AUTH => 'auth', + OperationDefinitionObjectHandler::ENTITY_OPERATION_URL => 'V1/Type1/{id}', + OperationDefinitionObjectHandler::ENTITY_OPERATION_METHOD => 'PUT', + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY => [ + 0 => [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_KEY => 'id', + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_VALUE => 'integer' + ], ] - ],[ - OperationDefinitionObjectHandler::ENTITY_OPERATION_DATA_TYPE => $dataType1, - OperationDefinitionObjectHandler::ENTITY_OPERATION_TYPE => $operationType2, - OperationDefinitionObjectHandler::ENTITY_OPERATION_AUTH => "auth", - OperationDefinitionObjectHandler::ENTITY_OPERATION_URL => "V1/Type1/{id}", - OperationDefinitionObjectHandler::ENTITY_OPERATION_METHOD => "PUT", - OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY => [ - 0 => [ - OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_KEY => "id", - OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_VALUE => "integer" - ], ] - ]]]; - $this->setMockParserOutput($mockData); + ] + ]; + $this->mockOperationHandlerWithData($mockData); //Perform Assertions $operationDefinitionManager = OperationDefinitionObjectHandler::getInstance(); @@ -72,28 +93,93 @@ public function testGetMultipleObjects() $this->assertArrayHasKey($operationType2 . $dataType1, $operations); } - public function testObjectCreation() + /** + * Validate testDeprecatedOperation. + * + * @return void + * @throws Exception + */ + public function testDeprecatedOperation(): void { // Data Variables for Assertions - $testDataTypeName1 = "type1"; - $testAuth = "auth"; - $testUrl = "V1/dataType"; - $testOperationType = "create"; - $testMethod = "POST"; - $testSuccessRegex = "/messages-message-success/"; - $testContentType = "application/json"; - $testHeaderParam = "testParameter"; - $testHeaderValue = "testHeader"; + $dataType1 = 'type1'; + $operationType1 = 'create'; + + /** + * Parser Output. Just one metadata with 1 field + * operationName + * createType1 + * has field + * key=id, value=integer + */ + $mockData = [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_ROOT_TAG => [ + 'testOperationName' => [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_DATA_TYPE => $dataType1, + OperationDefinitionObjectHandler::ENTITY_OPERATION_TYPE => $operationType1, + OperationDefinitionObjectHandler::ENTITY_OPERATION_AUTH => 'auth', + OperationDefinitionObjectHandler::ENTITY_OPERATION_URL => 'V1/Type1', + OperationDefinitionObjectHandler::ENTITY_OPERATION_METHOD => 'POST', + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY => [ + 0 => [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_KEY => 'id', + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_VALUE => 'integer' + ], + ], + OperationDefinitionObjectHandler::OBJ_DEPRECATED => 'deprecation message' + ] + ] + ]; + $this->mockOperationHandlerWithData($mockData); + + //Perform Assertions + $operationDefinitionManager = OperationDefinitionObjectHandler::getInstance(); + $operations = $operationDefinitionManager->getAllObjects(); + + $this->assertArrayHasKey($operationType1 . $dataType1, $operations); + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'notice', + 'NOTICE: 1 metadata operation name violations detected. See mftf.log for details.', + [] + ); + // test run time deprecation notice + $operation = $operationDefinitionManager->getOperationDefinition($operationType1, $dataType1); + $operation->logDeprecated(); + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'warning', + 'DEPRECATION: The operation testOperationName is deprecated.', + ['operationType' => 'create', 'deprecatedMessage' => 'deprecation message'] + ); + } + + /** + * Validate testObjectCreation. + * + * @return void + * @throws Exception + */ + public function testObjectCreation(): void + { + // Data Variables for Assertions + $testDataTypeName1 = 'type1'; + $testAuth = 'auth'; + $testUrl = 'V1/dataType'; + $testOperationType = 'create'; + $testMethod = 'POST'; + $testSuccessRegex = '/messages-message-success/'; + $testContentType = 'application/json'; + $testHeaderParam = 'testParameter'; + $testHeaderValue = 'testHeader'; // Nested Object variables - $nestedObjectKey = "objectKey"; - $nestedObjectType = "objectType"; - $nestedEntryKey1 = "id"; - $nestedEntryValue1 = "integer"; - $nestedEntryKey2 = "name"; - $nestedEntryValue2 = "string"; - $nestedEntryRequired2 = "true"; - $nestedEntryKey3 = "active"; - $nestedEntryValue3 = "boolean"; + $nestedObjectKey = 'objectKey'; + $nestedObjectType = 'objectType'; + $nestedEntryKey1 = 'id'; + $nestedEntryValue1 = 'integer'; + $nestedEntryKey2 = 'name'; + $nestedEntryValue2 = 'string'; + $nestedEntryRequired2 = 'true'; + $nestedEntryKey3 = 'active'; + $nestedEntryValue3 = 'boolean'; /** * Complex Object @@ -111,56 +197,70 @@ public function testObjectCreation() * key active, value boolean * */ - $mockData = [OperationDefinitionObjectHandler::ENTITY_OPERATION_ROOT_TAG => [ - "testOperationName" => [ - OperationDefinitionObjectHandler::ENTITY_OPERATION_DATA_TYPE => $testDataTypeName1, - OperationDefinitionObjectHandler::ENTITY_OPERATION_TYPE => $testOperationType, - OperationDefinitionObjectHandler::ENTITY_OPERATION_AUTH => $testAuth, - OperationDefinitionObjectHandler::ENTITY_OPERATION_URL => $testUrl, - OperationDefinitionObjectHandler::ENTITY_OPERATION_METHOD => $testMethod, - OperationDefinitionObjectHandler::ENTITY_OPERATION_SUCCESS_REGEX => $testSuccessRegex, - OperationDefinitionObjectHandler::ENTITY_OPERATION_CONTENT_TYPE => [ - 0 => [ - "value" => $testContentType - ] - ], - OperationDefinitionObjectHandler::ENTITY_OPERATION_HEADER => [ - 0 => [ - OperationDefinitionObjectHandler::ENTITY_OPERATION_HEADER_PARAM => $testHeaderParam, - OperationDefinitionObjectHandler::ENTITY_OPERATION_HEADER_VALUE => $testHeaderValue, - ] - ], - OperationDefinitionObjectHandler::ENTITY_OPERATION_URL_PARAM => [ - 0 => [ - OperationDefinitionObjectHandler::ENTITY_OPERATION_URL_PARAM_KEY => "testUrlParamKey", - OperationDefinitionObjectHandler::ENTITY_OPERATION_URL_PARAM_VALUE => "testUrlParamValue" - ] - ], - OperationDefinitionObjectHandler::ENTITY_OPERATION_OBJECT => [ - 0 => [ - OperationDefinitionObjectHandler::ENTITY_OPERATION_OBJECT_KEY => $nestedObjectKey, - OperationDefinitionObjectHandler::ENTITY_OPERATION_DATA_TYPE => $nestedObjectType, - OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY => [ - 0 => [ - OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_KEY => $nestedEntryKey1, - OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_VALUE => $nestedEntryValue1 - ], - 1 => [ - OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_KEY => $nestedEntryKey2, - OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_VALUE => $nestedEntryValue2, - OperationDefinitionObjectHandler::ENTITY_OPERATION_REQUIRED => $nestedEntryRequired2 - ], - 2 => [ - OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_KEY => $nestedEntryKey3, - OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_VALUE => $nestedEntryValue3 + $mockData = [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_ROOT_TAG => [ + 'testOperationName' => [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_DATA_TYPE => $testDataTypeName1, + OperationDefinitionObjectHandler::ENTITY_OPERATION_TYPE => $testOperationType, + OperationDefinitionObjectHandler::ENTITY_OPERATION_AUTH => $testAuth, + OperationDefinitionObjectHandler::ENTITY_OPERATION_URL => $testUrl, + OperationDefinitionObjectHandler::ENTITY_OPERATION_METHOD => $testMethod, + OperationDefinitionObjectHandler::ENTITY_OPERATION_SUCCESS_REGEX => $testSuccessRegex, + OperationDefinitionObjectHandler::ENTITY_OPERATION_CONTENT_TYPE => [ + 0 => [ + 'value' => $testContentType + ] + ], + OperationDefinitionObjectHandler::ENTITY_OPERATION_HEADER => [ + 0 => [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_HEADER_PARAM => $testHeaderParam, + OperationDefinitionObjectHandler::ENTITY_OPERATION_HEADER_VALUE => $testHeaderValue, + ] + ], + OperationDefinitionObjectHandler::ENTITY_OPERATION_URL_PARAM => [ + 0 => [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_URL_PARAM_KEY => 'testUrlParamKey', + OperationDefinitionObjectHandler::ENTITY_OPERATION_URL_PARAM_VALUE => 'testUrlParamValue' + ] + ], + OperationDefinitionObjectHandler::ENTITY_OPERATION_OBJECT => [ + 0 => [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_OBJECT_KEY => $nestedObjectKey, + OperationDefinitionObjectHandler::ENTITY_OPERATION_DATA_TYPE => $nestedObjectType, + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY => [ + 0 => [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_KEY => $nestedEntryKey1, + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_VALUE => + $nestedEntryValue1 + ], + 1 => [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_KEY => $nestedEntryKey2, + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_VALUE => + $nestedEntryValue2, + OperationDefinitionObjectHandler::ENTITY_OPERATION_REQUIRED => + $nestedEntryRequired2 + ], + 2 => [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_KEY => $nestedEntryKey3, + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_VALUE => + $nestedEntryValue3 + ] ] ] - ] - ], - ]]]; + ], + ] + ] + ]; // Prepare objects to compare against $field = OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY; - $expectedNestedField = new OperationElement($nestedEntryKey1, $nestedEntryValue1, $field, false, [], null); + $expectedNestedField = new OperationElement( + $nestedEntryKey1, + $nestedEntryValue1, + $field, + false, + [], + null + ); $expectedNestedField2 = new OperationElement( $nestedEntryKey2, $nestedEntryValue2, @@ -169,18 +269,29 @@ public function testObjectCreation() [], null ); - $expectedNestedField3 = new OperationElement($nestedEntryKey3, $nestedEntryValue3, $field, false, [], null); + $expectedNestedField3 = new OperationElement( + $nestedEntryKey3, + $nestedEntryValue3, + $field, + false, + [], + null + ); $expectedOperation = new OperationElement( $nestedObjectKey, $nestedObjectType, OperationDefinitionObjectHandler::ENTITY_OPERATION_OBJECT, false, [], - [0 => $expectedNestedField, 1 => $expectedNestedField2, 2 =>$expectedNestedField3] + [ + 0 => $expectedNestedField, + 1 => $expectedNestedField2, + 2 => $expectedNestedField3 + ] ); // Set up mocked data output - $this->setMockParserOutput($mockData); + $this->mockOperationHandlerWithData($mockData); // Get Operation $operationDefinitionManager = OperationDefinitionObjectHandler::getInstance(); @@ -204,16 +315,22 @@ public function testObjectCreation() $this->assertEquals($expectedOperation, $operation->getOperationMetadata()[0]); } - public function testObjectArrayCreation() + /** + * Validate testObjectArrayCreation. + * + * @return void + * @throws Exception + */ + public function testObjectArrayCreation(): void { // Data Variables for Assertions - $dataType1 = "type1"; - $operationType1 = "create"; - $objectArrayKey = "ObjectArray"; - $twiceNestedObjectKey = "nestedObjectKey"; - $twiceNestedObjectType = "nestedObjectType"; - $twiceNestedEntryKey = "nestedFieldKey"; - $twiceNestedEntryValue = "string"; + $dataType1 = 'type1'; + $operationType1 = 'create'; + $objectArrayKey = 'ObjectArray'; + $twiceNestedObjectKey = 'nestedObjectKey'; + $twiceNestedObjectType = 'nestedObjectType'; + $twiceNestedEntryKey = 'nestedFieldKey'; + $twiceNestedEntryValue = 'string'; // Parser Output /** * Metadata with nested array of objects, with a single field @@ -223,32 +340,38 @@ public function testObjectArrayCreation() * objects with key = nestedObjectKey, type = nestedObjectType * has field with key = nestedFieldKey, value = string */ - $mockData = [OperationDefinitionObjectHandler::ENTITY_OPERATION_ROOT_TAG => [ - "testOperationName" => [ - OperationDefinitionObjectHandler::ENTITY_OPERATION_DATA_TYPE => $dataType1, - OperationDefinitionObjectHandler::ENTITY_OPERATION_TYPE => $operationType1, - OperationDefinitionObjectHandler::ENTITY_OPERATION_AUTH => "auth", - OperationDefinitionObjectHandler::ENTITY_OPERATION_URL => "V1/Type1", - OperationDefinitionObjectHandler::ENTITY_OPERATION_METHOD => "POST", - OperationDefinitionObjectHandler::ENTITY_OPERATION_ARRAY => [ - 0 => [ - OperationDefinitionObjectHandler::ENTITY_OPERATION_OBJECT_KEY => $objectArrayKey, - OperationDefinitionObjectHandler::ENTITY_OPERATION_OBJECT => [ - 0 => [ - OperationDefinitionObjectHandler::ENTITY_OPERATION_OBJECT_KEY => $twiceNestedObjectKey, - OperationDefinitionObjectHandler::ENTITY_OPERATION_DATA_TYPE => $twiceNestedObjectType, - OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY => [ - 0 => [ - OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_KEY => - $twiceNestedEntryKey, - OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_VALUE => - $twiceNestedEntryValue + $mockData = [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_ROOT_TAG => [ + 'testOperationName' => [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_DATA_TYPE => $dataType1, + OperationDefinitionObjectHandler::ENTITY_OPERATION_TYPE => $operationType1, + OperationDefinitionObjectHandler::ENTITY_OPERATION_AUTH => 'auth', + OperationDefinitionObjectHandler::ENTITY_OPERATION_URL => 'V1/Type1', + OperationDefinitionObjectHandler::ENTITY_OPERATION_METHOD => 'POST', + OperationDefinitionObjectHandler::ENTITY_OPERATION_ARRAY => [ + 0 => [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_OBJECT_KEY => $objectArrayKey, + OperationDefinitionObjectHandler::ENTITY_OPERATION_OBJECT => [ + 0 => [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_OBJECT_KEY => + $twiceNestedObjectKey, + OperationDefinitionObjectHandler::ENTITY_OPERATION_DATA_TYPE => + $twiceNestedObjectType, + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY => [ + 0 => [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_KEY => + $twiceNestedEntryKey, + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_VALUE => + $twiceNestedEntryValue + ] ] ] ] ] ] - ]]]]; + ] + ] + ]; // Prepare Objects to compare against $twoLevelNestedMetadata = new OperationElement( $twiceNestedEntryKey, @@ -265,7 +388,9 @@ public function testObjectArrayCreation() OperationDefinitionObjectHandler::ENTITY_OPERATION_OBJECT, false, [], - [0 => $twoLevelNestedMetadata] + [ + 0 => $twoLevelNestedMetadata + ] ); $expectedOperation = new OperationElement( @@ -273,12 +398,14 @@ public function testObjectArrayCreation() $twiceNestedObjectType, $twiceNestedObjectKey, false, - [$twiceNestedObjectKey => $oneLevelNestedMetadata], + [ + $twiceNestedObjectKey => $oneLevelNestedMetadata + ], null ); // Set up mocked data output - $this->setMockParserOutput($mockData); + $this->mockOperationHandlerWithData($mockData); // Get Operation $operationDefinitionManager = OperationDefinitionObjectHandler::getInstance(); @@ -288,15 +415,21 @@ public function testObjectArrayCreation() $this->assertEquals($expectedOperation, $operation->getOperationMetadata()[0]); } - public function testLooseJsonCreation() + /** + * Validate testLooseJsonCreation. + * + * @return void + * @throws Exception + */ + public function testLooseJsonCreation(): void { // Data Variables for Assertions - $dataType = "dataType"; - $operationType = "create"; - $entryKey = "id"; - $entryValue = "integer"; - $arrayKey = "arrayKey"; - $arrayValue = "string"; + $dataType = 'dataType'; + $operationType = 'create'; + $entryKey = 'id'; + $entryValue = 'integer'; + $arrayKey = 'arrayKey'; + $arrayValue = 'string'; /** * Operation with no objects, just an entry and an array of strings * testOperationName @@ -305,28 +438,30 @@ public function testLooseJsonCreation() * has array key = arrayKey * fields of value = string */ - $mockData = [OperationDefinitionObjectHandler::ENTITY_OPERATION_ROOT_TAG => [ - "testOperationName" => [ - OperationDefinitionObjectHandler::ENTITY_OPERATION_DATA_TYPE => $dataType, - OperationDefinitionObjectHandler::ENTITY_OPERATION_TYPE => $operationType, - OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY => [ - 0 => [ - OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_KEY => $entryKey, - OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_VALUE => $entryValue - ] - ], - OperationDefinitionObjectHandler::ENTITY_OPERATION_ARRAY => [ - 0 => [ - OperationDefinitionObjectHandler::ENTITY_OPERATION_ARRAY_KEY => $arrayKey, - OperationDefinitionObjectHandler::ENTITY_OPERATION_ARRAY_VALUE => [ - 0 => [ - OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_VALUE => $arrayValue + $mockData = [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_ROOT_TAG => [ + 'testOperationName' => [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_DATA_TYPE => $dataType, + OperationDefinitionObjectHandler::ENTITY_OPERATION_TYPE => $operationType, + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY => [ + 0 => [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_KEY => $entryKey, + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_VALUE => $entryValue + ] + ], + OperationDefinitionObjectHandler::ENTITY_OPERATION_ARRAY => [ + 0 => [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_ARRAY_KEY => $arrayKey, + OperationDefinitionObjectHandler::ENTITY_OPERATION_ARRAY_VALUE => [ + 0 => [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_VALUE => $arrayValue + ] ] ] ] ] ] - ]]; + ]; // Prepare Objects to assert against $entry = new OperationElement( $entryKey, @@ -346,7 +481,7 @@ public function testLooseJsonCreation() ); // Set up mocked data output - $this->setMockParserOutput($mockData); + $this->mockOperationHandlerWithData($mockData); // get Operations $operationDefinitionManager = OperationDefinitionObjectHandler::getInstance(); @@ -358,25 +493,71 @@ public function testLooseJsonCreation() } /** - * Function used to set mock for parser return and force init method to run between tests. + * Create mock operation handler with data. * - * @param array $data + * @param array $mockData + * + * @return void */ - private function setMockParserOutput($data) + private function mockOperationHandlerWithData(array $mockData): void { - // clear Operation object handler value to inject parsed content - $property = new \ReflectionProperty( + $operationDefinitionObjectHandlerProperty = new ReflectionProperty( OperationDefinitionObjectHandler::class, 'INSTANCE' ); + $operationDefinitionObjectHandlerProperty->setAccessible(true); + $operationDefinitionObjectHandlerProperty->setValue(null, null); + + $mockOperationParser = $this->createMock(OperationDefinitionParser::class); + $mockOperationParser + ->method('readOperationMetadata') + ->willReturn($mockData); + + $objectManager = ObjectManagerFactory::getObjectManager(); + $mockObjectManagerInstance = $this->createMock(ObjectManager::class); + $mockObjectManagerInstance + ->method('create') + ->will( + $this->returnCallback( + function ( + string $class, + array $arguments = [] + ) use ( + $objectManager, + $mockOperationParser + ) { + if ($class === OperationDefinitionParser::class) { + return $mockOperationParser; + } + + return $objectManager->create($class, $arguments); + } + ) + ); + + $property = new ReflectionProperty(ObjectManager::class, 'instance'); $property->setAccessible(true); - $property->setValue(null); - - $mockOperationParser = AspectMock::double( - OperationDefinitionParser::class, - ["readOperationMetadata" => $data] - )->make(); - $instance = AspectMock::double(ObjectManager::class, ['create' => $mockOperationParser])->make(); - AspectMock::double(ObjectManagerFactory::class, ['getObjectManager' => $instance]); + $property->setValue(null, $mockObjectManagerInstance); + } + + /** + * @inheritDoc + */ + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + + $operationDefinitionObjectHandlerProperty = new ReflectionProperty( + OperationDefinitionObjectHandler::class, + 'INSTANCE' + ); + $operationDefinitionObjectHandlerProperty->setAccessible(true); + $operationDefinitionObjectHandlerProperty->setValue(null, null); + + $objectManagerProperty = new ReflectionProperty(ObjectManager::class, 'instance'); + $objectManagerProperty->setAccessible(true); + $objectManagerProperty->setValue(null, null); + + TestLoggingUtil::getInstance()->clearMockLoggingUtil(); } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/PersistedObjectHandlerTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/PersistedObjectHandlerTest.php index 592b87f4e..19237247b 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/PersistedObjectHandlerTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/PersistedObjectHandlerTest.php @@ -3,10 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace tests\unit\Magento\FunctionalTestFramework\DataGenerator\Handlers; -use AspectMock\Test as AspectMock; +use Exception; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\PersistedObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Parsers\DataProfileSchemaParser; @@ -15,7 +16,8 @@ use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; use Magento\FunctionalTestingFramework\ObjectManager; use Magento\FunctionalTestingFramework\ObjectManagerFactory; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use ReflectionProperty; +use tests\unit\Util\MagentoTestCase; use tests\unit\Util\TestLoggingUtil; /** @@ -24,28 +26,31 @@ class PersistedObjectHandlerTest extends MagentoTestCase { /** - * Before test functionality - * @return void + * @inheritDoc */ - public function setUp() + protected function setUp(): void { TestLoggingUtil::getInstance()->setMockLoggingUtil(); } - public function testCreateEntityWithNonExistingName() + /** + * Validate testCreateEntityWithNonExistingName. + * + * @return void + * @throws TestReferenceException + */ + public function testCreateEntityWithNonExistingName(): void { // Test Data and Variables - $entityName = "InvalidEntity"; - $entityStepKey = "StepKey"; + $entityName = 'InvalidEntity'; + $entityStepKey = 'StepKey'; $scope = PersistedObjectHandler::TEST_SCOPE; $exceptionMessage = "Entity \"" . $entityName . "\" does not exist." . "\nException occurred executing action at StepKey \"" . $entityStepKey . "\""; $this->expectException(TestReferenceException::class); - $this->expectExceptionMessage($exceptionMessage); - $handler = PersistedObjectHandler::getInstance(); // Call method @@ -56,13 +61,19 @@ public function testCreateEntityWithNonExistingName() ); } - public function testCreateSimpleEntity() + /** + * Validate testCreateSimpleEntity. + * + * @return void + * @throws TestReferenceException + */ + public function testCreateSimpleEntity(): void { // Test Data and Variables - $entityName = "EntityOne"; - $entityStepKey = "StepKey"; - $dataKey = "testKey"; - $dataValue = "testValue"; + $entityName = 'EntityOne'; + $entityStepKey = 'StepKey'; + $dataKey = 'testKey'; + $dataValue = 'testValue'; $scope = PersistedObjectHandler::TEST_SCOPE; $parserOutput = [ 'entity' => [ @@ -77,15 +88,13 @@ public function testCreateSimpleEntity() ] ] ]; - $jsonResponse = " + $jsonResponse = " { \"" . strtolower($dataKey) . "\" : \"{$dataValue}\" } "; - // Mock Classes - $this->mockDataHandlerWithOutput($parserOutput); - $this->mockCurlHandler($jsonResponse); + $this->mockCurlHandler($jsonResponse, $parserOutput); $handler = PersistedObjectHandler::getInstance(); // Call method @@ -99,13 +108,19 @@ public function testCreateSimpleEntity() $this->assertEquals($dataValue, $persistedValue); } - public function testDeleteSimpleEntity() + /** + * Validate testDeleteSimpleEntity. + * + * @return void + * @throws TestReferenceException + */ + public function testDeleteSimpleEntity(): void { // Test Data and Variables - $entityName = "EntityOne"; - $entityStepKey = "StepKey"; - $dataKey = "testKey"; - $dataValue = "testValue"; + $entityName = 'EntityOne'; + $entityStepKey = 'StepKey'; + $dataKey = 'testKey'; + $dataValue = 'testValue'; $scope = PersistedObjectHandler::TEST_SCOPE; $parserOutput = [ 'entity' => [ @@ -120,15 +135,14 @@ public function testDeleteSimpleEntity() ] ] ]; - $jsonResponse = " + $jsonResponse = " { \"" . strtolower($dataKey) . "\" : \"{$dataValue}\" } "; // Mock Classes - $this->mockDataHandlerWithOutput($parserOutput); - $this->mockCurlHandler($jsonResponse); + $this->mockCurlHandler($jsonResponse, $parserOutput); $handler = PersistedObjectHandler::getInstance(); // Call method @@ -147,13 +161,19 @@ public function testDeleteSimpleEntity() $this->addToAssertionCount(1); } - public function testGetSimpleEntity() + /** + * Validate testGetSimpleEntity. + * + * @return void + * @throws Exception + */ + public function testGetSimpleEntity(): void { // Test Data and Variables - $entityName = "EntityOne"; - $entityStepKey = "StepKey"; - $dataKey = "testKey"; - $dataValue = "testValue"; + $entityName = 'EntityOne'; + $entityStepKey = 'StepKey'; + $dataKey = 'testKey'; + $dataValue = 'testValue'; $scope = PersistedObjectHandler::TEST_SCOPE; $parserOutput = [ 'entity' => [ @@ -168,15 +188,14 @@ public function testGetSimpleEntity() ] ] ]; - $jsonResponse = " + $jsonResponse = " { \"" . strtolower($dataKey) . "\" : \"{$dataValue}\" } "; // Mock Classes - $this->mockDataHandlerWithOutput($parserOutput); - $this->mockCurlHandler($jsonResponse); + $this->mockCurlHandler($jsonResponse, $parserOutput); $handler = PersistedObjectHandler::getInstance(); // Call method @@ -190,16 +209,22 @@ public function testGetSimpleEntity() $this->assertEquals($dataValue, $persistedValue); } - public function testUpdateSimpleEntity() + /** + * Validate testUpdateSimpleEntity. + * + * @return void + * @throws TestReferenceException + */ + public function testUpdateSimpleEntity(): void { - $this->markTestSkipped("Potential Bug in DataPersistenceHandler class"); + $this->markTestSkipped('Potential Bug in DataPersistenceHandler class'); // Test Data and Variables - $entityName = "EntityOne"; - $entityStepKey = "StepKey"; - $dataKey = "testKey"; - $dataValue = "testValue"; - $updateName = "EntityTwo"; - $updateValue = "newValue"; + $entityName = 'EntityOne'; + $entityStepKey = 'StepKey'; + $dataKey = 'testKey'; + $dataValue = 'testValue'; + $updateName = 'EntityTwo'; + $updateValue = 'newValue'; $scope = PersistedObjectHandler::TEST_SCOPE; $parserOutput = [ 'entity' => [ @@ -223,7 +248,7 @@ public function testUpdateSimpleEntity() ] ] ]; - $jsonResponse = " + $jsonResponse = " { \"" . strtolower($dataKey) . "\" : \"{$dataValue}\" } @@ -235,15 +260,14 @@ public function testUpdateSimpleEntity() "; // Mock Classes - $this->mockDataHandlerWithOutput($parserOutput); - $this->mockCurlHandler($jsonResponse); + $this->mockCurlHandler($jsonResponse, $parserOutput); $handler = PersistedObjectHandler::getInstance(); $handler->createEntity( $entityStepKey, $scope, $entityName ); - $this->mockCurlHandler($updatedResponse); + $this->mockCurlHandler($updatedResponse, $parserOutput); // Call method $handler->updateEntity( @@ -256,21 +280,27 @@ public function testUpdateSimpleEntity() $this->assertEquals($updateValue, $persistedValue); } - public function testRetrieveEntityAcrossScopes() + /** + * Validate testRetrieveEntityAcrossScopes. + * + * @return void + * @throws TestReferenceException + */ + public function testRetrieveEntityAcrossScopes(): void { // Test Data and Variables - $entityNameOne = "EntityOne"; - $entityStepKeyOne = "StepKeyOne"; - $dataKeyOne = "testKeyOne"; - $dataValueOne = "testValueOne"; - $entityNameTwo = "EntityTwo"; - $entityStepKeyTwo = "StepKeyTwo"; - $dataKeyTwo = "testKeyTwo"; - $dataValueTwo = "testValueTwo"; - $entityNameThree = "EntityThree"; - $entityStepKeyThree = "StepKeyThree"; - $dataKeyThree = "testKeyThree"; - $dataValueThree = "testValueThree"; + $entityNameOne = 'EntityOne'; + $entityStepKeyOne = 'StepKeyOne'; + $dataKeyOne = 'testKeyOne'; + $dataValueOne = 'testValueOne'; + $entityNameTwo = 'EntityTwo'; + $entityStepKeyTwo = 'StepKeyTwo'; + $dataKeyTwo = 'testKeyTwo'; + $dataValueTwo = 'testValueTwo'; + $entityNameThree = 'EntityThree'; + $entityStepKeyThree = 'StepKeyThree'; + $dataKeyThree = 'testKeyThree'; + $dataValueThree = 'testValueThree'; $parserOutputOne = [ 'entity' => [ @@ -303,7 +333,7 @@ public function testRetrieveEntityAcrossScopes() ] ] ]; - $jsonReponseOne = " + $jsonResponseOne = " { \"" . strtolower($dataKeyOne) . "\" : \"{$dataValueOne}\" } @@ -322,22 +352,21 @@ public function testRetrieveEntityAcrossScopes() // Mock Classes and Create Entities $handler = PersistedObjectHandler::getInstance(); - $this->mockDataHandlerWithOutput($parserOutputOne); - $this->mockCurlHandler($jsonReponseOne); + $this->mockCurlHandler($jsonResponseOne, $parserOutputOne); $handler->createEntity( $entityStepKeyOne, PersistedObjectHandler::TEST_SCOPE, $entityNameOne ); - $this->mockCurlHandler($jsonReponseTwo); + $this->mockCurlHandler($jsonReponseTwo, $parserOutputOne); $handler->createEntity( $entityStepKeyTwo, PersistedObjectHandler::HOOK_SCOPE, $entityNameTwo ); - $this->mockCurlHandler($jsonReponseThree); + $this->mockCurlHandler($jsonReponseThree, $parserOutputOne); $handler->createEntity( $entityStepKeyThree, PersistedObjectHandler::SUITE_SCOPE, @@ -367,6 +396,8 @@ public function testRetrieveEntityAcrossScopes() } /** + * Validate testRetrieveEntityValidField. + * * @param string $name * @param string $key * @param string $value @@ -374,9 +405,18 @@ public function testRetrieveEntityAcrossScopes() * @param string $scope * @param string $stepKey * @dataProvider entityDataProvider + * + * @return void + * @throws TestReferenceException */ - public function testRetrieveEntityValidField($name, $key, $value, $type, $scope, $stepKey) - { + public function testRetrieveEntityValidField( + string $name, + string $key, + string $value, + string $type, + string $scope, + string $stepKey + ): void { $parserOutputOne = [ 'entity' => [ $name => [ @@ -390,7 +430,7 @@ public function testRetrieveEntityValidField($name, $key, $value, $type, $scope, ] ] ]; - $jsonReponseOne = " + $jsonResponseOne = " { \"" . strtolower($key) . "\" : \"{$value}\" } @@ -398,9 +438,7 @@ public function testRetrieveEntityValidField($name, $key, $value, $type, $scope, // Mock Classes and Create Entities $handler = PersistedObjectHandler::getInstance(); - - $this->mockDataHandlerWithOutput($parserOutputOne); - $this->mockCurlHandler($jsonReponseOne); + $this->mockCurlHandler($jsonResponseOne, $parserOutputOne); $handler->createEntity($stepKey, $scope, $name); // Call method @@ -410,6 +448,8 @@ public function testRetrieveEntityValidField($name, $key, $value, $type, $scope, } /** + * Validate testRetrieveEntityInValidField. + * * @param string $name * @param string $key * @param string $value @@ -417,14 +457,21 @@ public function testRetrieveEntityValidField($name, $key, $value, $type, $scope, * @param string $scope * @param string $stepKey * @dataProvider entityDataProvider - * @throws TestReferenceException - * @throws TestFrameworkException + * + * @return void + * @throws TestReferenceException|TestFrameworkException */ - public function testRetrieveEntityInValidField($name, $key, $value, $type, $scope, $stepKey) - { - $invalidDataKey = "invalidDataKey"; + public function testRetrieveEntityInValidField( + string $name, + string $key, + string $value, + string $type, + string $scope, + string $stepKey + ): void { + $invalidDataKey = 'invalidDataKey'; $warnMsg = "Undefined field {$invalidDataKey} in entity object with a stepKey of {$stepKey}\n"; - $warnMsg .= "Please fix the invalid reference. This will result in fatal error in next major release."; + $warnMsg .= 'Please fix the invalid reference. This will result in fatal error in next major release.'; $parserOutputOne = [ 'entity' => [ @@ -439,7 +486,7 @@ public function testRetrieveEntityInValidField($name, $key, $value, $type, $scop ] ] ]; - $jsonReponseOne = " + $jsonResponseOne = " { \"" . strtolower($key) . "\" : \"{$value}\" } @@ -447,9 +494,7 @@ public function testRetrieveEntityInValidField($name, $key, $value, $type, $scop // Mock Classes and Create Entities $handler = PersistedObjectHandler::getInstance(); - - $this->mockDataHandlerWithOutput($parserOutputOne); - $this->mockCurlHandler($jsonReponseOne); + $this->mockCurlHandler($jsonResponseOne, $parserOutputOne); $handler->createEntity($stepKey, $scope, $name); // Call method @@ -464,9 +509,11 @@ public function testRetrieveEntityInValidField($name, $key, $value, $type, $scop } /** - * Data provider for testRetrieveEntityField + * Data provider for testRetrieveEntityField. + * + * @return array */ - public static function entityDataProvider() + public static function entityDataProvider(): array { return [ ['Entity1', 'testKey1', 'testValue1', 'testType', PersistedObjectHandler::HOOK_SCOPE, 'StepKey1'], @@ -476,57 +523,78 @@ public static function entityDataProvider() } /** - * Mocks DataObjectHandler to use given output to create - * @param $parserOutput - * @throws \Exception + * Create mock curl handler. + * + * @param string $response + * @param array $parserOutput + * + * @return void */ - public function mockDataHandlerWithOutput($parserOutput) + public function mockCurlHandler(string $response, array $parserOutput): void { - // Clear DataObjectHandler singleton if already set - $property = new \ReflectionProperty(DataObjectHandler::class, "INSTANCE"); - $property->setAccessible(true); - $property->setValue(null); - - $mockDataProfileSchemaParser = AspectMock::double(DataProfileSchemaParser::class, [ - 'readDataProfiles' => $parserOutput - ])->make(); - - $mockObjectManager = AspectMock::double(ObjectManager::class, [ - 'create' => $mockDataProfileSchemaParser - ])->make(); - - AspectMock::double(ObjectManagerFactory::class, [ - 'getObjectManager' => $mockObjectManager - ]); + $dataObjectHandler = new ReflectionProperty(DataObjectHandler::class, 'INSTANCE'); + $dataObjectHandler->setAccessible(true); + $dataObjectHandler->setValue(null, null); + + $dataProfileSchemaParser = $this->createMock(DataProfileSchemaParser::class); + $dataProfileSchemaParser + ->method('readDataProfiles') + ->willReturn($parserOutput); + + $curlHandler = $this->createMock(CurlHandler::class); + $curlHandler + ->method('executeRequest') + ->willReturn($response); + $curlHandler + ->method('getRequestDataArray') + ->willReturn([]); + $curlHandler + ->method('isContentTypeJson') + ->willReturn(true); + + $objectManager = ObjectManagerFactory::getObjectManager(); + $objectManagerMockInstance = $this->createMock(ObjectManager::class); + $objectManagerMockInstance->expects($this->any()) + ->method('create') + ->will( + $this->returnCallback( + function ($class, $arguments = []) use ($curlHandler, $objectManager, $dataProfileSchemaParser) { + if ($class === CurlHandler::class) { + return $curlHandler; + } + + if ($class === DataProfileSchemaParser::class) { + return $dataProfileSchemaParser; + } + + return $objectManager->create($class, $arguments); + } + ) + ); + + $objectManagerProperty = new ReflectionProperty(ObjectManager::class, 'instance'); + $objectManagerProperty->setAccessible(true); + $objectManagerProperty->setValue(null, $objectManagerMockInstance); } - public function mockCurlHandler($response) + /** + * After class functionality. + * + * @return void + */ + public static function tearDownAfterClass(): void { - AspectMock::double(CurlHandler::class, [ - "__construct" => null, - "executeRequest" => $response, - "getRequestDataArray" => [], - "isContentTypeJson" => true - ]); - } + parent::tearDownAfterClass(); - public function tearDown() - { // Clear out Singleton between tests - $property = new \ReflectionProperty(PersistedObjectHandler::class, "INSTANCE"); - $property->setAccessible(true); - $property->setValue(null); + $persistedObjectHandlerProperty = new ReflectionProperty(PersistedObjectHandler::class, "INSTANCE"); + $persistedObjectHandlerProperty->setAccessible(true); + $persistedObjectHandlerProperty->setValue(null, null); - parent::tearDown(); // TODO: Change the autogenerated stub - } + $objectManagerProperty = new ReflectionProperty(ObjectManager::class, 'instance'); + $objectManagerProperty->setAccessible(true); + $objectManagerProperty->setValue(null, null); - /** - * After class functionality - * @return void - */ - public static function tearDownAfterClass() - { TestLoggingUtil::getInstance()->clearMockLoggingUtil(); - parent::tearDownAfterClass(); } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/SecretStorage/AwsSecretsManagerStorageTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/SecretStorage/AwsSecretsManagerStorageTest.php index e1f4e4879..b37e22bd0 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/SecretStorage/AwsSecretsManagerStorageTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/SecretStorage/AwsSecretsManagerStorageTest.php @@ -9,7 +9,7 @@ use Aws\SecretsManager\SecretsManagerClient; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\SecretStorage\AwsSecretsManagerStorage; use Aws\Result; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use tests\unit\Util\MagentoTestCase; use ReflectionClass; class AwsSecretsManagerStorageTest extends MagentoTestCase @@ -34,7 +34,7 @@ public function testEncryptAndDecrypt() $mockClient = $this->getMockBuilder(SecretsManagerClient::class) ->disableOriginalConstructor() - ->setMethods(['__call']) + ->onlyMethods(['__call']) ->getMock(); $mockClient->expects($this->once()) diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/SecretStorage/FileStorageTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/SecretStorage/FileStorageTest.php index 7e5824c8e..3d5922f9e 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/SecretStorage/FileStorageTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Handlers/SecretStorage/FileStorageTest.php @@ -3,29 +3,41 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace tests\unit\Magento\FunctionalTestFramework\DataGenerator\Handlers\SecretStorage; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\SecretStorage\FileStorage; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; -use AspectMock\Test as AspectMock; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use ReflectionClass; +use ReflectionException; +use tests\unit\Util\MagentoTestCase; class FileStorageTest extends MagentoTestCase { - /** * Test basic encryption/decryption functionality in FileStorage class. + * @throws TestFrameworkException|ReflectionException */ - public function testBasicEncryptDecrypt() + public function testBasicEncryptDecrypt(): void { $testKey = 'magento/myKey'; $testValue = 'myValue'; - - AspectMock::double(FileStorage::class, [ - 'readInCredentialsFile' => ["$testKey=$testValue"] - ]); + $cred = ["$testKey=$testValue"]; $fileStorage = new FileStorage(); + $reflection = new ReflectionClass(FileStorage::class); + + // Emulate initialize() function result with the test credentials + $reflectionMethod = $reflection->getMethod('encryptCredFileContents'); + $reflectionMethod->setAccessible(true); + $secretData = $reflectionMethod->invokeArgs($fileStorage, [$cred]); + + // Set encrypted test credentials to the private 'secretData' property + $reflectionProperty = $reflection->getProperty('secretData'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($fileStorage, $secretData); + $encryptedCred = $fileStorage->getEncryptedValue($testKey); // assert the value we've gotten is in fact not identical to our test value @@ -36,4 +48,32 @@ public function testBasicEncryptDecrypt() // assert that we are able to successfully decrypt our secret value $this->assertEquals($testValue, $actualValue); } + + /** + * Test empty value encryption/decryption functionality in FileStorage class. + * @return void + * @throws TestFrameworkException|ReflectionException + */ + public function testEmptyValueEncryptDecrypt(): void + { + $this->expectException(TestFrameworkException::class); + + $testKey = 'magento/myKey'; + $cred = ["$testKey"]; + + $fileStorage = new FileStorage(); + $reflection = new ReflectionClass(FileStorage::class); + + // Emulate initialize() function result with the test credentials + $reflectionMethod = $reflection->getMethod('encryptCredFileContents'); + $reflectionMethod->setAccessible(true); + $secretData = $reflectionMethod->invokeArgs($fileStorage, [$cred]); + + // Set encrypted test credentials to the private 'secretData' property + $reflectionProperty = $reflection->getProperty('secretData'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($fileStorage, $secretData); + + $fileStorage->getEncryptedValue($testKey); + } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Objects/EntityDataObjectTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Objects/EntityDataObjectTest.php index 802a6c108..20c16428c 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Objects/EntityDataObjectTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Objects/EntityDataObjectTest.php @@ -6,7 +6,7 @@ namespace Magento\FunctionalTestingFramework\DataGenerator\Objects; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use tests\unit\Util\MagentoTestCase; use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; use tests\unit\Util\TestLoggingUtil; @@ -20,12 +20,12 @@ function function_exists($val) return true; } -function msq($id = null) +function msq(?string $id = null) { return "msqUnique"; } -function msqs($id = null) +function msqs(?string $id = null) { return "msqsUnique"; } @@ -40,7 +40,7 @@ class EntityDataObjectTest extends MagentoTestCase * Before test functionality * @return void */ - public function setUp() + public function setUp(): void { TestLoggingUtil::getInstance()->setMockLoggingUtil(); } @@ -141,7 +141,7 @@ public function testGetCamelCaseKeys() * After class functionality * @return void */ - public static function tearDownAfterClass() + public static function tearDownAfterClass(): void { TestLoggingUtil::getInstance()->clearMockLoggingUtil(); } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Persist/OperationDataArrayResolverTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Persist/OperationDataArrayResolverTest.php index fb6dcd865..b4152901e 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Persist/OperationDataArrayResolverTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Persist/OperationDataArrayResolverTest.php @@ -3,14 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace tests\unit\Magento\FunctionalTestFramework\DataGenerator\Persist; -use AspectMock\Test as AspectMock; +use Exception; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\OperationDefinitionObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Persist\OperationDataArrayResolver; -use Magento\FunctionalTestingFramework\Util\Iterator\AbstractIterator; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use ReflectionProperty; +use tests\unit\Util\MagentoTestCase; use tests\unit\Util\EntityDataObjectBuilder; use tests\unit\Util\OperationDefinitionBuilder; use tests\unit\Util\OperationElementBuilder; @@ -18,30 +20,31 @@ class OperationDataArrayResolverTest extends MagentoTestCase { - const NESTED_METADATA_EXPECTED_RESULT = ["parentType" => [ - "name" => "Hopper", - "address" => ["city" => "Hawkins", "state" => "Indiana", "zip" => 78758], - "isPrimary" => true, - "gpa" => 3.5678, - "phone" => 5555555 + const NESTED_METADATA_EXPECTED_RESULT = ['parentType' => [ + 'name' => 'Hopper', + 'address' => ['city' => 'Hawkins', 'state' => 'Indiana', 'zip' => 78758], + 'isPrimary' => true, + 'gpa' => 3.5678, + 'phone' => 5555555 ]]; - const NESTED_METADATA_ARRAY_RESULT = ["parentType" => [ - "name" => "Hopper", - "isPrimary" => true, - "gpa" => 3.5678, - "phone" => 5555555, - "address" => [ - ["city" => "Hawkins", "state" => "Indiana", "zip" => 78758], - ["city" => "Austin", "state" => "Texas", "zip" => 78701], + const NESTED_METADATA_ARRAY_RESULT = ['parentType' => [ + 'name' => 'Hopper', + 'isPrimary' => true, + 'gpa' => 3.5678, + 'phone' => 5555555, + 'address' => [ + ['city' => 'Hawkins', 'state' => 'Indiana', 'zip' => 78758], + ['city' => 'Austin', 'state' => 'Texas', 'zip' => 78701], ] ]]; /** * Before test functionality + * * @return void */ - public function setUp() + public function setUp(): void { TestLoggingUtil::getInstance()->setMockLoggingUtil(); } @@ -54,8 +57,11 @@ public function setUp() * boolField * doubleField * + * + * @return void + * @throws Exception */ - public function testBasicPrimitiveMetadataResolve() + public function testBasicPrimitiveMetadataResolve(): void { // set up data object $entityObjectBuilder = new EntityDataObjectBuilder(); @@ -74,11 +80,11 @@ public function testBasicPrimitiveMetadataResolve() ); // assert on result - $expectedResult = ["testType" => [ - "name" => "Hopper", - "gpa" => 3.5678, - "phone" => 5555555, - "isPrimary" => true + $expectedResult = ['testType' => [ + 'name' => 'Hopper', + 'gpa' => 3.5678, + 'phone' => 5555555, + 'isPrimary' => true ]]; $this->assertEquals($expectedResult, $result); @@ -90,56 +96,54 @@ public function testBasicPrimitiveMetadataResolve() * someField * objectRef * + * + * @return void + * @throws Exception */ - public function testNestedMetadataResolve() + public function testNestedMetadataResolve(): void { // set up data objects $entityDataObjBuilder = new EntityDataObjectBuilder(); $parentDataObject = $entityDataObjBuilder - ->withName("parentObject") - ->withType("parentType") + ->withName('parentObject') + ->withType('parentType') ->withLinkedEntities(['childObject' => 'childType']) ->build(); $childDataObject = $entityDataObjBuilder - ->withName("childObject") - ->withType("childType") - ->withDataFields(["city" => "Hawkins", "state" => "Indiana", "zip" => "78758"]) + ->withName('childObject') + ->withType('childType') + ->withDataFields(['city' => 'Hawkins', 'state' => 'Indiana', 'zip' => '78758']) ->build(); // mock data object handler - $mockDOHInstance = AspectMock::double(DataObjectHandler::class, ['getObject' => $childDataObject])->make(); - AspectMock::double(DataObjectHandler::class, ['getInstance' => $mockDOHInstance]); + $this->mockDataObjectHandler($childDataObject); // set up metadata objects $parentOpElementBuilder = new OperationElementBuilder(); $parentElement = $parentOpElementBuilder - ->withKey("parentType") - ->withType("parentType") - ->addFields(["address" => "childType"]) + ->withKey('parentType') + ->withType('parentType') + ->addFields(['address' => 'childType']) ->build(); $operationDefinitionBuilder = new OperationDefinitionBuilder(); $childOperationDefinition = $operationDefinitionBuilder - ->withName("createChildType") - ->withOperation("create") - ->withType("childType") + ->withName('createChildType') + ->withOperation('create') + ->withType('childType') ->withMetadata([ - "city" => "string", - "state" => "string", - "zip" => "integer" + 'city' => 'string', + 'state' => 'string', + 'zip' => 'integer' ])->build(); // mock meta data object handler - $mockDOHInstance = AspectMock::double( - OperationDefinitionObjectHandler::class, - ['getObject' => $childOperationDefinition] - )->make(); - AspectMock::double(OperationDefinitionObjectHandler::class, ['getInstance' => $mockDOHInstance]); + $this->mockOperationDefinitionObjectHandler($childOperationDefinition); // resolve data object and metadata array $operationResolver = new OperationDataArrayResolver(); - $result = $operationResolver->resolveOperationDataArray($parentDataObject, [$parentElement], "create", false); + $result = $operationResolver->resolveOperationDataArray($parentDataObject, [$parentElement], 'create', false); // assert on the result $this->assertEquals(self::NESTED_METADATA_EXPECTED_RESULT, $result); @@ -153,45 +157,47 @@ public function testNestedMetadataResolve() * anotherField * * + * + * @return void + * @throws Exception */ - public function testNestedMetadata() + public function testNestedMetadata(): void { // set up data objects $entityDataObjectBuilder = new EntityDataObjectBuilder(); $parentDataObject = $entityDataObjectBuilder - ->withName("parentObject") - ->withType("parentType") + ->withName('parentObject') + ->withType('parentType') ->withLinkedEntities(['childObject' => 'childType']) ->build(); $childDataObject = $entityDataObjectBuilder - ->withName("childObject") - ->withType("childType") - ->withDataFields(["city" => "Hawkins", "state" => "Indiana", "zip" => "78758"]) + ->withName('childObject') + ->withType('childType') + ->withDataFields(['city' => 'Hawkins', 'state' => 'Indiana', 'zip' => '78758']) ->build(); // mock data object handler - $mockDOHInstance = AspectMock::double(DataObjectHandler::class, ['getObject' => $childDataObject])->make(); - AspectMock::double(DataObjectHandler::class, ['getInstance' => $mockDOHInstance]); + $this->mockDataObjectHandler($childDataObject); // set up metadata objects $childOpElementBuilder = new OperationElementBuilder(); $childElement = $childOpElementBuilder - ->withKey("address") - ->withType("childType") - ->withFields(["city" => "string", "state" => "string", "zip" => "integer"]) + ->withKey('address') + ->withType('childType') + ->withFields(['city' => 'string', 'state' => 'string', 'zip' => 'integer']) ->build(); $parentOpElementBuilder = new OperationElementBuilder(); $parentElement = $parentOpElementBuilder - ->withKey("parentType") - ->withType("parentType") - ->addElements(["address" => $childElement]) + ->withKey('parentType') + ->withType('parentType') + ->addElements(['address' => $childElement]) ->build(); // resolve data object and metadata array $operationResolver = new OperationDataArrayResolver(); - $result = $operationResolver->resolveOperationDataArray($parentDataObject, [$parentElement], "create", false); + $result = $operationResolver->resolveOperationDataArray($parentDataObject, [$parentElement], 'create', false); // assert on the result $this->assertEquals(self::NESTED_METADATA_EXPECTED_RESULT, $result); @@ -207,66 +213,69 @@ public function testNestedMetadata() * * + * + * @return void + * @throws Exception */ - public function testNestedMetadataArrayOfObjects() + public function testNestedMetadataArrayOfObjects(): void { // set up data objects $entityDataObjectBuilder = new EntityDataObjectBuilder(); $parentDataObject = $entityDataObjectBuilder - ->withName("parentObject") - ->withType("parentType") + ->withName('parentObject') + ->withType('parentType') ->withLinkedEntities(['childObject1' => 'childType', 'childObject2' => 'childType']) ->build(); // mock data object handler - $mockDOHInstance = AspectMock::double(DataObjectHandler::class, ["getObject" => function ($name) { + $callback = function ($name) { $entityDataObjectBuilder = new EntityDataObjectBuilder(); - if ($name == "childObject1") { + if ($name === 'childObject1') { return $entityDataObjectBuilder - ->withName("childObject1") - ->withType("childType") - ->withDataFields(["city" => "Hawkins", "state" => "Indiana", "zip" => "78758"]) + ->withName('childObject1') + ->withType('childType') + ->withDataFields(['city' => 'Hawkins', 'state' => 'Indiana', 'zip' => '78758']) ->build(); } - if ($name == "childObject2") { + if ($name === 'childObject2') { return $entityDataObjectBuilder - ->withName("childObject2") - ->withType("childType") - ->withDataFields(["city" => "Austin", "state" => "Texas", "zip" => "78701"]) + ->withName('childObject2') + ->withType('childType') + ->withDataFields(['city' => 'Austin', 'state' => 'Texas', 'zip' => '78701']) ->build(); } - }])->make(); - AspectMock::double(DataObjectHandler::class, ['getInstance' => $mockDOHInstance]); + }; + $this->mockDataObjectHandler($callback); // set up metadata objects $childOpElementBuilder = new OperationElementBuilder(); $childElement = $childOpElementBuilder - ->withKey("childType") - ->withType("childType") - ->withFields(["city" => "string", "state" => "string", "zip" => "integer"]) + ->withKey('childType') + ->withType('childType') + ->withFields(['city' => 'string', 'state' => 'string', 'zip' => 'integer']) ->build(); $arrayOpElementBuilder = new OperationElementBuilder(); $arrayElement = $arrayOpElementBuilder - ->withKey("address") - ->withType("childType") + ->withKey('address') + ->withType('childType') ->withFields([]) ->withElementType(OperationDefinitionObjectHandler::ENTITY_OPERATION_ARRAY) - ->withNestedElements(["childType" => $childElement]) + ->withNestedElements(['childType' => $childElement]) ->build(); $parentOpElementBuilder = new OperationElementBuilder(); $parentElement = $parentOpElementBuilder - ->withKey("parentType") - ->withType("parentType") - ->addElements(["address" => $arrayElement]) + ->withKey('parentType') + ->withType('parentType') + ->addElements(['address' => $arrayElement]) ->build(); // resolve data object and metadata array $operationResolver = new OperationDataArrayResolver(); - $result = $operationResolver->resolveOperationDataArray($parentDataObject, [$parentElement], "create", false); + $result = $operationResolver->resolveOperationDataArray($parentDataObject, [$parentElement], 'create', false); // Do assert on result here $this->assertEquals(self::NESTED_METADATA_ARRAY_RESULT, $result); @@ -280,44 +289,47 @@ public function testNestedMetadataArrayOfObjects() * object * + * + * @return void + * @throws Exception */ - public function testNestedMetadataArrayOfValue() + public function testNestedMetadataArrayOfValue(): void { // set up data objects $entityDataObjectBuilder = new EntityDataObjectBuilder(); $parentDataObject = $entityDataObjectBuilder - ->withName("parentObject") - ->withType("parentType") + ->withName('parentObject') + ->withType('parentType') ->withLinkedEntities(['childObject1' => 'childType', 'childObject2' => 'childType']) ->build(); - // mock data object handler - $mockDOHInstance = AspectMock::double(DataObjectHandler::class, ["getObject" => function ($name) { + $callback = function ($name) { $entityDataObjectBuilder = new EntityDataObjectBuilder(); - if ($name == "childObject1") { + if ($name === 'childObject1') { return $entityDataObjectBuilder - ->withName("childObject1") - ->withType("childType") - ->withDataFields(["city" => "Hawkins", "state" => "Indiana", "zip" => "78758"]) + ->withName('childObject1') + ->withType('childType') + ->withDataFields(['city' => 'Hawkins', 'state' => 'Indiana', 'zip' => '78758']) ->build(); }; - if ($name == "childObject2") { + if ($name === 'childObject2') { return $entityDataObjectBuilder - ->withName("childObject2") - ->withType("childType") - ->withDataFields(["city" => "Austin", "state" => "Texas", "zip" => "78701"]) + ->withName('childObject2') + ->withType('childType') + ->withDataFields(['city' => 'Austin', 'state' => 'Texas', 'zip' => '78701']) ->build(); } - }])->make(); - AspectMock::double(DataObjectHandler::class, ['getInstance' => $mockDOHInstance]); + }; + // mock data object handler + $this->mockDataObjectHandler($callback); // set up metadata objects $arrayOpElementBuilder = new OperationElementBuilder(); $arrayElement = $arrayOpElementBuilder - ->withKey("address") - ->withType("childType") + ->withKey('address') + ->withType('childType') ->withElementType(OperationDefinitionObjectHandler::ENTITY_OPERATION_ARRAY) ->withNestedElements([]) ->withFields([]) @@ -325,44 +337,39 @@ public function testNestedMetadataArrayOfValue() $parentOpElementBuilder = new OperationElementBuilder(); $parentElement = $parentOpElementBuilder - ->withKey("parentType") - ->withType("parentType") - ->addElements(["address" => $arrayElement]) + ->withKey('parentType') + ->withType('parentType') + ->addElements(['address' => $arrayElement]) ->build(); $operationDefinitionBuilder = new OperationDefinitionBuilder(); $childOperationDefinition = $operationDefinitionBuilder - ->withName("createChildType") - ->withOperation("create") - ->withType("childType") + ->withName('createChildType') + ->withOperation('create') + ->withType('childType') ->withMetadata([ - "city" => "string", - "state" => "string", - "zip" => "integer" + 'city' => 'string', + 'state' => 'string', + 'zip' => 'integer' ])->build(); // mock meta data object handler - $mockDOHInstance = AspectMock::double( - OperationDefinitionObjectHandler::class, - ['getObject' => $childOperationDefinition] - )->make(); - AspectMock::double(OperationDefinitionObjectHandler::class, ['getInstance' => $mockDOHInstance]); + $this->mockOperationDefinitionObjectHandler($childOperationDefinition); // resolve data object and metadata array $operationResolver = new OperationDataArrayResolver(); - $result = $operationResolver->resolveOperationDataArray($parentDataObject, [$parentElement], "create", false); + $result = $operationResolver->resolveOperationDataArray($parentDataObject, [$parentElement], 'create', false); // Do assert on result here $this->assertEquals(self::NESTED_METADATA_ARRAY_RESULT, $result); } - public function testNestedMetadataArrayOfDiverseObjects() + public function testNestedMetadataArrayOfDiverseObjects(): void { - $entityDataObjBuilder = new EntityDataObjectBuilder(); $parentDataObject = $entityDataObjBuilder - ->withName("parentObject") - ->withType("parentType") + ->withName('parentObject') + ->withType('parentType') ->withLinkedEntities(['child1Object' => 'childType1','child2Object' => 'childType2']) ->build(); @@ -378,22 +385,15 @@ public function testNestedMetadataArrayOfDiverseObjects() ->withDataFields(['city' => 'Testcity 2','zip' => 54321,'state' => 'Teststate']) ->build(); - $mockDOHInstance = AspectMock::double( - DataObjectHandler::class, - [ - 'getObject' => function ($name) use ($child1DataObject, $child2DataObject) { - switch ($name) { - case 'child1Object': - return $child1DataObject; - case 'child2Object': - return $child2DataObject; - } - } - ] - )->make(); - AspectMock::double(DataObjectHandler::class, [ - 'getInstance' => $mockDOHInstance - ]); + $dataObjectCallback = function ($name) use ($child1DataObject, $child2DataObject) { + switch ($name) { + case 'child1Object': + return $child1DataObject; + case 'child2Object': + return $child2DataObject; + } + }; + $this->mockDataObjectHandler($dataObjectCallback); $operationDefinitionBuilder = new OperationDefinitionBuilder(); $child1OperationDefinition = $operationDefinitionBuilder @@ -415,25 +415,15 @@ public function testNestedMetadataArrayOfDiverseObjects() 'state' => 'string' ])->build(); - $mockODOHInstance = AspectMock::double( - OperationDefinitionObjectHandler::class, - [ - 'getObject' => function ($name) use ($child1OperationDefinition, $child2OperationDefinition) { - switch ($name) { - case 'createchildType1': - return $child1OperationDefinition; - case 'createchildType2': - return $child2OperationDefinition; - } - } - ] - )->make(); - AspectMock::double( - OperationDefinitionObjectHandler::class, - [ - 'getInstance' => $mockODOHInstance - ] - ); + $operationObjectCallback = function ($name) use ($child1OperationDefinition, $child2OperationDefinition) { + switch ($name) { + case 'createchildType1': + return $child1OperationDefinition; + case 'createchildType2': + return $child2OperationDefinition; + } + }; + $this->mockOperationDefinitionObjectHandler($operationObjectCallback); $arrayObElementBuilder = new OperationElementBuilder(); $arrayElement = $arrayObElementBuilder @@ -477,12 +467,117 @@ public function testNestedMetadataArrayOfDiverseObjects() $this->assertEquals($expectedResult, $result); } + public function testExtendedWithRequiredEntity(): void + { + $entityDataObjectBuilder = new EntityDataObjectBuilder(); + $extEntityDataObject = $entityDataObjectBuilder + ->withName('extEntity') + ->withType('entity') + ->withLinkedEntities(['baseSubentity' => 'subentity','extSubentity' => 'subentity']) + ->build(); + + $callback = function ($name) { + $entityDataObjectBuilder = new EntityDataObjectBuilder(); + + if ($name === 'baseSubentity') { + return $entityDataObjectBuilder + ->withName('baseSubentity') + ->withType('subentity') + ->withDataFields(['subtest' => 'BaseSubtest']) + ->build(); + } + + if ($name === 'extSubentity') { + return $entityDataObjectBuilder + ->withName('extSubentity') + ->withType('subentity') + ->withDataFields(['subtest' => 'ExtSubtest']) + ->build(); + } + }; + $this->mockDataObjectHandler($callback); + + $subentityOpElementBuilder = new OperationElementBuilder(); + $subentityOpElement = $subentityOpElementBuilder + ->withKey('sub') + ->withType('subentity') + ->withElementType('object') + ->withFields(['subtest' => 'string']) + ->build(); + + $operationResolver = new OperationDataArrayResolver(); + $result = $operationResolver->resolveOperationDataArray( + $extEntityDataObject, + [$subentityOpElement], + 'create', + false + ); + + $expected = [ + 'sub' => [ + 'subtest' => 'ExtSubtest' + ] + ]; + + $this->assertEquals($expected, $result); + } /** * After class functionality + * * @return void */ - public static function tearDownAfterClass() + public static function tearDownAfterClass(): void { TestLoggingUtil::getInstance()->clearMockLoggingUtil(); } + + /** + * Set up mock DataObjectHandler + * + * @param $childDataObject + * + * @return void + */ + private function mockDataObjectHandler($childDataObject): void + { + $instance = $this->createMock(DataObjectHandler::class); + if (is_callable($childDataObject)) { + $instance->expects($this->any()) + ->method('getObject') + ->willReturnCallback($childDataObject); + } else { + $instance->expects($this->any()) + ->method('getObject') + ->willReturn($childDataObject); + } + + $property = new ReflectionProperty(DataObjectHandler::class, 'INSTANCE'); + $property->setAccessible(true); + $property->setValue(null, $instance); + } + + /** + * Set up mock OperationDefinitionObjectHandler + * + * @param $childOperationDefinition + * + * @return void + */ + private function mockOperationDefinitionObjectHandler($childOperationDefinition): void + { + $instance = $this->createPartialMock(OperationDefinitionObjectHandler::class, ['getObject']); + if (is_callable($childOperationDefinition)) { + $instance->expects($this->any()) + ->method('getObject') + ->willReturnCallback($childOperationDefinition); + } else { + $instance->expects($this->any()) + ->method('getObject') + ->willReturn($childOperationDefinition); + } + + $property = new ReflectionProperty(OperationDefinitionObjectHandler::class, 'INSTANCE'); + $property->setAccessible(true); + $property->setValue(null, $instance); + } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Util/DataExtensionUtilTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Util/DataExtensionUtilTest.php index e72c15b31..529af6e44 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Util/DataExtensionUtilTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/DataGenerator/Util/DataExtensionUtilTest.php @@ -3,37 +3,28 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); -namespace Magento\FunctionalTestingFramework\DataGenerator\Objects; +namespace tests\unit\Magento\FunctionalTestFramework\DataGenerator\Util; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Parsers\DataProfileSchemaParser; -use Magento\FunctionalTestingFramework\ObjectManager\ObjectManager; -use Magento\FunctionalTestingFramework\ObjectManagerFactory; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; -use AspectMock\Test as AspectMock; +use Magento\FunctionalTestingFramework\ObjectManager; +use ReflectionProperty; +use tests\unit\Util\MagentoTestCase; /** - * Class EntityDataObjectTest + * Class DataExtensionUtilTest */ -class EntityDataExtensionTest extends MagentoTestCase +class DataExtensionUtilTest extends MagentoTestCase { - /** - * Before method functionality - * @return void - */ - protected function setUp() - { - AspectMock::clean(); - } - - public function testNoParentData() + public function testNoParentData(): void { $extendedDataObject = [ 'entity' => [ 'extended' => [ 'type' => 'testType', - 'extends' => "parent", + 'extends' => 'parent', 'data' => [ 0 => [ 'key' => 'testKey', @@ -46,21 +37,21 @@ public function testNoParentData() $this->setMockEntities($extendedDataObject); - $this->expectExceptionMessage("Parent Entity parent not defined for Entity extended."); - DataObjectHandler::getInstance()->getObject("extended"); + $this->expectExceptionMessage('Parent Entity parent not defined for Entity extended.'); + DataObjectHandler::getInstance()->getObject('extended'); } - public function testAlreadyExtendedParentData() + public function testAlreadyExtendedParentData(): void { $extendedDataObjects = [ 'entity' => [ 'extended' => [ 'type' => 'testType', - 'extends' => "parent" + 'extends' => 'parent' ], 'parent' => [ 'type' => 'type', - 'extends' => "grandparent" + 'extends' => 'grandparent' ], 'grandparent' => [ 'type' => 'grand' @@ -71,18 +62,18 @@ public function testAlreadyExtendedParentData() $this->setMockEntities($extendedDataObjects); $this->expectExceptionMessage( - "Cannot extend an entity that already extends another entity. Entity: parent." . PHP_EOL + 'Cannot extend an entity that already extends another entity. Entity: parent.' . PHP_EOL ); - DataObjectHandler::getInstance()->getObject("extended"); + DataObjectHandler::getInstance()->getObject('extended'); } - public function testExtendedVarGetter() + public function testExtendedVarGetter(): void { $extendedDataObjects = [ 'entity' => [ 'extended' => [ 'type' => 'testType', - 'extends' => "parent" + 'extends' => 'parent' ], 'parent' => [ 'type' => 'type', @@ -98,18 +89,18 @@ public function testExtendedVarGetter() ]; $this->setMockEntities($extendedDataObjects); - $resultextendedDataObject = DataObjectHandler::getInstance()->getObject("extended"); + $resultextendedDataObject = DataObjectHandler::getInstance()->getObject('extended'); // Perform Asserts - $this->assertEquals("someOtherEntity->id", $resultextendedDataObject->getVarReference("someOtherEntity")); + $this->assertEquals('someOtherEntity->id', $resultextendedDataObject->getVarReference('someOtherEntity')); } - public function testGetLinkedEntities() + public function testGetLinkedEntities(): void { $extendedDataObjects = [ 'entity' => [ 'extended' => [ 'type' => 'testType', - 'extends' => "parent" + 'extends' => 'parent' ], 'parent' => [ 'type' => 'type', @@ -129,27 +120,36 @@ public function testGetLinkedEntities() $this->setMockEntities($extendedDataObjects); // Perform Asserts - $resultextendedDataObject = DataObjectHandler::getInstance()->getObject("extended"); - $this->assertEquals("linkedEntity1", $resultextendedDataObject->getLinkedEntitiesOfType("linkedEntityType")[0]); - $this->assertEquals("linkedEntity2", $resultextendedDataObject->getLinkedEntitiesOfType("otherEntityType")[0]); + $resultextendedDataObject = DataObjectHandler::getInstance()->getObject('extended'); + $this->assertEquals('linkedEntity1', $resultextendedDataObject->getLinkedEntitiesOfType('linkedEntityType')[0]); + $this->assertEquals('linkedEntity2', $resultextendedDataObject->getLinkedEntitiesOfType('otherEntityType')[0]); } - private function setMockEntities($mockEntityData) + /** + * Prepare mock entites. + * + * @param $mockEntityData + * + * @return void + */ + private function setMockEntities($mockEntityData): void { - $property = new \ReflectionProperty(DataObjectHandler::class, 'INSTANCE'); + $property = new ReflectionProperty(DataObjectHandler::class, 'INSTANCE'); $property->setAccessible(true); - $property->setValue(null); + $property->setValue(null, null); - $mockDataProfileSchemaParser = AspectMock::double(DataProfileSchemaParser::class, [ - 'readDataProfiles' => $mockEntityData - ])->make(); + $mockDataProfileSchemaParser = $this->createMock(DataProfileSchemaParser::class); + $mockDataProfileSchemaParser->expects($this->any()) + ->method('readDataProfiles') + ->willReturn($mockEntityData); - $mockObjectManager = AspectMock::double(ObjectManager::class, [ - 'create' => $mockDataProfileSchemaParser - ])->make(); + $mockObjectManager = $this->createMock(ObjectManager::class); + $mockObjectManager + ->method('create') + ->willReturn($mockDataProfileSchemaParser); - AspectMock::double(ObjectManagerFactory::class, [ - 'getObjectManager' => $mockObjectManager - ]); + $property = new ReflectionProperty(ObjectManager::class, 'instance'); + $property->setAccessible(true); + $property->setValue(null, $mockObjectManager); } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Extension/BrowserLogUtilTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Extension/BrowserLogUtilTest.php index 32cac698e..45d14ae99 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Extension/BrowserLogUtilTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Extension/BrowserLogUtilTest.php @@ -6,7 +6,7 @@ namespace tests\unit\Magento\FunctionalTestFramework\Extension; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use tests\unit\Util\MagentoTestCase; use Magento\FunctionalTestingFramework\Extension\BrowserLogUtil; class BrowserLogUtilTest extends MagentoTestCase diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Module/Util/ModuleUtilTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Module/Util/ModuleUtilTest.php new file mode 100644 index 000000000..6c2fdf322 --- /dev/null +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Module/Util/ModuleUtilTest.php @@ -0,0 +1,48 @@ +assertStringContainsString($output, $util->utf8SafeControlCharacterTrim($input)); + $this->assertStringNotContainsString($removed, $util->utf8SafeControlCharacterTrim($input)); + } + + /** + * Data input. + * + * @return array + */ + public static function inDataProvider(): array + { + $ctr1 = '‹'; + $ctr2 = 'Œ'; + $ctr3 = 'Œ ‹'; + return [ + ["some text $ctr1", 'some text', $ctr1], + ["some text $ctr2", 'some text', $ctr2], + ["some text $ctr3", 'some text', $ctr3] + ]; + } +} diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Page/Handlers/PageObjectHandlerTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Page/Handlers/PageObjectHandlerTest.php index 28ffcd9e1..1f84f4b14 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Page/Handlers/PageObjectHandlerTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Page/Handlers/PageObjectHandlerTest.php @@ -3,87 +3,186 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace tests\unit\Magento\FunctionalTestFramework\Page\Handlers; -use AspectMock\Test as AspectMock; +use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\ObjectManager; use Magento\FunctionalTestingFramework\ObjectManagerFactory; use Magento\FunctionalTestingFramework\Page\Handlers\PageObjectHandler; use Magento\FunctionalTestingFramework\XmlParser\PageParser; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use ReflectionProperty; +use tests\unit\Util\MagentoTestCase; +use tests\unit\Util\TestLoggingUtil; +/** + * Class PageObjectHandlerTest + */ class PageObjectHandlerTest extends MagentoTestCase { - public function testGetPageObject() + /** + * @inheritDoc + */ + protected function setUp(): void + { + TestLoggingUtil::getInstance()->setMockLoggingUtil(); + } + + /** + * Validate testGetPageObject. + * + * @return void + * @throws XmlException + */ + public function testGetPageObject(): void { $mockData = [ - "testPage1" => [ - "url" => "testURL1", - "module" => "testModule1", - "section" => [ - "someSection1" => [], - "someSection2" => [] + 'testPage1' => [ + 'url' => 'testURL1', + 'module' => 'testModule1', + 'section' => [ + 'someSection1' => [], + 'someSection2' => [] ], - "area" => "test" + 'area' => 'test' ], - "testPage2" => [ - "url" => "testURL2", - "module" => "testModule2", - "parameterized" => true, - "section" => [ - "someSection1" => [] + 'testPage2' => [ + 'url' => 'testURL2', + 'module' => 'testModule2', + 'parameterized' => true, + 'section' => [ + 'someSection1' => [] ], - "area" => "test" + 'area' => 'test' ]]; - $this->setMockParserOutput($mockData); - // get pages + $this->mockPageObjectHandlerWithData($mockData); $pageHandler = PageObjectHandler::getInstance(); $pages = $pageHandler->getAllObjects(); - $page = $pageHandler->getObject('testPage1'); + $pageHandler->getObject('testPage1'); $invalidPage = $pageHandler->getObject('someInvalidPage'); // perform asserts $this->assertCount(2, $pages); - $this->assertArrayHasKey("testPage1", $pages); - $this->assertArrayHasKey("testPage2", $pages); + $this->assertArrayHasKey('testPage1', $pages); + $this->assertArrayHasKey('testPage2', $pages); $this->assertNull($invalidPage); } - public function testGetEmptyPage() + /** + * Validate testGetEmptyPage. + * + * @return void + * @throws XmlException + */ + public function testGetEmptyPage(): void { $mockData = [ - "testPage1" => [ - "url" => "testURL1", - "module" => "testModule1", - "section" => [ + 'testPage1' => [ + 'url' => 'testURL1', + 'module' => 'testModule1', + 'section' => [ ], - "area" => "test" + 'area' => 'test' ]]; - $this->setMockParserOutput($mockData); - // get pages - $page = PageObjectHandler::getInstance()->getObject('testPage1'); + $this->mockPageObjectHandlerWithData($mockData); + PageObjectHandler::getInstance()->getObject('testPage1'); // Empty page has been read in and gotten without an exception being thrown. $this->addToAssertionCount(1); } /** - * Function used to set mock for parser return and force init method to run between tests. + * Validate testDeprecatedPage. + * + * @return void + * @throws XmlException + */ + public function testDeprecatedPage(): void + { + $mockData = [ + 'testPage1' => [ + 'url' => 'testURL1', + 'module' => 'testModule1', + 'section' => [ + ], + 'area' => 'test', + 'deprecated' => 'deprecation message', + 'filename' => 'filename.xml' + ]]; + + $this->mockPageObjectHandlerWithData($mockData); + PageObjectHandler::getInstance()->getObject('testPage1'); + + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'notice', + 'NOTICE: 1 Page name violations detected. See mftf.log for details.', + [] + ); + } + + /** + * Create mock page object handler with data. + * + * @param array $mockData * - * @param array $data + * @return void */ - private function setMockParserOutput($data) + private function mockPageObjectHandlerWithData(array $mockData): void { - // clear section object handler value to inject parsed content - $property = new \ReflectionProperty(PageObjectHandler::class, 'INSTANCE'); + $pageObjectHandlerProperty = new ReflectionProperty(PageObjectHandler::class, 'INSTANCE'); + $pageObjectHandlerProperty->setAccessible(true); + $pageObjectHandlerProperty->setValue(null, null); + + $mockSectionParser = $this->createMock(PageParser::class); + $mockSectionParser + ->method('getData') + ->willReturn($mockData); + + $objectManager = ObjectManagerFactory::getObjectManager(); + $mockObjectManagerInstance = $this->createMock(ObjectManager::class); + $mockObjectManagerInstance + ->method('get') + ->will( + $this->returnCallback( + function ( + string $class, + array $arguments = [] + ) use ( + $objectManager, + $mockSectionParser + ) { + if ($class === PageParser::class) { + return $mockSectionParser; + } + + return $objectManager->create($class, $arguments); + } + ) + ); + + $property = new ReflectionProperty(ObjectManager::class, 'instance'); $property->setAccessible(true); - $property->setValue(null); + $property->setValue(null, $mockObjectManagerInstance); + } + + /** + * @inheritDoc + */ + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + + $pageObjectHandlerProperty = new ReflectionProperty(PageObjectHandler::class, 'INSTANCE'); + $pageObjectHandlerProperty->setAccessible(true); + $pageObjectHandlerProperty->setValue(null, null); + + $objectManagerProperty = new ReflectionProperty(ObjectManager::class, 'instance'); + $objectManagerProperty->setAccessible(true); + $objectManagerProperty->setValue(null, null); - $mockSectionParser = AspectMock::double(PageParser::class, ["getData" => $data])->make(); - $instance = AspectMock::double(ObjectManager::class, ['get' => $mockSectionParser])->make(); - AspectMock::double(ObjectManagerFactory::class, ['getObjectManager' => $instance]); + TestLoggingUtil::getInstance()->clearMockLoggingUtil(); } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Page/Handlers/SectionObjectHandlerTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Page/Handlers/SectionObjectHandlerTest.php index 4a939dbad..6ef16096a 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Page/Handlers/SectionObjectHandlerTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Page/Handlers/SectionObjectHandlerTest.php @@ -3,69 +3,171 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace tests\unit\Magento\FunctionalTestFramework\Page\Handlers; -use AspectMock\Test as AspectMock; +use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\ObjectManager; use Magento\FunctionalTestingFramework\ObjectManagerFactory; use Magento\FunctionalTestingFramework\Page\Handlers\SectionObjectHandler; use Magento\FunctionalTestingFramework\XmlParser\SectionParser; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use ReflectionProperty; +use tests\unit\Util\MagentoTestCase; +use tests\unit\Util\TestLoggingUtil; +/** + * Class SectionObjectHandlerTest + */ class SectionObjectHandlerTest extends MagentoTestCase { - public function testGetSectionObject() + /** + * @inheritDoc + */ + protected function setUp(): void + { + TestLoggingUtil::getInstance()->setMockLoggingUtil(); + } + + /** + * Validate testGetSectionObject. + * + * @return void + * @throws XmlException + */ + public function testGetSectionObject(): void { $mockData = [ - "testSection1" => [ - "element" => [ - "testElement" => [ - "type" => "input", - "selector" => "#element" + 'testSection1' => [ + 'element' => [ + 'testElement' => [ + 'type' => 'input', + 'selector' => '#element' ] ] ], - "testSection2" => [ - "element" => [ - "testElement" => [ - "type" => "input", - "selector" => "#element" + 'testSection2' => [ + 'element' => [ + 'testElement' => [ + 'type' => 'input', + 'selector' => '#element' ] ] ] ]; - $this->setMockParserOutput($mockData); + $this->mockSectionObjectHandlerWithData($mockData); // get sections $sectionHandler = SectionObjectHandler::getInstance(); $sections = $sectionHandler->getAllObjects(); - $section = $sectionHandler->getObject("testSection1"); - $invalidSection = $sectionHandler->getObject("InvalidSection"); + $sectionHandler->getObject('testSection1'); + $invalidSection = $sectionHandler->getObject('InvalidSection'); // perform asserts $this->assertCount(2, $sections); - $this->assertArrayHasKey("testSection1", $sections); - $this->assertArrayHasKey("testSection2", $sections); + $this->assertArrayHasKey('testSection1', $sections); + $this->assertArrayHasKey('testSection2', $sections); $this->assertNull($invalidSection); } /** - * Set the mock parser return value + * Validate testDeprecatedSection. * - * @param array $data + * @return void + * @throws XmlException */ - private function setMockParserOutput($data) + public function testDeprecatedSection(): void { - // clear section object handler value to inject parsed content - $property = new \ReflectionProperty(SectionObjectHandler::class, "INSTANCE"); + $mockData = [ + 'testSection1' => [ + 'element' => [ + 'testElement' => [ + 'type' => 'input', + 'selector' => '#element', + 'deprecated' => 'element deprecation message' + ] + ], + 'filename' => 'filename.xml', + 'deprecated' => 'section deprecation message' + ] + ]; + + $this->mockSectionObjectHandlerWithData($mockData); + + // get sections + $sectionHandler = SectionObjectHandler::getInstance(); + $sectionHandler->getObject('testSection1'); + + //validate deprecation warning + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'notice', + 'NOTICE: 1 Section name violations detected. See mftf.log for details.', + [] + ); + } + + /** + * Create mock section object handler with data. + * + * @param array $mockData + * + * @return void + */ + private function mockSectionObjectHandlerWithData(array $mockData): void + { + $sectionObjectHandlerProperty = new ReflectionProperty(SectionObjectHandler::class, "INSTANCE"); + $sectionObjectHandlerProperty->setAccessible(true); + $sectionObjectHandlerProperty->setValue(null, null); + + $mockSectionParser = $this->createMock(SectionParser::class); + $mockSectionParser + ->method('getData') + ->willReturn($mockData); + + $objectManager = ObjectManagerFactory::getObjectManager(); + $mockObjectManagerInstance = $this->createMock(ObjectManager::class); + $mockObjectManagerInstance + ->method('get') + ->will( + $this->returnCallback( + function ( + string $class, + array $arguments = [] + ) use ( + $objectManager, + $mockSectionParser + ) { + if ($class === SectionParser::class) { + return $mockSectionParser; + } + + return $objectManager->create($class, $arguments); + } + ) + ); + + $property = new ReflectionProperty(ObjectManager::class, 'instance'); $property->setAccessible(true); - $property->setValue(null); + $property->setValue(null, $mockObjectManagerInstance); + } + + /** + * @inheritDoc + */ + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + + $sectionObjectHandlerProperty = new ReflectionProperty(SectionObjectHandler::class, "INSTANCE"); + $sectionObjectHandlerProperty->setAccessible(true); + $sectionObjectHandlerProperty->setValue(null, null); + + $objectManagerProperty = new ReflectionProperty(ObjectManager::class, 'instance'); + $objectManagerProperty->setAccessible(true); + $objectManagerProperty->setValue(null, null); - $mockSectionParser = AspectMock::double(SectionParser::class, ["getData" => $data])->make(); - $instance = AspectMock::double(ObjectManager::class, ["get" => $mockSectionParser])->make(); - AspectMock::double(ObjectManagerFactory::class, ["getObjectManager" => $instance]); + TestLoggingUtil::getInstance()->clearMockLoggingUtil(); } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Page/Objects/ElementObjectTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Page/Objects/ElementObjectTest.php index d585f4085..eacafbc68 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Page/Objects/ElementObjectTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Page/Objects/ElementObjectTest.php @@ -8,7 +8,7 @@ use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Page\Objects\ElementObject; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use tests\unit\Util\MagentoTestCase; /** * Class ElementObjectTest @@ -32,7 +32,7 @@ public function testTimeoutNotNull() $element = new ElementObject('name', 'type', 'selector', null, '15', false); $timeout = $element->getTimeout(); $this->assertEquals(15, $timeout); - $this->assertInternalType('int', $timeout); + $this->assertIsInt($timeout); } /** @@ -43,7 +43,7 @@ public function testTimeoutCastFromString() $element = new ElementObject('name', 'type', 'selector', null, 'helloString', true); $timeout = $element->getTimeout(); $this->assertEquals(0, $timeout); - $this->assertInternalType('int', $timeout); + $this->assertIsInt($timeout); } /** diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Page/Objects/PageObjectTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Page/Objects/PageObjectTest.php index 7f8053e77..29ec5366d 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Page/Objects/PageObjectTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Page/Objects/PageObjectTest.php @@ -7,7 +7,7 @@ namespace tests\unit\Magento\FunctionalTestFramework\Page\Objects; use Magento\FunctionalTestingFramework\Page\Objects\PageObject; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use tests\unit\Util\MagentoTestCase; /** * Class PageObjectTest diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Page/Objects/SectionObjectTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Page/Objects/SectionObjectTest.php index 5ed1f557f..4305c0101 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Page/Objects/SectionObjectTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Page/Objects/SectionObjectTest.php @@ -8,7 +8,7 @@ use Magento\FunctionalTestingFramework\Page\Objects\ElementObject; use Magento\FunctionalTestingFramework\Page\Objects\SectionObject; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use tests\unit\Util\MagentoTestCase; /** * Class SectionObjectTest diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/StaticCheck/AnnotationsCheckTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/StaticCheck/AnnotationsCheckTest.php new file mode 100644 index 000000000..004955998 --- /dev/null +++ b/dev/tests/unit/Magento/FunctionalTestFramework/StaticCheck/AnnotationsCheckTest.php @@ -0,0 +1,184 @@ +staticCheck = new AnnotationsCheck(); + $this->staticCheckClass = new ReflectionClass($this->staticCheck); + } + + public function testValidateRequiredAnnotationsNoError() + { + $annotations = [ + 'features' => [ + 0 => 'feature1' + ], + 'stories' => [ + 0 => 'story1' + ], + 'description' => [ + 'main' => 'description1', + 'test_files' => 'file1', + 'deprecated' => [ + 0 => 'deprecated1' + ] + ], + 'severity' => [ + 0 => 'severity1' + ], + 'title' => [ + 0 => '[NO TESTCASEID]: title1' + ], + ]; + $expected = []; + + $test = $this->createMock(TestObject::class); + + $test->expects($this->once())->method('getAnnotations')->willReturn($annotations); + + $validateRequiredAnnotations = $this->staticCheckClass->getMethod('validateRequiredAnnotations'); + $validateRequiredAnnotations->setAccessible(true); + + $validateRequiredAnnotations->invoke($this->staticCheck, $test); + $this->assertEquals($expected, $this->staticCheck->getErrors()); + } + + public function testValidateRequiredAnnotationsMissing() + { + $testCaseId = 'MC-12345'; + + $annotations = [ + 'features' => [ + 0 => 'feature1' + ], + 'stories' => [ + 0 => 'story1' + ], + 'description' => [ + 'test_files' => 'file1', + 'deprecated' => [ + 0 => 'deprecated1' + ] + ], + 'title' => [ + 0 => $testCaseId . ': title1' + ], + 'testCaseId' => [ + 0 => $testCaseId + ] + ]; + $expected = [ + 0 => [ + 0 => 'Test AnnotationsCheckTest is missing the required annotations: description, severity' + ] + ]; + + $test = $this->createMock(TestObject::class); + + $test->expects($this->once())->method('getAnnotations')->willReturn($annotations); + $test->expects($this->once())->method('getName')->willReturn('AnnotationsCheckTest'); + + $validateRequiredAnnotations = $this->staticCheckClass->getMethod('validateRequiredAnnotations'); + $validateRequiredAnnotations->setAccessible(true); + + $validateRequiredAnnotations->invoke($this->staticCheck, $test); + $this->assertEquals($expected, $this->staticCheck->getErrors()); + } + + public function testValidateRequiredAnnotationsMissingNoTestCaseId() + { + $annotations = [ + 'features' => [ + 0 => 'feature1' + ], + 'stories' => [ + 0 => 'story1' + ], + 'description' => [ + 'test_files' => 'file1', + 'deprecated' => [ + 0 => 'deprecated1' + ] + ], + 'title' => [ + 0 => "[NO TESTCASEID]: \t" + ], + ]; + $expected = [ + 0 => [ + 0 => 'Test AnnotationsCheckTest is missing the required annotations: title, description, severity' + ] + ]; + + $test = $this->createMock(TestObject::class); + + $test->expects($this->once())->method('getAnnotations')->willReturn($annotations); + $test->expects($this->once())->method('getName')->willReturn('AnnotationsCheckTest'); + + $validateRequiredAnnotations = $this->staticCheckClass->getMethod('validateRequiredAnnotations'); + $validateRequiredAnnotations->setAccessible(true); + + $validateRequiredAnnotations->invoke($this->staticCheck, $test); + $this->assertEquals($expected, $this->staticCheck->getErrors()); + } + + public function testValidateRequiredAnnotationsEmpty() + { + $annotations = [ + 'features' => [ + 0 => 'feature1' + ], + 'stories' => [ + 0 => 'story1' + ], + 'description' => [ + 'main' => 'description1', + 'test_files' => 'file1', + 'deprecated' => [ + 0 => 'deprecated1' + ] + ], + 'severity' => [ + 0 => 'severity1' + ], + 'title' => [ + 0 => '' + ], + ]; + $expected = [ + 0 => [ + 0 => 'Test AnnotationsCheckTest is missing the required annotations: title' + ] + ]; + + $test = $this->createMock(TestObject::class); + + $test->expects($this->once())->method('getAnnotations')->willReturn($annotations); + $test->expects($this->once())->method('getName')->willReturn('AnnotationsCheckTest'); + + $validateRequiredAnnotations = $this->staticCheckClass->getMethod('validateRequiredAnnotations'); + $validateRequiredAnnotations->setAccessible(true); + + $validateRequiredAnnotations->invoke($this->staticCheck, $test); + $this->assertEquals($expected, $this->staticCheck->getErrors()); + } +} diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/StaticCheck/DeprecatedEntityUsageCheckTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/StaticCheck/DeprecatedEntityUsageCheckTest.php new file mode 100644 index 000000000..57044f123 --- /dev/null +++ b/dev/tests/unit/Magento/FunctionalTestFramework/StaticCheck/DeprecatedEntityUsageCheckTest.php @@ -0,0 +1,368 @@ +staticCheck = new DeprecatedEntityUsageCheck(); + $this->staticCheckClass = new ReflectionClass($this->staticCheck); + } + + /** + * Validate testInvalidPathOption. + * + * @return void + * @throws ReflectionException + */ + public function testInvalidPathOption(): void + { + $input = $this->getMockBuilder(InputInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $input->method('getOption') + ->with('path') + ->willReturn('/invalidPath'); + + $loadAllXmlFiles = $this->staticCheckClass->getMethod('loadAllXMLFiles'); + $loadAllXmlFiles->setAccessible(true); + + $this->expectException(InvalidArgumentException::class); + $loadAllXmlFiles->invoke($this->staticCheck, $input); + } + + /** + * Validate testViolatingElementReferences. + * + * @return void + * @throws ReflectionException + */ + public function testViolatingElementReferences(): void + { + //variables for assertions + $elementName = 'elementOne'; + $sectionName = 'SectionOne'; + $fileName = 'section.xml'; + + $element = new ElementObject($elementName, 'type', '#selector1', null, '41', false, 'deprecated'); + $section = new SectionObject($sectionName, [$element], $fileName); + $elementRef = $sectionName . '.' . $elementName; + $references = [$elementRef => $element, $sectionName => $section]; + $actual = $this->callViolatingReferences($references); + $expected = [ + 'Deprecated Element(s)' => [ + 0 => [ + 'name' => $elementRef, + 'file' => $fileName + ] + ] + ]; + $this->assertEquals($actual, $expected); + } + + /** + * Validate testViolatingPageReferences. + * + * @return void + * @throws ReflectionException + */ + public function testViolatingPageReferences(): void + { + //Page variables for assertions + $pageName = 'Page'; + $fileName = 'page.xml'; + + $page = new PageObject($pageName, '/url.html', 'Test', [], false, 'test', $fileName, 'deprecated'); + $references = ['Page' => $page]; + $actual = $this->callViolatingReferences($references); + $expected = [ + 'Deprecated Page(s)' => [ + 0 => [ + 'name' => $pageName, + 'file' => $fileName + ] + ] + ]; + $this->assertEquals($actual, $expected); + } + + /** + * Validate testViolatingDataReferences. + * + * @return void + * @throws ReflectionException + */ + public function testViolatingDataReferences(): void + { + //Data entity variables for assertions + $entityName = 'EntityOne'; + $fileName = 'entity.xml'; + + $entity = new EntityDataObject( + $entityName, + 'testType', + ['testkey' => 'testValue'], + [], + null, + [], + null, + $fileName, + 'deprecated' + ); + $references = [$entityName => $entity]; + $actual = $this->callViolatingReferences($references); + $expected = [ + 'Deprecated Data(s)' => [ + 0 => [ + 'name' => $entityName, + 'file' => $fileName + ] + ] + ]; + $this->assertEquals($actual, $expected); + } + + /** + * Validate testViolatingTestReferences. + * + * @return void + * @throws ReflectionException + */ + public function testViolatingTestReferences(): void + { + // test variables for assertions + $testName = 'Test1'; + $fileName = 'test.xml'; + + $test = new TestObject($testName, [], [], [], $fileName, null, 'deprecated'); + $references = ['Test1' => $test]; + $actual = $this->callViolatingReferences($references); + $expected = [ + 'Deprecated Test(s)' => [ + 0 => [ + 'name' => $testName, + 'file' => $fileName + ] + ] + ]; + $this->assertEquals($actual, $expected); + } + + /** + * Validate testViolatingMetaDataReferences. + * + * @return void + * @throws ReflectionException + */ + public function testViolatingMetaDataReferences(): void + { + // Data Variables for Assertions + $dataType1 = 'type1'; + $operationType1 = 'create'; + $operationType2 = 'update'; + + /** + * Parser Output. + * operationName + * createType1 + * has field + * key=id, value=integer + * updateType1 + * has field + * key=id, value=integer + */ + $mockData = [OperationDefinitionObjectHandler::ENTITY_OPERATION_ROOT_TAG => [ + 'testOperationName' => [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_DATA_TYPE => $dataType1, + OperationDefinitionObjectHandler::ENTITY_OPERATION_TYPE => $operationType1, + OperationDefinitionObjectHandler::ENTITY_OPERATION_AUTH => 'auth', + OperationDefinitionObjectHandler::ENTITY_OPERATION_URL => 'V1/Type1', + OperationDefinitionObjectHandler::ENTITY_OPERATION_METHOD => 'POST', + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY => [ + 0 => [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_KEY => 'id', + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_VALUE => 'integer' + ], + ], + OperationDefinitionObjectHandler::OBJ_DEPRECATED => 'deprecated' + ],[ + OperationDefinitionObjectHandler::ENTITY_OPERATION_DATA_TYPE => $dataType1, + OperationDefinitionObjectHandler::ENTITY_OPERATION_TYPE => $operationType2, + OperationDefinitionObjectHandler::ENTITY_OPERATION_AUTH => 'auth', + OperationDefinitionObjectHandler::ENTITY_OPERATION_URL => 'V1/Type1/{id}', + OperationDefinitionObjectHandler::ENTITY_OPERATION_METHOD => 'PUT', + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY => [ + 0 => [ + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_KEY => 'id', + OperationDefinitionObjectHandler::ENTITY_OPERATION_ENTRY_VALUE => 'integer' + ], + ] + ]]]; + + $this->mockOperationHandlerWithData($mockData); + $dataName = 'dataName1'; + $references = [ + $dataName => [ + $dataType1 => [ + $operationType1, + $operationType2 + ] + ] + ]; + + $expected = [ + '"'.$dataName.'" references deprecated' => [ + 0 => [ + 'name' => $dataType1, + 'file' => 'metadata xml file' + ] + ] + ]; + $property = $this->staticCheckClass->getMethod('findViolatingMetadataReferences'); + $property->setAccessible(true); + $actual = $property->invoke($this->staticCheck, $references); + $this->assertEquals($actual, $expected); + } + + /** + * Validate testIsDeprecated. + * + * @return void + * @throws ReflectionException + */ + public function testIsDeprecated(): void + { + // Test Data + $contents = ' + + + + + + '; + + $property = $this->staticCheckClass->getMethod('isDeprecated'); + $property->setAccessible(true); + $output = $property->invoke($this->staticCheck, $contents); + $this->assertTrue($output); + } + + /** + * Create mock operation handler with data. + * + * @param array $mockData + * + * @return void + */ + private function mockOperationHandlerWithData(array $mockData): void + { + $operationDefinitionObjectHandlerProperty = new ReflectionProperty( + OperationDefinitionObjectHandler::class, + 'INSTANCE' + ); + $operationDefinitionObjectHandlerProperty->setAccessible(true); + $operationDefinitionObjectHandlerProperty->setValue(null, null); + + $mockOperationParser = $this->createMock(OperationDefinitionParser::class); + $mockOperationParser + ->method('readOperationMetadata') + ->willReturn($mockData); + + $objectManager = ObjectManagerFactory::getObjectManager(); + $mockObjectManagerInstance = $this->createMock(ObjectManager::class); + $mockObjectManagerInstance + ->method('create') + ->will( + $this->returnCallback( + function ( + string $class, + array $arguments = [] + ) use ( + $objectManager, + $mockOperationParser + ) { + if ($class === OperationDefinitionParser::class) { + return $mockOperationParser; + } + + return $objectManager->create($class, $arguments); + } + ) + ); + + $property = new ReflectionProperty(ObjectManager::class, 'instance'); + $property->setAccessible(true); + $property->setValue(null, $mockObjectManagerInstance); + } + + /** + * @inheritDoc + */ + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + + $operationDefinitionObjectHandlerProperty = new ReflectionProperty( + OperationDefinitionObjectHandler::class, + 'INSTANCE' + ); + $operationDefinitionObjectHandlerProperty->setAccessible(true); + $operationDefinitionObjectHandlerProperty->setValue(null, null); + + $objectManagerProperty = new ReflectionProperty(ObjectManager::class, 'instance'); + $objectManagerProperty->setAccessible(true); + $objectManagerProperty->setValue(null, null); + } + + /** + * Invoke findViolatingReferences. + * + * @param array $references + * + * @return mixed + * @throws ReflectionException + */ + public function callViolatingReferences(array $references) + { + $property = $this->staticCheckClass->getMethod('findViolatingReferences'); + $property->setAccessible(true); + + return $property->invoke($this->staticCheck, $references); + } +} diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Suite/Handlers/SuiteObjectHandlerTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Suite/Handlers/SuiteObjectHandlerTest.php index e45127f4c..7d1dd46bb 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Suite/Handlers/SuiteObjectHandlerTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Suite/Handlers/SuiteObjectHandlerTest.php @@ -3,32 +3,30 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Tests\unit\Magento\FunctionalTestFramework\Suite\Handlers; +declare(strict_types=1); -use AspectMock\Test as AspectMock; -use Magento\FunctionalTestingFramework\ObjectManager\ObjectManager; -use Magento\FunctionalTestingFramework\ObjectManagerFactory; +namespace tests\unit\Magento\FunctionalTestFramework\Suite\Handlers; + +use Exception; +use Magento\FunctionalTestingFramework\ObjectManager; use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; use Magento\FunctionalTestingFramework\Suite\Parsers\SuiteDataParser; use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; use Magento\FunctionalTestingFramework\Test\Parsers\TestDataParser; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use ReflectionProperty; +use tests\unit\Util\MagentoTestCase; use tests\unit\Util\SuiteDataArrayBuilder; use tests\unit\Util\TestDataArrayBuilder; -use tests\unit\Util\MockModuleResolverBuilder; class SuiteObjectHandlerTest extends MagentoTestCase { - public function setUp() - { - $resolverMock = new MockModuleResolverBuilder(); - $resolverMock->setup(); - } - /** - * Tests basic parsing and accesors of suite object and suite object supporting classes + * Tests basic parsing and accessors of suite object and suite object supporting classes. + * + * @return void + * @throws Exception */ - public function testGetSuiteObject() + public function testGetSuiteObject(): void { $suiteDataArrayBuilder = new SuiteDataArrayBuilder(); $mockData = $suiteDataArrayBuilder @@ -82,35 +80,54 @@ public function testGetSuiteObject() * Function used to set mock for parser return and force init method to run between tests. * * @param array $testData - * @throws \Exception + * @param array $suiteData + * + * @return void + * @throws Exception */ - private function setMockTestAndSuiteParserOutput($testData, $suiteData) + private function setMockTestAndSuiteParserOutput(array $testData, array $suiteData): void { // clear test object handler value to inject parsed content - $property = new \ReflectionProperty(TestObjectHandler::class, 'testObjectHandler'); + $property = new ReflectionProperty(TestObjectHandler::class, 'testObjectHandler'); $property->setAccessible(true); - $property->setValue(null); + $property->setValue(null, null); // clear suite object handler value to inject parsed content - $property = new \ReflectionProperty(SuiteObjectHandler::class, 'instance'); + $property = new ReflectionProperty(SuiteObjectHandler::class, 'instance'); + $property->setAccessible(true); + $property->setValue(null, null); + + $mockDataParser = $this->createMock(TestDataParser::class); + $mockDataParser + ->method('readTestData') + ->willReturn($testData); + + $mockSuiteDataParser = $this->createMock(SuiteDataParser::class); + $mockSuiteDataParser + ->method('readSuiteData') + ->willReturn($suiteData); + + $instance = $this->createMock(ObjectManager::class); + $instance + ->method('create') + ->will( + $this->returnCallback( + function ($clazz) use ($mockDataParser, $mockSuiteDataParser) { + if ($clazz === TestDataParser::class) { + return $mockDataParser; + } + + if ($clazz === SuiteDataParser::class) { + return $mockSuiteDataParser; + } + + return null; + } + ) + ); + + $property = new ReflectionProperty(ObjectManager::class, 'instance'); $property->setAccessible(true); - $property->setValue(null); - - $mockDataParser = AspectMock::double(TestDataParser::class, ['readTestData' => $testData])->make(); - $mockSuiteDataParser = AspectMock::double(SuiteDataParser::class, ['readSuiteData' => $suiteData])->make(); - $instance = AspectMock::double( - ObjectManager::class, - ['create' => function ($clazz) use ($mockDataParser, $mockSuiteDataParser) { - if ($clazz == TestDataParser::class) { - return $mockDataParser; - } - - if ($clazz == SuiteDataParser::class) { - return $mockSuiteDataParser; - } - }] - )->make(); - // bypass the private constructor - AspectMock::double(ObjectManagerFactory::class, ['getObjectManager' => $instance]); + $property->setValue(null, $instance); } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Suite/SuiteGeneratorTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Suite/SuiteGeneratorTest.php index 842be4f88..e6acefca9 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Suite/SuiteGeneratorTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Suite/SuiteGeneratorTest.php @@ -3,12 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Tests\unit\Magento\FunctionalTestFramework\Suite; +declare(strict_types=1); -use AspectMock\Test as AspectMock; +namespace tests\unit\Magento\FunctionalTestFramework\Suite; + +use Exception; use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; -use Magento\FunctionalTestingFramework\ObjectManager\ObjectManager; +use Magento\FunctionalTestingFramework\ObjectManager; use Magento\FunctionalTestingFramework\ObjectManagerFactory; +use Magento\FunctionalTestingFramework\Suite\Service\SuiteGeneratorService; use Magento\FunctionalTestingFramework\Suite\SuiteGenerator; use Magento\FunctionalTestingFramework\Suite\Generators\GroupClassGenerator; use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; @@ -16,43 +19,88 @@ use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; use Magento\FunctionalTestingFramework\Test\Util\TestObjectExtractor; use Magento\FunctionalTestingFramework\Test\Parsers\TestDataParser; +use Magento\FunctionalTestingFramework\Util\GenerationErrorHandler; use Magento\FunctionalTestingFramework\Util\Manifest\DefaultTestManifest; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use ReflectionProperty; +use tests\unit\Util\MagentoTestCase; use Magento\FunctionalTestingFramework\Util\Manifest\TestManifestFactory; use tests\unit\Util\SuiteDataArrayBuilder; use tests\unit\Util\TestDataArrayBuilder; use tests\unit\Util\TestLoggingUtil; -use tests\unit\Util\MockModuleResolverBuilder; class SuiteGeneratorTest extends MagentoTestCase { /** - * Setup entry append and clear for Suite Generator + * Before test functionality. + * + * @return void */ - public static function setUpBeforeClass() + protected function setUp(): void { - AspectMock::double(SuiteGenerator::class, [ - 'clearPreviousSessionConfigEntries' => null, - 'appendEntriesToConfig' => null - ]); + TestLoggingUtil::getInstance()->setMockLoggingUtil(); } /** - * Before test functionality + * Tests generating a suite given a set of parsed test data. + * * @return void + * @throws Exception */ - public function setUp() + public function testGenerateTestgroupmembership(): void { - TestLoggingUtil::getInstance()->setMockLoggingUtil(); - $resolverMock = new MockModuleResolverBuilder(); - $resolverMock->setup(); + $suiteDataArrayBuilder = new SuiteDataArrayBuilder(); + $mockSuiteData = $suiteDataArrayBuilder + ->withName('mockSuite') + ->includeGroups(['group1']) + ->build(); + $testDataArrayBuilder = new TestDataArrayBuilder(); + $mockSimpleTest1 = $testDataArrayBuilder + ->withName('simpleTest1') + ->withAnnotations(['group' => [['value' => 'group1']]]) + ->withTestReference("NonExistantTest") + ->withTestActions() + ->build(); + $mockSimpleTest2 = $testDataArrayBuilder + ->withName('simpleTest2') + ->withAnnotations(['group' => [['value' => 'group1']]]) + ->withTestActions() + ->build(); + $mockSimpleTest3 = $testDataArrayBuilder + ->withName('simpleTest3') + ->withAnnotations(['group' => [['value' => 'group1']]]) + ->withTestActions() + ->build(); + $mockTestData = array_merge($mockSimpleTest1, $mockSimpleTest2, $mockSimpleTest3); + $this->setMockTestAndSuiteParserOutput($mockTestData, $mockSuiteData); + + // Make manifest for split suites + $suiteConfig = [ + 'mockSuite' => [ + 'mockSuite_0_G' => ['simpleTest1', 'simpleTest2'], + 'mockSuite_1_G' => ['simpleTest3'], + ], + ]; + $manifest = TestManifestFactory::makeManifest('default', $suiteConfig); + + // parse and generate suite object with mocked data and manifest + $mockSuiteGenerator = SuiteGenerator::getInstance(); + $mockSuiteGenerator->generateAllSuites($manifest); + + // assert last split suite group generated + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'info', + 'suite generated', + ['suite' => 'mockSuite_1_G', 'relative_path' => '_generated' . DIRECTORY_SEPARATOR . 'mockSuite_1_G'] + ); } /** - * Tests generating a single suite given a set of parsed test data - * @throws \Exception + * Tests generating a single suite given a set of parsed test data. + * + * @return void + * @throws Exception */ - public function testGenerateSuite() + public function testGenerateSuite(): void { $suiteDataArrayBuilder = new SuiteDataArrayBuilder(); $mockData = $suiteDataArrayBuilder @@ -74,21 +122,23 @@ public function testGenerateSuite() // parse and generate suite object with mocked data $mockSuiteGenerator = SuiteGenerator::getInstance(); - $mockSuiteGenerator->generateSuite("basicTestSuite"); + $mockSuiteGenerator->generateSuite('basicTestSuite'); // assert that expected suite is generated TestLoggingUtil::getInstance()->validateMockLogStatement( 'info', - "suite generated", - ['suite' => 'basicTestSuite', 'relative_path' => "_generated" . DIRECTORY_SEPARATOR . "basicTestSuite"] + 'suite generated', + ['suite' => 'basicTestSuite', 'relative_path' => '_generated' . DIRECTORY_SEPARATOR . 'basicTestSuite'] ); } /** - * Tests generating all suites given a set of parsed test data - * @throws \Exception + * Tests generating all suites given a set of parsed test data. + * + * @return void + * @throws Exception */ - public function testGenerateAllSuites() + public function testGenerateAllSuites(): void { $suiteDataArrayBuilder = new SuiteDataArrayBuilder(); $mockData = $suiteDataArrayBuilder @@ -109,24 +159,33 @@ public function testGenerateAllSuites() $this->setMockTestAndSuiteParserOutput($mockTestData, $mockData); // parse and retrieve suite object with mocked data - $exampleTestManifest = new DefaultTestManifest([], "sample" . DIRECTORY_SEPARATOR . "path"); + $exampleTestManifest = new DefaultTestManifest([], 'sample' . DIRECTORY_SEPARATOR . 'path'); $mockSuiteGenerator = SuiteGenerator::getInstance(); $mockSuiteGenerator->generateAllSuites($exampleTestManifest); // assert that expected suites are generated TestLoggingUtil::getInstance()->validateMockLogStatement( 'info', - "suite generated", - ['suite' => 'basicTestSuite', 'relative_path' => "_generated" . DIRECTORY_SEPARATOR . "basicTestSuite"] + 'suite generated', + ['suite' => 'basicTestSuite', 'relative_path' => '_generated' . DIRECTORY_SEPARATOR . 'basicTestSuite'] ); } /** - * Tests attempting to generate a suite with no included/excluded tests and no hooks - * @throws \Exception + * Tests attempting to generate a suite with no included/excluded tests and no hooks. + * + * @return void + * @throws Exception */ - public function testGenerateEmptySuite() + public function testGenerateEmptySuite(): void { + $testDataArrayBuilder = new TestDataArrayBuilder(); + $mockTestData = $testDataArrayBuilder + ->withName('test') + ->withAnnotations() + ->withTestActions() + ->build(); + $suiteDataArrayBuilder = new SuiteDataArrayBuilder(); $mockData = $suiteDataArrayBuilder ->withName('basicTestSuite') @@ -134,18 +193,23 @@ public function testGenerateEmptySuite() unset($mockData['suites']['basicTestSuite'][TestObjectExtractor::TEST_BEFORE_HOOK]); unset($mockData['suites']['basicTestSuite'][TestObjectExtractor::TEST_AFTER_HOOK]); - $mockTestData = null; $this->setMockTestAndSuiteParserOutput($mockTestData, $mockData); // set expected error message - $this->expectExceptionMessage("Suites must not be empty. Suite: \"basicTestSuite\""); + $this->expectExceptionMessage('Suite basicTestSuite is not defined in xml or is invalid'); // parse and generate suite object with mocked data $mockSuiteGenerator = SuiteGenerator::getInstance(); - $mockSuiteGenerator->generateSuite("basicTestSuite"); + $mockSuiteGenerator->generateSuite('basicTestSuite'); } - public function testInvalidSuiteTestPair() + /** + * Tests generating all suites with a suite containing invalid test reference. + * + * @return void + * @throws TestReferenceException + */ + public function testInvalidSuiteTestPair(): void { // Mock Suite1 => Test1 and Suite2 => Test2 $suiteDataArrayBuilder = new SuiteDataArrayBuilder(); @@ -179,16 +243,24 @@ public function testInvalidSuiteTestPair() $suiteConfig = ['Suite2' => ['Test1']]; $manifest = TestManifestFactory::makeManifest('default', $suiteConfig); - // Set up Expected Exception - $this->expectException(TestReferenceException::class); - $this->expectExceptionMessageRegExp('(Suite: "Suite2" Tests: "Test1")'); - // parse and generate suite object with mocked data and manifest $mockSuiteGenerator = SuiteGenerator::getInstance(); $mockSuiteGenerator->generateAllSuites($manifest); + + // assert that no exception for generateAllSuites and suite generation error is stored in GenerationErrorHandler + $errMessage = 'Cannot reference tests which are not declared as part of suite (Suite: "Suite2" Tests: "Test1")'; + TestLoggingUtil::getInstance()->validateMockLogStatement('error', $errMessage, []); + $suiteErrors = GenerationErrorHandler::getInstance()->getErrorsByType('suite'); + $this->assertArrayHasKey('Suite2', $suiteErrors); } - public function testNonExistentSuiteTestPair() + /** + * Tests generating all suites with a non-existing suite. + * + * @return void + * @throws TestReferenceException + */ + public function testNonExistentSuiteTestPair(): void { $testDataArrayBuilder = new TestDataArrayBuilder(); $mockSimpleTest = $testDataArrayBuilder @@ -203,80 +275,197 @@ public function testNonExistentSuiteTestPair() $suiteConfig = ['Suite3' => ['Test1']]; $manifest = TestManifestFactory::makeManifest('default', $suiteConfig); - // Set up Expected Exception - $this->expectException(TestReferenceException::class); - $this->expectExceptionMessageRegExp('#Suite3 is not defined#'); + // parse and generate suite object with mocked data and manifest + $mockSuiteGenerator = SuiteGenerator::getInstance(); + $mockSuiteGenerator->generateAllSuites($manifest); + + // assert that no exception for generateAllSuites and suite generation error is stored in GenerationErrorHandler + $errMessage = 'Suite Suite3 is not defined in xml or is invalid.'; + TestLoggingUtil::getInstance()->validateMockLogStatement('error', $errMessage, []); + $suiteErrors = GenerationErrorHandler::getInstance()->getErrorsByType('suite'); + $this->assertArrayHasKey('Suite3', $suiteErrors); + } + + /** + * Tests generating split suites for parallel test generation. + * + * @return void + * @throws TestReferenceException + */ + public function testGenerateSplitSuiteFromTest(): void + { + $suiteDataArrayBuilder = new SuiteDataArrayBuilder(); + $mockSuiteData = $suiteDataArrayBuilder + ->withName('mockSuite') + ->includeGroups(['group1']) + ->build(); + $testDataArrayBuilder = new TestDataArrayBuilder(); + $mockSimpleTest1 = $testDataArrayBuilder + ->withName('simpleTest1') + ->withAnnotations(['group' => [['value' => 'group1']]]) + ->withTestReference("NonExistantTest") + ->withTestActions() + ->build(); + $mockSimpleTest2 = $testDataArrayBuilder + ->withName('simpleTest2') + ->withAnnotations(['group' => [['value' => 'group1']]]) + ->withTestActions() + ->build(); + $mockSimpleTest3 = $testDataArrayBuilder + ->withName('simpleTest3') + ->withAnnotations(['group' => [['value' => 'group1']]]) + ->withTestActions() + ->build(); + $mockTestData = array_merge($mockSimpleTest1, $mockSimpleTest2, $mockSimpleTest3); + $this->setMockTestAndSuiteParserOutput($mockTestData, $mockSuiteData); + + // Make manifest for split suites + $suiteConfig = [ + 'mockSuite' => [ + 'mockSuite_0_G' => ['simpleTest1', 'simpleTest2'], + 'mockSuite_1_G' => ['simpleTest3'], + ], + ]; + $manifest = TestManifestFactory::makeManifest('default', $suiteConfig); // parse and generate suite object with mocked data and manifest $mockSuiteGenerator = SuiteGenerator::getInstance(); $mockSuiteGenerator->generateAllSuites($manifest); + + // assert last split suite group generated + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'info', + 'suite generated', + ['suite' => 'mockSuite_1_G', 'relative_path' => '_generated' . DIRECTORY_SEPARATOR . 'mockSuite_1_G'] + ); } /** * Function used to set mock for parser return and force init method to run between tests. * * @param array $testData - * @throws \Exception + * @param array $suiteData + * + * @return void + * @throws Exception */ - private function setMockTestAndSuiteParserOutput($testData, $suiteData) + private function setMockTestAndSuiteParserOutput(array $testData, array $suiteData): void { - $property = new \ReflectionProperty(SuiteGenerator::class, 'instance'); + $this->clearMockResolverProperties(); + $mockSuiteGeneratorService = $this->createMock(SuiteGeneratorService::class); + $mockVoidReturnCallback = function () {};// phpcs:ignore + + $mockSuiteGeneratorService + ->method('clearPreviousSessionConfigEntries') + ->will($this->returnCallback($mockVoidReturnCallback)); + + $mockSuiteGeneratorService + ->method('appendEntriesToConfig') + ->will($this->returnCallback($mockVoidReturnCallback)); + + $mockSuiteGeneratorService + ->method('generateRelevantGroupTests') + ->will($this->returnCallback($mockVoidReturnCallback)); + + $suiteGeneratorServiceProperty = new ReflectionProperty(SuiteGeneratorService::class, 'INSTANCE'); + $suiteGeneratorServiceProperty->setAccessible(true); + $suiteGeneratorServiceProperty->setValue(null, $mockSuiteGeneratorService); + + $mockDataParser = $this->createMock(TestDataParser::class); + $mockDataParser + ->method('readTestData') + ->willReturn($testData); + + $mockSuiteDataParser = $this->createMock(SuiteDataParser::class); + $mockSuiteDataParser + ->method('readSuiteData') + ->willReturn($suiteData); + + $mockGroupClass = $this->createMock(GroupClassGenerator::class); + $mockGroupClass + ->method('generateGroupClass') + ->willReturn('namespace'); + + $objectManager = ObjectManagerFactory::getObjectManager(); + $objectManagerMockInstance = $this->createMock(ObjectManager::class); + $objectManagerMockInstance + ->method('create') + ->will( + $this->returnCallback( + function ( + string $class, + array $arguments = [] + ) use ( + $mockDataParser, + $mockSuiteDataParser, + $mockGroupClass, + $objectManager + ) { + if ($class === TestDataParser::class) { + return $mockDataParser; + } + if ($class === SuiteDataParser::class) { + return $mockSuiteDataParser; + } + if ($class === GroupClassGenerator::class) { + return $mockGroupClass; + } + + return $objectManager->create($class, $arguments); + } + ) + ); + + $objectManagerProperty = new ReflectionProperty(ObjectManager::class, 'instance'); + $objectManagerProperty->setAccessible(true); + $objectManagerProperty->setValue(null, $objectManagerMockInstance); + } + + /** + * Function used to clear mock properties. + * + * @return void + */ + private function clearMockResolverProperties(): void + { + $property = new ReflectionProperty(SuiteGenerator::class, 'instance'); $property->setAccessible(true); - $property->setValue(null); + $property->setValue(null, null); // clear test object handler value to inject parsed content - $property = new \ReflectionProperty(TestObjectHandler::class, 'testObjectHandler'); + $property = new ReflectionProperty(TestObjectHandler::class, 'testObjectHandler'); $property->setAccessible(true); - $property->setValue(null); + $property->setValue(null, null); // clear suite object handler value to inject parsed content - $property = new \ReflectionProperty(SuiteObjectHandler::class, 'instance'); - $property->setAccessible(true); - $property->setValue(null); - - $mockDataParser = AspectMock::double(TestDataParser::class, ['readTestData' => $testData])->make(); - $mockSuiteDataParser = AspectMock::double(SuiteDataParser::class, ['readSuiteData' => $suiteData])->make(); - $mockGroupClass = AspectMock::double( - GroupClassGenerator::class, - ['generateGroupClass' => 'namespace'] - )->make(); - $mockSuiteClass = AspectMock::double(SuiteGenerator::class, ['generateRelevantGroupTests' => null])->make(); - $instance = AspectMock::double( - ObjectManager::class, - ['create' => function ($clazz) use ( - $mockDataParser, - $mockSuiteDataParser, - $mockGroupClass, - $mockSuiteClass - ) { - if ($clazz == TestDataParser::class) { - return $mockDataParser; - } - if ($clazz == SuiteDataParser::class) { - return $mockSuiteDataParser; - } - if ($clazz == GroupClassGenerator::class) { - return $mockGroupClass; - } - if ($clazz == SuiteGenerator::class) { - return $mockSuiteClass; - } - }] - )->make(); - // bypass the private constructor - AspectMock::double(ObjectManagerFactory::class, ['getObjectManager' => $instance]); - - $property = new \ReflectionProperty(SuiteGenerator::class, 'groupClassGenerator'); + $property = new ReflectionProperty(SuiteObjectHandler::class, 'instance'); $property->setAccessible(true); - $property->setValue($instance, $instance); + $property->setValue(null, null); } /** - * clean up function runs after all tests + * @inheritDoc */ - public static function tearDownAfterClass() + protected function tearDown(): void + { + GenerationErrorHandler::getInstance()->reset(); + } + + /** + * @inheritDoc + */ + public static function tearDownAfterClass(): void { - TestLoggingUtil::getInstance()->clearMockLoggingUtil(); parent::tearDownAfterClass(); + + $objectManagerProperty = new ReflectionProperty(ObjectManager::class, 'instance'); + $objectManagerProperty->setAccessible(true); + $objectManagerProperty->setValue(null, null); + + $suiteGeneratorServiceProperty = new ReflectionProperty(SuiteGeneratorService::class, 'INSTANCE'); + $suiteGeneratorServiceProperty->setAccessible(true); + $suiteGeneratorServiceProperty->setValue(null, null); + + TestLoggingUtil::getInstance()->clearMockLoggingUtil(); } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Config/ActionGroupDomTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Config/ActionGroupDomTest.php index 6a0c926be..cf29b62b5 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Config/ActionGroupDomTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Config/ActionGroupDomTest.php @@ -3,12 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Tests\unit\Magento\FunctionalTestFramework\Test\Config; +namespace tests\unit\Magento\FunctionalTestFramework\Test\Config; use Magento\FunctionalTestingFramework\Exceptions\Collector\ExceptionCollector; use Magento\FunctionalTestingFramework\Config\Dom\ValidationException; use Magento\FunctionalTestingFramework\Test\Config\ActionGroupDom; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use tests\unit\Util\MagentoTestCase; class ActionGroupDomTest extends MagentoTestCase { @@ -65,7 +65,7 @@ public function testActionGroupDomDuplicateActionGroupsValidation() $exceptionCollector = new ExceptionCollector(); new ActionGroupDom($sampleXml, 'dupeNameActionGroup.xml', $exceptionCollector); $this->expectException(\Exception::class); - $this->expectExceptionMessageRegExp("/name: actionGroupName is used more than once./"); + $this->expectExceptionMessageMatches("/name: actionGroupName is used more than once./"); $exceptionCollector->throwException(); } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Config/DomTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Config/DomTest.php index e00926978..5601dce10 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Config/DomTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Config/DomTest.php @@ -3,12 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Tests\unit\Magento\FunctionalTestFramework\Test\Config; +namespace tests\unit\Magento\FunctionalTestFramework\Test\Config; use Magento\FunctionalTestingFramework\Exceptions\Collector\ExceptionCollector; use Magento\FunctionalTestingFramework\Config\Dom\ValidationException; use Magento\FunctionalTestingFramework\Test\Config\ActionGroupDom; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use tests\unit\Util\MagentoTestCase; class DomTest extends MagentoTestCase { @@ -28,7 +28,7 @@ public function testTestStepKeyDuplicateValidation() new ActionGroupDom($sampleXml, 'dupeStepKeyTest.xml', $exceptionCollector); $this->expectException(\Exception::class); - $this->expectExceptionMessageRegExp("/stepKey: key1 is used more than once. \(Parent: testName\)/"); + $this->expectExceptionMessageMatches("/stepKey: key1 is used more than once. \(Parent: testName\)/"); $exceptionCollector->throwException(); } @@ -49,7 +49,7 @@ public function testTestNameDuplicateValidation() $exceptionCollector = new ExceptionCollector(); new ActionGroupDom($sampleXml, 'dupeTestsTest.xml', $exceptionCollector); $this->expectException(\Exception::class); - $this->expectExceptionMessageRegExp("/name: testName is used more than once./"); + $this->expectExceptionMessageMatches("/name: testName is used more than once./"); $exceptionCollector->throwException(); } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Handlers/ActionGroupObjectHandlerTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Handlers/ActionGroupObjectHandlerTest.php index 4a7c6101e..ce5983f62 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Handlers/ActionGroupObjectHandlerTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Handlers/ActionGroupObjectHandlerTest.php @@ -3,27 +3,32 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); -namespace Tests\unit\Magento\FunctionalTestFramework\Test\Handlers; +namespace tests\unit\Magento\FunctionalTestFramework\Test\Handlers; -use AspectMock\Test as AspectMock; - -use Go\Aop\Aspect; +use Exception; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; use Magento\FunctionalTestingFramework\ObjectManager; use Magento\FunctionalTestingFramework\ObjectManagerFactory; use Magento\FunctionalTestingFramework\Test\Handlers\ActionGroupObjectHandler; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; -use tests\unit\Util\ActionGroupArrayBuilder; use Magento\FunctionalTestingFramework\Test\Parsers\ActionGroupDataParser; +use ReflectionProperty; +use tests\unit\Util\ActionGroupArrayBuilder; +use tests\unit\Util\MagentoTestCase; +/** + * Class ActionGroupObjectHandlerTest + */ class ActionGroupObjectHandlerTest extends MagentoTestCase { /** - * getObject should throw exception if test extends from itself + * Validate getObject should throw exception if test extends from itself. * - * @throws \Exception + * @return void + * @throws Exception */ - public function testGetTestObjectWithInvalidExtends() + public function testGetTestObjectWithInvalidExtends(): void { // Set up action group data $nameOne = 'actionGroupOne'; @@ -34,21 +39,22 @@ public function testGetTestObjectWithInvalidExtends() ->withFilename() ->withActionObjects() ->build(); - $this->setMockParserOutput(['actionGroups' => $actionGroupOne]); + $this->mockActionGroupObjectHandlerWithData(['actionGroups' => $actionGroupOne]); $handler = ActionGroupObjectHandler::getInstance(); - $this->expectException(\Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException::class); - $this->expectExceptionMessage("Mftf Action Group can not extend from itself: " . $nameOne); + $this->expectException(TestFrameworkException::class); + $this->expectExceptionMessage('Mftf Action Group can not extend from itself: ' . $nameOne); $handler->getObject('actionGroupOne'); } /** - * getAllObjects should throw exception if test extends from itself + * Validate getAllObjects should throw exception if test extends from itself. * - * @throws \Exception + * @return void + * @throws Exception */ - public function testGetAllTestObjectsWithInvalidExtends() + public function testGetAllTestObjectsWithInvalidExtends(): void { // Set up action group data $nameOne = 'actionGroupOne'; @@ -68,31 +74,79 @@ public function testGetAllTestObjectsWithInvalidExtends() ->withActionObjects() ->build(); - $this->setMockParserOutput(['actionGroups' => array_merge($actionGroupOne, $actionGroupTwo)]); + $this->mockActionGroupObjectHandlerWithData( + [ + 'actionGroups' => array_merge( + $actionGroupOne, + $actionGroupTwo + ) + ] + ); $handler = ActionGroupObjectHandler::getInstance(); - $this->expectException(\Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException::class); - $this->expectExceptionMessage("Mftf Action Group can not extend from itself: " . $nameOne); + $this->expectException(TestFrameworkException::class); + $this->expectExceptionMessage('Mftf Action Group can not extend from itself: ' . $nameOne); $handler->getAllObjects(); } /** - * Function used to set mock for parser return and force init method to run between tests. + * Create mock action group object handler with data. + * + * @param array $mockData * - * @param array $data - * @throws \Exception + * @return void */ - private function setMockParserOutput($data) + private function mockActionGroupObjectHandlerWithData(array $mockData): void { - // Clear action group object handler value to inject parsed content - $property = new \ReflectionProperty(ActionGroupObjectHandler::class, 'instance'); + $actionGroupObjectHandlerProperty = new ReflectionProperty(ActionGroupObjectHandler::class, 'instance'); + $actionGroupObjectHandlerProperty->setAccessible(true); + $actionGroupObjectHandlerProperty->setValue(null, null); + + $mockOperationParser = $this->createMock(ActionGroupDataParser::class); + $mockOperationParser + ->method('readActionGroupData') + ->willReturn($mockData); + $objectManager = ObjectManagerFactory::getObjectManager(); + $mockObjectManagerInstance = $this->createMock(ObjectManager::class); + $mockObjectManagerInstance + ->method('create') + ->will( + $this->returnCallback( + function ( + string $class, + array $arguments = [] + ) use ( + $objectManager, + $mockOperationParser + ) { + if ($class === ActionGroupDataParser::class) { + return $mockOperationParser; + } + + return $objectManager->create($class, $arguments); + } + ) + ); + + $property = new ReflectionProperty(ObjectManager::class, 'instance'); $property->setAccessible(true); - $property->setValue(null); + $property->setValue(null, $mockObjectManagerInstance); + } + + /** + * @inheritDoc + */ + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + + $actionGroupObjectHandlerProperty = new ReflectionProperty(ActionGroupObjectHandler::class, 'instance'); + $actionGroupObjectHandlerProperty->setAccessible(true); + $actionGroupObjectHandlerProperty->setValue(null, null); - $mockDataParser = AspectMock::double(ActionGroupDataParser::class, ['readActionGroupData' => $data])->make(); - $instance = AspectMock::double(ObjectManager::class, ['create' => $mockDataParser]) - ->make(); // bypass the private constructor - AspectMock::double(ObjectManagerFactory::class, ['getObjectManager' => $instance]); + $objectManagerProperty = new ReflectionProperty(ObjectManager::class, 'instance'); + $objectManagerProperty->setAccessible(true); + $objectManagerProperty->setValue(null, null); } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Handlers/TestObjectHandlerTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Handlers/TestObjectHandlerTest.php index a4504ab07..05c293e7d 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Handlers/TestObjectHandlerTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Handlers/TestObjectHandlerTest.php @@ -3,11 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); -namespace Tests\unit\Magento\FunctionalTestFramework\Test\Handlers; - -use AspectMock\Test as AspectMock; +namespace tests\unit\Magento\FunctionalTestFramework\Test\Handlers; +use Exception; +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; use Magento\FunctionalTestingFramework\ObjectManager; use Magento\FunctionalTestingFramework\ObjectManagerFactory; use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; @@ -16,18 +18,35 @@ use Magento\FunctionalTestingFramework\Test\Objects\TestObject; use Magento\FunctionalTestingFramework\Test\Parsers\TestDataParser; use Magento\FunctionalTestingFramework\Test\Util\TestObjectExtractor; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use Magento\FunctionalTestingFramework\Util\GenerationErrorHandler; +use Magento\FunctionalTestingFramework\Util\ModuleResolver; +use ReflectionProperty; +use tests\unit\Util\MagentoTestCase; use tests\unit\Util\TestDataArrayBuilder; -use tests\unit\Util\MockModuleResolverBuilder; +use tests\unit\Util\TestLoggingUtil; +use Magento\FunctionalTestingFramework\Filter\FilterList; +use Magento\FunctionalTestingFramework\Util\Script\TestDependencyUtil; class TestObjectHandlerTest extends MagentoTestCase { + /** + * Before test functionality. + * + * @return void + * @throws Exception + */ + protected function setUp(): void + { + TestLoggingUtil::getInstance()->setMockLoggingUtil(); + } + /** * Basic test to validate array => test object conversion. * - * @throws \Exception + * @return void + * @throws Exception */ - public function testGetTestObject() + public function testGetTestObject(): void { // set up mock data $testDataArrayBuilder = new TestDataArrayBuilder(); @@ -39,13 +58,10 @@ public function testGetTestObject() ->withTestActions() ->build(); - $resolverMock = new MockModuleResolverBuilder(); - $resolverMock->setup(); - $this->setMockParserOutput($mockData); + $this->mockTestObjectHandler($mockData); // run object handler method $toh = TestObjectHandler::getInstance(); - $mockConfig = AspectMock::double(TestObjectHandler::class, ['initTestData' => false]); $actualTestObject = $toh->getObject($testDataArrayBuilder->testName); // perform asserts @@ -78,7 +94,7 @@ public function testGetTestObject() $expectedFailedHookObject = new TestHookObject( TestObjectExtractor::TEST_FAILED_HOOK, $testDataArrayBuilder->testName, - [$expectedFailedActionObject] + ["saveScreenshot" => $expectedFailedActionObject] ); $expectedTestActionObject = new ActionObject( @@ -106,20 +122,22 @@ public function testGetTestObject() } /** - * Tests basic getting of a test that has a fileName + * Tests basic getting of a test that has a fileName. + * + * @return void */ - public function testGetTestWithFileName() + public function testGetTestWithFileName(): void { - $this->markTestIncomplete(); - //TODO + $this->markTestIncomplete('TODO'); } /** * Tests the function used to get a series of relevant tests by group. * - * @throws \Exception + * @return void + * @throws Exception */ - public function testGetTestsByGroup() + public function testGetTestsByGroup(): void { // set up mock data with Exclude Test $includeTest = (new TestDataArrayBuilder()) @@ -133,9 +151,7 @@ public function testGetTestsByGroup() ->withTestActions() ->build(); - $resolverMock = new MockModuleResolverBuilder(); - $resolverMock->setup(); - $this->setMockParserOutput(array_merge($includeTest, $excludeTest)); + $this->mockTestObjectHandler(array_merge($includeTest, $excludeTest)); // execute test method $toh = TestObjectHandler::getInstance(); @@ -148,11 +164,12 @@ public function testGetTestsByGroup() } /** - * Tests the function used to parse and determine a test's Module (used in allure Features annotation) + * Tests the function used to parse and determine a test's Module (used in allure Features annotation). * - * @throws \Exception + * @return void + * @throws Exception */ - public function testGetTestWithModuleName() + public function testGetTestWithModuleName(): void { // set up Test Data $moduleExpected = "SomeModuleName"; @@ -181,10 +198,8 @@ public function testGetTestWithModuleName() ->withFileName($file) ->build(); - $resolverMock = new MockModuleResolverBuilder(); - $resolverMock->setup(['Vendor_' . $moduleExpected => $filepath]); + $this->mockTestObjectHandler($mockData, ['Vendor_' . $moduleExpected => $filepath]); - $this->setMockParserOutput($mockData); // Execute Test Method $toh = TestObjectHandler::getInstance(); $actualTestObject = $toh->getObject($testDataArrayBuilder->testName); @@ -194,11 +209,12 @@ public function testGetTestWithModuleName() } /** - * getObject should throw exception if test extends from itself + * getObject should throw exception if test extends from itself. * - * @throws \Exception + * @return void + * @throws Exception */ - public function testGetTestObjectWithInvalidExtends() + public function testGetTestObjectWithInvalidExtends(): void { // set up Test Data $testOne = (new TestDataArrayBuilder()) @@ -210,24 +226,23 @@ public function testGetTestObjectWithInvalidExtends() ->withBeforeHook() ->withTestActions() ->build(); - $resolverMock = new MockModuleResolverBuilder(); - $resolverMock->setup(); - $this->setMockParserOutput($testOne); - $toh = TestObjectHandler::getInstance(); + $this->mockTestObjectHandler($testOne); - $this->expectException(\Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException::class); + $toh = TestObjectHandler::getInstance(); + $this->expectException(TestFrameworkException::class); $this->expectExceptionMessage("Mftf Test can not extend from itself: " . "testOne"); $toh->getObject('testOne'); } /** - * getAllObjects should throw exception if test extends from itself + * getAllObjects should throw exception if test extends from itself. * - * @throws \Exception + * @return void + * @throws Exception */ - public function testGetAllTestObjectsWithInvalidExtends() + public function testGetAllTestObjectsWithInvalidExtends(): void { // set up Test Data $testOne = (new TestDataArrayBuilder()) @@ -248,43 +263,241 @@ public function testGetAllTestObjectsWithInvalidExtends() ->withTestActions() ->build(); - $resolverMock = new MockModuleResolverBuilder(); - $resolverMock->setup(); - $this->setMockParserOutput(array_merge($testOne, $testTwo)); + $this->mockTestObjectHandler(array_merge($testOne, $testTwo)); $toh = TestObjectHandler::getInstance(); - - $this->expectException(\Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException::class); - $this->expectExceptionMessage("Mftf Test can not extend from itself: " . "testOne"); $toh->getAllObjects(); + + // assert that no exception for getAllObjects and test generation error is stored in GenerationErrorHandler + $errorMessage = '/' . preg_quote("Mftf Test can not extend from itself: " . "testOne") . '/'; + TestLoggingUtil::getInstance()->validateMockLogStatmentRegex('error', $errorMessage, []); + $testErrors = GenerationErrorHandler::getInstance()->getErrorsByType('test'); + $this->assertArrayHasKey('testOne', $testErrors); } /** - * Function used to set mock for parser return and force init method to run between tests. + * Validate test object when ENABLE_PAUSE is set to true. + * + * @return void + * @throws Exception + */ + public function testGetTestObjectWhenEnablePause(): void + { + // set up mock data + putenv('ENABLE_PAUSE=true'); + $testDataArrayBuilder = new TestDataArrayBuilder(); + $mockData = $testDataArrayBuilder + ->withAnnotations() + ->withFailedHook() + ->withAfterHook() + ->withBeforeHook() + ->withTestActions() + ->build(); + + $this->mockTestObjectHandler($mockData); + + // run object handler method + $toh = TestObjectHandler::getInstance(); + $actualTestObject = $toh->getObject($testDataArrayBuilder->testName); + + // perform asserts + $expectedBeforeActionObject = new ActionObject( + $testDataArrayBuilder->testActionBeforeName, + $testDataArrayBuilder->testActionType, + [] + ); + $expectedAfterActionObject = new ActionObject( + $testDataArrayBuilder->testActionAfterName, + $testDataArrayBuilder->testActionType, + [] + ); + $expectedFailedActionObject1 = new ActionObject( + 'saveScreenshot', + 'saveScreenshot', + [] + ); + $expectedFailedActionObject2 = new ActionObject( + 'pauseWhenFailed', + 'pause', + [ActionObject::PAUSE_ACTION_INTERNAL_ATTRIBUTE => true] + ); + + $expectedBeforeHookObject = new TestHookObject( + TestObjectExtractor::TEST_BEFORE_HOOK, + $testDataArrayBuilder->testName, + ["testActionBefore" => $expectedBeforeActionObject] + ); + $expectedAfterHookObject = new TestHookObject( + TestObjectExtractor::TEST_AFTER_HOOK, + $testDataArrayBuilder->testName, + ["testActionAfter" => $expectedAfterActionObject] + ); + $expectedFailedHookObject = new TestHookObject( + TestObjectExtractor::TEST_FAILED_HOOK, + $testDataArrayBuilder->testName, + [ + "saveScreenshot" => $expectedFailedActionObject1, + "pauseWhenFailed" => $expectedFailedActionObject2, + ] + ); + + $expectedTestActionObject = new ActionObject( + $testDataArrayBuilder->testTestActionName, + $testDataArrayBuilder->testActionType, + [] + ); + $expectedTestObject = new TestObject( + $testDataArrayBuilder->testName, + ["testActionInTest" => $expectedTestActionObject], + [ + 'features' => ['NO MODULE DETECTED'], + 'group' => ['test'], + 'description' => ['test_files' => '

Test files

', 'deprecated' => []] + ], + [ + TestObjectExtractor::TEST_BEFORE_HOOK => $expectedBeforeHookObject, + TestObjectExtractor::TEST_AFTER_HOOK => $expectedAfterHookObject, + TestObjectExtractor::TEST_FAILED_HOOK => $expectedFailedHookObject + ], + null + ); + + $this->assertEquals($expectedTestObject, $actualTestObject); + putenv('ENABLE_PAUSE'); + } + + /** + * After method functionality. + * + * @return void + */ + protected function tearDown(): void + { + TestLoggingUtil::getInstance()->clearMockLoggingUtil(); + parent::tearDownAfterClass(); + } + + /** + * Mock test object handler. * * @param array $data - * @throws \Exception + * @param array|null $paths + * + * @return void */ - private function setMockParserOutput($data) + private function mockTestObjectHandler(array $data, ?array $paths = null): void { + if (!$paths) { + $paths = ['Magento_Module' => '/base/path/some/other/path/Magento/Module']; + } // clear test object handler value to inject parsed content - $property = new \ReflectionProperty(TestObjectHandler::class, 'testObjectHandler'); + $property = new ReflectionProperty(TestObjectHandler::class, 'testObjectHandler'); $property->setAccessible(true); - $property->setValue(null); + $property->setValue(null, null); + + $mockDataParser = $this->createMock(TestDataParser::class); + $mockDataParser + ->method('readTestData') + ->willReturn($data); + + $mockConfig = $this->createMock(MftfApplicationConfig::class); + $mockConfig + ->method('forceGenerateEnabled') + ->willReturn(false); + + $mockResolver = $this->createMock(ModuleResolver::class); + $mockResolver + ->method('getEnabledModules') + ->willReturn([]); + + $objectManager = ObjectManagerFactory::getObjectManager(); + $objectManagerMockInstance = $this->createMock(ObjectManager::class); + $objectManagerMockInstance + ->method('create') + ->will( + $this->returnCallback( + function ( + $class, + $arguments = [] + ) use ( + $objectManager, + $mockDataParser, + $mockConfig, + $mockResolver + ) { + if ($class === TestDataParser::class) { + return $mockDataParser; + } + if ($class === MftfApplicationConfig::class) { + return $mockConfig; + } + if ($class === ModuleResolver::class) { + return $mockResolver; + } + + return $objectManager->create($class, $arguments); + } + ) + ); + + $objectManagerProperty = new ReflectionProperty(ObjectManager::class, 'instance'); + $objectManagerProperty->setAccessible(true); + $objectManagerProperty->setValue(null, $objectManagerMockInstance); - $mockDataParser = AspectMock::double(TestDataParser::class, ['readTestData' => $data])->make(); - $instance = AspectMock::double(ObjectManager::class, ['create' => $mockDataParser]) - ->make(); // bypass the private constructor - AspectMock::double(ObjectManagerFactory::class, ['getObjectManager' => $instance]); + $resolver = ModuleResolver::getInstance(); + $property = new ReflectionProperty(ModuleResolver::class, 'enabledModuleNameAndPaths'); + $property->setAccessible(true); + $property->setValue($resolver, $paths); + } + + /** + * Basic test for exclude group Filter + * + * @return void + * @throws Exception + */ + public function testGetFilteredTestNamesWhenExcludeGroupFilterIsApplied() + { + $fileList = new FilterList(['excludeGroup' => ['test']]); + $toh = TestObjectHandler::getInstance()->getAllObjects(); + $testDependencyUtil = new TestDependencyUtil(); + $result = $testDependencyUtil->getFilteredTestNames($toh, $fileList->getFilters()); + $this->assertIsArray($result); + $this->assertEquals(count($result), 0); + } + + /** + * Basic test for include group Filter + * + * @return void + * @throws Exception + */ + public function testGetFilteredTestNamesWhenIncludeGroupFilterIsApplied() + { + $fileList = new FilterList(['includeGroup' => ['test']]); + $toh = TestObjectHandler::getInstance()->getAllObjects(); + $testDependencyUtil = new TestDependencyUtil(); + $result = $testDependencyUtil->getFilteredTestNames($toh, $fileList->getFilters()); + $this->assertIsArray($result); + $this->assertEquals(count($result), 1); + $this->assertEquals($result['testTest'], 'testTest'); } /** - * After method functionality + * Basic test when no filter applied * * @return void + * @throws Exception */ - public function tearDown() + public function testGetFilteredTestNamesWhenNoFilterIsApplied() { - AspectMock::clean(); + $fileList = new FilterList(); + $toh = TestObjectHandler::getInstance()->getAllObjects(); + $testDependencyUtil = new TestDependencyUtil(); + $result = $testDependencyUtil->getFilteredTestNames($toh, $fileList->getFilters()); + $this->assertIsArray($result); + $this->assertEquals(count($result), 1); + //returns all test Names + $this->assertEquals($result['testTest'], 'testTest'); } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Objects/ActionGroupObjectTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Objects/ActionGroupObjectTest.php index 796e8a28b..1389c0254 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Objects/ActionGroupObjectTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Objects/ActionGroupObjectTest.php @@ -3,10 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace tests\unit\Magento\FunctionalTestFramework\Test\Objects; -use AspectMock\Test as AspectMock; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; use Magento\FunctionalTestingFramework\Page\Handlers\SectionObjectHandler; @@ -15,9 +15,10 @@ use Magento\FunctionalTestingFramework\Test\Objects\ActionGroupObject; use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; use Magento\FunctionalTestingFramework\Test\Objects\ArgumentObject; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use ReflectionProperty; use tests\unit\Util\ActionGroupObjectBuilder; use tests\unit\Util\EntityDataObjectBuilder; +use tests\unit\Util\MagentoTestCase; use tests\unit\Util\TestLoggingUtil; class ActionGroupObjectTest extends MagentoTestCase @@ -25,18 +26,22 @@ class ActionGroupObjectTest extends MagentoTestCase const ACTION_GROUP_MERGE_KEY = 'TestKey'; /** - * Before test functionality + * Before test functionality. + * * @return void */ - public function setUp() + protected function setUp(): void { TestLoggingUtil::getInstance()->setMockLoggingUtil(); } /** - * Tests a string literal in an action group + * Tests a string literal in an action group. + * + * @return void + * @throws TestReferenceException */ - public function testGetStepsWithDefaultCase() + public function testGetStepsWithDefaultCase(): void { $entity = (new EntityDataObjectBuilder()) ->withDataFields(['field1' => 'testValue']) @@ -44,126 +49,187 @@ public function testGetStepsWithDefaultCase() $this->setEntityObjectHandlerReturn($entity); $actionGroupUnderTest = (new ActionGroupObjectBuilder())->build(); $steps = $actionGroupUnderTest->getSteps(null, self::ACTION_GROUP_MERGE_KEY); - $this->assertOnMergeKeyAndActionValue($steps, ['userInput' => 'literal']); + $this->assertOnMergeKeyAndActionValue($steps, ['userInput' => 'literal', 'requiredCredentials' => '']); } /** - * Tests a data reference in an action group, replaced by the user + * Tests a data reference in an action group, replaced by the user. + * + * @return void + * @throws TestReferenceException */ - public function testGetStepsWithCustomArgs() + public function testGetStepsWithCustomArgs(): void { $this->setEntityObjectHandlerReturn(function ($entityName) { - if ($entityName == "data2") { + if ($entityName === 'data2') { return (new EntityDataObjectBuilder())->withDataFields(['field2' => 'testValue2'])->build(); } }); $actionGroupUnderTest = (new ActionGroupObjectBuilder()) - ->withActionObjects([new ActionObject('action1', 'testAction', ['userInput' => '{{arg1.field2}}'])]) - ->withArguments([new ArgumentObject('arg1', null, 'entity')]) - ->build(); + ->withActionObjects([new ActionObject( + 'action1', + 'testAction', + [ + 'userInput' => '{{arg1.field2}}','requiredCredentials' => '' + ] + )]) + ->withArguments([new ArgumentObject('arg1', null, 'entity')]) + ->build(); $steps = $actionGroupUnderTest->getSteps(['arg1' => 'data2'], self::ACTION_GROUP_MERGE_KEY); - $this->assertOnMergeKeyAndActionValue($steps, ['userInput' => 'testValue2']); + $this->assertOnMergeKeyAndActionValue($steps, ['userInput' => 'testValue2','requiredCredentials' => '']); // entity.field as argument $actionGroupUnderTest = (new ActionGroupObjectBuilder()) - ->withActionObjects([new ActionObject('action1', 'testAction', ['userInput' => '{{arg1}}'])]) + ->withActionObjects([new ActionObject( + 'action1', + 'testAction', + ['userInput' => '{{arg1}}', + 'requiredCredentials' => '' + ] + )]) ->withArguments([new ArgumentObject('arg1', null, 'entity')]) ->build(); $steps = $actionGroupUnderTest->getSteps(['arg1' => 'data2.field2'], self::ACTION_GROUP_MERGE_KEY); - $this->assertOnMergeKeyAndActionValue($steps, ['userInput' => 'testValue2']); + $this->assertOnMergeKeyAndActionValue($steps, ['userInput' => 'testValue2', 'requiredCredentials' => '']); // String Data $actionGroupUnderTest = (new ActionGroupObjectBuilder()) - ->withActionObjects([new ActionObject('action1', 'testAction', ['userInput' => '{{simple}}'])]) + ->withActionObjects([new ActionObject( + 'action1', + 'testAction', + ['userInput' => '{{simple}}', + 'requiredCredentials' => '' + ] + )]) ->withArguments([new ArgumentObject('simple', null, 'string')]) ->build(); $steps = $actionGroupUnderTest->getSteps(['simple' => 'override'], self::ACTION_GROUP_MERGE_KEY); - $this->assertOnMergeKeyAndActionValue($steps, ['userInput' => 'override']); + $this->assertOnMergeKeyAndActionValue($steps, ['userInput' => 'override', 'requiredCredentials' => '']); } /** * Tests a data reference in an action group replaced with a persisted reference. + * + * @return void + * @throws TestReferenceException */ - public function testGetStepsWithPersistedArgs() + public function testGetStepsWithPersistedArgs(): void { $actionGroupUnderTest = (new ActionGroupObjectBuilder()) - ->withActionObjects([new ActionObject('action1', 'testAction', ['userInput' => '{{arg1.field2}}'])]) + ->withActionObjects([new ActionObject( + 'action1', + 'testAction', + ['userInput' => '{{arg1.field2}}', + 'requiredCredentials' => ''] + )]) ->withArguments([new ArgumentObject('arg1', null, 'entity')]) ->build(); $steps = $actionGroupUnderTest->getSteps(['arg1' => '$data3$'], self::ACTION_GROUP_MERGE_KEY); - $this->assertOnMergeKeyAndActionValue($steps, ['userInput' => '$data3.field2$']); + $this->assertOnMergeKeyAndActionValue($steps, ['userInput' => '$data3.field2$','requiredCredentials' => '']); // Simple Data $actionGroupUnderTest = (new ActionGroupObjectBuilder()) - ->withActionObjects([new ActionObject('action1', 'testAction', ['userInput' => '{{simple}}'])]) + ->withActionObjects([new ActionObject( + 'action1', + 'testAction', + ['userInput' => '{{simple}}', + 'requiredCredentials' => '' + ] + )]) ->withArguments([new ArgumentObject('simple', null, 'string')]) ->build(); $steps = $actionGroupUnderTest->getSteps(['simple' => '$data3.field2$'], self::ACTION_GROUP_MERGE_KEY); - $this->assertOnMergeKeyAndActionValue($steps, ['userInput' => '$data3.field2$']); + $this->assertOnMergeKeyAndActionValue($steps, ['userInput' => '$data3.field2$','requiredCredentials' => '']); } /** * Tests a data reference in an action group replaced with a data.field reference. + * + * @return void + * @throws TestReferenceException */ - public function testGetStepsWithNoFieldArg() + public function testGetStepsWithNoFieldArg(): void { $this->setEntityObjectHandlerReturn(function ($entityName) { - if ($entityName == "data2") { + if ($entityName === 'data2') { return (new EntityDataObjectBuilder())->withDataFields(['field2' => 'testValue2'])->build(); } }); $actionGroupUnderTest = (new ActionGroupObjectBuilder()) - ->withActionObjects([new ActionObject('action1', 'testAction', ['userInput' => '{{arg1}}'])]) + ->withActionObjects([new ActionObject( + 'action1', + 'testAction', + ['userInput' => '{{arg1}}', + 'requiredCredentials' => '' + ] + )]) ->withArguments([new ArgumentObject('arg1', null, 'entity')]) ->build(); $steps = $actionGroupUnderTest->getSteps(['arg1' => 'data2.field2'], self::ACTION_GROUP_MERGE_KEY); - $this->assertOnMergeKeyAndActionValue($steps, ['userInput' => 'testValue2']); + $this->assertOnMergeKeyAndActionValue($steps, ['userInput' => 'testValue2','requiredCredentials' => '']); } /** * Tests a data reference in an action group resolved with its default state. + * + * @return void + * @throws TestReferenceException */ - public function testGetStepsWithNoArgs() + public function testGetStepsWithNoArgs(): void { $this->setEntityObjectHandlerReturn(function ($entityName) { - if ($entityName == "data1") { + if ($entityName === 'data1') { return (new EntityDataObjectBuilder())->withDataFields(['field1' => 'testValue'])->build(); } }); $actionGroupUnderTest = (new ActionGroupObjectBuilder()) - ->withActionObjects([new ActionObject('action1', 'testAction', ['userInput' => '{{data1.field1}}'])]) + ->withActionObjects([new ActionObject( + 'action1', + 'testAction', + ['userInput' => '{{data1.field1}}', + 'requiredCredentials' => '' + ] + )]) ->build(); $steps = $actionGroupUnderTest->getSteps(null, self::ACTION_GROUP_MERGE_KEY); - $this->assertOnMergeKeyAndActionValue($steps, ['userInput' => 'testValue']); + $this->assertOnMergeKeyAndActionValue($steps, ['userInput' => 'testValue','requiredCredentials' => '']); } /** * Tests a parameterized section reference in an action group resolved with user args. + * + * @return void + * @throws TestReferenceException */ - public function testGetStepsWithParameterizedArg() + public function testGetStepsWithParameterizedArg(): void { // Mock Entity Object Handler $this->setEntityObjectHandlerReturn(function ($entityName) { - if ($entityName == "data2") { + if ($entityName === 'data2') { return (new EntityDataObjectBuilder())->withDataFields(['field2' => 'testValue2'])->build(); } }); // mock the section object handler response $element = new ElementObject("element1", "textArea", ".selector {{var1}}", null, null, true); $section = new SectionObject("testSection", ["element1" => $element]); + $sectionInstance = $this->createMock(SectionObjectHandler::class); + $sectionInstance + ->method('getObject') + ->willReturn($section); // bypass the private constructor - $sectionInstance = AspectMock::double(SectionObjectHandler::class, ['getObject' => $section])->make(); - AspectMock::double(SectionObjectHandler::class, ['getInstance' => $sectionInstance]); + $property = new ReflectionProperty(SectionObjectHandler::class, 'INSTANCE'); + $property->setAccessible(true); + $property->setValue(null, $sectionInstance); $actionGroupUnderTest = (new ActionGroupObjectBuilder()) ->withActionObjects( @@ -174,30 +240,50 @@ public function testGetStepsWithParameterizedArg() // XML Data $steps = $actionGroupUnderTest->getSteps(['arg1' => 'data2'], self::ACTION_GROUP_MERGE_KEY); - $this->assertOnMergeKeyAndActionValue($steps, ['selector' => '.selector testValue2']); + $this->assertOnMergeKeyAndActionValue($steps, [ + 'selector' => '.selector testValue2', + 'requiredCredentials' => '' + ]); // Persisted Data - $steps = $actionGroupUnderTest->getSteps(['arg1' => '$data2$'], self::ACTION_GROUP_MERGE_KEY); - $this->assertOnMergeKeyAndActionValue($steps, ['selector' => '.selector $data2.field2$']); + $steps = $actionGroupUnderTest->getSteps( + ['arg1' => '$data2$'], + self::ACTION_GROUP_MERGE_KEY + ); + $this->assertOnMergeKeyAndActionValue( + $steps, + ['selector' => '.selector $data2.field2$', + 'requiredCredentials' => '' + ] + ); } /** * Tests a parameterized section reference in an action group resolved with user simpleArgs. + * + * @return void + * @throws TestReferenceException */ - public function testGetStepsWithParameterizedSimpleArg() + public function testGetStepsWithParameterizedSimpleArg(): void { // Mock Entity Object Handler $this->setEntityObjectHandlerReturn(function ($entityName) { - if ($entityName == "data2") { + if ($entityName === 'data2') { return (new EntityDataObjectBuilder())->withDataFields(['field2' => 'testValue2'])->build(); } }); // mock the section object handler response $element = new ElementObject("element1", "textArea", ".selector {{var1}}", null, null, true); $section = new SectionObject("testSection", ["element1" => $element]); + + $sectionInstance = $this->createMock(SectionObjectHandler::class); + $sectionInstance + ->method('getObject') + ->willReturn($section); // bypass the private constructor - $sectionInstance = AspectMock::double(SectionObjectHandler::class, ['getObject' => $section])->make(); - AspectMock::double(SectionObjectHandler::class, ['getInstance' => $sectionInstance]); + $property = new ReflectionProperty(SectionObjectHandler::class, 'INSTANCE'); + $property->setAccessible(true); + $property->setValue(null, $sectionInstance); $actionGroupUnderTest = (new ActionGroupObjectBuilder()) ->withActionObjects( @@ -208,21 +294,37 @@ public function testGetStepsWithParameterizedSimpleArg() // String Literal $steps = $actionGroupUnderTest->getSteps(['simple' => 'stringLiteral'], self::ACTION_GROUP_MERGE_KEY); - $this->assertOnMergeKeyAndActionValue($steps, ['selector' => '.selector stringLiteral']); + $this->assertOnMergeKeyAndActionValue($steps, [ + 'selector' => '.selector stringLiteral', + 'requiredCredentials' => '' + ]); // String Literal w/ data-like structure $steps = $actionGroupUnderTest->getSteps(['simple' => 'data2.field2'], self::ACTION_GROUP_MERGE_KEY); - $this->assertOnMergeKeyAndActionValue($steps, ['selector' => '.selector data2.field2']); + $this->assertOnMergeKeyAndActionValue( + $steps, + ['selector' => '.selector data2.field2', + 'requiredCredentials' => '' + ] + ); // Persisted Data $steps = $actionGroupUnderTest->getSteps(['simple' => '$someData.field1$'], self::ACTION_GROUP_MERGE_KEY); - $this->assertOnMergeKeyAndActionValue($steps, ['selector' => '.selector $someData.field1$']); + $this->assertOnMergeKeyAndActionValue( + $steps, + ['selector' => '.selector $someData.field1$', + 'requiredCredentials' => '' + ] + ); } /** * Tests a data reference in an action group resolved with a persisted reference used in another function. + * + * @return void + * @throws TestReferenceException */ - public function testGetStepsWithOuterScopePersistence() + public function testGetStepsWithOuterScopePersistence(): void { $actionGroupUnderTest = (new ActionGroupObjectBuilder()) ->withActionObjects([new ActionObject('action1', 'testAction', ['userInput' => '{{arg1.field1}}'])]) @@ -230,42 +332,53 @@ public function testGetStepsWithOuterScopePersistence() ->build(); $steps = $actionGroupUnderTest->getSteps(['arg1' => '$$someData$$'], self::ACTION_GROUP_MERGE_KEY); - $this->assertOnMergeKeyAndActionValue($steps, ['userInput' => '$$someData.field1$$']); + $this->assertOnMergeKeyAndActionValue( + $steps, + ['userInput' => '$$someData.field1$$', + 'requiredCredentials' => '' + ] + ); } /** * Tests an action group with mismatching args. + * + * @return void */ - public function testExceptionOnMissingActions() + public function testExceptionOnMissingActions(): void { $actionGroupUnderTest = (new ActionGroupObjectBuilder()) ->withArguments([new ArgumentObject('arg1', null, 'entity')]) ->build(); $this->expectException(TestReferenceException::class); - $this->expectExceptionMessageRegExp('/Arguments missed .* for actionGroup/'); + $this->expectExceptionMessageMatches('/Arguments missed .* for actionGroup/'); $actionGroupUnderTest->getSteps(['arg2' => 'data1'], self::ACTION_GROUP_MERGE_KEY); } /** * Tests an action group with missing args. + * + * @return void */ - public function testExceptionOnMissingArguments() + public function testExceptionOnMissingArguments(): void { $actionGroupUnderTest = (new ActionGroupObjectBuilder()) ->withArguments([new ArgumentObject('arg1', null, 'entity')]) ->build(); $this->expectException(TestReferenceException::class); - $this->expectExceptionMessageRegExp('/Arguments missed .* for actionGroup/'); + $this->expectExceptionMessageMatches('/Arguments missed .* for actionGroup/'); $actionGroupUnderTest->getSteps(null, self::ACTION_GROUP_MERGE_KEY); } /** - * Tests the stepKey replacement with "stepKey + invocationKey" process filter - * Specific to actions that make it past a "require stepKey replacement" filter + * Tests the stepKey replacement with "stepKey + invocationKey" process filter. + * Specific to actions that make it past a "require stepKey replacement" filter. + * + * @return void */ - public function testStepKeyReplacementFilteredIn() + public function testStepKeyReplacementFilteredIn(): void { $createStepKey = "createDataStepKey"; $updateStepKey = "updateDataStepKey"; @@ -293,10 +406,12 @@ public function testStepKeyReplacementFilteredIn() } /** - * Tests the stepKey replacement with "stepKey + invocationKey" process filter - * Specific to actions that make are removed by a "require stepKey replacement" filter + * Tests the stepKey replacement with "stepKey + invocationKey" process filter. + * Specific to actions that make are removed by a "require stepKey replacement" filter. + * + * @return void */ - public function testStepKeyReplacementFilteredOut() + public function testStepKeyReplacementFilteredOut(): void { $clickStepKey = "clickStepKey"; $fillFieldStepKey = "fillFieldStepKey"; @@ -322,13 +437,26 @@ public function testStepKeyReplacementFilteredOut() * duration of a single test case. * * @param mixed $return + * * @return void */ - private function setEntityObjectHandlerReturn($return) + private function setEntityObjectHandlerReturn($return): void { - $instance = AspectMock::double(DataObjectHandler::class, ['getObject' => $return]) - ->make(); // bypass the private constructor - AspectMock::double(DataObjectHandler::class, ['getInstance' => $instance]); + $instance = $this->createMock(DataObjectHandler::class); + + if (is_callable($return)) { + $instance + ->method('getObject') + ->will($this->returnCallback($return)); + } else { + $instance + ->method('getObject') + ->willReturn($return); + } + // bypass the private constructor + $property = new ReflectionProperty(DataObjectHandler::class, 'INSTANCE'); + $property->setAccessible(true); + $property->setValue(null, $instance); } /** @@ -337,11 +465,15 @@ private function setEntityObjectHandlerReturn($return) * * @param array $actions * @param array $expectedValue - * @param string $expectedMergeKey + * @param string|null $expectedMergeKey + * * @return void */ - private function assertOnMergeKeyAndActionValue($actions, $expectedValue, $expectedMergeKey = null) - { + private function assertOnMergeKeyAndActionValue( + array $actions, + array $expectedValue, + ?string $expectedMergeKey = null + ): void { $expectedMergeKey = $expectedMergeKey ?? ActionGroupObjectBuilder::DEFAULT_ACTION_OBJECT_NAME . self::ACTION_GROUP_MERGE_KEY; $this->assertArrayHasKey($expectedMergeKey, $actions); @@ -352,10 +484,11 @@ private function assertOnMergeKeyAndActionValue($actions, $expectedValue, $expec } /** - * After class functionality + * After class functionality. + * * @return void */ - public static function tearDownAfterClass() + public static function tearDownAfterClass(): void { TestLoggingUtil::getInstance()->clearMockLoggingUtil(); } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Objects/ActionObjectTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Objects/ActionObjectTest.php index 24a7e2ee6..76a0272e8 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Objects/ActionObjectTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Objects/ActionObjectTest.php @@ -3,21 +3,24 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace tests\unit\Magento\FunctionalTestFramework\Test\Objects; -use AspectMock\Test as AspectMock; +use Exception; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; +use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Page\Handlers\PageObjectHandler; +use Magento\FunctionalTestingFramework\Page\Handlers\SectionObjectHandler; use Magento\FunctionalTestingFramework\Page\Objects\ElementObject; use Magento\FunctionalTestingFramework\Page\Objects\PageObject; -use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; -use Magento\FunctionalTestingFramework\Page\Handlers\SectionObjectHandler; use Magento\FunctionalTestingFramework\Page\Objects\SectionObject; -use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; +use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; +use ReflectionProperty; +use tests\unit\Util\MagentoTestCase; use tests\unit\Util\TestLoggingUtil; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; /** * Class ActionObjectTest @@ -25,41 +28,51 @@ class ActionObjectTest extends MagentoTestCase { /** - * Before test functionality + * Before test functionality. + * * @return void */ - public function setUp() + protected function setUp(): void { TestLoggingUtil::getInstance()->setMockLoggingUtil(); } /** - * The order offset should be 0 when the action is instantiated with 'before' + * The order offset should be 0 when the action is instantiated with 'before'. + * + * @return void */ - public function testConstructOrderBefore() + public function testConstructOrderBefore(): void { $actionObject = new ActionObject('stepKey', 'type', [], null, 'before'); $this->assertEquals(0, $actionObject->getOrderOffset()); } /** - * The order offset should be 1 when the action is instantiated with 'after' + * The order offset should be 1 when the action is instantiated with 'after'. + * + * @return void */ - public function testConstructOrderAfter() + public function testConstructOrderAfter(): void { $actionObject = new ActionObject('stepKey', 'type', [], null, 'after'); $this->assertEquals(1, $actionObject->getOrderOffset()); } /** - * {{Section.element}} should be replaced with #theElementSelector + * {{Section.element}} should be replaced with #theElementSelector. + * + * @return void + * @throws TestReferenceException + * @throws XmlException */ - public function testResolveElementInSelector() + public function testResolveElementInSelector(): void { // Set up mocks $actionObject = new ActionObject('merge123', 'fillField', [ 'selector' => '{{SectionObject.elementObject}}', - 'userInput' => 'Hello world' + 'userInput' => 'Hello world', + 'requiredCredentials' => '' ]); $elementObject = new ElementObject('elementObject', 'button', '#replacementSelector', null, '42', false); $this->mockSectionHandlerWithElement($elementObject); @@ -70,19 +83,25 @@ public function testResolveElementInSelector() // Verify $expected = [ 'selector' => '#replacementSelector', - 'userInput' => 'Hello world' + 'userInput' => 'Hello world', + 'requiredCredentials' => '' ]; $this->assertEquals($expected, $actionObject->getCustomActionAttributes()); } /** - * {{Section.element(param)}} should replace correctly with 'stringLiterals' + * {{Section.element(param)}} should replace correctly with 'stringLiterals'. + * + * @return void + * @throws TestReferenceException + * @throws XmlException */ - public function testResolveSelectorWithOneStringLiteral() + public function testResolveSelectorWithOneStringLiteral(): void { $actionObject = new ActionObject('key123', 'fillField', [ 'selector' => "{{SectionObject.elementObject('stringliteral')}}", - 'userInput' => 'Input' + 'userInput' => 'Input', + 'requiredCredentials' => '' ]); $elementObject = new ElementObject('elementObject', 'button', '#{{var1}}', null, '42', true); $this->mockSectionHandlerWithElement($elementObject); @@ -93,19 +112,25 @@ public function testResolveSelectorWithOneStringLiteral() // Verify $expected = [ 'selector' => '#stringliteral', - 'userInput' => 'Input' + 'userInput' => 'Input', + 'requiredCredentials' => '' ]; $this->assertEquals($expected, $actionObject->getCustomActionAttributes()); } /** - * {{Section.element(param)}} should replace correctly with {{data.key}} references + * {{Section.element(param)}} should replace correctly with {{data.key}} references. + * + * @return void + * @throws TestReferenceException + * @throws XmlException */ - public function testResolveSelectorWithOneDataReference() + public function testResolveSelectorWithOneDataReference(): void { $actionObject = new ActionObject('key123', 'fillField', [ 'selector' => "{{SectionObject.elementObject(dataObject.key)}}", - 'userInput' => 'Input' + 'userInput' => 'Input', + 'requiredCredentials' => '' ]); // Mock SectionHandler @@ -122,19 +147,25 @@ public function testResolveSelectorWithOneDataReference() // Verify $expected = [ 'selector' => '#myValue', - 'userInput' => 'Input' + 'userInput' => 'Input', + 'requiredCredentials' => '' ]; $this->assertEquals($expected, $actionObject->getCustomActionAttributes()); } /** - * {{Section.element(param)}} should replace correctly with $data.key$ references + * {{Section.element(param)}} should replace correctly with $data.key$ references. + * + * @return void + * @throws TestReferenceException + * @throws XmlException */ - public function testResolveSelectorWithOnePersistedReference() + public function testResolveSelectorWithOnePersistedReference(): void { $actionObject = new ActionObject('key123', 'fillField', [ 'selector' => '{{SectionObject.elementObject($data.key$)}}', - 'userInput' => 'Input' + 'userInput' => 'Input', + 'requiredCredentials' => '' ]); // Mock SectionHandler @@ -147,15 +178,20 @@ public function testResolveSelectorWithOnePersistedReference() // Verify $expected = [ 'selector' => '#$data.key$', - 'userInput' => 'Input' + 'userInput' => 'Input', + 'requiredCredentials' => '' ]; $this->assertEquals($expected, $actionObject->getCustomActionAttributes()); } /** * {{Section.element(param1,param2,param3)}} should replace correctly with all 3 data types. + * + * @return void + * @throws TestReferenceException + * @throws XmlException */ - public function testResolveSelectorWithManyParams() + public function testResolveSelectorWithManyParams(): void { $actionObject = new ActionObject('key123', 'fillField', [ 'selector' => "{{SectionObject.elementObject('stringLiteral', data.key, \$data.key\$)}}", @@ -176,15 +212,20 @@ public function testResolveSelectorWithManyParams() // Verify $expected = [ 'selector' => '#stringLiteral[myValue,$data.key$]', - 'userInput' => 'Input' + 'userInput' => 'Input', + 'requiredCredentials' => '' ]; $this->assertEquals($expected, $actionObject->getCustomActionAttributes()); } /** - * Timeout property on the ActionObject should be set if the ElementObject has a timeout + * Timeout property on the ActionObject should be set if the ElementObject has a timeout. + * + * @return void + * @throws TestReferenceException + * @throws XmlException */ - public function testTimeoutFromElement() + public function testTimeoutFromElement(): void { // Set up mocks $actionObject = new ActionObject('merge123', 'click', [ @@ -201,37 +242,45 @@ public function testTimeoutFromElement() } /** - * {{PageObject.url}} should be replaced with someUrl.html + * {{PageObject.url}} should be replaced with someUrl.html. * - * @throws /Exception + * @return void + * @throws Exception */ - public function testResolveUrl() + public function testResolveUrl(): void { // Set up mocks $actionObject = new ActionObject('merge123', 'amOnPage', [ 'url' => '{{PageObject.url}}' ]); $pageObject = new PageObject('PageObject', '/replacement/url.html', 'Test', [], false, "test"); - $instance = AspectMock::double(PageObjectHandler::class, ['getObject' => $pageObject]) - ->make(); // bypass the private constructor - AspectMock::double(PageObjectHandler::class, ['getInstance' => $instance]); + + $instance = $this->createMock(PageObjectHandler::class); + $instance + ->method('getObject') + ->willReturn($pageObject); + // bypass the private constructor + $property = new ReflectionProperty(PageObjectHandler::class, 'INSTANCE'); + $property->setAccessible(true); + $property->setValue(null, $instance); // Call the method under test $actionObject->resolveReferences(); // Verify $expected = [ - 'url' => '/replacement/url.html' + 'url' => '/replacement/url.html','requiredCredentials' => '' ]; $this->assertEquals($expected, $actionObject->getCustomActionAttributes()); } /** - * {{PageObject}} should not be replaced and should elicit a warning in console + * {{PageObject}} should not be replaced and should elicit a warning in console. * - * @throws /Exception + * @return void + * @throws Exception */ - public function testResolveUrlWithNoAttribute() + public function testResolveUrlWithNoAttribute(): void { $this->expectException(TestReferenceException::class); @@ -241,41 +290,57 @@ public function testResolveUrlWithNoAttribute() ]); $pageObject = new PageObject('PageObject', '/replacement/url.html', 'Test', [], false, "test"); $pageObjectList = ["PageObject" => $pageObject]; - $instance = AspectMock::double( - PageObjectHandler::class, - ['getObject' => $pageObject, 'getAllObjects' => $pageObjectList] - )->make(); // bypass the private constructor - AspectMock::double(PageObjectHandler::class, ['getInstance' => $instance]); + + $instance = $this->createMock(PageObjectHandler::class); + $instance + ->method('getObject') + ->willReturn($pageObject); + $instance + ->method('getAllObjects') + ->willReturn($pageObjectList); + // bypass the private constructor + $property = new ReflectionProperty(PageObjectHandler::class, 'INSTANCE'); + $property->setAccessible(true); + $property->setValue(null, $instance); // Call the method under test $actionObject->resolveReferences(); } /** - * {{PageObject.url(param)}} should be replaced + * {{PageObject.url(param)}} should be replaced. + * + * @return void */ - public function testResolveUrlWithOneParam() + public function testResolveUrlWithOneParam(): void { $this->markTestIncomplete('TODO'); } /** - * {{PageObject.url(param1,param2,param3)}} should be replaced + * {{PageObject.url(param1,param2,param3)}} should be replaced. + * + * @return void */ - public function testResolveUrlWithManyParams() + public function testResolveUrlWithManyParams(): void { $this->markTestIncomplete('TODO'); } /** - * {{EntityDataObject.key}} should be replaced with someDataValue + * {{EntityDataObject.key}} should be replaced with someDataValue. + * + * @return void + * @throws TestReferenceException + * @throws XmlException */ - public function testResolveDataInUserInput() + public function testResolveDataInUserInput(): void { // Set up mocks $actionObject = new ActionObject('merge123', 'fillField', [ 'selector' => '#selector', - 'userInput' => '{{EntityDataObject.key}}' + 'userInput' => '{{EntityDataObject.key}}', + 'requiredCredentials' => '' ]); $entityDataObject = new EntityDataObject('EntityDataObject', 'test', [ 'key' => 'replacementData' @@ -288,20 +353,26 @@ public function testResolveDataInUserInput() // Verify $expected = [ 'selector' => '#selector', - 'userInput' => 'replacementData' + 'userInput' => 'replacementData', + 'requiredCredentials' => '' ]; $this->assertEquals($expected, $actionObject->getCustomActionAttributes()); } /** - * {{EntityDataObject.values}} should be replaced with ["value1","value2"] + * {{EntityDataObject.values}} should be replaced with ["value1","value2"]. + * + * @return void + * @throws TestReferenceException + * @throws XmlException */ - public function testResolveArrayData() + public function testResolveArrayData(): void { // Set up mocks $actionObject = new ActionObject('merge123', 'fillField', [ 'selector' => '#selector', - 'userInput' => '{{EntityDataObject.values}}' + 'userInput' => '{{EntityDataObject.values}}', + 'requiredCredentials' => '' ]); $entityDataObject = new EntityDataObject('EntityDataObject', 'test', [ 'values' => [ @@ -314,25 +385,30 @@ public function testResolveArrayData() // Call the method under test $actionObject->resolveReferences(); - - // Verify + //Verify $expected = [ 'selector' => '#selector', - 'userInput' => '["value1","value2","\"My\" Value"]' + 'userInput' => '["value1","value2","\"My\" Value"]', + 'requiredCredentials' => '' ]; $this->assertEquals($expected, $actionObject->getCustomActionAttributes()); } /** * Action object should throw an exception if a reference to a parameterized selector has too few given args. + * + * @return void + * @throws TestReferenceException + * @throws XmlException */ - public function testTooFewArgumentException() + public function testTooFewArgumentException(): void { $this->expectException(TestReferenceException::class); $actionObject = new ActionObject('key123', 'fillField', [ 'selector' => "{{SectionObject.elementObject('arg1')}}", - 'userInput' => 'Input' + 'userInput' => 'Input', + 'requiredCredentials' => '' ]); $elementObject = new ElementObject('elementObject', 'button', '#{{var1}} {{var2}}', null, '42', true); $this->mockSectionHandlerWithElement($elementObject); @@ -343,14 +419,19 @@ public function testTooFewArgumentException() /** * Action object should throw an exception if a reference to a parameterized selector has too many given args. + * + * @return void + * @throws TestReferenceException + * @throws XmlException */ - public function testTooManyArgumentException() + public function testTooManyArgumentException(): void { $this->expectException(TestReferenceException::class); $actionObject = new ActionObject('key123', 'fillField', [ 'selector' => "{{SectionObject.elementObject('arg1', 'arg2', 'arg3')}}", - 'userInput' => 'Input' + 'userInput' => 'Input', + 'requiredCredentials' => '' ]); $elementObject = new ElementObject('elementObject', 'button', '#{{var1}}', null, '42', true); $this->mockSectionHandlerWithElement($elementObject); @@ -361,39 +442,71 @@ public function testTooManyArgumentException() /** * Action object should throw an exception if the timezone provided is not valid. + * + * @return void + * @throws TestReferenceException + * @throws XmlException */ - public function testInvalidTimezoneException() + public function testInvalidTimezoneException(): void { $this->expectException(TestReferenceException::class); $actionObject = new ActionObject('key123', 'generateDate', [ - 'timezone' => "INVALID_TIMEZONE" + 'timezone' => "INVALID_TIMEZONE", + 'requiredCredentials' => '' ]); // Call the method under test $actionObject->resolveReferences(); } - private function mockSectionHandlerWithElement($elementObject) + /** + * Mock section handler with the specified ElementObject. + * + * @param ElementObject $elementObject + * + * @return void + * @throws Exception + */ + private function mockSectionHandlerWithElement(ElementObject $elementObject): void { $sectionObject = new SectionObject('SectionObject', ['elementObject' => $elementObject]); - $instance = AspectMock::double(SectionObjectHandler::class, ['getObject' => $sectionObject]) - ->make(); // bypass the private constructor - AspectMock::double(SectionObjectHandler::class, ['getInstance' => $instance]); + $instance = $this->createMock(SectionObjectHandler::class); + $instance + ->method('getObject') + ->willReturn($sectionObject); + // bypass the private constructor + $property = new ReflectionProperty(SectionObjectHandler::class, 'INSTANCE'); + $property->setAccessible(true); + $property->setValue(null, $instance); } - private function mockDataHandlerWithData($dataObject) + /** + * Mock data handler with the specified EntityDataObject. + * + * @param EntityDataObject $dataObject + * + * @return void + * @throws Exception + */ + private function mockDataHandlerWithData(EntityDataObject $dataObject): void { - $dataInstance = AspectMock::double(DataObjectHandler::class, ['getObject' => $dataObject]) - ->make(); - AspectMock::double(DataObjectHandler::class, ['getInstance' => $dataInstance]); + $dataInstance = $this->createMock(DataObjectHandler::class); + $dataInstance + ->method('getObject') + ->willReturn($dataObject); + // bypass the private constructor + $property = new ReflectionProperty(DataObjectHandler::class, 'INSTANCE'); + $property->setAccessible(true); + $property->setValue(null, $dataInstance); } /** - * After class functionality + * After class functionality. + * * @return void */ - public static function tearDownAfterClass() + public static function tearDownAfterClass(): void { TestLoggingUtil::getInstance()->clearMockLoggingUtil(); } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionGroupAnnotationExtractorTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionGroupAnnotationExtractorTest.php index 392c870d9..23c97fd4a 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionGroupAnnotationExtractorTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionGroupAnnotationExtractorTest.php @@ -3,52 +3,56 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace tests\unit\Magento\FunctionalTestFramework\Test\Util; -use AspectMock\Test as AspectMock; +use Exception; use Magento\FunctionalTestingFramework\Test\Util\ActionGroupAnnotationExtractor; use PHPUnit\Framework\TestCase; use tests\unit\Util\TestLoggingUtil; +/** + * Class ActionGroupAnnotationExtractorTest + */ class ActionGroupAnnotationExtractorTest extends TestCase { /** - * Before test functionality - * @return void + * @inheritDoc */ - public function setUp() + protected function setUp(): void { TestLoggingUtil::getInstance()->setMockLoggingUtil(); } /** - * Annotation extractor takes in raw array and condenses it to expected format + * Annotation extractor takes in raw array and condenses it to expected format. * - * @throws \Exception + * @return void + * @throws Exception */ - public function testActionGroupExtractAnnotations() + public function testActionGroupExtractAnnotations(): void { // Test Data $actionGroupAnnotations = [ - "nodeName" => "annotations", - "description" => [ - "nodeName" => "description", - "value" => "someDescription" + 'nodeName' => 'annotations', + 'description' => [ + 'nodeName' => 'description', + 'value' => 'someDescription' ] ]; // Perform Test $extractor = new ActionGroupAnnotationExtractor(); - $returnedAnnotations = $extractor->extractAnnotations($actionGroupAnnotations, "fileName"); + $returnedAnnotations = $extractor->extractAnnotations($actionGroupAnnotations, 'fileName'); // Asserts - $this->assertEquals("someDescription", $returnedAnnotations['description']); + $this->assertEquals('someDescription', $returnedAnnotations['description']); } /** - * After class functionality - * @return void + * @inheritDoc */ - public static function tearDownAfterClass() + public static function tearDownAfterClass(): void { TestLoggingUtil::getInstance()->clearMockLoggingUtil(); } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionGroupObjectExtractorTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionGroupObjectExtractorTest.php index 47b797c1c..1fea7e848 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionGroupObjectExtractorTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionGroupObjectExtractorTest.php @@ -6,7 +6,7 @@ namespace tests\unit\Magento\FunctionalTestFramework\Test\Util; use Magento\FunctionalTestingFramework\Test\Util\ActionGroupObjectExtractor; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use tests\unit\Util\MagentoTestCase; use tests\unit\Util\TestLoggingUtil; class ActionGroupObjectExtractorTest extends MagentoTestCase @@ -17,7 +17,7 @@ class ActionGroupObjectExtractorTest extends MagentoTestCase /** * Setup method */ - public function setUp() + public function setUp(): void { $this->testActionGroupObjectExtractor = new ActionGroupObjectExtractor(); TestLoggingUtil::getInstance()->setMockLoggingUtil(); @@ -34,23 +34,47 @@ public function testEmptyStepKey() $this->testActionGroupObjectExtractor->extractActionGroup($this->createBasicActionObjectArray("")); } + /** + * Tests deprecation message for an action group + */ + public function testDeprecationMessage() + { + $this->testActionGroupObjectExtractor->extractActionGroup( + $this->createBasicActionObjectArray( + "testDeprecatedAction1", + "actionGroup", + "filename1.xml", + "message" + ) + ); + + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'warning', + "DEPRECATION: The action group 'actionGroup' is deprecated.", + ["fileName" => "filename1.xml", "deprecatedMessage" => "message"] + ); + } + /** * Utility function to return mock parser output for testing extraction into ActionObjects. * * @param string $stepKey * @param string $actionGroup * @param string $filename + * @param string $deprecated * @return array */ private function createBasicActionObjectArray( $stepKey = 'testAction1', $actionGroup = "actionGroup", - $filename = "filename.xml" + $filename = "filename.xml", + $deprecated = null ) { $baseArray = [ 'nodeName' => 'actionGroup', 'name' => $actionGroup, 'filename' => $filename, + 'deprecated' => $deprecated, $stepKey => [ "nodeName" => "sampleAction", "stepKey" => $stepKey, @@ -63,7 +87,7 @@ private function createBasicActionObjectArray( /** * clean up function runs after all tests */ - public static function tearDownAfterClass() + public static function tearDownAfterClass(): void { TestLoggingUtil::getInstance()->clearMockLoggingUtil(); } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionMergeUtilTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionMergeUtilTest.php index 7917d663b..e388d176c 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionMergeUtilTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionMergeUtilTest.php @@ -3,9 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace tests\unit\Magento\FunctionalTestFramework\Test\Util; -use AspectMock\Test as AspectMock; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; @@ -13,17 +14,18 @@ use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; use Magento\FunctionalTestingFramework\Test\Util\ActionMergeUtil; use Magento\FunctionalTestingFramework\Test\Util\ActionObjectExtractor; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; -use tests\unit\Util\DataObjectHandlerReflectionUtil; +use ReflectionProperty; +use tests\unit\Util\MagentoTestCase; use tests\unit\Util\TestLoggingUtil; class ActionMergeUtilTest extends MagentoTestCase { /** - * Before test functionality + * Before test functionality. + * * @return void */ - public function setUp() + protected function setUp(): void { TestLoggingUtil::getInstance()->setMockLoggingUtil(); } @@ -32,8 +34,10 @@ public function setUp() * Test to validate actions are properly ordered during a merge. * * @return void + * @throws TestReferenceException + * @throws XmlException */ - public function testResolveActionStepOrdering() + public function testResolveActionStepOrdering(): void { $actions = []; $actionsLength = 11; @@ -47,7 +51,6 @@ public function testResolveActionStepOrdering() $stepKey = 'stepKey'. $i; $type = 'testType'; $actionAttributes = []; - $actions[] = new ActionObject($stepKey, $type, $actionAttributes); } @@ -94,15 +97,17 @@ public function testResolveActionStepOrdering() * Test to validate action steps properly resolve entity data references. * * @return void + * @throws TestReferenceException + * @throws XmlException */ - public function testResolveActionStepEntityData() + public function testResolveActionStepEntityData(): void { $dataObjectName = 'myObject'; $dataObjectType = 'testObject'; $dataFieldName = 'myfield'; $dataFieldValue = 'myValue'; $userInputKey = "userInput"; - $userInputValue = "{{" . "${dataObjectName}.${dataFieldName}}}"; + $userInputValue = "{{" . "{$dataObjectName}.{$dataFieldName}}}"; $actionName = "myAction"; $actionType = "myCustomType"; @@ -111,36 +116,35 @@ public function testResolveActionStepEntityData() $mockDataObject = new EntityDataObject($dataObjectName, $dataObjectType, $mockData, null, null, null); // Set up mock DataObject Handler - $mockDOHInstance = AspectMock::double(DataObjectHandler::class, ['getObject' => $mockDataObject])->make(); - AspectMock::double(DataObjectHandler::class, ['getInstance' => $mockDOHInstance]); + $mockDOHInstance = $this->createMock(DataObjectHandler::class); + $mockDOHInstance + ->expects($this->any()) + ->method('getObject') + ->willReturn($mockDataObject); + $property = new ReflectionProperty(DataObjectHandler::class, 'INSTANCE'); + $property->setAccessible(true); + $property->setValue($mockDOHInstance, $mockDOHInstance); // Create test object and action object - $actionAttributes = [$userInputKey => $userInputValue]; + $actionAttributes = [$userInputKey => $userInputValue,'requiredCredentials'=>'']; $actions[$actionName] = new ActionObject($actionName, $actionType, $actionAttributes); - $this->assertEquals($userInputValue, $actions[$actionName]->getCustomActionAttributes()[$userInputKey]); $mergeUtil = new ActionMergeUtil("test", "TestCase"); $resolvedActions = $mergeUtil->resolveActionSteps($actions); - $this->assertEquals($dataFieldValue, $resolvedActions[$actionName]->getCustomActionAttributes()[$userInputKey]); } /** * Verify that an XmlException is thrown when an action references a non-existant action. * - * @throws TestReferenceException - * @throws XmlException * @return void - */ - /** * @throws TestReferenceException * @throws XmlException */ - public function testNoActionException() + public function testNoActionException(): void { $actionObjects = []; - $actionObjects[] = new ActionObject('actionKey1', 'bogusType', []); $actionObjects[] = new ActionObject( 'actionKey2', @@ -150,8 +154,7 @@ public function testNoActionException() ActionObject::MERGE_ACTION_ORDER_BEFORE ); - $this->expectException(\Magento\FunctionalTestingFramework\Exceptions\XmlException::class); - + $this->expectException(XmlException::class); $actionMergeUtil = new ActionMergeUtil("actionMergeUtilTest", "TestCase"); $actionMergeUtil->resolveActionSteps($actionObjects); } @@ -159,11 +162,11 @@ public function testNoActionException() /** * Verify that a action is added after actions that have a wait (timeout property). * + * @return void * @throws TestReferenceException * @throws XmlException - * @return void */ - public function testInsertWait() + public function testInsertWait(): void { $actionObjectOne = new ActionObject('actionKey1', 'bogusType', []); $actionObjectOne->setTimeout(42); @@ -186,26 +189,26 @@ public function testInsertWait() /** * Verify that a action is replaced by when secret _CREDS are referenced. * + * @return void * @throws TestReferenceException * @throws XmlException */ - public function testValidFillFieldSecretFunction() + public function testValidFillFieldSecretFunction(): void { $actionObjectOne = new ActionObject( 'actionKey1', 'fillField', - ['userInput' => '{{_CREDS.username}}'] + ['userInput' => '{{_CREDS.username}}', 'requiredCredentials' => 'username'] ); $actionObject = [$actionObjectOne]; $actionMergeUtil = new ActionMergeUtil('actionMergeUtilTest', 'TestCase'); - $result = $actionMergeUtil->resolveActionSteps($actionObject); $expectedValue = new ActionObject( 'actionKey1', 'fillSecretField', - ['userInput' => '{{_CREDS.username}}'] + ['userInput' => '{{_CREDS.username}}','requiredCredentials' => 'username'] ); $this->assertEquals($expectedValue, $result['actionKey1']); } @@ -213,26 +216,32 @@ public function testValidFillFieldSecretFunction() /** * Verify that a action uses when secret _CREDS are referenced. * + * @return void * @throws TestReferenceException * @throws XmlException */ - public function testValidMagentoCLISecretFunction() + public function testValidMagentoCLISecretFunction(): void { $actionObjectOne = new ActionObject( 'actionKey1', 'magentoCLI', - ['command' => 'config:set cms/wysiwyg/enabled {{_CREDS.payment_authorizenet_login}}'] + ['command' => + 'config:set cms/wysiwyg/enabled {{_CREDS.payment_authorizenet_login}}', + 'requiredCredentials' => '' + ] ); $actionObject = [$actionObjectOne]; $actionMergeUtil = new ActionMergeUtil('actionMergeUtilTest', 'TestCase'); - $result = $actionMergeUtil->resolveActionSteps($actionObject); $expectedValue = new ActionObject( 'actionKey1', 'magentoCLISecret', - ['command' => 'config:set cms/wysiwyg/enabled {{_CREDS.payment_authorizenet_login}}'] + ['command' => + 'config:set cms/wysiwyg/enabled {{_CREDS.payment_authorizenet_login}}', + 'requiredCredentials' => '' + ] ); $this->assertEquals($expectedValue, $result['actionKey1']); } @@ -240,26 +249,26 @@ public function testValidMagentoCLISecretFunction() /** * Verify that a override in a action uses when secret _CREDS are referenced. * + * @return void * @throws TestReferenceException * @throws XmlException */ - public function testValidCreateDataSecretFunction() + public function testValidCreateDataSecretFunction(): void { $actionObjectOne = new ActionObject( 'actionKey1', 'field', - ['value' => '{{_CREDS.payment_authorizenet_login}}'] + ['value' => '{{_CREDS.payment_authorizenet_login}}','requiredCredentials' => ''] ); $actionObject = [$actionObjectOne]; $actionMergeUtil = new ActionMergeUtil('actionMergeUtilTest', 'TestCase'); - $result = $actionMergeUtil->resolveActionSteps($actionObject); $expectedValue = new ActionObject( 'actionKey1', 'field', - ['value' => '{{_CREDS.payment_authorizenet_login}}'] + ['value' => '{{_CREDS.payment_authorizenet_login}}','requiredCredentials' => ''] ); $this->assertEquals($expectedValue, $result['actionKey1']); } @@ -267,10 +276,11 @@ public function testValidCreateDataSecretFunction() /** * Verify that a action throws an exception when secret _CREDS are referenced. * + * @return void * @throws TestReferenceException * @throws XmlException */ - public function testInvalidSecretFunctions() + public function testInvalidSecretFunctions(): void { $this->expectException(TestReferenceException::class); $this->expectExceptionMessage( @@ -280,7 +290,7 @@ public function testInvalidSecretFunctions() $actionObjectOne = new ActionObject( 'actionKey1', 'click', - ['userInput' => '{{_CREDS.username}}'] + ['userInput' => '{{_CREDS.username}}','requiredCredentials' => 'username'] ); $actionObject = [$actionObjectOne]; @@ -289,10 +299,11 @@ public function testInvalidSecretFunctions() } /** - * After class functionality + * After class functionality. + * * @return void */ - public static function tearDownAfterClass() + public static function tearDownAfterClass(): void { TestLoggingUtil::getInstance()->clearMockLoggingUtil(); } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionObjectExtractorTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionObjectExtractorTest.php index f584adea9..6f464b41e 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionObjectExtractorTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ActionObjectExtractorTest.php @@ -7,7 +7,7 @@ use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; use Magento\FunctionalTestingFramework\Test\Util\ActionObjectExtractor; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use tests\unit\Util\MagentoTestCase; use tests\unit\Util\TestLoggingUtil; class ActionObjectExtractorTest extends MagentoTestCase @@ -18,7 +18,7 @@ class ActionObjectExtractorTest extends MagentoTestCase /** * Setup method */ - public function setUp() + public function setUp(): void { $this->testActionObjectExtractor = new ActionObjectExtractor(); TestLoggingUtil::getInstance()->setMockLoggingUtil(); @@ -130,7 +130,7 @@ private function createBasicActionObjectArray($stepKey = 'testAction1', $before /** * clean up function runs after all tests */ - public static function tearDownAfterClass() + public static function tearDownAfterClass(): void { TestLoggingUtil::getInstance()->clearMockLoggingUtil(); } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/AnnotationExtractorTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/AnnotationExtractorTest.php index fb93cbb88..44ec3a7aa 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/AnnotationExtractorTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/AnnotationExtractorTest.php @@ -3,113 +3,182 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace tests\unit\Magento\FunctionalTestFramework\Test\Util; -use AspectMock\Proxy\Verifier; -use AspectMock\Test as AspectMock; +use Exception; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Test\Util\AnnotationExtractor; -use Monolog\Handler\TestHandler; -use Monolog\Logger; +use Magento\FunctionalTestingFramework\Util\GenerationErrorHandler; use PHPUnit\Framework\TestCase; use tests\unit\Util\TestLoggingUtil; +/** + * Class AnnotationExtractorTest + */ class AnnotationExtractorTest extends TestCase { /** - * Before test functionality - * @return void + * @inheritDoc */ - public function setUp() + protected function setUp(): void { TestLoggingUtil::getInstance()->setMockLoggingUtil(); } /** - * Annotation extractor takes in raw array and condenses it to expected format + * Annotation extractor takes in raw array and condenses it to expected format. * - * @throws \Exception + * @return void + * @throws Exception */ - public function testExtractAnnotations() + public function testExtractAnnotations(): void { // Test Data $testAnnotations = [ - "nodeName" => "annotations", - "features" => [ + 'nodeName' => 'annotations', + 'features' => [ [ - "nodeName" => "features", - "value" => "TestFeatures" + 'nodeName' => 'features', + 'value' => 'TestFeatures' ] ], - "stories" => [ + 'stories' => [ [ - "nodeName" => "stories", - "value" => "TestStories" + 'nodeName' => 'stories', + 'value' => 'TestStories' ] ], - "description" => [ + 'description' => [ [ - "nodeName" => "description", - "value" => "TestDescription" + 'nodeName' => 'description', + 'value' => 'TestDescription' ] ], - "severity" => [ + 'severity' => [ [ - "nodeName" => "severity", - "value" => "CRITICAL" + 'nodeName' => 'severity', + 'value' => 'CRITICAL' ] ], - "group" => [ + 'group' => [ [ - "nodeName" => "group", - "value" => "TestGroup" + 'nodeName' => 'group', + 'value' => 'TestGroup' ] ], ]; // Perform Test $extractor = new AnnotationExtractor(); - $returnedAnnotations = $extractor->extractAnnotations($testAnnotations, "testFileName"); + $returnedAnnotations = $extractor->extractAnnotations($testAnnotations, 'testFileName'); // Asserts - $this->assertEquals("TestFeatures", $returnedAnnotations['features'][0]); - $this->assertEquals("TestStories", $returnedAnnotations['stories'][0]); - $this->assertEquals("TestDescription", $returnedAnnotations['description'][0]); - $this->assertEquals("CRITICAL", $returnedAnnotations['severity'][0]); - $this->assertEquals("TestGroup", $returnedAnnotations['group'][0]); + $this->assertEquals('TestFeatures', $returnedAnnotations['features'][0]); + $this->assertEquals('TestStories', $returnedAnnotations['stories'][0]); + $this->assertEquals('TestDescription', $returnedAnnotations['description'][0]); + $this->assertEquals('CRITICAL', $returnedAnnotations['severity'][0]); + $this->assertEquals('TestGroup', $returnedAnnotations['group'][0]); + } + + /** + * Annotation extractor should throw warning when required annotations are missing. + * + * @return void + * @throws Exception + */ + public function testMissingAnnotations(): void + { + // Test Data, missing title, description, and severity + $testAnnotations = [ + 'nodeName' => 'annotations', + 'features' => [ + [ + 'nodeName' => 'features', + 'value' => 'TestFeatures' + ] + ], + 'stories' => [ + [ + 'nodeName' => 'stories', + 'value' => 'TestStories' + ] + ], + 'group' => [ + [ + 'nodeName' => 'group', + 'value' => 'TestGroup' + ] + ], + ]; + // Perform Test + $extractor = new AnnotationExtractor(); + $extractor->extractAnnotations($testAnnotations, 'testFileName'); + + // Asserts + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'warning', + 'DEPRECATION: Test testFileName is missing required annotations.', + [ + 'testName' => 'testFileName', + 'missingAnnotations' => 'title, description, severity' + ] + ); } /** - * Annotation extractor should throw warning when required annotations are missing + * Annotation extractor should throw warning when required annotations are empty. * - * @throws \Exception + * @return void + * @throws Exception */ - public function testMissingAnnotations() + public function testEmptyRequiredAnnotations(): void { // Test Data, missing title, description, and severity $testAnnotations = [ - "nodeName" => "annotations", - "features" => [ + 'nodeName' => 'annotations', + 'features' => [ + [ + 'nodeName' => 'features', + 'value' => '' + ] + ], + 'stories' => [ + [ + 'nodeName' => 'stories', + 'value' => 'TestStories' + ] + ], + 'title' => [ [ - "nodeName" => "features", - "value" => "TestFeatures" + 'nodeName' => 'title', + 'value' => ' ' ] ], - "stories" => [ + 'description' => [ [ - "nodeName" => "stories", - "value" => "TestStories" + 'nodeName' => 'description', + 'value' => "\t" ] ], - "group" => [ + 'severity' => [ [ - "nodeName" => "group", - "value" => "TestGroup" + 'nodeName' => 'severity', + 'value' => '' + ] + ], + 'group' => [ + [ + 'nodeName' => 'group', + 'value' => 'TestGroup' ] ], ]; // Perform Test $extractor = new AnnotationExtractor(); - $returnedAnnotations = $extractor->extractAnnotations($testAnnotations, "testFileName"); + $returnedAnnotations = $extractor->extractAnnotations($testAnnotations, 'testFileName'); // Asserts TestLoggingUtil::getInstance()->validateMockLogStatement( @@ -117,99 +186,115 @@ public function testMissingAnnotations() 'DEPRECATION: Test testFileName is missing required annotations.', [ 'testName' => 'testFileName', - 'missingAnnotations' => "title, description, severity" + 'missingAnnotations' => 'title, description, severity' ] ); } - public function testTestCaseIdUniqueness() + /** + * Validate testTestCaseIdUniqueness. + * + * @return void + * @throws TestFrameworkException|XmlException + */ + public function testTestCaseIdUniqueness(): void { // Test Data $firstTestAnnotation = [ - "nodeName" => "annotations", - "features" => [ + 'nodeName' => 'annotations', + 'features' => [ [ - "nodeName" => "features", - "value" => "TestFeatures" + 'nodeName' => 'features', + 'value' => 'TestFeatures' ] ], - "stories" => [ + 'stories' => [ [ - "nodeName" => "stories", - "value" => "TestStories" + 'nodeName' => 'stories', + 'value' => 'TestStories' ] ], - "title" => [ + 'title' => [ [ - "nodeName" => "title", - "value" => "TEST TITLE" + 'nodeName' => 'title', + 'value' => 'TEST TITLE' ] ], - "severity" => [ + 'severity' => [ [ - "nodeName" => "severity", - "value" => "CRITICAL" + 'nodeName' => 'severity', + 'value' => 'CRITICAL' ] ], - "testCaseId" => [ + 'testCaseId' => [ [ - "nodeName" => "testCaseId", - "value" => "MQE-0001" + 'nodeName' => 'testCaseId', + 'value' => 'MQE-0001' ] ], ]; $secondTestannotation = [ - "nodeName" => "annotations", - "features" => [ + 'nodeName' => 'annotations', + 'features' => [ [ - "nodeName" => "features", - "value" => "TestFeatures" + 'nodeName' => 'features', + 'value' => 'TestFeatures' ] ], - "stories" => [ + 'stories' => [ [ - "nodeName" => "stories", - "value" => "TestStories" + 'nodeName' => 'stories', + 'value' => 'TestStories' ] ], - "title" => [ + 'title' => [ [ - "nodeName" => "title", - "value" => "TEST TITLE" + 'nodeName' => 'title', + 'value' => 'TEST TITLE' ] ], - "severity" => [ + 'severity' => [ [ - "nodeName" => "severity", - "value" => "CRITICAL" + 'nodeName' => 'severity', + 'value' => 'CRITICAL' ] ], - "testCaseId" => [ + 'testCaseId' => [ [ - "nodeName" => "testCaseId", - "value" => "MQE-0001" + 'nodeName' => 'testCaseId', + 'value' => 'MQE-0001' ] ], ]; // Perform Test $extractor = new AnnotationExtractor(); - $extractor->extractAnnotations($firstTestAnnotation, "firstTest"); - $extractor->extractAnnotations($secondTestannotation, "secondTest"); + $extractor->extractAnnotations($firstTestAnnotation, 'firstTest'); + $extractor->extractAnnotations($secondTestannotation, 'secondTest'); + $extractor->validateTestCaseIdTitleUniqueness(); - //Expect Exception - $this->expectException(\Magento\FunctionalTestingFramework\Exceptions\XmlException::class); - $this->expectExceptionMessage("TestCaseId and Title pairs must be unique:\n\n" . - "TestCaseId: 'MQE-0001' Title: 'TEST TITLE' in Tests 'firstTest', 'secondTest'"); + // assert that no exception for validateTestCaseIdTitleUniqueness + // and validation error is stored in GenerationErrorHandler + $errorMessage = '/' + . preg_quote('TestCaseId and Title pairs is not unique in Tests \'firstTest\', \'secondTest\'') + . '/'; + TestLoggingUtil::getInstance()->validateMockLogStatmentRegex('error', $errorMessage, []); + $testErrors = GenerationErrorHandler::getInstance()->getErrorsByType('test'); + $this->assertArrayHasKey('firstTest', $testErrors); + $this->assertArrayHasKey('secondTest', $testErrors); + } - //Trigger Exception - $extractor->validateTestCaseIdTitleUniqueness(); + /** + * @inheritDoc + */ + protected function tearDown(): void + { + GenerationErrorHandler::getInstance()->reset(); } /** - * After class functionality - * @return void + * @inheritDoc */ - public static function tearDownAfterClass() + public static function tearDownAfterClass(): void { TestLoggingUtil::getInstance()->clearMockLoggingUtil(); } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ObjectExtensionUtilTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ObjectExtensionUtilTest.php index 9982b8040..466edc322 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ObjectExtensionUtilTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Test/Util/ObjectExtensionUtilTest.php @@ -3,55 +3,54 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace tests\unit\Magento\FunctionalTestFramework\Test\Util; -use AspectMock\Proxy\Verifier; -use AspectMock\Test as AspectMock; -use Magento\FunctionalTestingFramework\ObjectManager\ObjectManager; -use Magento\FunctionalTestingFramework\ObjectManagerFactory; +use Exception; +use Magento\FunctionalTestingFramework\ObjectManager; use Magento\FunctionalTestingFramework\Test\Handlers\ActionGroupObjectHandler; use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; use Magento\FunctionalTestingFramework\Test\Parsers\ActionGroupDataParser; use Magento\FunctionalTestingFramework\Test\Parsers\TestDataParser; -use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; -use Monolog\Handler\TestHandler; -use Monolog\Logger; use PHPUnit\Framework\TestCase; +use ReflectionProperty; use tests\unit\Util\TestDataArrayBuilder; use tests\unit\Util\TestLoggingUtil; -use tests\unit\Util\MockModuleResolverBuilder; class ObjectExtensionUtilTest extends TestCase { /** - * Before test functionality + * Before test functionality. + * * @return void + * @throws Exception */ - public function setUp() + protected function setUp(): void { TestLoggingUtil::getInstance()->setMockLoggingUtil(); - $resolverMock = new MockModuleResolverBuilder(); - $resolverMock->setup(); } /** - * After class functionality + * After class functionality. + * * @return void */ - public static function tearDownAfterClass() + public static function tearDownAfterClass(): void { TestLoggingUtil::getInstance()->clearMockLoggingUtil(); } /** - * Tests generating a test that extends another test - * @throws \Exception + * Tests generating a test that extends another test. + * + * @return void + * @throws Exception */ - public function testGenerateExtendedTest() + public function testGenerateExtendedTest(): void { $mockActions = [ - "mockStep" => ["nodeName" => "mockNode", "stepKey" => "mockStep"] + 'mockStep' => ['nodeName' => 'mockNode', 'stepKey' => 'mockStep'] ]; $testDataArrayBuilder = new TestDataArrayBuilder(); @@ -64,7 +63,7 @@ public function testGenerateExtendedTest() $mockExtendedTest = $testDataArrayBuilder ->withName('extendedTest') ->withAnnotations(['title' => [['value' => 'extendedTest']]]) - ->withTestReference("simpleTest") + ->withTestReference('simpleTest') ->build(); $mockTestData = array_merge($mockSimpleTest, $mockExtendedTest); @@ -81,21 +80,23 @@ public function testGenerateExtendedTest() ); // assert that expected test is generated - $this->assertEquals($testObject->getParentName(), "simpleTest"); - $this->assertArrayHasKey("mockStep", $testObject->getOrderedActions()); + $this->assertEquals($testObject->getParentName(), 'simpleTest'); + $this->assertArrayHasKey('mockStep', $testObject->getOrderedActions()); } /** - * Tests generating a test that extends another test - * @throws \Exception + * Tests generating a test that extends another test. + * + * @return void + * @throws Exception */ - public function testGenerateExtendedWithHooks() + public function testGenerateExtendedWithHooks(): void { $mockBeforeHooks = [ - "beforeHookAction" => ["nodeName" => "mockNodeBefore", "stepKey" => "mockStepBefore"] + 'beforeHookAction' => ['nodeName' => 'mockNodeBefore', 'stepKey' => 'mockStepBefore'] ]; $mockAfterHooks = [ - "afterHookAction" => ["nodeName" => "mockNodeAfter", "stepKey" => "mockStepAfter"] + 'afterHookAction' => ['nodeName' => 'mockNodeAfter', 'stepKey' => 'mockStepAfter'] ]; $testDataArrayBuilder = new TestDataArrayBuilder(); @@ -109,7 +110,7 @@ public function testGenerateExtendedWithHooks() $mockExtendedTest = $testDataArrayBuilder ->withName('extendedTest') ->withAnnotations(['title' => [['value' => 'extendedTest']]]) - ->withTestReference("simpleTest") + ->withTestReference('simpleTest') ->build(); $mockTestData = array_merge($mockSimpleTest, $mockExtendedTest); @@ -126,21 +127,23 @@ public function testGenerateExtendedWithHooks() ); // assert that expected test is generated - $this->assertEquals($testObject->getParentName(), "simpleTest"); - $this->assertArrayHasKey("mockStepBefore", $testObject->getHooks()['before']->getActions()); - $this->assertArrayHasKey("mockStepAfter", $testObject->getHooks()['after']->getActions()); + $this->assertEquals($testObject->getParentName(), 'simpleTest'); + $this->assertArrayHasKey('mockStepBefore', $testObject->getHooks()['before']->getActions()); + $this->assertArrayHasKey('mockStepAfter', $testObject->getHooks()['after']->getActions()); } /** - * Tests generating a test that extends another test - * @throws \Exception + * Tests generating a test that extends another test. + * + * @return void + * @throws Exception */ - public function testExtendedTestNoParent() + public function testExtendedTestNoParent(): void { $testDataArrayBuilder = new TestDataArrayBuilder(); $mockExtendedTest = $testDataArrayBuilder ->withName('extendedTest') - ->withTestReference("simpleTest") + ->withTestReference('simpleTest') ->build(); $mockTestData = array_merge($mockExtendedTest); @@ -152,16 +155,18 @@ public function testExtendedTestNoParent() // validate log statement TestLoggingUtil::getInstance()->validateMockLogStatement( 'debug', - "parent test not defined. test will be skipped", + 'parent test not defined. test will be skipped', ['parent' => 'simpleTest', 'test' => 'extendedTest'] ); } /** - * Tests generating a test that extends another test - * @throws \Exception + * Tests generating a test that extends another test. + * + * @return void + * @throws Exception */ - public function testExtendingExtendedTest() + public function testExtendingExtendedTest(): void { $testDataArrayBuilder = new TestDataArrayBuilder(); $mockParentTest = $testDataArrayBuilder @@ -173,19 +178,19 @@ public function testExtendingExtendedTest() ->withName('simpleTest') ->withAnnotations(['title' => [['value' => 'simpleTest']]]) ->withTestActions() - ->withTestReference("anotherTest") + ->withTestReference('anotherTest') ->build(); $mockExtendedTest = $testDataArrayBuilder ->withName('extendedTest') ->withAnnotations(['title' => [['value' => 'extendedTest']]]) - ->withTestReference("simpleTest") + ->withTestReference('simpleTest') ->build(); $mockTestData = array_merge($mockParentTest, $mockSimpleTest, $mockExtendedTest); $this->setMockTestOutput($mockTestData); - $this->expectExceptionMessage("Cannot extend a test that already extends another test. Test: simpleTest"); + $this->expectExceptionMessage('Cannot extend a test that already extends another test. Test: simpleTest'); // parse and generate test object with mocked data TestObjectHandler::getInstance()->getObject('extendedTest'); @@ -193,43 +198,45 @@ public function testExtendingExtendedTest() // validate log statement TestLoggingUtil::getInstance()->validateMockLogStatement( 'debug', - "parent test not defined. test will be skipped", + 'parent test not defined. test will be skipped', ['parent' => 'simpleTest', 'test' => 'extendedTest'] ); - $this->expectOutputString("Extending Test: anotherTest => simpleTest" . PHP_EOL); + $this->expectOutputString('Extending Test: anotherTest => simpleTest' . PHP_EOL); } /** - * Tests generating an action group that extends another action group - * @throws \Exception + * Tests generating an action group that extends another action group. + * + * @return void + * @throws Exception */ - public function testGenerateExtendedActionGroup() + public function testGenerateExtendedActionGroup(): void { $mockSimpleActionGroup = [ - "nodeName" => "actionGroup", - "name" => "mockSimpleActionGroup", - "filename" => "someFile", - "commentHere" => [ - "nodeName" => "comment", - "selector" => "selector", - "stepKey" => "commentHere" + 'nodeName' => 'actionGroup', + 'name' => 'mockSimpleActionGroup', + 'filename' => 'someFile', + 'commentHere' => [ + 'nodeName' => 'comment', + 'selector' => 'selector', + 'stepKey' => 'commentHere' ], - "parentComment" => [ - "nodeName" => "comment", - "selector" => "parentSelector", - "stepKey" => "parentComment" + 'parentComment' => [ + 'nodeName' => 'comment', + 'selector' => 'parentSelector', + 'stepKey' => 'parentComment' ], ]; $mockExtendedActionGroup = [ - "nodeName" => "actionGroup", - "name" => "mockExtendedActionGroup", - "filename" => "someFile", - "extends" => "mockSimpleActionGroup", - "commentHere" => [ - "nodeName" => "comment", - "selector" => "otherSelector", - "stepKey" => "commentHere" + 'nodeName' => 'actionGroup', + 'name' => 'mockExtendedActionGroup', + 'filename' => 'someFile', + 'extends' => 'mockSimpleActionGroup', + 'commentHere' => [ + 'nodeName' => 'comment', + 'selector' => 'otherSelector', + 'stepKey' => 'commentHere' ], ]; @@ -252,27 +259,29 @@ public function testGenerateExtendedActionGroup() ); // assert that expected test is generated - $this->assertEquals("mockSimpleActionGroup", $actionGroupObject->getParentName()); + $this->assertEquals('mockSimpleActionGroup', $actionGroupObject->getParentName()); $actions = $actionGroupObject->getActions(); - $this->assertEquals("otherSelector", $actions["commentHere"]->getCustomActionAttributes()["selector"]); - $this->assertEquals("parentSelector", $actions["parentComment"]->getCustomActionAttributes()["selector"]); + $this->assertEquals('otherSelector', $actions['commentHere']->getCustomActionAttributes()['selector']); + $this->assertEquals('parentSelector', $actions['parentComment']->getCustomActionAttributes()['selector']); } /** - * Tests generating an action group that extends an action group that does not exist - * @throws \Exception + * Tests generating an action group that extends an action group that does not exist. + * + * @return void + * @throws Exception */ - public function testGenerateExtendedActionGroupNoParent() + public function testGenerateExtendedActionGroupNoParent(): void { $mockExtendedActionGroup = [ - "nodeName" => "actionGroup", - "name" => "mockExtendedActionGroup", - "filename" => "someFile", - "extends" => "mockSimpleActionGroup", - "commentHere" => [ - "nodeName" => "comment", - "selector" => "otherSelector", - "stepKey" => "commentHere" + 'nodeName' => 'actionGroup', + 'name' => 'mockExtendedActionGroup', + 'filename' => 'someFile', + 'extends' => 'mockSimpleActionGroup', + 'commentHere' => [ + 'nodeName' => 'comment', + 'selector' => 'otherSelector', + 'stepKey' => 'commentHere' ], ]; @@ -284,7 +293,7 @@ public function testGenerateExtendedActionGroupNoParent() $this->setMockTestOutput(null, $mockActionGroupData); $this->expectExceptionMessage( - "Parent Action Group mockSimpleActionGroup not defined for Test " . $mockExtendedActionGroup['name'] + 'Parent Action Group mockSimpleActionGroup not defined for Test ' . $mockExtendedActionGroup['name'] ); // parse and generate test object with mocked data @@ -292,29 +301,31 @@ public function testGenerateExtendedActionGroupNoParent() } /** - * Tests generating an action group that extends another action group that is already extended - * @throws \Exception + * Tests generating an action group that extends another action group that is already extended. + * + * @return void + * @throws Exception */ - public function testExtendingExtendedActionGroup() + public function testExtendingExtendedActionGroup(): void { $mockParentActionGroup = [ - "nodeName" => "actionGroup", - "name" => "mockParentActionGroup", - "filename" => "someFile" + 'nodeName' => 'actionGroup', + 'name' => 'mockParentActionGroup', + 'filename' => 'someFile' ]; $mockSimpleActionGroup = [ - "nodeName" => "actionGroup", - "name" => "mockSimpleActionGroup", - "filename" => "someFile", - "extends" => "mockParentActionGroup", + 'nodeName' => 'actionGroup', + 'name' => 'mockSimpleActionGroup', + 'filename' => 'someFile', + 'extends' => 'mockParentActionGroup' ]; $mockExtendedActionGroup = [ - "nodeName" => "actionGroup", - "name" => "mockExtendedActionGroup", - "filename" => "someFile", - "extends" => "mockSimpleActionGroup", + 'nodeName' => 'actionGroup', + 'name' => 'mockExtendedActionGroup', + 'filename' => 'someFile', + 'extends' => 'mockSimpleActionGroup' ]; $mockActionGroupData = [ @@ -327,65 +338,113 @@ public function testExtendingExtendedActionGroup() $this->setMockTestOutput(null, $mockActionGroupData); $this->expectExceptionMessage( - "Cannot extend an action group that already extends another action group. " . $mockSimpleActionGroup['name'] + 'Cannot extend an action group that already extends another action group. ' . $mockSimpleActionGroup['name'] ); // parse and generate test object with mocked data try { ActionGroupObjectHandler::getInstance()->getObject('mockExtendedActionGroup'); - } catch (\Exception $e) { + } catch (Exception $exception) { // validate log statement TestLoggingUtil::getInstance()->validateMockLogStatement( 'error', - "Cannot extend an action group that already extends another action group. " . + 'Cannot extend an action group that already extends another action group. ' . $mockSimpleActionGroup['name'], ['parent' => $mockSimpleActionGroup['name'], 'actionGroup' => $mockExtendedActionGroup['name']] ); - throw $e; + throw $exception; } } + /** + * Tests generating a test that extends a skipped parent test. + * + * @return void + * @throws Exception + */ + public function testExtendedTestSkippedParent(): void + { + $testDataArrayBuilder = new TestDataArrayBuilder(); + $mockParentTest = $testDataArrayBuilder + ->withName('baseTest') + ->withAnnotations([ + 'skip' => ['nodeName' => 'skip', 'issueId' => [['nodeName' => 'issueId', 'value' => 'someIssue']]] + ]) + ->build(); + + $testDataArrayBuilder->reset(); + $mockExtendedTest = $testDataArrayBuilder + ->withName('extendTest') + ->withTestReference('baseTest') + ->build(); + + $mockTestData = array_merge($mockParentTest, $mockExtendedTest); + $this->setMockTestOutput($mockTestData); + + // parse and generate test object with mocked data + TestObjectHandler::getInstance()->getObject('extendTest'); + + // validate log statement + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'debug', + 'extendTest is skipped due to ParentTestIsSkipped', + [] + ); + } + /** * Function used to set mock for parser return and force init method to run between tests. * - * @param array $testData - * @throws \Exception + * @param array|null $testData + * @param array|null $actionGroupData + * + * @return void + * @throws Exception */ - private function setMockTestOutput($testData = null, $actionGroupData = null) + private function setMockTestOutput(?array $testData = null, ?array $actionGroupData = null): void { // clear test object handler value to inject parsed content - $property = new \ReflectionProperty(TestObjectHandler::class, 'testObjectHandler'); + $property = new ReflectionProperty(TestObjectHandler::class, 'testObjectHandler'); $property->setAccessible(true); - $property->setValue(null); + $property->setValue(null, null); // clear test object handler value to inject parsed content - $property = new \ReflectionProperty(ActionGroupObjectHandler::class, 'instance'); + $property = new ReflectionProperty(ActionGroupObjectHandler::class, 'instance'); $property->setAccessible(true); - $property->setValue(null); - - $mockDataParser = AspectMock::double(TestDataParser::class, ['readTestData' => $testData])->make(); - $mockActionGroupParser = AspectMock::double( - ActionGroupDataParser::class, - ['readActionGroupData' => $actionGroupData] - )->make(); - $instance = AspectMock::double( - ObjectManager::class, - [ - 'create' => function ($className) use ( - $mockDataParser, - $mockActionGroupParser - ) { - if ($className == TestDataParser::class) { - return $mockDataParser; - } - if ($className == ActionGroupDataParser::class) { - return $mockActionGroupParser; + $property->setValue(null, null); + + $mockDataParser = $this->createMock(TestDataParser::class); + $mockDataParser + ->method('readTestData') + ->willReturn($testData); + + $mockActionGroupParser = $this->createMock(ActionGroupDataParser::class); + $mockActionGroupParser + ->method('readActionGroupData') + ->willReturn($actionGroupData); + + $instance = $this->createMock(ObjectManager::class); + $instance + ->method('create') + ->will( + $this->returnCallback( + function ($className) use ($mockDataParser, $mockActionGroupParser) { + if ($className === TestDataParser::class) { + return $mockDataParser; + } + + if ($className === ActionGroupDataParser::class) { + return $mockActionGroupParser; + } + + return null; } - } - ] - )->make(); - // bypass the private constructor - AspectMock::double(ObjectManagerFactory::class, ['getObjectManager' => $instance]); + ) + ); + // clear object manager value to inject expected instance + $property = new ReflectionProperty(ObjectManager::class, 'instance'); + $property->setAccessible(true); + $property->setValue(null, $instance); } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Util/ActionMergeUtilTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Util/ActionMergeUtilTest.php index 913b106a7..864149b2f 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Util/ActionMergeUtilTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/ActionMergeUtilTest.php @@ -4,11 +4,11 @@ * See COPYING.txt for license details. */ -namespace Tests\unit\Magento\FunctionalTestFramework\Test\Handlers; +namespace tests\unit\Magento\FunctionalTestFramework\Util; use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; use Magento\FunctionalTestingFramework\Test\Util\ActionMergeUtil; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use tests\unit\Util\MagentoTestCase; class ActionMergeUtilTest extends MagentoTestCase { diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Util/ClassFileNameCheckTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Util/ClassFileNameCheckTest.php new file mode 100644 index 000000000..4c830ca82 --- /dev/null +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/ClassFileNameCheckTest.php @@ -0,0 +1,42 @@ +getAllModulePaths(); + $testXmlFiles = $scriptUtil->getModuleXmlFilesByScope($modulePaths, "Test"); + $classFileNameCheck = new ClassFileNamingCheck(); + $result = $classFileNameCheck->findErrorsInFileSet($testXmlFiles, "test"); + $this->assertMatchesRegularExpression('/does not match with file name/', $result[array_keys($result)[0]][0]); + } + + /** + * This Test checks if the file name is renamed to match the class name if + * mismatch not found in class and file name + */ + public function testClassAndFileMismatchStaticCheckWhenViolationsNotFound() + { + $scriptUtil = new ScriptUtil(); + $modulePaths = $scriptUtil->getAllModulePaths(); + $testXmlFiles = $scriptUtil->getModuleXmlFilesByScope($modulePaths, "Page"); + $classFileNameCheck = new ClassFileNamingCheck(); + $result = $classFileNameCheck->findErrorsInFileSet($testXmlFiles, "page"); + $this->assertEquals(count($result), 0); + } +} diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Util/ComposerModuleResolverTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Util/ComposerModuleResolverTest.php index 3173730b3..39f8c555d 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Util/ComposerModuleResolverTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/ComposerModuleResolverTest.php @@ -4,11 +4,11 @@ * See COPYING.txt for license details. */ -namespace tests\unit\Magento\FunctionalTestFramework\Test\Util; +namespace tests\unit\Magento\FunctionalTestFramework\Util; use ReflectionClass; use Magento\FunctionalTestingFramework\Util\ComposerModuleResolver; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use tests\unit\Util\MagentoTestCase; class ComposerModuleResolverTest extends MagentoTestCase { @@ -98,7 +98,7 @@ public function testFindAllComposerJsonFiles($dir, $expected) * * @return array */ - public function findComposerJsonFilesAtDepthDataProvider() + public static function findComposerJsonFilesAtDepthDataProvider() { $baseDir = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Composer' . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'dir1' . DIRECTORY_SEPARATOR . 'dir2'; @@ -138,7 +138,7 @@ public function findComposerJsonFilesAtDepthDataProvider() * * @return array */ - public function findAllComposerJsonFilesDataProvider() + public static function findAllComposerJsonFilesDataProvider() { $baseDir = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Composer' . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'dir1' . DIRECTORY_SEPARATOR . 'dir2'; diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Util/GenerationErrorHandlerTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Util/GenerationErrorHandlerTest.php new file mode 100644 index 000000000..33cc78eee --- /dev/null +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/GenerationErrorHandlerTest.php @@ -0,0 +1,345 @@ + [ + 'Sameple1Test' => [ + 'message' => 'TestError1', + 'generated' => false, + ], + 'Sameple2Test' => [ + 'message' => 'TestError2', + 'generated' => true, + ] + ], + 'suite' => [ + 'Sameple1Suite' => [ + 'message' => 'SuiteError1', + 'generated' => false, + ], + ] + ]; + + $expectedTestErrors = [ + 'Sameple1Test' => [ + 'message' => 'TestError1', + 'generated' => false, + ], + 'Sameple2Test' => [ + 'message' => 'TestError2', + 'generated' => true, + ] + ]; + + $expectedSuiteErrors = [ + 'Sameple1Suite' => [ + 'message' => 'SuiteError1', + 'generated' => false, + ], + ]; + + GenerationErrorHandler::getInstance()->addError('test', 'Sameple1Test', 'TestError1'); + GenerationErrorHandler::getInstance()->addError('test', 'Sameple2Test', 'TestError2', true); + GenerationErrorHandler::getInstance()->addError('suite', 'Sameple1Suite', 'SuiteError1'); + + // Assert getAllErrors + $this->assertEquals($expectedAllErrors, GenerationErrorHandler::getInstance()->getAllErrors()); + // Assert getErrorsByType + $this->assertEquals($expectedTestErrors, GenerationErrorHandler::getInstance()->getErrorsByType('test')); + $this->assertEquals($expectedSuiteErrors, GenerationErrorHandler::getInstance()->getErrorsByType('suite')); + } + + /** + * Test get errors when some errors have the same key + */ + public function testGetErrorsWithSameKey(): void + { + $expectedAllErrors = [ + 'test' => [ + 'Sameple1Test' => [ + 'message' => [ + 0 => 'TestError1', + 1 => 'TestError3' + ], + 'generated' => [ + 0 => false, + 1 => true + ], + ], + 'Sameple2Test' => [ + 'message' => 'TestError2', + 'generated' => true, + ], + ], + 'suite' => [ + 'Sameple1Suite' => [ + 'message' => [ + 0 => 'SuiteError1', + 1 => 'SuiteError2', + ], + 'generated' => [ + 0 => false, + 1 => false, + ], + ], + ], + ]; + + $expectedTestErrors = [ + 'Sameple1Test' => [ + 'message' => [ + 0 => 'TestError1', + 1 => 'TestError3' + ], + 'generated' => [ + 0 => false, + 1 => true + ], + ], + 'Sameple2Test' => [ + 'message' => 'TestError2', + 'generated' => true, + ], + ]; + + $expectedSuiteErrors = [ + 'Sameple1Suite' => [ + 'message' => [ + 0 => 'SuiteError1', + 1 => 'SuiteError2', + ], + 'generated' => [ + 0 => false, + 1 => false, + ], + ], + ]; + + GenerationErrorHandler::getInstance()->addError('test', 'Sameple1Test', 'TestError1'); + GenerationErrorHandler::getInstance()->addError('suite', 'Sameple1Suite', 'SuiteError1'); + GenerationErrorHandler::getInstance()->addError('test', 'Sameple2Test', 'TestError2', true); + GenerationErrorHandler::getInstance()->addError('suite', 'Sameple1Suite', 'SuiteError2'); + GenerationErrorHandler::getInstance()->addError('test', 'Sameple1Test', 'TestError3', true); + + // Assert getAllErrors + $this->assertEquals($expectedAllErrors, GenerationErrorHandler::getInstance()->getAllErrors()); + // Assert getErrorsByType + $this->assertEquals($expectedTestErrors, GenerationErrorHandler::getInstance()->getErrorsByType('test')); + $this->assertEquals($expectedSuiteErrors, GenerationErrorHandler::getInstance()->getErrorsByType('suite')); + } + + /** + * Test get errors when some errors are duplicate + */ + public function testGetAllErrorsDuplicate(): void + { + $expectedAllErrors = [ + 'test' => [ + 'Sameple1Test' => [ + 'message' => [ + 0 => 'TestError1', + 1 => 'TestError1' + ], + 'generated' => [ + 0 => false, + 1 => false + ], + ], + 'Sameple2Test' => [ + 'message' => 'TestError2', + 'generated' => true, + ], + ], + 'suite' => [ + 'Sameple1Suite' => [ + 'message' => [ + 0 => 'SuiteError1', + 1 => 'SuiteError2', + ], + 'generated' => [ + 0 => false, + 1 => false, + ], + ], + ], + ]; + + $expectedTestErrors = [ + 'Sameple1Test' => [ + 'message' => [ + 0 => 'TestError1', + 1 => 'TestError1' + ], + 'generated' => [ + 0 => false, + 1 => false + ], + ], + 'Sameple2Test' => [ + 'message' => 'TestError2', + 'generated' => true, + ], + ]; + + $expectedSuiteErrors = [ + 'Sameple1Suite' => [ + 'message' => [ + 0 => 'SuiteError1', + 1 => 'SuiteError2', + ], + 'generated' => [ + 0 => false, + 1 => false, + ], + ], + ]; + + GenerationErrorHandler::getInstance()->addError('test', 'Sameple1Test', 'TestError1'); + GenerationErrorHandler::getInstance()->addError('suite', 'Sameple1Suite', 'SuiteError1'); + GenerationErrorHandler::getInstance()->addError('test', 'Sameple2Test', 'TestError2', true); + GenerationErrorHandler::getInstance()->addError('suite', 'Sameple1Suite', 'SuiteError2'); + GenerationErrorHandler::getInstance()->addError('test', 'Sameple1Test', 'TestError1'); + + // Assert getAllErrors + $this->assertEquals($expectedAllErrors, GenerationErrorHandler::getInstance()->getAllErrors()); + // Assert getErrorsByType + $this->assertEquals($expectedTestErrors, GenerationErrorHandler::getInstance()->getErrorsByType('test')); + $this->assertEquals($expectedSuiteErrors, GenerationErrorHandler::getInstance()->getErrorsByType('suite')); + } + + /** + * Test get all error messages + * + * @param string $expectedErrMessages + * @param array $errors + * + * @return void + * @dataProvider getAllErrorMessagesDataProvider + */ + public function testGetAllErrorMessages(string $expectedErrMessages, array $errors): void + { + $handler = GenerationErrorHandler::getInstance(); + $handler->reset(); + + $property = new ReflectionProperty(GenerationErrorHandler::class, 'errors'); + $property->setAccessible(true); + $property->setValue($handler, $errors); + + // Assert getAllErrorMessages + $this->assertEquals($expectedErrMessages, GenerationErrorHandler::getInstance()->getAllErrorMessages()); + } + + /** + * Data provider for testGetAllErrorMessages() + * + * @return array + */ + public static function getAllErrorMessagesDataProvider(): array + { + return [ + ['', []], + ['', [ + 'test' => [], + 'suite' => [], + ] + ], + ['TestError1' + . PHP_EOL + . 'TestError2' + . PHP_EOL + . 'TestError3' + . PHP_EOL + . 'SuiteError1' + . PHP_EOL + . 'SuiteError2' + . PHP_EOL + . 'SuiteError3' + . PHP_EOL + . 'SuiteError4', + [ + 'test' => [ + 'Sameple1Test' => [ + 'message' => [ + 0 => 'TestError1', + 1 => 'TestError2' + ], + 'generated' => [ + 0 => false, + 1 => false + ], + ], + 'Sameple2Test' => [ + 'message' => 'TestError3', + 'generated' => true, + ], + ], + 'suite' => [ + 'Sameple1Suite' => [ + 'message' => 'SuiteError1', + 'generated' => true, + ], + 'Sameple2Suite' => [ + 'message' => [ + 0 => 'SuiteError2', + 1 => 'SuiteError3', + 2 => 'SuiteError4', + ], + 'generated' => [ + 0 => false, + 1 => true, + 2 => false, + ], + ], + ], + ] + ], + ]; + } + + /** + * Test reset + */ + public function testResetError(): void + { + GenerationErrorHandler::getInstance()->addError('something', 'some', 'error'); + GenerationErrorHandler::getInstance()->addError('otherthing', 'other', 'error'); + GenerationErrorHandler::getInstance()->reset(); + + // Assert getAllErrors + $this->assertEquals([], GenerationErrorHandler::getInstance()->getAllErrors()); + // Assert getErrorsByType + $this->assertEquals([], GenerationErrorHandler::getInstance()->getErrorsByType('something')); + $this->assertEquals([], GenerationErrorHandler::getInstance()->getErrorsByType('otherthing')); + $this->assertEquals([], GenerationErrorHandler::getInstance()->getErrorsByType('nothing')); + } + + /** + * @inheritdoc + */ + public function tearDown(): void + { + $property = new ReflectionProperty(GenerationErrorHandler::class, 'instance'); + $property->setAccessible(true); + $property->setValue(null, null); + } +} diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModulePathExtractorTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModulePathExtractorTest.php index 05bc99b89..6ee4a8cdd 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModulePathExtractorTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModulePathExtractorTest.php @@ -3,12 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); -namespace tests\unit\Magento\FunctionalTestFramework\Test\Util; +namespace tests\unit\Magento\FunctionalTestFramework\Util; +use Magento\FunctionalTestingFramework\ObjectManager; use Magento\FunctionalTestingFramework\Util\ModulePathExtractor; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; -use tests\unit\Util\MockModuleResolverBuilder; +use Magento\FunctionalTestingFramework\Util\ModuleResolver; +use ReflectionProperty; +use tests\unit\Util\MagentoTestCase; class ModulePathExtractorTest extends MagentoTestCase { @@ -36,8 +39,7 @@ public function testGetModuleAppCode() { $mockPath = '/base/path/app/code/Magento/ModuleA/Test/Mftf/Test/SomeTest.xml'; - $resolverMock = new MockModuleResolverBuilder(); - $resolverMock->setup($this->mockTestModulePaths); + $this->mockModuleResolver($this->mockTestModulePaths); $extractor = new ModulePathExtractor(); $this->assertEquals('ModuleA', $extractor->extractModuleName($mockPath)); } @@ -51,8 +53,7 @@ public function testGetVendorAppCode() { $mockPath = '/base/path/app/code/VendorB/ModuleB/Test/Mftf/Test/SomeTest.xml'; - $resolverMock = new MockModuleResolverBuilder(); - $resolverMock->setup($this->mockTestModulePaths); + $this->mockModuleResolver($this->mockTestModulePaths); $extractor = new ModulePathExtractor(); $this->assertEquals('VendorB', $extractor->getExtensionPath($mockPath)); } @@ -66,8 +67,7 @@ public function testGetModuleDevTests() { $mockPath = '/base/path/dev/tests/acceptance/tests/functional/Magento/ModuleCTest/Test/SomeTest.xml'; - $resolverMock = new MockModuleResolverBuilder(); - $resolverMock->setup($this->mockTestModulePaths); + $this->mockModuleResolver($this->mockTestModulePaths); $extractor = new ModulePathExtractor(); $this->assertEquals('ModuleC', $extractor->extractModuleName($mockPath)); } @@ -81,8 +81,7 @@ public function testGetVendorDevTests() { $mockPath = '/base/path/dev/tests/acceptance/tests/functional/VendorD/ModuleDTest/Test/SomeTest.xml'; - $resolverMock = new MockModuleResolverBuilder(); - $resolverMock->setup($this->mockTestModulePaths); + $this->mockModuleResolver($this->mockTestModulePaths); $extractor = new ModulePathExtractor(); $this->assertEquals('VendorD', $extractor->getExtensionPath($mockPath)); } @@ -96,8 +95,7 @@ public function testGetModule() { $mockPath = '/base/path/dev/tests/acceptance/tests/functional/FunctionalTest/SomeModuleE/Test/SomeTest.xml'; - $resolverMock = new MockModuleResolverBuilder(); - $resolverMock->setup($this->mockTestModulePaths); + $this->mockModuleResolver($this->mockTestModulePaths); $extractor = new ModulePathExtractor(); $this->assertEquals('NO MODULE DETECTED', $extractor->extractModuleName($mockPath)); } @@ -111,8 +109,7 @@ public function testGetModuleVendorDir() { $mockPath = '/base/path/vendor/magento/module-modulef/Test/Mftf/Test/SomeTest.xml'; - $resolverMock = new MockModuleResolverBuilder(); - $resolverMock->setup($this->mockTestModulePaths); + $this->mockModuleResolver($this->mockTestModulePaths); $extractor = new ModulePathExtractor(); $this->assertEquals('ModuleF', $extractor->extractModuleName($mockPath)); } @@ -126,9 +123,47 @@ public function testGetVendorVendorDir() { $mockPath = '/base/path/vendor/vendorg/module-moduleg-test/Test/SomeTest.xml'; - $resolverMock = new MockModuleResolverBuilder(); - $resolverMock->setup($this->mockTestModulePaths); + $this->mockModuleResolver($this->mockTestModulePaths); $extractor = new ModulePathExtractor(); $this->assertEquals('VendorG', $extractor->getExtensionPath($mockPath)); } + + /** + * Mock module resolver. + * + * @param array $paths + * + * @return void + */ + private function mockModuleResolver(array $paths): void + { + $mockResolver = $this->createMock(ModuleResolver::class); + $mockResolver + ->method('getEnabledModules') + ->willReturn([]); + + $objectManagerMockInstance = $this->createMock(ObjectManager::class); + $objectManagerMockInstance + ->method('create') + ->will( + $this->returnCallback( + function ($class) use ($mockResolver) { + if ($class === ModuleResolver::class) { + return $mockResolver; + } + + return null; + } + ) + ); + + $objectManagerProperty = new ReflectionProperty(ObjectManager::class, 'instance'); + $objectManagerProperty->setAccessible(true); + $objectManagerProperty->setValue(null, $objectManagerMockInstance); + + $resolver = ModuleResolver::getInstance(); + $property = new ReflectionProperty(ModuleResolver::class, 'enabledModuleNameAndPaths'); + $property->setAccessible(true); + $property->setValue($resolver, $paths); + } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModuleResolverTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModuleResolverTest.php index 5244e6995..9809e50ff 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModuleResolverTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/ModuleResolverTest.php @@ -3,80 +3,94 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); -namespace tests\unit\Magento\FunctionalTestFramework\Test\Util; - -use AspectMock\Proxy\Verifier; -use AspectMock\Test as AspectMock; +namespace tests\unit\Magento\FunctionalTestFramework\Util; +use Exception; use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; -use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; -use Magento\FunctionalTestingFramework\ObjectManager; -use Magento\FunctionalTestingFramework\ObjectManagerFactory; -use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; +use Magento\FunctionalTestingFramework\Exceptions\FastFailException; +use Magento\FunctionalTestingFramework\Util\ModuleResolver\ModuleResolverService; use Magento\FunctionalTestingFramework\Util\ModuleResolver; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; -use PHPUnit\Runner\Exception; +use PHPUnit\Framework\MockObject\MockObject; +use ReflectionProperty; +use tests\unit\Util\MagentoTestCase; use tests\unit\Util\TestLoggingUtil; +/** + * Class ModuleResolverTest + */ class ModuleResolverTest extends MagentoTestCase { /** - * Before test functionality - * @return void + * @inheritDoc */ - public function setUp() + protected function setUp(): void { TestLoggingUtil::getInstance()->setMockLoggingUtil(); } /** - * After class functionality - * @return void + * @inheritDoc */ - public static function tearDownAfterClass() + public static function tearDownAfterClass(): void { TestLoggingUtil::getInstance()->clearMockLoggingUtil(); + + $moduleResolverServiceInstance = new ReflectionProperty(ModuleResolverService::class, 'INSTANCE'); + $moduleResolverServiceInstance->setAccessible(true); + $moduleResolverServiceInstance->setValue(null, null); + + $mftfAppConfigInstance = new ReflectionProperty(MftfApplicationConfig::class, 'MFTF_APPLICATION_CONTEXT'); + $mftfAppConfigInstance->setAccessible(true); + $mftfAppConfigInstance->setValue(null, null); } /** - * Validate that Paths that are already set are returned - * @throws \Exception + * Validate that Paths that are already set are returned. + * + * @return void + * @throws Exception */ - public function testGetModulePathsAlreadySet() + public function testGetModulePathsAlreadySet(): void { - $this->setMockResolverClass(); $resolver = ModuleResolver::getInstance(); - $this->setMockResolverProperties($resolver, ["example" . DIRECTORY_SEPARATOR . "paths"]); - $this->assertEquals(["example" . DIRECTORY_SEPARATOR . "paths"], $resolver->getModulesPath()); + $this->setMockResolverProperties($resolver, ['example' . DIRECTORY_SEPARATOR . 'paths']); + $this->assertEquals(['example' . DIRECTORY_SEPARATOR . 'paths'], $resolver->getModulesPath()); } /** - * Validate paths are aggregated correctly - * @throws \Exception + * Validate paths are aggregated correctly. + * + * @return void + * @throws Exception */ - public function testGetModulePathsAggregate() + public function testGetModulePathsAggregate(): void { $this->mockForceGenerate(false); - $this->setMockResolverClass( - false, - null, - null, - null, - [ - 'some' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'example' => ['example'], - 'other' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'sample' => ['sample'], - ], - null, - [ - 'Magento_example' => 'some' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'example', - 'Magento_sample' => 'other' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'sample', - ], - null, - null, - [], - [] - ); + + $moduleResolverService = $this->createPartialMock( + ModuleResolverService::class, + ['getRegisteredModuleList', 'aggregateTestModulePaths'] + ); + $moduleResolverService->expects($this->any()) + ->method('getRegisteredModuleList') + ->willReturn( + [ + 'Magento_example' => 'some' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'example', + 'Magento_sample' => 'other' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'sample' + ] + ); + $moduleResolverService->expects($this->any()) + ->method('aggregateTestModulePaths') + ->willReturn( + [ + 'some' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'example' => ['example'], + 'other' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'sample' => ['sample'] + ] + ); + + $this->setMockResolverCreatorProperties($moduleResolverService); $resolver = ModuleResolver::getInstance(); $this->setMockResolverProperties($resolver, null, [0 => 'Magento_example', 1 => 'Magento_sample']); $this->assertEquals( @@ -89,141 +103,149 @@ public function testGetModulePathsAggregate() } /** - * Validate aggregateTestModulePaths() when module path part of DEV_TESTS + * Validate aggregateTestModulePaths() when module path part of DEV_TESTS. * - * @throws \Exception + * @return void + * @throws Exception */ - public function testAggregateTestModulePathsDevTests() + public function testAggregateTestModulePathsDevTests(): void { $origin = TESTS_MODULE_PATH; $modulePath = ModuleResolver::DEV_TESTS . DIRECTORY_SEPARATOR . "Magento"; putenv("TESTS_MODULE_PATH=$modulePath"); $this->mockForceGenerate(false); - $mockResolver = $this->setMockResolverClass( - true, - [], - null, - null, - [], - [], - [], - null, - null, - [], - [], - null, - function ($arg) { - return $arg; - }, - function ($arg) { - return $arg; - } - ); + $moduleResolverService = $this->createPartialMock(ModuleResolverService::class, ['globRelevantPaths']); + $moduleResolverService + ->method('globRelevantPaths') + ->will( + $this->returnCallback( + function ($codePath, $pattern) use ($modulePath) { + if ($codePath === $modulePath && $pattern === '') { + $this->fail(sprintf( + 'Not expected parameter: \'%s\' when invoked method globRelevantPaths().', + $modulePath + )); + } + + return []; + } + ) + ); + $this->setMockResolverCreatorProperties($moduleResolverService); $resolver = ModuleResolver::getInstance(); - $this->setMockResolverProperties($resolver, null, null); - $this->assertEquals( - [], - $resolver->getModulesPath() - ); - - $mockResolver->verifyNeverInvoked('globRelevantPaths', [$modulePath, '']); + $this->setMockResolverProperties($resolver, null, []); + $this->assertEquals([], $resolver->getModulesPath()); putenv("TESTS_MODULE_PATH=$origin"); } /** - * Validate correct path locations are fed into globRelevantPaths - * @throws \Exception + * Validate correct path locations are fed into globRelevantPaths. + * + * @return void + * @throws Exception */ - public function testGetModulePathsLocations() + public function testGetModulePathsLocations(): void { // clear test object handler value to inject parsed content - $property = new \ReflectionProperty(ModuleResolver::class, 'instance'); + $property = new ReflectionProperty(ModuleResolver::class, 'instance'); $property->setAccessible(true); - $property->setValue(null); + $property->setValue(null, null); $this->mockForceGenerate(false); - $mockResolver = $this->setMockResolverClass( - true, - [], - null, - null, - [], - [], - [], - null, - null, - [], - [], - null, - function ($arg) { - return $arg; - }, - function ($arg) { - return $arg; - } - ); - $resolver = ModuleResolver::getInstance(); - $this->setMockResolverProperties($resolver, null, null); - $this->assertEquals( - [], - $resolver->getModulesPath() - ); - - // Define the Module paths from app/code - $magentoBaseCodePath = MAGENTO_BP; - // Define the Module paths from default TESTS_MODULE_PATH $modulePath = defined('TESTS_MODULE_PATH') ? TESTS_MODULE_PATH : TESTS_BP; - - $mockResolver->verifyInvoked('globRelevantPaths', [$modulePath, '']); - $mockResolver->verifyInvoked( - 'globRelevantPaths', - [$magentoBaseCodePath . DIRECTORY_SEPARATOR . "vendor" , 'Test' . DIRECTORY_SEPARATOR .'Mftf'] - ); - $mockResolver->verifyInvoked( - 'globRelevantPaths', + $invokedWithParams = $expectedParams = [ [ - $magentoBaseCodePath . DIRECTORY_SEPARATOR . "app" . DIRECTORY_SEPARATOR . "code", - 'Test' . DIRECTORY_SEPARATOR .'Mftf' + $modulePath, + '' + ], + [ + MAGENTO_BP . '/vendor', + 'Test/Mftf' + ], + [ + MAGENTO_BP . '/app/code', + 'Test/Mftf' ] - ); + ]; + + $moduleResolverService = $this->createPartialMock(ModuleResolverService::class, ['globRelevantPaths']); + $moduleResolverService + ->method('globRelevantPaths') + ->will( + $this->returnCallback( + function ($codePath, $pattern) use (&$invokedWithParams, $expectedParams) { + foreach ($expectedParams as $key => $parameter) { + list($expectedCodePath, $expectedPattern) = $parameter; + + if ($codePath === $expectedCodePath && $pattern === $expectedPattern) { + if (isset($invokedWithParams[$key])) { + unset($invokedWithParams[$key]); + } + + return []; + } + } + + $this->fail(sprintf( + 'Not expected parameter: [%s] when invoked method globRelevantPaths().', + $codePath . ';' . $pattern + )); + } + ) + ); + + $this->setMockResolverCreatorProperties($moduleResolverService); + $resolver = ModuleResolver::getInstance(); + $this->setMockResolverProperties($resolver, null, []); + $this->assertEquals([], $resolver->getModulesPath()); + + if ($invokedWithParams) { + $parameters = ''; + + foreach ($invokedWithParams as $parameter) { + $parameters .= sprintf('[%s]', implode(';', $parameter)); + } + + $this->fail('The method globRelevantPaths() was not called with expected parameters:' . $parameters); + } } /** - * Validate aggregateTestModulePathsFromComposerJson + * Validate aggregateTestModulePathsFromComposerJson. * - * @throws \Exception + * @return void + * @throws Exception */ - public function testAggregateTestModulePathsFromComposerJson() + public function testAggregateTestModulePathsFromComposerJson(): void { $this->mockForceGenerate(false); - $this->setMockResolverClass( - false, - null, // getEnabledModules - null, // applyCustomMethods - null, // globRelevantWrapper - [], // relevantPath - null, // getCustomModulePaths - null, // getRegisteredModuleList - null, // aggregateTestModulePathsFromComposerJson - [], // aggregateTestModulePathsFromComposerInstaller - [ - 'composer' . DIRECTORY_SEPARATOR . 'json' . DIRECTORY_SEPARATOR . 'pathA' => - [ - 'Magento_ModuleA', - 'Magento_ModuleB' - ], - 'composer' . DIRECTORY_SEPARATOR . 'json' . DIRECTORY_SEPARATOR . 'pathB' => - [ - 'Magento_ModuleB', - 'Magento_ModuleC' - ], - ], // getComposerJsonTestModulePaths - [] // getComposerInstalledTestModulePaths - ); + $moduleResolverService = $this->createPartialMock( + ModuleResolverService::class, + ['getComposerJsonTestModulePaths'] + ); + $moduleResolverService + ->expects($this->any()) + ->method('getComposerJsonTestModulePaths') + ->willReturn( + [ + 'composer' . DIRECTORY_SEPARATOR . 'json' . DIRECTORY_SEPARATOR . 'pathA' => + [ + 'Magento_ModuleA', + 'Magento_ModuleB' + ], + 'composer' . DIRECTORY_SEPARATOR . 'json' . DIRECTORY_SEPARATOR . 'pathB' => + [ + 'Magento_ModuleB', + 'Magento_ModuleC' + ] + ] + ); + + $this->setMockResolverCreatorProperties($moduleResolverService); $resolver = ModuleResolver::getInstance(); $this->setMockResolverProperties($resolver, null, [0 => 'Magento_ModuleB', 1 => 'Magento_ModuleC']); $this->assertEquals( @@ -235,33 +257,14 @@ public function testAggregateTestModulePathsFromComposerJson() } /** - * Validate getComposerJsonTestModulePaths with paths invocation + * Validate getComposerJsonTestModulePaths with paths invocation. * - * @throws \Exception + * @return void + * @throws Exception */ - public function testGetComposerJsonTestModulePathsForPathInvocation() + public function testGetComposerJsonTestModulePathsForPathInvocation(): void { $this->mockForceGenerate(false); - $mockResolver = $this->setMockResolverClass( - false, - [], - null, - null, - [], - null, - null, - null, - null, - [], - [] - ); - - $resolver = ModuleResolver::getInstance(); - $this->setMockResolverProperties($resolver, null, null); - $this->assertEquals( - [], - $resolver->getModulesPath() - ); // Expected dev tests path $expectedSearchPaths[] = MAGENTO_BP @@ -283,42 +286,64 @@ public function testGetComposerJsonTestModulePathsForPathInvocation() $expectedSearchPaths[] = $testModulePath; } - $mockResolver->verifyInvoked('getComposerJsonTestModulePaths', [$expectedSearchPaths]); + $moduleResolverService = $this->createPartialMock( + ModuleResolverService::class, + ['getComposerJsonTestModulePaths'] + ); + $moduleResolverService + ->method('getComposerJsonTestModulePaths') + ->will( + $this->returnCallback( + function ($codePaths) use ($expectedSearchPaths) { + if ($codePaths === $expectedSearchPaths) { + return []; + } + + $this->fail(sprintf( + 'Not expected parameter: \'%s\' when invoked method getComposerJsonTestModulePaths().', + $codePaths + )); + } + ) + ); + + $this->setMockResolverCreatorProperties($moduleResolverService); + $resolver = ModuleResolver::getInstance(); + $this->setMockResolverProperties($resolver, null, []); + $this->assertEquals([], $resolver->getModulesPath()); } /** - * Validate aggregateTestModulePathsFromComposerInstaller + * Validate aggregateTestModulePathsFromComposerInstaller. * - * @throws \Exception + * @return void + * @throws Exception */ - public function testAggregateTestModulePathsFromComposerInstaller() + public function testAggregateTestModulePathsFromComposerInstaller(): void { $this->mockForceGenerate(false); - $this->setMockResolverClass( - false, - null, - null, - null, - [], - null, - null, - null, - null, - [], - [ - 'composer' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR . 'pathA' => - [ - 'Magento_ModuleA', - 'Magento_ModuleB' - ], - 'composer' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR . 'pathB' => - [ - 'Magento_ModuleB', - 'Magento_ModuleC' - ], - ] - ); - + $moduleResolverService = $this->createPartialMock( + ModuleResolverService::class, + ['getComposerInstalledTestModulePaths'] + ); + $moduleResolverService->expects($this->any()) + ->method('getComposerInstalledTestModulePaths') + ->willReturn( + [ + 'composer' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR . 'pathA' => + [ + 'Magento_ModuleA', + 'Magento_ModuleB' + ], + 'composer' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR . 'pathB' => + [ + 'Magento_ModuleB', + 'Magento_ModuleC' + ] + ] + ); + + $this->setMockResolverCreatorProperties($moduleResolverService); $resolver = ModuleResolver::getInstance(); $this->setMockResolverProperties( $resolver, @@ -335,93 +360,104 @@ public function testAggregateTestModulePathsFromComposerInstaller() } /** - * Validate getComposerInstalledTestModulePaths with paths invocation + * Validate getComposerInstalledTestModulePaths with paths invocation. * - * @throws \Exception + * @return void + * @throws Exception */ - public function testGetComposerInstalledTestModulePathsForPathInvocation() + public function testGetComposerInstalledTestModulePathsForPathInvocation(): void { $this->mockForceGenerate(false); - $mockResolver = $this->setMockResolverClass( - false, - [], - null, - null, - [], - null, - null, - null, - null, - [], - [] - ); - - $resolver = ModuleResolver::getInstance(); - $this->setMockResolverProperties($resolver, null, null); - $this->assertEquals( - [], - $resolver->getModulesPath() - ); // Expected file path $expectedSearchPath = MAGENTO_BP . DIRECTORY_SEPARATOR . 'composer.json'; - - $mockResolver->verifyInvoked('getComposerInstalledTestModulePaths', [$expectedSearchPath]); + $moduleResolverService = $this->createPartialMock( + ModuleResolverService::class, + ['getComposerInstalledTestModulePaths'] + ); + $moduleResolverService + ->method('getComposerInstalledTestModulePaths') + ->will( + $this->returnCallback( + function ($composerFile) use ($expectedSearchPath) { + if ($composerFile === $expectedSearchPath) { + return []; + } + + $this->fail(sprintf( + 'Not expected parameter: \'%s\' when invoked method getComposerInstalledTestModulePaths().', + $composerFile + )); + } + ) + ); + + $this->setMockResolverCreatorProperties($moduleResolverService); + $resolver = ModuleResolver::getInstance(); + $this->setMockResolverProperties($resolver, null, []); + $this->assertEquals([], $resolver->getModulesPath()); } /** - * Validate mergeModulePaths() and flipAndFilterModulePathsArray() + * Validate mergeModulePaths() and flipAndFilterModulePathsArray(). * - * @throws \Exception + * @return void + * @throws Exception */ - public function testMergeFlipAndFilterModulePathsNoForceGenerate() + public function testMergeFlipAndFilterModulePathsNoForceGenerate(): void { $this->mockForceGenerate(false); - $this->setMockResolverClass( - false, - null, - null, - null, - null, - null, - null, - null, - null, - [ - 'composer' . DIRECTORY_SEPARATOR . 'json' . DIRECTORY_SEPARATOR . 'pathA' => - [ - 'Magento_ModuleA' - ], - 'composer' . DIRECTORY_SEPARATOR . 'json' . DIRECTORY_SEPARATOR . 'pathB' => - [ - 'Magento_ModuleB' - ], - ], - [ - 'composer' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR . 'pathD' => - [ - 'Magento_ModuleD' - ], - 'composer' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR . 'pathE' => - [ - 'Magento_ModuleE' - ], - 'composer' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR . 'pathC' => - [ - 'Magento_ModuleC', - 'Magento_ModuleB', - ], - ], - [ - 'some' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'example' => ['Magento_Example'], - 'other' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'sample' => ['Magento_Sample'], - 'some' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'path1' => ['Magento_Path1'], - 'other' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'path2' => ['Magento_Path2'], - 'some' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'path3' => ['Magento_Path3'], - 'other' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'path4' => ['Magento_Path4'], - ] - ); - + $moduleResolverService = $this->createPartialMock( + ModuleResolverService::class, + ['getComposerJsonTestModulePaths', 'getComposerInstalledTestModulePaths', 'aggregateTestModulePaths'] + ); + $moduleResolverService->expects($this->any()) + ->method('getComposerJsonTestModulePaths') + ->willReturn( + [ + 'composer' . DIRECTORY_SEPARATOR . 'json' . DIRECTORY_SEPARATOR . 'pathA' => + [ + 'Magento_ModuleA' + ], + 'composer' . DIRECTORY_SEPARATOR . 'json' . DIRECTORY_SEPARATOR . 'pathB' => + [ + 'Magento_ModuleB' + ] + ] + ); + $moduleResolverService->expects($this->any()) + ->method('getComposerInstalledTestModulePaths') + ->willReturn( + [ + 'composer' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR . 'pathD' => + [ + 'Magento_ModuleD' + ], + 'composer' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR . 'pathE' => + [ + 'Magento_ModuleE' + ], + 'composer' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR . 'pathC' => + [ + 'Magento_ModuleC', + 'Magento_ModuleB', + ] + ] + ); + $moduleResolverService->expects($this->any()) + ->method('aggregateTestModulePaths') + ->willReturn( + [ + 'some' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'example' => ['Magento_Example'], + 'other' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'sample' => ['Magento_Sample'], + 'some' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'path1' => ['Magento_Path1'], + 'other' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'path2' => ['Magento_Path2'], + 'some' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'path3' => ['Magento_Path3'], + 'other' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'path4' => ['Magento_Path4'] + ] + ); + + $this->setMockResolverCreatorProperties($moduleResolverService); $resolver = ModuleResolver::getInstance(); $this->setMockResolverProperties( $resolver, @@ -434,7 +470,7 @@ public function testMergeFlipAndFilterModulePathsNoForceGenerate() 4 => 'Magento_ModuleB', 5 => 'Magento_ModuleD', 6 => 'Magento_Otherexample', - 7 => 'Magento_ModuleC', + 7 => 'Magento_ModuleC' ] ); $this->assertEquals( @@ -445,7 +481,7 @@ public function testMergeFlipAndFilterModulePathsNoForceGenerate() 'some' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'example', 'composer' . DIRECTORY_SEPARATOR . 'json' . DIRECTORY_SEPARATOR . 'pathB', 'composer' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR . 'pathD', - 'composer' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR . 'pathC', + 'composer' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR . 'pathC' ], $resolver->getModulesPath() @@ -453,55 +489,62 @@ public function testMergeFlipAndFilterModulePathsNoForceGenerate() } /** - * Validate mergeModulePaths() and flipAndSortModulePathsArray() + * Validate mergeModulePaths() and flipAndSortModulePathsArray(). * - * @throws \Exception + * @return void + * @throws Exception */ - public function testMergeFlipNoSortModulePathsNoForceGenerate() + public function testMergeFlipNoSortModulePathsNoForceGenerate(): void { $this->mockForceGenerate(false); - $this->setMockResolverClass( - false, - null, - null, - null, - null, - null, - null, - null, - null, - [ - 'composer' . DIRECTORY_SEPARATOR . 'json' . DIRECTORY_SEPARATOR - . 'Magento' . DIRECTORY_SEPARATOR . 'ModuleA' => - [ - 'Magento_ModuleA' - ], - 'composer' . DIRECTORY_SEPARATOR . 'json' . DIRECTORY_SEPARATOR - . 'Magento' . DIRECTORY_SEPARATOR . 'ModuleBC' => - [ - 'Magento_ModuleB', - 'Magento_ModuleC', - ], - ], - [ - 'composer' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR - . 'Magento' . DIRECTORY_SEPARATOR . 'ModuleCD' => - [ - 'Magento_ModuleC', - 'Magento_ModuleD' - ], - 'composer' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR - . 'Magento' . DIRECTORY_SEPARATOR . 'ModuleE' => - [ - 'Magento_ModuleE' - ], - ], - [ - 'some' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'example' => ['Magento_Example'], - 'other' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'sample' => ['Magento_Sample'], - ] - ); - + $moduleResolverService = $this->createPartialMock( + ModuleResolverService::class, + ['getComposerJsonTestModulePaths', 'getComposerInstalledTestModulePaths', 'aggregateTestModulePaths'] + ); + $moduleResolverService->expects($this->any()) + ->method('getComposerJsonTestModulePaths') + ->willReturn( + [ + 'composer' . DIRECTORY_SEPARATOR . 'json' . DIRECTORY_SEPARATOR + . 'Magento' . DIRECTORY_SEPARATOR . 'ModuleA' => + [ + 'Magento_ModuleA' + ], + 'composer' . DIRECTORY_SEPARATOR . 'json' . DIRECTORY_SEPARATOR + . 'Magento' . DIRECTORY_SEPARATOR . 'ModuleBC' => + [ + 'Magento_ModuleB', + 'Magento_ModuleC' + ] + ] + ); + $moduleResolverService->expects($this->any()) + ->method('getComposerInstalledTestModulePaths') + ->willReturn( + [ + 'composer' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR + . 'Magento' . DIRECTORY_SEPARATOR . 'ModuleCD' => + [ + 'Magento_ModuleC', + 'Magento_ModuleD' + ], + 'composer' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR + . 'Magento' . DIRECTORY_SEPARATOR . 'ModuleE' => + [ + 'Magento_ModuleE' + ] + ] + ); + $moduleResolverService->expects($this->any()) + ->method('aggregateTestModulePaths') + ->willReturn( + [ + 'some' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'example' => ['Magento_Example'], + 'other' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'sample' => ['Magento_Sample'] + ] + ); + + $this->setMockResolverCreatorProperties($moduleResolverService); $resolver = ModuleResolver::getInstance(); $this->setMockResolverProperties( $resolver, @@ -528,55 +571,62 @@ public function testMergeFlipNoSortModulePathsNoForceGenerate() } /** - * Validate mergeModulePaths() and flipAndSortModulePathsArray() + * Validate mergeModulePaths() and flipAndSortModulePathsArray(). * - * @throws \Exception + * @return void + * @throws Exception */ - public function testMergeFlipAndSortModulePathsForceGenerate() + public function testMergeFlipAndSortModulePathsForceGenerate(): void { $this->mockForceGenerate(true); - $this->setMockResolverClass( - false, - null, - null, - null, - null, - null, - null, - null, - null, - [ - 'composer' . DIRECTORY_SEPARATOR . 'json' . DIRECTORY_SEPARATOR + $moduleResolverService = $this->createPartialMock( + ModuleResolverService::class, + ['getComposerJsonTestModulePaths', 'getComposerInstalledTestModulePaths', 'aggregateTestModulePaths'] + ); + $moduleResolverService->expects($this->any()) + ->method('getComposerJsonTestModulePaths') + ->willReturn( + [ + 'composer' . DIRECTORY_SEPARATOR . 'json' . DIRECTORY_SEPARATOR . 'Magento' . DIRECTORY_SEPARATOR . 'ModuleA' => - [ - 'Magento_ModuleA' - ], - 'composer' . DIRECTORY_SEPARATOR . 'json' . DIRECTORY_SEPARATOR + [ + 'Magento_ModuleA' + ], + 'composer' . DIRECTORY_SEPARATOR . 'json' . DIRECTORY_SEPARATOR . 'Magento' . DIRECTORY_SEPARATOR . 'ModuleBC' => - [ - 'Magento_ModuleB', - 'Magento_ModuleC', - ], - ], - [ - 'composer' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR + [ + 'Magento_ModuleB', + 'Magento_ModuleC' + ] + ] + ); + $moduleResolverService->expects($this->any()) + ->method('getComposerInstalledTestModulePaths') + ->willReturn( + [ + 'composer' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR . 'Magento' . DIRECTORY_SEPARATOR . 'ModuleCD' => - [ - 'Magento_ModuleC', - 'Magento_ModuleD' - ], - 'composer' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR + [ + 'Magento_ModuleC', + 'Magento_ModuleD' + ], + 'composer' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR . 'Magento' . DIRECTORY_SEPARATOR . 'ModuleD' => - [ - 'Magento_ModuleD' - ], - ], - [ - 'some' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'example' => ['Magento_Example'], - 'other' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'sample' => ['Magento_Sample'], - ] - ); - + [ + 'Magento_ModuleD' + ] + ] + ); + $moduleResolverService->expects($this->any()) + ->method('aggregateTestModulePaths') + ->willReturn( + [ + 'some' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'example' => ['Magento_Example'], + 'other' . DIRECTORY_SEPARATOR . 'path' . DIRECTORY_SEPARATOR . 'sample' => ['Magento_Sample'] + ] + ); + + $this->setMockResolverCreatorProperties($moduleResolverService); $resolver = ModuleResolver::getInstance(); $this->setMockResolverProperties( $resolver, @@ -608,45 +658,48 @@ public function testMergeFlipAndSortModulePathsForceGenerate() } /** - * Validate logging warning in flipAndFilterModulePathsArray() + * Validate logging warning in flipAndFilterModulePathsArray(). * - * @throws \Exception + * @return void + * @throws Exception */ - public function testMergeFlipAndFilterModulePathsWithLogging() + public function testMergeFlipAndFilterModulePathsWithLogging(): void { $this->mockForceGenerate(false); - $this->setMockResolverClass( - false, - null, - null, - null, - [], - null, - null, - null, - null, - [ - 'composer' . DIRECTORY_SEPARATOR . 'json' . DIRECTORY_SEPARATOR . 'pathA' => - [ - 'Magento_ModuleA' - ], - 'composer' . DIRECTORY_SEPARATOR . 'json' . DIRECTORY_SEPARATOR . 'pathB' => - [ - 'Magento_ModuleB' - ], - ], - [ - 'composer' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR . 'pathA' => - [ - 'Magento_ModuleA' - ], - 'composer' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR . 'pathB' => - [ - 'Magento_ModuleC' - ], - ] - ); - + $moduleResolverService = $this->createPartialMock( + ModuleResolverService::class, + ['getComposerJsonTestModulePaths', 'getComposerInstalledTestModulePaths'] + ); + $moduleResolverService->expects($this->any()) + ->method('getComposerJsonTestModulePaths') + ->willReturn( + [ + 'composer' . DIRECTORY_SEPARATOR . 'json' . DIRECTORY_SEPARATOR . 'pathA' => + [ + 'Magento_ModuleA' + ], + 'composer' . DIRECTORY_SEPARATOR . 'json' . DIRECTORY_SEPARATOR . 'pathB' => + [ + 'Magento_ModuleB' + ] + ] + ); + $moduleResolverService->expects($this->any()) + ->method('getComposerInstalledTestModulePaths') + ->willReturn( + [ + 'composer' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR . 'pathA' => + [ + 'Magento_ModuleA' + ], + 'composer' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR . 'pathB' => + [ + 'Magento_ModuleC' + ] + ] + ); + + $this->setMockResolverCreatorProperties($moduleResolverService); $resolver = ModuleResolver::getInstance(); $this->setMockResolverProperties( $resolver, @@ -665,37 +718,37 @@ public function testMergeFlipAndFilterModulePathsWithLogging() ], $resolver->getModulesPath() ); + $warnMsg = 'Path: composer' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR; $warnMsg .= 'pathA is ignored by ModuleResolver. ' . PHP_EOL . 'Path: composer' . DIRECTORY_SEPARATOR; $warnMsg .= 'json' . DIRECTORY_SEPARATOR . 'pathA is set for Module: Magento_ModuleA' . PHP_EOL; - TestLoggingUtil::getInstance()->validateMockLogStatement( - 'warning', - $warnMsg, - [] - ); + TestLoggingUtil::getInstance()->validateMockLogStatement('warning', $warnMsg, []); } /** - * Validate custom modules are added - * @throws \Exception + * Validate custom modules are added. + * + * @return void + * @throws Exception */ - public function testApplyCustomModuleMethods() + public function testApplyCustomModuleMethods(): void { - $this->setMockResolverClass( - false, - null, - null, - null, - [], - [ 'Magento_Module' => 'otherPath'], - null, - null, - null, - [], - [] + $this->mockForceGenerate(true); + $moduleResolverService = $this->createPartialMock( + ModuleResolverService::class, + ['getCustomModulePaths', 'aggregateTestModulePaths'] ); + $moduleResolverService->expects($this->any()) + ->method('getCustomModulePaths') + ->willReturn(['Magento_Module' => 'otherPath']); + + $moduleResolverService + ->method('aggregateTestModulePaths') + ->willReturn([]); + + $this->setMockResolverCreatorProperties($moduleResolverService); $resolver = ModuleResolver::getInstance(); - $this->setMockResolverProperties($resolver, null, null, null); + $this->setMockResolverProperties($resolver); $this->assertEquals(['otherPath'], $resolver->getModulesPath()); TestLoggingUtil::getInstance()->validateMockLogStatement( 'info', @@ -705,38 +758,27 @@ public function testApplyCustomModuleMethods() } /** - * Validate blacklisted modules are removed + * Validate blocklisted modules are removed * Module paths are sorted according to module name in alphabetically ascending order * - * @throws \Exception + * @throws Exception */ - public function testGetModulePathsBlacklist() + public function testGetModulePathsBlocklist(): void { - $this->setMockResolverClass( - false, - null, - null, - null, - [], - null, - null, - null, - null, - [], - [], - [ - 'thisPath/some/path4' => ['Some_Module4'], - 'devTests/Magento/path3' => ['Magento_Module3'], - 'appCode/Magento/path2' => ['Magento_Module2'], - 'vendor/amazon/path1' => ['Amazon_Module1'], - ], - function ($arg) { - return $arg; - }, - function ($arg) { - return $arg; - } - ); + $this->mockForceGenerate(true); + $moduleResolverService = $this->createPartialMock(ModuleResolverService::class, ['aggregateTestModulePaths']); + $moduleResolverService->expects($this->any()) + ->method('aggregateTestModulePaths') + ->willReturn( + [ + 'thisPath/some/path4' => ['Some_Module4'], + 'devTests/Magento/path3' => ['Magento_Module3'], + 'appCode/Magento/path2' => ['Magento_Module2'], + 'vendor/amazon/path1' => ['Amazon_Module1'] + ] + ); + + $this->setMockResolverCreatorProperties($moduleResolverService); $resolver = ModuleResolver::getInstance(); $this->setMockResolverProperties($resolver, null, null, ['Magento_Module3']); $this->assertEquals( @@ -751,249 +793,168 @@ function ($arg) { } /** - * Validate that getEnabledModules errors out when no Admin Token is returned and --force is false - * @throws \Exception + * Validate that getEnabledModules errors out when no Admin Token is returned and --force is false. + * + * @return void + * @throws Exception */ - public function testGetModulePathsNoAdminToken() + public function testGetModulePathsNoAdminToken(): void { // Set --force to false $this->mockForceGenerate(false); - // Mock ModuleResolver and $enabledModulesPath - $this->setMockResolverClass( - false, - null, - ["example" . DIRECTORY_SEPARATOR . "paths"], - [], - null, - null, - null, - null, - null, - [], - [] - ); $resolver = ModuleResolver::getInstance(); - $this->setMockResolverProperties($resolver, null, null); + $this->setMockResolverProperties($resolver); - // Cannot Generate if no --force was passed in and no Admin Token is returned succesfully - $this->expectException(TestFrameworkException::class); + // Cannot Generate if no --force was passed in and no Admin Token is returned successfully + $this->expectException(FastFailException::class); $resolver->getModulesPath(); } /** - * Validates that getAdminToken is not called when --force is enabled + * Validates that getAdminToken is not called when --force is enabled. + * + * @return void + * @throws Exception */ - public function testGetAdminTokenNotCalledWhenForce() + public function testGetAdminTokenNotCalledWhenForce(): void { // Set --force to true $this->mockForceGenerate(true); // Mock ModuleResolver and applyCustomModuleMethods() - $mockResolver = $this->setMockResolverClass(); + $moduleResolverService = $this->createMock(ModuleResolverService::class); + $moduleResolverService + ->method('getAdminToken') + ->with( + $this->returnCallback( + function () { + $this->fail('Not expected to call method \'getAdminToken()\'.'); + } + ) + ); + + $this->setMockResolverCreatorProperties($moduleResolverService); $resolver = ModuleResolver::getInstance(); - $this->setMockResolverProperties($resolver, null, null); + $this->setMockResolverProperties($resolver, null, []); $resolver->getModulesPath(); - $mockResolver->verifyNeverInvoked("getAdminToken"); - - // verifyNeverInvoked does not add to assertion count $this->addToAssertionCount(1); } /** * Verify the getAdminToken method returns throws an exception if ENV is not fully loaded. + * + * @return void + * @throws Exception */ - public function testGetAdminTokenWithMissingEnv() + public function testGetAdminTokenWithMissingEnv(): void { - // Set --force to true + // Set --force to false $this->mockForceGenerate(false); + $this->setMockResolverCreatorProperties(null); // Unset env unset($_ENV['MAGENTO_ADMIN_USERNAME']); - - // Mock ModuleResolver and applyCustomModuleMethods() - $mockResolver = $this->setMockResolverClass(); $resolver = ModuleResolver::getInstance(); + $this->setMockResolverProperties($resolver); // Expect exception - $this->expectException(TestFrameworkException::class); + $this->expectException(FastFailException::class); $resolver->getModulesPath(); } /** * Verify the getAdminToken method returns throws an exception if Token was bad. + * + * @return void + * @throws Exception */ - public function testGetAdminTokenWithBadResponse() + public function testGetAdminTokenWithBadResponse(): void { - // Set --force to true + // Set --force to false $this->mockForceGenerate(false); + $this->setMockResolverCreatorProperties(null); - // Mock ModuleResolver and applyCustomModuleMethods() - $mockResolver = $this->setMockResolverClass(); $resolver = ModuleResolver::getInstance(); + $this->setMockResolverProperties($resolver); // Expect exception - $this->expectException(TestFrameworkException::class); + $this->expectException(FastFailException::class); $resolver->getModulesPath(); } /** - * Function used to set mock for parser return and force init method to run between tests. + * Function used to set mock for Resolver properties. + * + * @param ModuleResolver $instance + * @param null $mockPaths + * @param null $mockModules + * @param array $mockBlockList * - * @param string $mockToken - * @param array $mockGetEnabledModules - * @param string[] $mockApplyCustomMethods - * @param string[] $mockGlobRelevantWrapper - * @param string[] $mockRelevantPaths - * @param string[] $mockGetCustomModulePaths - * @param string[] $mockGetRegisteredModuleList - * @param string[] $mockAggregateTestModulePathsFromComposerJson - * @param string[] $mockAggregateTestModulePathsFromComposerInstaller - * @param string[] $mockGetComposerJsonTestModulePaths - * @param string[] $mockGetComposerInstalledTestModulePaths - * @param string[] $mockAggregateTestModulePaths - * @param string[] $mockNormalizeModuleNames - * @param string[] $mockFlipAndFilterModulePathsArray - * @param string[] $mockFlipAndSortModulePathsArray - * @throws \Exception - * @return Verifier ModuleResolver double + * @return void */ - private function setMockResolverClass( - $mockToken = null, - $mockGetEnabledModules = null, - $mockApplyCustomMethods = null, - $mockGlobRelevantWrapper = null, - $mockRelevantPaths = null, - $mockGetCustomModulePaths = null, - $mockGetRegisteredModuleList = null, - $mockAggregateTestModulePathsFromComposerJson = null, - $mockAggregateTestModulePathsFromComposerInstaller = null, - $mockGetComposerJsonTestModulePaths = null, - $mockGetComposerInstalledTestModulePaths = null, - $mockAggregateTestModulePaths = null, - $mockNormalizeModuleNames = null, - $mockFlipAndFilterModulePathsArray = null, - $mockFlipAndSortModulePathsArray = null - ) { - $property = new \ReflectionProperty(ModuleResolver::class, 'instance'); + private function setMockResolverProperties( + ModuleResolver $instance, + $mockPaths = null, + $mockModules = null, + $mockBlockList = [] + ): void { + $property = new ReflectionProperty(ModuleResolver::class, 'enabledModulePaths'); $property->setAccessible(true); - $property->setValue(null); + $property->setValue($instance, $mockPaths); - $mockMethods = []; - if (isset($mockToken)) { - $mockMethods['getAdminToken'] = $mockToken; - } - if (isset($mockGetEnabledModules)) { - $mockMethods['getEnabledModules'] = $mockGetEnabledModules; - } - if (isset($mockApplyCustomMethods)) { - $mockMethods['applyCustomModuleMethods'] = $mockApplyCustomMethods; - } - if (isset($mockGlobRelevantWrapper)) { - $mockMethods['globRelevantWrapper'] = $mockGlobRelevantWrapper; - } - if (isset($mockRelevantPaths)) { - $mockMethods['globRelevantPaths'] = $mockRelevantPaths; - } - if (isset($mockGetCustomModulePaths)) { - $mockMethods['getCustomModulePaths'] = $mockGetCustomModulePaths; - } - if (isset($mockGetRegisteredModuleList)) { - $mockMethods['getRegisteredModuleList'] = $mockGetRegisteredModuleList; - } - if (isset($mockAggregateTestModulePathsFromComposerJson)) { - $mockMethods['aggregateTestModulePathsFromComposerJson'] = $mockAggregateTestModulePathsFromComposerJson; - } - if (isset($mockAggregateTestModulePathsFromComposerInstaller)) { - $mockMethods['aggregateTestModulePathsFromComposerInstaller'] = - $mockAggregateTestModulePathsFromComposerInstaller; - } - if (isset($mockGetComposerJsonTestModulePaths)) { - $mockMethods['getComposerJsonTestModulePaths'] = $mockGetComposerJsonTestModulePaths; - } - if (isset($mockGetComposerInstalledTestModulePaths)) { - $mockMethods['getComposerInstalledTestModulePaths'] = $mockGetComposerInstalledTestModulePaths; - } - if (isset($mockAggregateTestModulePaths)) { - $mockMethods['aggregateTestModulePaths'] = $mockAggregateTestModulePaths; - } - if (isset($mockNormalizeModuleNames)) { - $mockMethods['normalizeModuleNames'] = $mockNormalizeModuleNames; - } - if (isset($mockFlipAndFilterModulePathsArray)) { - $mockMethods['flipAndFilterModulePathsArray'] = $mockFlipAndFilterModulePathsArray; - } - if (isset($mockFlipAndSortModulePathsArray)) { - $mockMethods['flipAndSortModulePathsArray'] = $mockFlipAndSortModulePathsArray; - } - $mockResolver = AspectMock::double( - ModuleResolver::class, - $mockMethods - ); - $instance = AspectMock::double( - ObjectManager::class, - ['create' => $mockResolver->make(), 'get' => null] - )->make(); - // bypass the private constructor - AspectMock::double(ObjectManagerFactory::class, ['getObjectManager' => $instance]); - - return $mockResolver; + $property = new ReflectionProperty(ModuleResolver::class, 'enabledModules'); + $property->setAccessible(true); + $property->setValue($instance, $mockModules); + + $property = new ReflectionProperty(ModuleResolver::class, 'moduleBlocklist'); + $property->setAccessible(true); + $property->setValue($instance, $mockBlockList); } /** - * Function used to set mock for Resolver properties + * Function used to set mock for ResolverCreator properties. * - * @param ModuleResolver $instance - * @param array $mockPaths - * @param array $mockModules - * @param array $mockBlacklist - * @throws \Exception + * @param MockObject|null $moduleResolverService + * + * @return void */ - private function setMockResolverProperties($instance, $mockPaths = null, $mockModules = null, $mockBlacklist = []) + private function setMockResolverCreatorProperties(?MockObject $moduleResolverService): void { - $property = new \ReflectionProperty(ModuleResolver::class, 'enabledModulePaths'); - $property->setAccessible(true); - $property->setValue($instance, $mockPaths); - - $property = new \ReflectionProperty(ModuleResolver::class, 'enabledModules'); - $property->setAccessible(true); - $property->setValue($instance, $mockModules); - - $property = new \ReflectionProperty(ModuleResolver::class, 'moduleBlacklist'); + $property = new ReflectionProperty(ModuleResolverService::class, 'INSTANCE'); $property->setAccessible(true); - $property->setValue($instance, $mockBlacklist); + $property->setValue(null, $moduleResolverService); } /** * Mocks MftfApplicationConfig->forceGenerateEnabled() - * @param $forceGenerate - * @throws \Exception + * @param bool $forceGenerate + * * @return void + * @throws Exception */ - private function mockForceGenerate($forceGenerate) + private function mockForceGenerate(bool $forceGenerate): void { - $mockConfig = AspectMock::double( - MftfApplicationConfig::class, - ['forceGenerateEnabled' => $forceGenerate] - ); - $instance = AspectMock::double( - ObjectManager::class, - ['create' => $mockConfig->make(), 'get' => null] - )->make(); - AspectMock::double(ObjectManagerFactory::class, ['getObjectManager' => $instance]); + $mockConfig = $this->createMock(MftfApplicationConfig::class); + $mockConfig->expects($this->once()) + ->method('forceGenerateEnabled') + ->willReturn($forceGenerate); + + $property = new ReflectionProperty(MftfApplicationConfig::class, 'MFTF_APPLICATION_CONTEXT'); + $property->setAccessible(true); + $property->setValue(null, $mockConfig); } /** - * After method functionality + * After method functionality. + * * @return void */ - protected function tearDown() + protected function tearDown(): void { // re set env if (!isset($_ENV['MAGENTO_ADMIN_USERNAME'])) { - $_ENV['MAGENTO_ADMIN_USERNAME'] = "admin"; + $_ENV['MAGENTO_ADMIN_USERNAME'] = 'admin'; } - - AspectMock::clean(); } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Util/Path/FilePathFormatterTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Util/Path/FilePathFormatterTest.php index bac14113d..9aee9759e 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Util/Path/FilePathFormatterTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/Path/FilePathFormatterTest.php @@ -3,82 +3,100 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Tests\unit\Magento\FunctionalTestFramework\Util\Path; +declare(strict_types=1); + +namespace tests\unit\Magento\FunctionalTestFramework\Util\Path; use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; use Magento\FunctionalTestingFramework\Util\Path\FilePathFormatter; +use tests\unit\Util\MagentoTestCase; class FilePathFormatterTest extends MagentoTestCase { /** - * Test file format + * Test file format. + * + * @param string $path + * @param bool|null $withTrailingSeparator + * @param string|null $expectedPath * - * @dataProvider formatDataProvider - * @param string $path - * @param boolean $withTrailingSeparator - * @param mixed string|boolean $expectedPath * @return void * @throws TestFrameworkException + * @dataProvider formatDataProvider */ - public function testFormat($path, $withTrailingSeparator, $expectedPath) + public function testFormat(string $path, ?bool $withTrailingSeparator, ?string $expectedPath): void { if (null !== $expectedPath) { + if ($withTrailingSeparator === null) { + $this->assertEquals($expectedPath, FilePathFormatter::format($path)); + return; + } $this->assertEquals($expectedPath, FilePathFormatter::format($path, $withTrailingSeparator)); } else { // Assert no exception - FilePathFormatter::format($path, $withTrailingSeparator); + if ($withTrailingSeparator === null) { + FilePathFormatter::format($path); + } else { + FilePathFormatter::format($path, $withTrailingSeparator); + } $this->assertTrue(true); } } /** - * Test file format with exception + * Test file format with exception. + * + * @param string $path + * @param bool|null $withTrailingSeparator * - * @dataProvider formatExceptionDataProvider - * @param string $path - * @param boolean $withTrailingSeparator * @return void * @throws TestFrameworkException + * @dataProvider formatExceptionDataProvider */ - public function testFormatWithException($path, $withTrailingSeparator) + public function testFormatWithException(string $path, ?bool $withTrailingSeparator): void { $this->expectException(TestFrameworkException::class); $this->expectExceptionMessage("Invalid or non-existing file: $path\n"); + + if ($withTrailingSeparator === null) { + FilePathFormatter::format($path); + return; + } FilePathFormatter::format($path, $withTrailingSeparator); } /** - * Data input + * Data input. * * @return array */ - public function formatDataProvider() + public static function formatDataProvider(): array { $path1 = rtrim(TESTS_BP, '/'); $path2 = $path1 . DIRECTORY_SEPARATOR; + return [ - [$path1, null, $path1], + [$path1, null, $path2], [$path1, false, $path1], [$path1, true, $path2], - [$path2, null, $path1], + [$path2, null, $path2], [$path2, false, $path1], [$path2, true, $path2], - [__DIR__. DIRECTORY_SEPARATOR . basename(__FILE__), null, __FILE__], + [__DIR__ . DIRECTORY_SEPARATOR . basename(__FILE__), null, __FILE__ . DIRECTORY_SEPARATOR], ['', null, null] // Empty string is valid ]; } /** - * Invalid data input + * Invalid data input. * * @return array */ - public function formatExceptionDataProvider() + public static function formatExceptionDataProvider(): array { return [ ['abc', null], - ['X://some\dir/@', null], + ['X://some\dir/@', null] ]; } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Util/Path/UrlFormatterTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Util/Path/UrlFormatterTest.php index c66c9558c..e444812ce 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Util/Path/UrlFormatterTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/Path/UrlFormatterTest.php @@ -3,51 +3,62 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Tests\unit\Magento\FunctionalTestFramework\Util\Path; +declare(strict_types=1); + +namespace tests\unit\Magento\FunctionalTestFramework\Util\Path; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; -use Magento\FunctionalTestingFramework\Util\Path\UrlFormatter; use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Util\Path\UrlFormatter; +use tests\unit\Util\MagentoTestCase; class UrlFormatterTest extends MagentoTestCase { /** - * Test url format + * Test url format. * - * @dataProvider formatDataProvider * @param string $path - * @param boolean $withTrailingSeparator - * @param mixed string|boolean $expectedPath + * @param bool|null $withTrailingSeparator + * @param string $expectedPath + * * @return void - * @throws TestFrameworkException + * @dataProvider formatDataProvider */ - public function testFormat($path, $withTrailingSeparator, $expectedPath) + public function testFormat(string $path, ?bool $withTrailingSeparator, string $expectedPath): void { + if ($withTrailingSeparator === null) { + $this->assertEquals($expectedPath, UrlFormatter::format($path)); + return; + } $this->assertEquals($expectedPath, UrlFormatter::format($path, $withTrailingSeparator)); } /** - * Test url format with exception + * Test url format with exception. * - * @dataProvider formatExceptionDataProvider * @param string $path - * @param boolean $withTrailingSeparator + * @param bool|null $withTrailingSeparator + * * @return void - * @throws TestFrameworkException + * @dataProvider formatExceptionDataProvider */ - public function testFormatWithException($path, $withTrailingSeparator) + public function testFormatWithException(string $path, ?bool $withTrailingSeparator): void { $this->expectException(TestFrameworkException::class); $this->expectExceptionMessage("Invalid url: $path\n"); + + if ($withTrailingSeparator === null) { + UrlFormatter::format($path); + return; + } UrlFormatter::format($path, $withTrailingSeparator); } /** - * Data input + * Data input. * * @return array */ - public function formatDataProvider() + public static function formatDataProvider(): array { $url1 = 'http://magento.local/index.php'; $url2 = $url1 . '/'; @@ -58,34 +69,38 @@ public function formatDataProvider() $url7 = 'http://127.0.0.1:8200'; $url8 = 'wwøw.goåoøgle.coøm'; $url9 = 'http://www.google.com'; + return [ - [$url1, null, $url1], + [$url1, null, $url2], [$url1, false, $url1], [$url1, true, $url2], - [$url2, null, $url1], + [$url2, null, $url2], [$url2, false, $url1], [$url2, true, $url2], - [$url3, null, $url3], + [$url3, null, $url4], [$url3, false, $url3], [$url3, true, $url4], - [$url4, null, $url3], + [$url4, null, $url4], [$url4, false, $url3], [$url4, true, $url4], [$url5, true, $url6], [$url7, false, $url7], [$url8, false, $url9], + ['https://magento.local/path?', false, 'https://magento.local/path?'], + ['https://magento.local/path#', false, 'https://magento.local/path#'], + ['https://magento.local/path?#', false, 'https://magento.local/path?#'] ]; } /** - * Invalid data input + * Invalid data input. * * @return array */ - public function formatExceptionDataProvider() + public static function formatExceptionDataProvider(): array { return [ - ['', null], + ['', null] ]; } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Util/Sorter/ParallelGroupSorterTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Util/Sorter/ParallelGroupSorterTest.php index 997bf0c3e..7bd29acab 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Util/Sorter/ParallelGroupSorterTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/Sorter/ParallelGroupSorterTest.php @@ -3,22 +3,26 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Tests\unit\Magento\FunctionalTestFramework\Util\Sorter; +declare(strict_types=1); -use AspectMock\Test as AspectMock; -use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; -use Magento\FunctionalTestingFramework\Suite\Objects\SuiteObject; +namespace tests\unit\Magento\FunctionalTestFramework\Util\Sorter; + +use Magento\FunctionalTestingFramework\Exceptions\FastFailException; use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; use Magento\FunctionalTestingFramework\Test\Objects\TestObject; use Magento\FunctionalTestingFramework\Util\Sorter\ParallelGroupSorter; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use ReflectionProperty; +use tests\unit\Util\MagentoTestCase; class ParallelGroupSorterTest extends MagentoTestCase { /** - * Test a basic sort of available tests based on size + * Test a basic sort of available tests based on size. + * + * @return void + * @throws FastFailException */ - public function testBasicTestGroupSort() + public function testBasicTestsSplitByTime(): void { $sampleTestArray = [ 'test1' => 100, @@ -43,41 +47,24 @@ public function testBasicTestGroupSort() $testSorter = new ParallelGroupSorter(); $actualResult = $testSorter->getTestsGroupedBySize([], $sampleTestArray, 200); - $this->assertCount(5, $actualResult); - foreach ($actualResult as $gropuNumber => $actualTests) { - $expectedTests = $expectedResult[$gropuNumber]; + foreach ($actualResult as $groupNumber => $actualTests) { + $expectedTests = $expectedResult[$groupNumber]; $this->assertEquals($expectedTests, array_keys($actualTests)); } } /** - * Test a sort of both tests and a suite which is larger than the given line limitation + * Test a sort of both tests and a suite which is larger than the given line limitation. + * + * @return void + * @throws FastFailException */ - public function testSortWithSuites() + public function testTestsAndSuitesSplitByTime(): void { // mock tests for test object handler. - $numberOfCalls = 0; - $mockTest1 = AspectMock::double( - TestObject::class, - ['getEstimatedDuration' => function () use (&$numberOfCalls) { - $actionCount = [300, 275]; - $result = $actionCount[$numberOfCalls]; - $numberOfCalls++; - - return $result; - }] - )->make(); - - $mockHandler = AspectMock::double( - TestObjectHandler::class, - ['getObject' => function () use ($mockTest1) { - return $mockTest1; - }] - )->make(); - - AspectMock::double(TestObjectHandler::class, ['getInstance' => $mockHandler])->make(); + $this->createMockForTest(0, [300, 275]); // create test to size array $sampleTestArray = [ @@ -101,8 +88,8 @@ public function testSortWithSuites() $this->assertCount(5, $actualResult); $expectedResults = [ - 1 => ['mockSuite1_0'], - 2 => ['mockSuite1_1'], + 1 => ['mockSuite1_0_G'], + 2 => ['mockSuite1_1_G'], 3 => ['test3'], 4 => ['test2','test5', 'test4'], 5 => ['test1'], @@ -112,4 +99,435 @@ public function testSortWithSuites() $this->assertEquals($expectedResults[$groupNum], array_keys($group)); } } + + /** + * Test splitting tests based on a fixed group number. + * + * @return void + * @throws FastFailException + */ + public function testBasicTestsSplitByGroup(): void + { + $sampleTestArray = [ + 'test1' => 100, + 'test2' => 300, + 'test3' => 50, + 'test4' => 60, + 'test5' => 25, + 'test6' => 125, + 'test7' => 250, + 'test8' => 1, + 'test9' => 80, + 'test10' => 25, + 'test11' => 89, + 'test12' => 69, + 'test13' => 23, + 'test14' => 15, + 'test15' => 25, + 'test16' => 71, + 'test17' => 67, + 'test18' => 34, + 'test19' => 45, + 'test20' => 58, + 'test21' => 9 + ]; + + $expectedResult = [ + 1 => ['test2', 'test8'], + 2 => ['test7', 'test18', 'test14', 'test21'], + 3 => ['test6', 'test12', 'test20', 'test5', 'test10'], + 4 => ['test1', 'test16', 'test4', 'test3', 'test15'], + 5 => ['test11', 'test9', 'test17', 'test19', 'test13'], + ]; + + $testSorter = new ParallelGroupSorter(); + $actualResult = $testSorter->getTestsGroupedByFixedGroupCount([], $sampleTestArray, 5); + $this->assertCount(5, $actualResult); + + foreach ($actualResult as $groupNumber => $actualTests) { + $expectedTests = $expectedResult[$groupNumber]; + $this->assertEquals($expectedTests, array_keys($actualTests)); + } + } + + /** + * Test splitting tests based a group number bigger than ever needed. + * + * @return void + * @throws FastFailException + */ + public function testBasicTestsSplitByBigGroupNumber(): void + { + $sampleTestArray = [ + 'test1' => 100, + 'test2' => 300, + 'test3' => 50, + 'test4' => 60, + 'test5' => 25 + ]; + + $expectedResult = [ + 1 => ['test2'], + 2 => ['test1'], + 3 => ['test4'], + 4 => ['test3'], + 5 => ['test5'] + ]; + + $testSorter = new ParallelGroupSorter(); + $actualResult = $testSorter->getTestsGroupedByFixedGroupCount([], $sampleTestArray, 10); + $this->assertCount(5, $actualResult); + + foreach ($actualResult as $groupNumber => $actualTests) { + $expectedTests = $expectedResult[$groupNumber]; + $this->assertEquals($expectedTests, array_keys($actualTests)); + } + } + + /** + * Test splitting tests based a minimum group number. + * + * @return void + * @throws FastFailException + */ + public function testBasicTestsSplitByMinGroupNumber(): void + { + $sampleTestArray = [ + 'test1' => 100, + 'test2' => 300, + 'test3' => 50, + 'test4' => 60, + 'test5' => 25, + ]; + + $expectedResult = [ + 1 => ['test2', 'test1', 'test4', 'test3', 'test5'] + ]; + + $testSorter = new ParallelGroupSorter(); + $actualResult = $testSorter->getTestsGroupedByFixedGroupCount([], $sampleTestArray, 1); + $this->assertCount(1, $actualResult); + + foreach ($actualResult as $groupNumber => $actualTests) { + $expectedTests = $expectedResult[$groupNumber]; + $this->assertEquals($expectedTests, array_keys($actualTests)); + } + } + + /** + * Test splitting tests and suites based on a fixed group number. + * + * @return void + * @throws FastFailException + */ + public function testTestsAndSuitesSplitByGroup(): void + { + // mock tests for test object handler. + $this->createMockForTest(0); + + // create test to size array + $sampleTestArray = [ + 'test1' => 1, + 'test2' => 125, + 'test3' => 35, + 'test4' => 111, + 'test5' => 43, + 'test6' => 321, + 'test7' => 260, + 'test8' => 5, + 'test9' => 189, + 'test10' => 246, + 'test11' => 98, + 'test12' => 96, + 'test13' => 232, + 'test14' => 51, + 'test15' => 52, + 'test16' => 127, + 'test17' => 76, + 'test18' => 43, + 'test19' => 154, + 'test20' => 85, + 'test21' => 219, + 'test22' => 87, + 'test23' => 65, + 'test24' => 216, + 'test25' => 271, + 'test26' => 99, + 'test27' => 102, + 'test28' => 179, + 'test29' => 243, + 'test30' => 93, + 'test31' => 330, + 'test32' => 85, + 'test33' => 291 + ]; + + // create mock suite references + $sampleSuiteArray = [ + 'mockSuite1' => ['mockTest1', 'mockTest2'] + ]; + + // perform sort + $testSorter = new ParallelGroupSorter(); + $actualResult = $testSorter->getTestsGroupedByFixedGroupCount($sampleSuiteArray, $sampleTestArray, 15); + // verify the resulting groups + $this->assertCount(15, $actualResult); + + $expectedResults = [ + 1 => ['test31', 'test3'], + 2 => ['test6', 'test5'], + 3 => ['test33', 'test17'], + 4 => ['test25', 'test32'], + 5 => ['test7', 'test22'], + 6 => ['test10', 'test30'], + 7 => ['test29', 'test12'], + 8 => ['test13', 'test11', 'test8', 'test1'], + 9 => ['test21', 'test26', 'test14'], + 10 => ['test24', 'test27', 'test18'], + 11 => ['test9', 'test4', 'test23'], + 12 => ['test28', 'test2', 'test15'], + 13 => ['test19', 'test16', 'test20'], + 14 => ['mockSuite1_0_G'], + 15 => ['mockSuite1_1_G'] + ]; + + foreach ($actualResult as $groupNum => $group) { + $this->assertEquals($expectedResults[$groupNum], array_keys($group)); + } + } + + /** + * Test splitting tests and suites based a group number bigger than ever needed. + * + * @return void + * @throws FastFailException + */ + public function testTestsAndSuitesSplitByBigGroupNumber(): void + { + // mock tests for test object handler. + $this->createMockForTest(0); + + // create test to size array + $sampleTestArray = [ + 'test1' => 275, + 'test2' => 190, + 'test3' => 200 + ]; + + // create mock suite references + $sampleSuiteArray = [ + 'mockSuite1' => ['mockTest1', 'mockTest2'] + ]; + + // perform sort + $testSorter = new ParallelGroupSorter(); + $actualResult = $testSorter->getTestsGroupedByFixedGroupCount($sampleSuiteArray, $sampleTestArray, 10); + // verify the resulting groups + $this->assertCount(5, $actualResult); + + $expectedResults = [ + 1 => ['test1'], + 2 => ['test3'], + 3 => ['test2'], + 4 => ['mockSuite1_0_G'], + 5 => ['mockSuite1_1_G'], + ]; + + foreach ($actualResult as $groupNum => $group) { + $this->assertEquals($expectedResults[$groupNum], array_keys($group)); + } + } + + /** + * Test splitting tests and suites based a minimum group number. + * + * @return void + * @throws FastFailException + */ + public function testTestsAndSuitesSplitByMinGroupNumber(): void + { + // mock tests for test object handler. + $this->createMockForTest(0); + + // create test to size array + $sampleTestArray = [ + 'test1' => 1, + 'test2' => 125, + 'test3' => 35 + ]; + + // create mock suite references + $sampleSuiteArray = [ + 'mockSuite1' => ['mockTest1', 'mockTest2'] + ]; + + // perform sort + $testSorter = new ParallelGroupSorter(); + $actualResult = $testSorter->getTestsGroupedByFixedGroupCount($sampleSuiteArray, $sampleTestArray, 3); + // verify the resulting groups + $this->assertCount(3, $actualResult); + + $expectedResults = [ + 1 => ['test2', 'test3', 'test1'], + 2 => ['mockSuite1_0_G'], + 3 => ['mockSuite1_1_G'] + ]; + + foreach ($actualResult as $groupNum => $group) { + $this->assertEquals($expectedResults[$groupNum], array_keys($group)); + } + } + + /** + * Test splitting tests and suites when none of the suites has test. + * For example, this can happen when --filter option is used. + * + * @return void + * @throws FastFailException + */ + public function testTestsAndSuitesSplitByGroupNumberSuiteNoTest(): void + { + // mock tests for test object handler. + $this->createMockForTest(0); + + // create test to size array + $sampleTestArray = [ + 'test1' => 1, + 'test2' => 125, + 'test3' => 35 + ]; + + // create mock suite references + $sampleSuiteArray = [ + 'mockSuite1' => [], + 'mockSuite2' => [], + ]; + + // perform sort + $testSorter = new ParallelGroupSorter(); + $actualResult = $testSorter->getTestsGroupedByFixedGroupCount($sampleSuiteArray, $sampleTestArray, 3); + // verify the resulting groups + $this->assertCount(3, $actualResult); + + $expectedResults = [ + 1 => ['test2'], + 2 => ['test3'], + 3 => ['test1'] + ]; + + foreach ($actualResult as $groupNum => $group) { + $this->assertEquals($expectedResults[$groupNum], array_keys($group)); + } + } + + /** + * Test splitting into minimum groups. + * + * @return void + */ + public function testSplitMinGroups(): void + { + // mock tests for test object handler. + $this->createMockForTest(0); + + // create test to size array + $sampleTestArray = [ + 'test1' => 1, + 'test2' => 125, + 'test3' => 35 + ]; + // create mock suite references + $sampleSuiteArray = [ + 'mockSuite1' => ['mockTest1', 'mockTest2'], + 'mockSuite2' => ['mockTest3', 'mockTest4'], + ]; + + // perform sort + $testSorter = new ParallelGroupSorter(); + $actualResult = $testSorter->getTestsGroupedByFixedGroupCount($sampleSuiteArray, $sampleTestArray, 3); + // verify the resulting groups + $this->assertCount(3, $actualResult); + + $expectedResults = [ + 1 => ['test2', 'test3', 'test1'], + 2 => ['mockSuite1'], + 3 => ['mockSuite2'], + ]; + + foreach ($actualResult as $groupNum => $group) { + $this->assertEquals($expectedResults[$groupNum], array_keys($group)); + } + } + + /** + * Test splitting tests and suites with invalid group number. + * + * @return void + */ + public function testTestsAndSuitesSplitByInvalidGroupNumber(): void + { + // mock tests for test object handler. + $this->createMockForTest(0); + + // create test to size array + $sampleTestArray = [ + 'test1' => 1, + 'test2' => 125, + 'test3' => 35 + ]; + // create mock suite references + $sampleSuiteArray = [ + 'mockSuite1' => ['mockTest1', 'mockTest2'], + 'mockSuite2' => ['mockTest3', 'mockTest4'], + ]; + + $this->expectException(FastFailException::class); + $this->expectExceptionMessage("Invalid parameter 'groupTotal': must be equal or greater than 3"); + + // perform sort + $testSorter = new ParallelGroupSorter(); + $testSorter->getTestsGroupedByFixedGroupCount($sampleSuiteArray, $sampleTestArray, 2); + } + + /** + * @inheritDoc + */ + public static function tearDownAfterClass(): void + { + $instanceProperty = new ReflectionProperty(TestObjectHandler::class, 'testObjectHandler'); + $instanceProperty->setAccessible(true); + $instanceProperty->setValue(null, null); + } + + /** + * Mock test object and test object handler. + * + * @param int $numberOfCalls + * @param array $actionCount + * + * @return void + */ + private function createMockForTest(int $numberOfCalls, array $actionCount = [300, 275, 300, 275]): void + { + $mockTest1 = $this->createMock(TestObject::class); + $mockTest1 + ->method('getEstimatedDuration') + ->willReturnCallback( + function () use (&$numberOfCalls, $actionCount) { + $result = $actionCount[$numberOfCalls]; + $numberOfCalls++; + + return $result; + } + ); + + $mockHandler = $this->createMock(TestObjectHandler::class); + $mockHandler + ->method('getObject') + ->willReturn($mockTest1); + + $instanceProperty = new ReflectionProperty(TestObjectHandler::class, 'testObjectHandler'); + $instanceProperty->setAccessible(true); + $instanceProperty->setValue($mockHandler, $mockHandler); + } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Util/TestGeneratorTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Util/TestGeneratorTest.php index e11b0f9b2..781e69584 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Util/TestGeneratorTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/TestGeneratorTest.php @@ -3,61 +3,177 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); -namespace Tests\unit\Magento\FunctionalTestFramework\Test\Handlers; - -use AspectMock\Test as AspectMock; +namespace tests\unit\Magento\FunctionalTestFramework\Util; +use Exception; +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; use Magento\FunctionalTestingFramework\Filter\FilterList; +use Magento\FunctionalTestingFramework\ObjectManager; +use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; use Magento\FunctionalTestingFramework\Test\Objects\TestHookObject; use Magento\FunctionalTestingFramework\Test\Objects\TestObject; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use Magento\FunctionalTestingFramework\Util\Filesystem\CestFileCreatorUtil; +use Magento\FunctionalTestingFramework\Util\GenerationErrorHandler; +use Magento\FunctionalTestingFramework\Util\ModuleResolver; use Magento\FunctionalTestingFramework\Util\TestGenerator; -use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use ReflectionClass; +use ReflectionProperty; +use tests\unit\Util\MagentoTestCase; +use tests\unit\Util\TestLoggingUtil; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; class TestGeneratorTest extends MagentoTestCase { /** - * After method functionality + * @inheritdoc + */ + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + + $property = new ReflectionProperty(ObjectManager::class, 'instance'); + $property->setAccessible(true); + $property->setValue(null, null); + + $property = new ReflectionProperty(ModuleResolver::class, 'instance'); + $property->setAccessible(true); + $property->setValue(null, null); + } + + /** + * Before method functionality. * * @return void */ - public function tearDown() + protected function setUp(): void { - AspectMock::clean(); + TestLoggingUtil::getInstance()->setMockLoggingUtil(); + } + + /** + * After method functionality. + * + * @return void + */ + protected function tearDown(): void + { + GenerationErrorHandler::getInstance()->reset(); } /** * Basic test to check exceptions for incorrect entities. * - * @throws \Exception + * @return void + * @throws Exception */ - public function testEntityException() + public function testEntityException(): void { $actionObject = new ActionObject('fakeAction', 'comment', [ 'userInput' => '{{someEntity.entity}}' ]); - $testObject = new TestObject("sampleTest", ["merge123" => $actionObject], [], [], "filename"); + $testObject = new TestObject('sampleTest', ['merge123' => $actionObject], [], [], 'filename'); + $testGeneratorObject = TestGenerator::getInstance('', ['sampleTest' => $testObject]); + $this->mockTestObjectHandler(); + + $testGeneratorObject->createAllTestFiles(null, []); + + // assert that no exception for createAllTestFiles and generation error is stored in GenerationErrorHandler + $errorMessage = '/' . preg_quote('Removed invalid test object sampleTest') . '/'; + TestLoggingUtil::getInstance()->validateMockLogStatmentRegex('error', $errorMessage, []); + $testErrors = GenerationErrorHandler::getInstance()->getErrorsByType('test'); + $this->assertArrayHasKey('sampleTest', $testErrors); + } + + /** + * Basic test to check unique id is appended to input as prefix + * + * @return void + * @throws Exception + */ + public function testUniqueIdAppendedToInputStringAsPrefix() + { + $actionObject = new ActionObject('fakeAction', 'comment', [ + 'userInput' => '{{someEntity.entity}}' + ]); - $testGeneratorObject = TestGenerator::getInstance("", ["sampleTest" => $testObject]); + $testObject = new TestObject('sampleTest', ['merge123' => $actionObject], [], [], 'filename'); + $testGeneratorObject = TestGenerator::getInstance('', ['sampleTest' => $testObject]); - AspectMock::double(TestGenerator::class, ['loadAllTestObjects' => ["sampleTest" => $testObject]]); + $result = $testGeneratorObject->getUniqueIdForInput('prefix', "foo"); + + $this->assertMatchesRegularExpression('/[A-Za-z0-9]+foo/', $result); + } - $this->expectExceptionMessage("Could not resolve entity reference \"{{someEntity.entity}}\" " . - "in Action with stepKey \"fakeAction\".\n" . - "Exception occurred parsing action at StepKey \"fakeAction\" in Test \"sampleTest\""); + /** + * Basic test to check if exception is thrown when invalid entity is found in xml file + * + * @return void + * @throws Exception + */ + public function testInvalidEntity() + { + $actionObject = new ActionObject('fakeAction', 'comment', [ + 'userInput' => '{{someEntity.entity}}' + ]); - $testGeneratorObject->createAllTestFiles(null, []); + $testObject = new TestObject('sampleTest', ['merge123' => $actionObject], [], [], 'filename'); + $testGeneratorObject = TestGenerator::getInstance('', ['sampleTest' => $testObject]); + $this->expectException(TestReferenceException::class); + $result = $testGeneratorObject->entityExistsCheck('testintity', "teststepkey"); } /** - * Tests that skipped tests do not have a fully generated body + * Basic test to check unique id is appended to input as suffix * - * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + * @return void + * @throws Exception */ - public function testSkippedNoGeneration() + public function testUniqueIdAppendedToInputStringAsSuffix() + { + $actionObject = new ActionObject('fakeAction', 'comment', [ + 'userInput' => '{{someEntity.entity}}' + ]); + + $testObject = new TestObject('sampleTest', ['merge123' => $actionObject], [], [], 'filename'); + $testGeneratorObject = TestGenerator::getInstance('', ['sampleTest' => $testObject]); + + $result = $testGeneratorObject->getUniqueIdForInput('suffix', "foo"); + + $this->assertMatchesRegularExpression('/foo[A-Za-z0-9]+/', $result); + } + + /** + * Basic test for wrong output for input + * + * @return void + * @throws Exception + */ + public function testFailedRegexForUniqueAttribute() + { + $actionObject = new ActionObject('fakeAction', 'comment', [ + 'userInput' => '{{someEntity.entity}}' + ]); + + $testObject = new TestObject('sampleTest', ['merge123' => $actionObject], [], [], 'filename'); + $testGeneratorObject = TestGenerator::getInstance('', ['sampleTest' => $testObject]); + + $result = $testGeneratorObject->getUniqueIdForInput('suffix', "foo"); + + $this->assertDoesNotMatchRegularExpression('/bar[A-Za-z0-9]+/', $result); + } + + /** + * Tests that skipped tests do not have a fully generated body. + * + * @return void + * @throws TestReferenceException + */ + public function testSkippedNoGeneration(): void { $actionInput = 'fakeInput'; $actionObject = new ActionObject('fakeAction', 'comment', [ @@ -65,24 +181,32 @@ public function testSkippedNoGeneration() ]); $annotations = ['skip' => ['issue']]; - $testObject = new TestObject("sampleTest", ["merge123" => $actionObject], $annotations, [], "filename"); + $testObject = new TestObject('sampleTest', ['merge123' => $actionObject], $annotations, [], 'filename'); - $testGeneratorObject = TestGenerator::getInstance("", ["sampleTest" => $testObject]); + $testGeneratorObject = TestGenerator::getInstance('', ['sampleTest' => $testObject]); $output = $testGeneratorObject->assembleTestPhp($testObject); - $this->assertContains('This test is skipped', $output); - $this->assertNotContains($actionInput, $output); + $this->assertStringContainsString('This test is skipped', $output); + $this->assertStringNotContainsString($actionInput, $output); } /** - * Tests that skipped tests have a fully generated body when --allowSkipped is passed in + * Tests that skipped tests have a fully generated body when --allowSkipped is passed in. * - * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + * @return void + * @throws TestReferenceException */ - public function testAllowSkipped() + public function testAllowSkipped(): void { // Mock allowSkipped for TestGenerator - AspectMock::double(MftfApplicationConfig::class, ['allowSkipped' => true]); + $mockConfig = $this->createMock(MftfApplicationConfig::class); + $mockConfig + ->method('allowSkipped') + ->willReturn(true); + + $property = new ReflectionProperty(MftfApplicationConfig::class, 'MFTF_APPLICATION_CONTEXT'); + $property->setAccessible(true); + $property->setValue(null, $mockConfig); $actionInput = 'fakeInput'; $actionObject = new ActionObject('fakeAction', 'comment', [ @@ -94,35 +218,40 @@ public function testAllowSkipped() ]); $annotations = ['skip' => ['issue']]; - $beforeHook = new TestHookObject("before", "sampleTest", ['beforeAction' => $beforeActionObject]); + $beforeHook = new TestHookObject('before', 'sampleTest', ['beforeAction' => $beforeActionObject]); $testObject = new TestObject( - "sampleTest", - ["fakeAction" => $actionObject], + 'sampleTest', + ['fakeAction' => $actionObject], $annotations, - ["before" => $beforeHook], - "filename" + ['before' => $beforeHook], + 'filename' ); - $testGeneratorObject = TestGenerator::getInstance("", ["sampleTest" => $testObject]); + $testGeneratorObject = TestGenerator::getInstance('', ['sampleTest' => $testObject]); $output = $testGeneratorObject->assembleTestPhp($testObject); - $this->assertNotContains('This test is skipped', $output); - $this->assertContains($actionInput, $output); - $this->assertContains($beforeActionInput, $output); + $this->assertStringNotContainsString('This test is skipped', $output); + $this->assertStringContainsString($actionInput, $output); + $this->assertStringContainsString($beforeActionInput, $output); } /** - * Tests that TestGenerator createAllTestFiles correctly filters based on severity + * Tests that TestGenerator createAllTestFiles correctly filters based on severity. * - * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + * @return void + * @throws TestReferenceException */ - public function testFilter() + public function testSeverityFilter(): void { - // Mock filters for TestGenerator - AspectMock::double( - MftfApplicationConfig::class, - ['getFilterList' => new FilterList(['severity' => ["CRITICAL"]])] - ); + $mockConfig = $this->createMock(MftfApplicationConfig::class); + $fileList = new FilterList(['severity' => ['CRITICAL']]); + $mockConfig + ->method('getFilterList') + ->willReturn($fileList); + + $property = new ReflectionProperty(MftfApplicationConfig::class, 'MFTF_APPLICATION_CONTEXT'); + $property->setAccessible(true); + $property->setValue(null, $mockConfig); $actionInput = 'fakeInput'; $actionObject = new ActionObject('fakeAction', 'comment', [ @@ -132,32 +261,306 @@ public function testFilter() $annotation1 = ['severity' => ['CRITICAL']]; $annotation2 = ['severity' => ['MINOR']]; $test1 = new TestObject( - "test1", - ["fakeAction" => $actionObject], + 'test1', + ['fakeAction' => $actionObject], $annotation1, [], - "filename" + 'filename' ); $test2 = new TestObject( - "test2", - ["fakeAction" => $actionObject], + 'test2', + ['fakeAction' => $actionObject], $annotation2, [], - "filename" + 'filename' ); - AspectMock::double(TestGenerator::class, ['loadAllTestObjects' => ["sampleTest" => $test1, "test2" => $test2]]); // Mock createCestFile to return name of tests that testGenerator tried to create $generatedTests = []; - AspectMock::double(TestGenerator::class, ['createCestFile' => function ($arg1, $arg2) use (&$generatedTests) { - $generatedTests[$arg2] = true; - }]); + $cestFileCreatorUtil = $this->createMock(CestFileCreatorUtil::class); + $cestFileCreatorUtil + ->method('create') + ->will( + $this->returnCallback( + function ($filename) use (&$generatedTests) { + $generatedTests[$filename] = true; + } + ) + ); - $testGeneratorObject = TestGenerator::getInstance("", ["sampleTest" => $test1, "test2" => $test2]); - $testGeneratorObject->createAllTestFiles(null, []); + $property = new ReflectionProperty(CestFileCreatorUtil::class, 'INSTANCE'); + $property->setAccessible(true); + $property->setValue(null, $cestFileCreatorUtil); + + $testGeneratorObject = TestGenerator::getInstance('', ['sampleTest' => $test1, 'test2' => $test2]); + $testGeneratorObject->createAllTestFiles(); + + // Ensure Test1 was Generated but not Test 2 + $this->assertArrayHasKey('test1Cest', $generatedTests); + $this->assertArrayNotHasKey('test2Cest', $generatedTests); + } + + /** + * Test for exception thrown when duplicate arguments found + * + * @return void + * @throws TestFrameworkException + */ + public function testIfExceptionThrownWhenDuplicateArgumentsFound() + { + $fileContents = ' + + + + + + + + {{count}} + grabProducts1 + + + + '; + $actionInput = 'fakeInput'; + $actionObject = new ActionObject('fakeAction', 'comment', [ + 'userInput' => $actionInput + ]); + $annotation1 = ['group' => ['someGroupValue']]; + + $test1 = new TestObject( + 'test1', + ['fakeAction' => $actionObject], + $annotation1, + [], + 'filename' + ); + $annotation2 = ['group' => ['someOtherGroupValue']]; + + $test2 = new TestObject( + 'test2', + ['fakeAction' => $actionObject], + $annotation2, + [], + 'filename' + ); + $testGeneratorObject = TestGenerator::getInstance('', ['sampleTest' => $test1, 'test2' => $test2]); + $result = $testGeneratorObject->throwExceptionIfDuplicateArgumentsFound($testGeneratorObject); + $this->assertEquals($result, ""); + } + + /** + * Test for exception not thrown when duplicate arguments not found + * + * @return void + */ + public function testIfExceptionNotThrownWhenDuplicateArgumentsNotFound() + { + $fileContents = ' + + + + + + + {{count}} + grabProducts1 + + + + '; + $actionInput = 'fakeInput'; + $actionObject = new ActionObject('fakeAction', 'comment', [ + 'userInput' => $actionInput + ]); + $annotation1 = ['group' => ['someGroupValue']]; + + $test1 = new TestObject( + 'test1', + ['fakeAction' => $actionObject], + $annotation1, + [], + 'filename' + ); + $annotation2 = ['group' => ['someOtherGroupValue']]; + + $test2 = new TestObject( + 'test2', + ['fakeAction' => $actionObject], + $annotation2, + [], + 'filename' + ); + $testGeneratorObject = TestGenerator::getInstance('', ['sampleTest' => $test1, 'test2' => $test2]); + $result = $testGeneratorObject->throwExceptionIfDuplicateArgumentsFound($testGeneratorObject); + $this->assertEquals($result, ""); + } + + /** + * Tests that TestGenerator createAllTestFiles correctly filters based on group. + * + * @return void + * @throws TestReferenceException + */ + public function testIncludeGroupFilter(): void + { + $mockConfig = $this->createMock(MftfApplicationConfig::class); + $fileList = new FilterList(['includeGroup' => ['someGroupValue']]); + $mockConfig + ->method('getFilterList') + ->willReturn($fileList); + + $property = new ReflectionProperty(MftfApplicationConfig::class, 'MFTF_APPLICATION_CONTEXT'); + $property->setAccessible(true); + $property->setValue(null, $mockConfig); + + $actionInput = 'fakeInput'; + $actionObject = new ActionObject('fakeAction', 'comment', [ + 'userInput' => $actionInput + ]); + + $annotation1 = ['group' => ['someGroupValue']]; + $annotation2 = ['group' => ['someOtherGroupValue']]; + $test1 = new TestObject( + 'test1', + ['fakeAction' => $actionObject], + $annotation1, + [], + 'filename' + ); + $test2 = new TestObject( + 'test2', + ['fakeAction' => $actionObject], + $annotation2, + [], + 'filename' + ); + + // Mock createCestFile to return name of tests that testGenerator tried to create + $generatedTests = []; + $cestFileCreatorUtil = $this->createMock(CestFileCreatorUtil::class); + $cestFileCreatorUtil + ->method('create') + ->will( + $this->returnCallback( + function ($filename) use (&$generatedTests) { + $generatedTests[$filename] = true; + } + ) + ); + + $property = new ReflectionProperty(CestFileCreatorUtil::class, 'INSTANCE'); + $property->setAccessible(true); + $property->setValue(null, $cestFileCreatorUtil); + + $testGeneratorObject = TestGenerator::getInstance('', ['sampleTest' => $test1, 'test2' => $test2]); + $testGeneratorObject->createAllTestFiles(); // Ensure Test1 was Generated but not Test 2 $this->assertArrayHasKey('test1Cest', $generatedTests); $this->assertArrayNotHasKey('test2Cest', $generatedTests); } + + /** + * Tests that TestGenerator createAllTestFiles correctly filters based on group not included. + * + * @return void + * @throws TestReferenceException + */ + public function testExcludeGroupFilter(): void + { + $mockConfig = $this->createMock(MftfApplicationConfig::class); + $fileList = new FilterList(['excludeGroup' => ['someGroupValue']]); + $mockConfig + ->method('getFilterList') + ->willReturn($fileList); + + $property = new ReflectionProperty(MftfApplicationConfig::class, 'MFTF_APPLICATION_CONTEXT'); + $property->setAccessible(true); + $property->setValue(null, $mockConfig); + + $actionInput = 'fakeInput'; + $actionObject = new ActionObject('fakeAction', 'comment', [ + 'userInput' => $actionInput + ]); + + $annotation1 = ['group' => ['someGroupValue']]; + $annotation2 = ['group' => ['someOtherGroupValue']]; + $test1 = new TestObject( + 'test1', + ['fakeAction' => $actionObject], + $annotation1, + [], + 'filename' + ); + $test2 = new TestObject( + 'test2', + ['fakeAction' => $actionObject], + $annotation2, + [], + 'filename' + ); + + // Mock createCestFile to return name of tests that testGenerator tried to create + $generatedTests = []; + $cestFileCreatorUtil = $this->createMock(CestFileCreatorUtil::class); + $cestFileCreatorUtil + ->method('create') + ->will( + $this->returnCallback( + function ($filename) use (&$generatedTests) { + $generatedTests[$filename] = true; + } + ) + ); + + $property = new ReflectionProperty(CestFileCreatorUtil::class, 'INSTANCE'); + $property->setAccessible(true); + $property->setValue(null, $cestFileCreatorUtil); + + $testGeneratorObject = TestGenerator::getInstance('', ['sampleTest' => $test1, 'test2' => $test2]); + $testGeneratorObject->createAllTestFiles(); + + // Ensure Test2 was Generated but not Test 1 + $this->assertArrayNotHasKey('test1Cest', $generatedTests); + $this->assertArrayHasKey('test2Cest', $generatedTests); + } + + /** + * @inheritDoc + */ + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + + $cestFileCreatorUtilInstance = new ReflectionProperty(CestFileCreatorUtil::class, 'INSTANCE'); + $cestFileCreatorUtilInstance->setAccessible(true); + $cestFileCreatorUtilInstance->setValue(null, null); + + $mftfAppConfigInstance = new ReflectionProperty(MftfApplicationConfig::class, 'MFTF_APPLICATION_CONTEXT'); + $mftfAppConfigInstance->setAccessible(true); + $mftfAppConfigInstance->setValue(null, null); + + $property = new ReflectionProperty(TestObjectHandler::class, 'testObjectHandler'); + $property->setAccessible(true); + $property->setValue(null, null); + } + + /** + * Mock test object handler for test. + */ + private function mockTestObjectHandler(): void + { + $testObjectHandlerClass = new ReflectionClass(TestObjectHandler::class); + $testObjectHandlerConstructor = $testObjectHandlerClass->getConstructor(); + $testObjectHandlerConstructor->setAccessible(true); + $testObjectHandler = $testObjectHandlerClass->newInstanceWithoutConstructor(); + $testObjectHandlerConstructor->invoke($testObjectHandler); + + $property = new ReflectionProperty(TestObjectHandler::class, 'testObjectHandler'); + $property->setAccessible(true); + $property->setValue(null, $testObjectHandler); + } } diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Util/UnusedEntityCheckTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Util/UnusedEntityCheckTest.php new file mode 100644 index 000000000..8d0d8995d --- /dev/null +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/UnusedEntityCheckTest.php @@ -0,0 +1,190 @@ +unusedEntities(); + $this->assertIsArray($result); + } + + public function testUnusedActiongroupFiles() + { + $unusedEntityCheck = new UnusedEntityCheck(); + $domDocument = new \DOMDocument(); + $actionGroupFiles = ['DeprecationCheckActionGroup' => + '/verification/DeprecationCheckModule/ActionGroup/DeprecationCheckActionGroup.xml', + 'ActionGroupWithMultiplePausesActionGroup'=> + '/verification/PauseCheckModule/ActionGroup/ActionGroupWithMultiplePausesActionGroup.xml', + 'ActionGroupWithNoPauseActionGroup'=> + '/verification/PauseCheckModule/ActionGroup/ActionGroupWithNoPauseActionGroup.xml']; + $result = $unusedEntityCheck->unusedActionEntity($domDocument, [], [], $actionGroupFiles, []); + $this->assertIsArray($result); + $this->assertCount(3, $result); + } + + public function testUnusedActiongroupFilesReturnedWhenActionXmlFilesAreNotEmpty() + { + $scriptUtil = new ScriptUtil(); + $unusedEntityCheck = new UnusedEntityCheck(); + $domDocument = new \DOMDocument(); + $modulePaths = $scriptUtil->getAllModulePaths(); + $actionGroupXmlFiles = $scriptUtil->getModuleXmlFilesByScope($modulePaths, "ActionGroup"); + $actionGroupFiles = ['DeprecationCheckActionGroup' => + '/verification/DeprecationCheckModule/ActionGroup/DeprecationCheckActionGroup.xml', + 'ActionGroupWithMultiplePausesActionGroup'=> + '/verification/PauseCheckModule/ActionGroup/ActionGroupWithMultiplePausesActionGroup.xml', + 'ActionGroupWithNoPauseActionGroup' => + '/verification/PauseCheckModule/ActionGroup/ActionGroupWithNoPauseActionGroup.xml']; + $result = $unusedEntityCheck->unusedActionEntity( + $domDocument, + $actionGroupXmlFiles, + [], + $actionGroupFiles, + [] + ); + $this->assertIsArray($result); + $this->assertCount(3, $result); + } + + public function testUnusedSectionFiles() + { + $unusedEntityCheck = new UnusedEntityCheck(); + $domDocument = new \DOMDocument(); + $section = [ + 'DeprecationCheckSection' => '/verification/DeprecationCheckModule/Section/DeprecationCheckSection.xml', + 'DeprecatedSection' => '/verification/TestModule/Section/DeprecatedSection.xml', + 'LocatorFunctionSection' => '/verification/TestModule/Section/LocatorFunctionSection.xml', + 'SampleSection' => '/verification/TestModuleMerged/Section/MergeSection.xml' + ]; + $result=$unusedEntityCheck->unusedSectionEntity($domDocument, [], [], [], $section, []); + $this->assertIsArray($result); + $this->assertCount(4, $result); + } + + public function testUnusedSectionFilesReturnedWhenSectionXmlFilesAreNotEmpty() + { + $unusedEntityCheck = new UnusedEntityCheck(); + $scriptUtil = new ScriptUtil(); + $modulePaths = $scriptUtil->getAllModulePaths(); + $sectionXmlFiles = $scriptUtil->getModuleXmlFilesByScope($modulePaths, "Section"); + + $domDocument = new \DOMDocument(); + $section = [ + 'DeprecationCheckSection' => '/verification/DeprecationCheckModule/Section/DeprecationCheckSection.xml', + 'DeprecatedSection' => '/verification/TestModule/Section/DeprecatedSection.xml', + 'LocatorFunctionSection' => '/verification/TestModule/Section/LocatorFunctionSection.xml', + 'SampleSection' => '/verification/TestModuleMerged/Section/MergeSection.xml' + ]; + $result = $unusedEntityCheck->unusedSectionEntity($domDocument, $sectionXmlFiles, [], [], $section, []); + $this->assertIsArray($result); + $this->assertCount(4, $result); + } + + public function testUnusedPageFiles() + { + $unusedEntityCheck = new UnusedEntityCheck(); + $domDocument = new \DOMDocument(); + $page = [ + 'DeprecationCheckPage' => '/verification/DeprecationCheckModule/Page/DeprecationCheckPage.xml', + 'DeprecatedPage' => '/verification/TestModule/Page/DeprecatedPage.xml', + 'AdminOneParamPage' => '/verification/TestModule/Page/SamplePage/AdminOneParamPage.xml', + 'AdminPage' => '/verification/TestModule/Page/SamplePage/AdminPage.xml', + 'ExternalPage' => '/verification/TestModule/Page/SamplePage/ExternalPage.xml', + 'NoParamPage' => '/verification/TestModule/Page/SamplePage/NoParamPage.xml' + ]; + $result = $unusedEntityCheck->unusedPageEntity($domDocument, [], [], $page, []); + $this->assertIsArray($result); + $this->assertCount(6, $result); + } + + public function testUnusedPageFilesReturnedWhenPageXmlFilesPassedAreNotEmpty() + { + $unusedEntityCheck = new UnusedEntityCheck(); + $domDocument = new \DOMDocument(); + $scriptUtil = new ScriptUtil(); + $modulePaths = $scriptUtil->getAllModulePaths(); + $pageXmlFiles = $scriptUtil->getModuleXmlFilesByScope($modulePaths, "Page"); + $page = [ + 'DeprecationCheckPage' => '/verification/DeprecationCheckModule/Page/DeprecationCheckPage.xml', + 'DeprecatedPage' => '/verification/TestModule/Page/DeprecatedPage.xml', + 'AdminOneParamPage' => '/verification/TestModule/Page/SamplePage/AdminOneParamPage.xml', + 'AdminPage' => '/verification/TestModule/Page/SamplePage/AdminPage.xml', + 'ExternalPage' => '/verification/TestModule/Page/SamplePage/ExternalPage.xml', + 'NoParamPage' => '/verification/TestModule/Page/SamplePage/NoParamPage.xml' + ]; + $result = $unusedEntityCheck->unusedPageEntity($domDocument, $pageXmlFiles, [], $page, []); + $this->assertIsArray($result); + $this->assertCount(6, $result); + } + + public function testUnusedData() + { + $unusedEntityCheck = new UnusedEntityCheck(); + $domDocument = new \DOMDocument(); + $data = [ + "simpleData" => + [ + "dataFilePath" => "/verification/TestModule/Data/ReplacementData.xml" + ], + + "uniqueData" => [ + + "dataFilePath" => "/verification/TestModule/Data/ReplacementData.xml" + ], + + "offset"=> + [ + "dataFilePath" => "/verification/TestModule/Data/ReplacementData.xml" + ] + ]; + $result=$unusedEntityCheck->unusedPageEntity($domDocument, [], [], $data, []); + $this->assertIsArray($result); + $this->assertCount(3, $result); + } + + public function testUnusedDataReturnedWhenCreateDataEntityAreNotEmpty() + { + $unusedEntityCheck = new UnusedEntityCheck(); + $domDocument = new \DOMDocument(); + $scriptUtil = new ScriptUtil(); + $modulePaths = $scriptUtil->getAllModulePaths(); + $dataXmlFiles = $scriptUtil->getModuleXmlFilesByScope($modulePaths, "Data"); + $data = [ + "simpleData" => + [ + "dataFilePath" => "/verification/TestModule/Data/ReplacementData.xml" + ], + + "uniqueData" => [ + + "dataFilePath" => "/verification/TestModule/Data/ReplacementData.xml" + ], + + "offset" => + [ + "dataFilePath" => "/verification/TestModule/Data/ReplacementData.xml" + ] + ]; + $result = $unusedEntityCheck->unusedPageEntity($domDocument, $dataXmlFiles, [], $data, []); + $this->assertIsArray($result); + $this->assertCount(3, $result); + } +} diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Util/Validation/DuplicateNodeValidationUtilTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Util/Validation/DuplicateNodeValidationUtilTest.php index 5dba1cb8b..db117c6b6 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Util/Validation/DuplicateNodeValidationUtilTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/Validation/DuplicateNodeValidationUtilTest.php @@ -4,13 +4,11 @@ * See COPYING.txt for license details. */ -namespace tests\unit\Magento\FunctionalTestFramework\Test\Util; +namespace tests\unit\Magento\FunctionalTestFramework\Util\Validation; use Magento\FunctionalTestingFramework\Exceptions\Collector\ExceptionCollector; -use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Util\Validation\DuplicateNodeValidationUtil; -use Magento\FunctionalTestingFramework\Util\Validation\NameValidationUtil; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use tests\unit\Util\MagentoTestCase; class DuplicateNodeValidationUtilTest extends MagentoTestCase { diff --git a/dev/tests/unit/Magento/FunctionalTestFramework/Util/Validation/NameValidationUtilTest.php b/dev/tests/unit/Magento/FunctionalTestFramework/Util/Validation/NameValidationUtilTest.php index 28d75b1ad..d43eb5282 100644 --- a/dev/tests/unit/Magento/FunctionalTestFramework/Util/Validation/NameValidationUtilTest.php +++ b/dev/tests/unit/Magento/FunctionalTestFramework/Util/Validation/NameValidationUtilTest.php @@ -4,11 +4,11 @@ * See COPYING.txt for license details. */ -namespace tests\unit\Magento\FunctionalTestFramework\Test\Util; +namespace tests\unit\Magento\FunctionalTestFramework\Util\Validation; use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Util\Validation\NameValidationUtil; -use Magento\FunctionalTestingFramework\Util\MagentoTestCase; +use tests\unit\Util\MagentoTestCase; class NameValidationUtilTest extends MagentoTestCase { @@ -17,7 +17,7 @@ class NameValidationUtilTest extends MagentoTestCase */ public function testCurlyBracesInTestName() { - $this->validateBlacklistedTestName("{{curlyBraces}}"); + $this->validateBlocklistedTestName("{{curlyBraces}}"); } /** @@ -25,7 +25,7 @@ public function testCurlyBracesInTestName() */ public function testQuotesInTestName() { - $this->validateBlacklistedTestName("\"quotes\""); + $this->validateBlocklistedTestName("\"quotes\""); } /** @@ -33,7 +33,7 @@ public function testQuotesInTestName() */ public function testSingleQuotesInTestName() { - $this->validateBlacklistedTestName("'singleQuotes'"); + $this->validateBlocklistedTestName("'singleQuotes'"); } /** @@ -41,7 +41,7 @@ public function testSingleQuotesInTestName() */ public function testParenthesesInTestName() { - $this->validateBlacklistedTestName("(parenthesis)"); + $this->validateBlocklistedTestName("(parenthesis)"); } /** @@ -49,7 +49,7 @@ public function testParenthesesInTestName() */ public function testDollarSignInTestName() { - $this->validateBlacklistedTestName("\$dollarSign\$"); + $this->validateBlocklistedTestName("\$dollarSign\$"); } /** @@ -57,7 +57,7 @@ public function testDollarSignInTestName() */ public function testSpacesInTestName() { - $this->validateBlacklistedTestName("Test Name With Spaces"); + $this->validateBlocklistedTestName("Test Name With Spaces"); } /** @@ -66,7 +66,7 @@ public function testSpacesInTestName() * @param string $testName * @return void */ - private function validateBlacklistedTestName($testName) + private function validateBlocklistedTestName($testName) { $this->expectException(XmlException::class); NameValidationUtil::validateName($testName, "Test"); diff --git a/dev/tests/unit/Util/ActionGroupArrayBuilder.php b/dev/tests/unit/Util/ActionGroupArrayBuilder.php index 14877f4dd..49f9076e1 100644 --- a/dev/tests/unit/Util/ActionGroupArrayBuilder.php +++ b/dev/tests/unit/Util/ActionGroupArrayBuilder.php @@ -111,7 +111,7 @@ public function withActionObjects($actionObjs = []) * @param string $extendedActionGroup * @return $this */ - public function withExtendedAction($extendedActionGroup = null) + public function withExtendedAction(?string $extendedActionGroup = null) { $this->extends = $extendedActionGroup; return $this; diff --git a/dev/tests/unit/Util/EntityDataObjectBuilder.php b/dev/tests/unit/Util/EntityDataObjectBuilder.php index dfe7d6836..82496d2ce 100644 --- a/dev/tests/unit/Util/EntityDataObjectBuilder.php +++ b/dev/tests/unit/Util/EntityDataObjectBuilder.php @@ -18,7 +18,8 @@ class EntityDataObjectBuilder "name" => "Hopper", "gpa" => "3.5678", "phone" => "5555555", - "isprimary" => "true" + "isprimary" => "true", + "empty_string" => "" ]; /** diff --git a/dev/tests/unit/Util/MagentoTestCase.php b/dev/tests/unit/Util/MagentoTestCase.php index 6a4e5d35b..13c79403f 100644 --- a/dev/tests/unit/Util/MagentoTestCase.php +++ b/dev/tests/unit/Util/MagentoTestCase.php @@ -3,10 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); -namespace Magento\FunctionalTestingFramework\Util; +namespace tests\unit\Util; -use AspectMock\Test as AspectMock; use PHPUnit\Framework\TestCase; /** @@ -14,22 +14,25 @@ */ class MagentoTestCase extends TestCase { - public static function setUpBeforeClass() + /** + * @inheritDoc + */ + public static function setUpBeforeClass(): void { if (!self::fileExists(DOCS_OUTPUT_DIR)) { mkdir(DOCS_OUTPUT_DIR, 0755, true); } + parent::setUpBeforeClass(); } /** - * Teardown for removing AspectMock Double References - * @return void + * @inheritDoc */ - public static function tearDownAfterClass() + public static function tearDownAfterClass(): void { - AspectMock::clean(); array_map('unlink', glob(DOCS_OUTPUT_DIR . DIRECTORY_SEPARATOR . "*")); + if (file_exists(DOCS_OUTPUT_DIR)) { rmdir(DOCS_OUTPUT_DIR); } diff --git a/dev/tests/unit/Util/MockModuleResolverBuilder.php b/dev/tests/unit/Util/MockModuleResolverBuilder.php deleted file mode 100644 index 0e1b6fc31..000000000 --- a/dev/tests/unit/Util/MockModuleResolverBuilder.php +++ /dev/null @@ -1,61 +0,0 @@ - '/base/path/some/other/path/Magento/Module']; - - /** - * Mock ModuleResolver builder - * - * @param array $paths - * @return void - * @throws \Exception - */ - public function setup($paths = null) - { - if (empty($paths)) { - $paths = $this->defaultPaths; - } - - $mockConfig = AspectMock::double(MftfApplicationConfig::class, ['forceGenerateEnabled' => false]); - $instance = AspectMock::double(ObjectManager::class, ['create' => $mockConfig->make(), 'get' => null])->make(); - AspectMock::double(ObjectManagerFactory::class, ['getObjectManager' => $instance]); - - $property = new \ReflectionProperty(ModuleResolver::class, 'instance'); - $property->setAccessible(true); - $property->setValue(null); - - $mockResolver = AspectMock::double( - ModuleResolver::class, - [ - 'getAdminToken' => false, - 'globRelevantPaths' => [], - 'getEnabledModules' => [] - ] - ); - $instance = AspectMock::double(ObjectManager::class, ['create' => $mockResolver->make(), 'get' => null]) - ->make(); - AspectMock::double(ObjectManagerFactory::class, ['getObjectManager' => $instance]); - - $resolver = ModuleResolver::getInstance(); - $property = new \ReflectionProperty(ModuleResolver::class, 'enabledModuleNameAndPaths'); - $property->setAccessible(true); - $property->setValue($resolver, $paths); - } -} diff --git a/dev/tests/unit/Util/OperationElementBuilder.php b/dev/tests/unit/Util/OperationElementBuilder.php index 2c2a4d9da..69e848de1 100644 --- a/dev/tests/unit/Util/OperationElementBuilder.php +++ b/dev/tests/unit/Util/OperationElementBuilder.php @@ -20,7 +20,8 @@ class OperationElementBuilder 'name' => 'string', 'gpa' => 'number', 'phone' => 'integer', - 'isPrimary' => 'boolean' + 'isPrimary' => 'boolean', + 'empty_string' => 'string' ]; /** diff --git a/dev/tests/unit/Util/SuiteDataArrayBuilder.php b/dev/tests/unit/Util/SuiteDataArrayBuilder.php index 85411b447..fefc535df 100644 --- a/dev/tests/unit/Util/SuiteDataArrayBuilder.php +++ b/dev/tests/unit/Util/SuiteDataArrayBuilder.php @@ -179,9 +179,9 @@ private function appendEntriesToSuiteContents($currentContents, $type, $contents * @param null $afterHook * @return $this */ - public function withAfterHook($afterHook = null) + public function withAfterHook(?array $afterHook = null) { - if ($afterHook == null) { + if ($afterHook === null) { $this->afterHook = [$this->testActionAfterName => [ ActionObjectExtractor::NODE_NAME => $this->testActionType, ActionObjectExtractor::TEST_STEP_MERGE_KEY => $this->testActionAfterName @@ -200,9 +200,9 @@ public function withAfterHook($afterHook = null) * @param null $beforeHook * @return $this */ - public function withBeforeHook($beforeHook = null) + public function withBeforeHook(?array $beforeHook = null) { - if ($beforeHook == null) { + if ($beforeHook === null) { $this->beforeHook = [$this->testActionBeforeName => [ ActionObjectExtractor::NODE_NAME => $this->testActionType, ActionObjectExtractor::TEST_STEP_MERGE_KEY => $this->testActionBeforeName diff --git a/dev/tests/unit/Util/TestDataArrayBuilder.php b/dev/tests/unit/Util/TestDataArrayBuilder.php index eb192e040..b2569111b 100644 --- a/dev/tests/unit/Util/TestDataArrayBuilder.php +++ b/dev/tests/unit/Util/TestDataArrayBuilder.php @@ -107,9 +107,9 @@ public function withName($name) * @param array $annotations * @return $this */ - public function withAnnotations($annotations = null) + public function withAnnotations(?array $annotations = null) { - if ($annotations == null) { + if ($annotations === null) { $this->annotations = ['group' => [['value' => 'test']]]; } else { $this->annotations = $annotations; @@ -124,9 +124,9 @@ public function withAnnotations($annotations = null) * @param null $beforeHook * @return $this */ - public function withBeforeHook($beforeHook = null) + public function withBeforeHook(?array $beforeHook = null) { - if ($beforeHook == null) { + if ($beforeHook === null) { $this->beforeHook = [$this->testActionBeforeName => [ ActionObjectExtractor::NODE_NAME => $this->testActionType, ActionObjectExtractor::TEST_STEP_MERGE_KEY => $this->testActionBeforeName @@ -144,9 +144,9 @@ public function withBeforeHook($beforeHook = null) * @param null $afterHook * @return $this */ - public function withAfterHook($afterHook = null) + public function withAfterHook(?array $afterHook = null) { - if ($afterHook == null) { + if ($afterHook === null) { $this->afterHook = [$this->testActionAfterName => [ ActionObjectExtractor::NODE_NAME => $this->testActionType, ActionObjectExtractor::TEST_STEP_MERGE_KEY => $this->testActionAfterName @@ -165,9 +165,9 @@ public function withAfterHook($afterHook = null) * @param null $failedHook * @return $this */ - public function withFailedHook($failedHook = null) + public function withFailedHook(?array $failedHook = null) { - if ($failedHook == null) { + if ($failedHook === null) { $this->failedHook = [$this->testActionFailedName => [ ActionObjectExtractor::NODE_NAME => $this->testActionType, ActionObjectExtractor::TEST_STEP_MERGE_KEY => $this->testActionFailedName @@ -186,9 +186,9 @@ public function withFailedHook($failedHook = null) * @param array $actions * @return $this */ - public function withTestActions($actions = null) + public function withTestActions(?array $actions = null) { - if ($actions == null) { + if ($actions === null) { $this->testActions = [$this->testTestActionName => [ ActionObjectExtractor::NODE_NAME => $this->testActionType, ActionObjectExtractor::TEST_STEP_MERGE_KEY => $this->testTestActionName @@ -205,9 +205,9 @@ public function withTestActions($actions = null) * @param string $filename * @return $this */ - public function withFileName($filename = null) + public function withFileName(?string $filename = null) { - if ($filename == null) { + if ($filename === null) { $this->filename = "/magento2-functional-testing-framework/dev/tests/verification/TestModule/Test/BasicFunctionalTest.xml"; } else { @@ -223,7 +223,7 @@ public function withFileName($filename = null) * @param string $reference * @return $this */ - public function withTestReference($reference = null) + public function withTestReference(?string $reference = null) { if ($reference !== null) { $this->testReference = $reference; @@ -232,6 +232,28 @@ public function withTestReference($reference = null) return $this; } + /** + * Reset data array builder + * + * @return void + */ + public function reset() + { + // reset + $this->testName = 'testTest'; + $this->filename = null; + $this->testActionBeforeName = 'testActionBefore'; + $this->testActionAfterName = 'testActionAfter'; + $this->testActionFailedName = 'testActionFailed'; + $this->testActionType = 'testAction'; + $this->annotations = []; + $this->beforeHook = []; + $this->afterHook = []; + $this->failedHook = []; + $this->testReference = null; + $this->testActions = []; + } + /** * Output the resulting test data array based on parameters set in the object * diff --git a/dev/tests/unit/Util/TestLoggingUtil.php b/dev/tests/unit/Util/TestLoggingUtil.php index 7becc609a..b225a4874 100644 --- a/dev/tests/unit/Util/TestLoggingUtil.php +++ b/dev/tests/unit/Util/TestLoggingUtil.php @@ -3,16 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace tests\unit\Util; -use AspectMock\Test as AspectMock; use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; use Magento\FunctionalTestingFramework\Util\Logger\MftfLogger; use Monolog\Handler\TestHandler; -use PHPUnit\Framework\Assert; +use PHPUnit\Framework\TestCase; +use ReflectionProperty; +use ReflectionClass; -class TestLoggingUtil extends Assert +class TestLoggingUtil extends TestCase { /** * @var TestLoggingUtil @@ -25,24 +27,23 @@ class TestLoggingUtil extends Assert private $testLogHandler; /** - * TestLoggingUtil constructor. + * Private constructor. */ private function __construct() { - // private constructor + parent::__construct('', [], ''); } /** - * Static singleton get function + * Static singleton get function. * * @return TestLoggingUtil */ - public static function getInstance() + public static function getInstance(): TestLoggingUtil { - if (self::$instance == null) { + if (self::$instance === null) { self::$instance = new TestLoggingUtil(); } - return self::$instance; } @@ -51,21 +52,28 @@ public static function getInstance() * * @return void */ - public function setMockLoggingUtil() + public function setMockLoggingUtil(): void { $this->testLogHandler = new TestHandler(); $testLogger = new MftfLogger('testLogger'); $testLogger->pushHandler($this->testLogHandler); - $mockLoggingUtil = AspectMock::double( - LoggingUtil::class, - ['getLogger' => $testLogger] - )->make(); - $property = new \ReflectionProperty(LoggingUtil::class, 'instance'); + + $mockLoggingUtil = $this->createMock(LoggingUtil::class); + $mockLoggingUtil + ->method('getLogger') + ->willReturn($testLogger); + + $property = new ReflectionProperty(LoggingUtil::class, 'instance'); $property->setAccessible(true); - $property->setValue($mockLoggingUtil); + $property->setValue(null, $mockLoggingUtil); } - public function validateMockLogEmpty() + /** + * Check if mock log is empty. + * + * @return void + */ + public function validateMockLogEmpty(): void { $records = $this->testLogHandler->getRecords(); $this->assertTrue(empty($records)); @@ -77,9 +85,10 @@ public function validateMockLogEmpty() * @param string $type * @param string $message * @param array $context + * * @return void */ - public function validateMockLogStatement($type, $message, $context) + public function validateMockLogStatement(string $type, string $message, array $context): void { $records = $this->testLogHandler->getRecords(); $record = $records[count($records)-1]; // we assume the latest record is what requires validation @@ -88,12 +97,21 @@ public function validateMockLogStatement($type, $message, $context) $this->assertEquals($context, $record['context']); } - public function validateMockLogStatmentRegex($type, $regex, $context) + /** + * Check mock log statement regular expression. + * + * @param string $type + * @param string $regex + * @param array $context + * + * @return void + */ + public function validateMockLogStatmentRegex(string $type, string $regex, array $context): void { $records = $this->testLogHandler->getRecords(); $record = $records[count($records)-1]; // we assume the latest record is what requires validation $this->assertEquals(strtoupper($type), $record['level_name']); - $this->assertRegExp($regex, $record['message']); + $this->assertMatchesRegularExpression($regex, $record['message']); $this->assertEquals($context, $record['context']); } @@ -103,8 +121,9 @@ public function validateMockLogStatmentRegex($type, $regex, $context) * * @return void */ - public function clearMockLoggingUtil() + public function clearMockLoggingUtil(): void { - AspectMock::clean(LoggingUtil::class); + $reflectionClass = new ReflectionClass(LoggingUtil::class); + $reflectionClass->setStaticPropertyValue('instance', null); } } diff --git a/dev/tests/util/MftfStaticTestCase.php b/dev/tests/util/MftfStaticTestCase.php new file mode 100644 index 000000000..708561031 --- /dev/null +++ b/dev/tests/util/MftfStaticTestCase.php @@ -0,0 +1,49 @@ +getMockBuilder(InputInterface::class) + ->disableOriginalConstructor() + ->getMock(); + if ($path) { + $input->method('getOption') + ->with('path') + ->willReturn($path); + } + return $input; + } + + public function tearDown(): void + { + DirSetupUtil::rmdirRecursive(self::STATIC_RESULTS_DIR); + } +} diff --git a/dev/tests/util/MftfTestCase.php b/dev/tests/util/MftfTestCase.php index 8bfa1009f..365a8351f 100644 --- a/dev/tests/util/MftfTestCase.php +++ b/dev/tests/util/MftfTestCase.php @@ -6,9 +6,13 @@ namespace tests\util; use Magento\FunctionalTestingFramework\ObjectManager; +use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; +use Magento\FunctionalTestingFramework\Suite\SuiteGenerator; use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; use Magento\FunctionalTestingFramework\Util\TestGenerator; use PHPUnit\Framework\TestCase; +use ReflectionClass; abstract class MftfTestCase extends TestCase { @@ -80,6 +84,28 @@ public function validateSchemaErrorWithTest($fileContents, $objectType ,$expecte } } + /** + * Asserts that the given callback throws the given exception + * + * @param string $expectClass + * @param array $expectedMessages + * @param callable $callback + */ + protected function assertExceptionRegex(string $expectClass, array $expectedMessages, callable $callback) + { + try { + $callback(); + } catch (\Throwable $exception) { + $this->assertInstanceOf($expectClass, $exception, 'An invalid exception was thrown.'); + foreach ($expectedMessages as $expectedMessage) { + $this->assertMatchesRegularExpression($expectedMessage, $exception->getMessage()); + } + return; + } + + $this->fail('No exception was thrown.'); + } + /** * Clears test handler and object manager to force recollection of test data * @@ -88,13 +114,19 @@ public function validateSchemaErrorWithTest($fileContents, $objectType ,$expecte private function clearHandler() { // clear test object handler to force recollection of test data - $property = new \ReflectionProperty(TestObjectHandler::class, 'testObjectHandler'); - $property->setAccessible(true); - $property->setValue(null); + $reflectionClass = new ReflectionClass(TestObjectHandler::class); + $reflectionClass->setStaticPropertyValue('testObjectHandler', null); // clear test object handler to force recollection of test data - $property = new \ReflectionProperty(ObjectManager::class, 'instance'); - $property->setAccessible(true); - $property->setValue(null); + $reflectionClass = new ReflectionClass(ObjectManager::class); + $reflectionClass->setStaticPropertyValue('instance', null); + + // clear suite generator to force recollection of test data + $reflectionClass = new ReflectionClass(SuiteGenerator::class); + $reflectionClass->setStaticPropertyValue('instance', null); + + // clear suite object handler to force recollection of test data + $reflectionClass = new ReflectionClass(SuiteObjectHandler::class); + $reflectionClass->setStaticPropertyValue('instance', null); } } diff --git a/dev/tests/verification/DeprecationCheckModule/ActionGroup/DeprecationCheckActionGroup.xml b/dev/tests/verification/DeprecationCheckModule/ActionGroup/DeprecationCheckActionGroup.xml new file mode 100644 index 000000000..c83dab211 --- /dev/null +++ b/dev/tests/verification/DeprecationCheckModule/ActionGroup/DeprecationCheckActionGroup.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/dev/tests/verification/DeprecationCheckModule/ActionGroup/DeprecationCheckDeprecatedActionGroup.xml b/dev/tests/verification/DeprecationCheckModule/ActionGroup/DeprecationCheckDeprecatedActionGroup.xml new file mode 100644 index 000000000..e8412a29e --- /dev/null +++ b/dev/tests/verification/DeprecationCheckModule/ActionGroup/DeprecationCheckDeprecatedActionGroup.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/dev/tests/verification/DeprecationCheckModule/Data/DeprecationCheckData.xml b/dev/tests/verification/DeprecationCheckModule/Data/DeprecationCheckData.xml new file mode 100644 index 000000000..75a6b6678 --- /dev/null +++ b/dev/tests/verification/DeprecationCheckModule/Data/DeprecationCheckData.xml @@ -0,0 +1,14 @@ + + + + + + value + + diff --git a/dev/tests/verification/DeprecationCheckModule/Metadata/DeprecationCheckMeta.xml b/dev/tests/verification/DeprecationCheckModule/Metadata/DeprecationCheckMeta.xml new file mode 100644 index 000000000..757c6284b --- /dev/null +++ b/dev/tests/verification/DeprecationCheckModule/Metadata/DeprecationCheckMeta.xml @@ -0,0 +1,18 @@ + + + + + + application/json + + value1 + value2 + + + \ No newline at end of file diff --git a/dev/tests/verification/DeprecationCheckModule/Page/DeprecationCheckPage.xml b/dev/tests/verification/DeprecationCheckModule/Page/DeprecationCheckPage.xml new file mode 100644 index 000000000..dd3f187dc --- /dev/null +++ b/dev/tests/verification/DeprecationCheckModule/Page/DeprecationCheckPage.xml @@ -0,0 +1,14 @@ + + + + + +
+ + diff --git a/dev/tests/verification/DeprecationCheckModule/Section/DeprecationCheckSection.xml b/dev/tests/verification/DeprecationCheckModule/Section/DeprecationCheckSection.xml new file mode 100644 index 000000000..1837e1ef5 --- /dev/null +++ b/dev/tests/verification/DeprecationCheckModule/Section/DeprecationCheckSection.xml @@ -0,0 +1,16 @@ + + + + +
+ + + +
+
diff --git a/dev/tests/verification/DeprecationCheckModule/Suite/deprecationCheckSuite.xml b/dev/tests/verification/DeprecationCheckModule/Suite/deprecationCheckSuite.xml new file mode 100644 index 000000000..b89ad99f6 --- /dev/null +++ b/dev/tests/verification/DeprecationCheckModule/Suite/deprecationCheckSuite.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/dev/tests/verification/DeprecationCheckModule/Test/DeprecationCheckDeprecatedTest.xml b/dev/tests/verification/DeprecationCheckModule/Test/DeprecationCheckDeprecatedTest.xml new file mode 100644 index 000000000..1b3ace053 --- /dev/null +++ b/dev/tests/verification/DeprecationCheckModule/Test/DeprecationCheckDeprecatedTest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/dev/tests/verification/DeprecationCheckModule/Test/DeprecationCheckTest.xml b/dev/tests/verification/DeprecationCheckModule/Test/DeprecationCheckTest.xml new file mode 100644 index 000000000..1c4fc3dba --- /dev/null +++ b/dev/tests/verification/DeprecationCheckModule/Test/DeprecationCheckTest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/dev/tests/verification/PauseCheckModule/ActionGroup/ActionGroupWithMultiplePausesActionGroup.xml b/dev/tests/verification/PauseCheckModule/ActionGroup/ActionGroupWithMultiplePausesActionGroup.xml new file mode 100644 index 000000000..5e9299631 --- /dev/null +++ b/dev/tests/verification/PauseCheckModule/ActionGroup/ActionGroupWithMultiplePausesActionGroup.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/dev/tests/verification/PauseCheckModule/ActionGroup/ActionGroupWithNoPauseActionGroup.xml b/dev/tests/verification/PauseCheckModule/ActionGroup/ActionGroupWithNoPauseActionGroup.xml new file mode 100644 index 000000000..8acae47e8 --- /dev/null +++ b/dev/tests/verification/PauseCheckModule/ActionGroup/ActionGroupWithNoPauseActionGroup.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/dev/tests/verification/PauseCheckModule/ActionGroup/ActionGroupWithPauseActionGroup.xml b/dev/tests/verification/PauseCheckModule/ActionGroup/ActionGroupWithPauseActionGroup.xml new file mode 100644 index 000000000..aaef1befa --- /dev/null +++ b/dev/tests/verification/PauseCheckModule/ActionGroup/ActionGroupWithPauseActionGroup.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/dev/tests/verification/PauseCheckModule/Suite/suiteWithMultiplePauseActionsSuite.xml b/dev/tests/verification/PauseCheckModule/Suite/suiteWithMultiplePauseActionsSuite.xml new file mode 100644 index 000000000..47ff1088b --- /dev/null +++ b/dev/tests/verification/PauseCheckModule/Suite/suiteWithMultiplePauseActionsSuite.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + dataHere + + + + + + + + + + + diff --git a/dev/tests/verification/PauseCheckModule/Suite/suiteWithPauseActionSuite.xml b/dev/tests/verification/PauseCheckModule/Suite/suiteWithPauseActionSuite.xml new file mode 100644 index 000000000..71a3b5769 --- /dev/null +++ b/dev/tests/verification/PauseCheckModule/Suite/suiteWithPauseActionSuite.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + dataHere + + + + + + + + + + diff --git a/dev/tests/verification/PauseCheckModule/Test/TestWithMultiplePauseActionsTest.xml b/dev/tests/verification/PauseCheckModule/Test/TestWithMultiplePauseActionsTest.xml new file mode 100644 index 000000000..fa47e976c --- /dev/null +++ b/dev/tests/verification/PauseCheckModule/Test/TestWithMultiplePauseActionsTest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dev/tests/verification/PauseCheckModule/Test/TestWithPauseActionTest.xml b/dev/tests/verification/PauseCheckModule/Test/TestWithPauseActionTest.xml new file mode 100644 index 000000000..70d0903b8 --- /dev/null +++ b/dev/tests/verification/PauseCheckModule/Test/TestWithPauseActionTest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/ActionGroup/BasicActionGroup/NotGenerateArgActionGroup.xml b/dev/tests/verification/ResilientGenerationModule/ActionGroup/BasicActionGroup/NotGenerateArgActionGroup.xml new file mode 100644 index 000000000..5d0246b08 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/ActionGroup/BasicActionGroup/NotGenerateArgActionGroup.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/ActionGroup/BasicActionGroup/NotGenerateCreateDataActionGroup.xml b/dev/tests/verification/ResilientGenerationModule/ActionGroup/BasicActionGroup/NotGenerateCreateDataActionGroup.xml new file mode 100644 index 000000000..580c9081c --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/ActionGroup/BasicActionGroup/NotGenerateCreateDataActionGroup.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/ActionGroup/BasicActionGroup/NotGenerateExtendChildActionGroup.xml b/dev/tests/verification/ResilientGenerationModule/ActionGroup/BasicActionGroup/NotGenerateExtendChildActionGroup.xml new file mode 100644 index 000000000..9566ad170 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/ActionGroup/BasicActionGroup/NotGenerateExtendChildActionGroup.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + {{otherCount}} + grabProducts + + + diff --git a/dev/tests/verification/ResilientGenerationModule/ActionGroup/BasicActionGroup/NotGenerateExtendChildBadReferenceActionGroup.xml b/dev/tests/verification/ResilientGenerationModule/ActionGroup/BasicActionGroup/NotGenerateExtendChildBadReferenceActionGroup.xml new file mode 100644 index 000000000..4896c239f --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/ActionGroup/BasicActionGroup/NotGenerateExtendChildBadReferenceActionGroup.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/ActionGroup/BasicActionGroup/NotGenerateExtendParentActionGroup.xml b/dev/tests/verification/ResilientGenerationModule/ActionGroup/BasicActionGroup/NotGenerateExtendParentActionGroup.xml new file mode 100644 index 000000000..5e3cfaa54 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/ActionGroup/BasicActionGroup/NotGenerateExtendParentActionGroup.xml @@ -0,0 +1,21 @@ + + + + + + + + + + {{count}} + grabProducts + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/ActionGroup/BasicActionGroup/NotGenerateParamsActionGroup.xml b/dev/tests/verification/ResilientGenerationModule/ActionGroup/BasicActionGroup/NotGenerateParamsActionGroup.xml new file mode 100644 index 000000000..29ebba5f7 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/ActionGroup/BasicActionGroup/NotGenerateParamsActionGroup.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/ActionGroup/BasicActionGroup/NotGenerateSectionActionGroup.xml b/dev/tests/verification/ResilientGenerationModule/ActionGroup/BasicActionGroup/NotGenerateSectionActionGroup.xml new file mode 100644 index 000000000..81a4d4c8d --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/ActionGroup/BasicActionGroup/NotGenerateSectionActionGroup.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/ActionGroup/FunctionalActionGroup/NotGenerateMassMergeAfterActionGroup.xml b/dev/tests/verification/ResilientGenerationModule/ActionGroup/FunctionalActionGroup/NotGenerateMassMergeAfterActionGroup.xml new file mode 100644 index 000000000..5fc49e0b3 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/ActionGroup/FunctionalActionGroup/NotGenerateMassMergeAfterActionGroup.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/ActionGroup/FunctionalActionGroup/NotGenerateMassMergeBeforeActionGroup.xml b/dev/tests/verification/ResilientGenerationModule/ActionGroup/FunctionalActionGroup/NotGenerateMassMergeBeforeActionGroup.xml new file mode 100644 index 000000000..23352d61e --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/ActionGroup/FunctionalActionGroup/NotGenerateMassMergeBeforeActionGroup.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/ActionGroup/FunctionalActionGroup/NotGenerateMergeActionGroup.xml b/dev/tests/verification/ResilientGenerationModule/ActionGroup/FunctionalActionGroup/NotGenerateMergeActionGroup.xml new file mode 100644 index 000000000..6796df94e --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/ActionGroup/FunctionalActionGroup/NotGenerateMergeActionGroup.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/ActionGroup/MergeFunctionalActionGroup/NotGenerateMassMergeAfterActionGroup.xml b/dev/tests/verification/ResilientGenerationModule/ActionGroup/MergeFunctionalActionGroup/NotGenerateMassMergeAfterActionGroup.xml new file mode 100644 index 000000000..18d4332ac --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/ActionGroup/MergeFunctionalActionGroup/NotGenerateMassMergeAfterActionGroup.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/ActionGroup/MergeFunctionalActionGroup/NotGenerateMassMergeBeforeActionGroup.xml b/dev/tests/verification/ResilientGenerationModule/ActionGroup/MergeFunctionalActionGroup/NotGenerateMassMergeBeforeActionGroup.xml new file mode 100644 index 000000000..07166d1fb --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/ActionGroup/MergeFunctionalActionGroup/NotGenerateMassMergeBeforeActionGroup.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/ActionGroup/MergeFunctionalActionGroup/NotGenerateMergeActionGroup.xml b/dev/tests/verification/ResilientGenerationModule/ActionGroup/MergeFunctionalActionGroup/NotGenerateMergeActionGroup.xml new file mode 100644 index 000000000..964a40a74 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/ActionGroup/MergeFunctionalActionGroup/NotGenerateMergeActionGroup.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/Data/ExtendedData.xml b/dev/tests/verification/ResilientGenerationModule/Data/ExtendedData.xml new file mode 100644 index 000000000..59243531e --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Data/ExtendedData.xml @@ -0,0 +1,18 @@ + + + + + + otherName + extendName + item + postnameExtend + value + + diff --git a/dev/tests/verification/ResilientGenerationModule/Suite/NotGenerateEmptySuite.xml b/dev/tests/verification/ResilientGenerationModule/Suite/NotGenerateEmptySuite.xml new file mode 100644 index 000000000..869b4acf6 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Suite/NotGenerateEmptySuite.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/Suite/NotGenerateHookAfterSuite.xml b/dev/tests/verification/ResilientGenerationModule/Suite/NotGenerateHookAfterSuite.xml new file mode 100644 index 000000000..96054071e --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Suite/NotGenerateHookAfterSuite.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/Suite/NotGenerateHookBeforeSuite.xml b/dev/tests/verification/ResilientGenerationModule/Suite/NotGenerateHookBeforeSuite.xml new file mode 100644 index 000000000..36e046114 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Suite/NotGenerateHookBeforeSuite.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/Suite/PartialGenerateForIncludeSuite.xml b/dev/tests/verification/ResilientGenerationModule/Suite/PartialGenerateForIncludeSuite.xml new file mode 100644 index 000000000..7e9f3d76c --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Suite/PartialGenerateForIncludeSuite.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/Suite/PartialGenerateNoExcludeSuite.xml b/dev/tests/verification/ResilientGenerationModule/Suite/PartialGenerateNoExcludeSuite.xml new file mode 100644 index 000000000..cde48cbbb --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Suite/PartialGenerateNoExcludeSuite.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateActionGroupHasBeforeOrAfterTest.xml b/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateActionGroupHasBeforeOrAfterTest.xml new file mode 100644 index 000000000..1cb3dd4a2 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateActionGroupHasBeforeOrAfterTest.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateArgTest.xml b/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateArgTest.xml new file mode 100644 index 000000000..8b98a9a19 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateArgTest.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateExtendChildTest.xml b/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateExtendChildTest.xml new file mode 100644 index 000000000..9f9d022f6 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateExtendChildTest.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateMassMergeAfterTest.xml b/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateMassMergeAfterTest.xml new file mode 100644 index 000000000..ed3f31a79 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateMassMergeAfterTest.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateMassMergeBeforeTest.xml b/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateMassMergeBeforeTest.xml new file mode 100644 index 000000000..1046138b4 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateMassMergeBeforeTest.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateMergeTest.xml b/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateMergeTest.xml new file mode 100644 index 000000000..7fa4684e0 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateMergeTest.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateNonExistingActionGroupTest.xml b/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateNonExistingActionGroupTest.xml new file mode 100644 index 000000000..61b7544e8 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateNonExistingActionGroupTest.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateParamsTest.xml b/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateParamsTest.xml new file mode 100644 index 000000000..3704ba695 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateParamsTest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateRequiredDataTest.xml b/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateRequiredDataTest.xml new file mode 100644 index 000000000..93da0c5ce --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateRequiredDataTest.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateSectionTest.xml b/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateSectionTest.xml new file mode 100644 index 000000000..3d7499b8b --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/ActionGroupTest/NotGenerateSectionTest.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/dev/tests/verification/ResilientGenerationModule/Test/ExtendTest/NotGenerateChildExtendedBadReferenceTest.xml b/dev/tests/verification/ResilientGenerationModule/Test/ExtendTest/NotGenerateChildExtendedBadReferenceTest.xml new file mode 100644 index 000000000..45e1177f1 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/ExtendTest/NotGenerateChildExtendedBadReferenceTest.xml @@ -0,0 +1,25 @@ + + + + + + + + <features value="Child"/> + <stories value="Child"/> + </annotations> + <before> + <amOnPage url="/firstUrl" stepKey="firstBeforeAmOnPageKey" before="beforeAmOnPageKey"/> + <amOnPage url="/lastUrl" stepKey="lastBefore" after="beforeAmOnPageKey"/> + </before> + <comment stepKey="lastStepKey" userInput="Last Comment"/> + <comment stepKey="beforeBasicCommentWithNoData" userInput="{{N.N}}" before="basicCommentWithNoData"/> + <comment stepKey="afterBasicCommentWithNoData" userInput="After Comment" after="basicCommentWithNoData"/> + </test> +</tests> diff --git a/dev/tests/verification/ResilientGenerationModule/Test/ExtendTest/NotGenerateChildExtendedMergingTest.xml b/dev/tests/verification/ResilientGenerationModule/Test/ExtendTest/NotGenerateChildExtendedMergingTest.xml new file mode 100644 index 000000000..61f15a31a --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/ExtendTest/NotGenerateChildExtendedMergingTest.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="NotGenerateChildExtendedMergingTest" extends="ParentExtendedTest"> + <annotations> + <severity value="MINOR"/> + <title value="NotGenerateChildExtendedTestMerging"/> + <features value="Child"/> + <stories value="Child"/> + </annotations> + <before> + <amOnPage url="/firstUrl" stepKey="firstBeforeAmOnPageKey" before="beforeAmOnPageKey"/> + <amOnPage url="/lastUrl" stepKey="lastBefore" after="beforeAmOnPageKeyN"/> + </before> + <comment stepKey="lastStepKey" userInput="Last Comment"/> + <comment stepKey="beforeBasicCommentWithNoData" userInput="Before Comment" before="basicCommentWithNoData"/> + <comment stepKey="afterBasicCommentWithNoData" userInput="After Comment" after="basicCommentWithNoData"/> + </test> +</tests> diff --git a/dev/tests/verification/ResilientGenerationModule/Test/ExtendTest/NotGenerateExtendNonExistingParentTest.xml b/dev/tests/verification/ResilientGenerationModule/Test/ExtendTest/NotGenerateExtendNonExistingParentTest.xml new file mode 100644 index 000000000..9f42cb55c --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/ExtendTest/NotGenerateExtendNonExistingParentTest.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="NotGenerateExtendNonExistingParentTest" extends="N"> + <comment stepKey="comment" userInput="comment"/> + </test> +</tests> diff --git a/dev/tests/verification/ResilientGenerationModule/Test/ExtendTest/NotGenerateExtendSelfTest.xml b/dev/tests/verification/ResilientGenerationModule/Test/ExtendTest/NotGenerateExtendSelfTest.xml new file mode 100644 index 000000000..46ae93933 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/ExtendTest/NotGenerateExtendSelfTest.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="NotGenerateExtendSelfTest" extends="NotGenerateExtendSelfTest"> + <comment stepKey="comment" userInput="comment"/> + </test> +</tests> diff --git a/dev/tests/verification/ResilientGenerationModule/Test/ExtendTest/NotGenerateExtendedChildTest.xml b/dev/tests/verification/ResilientGenerationModule/Test/ExtendTest/NotGenerateExtendedChildTest.xml new file mode 100644 index 000000000..75154b83d --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/ExtendTest/NotGenerateExtendedChildTest.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="NotGenerateExtendedChildTest" extends="NotGenerateParentTest"> + <annotations> + <severity value="MINOR"/> + <title value="NotGenerateExtendedChildTest"/> + <features value="NotGenerateExtendedChildTest"/> + <stories value="NotGenerateExtendedChildTest"/> + </annotations> + <comment stepKey="basicCommentWithNoData" userInput="Different Input"/> + </test> +</tests> diff --git a/dev/tests/verification/ResilientGenerationModule/Test/ExtendTest/NotGenerateParentTest.xml b/dev/tests/verification/ResilientGenerationModule/Test/ExtendTest/NotGenerateParentTest.xml new file mode 100644 index 000000000..0735ad13a --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/ExtendTest/NotGenerateParentTest.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="NotGenerateParentTest"> + <annotations> + <severity value="AVERAGE"/> + <title value="NotGenerateParentTest"/> + <features value="NotGenerateParentTest"/> + <stories value="NotGenerateParentTest"/> + </annotations> + <before> + <amOnPage url="{{NOTaPAGE.url}}" stepKey="beforeAmOnPageKey"/> + </before> + <after> + <amOnPage url="/afterUrl" stepKey="afterAmOnPageKey"/> + </after> + <comment stepKey="basicCommentWithNoData" userInput="Parent Comment"/> + <amOnPage url="/url/in/parent" stepKey="amOnPageInParent"/> + </test> +</tests> diff --git a/dev/tests/verification/ResilientGenerationModule/Test/MergeTest/NotGenerateBasicMergeTest.xml b/dev/tests/verification/ResilientGenerationModule/Test/MergeTest/NotGenerateBasicMergeTest.xml new file mode 100644 index 000000000..a2f68271c --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/MergeTest/NotGenerateBasicMergeTest.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="NotGenerateBasicMergeTest"> + <before> + <see stepKey="before2" selector="#before2" after="before1N"/> + </before> + <after> + <amOnPage url="/afterUrl1" stepKey="after1"/> + </after> + <click stepKey="step7Merge" selector="{{SampleSection.oneParamElement(DefaultPerson.firstname)}} .step7Merge" after="step6Merge"/> + <click stepKey="step2" selector="#step2" after="step1NNN"/> + <click stepKey="step4" selector="#step4" before="step5"/> + <remove keyForRemoval="step6"/> + <click stepKey="step6Merge" selector="#step6Merged" after="step5"/> + <click stepKey="step10" selector="#step10MergedInResult"/> + <actionGroup ref="FunctionalActionGroupWithData" stepKey="step8Merge" after="step7MergeNNN"/> + </test> +</tests> diff --git a/dev/tests/verification/ResilientGenerationModule/Test/MergeTest/NotGenerateMergeMassViaInsertAfter.xml b/dev/tests/verification/ResilientGenerationModule/Test/MergeTest/NotGenerateMergeMassViaInsertAfter.xml new file mode 100644 index 000000000..1bde12ac5 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/MergeTest/NotGenerateMergeMassViaInsertAfter.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="NotGenerateMergeMassViaInsertAfter" insertAfter="fillField2N"> + <click stepKey="clickOne" selector="#mergeOne"/> + <click stepKey="clickTwo" selector="#mergeTwo"/> + <click stepKey="clickThree" selector="#mergeThree"/> + </test> +</tests> diff --git a/dev/tests/verification/ResilientGenerationModule/Test/MergeTest/NotGenerateMergeMassViaInsertBefore.xml b/dev/tests/verification/ResilientGenerationModule/Test/MergeTest/NotGenerateMergeMassViaInsertBefore.xml new file mode 100644 index 000000000..fba36fa01 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/MergeTest/NotGenerateMergeMassViaInsertBefore.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="NotGenerateMergeMassViaInsertBefore" insertBefore="fillField2N"> + <click stepKey="clickOne" selector="#mergeOne"/> + <click stepKey="clickTwo" selector="#mergeTwo"/> + <click stepKey="clickThree" selector="#mergeThree"/> + </test> +</tests> diff --git a/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateActionHasBothBeforeAndAfterTest.xml b/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateActionHasBothBeforeAndAfterTest.xml new file mode 100644 index 000000000..2ba798498 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateActionHasBothBeforeAndAfterTest.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="NotGenerateActionHasBothBeforeAndAfterTest"> + <amOnPage url="{{AdminLoginPage.url}}" stepKey="navigateToAdmin" before="B" after="A"/> + </test> +</tests> diff --git a/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateActionHasInvalidBeforeOrAfterTest.xml b/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateActionHasInvalidBeforeOrAfterTest.xml new file mode 100644 index 000000000..eddb2da94 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateActionHasInvalidBeforeOrAfterTest.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="NotGenerateActionHasInvalidBeforeOrAfterTest"> + <amOnPage url="{{AdminLoginPage.url}}" stepKey="navigateToAdmin" after="N"/> + <click stepKey="a" selector="{{$a.b$}}" userInput="a"/> + </test> +</tests> diff --git a/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateAssertTest.xml b/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateAssertTest.xml new file mode 100644 index 000000000..77368f232 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateAssertTest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="NotGenerateAssertTest"> + <before> + <createData entity="ReplacementPerson" stepKey="createData1"/> + </before> + <createData entity="UniquePerson" stepKey="createData2"/> + <grabTextFrom selector=".copyright>span" stepKey="grabTextFrom1"/> + + <!-- assert entity resolution --> + <assertEquals stepKey="assertEqualsEntity" message="pass"> + <expectedResult type="string">{{simpleData.firstnameN}}</expectedResult> + <actualResult type="string">{{simpleData.lastname}}</actualResult> + </assertEquals> + </test> +</tests> diff --git a/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateBasicMergeTest.xml b/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateBasicMergeTest.xml new file mode 100644 index 000000000..7344b4f42 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateBasicMergeTest.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="NotGenerateBasicMergeTest"> + <annotations> + <severity value="CRITICAL"/> + <title value="NotGenerateBasicMergeTest"/> + <features value="Merge Functional Cest"/> + <stories value="MQE-433"/> + </annotations> + <before> + <amOnPage url="/beforeUrl" stepKey="before1"/> + </before> + <after> + <amOnPage url="/afterUrl" stepKey="after1"/> + </after> + <amOnPage stepKey="step1" url="/step1"/> + <fillField stepKey="step3" selector="#username" userInput="step3"/> + <fillField stepKey="step5" selector="#password" userInput="step5"/> + <click stepKey="step6" selector=".step6"/> + <click stepKey="step10" selector="#step10ShouldNotInResult"/> + </test> +</tests> diff --git a/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateBasicXYOffsetTest.xml b/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateBasicXYOffsetTest.xml new file mode 100644 index 000000000..5fee83988 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateBasicXYOffsetTest.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="NotGenerateBasicXYOffsetTest"> + <clickWithLeftButton selector="{{SampleSection.simpleElement}}{{SampleSection.simpleElementOneParam(offset.x)}}" x="{{offsetN.x}}" y="{{offset.y}}" stepKey="clickWithLeftButtonKeyXY1" /> + <clickWithRightButton selector="{{SampleSection.simpleElementOneParam('4123')}}{{SampleSection.simpleElement}}" x="{{offset.x}}" y="{{offset.yN}}" stepKey="clickWithRightButtonKeyXY1" /> + </test> +</tests> diff --git a/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateDataReferenceTest.xml b/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateDataReferenceTest.xml new file mode 100644 index 000000000..be05ca994 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateDataReferenceTest.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="NotGenerateDataReferenceTest"> + <fillField stepKey="fill" userInput="{{NotExtendParentData.name}}" selector="#name"/> + </test> +</tests> diff --git a/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateDataReplacementTest.xml b/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateDataReplacementTest.xml new file mode 100644 index 000000000..efab25f7e --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateDataReplacementTest.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="NotGenerateDataReplacementTest"> + <fillField stepKey="inputReplace" selector="#selector" userInput="StringBefore {{simpleDataN.firstname}} StringAfter"/> + <seeCurrentUrlMatches stepKey="seeInRegex" regex="~\/{{simpleData.Nfirstname}}~i"/> + <fillField stepKey="selectorReplace" selector="#{{simpleData.Nfirstname}}" userInput="input"/> + <dragAndDrop stepKey="selector12Replace" selector1="#{{simpleData.Nfirstname}}" selector2="{{NsimpleData.lastname}}"/> + <conditionalClick stepKey="dependentSelectorReplace" dependentSelector="#{{simpleData.Nfirstname}}" selector="{{simpleDataN.lastname}}" visible="true"/> + <amOnUrl stepKey="urlReplace" url="{{simpleData.firstnameN}}.html"/> + <searchAndMultiSelectOption stepKey="parameterArrayReplacement" selector="#selector" parameterArray="[{{simpleData.firstnameN}}, {{NsimpleData.lastname}}]"/> + + <seeInPageSource html="StringBefore {{simpleData.Nfirstname}} StringAfter" stepKey="htmlReplace1"/> + <seeInPageSource html="#{{simpleDataN.firstname}}" stepKey="htmlReplace2"/> + </test> +</tests> \ No newline at end of file diff --git a/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateMergeMassViaInsertAfter.xml b/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateMergeMassViaInsertAfter.xml new file mode 100644 index 000000000..1f2bae414 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateMergeMassViaInsertAfter.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="NotGenerateMergeMassViaInsertAfter"> + <fillField selector="#foo" userInput="foo" stepKey="fillField1"/> + <fillField selector="#bar" userInput="bar" stepKey="fillField2"/> + <fillField selector="#baz" userInput="baz" stepKey="fillField3"/> + </test> +</tests> diff --git a/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateMergeMassViaInsertBefore.xml b/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateMergeMassViaInsertBefore.xml new file mode 100644 index 000000000..ceb7f2e81 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateMergeMassViaInsertBefore.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="NotGenerateMergeMassViaInsertBefore"> + <fillField selector="#foo" userInput="foo" stepKey="fillField1"/> + <fillField selector="#bar" userInput="bar" stepKey="fillField2"/> + <fillField selector="#baz" userInput="baz" stepKey="fillField3"/> + </test> +</tests> diff --git a/dev/tests/verification/ResilientGenerationModule/Test/NotGeneratePageReplacementTest.xml b/dev/tests/verification/ResilientGenerationModule/Test/NotGeneratePageReplacementTest.xml new file mode 100644 index 000000000..1a4a61336 --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/NotGeneratePageReplacementTest.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="NotGeneratePageReplacementTest"> + <createData entity="simpleData" stepKey="datakey"/> + <amOnPage stepKey="noParamPage" url="{{NoParamPageN.url}}"/> + <amOnPage stepKey="noParamPageN" url="{{NoParamPage.urlN}}"/> + <amOnPage stepKey="oneParamPageString" url="{{OneParamPage.url('StringLiteral', 'N')}}"/> + <amOnPage stepKey="oneParamPageData" url="{{OneParamPage.url(simpleDataN.firstname)}}"/> + <amOnPage stepKey="oneParamPagePersist" url="{{OneParamPage.url($datakey.firstnameN$)}}"/> + <amOnPage stepKey="twoParamPageString" url="{{TwoParamPage.url('StringLiteral1')}}"/> + <amOnPage stepKey="twoParamPageStringData" url="{{TwoParamPage.url(simpleData.firstnameN, 'StringLiteral2')}}"/> + <amOnPage stepKey="twoParamPageDataPersist" url="{{TwoParamPage.url(simpleData.firstname, $datakey.firstnameN$)}}"/> + <amOnPage stepKey="twoParamPagePersistString" url="{{TwoParamPage.url($datakey.firstnameN$, 'StringLiteral2')}}"/> + </test> +</tests> \ No newline at end of file diff --git a/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateSectionReplacementTest.xml b/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateSectionReplacementTest.xml new file mode 100644 index 000000000..ea527d49f --- /dev/null +++ b/dev/tests/verification/ResilientGenerationModule/Test/NotGenerateSectionReplacementTest.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="NotGenerateSectionReplacementTest"> + <click stepKey="selectorReplace" selector="{{SampleSectionN.simpleElement}}"/> + <click stepKey="selectorReplaceTimeout" selector="{{SampleSection.timeoutElementN}}"/> + + <click stepKey="selectorReplaceOneParam" selector="{{SampleSection.oneParamElement('stringLiteral', 'N')}}"/> + <click stepKey="selectorReplaceTwoParam" selector="{{SampleSection.twoParamElement('stringLiteral1')}}"/> + + <click stepKey="selectorReplaceThreeParamDataRef" selector="{{SampleSection.threeParamElement(simpleData.firstname, simpleDataN.lastname, simpleData.middlename)}}"/> + <click stepKey="selectorReplaceThreeParamOneDupeDataRef" selector="{{SampleSection.threeOneDuplicateParamElement(simpleData.firstname, simpleData.lastname, simpleData.Nmiddlename)}}"/> + + <click stepKey="selectorReplaceOneParamVariable" selector="{{SampleSection.oneParamElement({$dataN})}}"/> + + <click stepKey="selectorReplaceThreeParamMixed1" selector="{{SampleSection.threeParamElement('stringLiteral1', $createdData.Nfirstname$, simpleData.firstname)}}"/> + <click stepKey="selectorReplaceThreeParamMixed2" selector="{{SampleSection.threeParamElement('stringLiteral1', $NcreatedData.firstname$, {$data})}}"/> + + <click stepKey="selectorReplaceTwoParamElements" selector="{{SampleSection.oneParamElement('1')}}{{SampleSectionN.oneParamElement('2')}}"/> + <click stepKey="selectorReplaceTwoParamMixedTypes" selector="{{SampleSection.oneParamElement('1')}}{{SampleSection.oneParamElementN({$data})}}"/> + </test> +</tests> diff --git a/dev/tests/verification/Resources/ActionGroupContainsStepKeyInArgText.txt b/dev/tests/verification/Resources/ActionGroupContainsStepKeyInArgText.txt index d6640846f..3faa07c06 100644 --- a/dev/tests/verification/Resources/ActionGroupContainsStepKeyInArgText.txt +++ b/dev/tests/verification/Resources/ActionGroupContainsStepKeyInArgText.txt @@ -17,20 +17,37 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ActionGroupContainsStepKeyInArgTextCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $I->comment("Entering Action Group [actionGroup] actionGroupContainsStepKeyInArgValue"); $I->see("arg1", ".selector"); // stepKey: arg1ActionGroup $I->comment("Exiting Action Group [actionGroup] actionGroupContainsStepKeyInArgValue"); + $I->comment('[END BEFORE HOOK]'); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } } /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -41,4 +58,10 @@ class ActionGroupContainsStepKeyInArgTextCest $I->see("arg1", ".selector"); // stepKey: arg1ActionGroup $I->comment("Exiting Action Group [actionGroup] actionGroupContainsStepKeyInArgValue"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ActionGroupMergedViaInsertAfter.txt b/dev/tests/verification/Resources/ActionGroupMergedViaInsertAfter.txt index 352af3a2b..56f80c3af 100644 --- a/dev/tests/verification/Resources/ActionGroupMergedViaInsertAfter.txt +++ b/dev/tests/verification/Resources/ActionGroupMergedViaInsertAfter.txt @@ -17,9 +17,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ActionGroupMergedViaInsertAfterCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -35,4 +50,10 @@ class ActionGroupMergedViaInsertAfterCest $I->fillField("#baz", "baz"); // stepKey: fillField3Keyone $I->comment("Exiting Action Group [keyone] FunctionalActionGroupForMassMergeAfter"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ActionGroupMergedViaInsertBefore.txt b/dev/tests/verification/Resources/ActionGroupMergedViaInsertBefore.txt index 009109146..1479f8d9d 100644 --- a/dev/tests/verification/Resources/ActionGroupMergedViaInsertBefore.txt +++ b/dev/tests/verification/Resources/ActionGroupMergedViaInsertBefore.txt @@ -17,9 +17,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ActionGroupMergedViaInsertBeforeCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -35,4 +50,10 @@ class ActionGroupMergedViaInsertBeforeCest $I->fillField("#baz", "baz"); // stepKey: fillField3Keyone $I->comment("Exiting Action Group [keyone] FunctionalActionGroupForMassMergeBefore"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ActionGroupReturningValueTest.txt b/dev/tests/verification/Resources/ActionGroupReturningValueTest.txt new file mode 100644 index 000000000..fdf05ad4b --- /dev/null +++ b/dev/tests/verification/Resources/ActionGroupReturningValueTest.txt @@ -0,0 +1,92 @@ +<?php +namespace Magento\AcceptanceTest\_default\Backend; + +use Magento\FunctionalTestingFramework\AcceptanceTester; +use \Codeception\Util\Locator; +use Yandex\Allure\Adapter\Annotation\Features; +use Yandex\Allure\Adapter\Annotation\Stories; +use Yandex\Allure\Adapter\Annotation\Title; +use Yandex\Allure\Adapter\Annotation\Description; +use Yandex\Allure\Adapter\Annotation\Parameter; +use Yandex\Allure\Adapter\Annotation\Severity; +use Yandex\Allure\Adapter\Model\SeverityLevel; +use Yandex\Allure\Adapter\Annotation\TestCaseId; + +/** + * @group functional + * @Description("<h3>Test files</h3>verification/TestModule/Test/ActionGroupFunctionalTest/ActionGroupReturningValueTest.xml<br>") + */ +class ActionGroupReturningValueTestCest +{ + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _before(AcceptanceTester $I) + { + $I->comment('[START BEFORE HOOK]'); + $I->createEntity("createPersonParam", "hook", "ReplacementPerson", [], []); // stepKey: createPersonParam + $I->comment("Entering Action Group [beforeGroup] FunctionalActionGroup"); + $I->fillField("#foo", "myData1"); // stepKey: fillField1BeforeGroup + $I->fillField("#bar", "myData2"); // stepKey: fillField2BeforeGroup + $I->comment("Exiting Action Group [beforeGroup] FunctionalActionGroup"); + $I->comment('[END BEFORE HOOK]'); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + $I->comment('[START AFTER HOOK]'); + $I->comment("Entering Action Group [afterGroup] FunctionalActionGroup"); + $I->fillField("#foo", "myData1"); // stepKey: fillField1AfterGroup + $I->fillField("#bar", "myData2"); // stepKey: fillField2AfterGroup + $I->comment("Exiting Action Group [afterGroup] FunctionalActionGroup"); + $I->comment('[END AFTER HOOK]'); + if ($this->isSuccess) { + unlink(__FILE__); + } + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _failed(AcceptanceTester $I) + { + $I->saveScreenshot(); // stepKey: saveScreenshot + } + + /** + * @Severity(level = SeverityLevel::CRITICAL) + * @Features({"TestModule"}) + * @Stories({"MQE-433"}) + * @param AcceptanceTester $I + * @return void + * @throws \Exception + */ + public function ActionGroupReturningValueTest(AcceptanceTester $I) + { + $I->amOnPage("/someUrl"); // stepKey: step1 + $I->comment("Entering Action Group [actionGroupWithReturnValue1] FunctionalActionGroupWithReturnValueActionGroup"); + $grabTextFrom1ActionGroupWithReturnValue1 = $I->grabTextFrom("#foo"); // stepKey: grabTextFrom1ActionGroupWithReturnValue1 + $actionGroupWithReturnValue1 = $I->return($grabTextFrom1ActionGroupWithReturnValue1); // stepKey: returnActionGroupWithReturnValue1 + $I->comment("Exiting Action Group [actionGroupWithReturnValue1] FunctionalActionGroupWithReturnValueActionGroup"); + $I->comment("Entering Action Group [actionGroupWithStringUsage1] actionGroupWithStringUsage"); + $I->see($actionGroupWithReturnValue1, "#element .{$actionGroupWithReturnValue1}"); // stepKey: see1ActionGroupWithStringUsage1 + $I->comment("Exiting Action Group [actionGroupWithStringUsage1] actionGroupWithStringUsage"); + } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } +} diff --git a/dev/tests/verification/Resources/ActionGroupToExtend.txt b/dev/tests/verification/Resources/ActionGroupToExtend.txt index bbb9efa0f..7fd1a8f05 100644 --- a/dev/tests/verification/Resources/ActionGroupToExtend.txt +++ b/dev/tests/verification/Resources/ActionGroupToExtend.txt @@ -17,9 +17,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ActionGroupToExtendCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -31,4 +46,10 @@ class ActionGroupToExtendCest $I->assertCount(99, $grabProductsActionGroup); // stepKey: assertCountActionGroup $I->comment("Exiting Action Group [actionGroup] ActionGroupToExtend"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ActionGroupUsingCreateData.txt b/dev/tests/verification/Resources/ActionGroupUsingCreateData.txt index cbddc5f4d..2ca4e0a63 100644 --- a/dev/tests/verification/Resources/ActionGroupUsingCreateData.txt +++ b/dev/tests/verification/Resources/ActionGroupUsingCreateData.txt @@ -17,21 +17,37 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ActionGroupUsingCreateDataCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $I->comment("Entering Action Group [Key1] actionGroupWithCreateData"); - $I->createEntity("createCategoryKey1", "hook", "ApiCategory", [], []); // stepKey: createCategoryKey1 - $I->createEntity("createConfigProductKey1", "hook", "ApiConfigurableProduct", ["createCategoryKey1"], []); // stepKey: createConfigProductKey1 + $I->createEntity("createConfigProductKey1", "hook", "TestData", ["createCategory"], []); // stepKey: createConfigProductKey1 $I->comment("Exiting Action Group [Key1] actionGroupWithCreateData"); + $I->comment('[END BEFORE HOOK]'); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } } /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -39,4 +55,10 @@ class ActionGroupUsingCreateDataCest public function ActionGroupUsingCreateData(AcceptanceTester $I) { } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ActionGroupUsingNestedArgument.txt b/dev/tests/verification/Resources/ActionGroupUsingNestedArgument.txt index 0cfd7f84d..40254de00 100644 --- a/dev/tests/verification/Resources/ActionGroupUsingNestedArgument.txt +++ b/dev/tests/verification/Resources/ActionGroupUsingNestedArgument.txt @@ -17,9 +17,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ActionGroupUsingNestedArgumentCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -31,4 +46,10 @@ class ActionGroupUsingNestedArgumentCest $I->assertCount(99, $grabProductsActionGroup); // stepKey: assertCountActionGroup $I->comment("Exiting Action Group [actionGroup] ActionGroupToExtend"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ActionGroupWithDataOverrideTest.txt b/dev/tests/verification/Resources/ActionGroupWithDataOverrideTest.txt index b60a4da5f..79f182e8a 100644 --- a/dev/tests/verification/Resources/ActionGroupWithDataOverrideTest.txt +++ b/dev/tests/verification/Resources/ActionGroupWithDataOverrideTest.txt @@ -18,17 +18,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ActionGroupWithDataOverrideTestCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $I->createEntity("createPersonParam", "hook", "ReplacementPerson", [], []); // stepKey: createPersonParam $I->comment("Entering Action Group [beforeGroup] FunctionalActionGroup"); $I->fillField("#foo", "myData1"); // stepKey: fillField1BeforeGroup $I->fillField("#bar", "myData2"); // stepKey: fillField2BeforeGroup $I->comment("Exiting Action Group [beforeGroup] FunctionalActionGroup"); + $I->comment('[END BEFORE HOOK]'); } /** @@ -37,10 +44,15 @@ class ActionGroupWithDataOverrideTestCest */ public function _after(AcceptanceTester $I) { + $I->comment('[START AFTER HOOK]'); $I->comment("Entering Action Group [afterGroup] FunctionalActionGroup"); $I->fillField("#foo", "myData1"); // stepKey: fillField1AfterGroup $I->fillField("#bar", "myData2"); // stepKey: fillField2AfterGroup $I->comment("Exiting Action Group [afterGroup] FunctionalActionGroup"); + $I->comment('[END AFTER HOOK]'); + if ($this->isSuccess) { + unlink(__FILE__); + } } /** @@ -56,7 +68,6 @@ class ActionGroupWithDataOverrideTestCest * @Severity(level = SeverityLevel::CRITICAL) * @Features({"TestModule"}) * @Stories({"MQE-433"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -73,4 +84,10 @@ class ActionGroupWithDataOverrideTestCest $I->comment("Exiting Action Group [actionGroupWithDataOverride1] FunctionalActionGroupWithData"); $I->click("loginButton"); // stepKey: step6 } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ActionGroupWithDataTest.txt b/dev/tests/verification/Resources/ActionGroupWithDataTest.txt index 5f19cdc37..ed8b69a77 100644 --- a/dev/tests/verification/Resources/ActionGroupWithDataTest.txt +++ b/dev/tests/verification/Resources/ActionGroupWithDataTest.txt @@ -18,17 +18,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ActionGroupWithDataTestCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $I->createEntity("createPersonParam", "hook", "ReplacementPerson", [], []); // stepKey: createPersonParam $I->comment("Entering Action Group [beforeGroup] FunctionalActionGroup"); $I->fillField("#foo", "myData1"); // stepKey: fillField1BeforeGroup $I->fillField("#bar", "myData2"); // stepKey: fillField2BeforeGroup $I->comment("Exiting Action Group [beforeGroup] FunctionalActionGroup"); + $I->comment('[END BEFORE HOOK]'); } /** @@ -37,10 +44,15 @@ class ActionGroupWithDataTestCest */ public function _after(AcceptanceTester $I) { + $I->comment('[START AFTER HOOK]'); $I->comment("Entering Action Group [afterGroup] FunctionalActionGroup"); $I->fillField("#foo", "myData1"); // stepKey: fillField1AfterGroup $I->fillField("#bar", "myData2"); // stepKey: fillField2AfterGroup $I->comment("Exiting Action Group [afterGroup] FunctionalActionGroup"); + $I->comment('[END AFTER HOOK]'); + if ($this->isSuccess) { + unlink(__FILE__); + } } /** @@ -56,7 +68,6 @@ class ActionGroupWithDataTestCest * @Severity(level = SeverityLevel::CRITICAL) * @Features({"TestModule"}) * @Stories({"MQE-433"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -73,4 +84,10 @@ class ActionGroupWithDataTestCest $I->comment("Exiting Action Group [actionGroupWithData1] FunctionalActionGroupWithData"); $I->click("loginButton"); // stepKey: step6 } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ActionGroupWithDefaultArgumentAndStringSelectorParam.txt b/dev/tests/verification/Resources/ActionGroupWithDefaultArgumentAndStringSelectorParam.txt index 65339e970..838c98e3b 100644 --- a/dev/tests/verification/Resources/ActionGroupWithDefaultArgumentAndStringSelectorParam.txt +++ b/dev/tests/verification/Resources/ActionGroupWithDefaultArgumentAndStringSelectorParam.txt @@ -18,10 +18,25 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ActionGroupWithDefaultArgumentAndStringSelectorParamCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Severity(level = SeverityLevel::BLOCKER) * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -32,4 +47,10 @@ class ActionGroupWithDefaultArgumentAndStringSelectorParamCest $I->see("John", "#element .test1"); // stepKey: seeFirstNameActionGroup $I->comment("Exiting Action Group [actionGroup] actionGroupWithDefaultArgumentAndStringSelectorParam"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ActionGroupWithMultipleParameterSelectorsFromDefaultArgument.txt b/dev/tests/verification/Resources/ActionGroupWithMultipleParameterSelectorsFromDefaultArgument.txt index e8cedee97..31a77b903 100644 --- a/dev/tests/verification/Resources/ActionGroupWithMultipleParameterSelectorsFromDefaultArgument.txt +++ b/dev/tests/verification/Resources/ActionGroupWithMultipleParameterSelectorsFromDefaultArgument.txt @@ -18,10 +18,25 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ActionGroupWithMultipleParameterSelectorsFromDefaultArgumentCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Severity(level = SeverityLevel::BLOCKER) * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -32,4 +47,10 @@ class ActionGroupWithMultipleParameterSelectorsFromDefaultArgumentCest $I->see("Doe", "#John-Doe .test"); // stepKey: seeLastNameActionGroup $I->comment("Exiting Action Group [actionGroup] actionGroupWithMultipleParameterSelectorsFromArgument"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ActionGroupWithNoArguments.txt b/dev/tests/verification/Resources/ActionGroupWithNoArguments.txt index e5b9717c2..9d06b309e 100644 --- a/dev/tests/verification/Resources/ActionGroupWithNoArguments.txt +++ b/dev/tests/verification/Resources/ActionGroupWithNoArguments.txt @@ -18,10 +18,25 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ActionGroupWithNoArgumentsCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Severity(level = SeverityLevel::BLOCKER) * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -32,4 +47,10 @@ class ActionGroupWithNoArgumentsCest $I->wait(1); // stepKey: waitForNothingActionGroup $I->comment("Exiting Action Group [actionGroup] actionGroupWithoutArguments"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ActionGroupWithNoDefaultTest.txt b/dev/tests/verification/Resources/ActionGroupWithNoDefaultTest.txt index c0bde006d..435b8a5cc 100644 --- a/dev/tests/verification/Resources/ActionGroupWithNoDefaultTest.txt +++ b/dev/tests/verification/Resources/ActionGroupWithNoDefaultTest.txt @@ -18,17 +18,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ActionGroupWithNoDefaultTestCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $I->createEntity("createPersonParam", "hook", "ReplacementPerson", [], []); // stepKey: createPersonParam $I->comment("Entering Action Group [beforeGroup] FunctionalActionGroup"); $I->fillField("#foo", "myData1"); // stepKey: fillField1BeforeGroup $I->fillField("#bar", "myData2"); // stepKey: fillField2BeforeGroup $I->comment("Exiting Action Group [beforeGroup] FunctionalActionGroup"); + $I->comment('[END BEFORE HOOK]'); } /** @@ -37,10 +44,15 @@ class ActionGroupWithNoDefaultTestCest */ public function _after(AcceptanceTester $I) { + $I->comment('[START AFTER HOOK]'); $I->comment("Entering Action Group [afterGroup] FunctionalActionGroup"); $I->fillField("#foo", "myData1"); // stepKey: fillField1AfterGroup $I->fillField("#bar", "myData2"); // stepKey: fillField2AfterGroup $I->comment("Exiting Action Group [afterGroup] FunctionalActionGroup"); + $I->comment('[END AFTER HOOK]'); + if ($this->isSuccess) { + unlink(__FILE__); + } } /** @@ -56,7 +68,6 @@ class ActionGroupWithNoDefaultTestCest * @Severity(level = SeverityLevel::CRITICAL) * @Features({"TestModule"}) * @Stories({"MQE-433"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -71,4 +82,10 @@ class ActionGroupWithNoDefaultTestCest $I->comment("Exiting Action Group [actionGroupWithDataOverride1] FunctionalActionGroupNoDefault"); $I->click("loginButton"); // stepKey: step6 } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ActionGroupWithParameterizedElementWithHyphen.txt b/dev/tests/verification/Resources/ActionGroupWithParameterizedElementWithHyphen.txt index 812285111..376a6298d 100644 --- a/dev/tests/verification/Resources/ActionGroupWithParameterizedElementWithHyphen.txt +++ b/dev/tests/verification/Resources/ActionGroupWithParameterizedElementWithHyphen.txt @@ -17,9 +17,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ActionGroupWithParameterizedElementWithHyphenCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -30,4 +45,10 @@ class ActionGroupWithParameterizedElementWithHyphenCest $keyoneActionGroup = $I->executeJS("#element .full-width"); // stepKey: keyoneActionGroup $I->comment("Exiting Action Group [actionGroup] SectionArgumentWithParameterizedSelector"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ActionGroupWithParameterizedElementsWithStepKeyReferences.txt b/dev/tests/verification/Resources/ActionGroupWithParameterizedElementsWithStepKeyReferences.txt index 6c0c3fd0f..83aaecf08 100644 --- a/dev/tests/verification/Resources/ActionGroupWithParameterizedElementsWithStepKeyReferences.txt +++ b/dev/tests/verification/Resources/ActionGroupWithParameterizedElementsWithStepKeyReferences.txt @@ -17,9 +17,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ActionGroupWithParameterizedElementsWithStepKeyReferencesCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -35,4 +50,10 @@ class ActionGroupWithParameterizedElementsWithStepKeyReferencesCest $I->seeElement("//div[@name='Tiberius'][@class={$testVariableActionGroup}][@data-element='{$testVariable2ActionGroup}'][" . $I->retrieveEntityField('createSimpleData', 'name', 'test') . "]"); // stepKey: see1ActionGroup $I->comment("Exiting Action Group [actionGroup] actionGroupWithParametrizedSelectors"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ActionGroupWithPassedArgumentAndStringSelectorParam.txt b/dev/tests/verification/Resources/ActionGroupWithPassedArgumentAndStringSelectorParam.txt index 6614a320d..1a2e886bb 100644 --- a/dev/tests/verification/Resources/ActionGroupWithPassedArgumentAndStringSelectorParam.txt +++ b/dev/tests/verification/Resources/ActionGroupWithPassedArgumentAndStringSelectorParam.txt @@ -18,10 +18,25 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ActionGroupWithPassedArgumentAndStringSelectorParamCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Severity(level = SeverityLevel::BLOCKER) * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -32,4 +47,10 @@ class ActionGroupWithPassedArgumentAndStringSelectorParamCest $I->see("John" . msq("UniquePerson"), "#element .test1"); // stepKey: seeFirstNameActionGroup $I->comment("Exiting Action Group [actionGroup] actionGroupWithDefaultArgumentAndStringSelectorParam"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ActionGroupWithPersistedData.txt b/dev/tests/verification/Resources/ActionGroupWithPersistedData.txt index c1905ff39..12aae9c21 100644 --- a/dev/tests/verification/Resources/ActionGroupWithPersistedData.txt +++ b/dev/tests/verification/Resources/ActionGroupWithPersistedData.txt @@ -18,17 +18,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ActionGroupWithPersistedDataCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $I->createEntity("createPersonParam", "hook", "ReplacementPerson", [], []); // stepKey: createPersonParam $I->comment("Entering Action Group [beforeGroup] FunctionalActionGroup"); $I->fillField("#foo", "myData1"); // stepKey: fillField1BeforeGroup $I->fillField("#bar", "myData2"); // stepKey: fillField2BeforeGroup $I->comment("Exiting Action Group [beforeGroup] FunctionalActionGroup"); + $I->comment('[END BEFORE HOOK]'); } /** @@ -37,10 +44,15 @@ class ActionGroupWithPersistedDataCest */ public function _after(AcceptanceTester $I) { + $I->comment('[START AFTER HOOK]'); $I->comment("Entering Action Group [afterGroup] FunctionalActionGroup"); $I->fillField("#foo", "myData1"); // stepKey: fillField1AfterGroup $I->fillField("#bar", "myData2"); // stepKey: fillField2AfterGroup $I->comment("Exiting Action Group [afterGroup] FunctionalActionGroup"); + $I->comment('[END AFTER HOOK]'); + if ($this->isSuccess) { + unlink(__FILE__); + } } /** @@ -56,7 +68,6 @@ class ActionGroupWithPersistedDataCest * @Severity(level = SeverityLevel::CRITICAL) * @Features({"TestModule"}) * @Stories({"MQE-433"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -72,4 +83,10 @@ class ActionGroupWithPersistedDataCest $I->see("#element ." . $I->retrieveEntityField('createPerson', 'firstname', 'test')); // stepKey: see1ActionGroupWithPersistedData1 $I->comment("Exiting Action Group [actionGroupWithPersistedData1] FunctionalActionGroupWithData"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ActionGroupWithSectionAndDataAsArguments.txt b/dev/tests/verification/Resources/ActionGroupWithSectionAndDataAsArguments.txt index b3dcc585f..65d25df00 100644 --- a/dev/tests/verification/Resources/ActionGroupWithSectionAndDataAsArguments.txt +++ b/dev/tests/verification/Resources/ActionGroupWithSectionAndDataAsArguments.txt @@ -17,9 +17,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ActionGroupWithSectionAndDataAsArgumentsCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -30,4 +45,10 @@ class ActionGroupWithSectionAndDataAsArgumentsCest $I->waitForElementVisible("#element .John", 10); // stepKey: arg1ActionGroup $I->comment("Exiting Action Group [actionGroup] actionGroupWithSectionAndData"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromDefaultArgument.txt b/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromDefaultArgument.txt index 6d540cb95..65ce18e9f 100644 --- a/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromDefaultArgument.txt +++ b/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromDefaultArgument.txt @@ -18,10 +18,25 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ActionGroupWithSimpleDataUsageFromDefaultArgumentCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Severity(level = SeverityLevel::CRITICAL) * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -32,4 +47,10 @@ class ActionGroupWithSimpleDataUsageFromDefaultArgumentCest $I->see("stringLiteral", "#element .stringLiteral"); // stepKey: see1ActionGroup $I->comment("Exiting Action Group [actionGroup] actionGroupWithStringUsage"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromPassedArgument.txt b/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromPassedArgument.txt index 826671a63..d33a7da1e 100644 --- a/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromPassedArgument.txt +++ b/dev/tests/verification/Resources/ActionGroupWithSimpleDataUsageFromPassedArgument.txt @@ -18,10 +18,25 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ActionGroupWithSimpleDataUsageFromPassedArgumentCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Severity(level = SeverityLevel::CRITICAL) * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -59,4 +74,10 @@ class ActionGroupWithSimpleDataUsageFromPassedArgumentCest $I->see($I->retrieveEntityField('simpleData', 'firstname[data_index]', 'test'), "#element ." . $I->retrieveEntityField('simpleData', 'firstname[data_index]', 'test')); // stepKey: see1ActionGroup7 $I->comment("Exiting Action Group [actionGroup7] actionGroupWithEntityUsage"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromDefaultArgument.txt b/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromDefaultArgument.txt index 5e64aded3..7aa0ba5a6 100644 --- a/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromDefaultArgument.txt +++ b/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromDefaultArgument.txt @@ -18,10 +18,25 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ActionGroupWithSingleParameterSelectorFromDefaultArgumentCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Severity(level = SeverityLevel::BLOCKER) * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -32,4 +47,10 @@ class ActionGroupWithSingleParameterSelectorFromDefaultArgumentCest $I->see("Doe", "#element .John"); // stepKey: seeLastNameActionGroup $I->comment("Exiting Action Group [actionGroup] actionGroupWithSingleParameterSelectorFromArgument"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromPassedArgument.txt b/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromPassedArgument.txt index e71a8a716..93fb74898 100644 --- a/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromPassedArgument.txt +++ b/dev/tests/verification/Resources/ActionGroupWithSingleParameterSelectorFromPassedArgument.txt @@ -18,10 +18,25 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ActionGroupWithSingleParameterSelectorFromPassedArgumentCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Severity(level = SeverityLevel::BLOCKER) * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -32,4 +47,10 @@ class ActionGroupWithSingleParameterSelectorFromPassedArgumentCest $I->see("Doe", "#element .John" . msq("UniquePerson")); // stepKey: seeLastNameActionGroup $I->comment("Exiting Action Group [actionGroup] actionGroupWithSingleParameterSelectorFromArgument"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ActionGroupWithStepKeyReferences.txt b/dev/tests/verification/Resources/ActionGroupWithStepKeyReferences.txt index d623839d3..0d62a57e3 100644 --- a/dev/tests/verification/Resources/ActionGroupWithStepKeyReferences.txt +++ b/dev/tests/verification/Resources/ActionGroupWithStepKeyReferences.txt @@ -17,9 +17,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ActionGroupWithStepKeyReferencesCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -27,7 +42,7 @@ class ActionGroupWithStepKeyReferencesCest public function ActionGroupWithStepKeyReferences(AcceptanceTester $I) { $I->comment("Entering Action Group [actionGroup] FunctionActionGroupWithStepKeyReferences"); - $I->createEntity("createSimpleDataActionGroup", "test", "simpleData", [], []); // stepKey: createSimpleDataActionGroup + $I->createEntity("createSimpleDataActionGroup", "test", "TestData", [], []); // stepKey: createSimpleDataActionGroup $grabTextDataActionGroup = $I->grabTextFrom(".class"); // stepKey: grabTextDataActionGroup $I->fillField(".{$grabTextDataActionGroup}", $I->retrieveEntityField('createSimpleDataActionGroup', 'field', 'test')); // stepKey: fill1ActionGroup $I->comment("Invocation stepKey will not be appended in non stepKey instances"); @@ -42,17 +57,23 @@ class ActionGroupWithStepKeyReferencesCest $date->setTimezone(new \DateTimeZone("America/Los_Angeles")); $action5ActionGroup = $date->format("H:i:s"); - $action6ActionGroup = $I->formatMoney($action6ActionGroup); // stepKey: action6ActionGroup + $action6ActionGroup = $I->formatCurrency($action6ActionGroup, "en_CA", "USD"); // stepKey: action6ActionGroup $I->deleteEntity("{$action7ActionGroupActionGroup}", "test"); // stepKey: action7ActionGroup $I->getEntity("action8ActionGroup", "test", "{$action8}", [], null); // stepKey: action8ActionGroup $I->updateEntity("1", "test", "{$action9}",[]); // stepKey: action9ActionGroup - $I->createEntity("action10ActionGroup", "test", "{$action10}", [], []); // stepKey: action10ActionGroup $action11ActionGroup = $I->grabAttributeFrom($action11ActionGroup, "someInput"); // stepKey: action11ActionGroup $action12ActionGroup = $I->grabCookie($action12ActionGroup, ['domain' => 'www.google.com']); // stepKey: action12ActionGroup $action13ActionGroup = $I->grabFromCurrentUrl($action13ActionGroup); // stepKey: action13ActionGroup $action14ActionGroup = $I->grabMultiple($action14ActionGroup); // stepKey: action14ActionGroup $action15ActionGroup = $I->grabTextFrom($action15ActionGroup); // stepKey: action15ActionGroup $action16ActionGroup = $I->grabValueFrom($action16ActionGroup); // stepKey: action16ActionGroup + $action17ActionGroup = $I->grabCookieAttributes($action17ActionGroup, ['domain' => 'www.google.com']); // stepKey: action17ActionGroup $I->comment("Exiting Action Group [actionGroup] FunctionActionGroupWithStepKeyReferences"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ActionGroupWithTopLevelPersistedData.txt b/dev/tests/verification/Resources/ActionGroupWithTopLevelPersistedData.txt index 18a205544..4db67f901 100644 --- a/dev/tests/verification/Resources/ActionGroupWithTopLevelPersistedData.txt +++ b/dev/tests/verification/Resources/ActionGroupWithTopLevelPersistedData.txt @@ -18,17 +18,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ActionGroupWithTopLevelPersistedDataCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $I->createEntity("createPersonParam", "hook", "ReplacementPerson", [], []); // stepKey: createPersonParam $I->comment("Entering Action Group [beforeGroup] FunctionalActionGroup"); $I->fillField("#foo", "myData1"); // stepKey: fillField1BeforeGroup $I->fillField("#bar", "myData2"); // stepKey: fillField2BeforeGroup $I->comment("Exiting Action Group [beforeGroup] FunctionalActionGroup"); + $I->comment('[END BEFORE HOOK]'); } /** @@ -37,10 +44,15 @@ class ActionGroupWithTopLevelPersistedDataCest */ public function _after(AcceptanceTester $I) { + $I->comment('[START AFTER HOOK]'); $I->comment("Entering Action Group [afterGroup] FunctionalActionGroup"); $I->fillField("#foo", "myData1"); // stepKey: fillField1AfterGroup $I->fillField("#bar", "myData2"); // stepKey: fillField2AfterGroup $I->comment("Exiting Action Group [afterGroup] FunctionalActionGroup"); + $I->comment('[END AFTER HOOK]'); + if ($this->isSuccess) { + unlink(__FILE__); + } } /** @@ -56,7 +68,6 @@ class ActionGroupWithTopLevelPersistedDataCest * @Severity(level = SeverityLevel::CRITICAL) * @Features({"TestModule"}) * @Stories({"MQE-433"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -71,4 +82,10 @@ class ActionGroupWithTopLevelPersistedDataCest $I->see("#element ." . $I->retrieveEntityField('createPersonParam', 'firstname', 'test')); // stepKey: see1ActionGroupWithPersistedData1 $I->comment("Exiting Action Group [actionGroupWithPersistedData1] FunctionalActionGroupWithData"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ActionsInDifferentModulesSuite.txt b/dev/tests/verification/Resources/ActionsInDifferentModulesSuite.txt new file mode 100644 index 000000000..c986b6fb4 --- /dev/null +++ b/dev/tests/verification/Resources/ActionsInDifferentModulesSuite.txt @@ -0,0 +1,254 @@ +<?php + +namespace Group; + +use Facebook\WebDriver\Remote\RemoteWebDriver; +use Facebook\WebDriver\Remote\DesiredCapabilities; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\PersistedObjectHandler; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; +use Magento\FunctionalTestingFramework\Module\MagentoWebDriver; +use Magento\FunctionalTestingFramework\Module\MagentoAssert; +use Magento\FunctionalTestingFramework\Module\MagentoActionProxies; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Codeception\Lib\ModuleContainer; +use Codeception\Module; +use Facebook\WebDriver\Chrome\ChromeOptions; + +/** + * Group class is Codeception Extension which is allowed to handle to all internal events. + * This class itself can be used to listen events for test execution of one particular group. + * It may be especially useful to create fixtures data, prepare server, etc. + * + * INSTALLATION: + * + * To use this group extension, include it to "extensions" option of global Codeception config. + */ +class ActionsInDifferentModulesSuite extends \Codeception\GroupObject +{ + public static $group = 'ActionsInDifferentModulesSuite'; + private $testCount = 1; + private $preconditionFailure = null; + private $currentTestRun = 0; + private static $HOOK_EXECUTION_INIT = "\n/******** Beginning execution of ActionsInDifferentModulesSuite suite %s block ********/\n"; + private static $HOOK_EXECUTION_END = "\n/******** Execution of ActionsInDifferentModulesSuite suite %s block complete ********/\n"; + /** @var MagentoWebDriver */ + private $webDriver; + /** @var ModuleContainer */ + private $moduleContainer; + + public function _before(\Codeception\Event\TestEvent $e) + { + $this->webDriver = $this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver'); + $this->moduleContainer = $this->webDriver->getModuleContainer(); + // increment test count per execution + $this->currentTestRun++; + $this->executePreConditions(); + + if ($this->preconditionFailure != null) { + //if our preconditions fail, we need to mark all the tests as incomplete. + $e->getTest()->getMetadata()->setIncomplete("SUITE PRECONDITION FAILED:" . PHP_EOL . $this->preconditionFailure); + } + } + + private function executePreConditions() + { + if ($this->currentTestRun == 1) { + $this->testCount = $this->getTestCount(); + + print sprintf(self::$HOOK_EXECUTION_INIT, "before"); + + try { + if ($this->webDriver->webDriver != null) { + $this->webDriver->_restart(); + } else { + $this->webDriver->_initializeSession(); + } + $cli = $this->getModuleForAction("magentoCLISecret")->magentoCLISecret($this->getModuleForAction("getSecret")->getSecret("magento/some/secret"), 60); // stepKey: cli + print($cli); // stepKey: cli + $create1Fields['someKey'] = "dataHere"; + PersistedObjectHandler::getInstance()->createEntity( + "create1", + "suite", + "SecretData", + [], + $create1Fields + ); + PersistedObjectHandler::getInstance()->createEntity( + "create2", + "suite", + "SecretData", + [] + ); + PersistedObjectHandler::getInstance()->createEntity( + "create3", + "suite", + "SecretData", + ["create1", "create2"] + ); + $this->getModuleForAction("fillSecretField")->fillSecretField("#fill", $this->getModuleForAction("getSecret")->getSecret("magento/some/secret") . "+" . $this->getModuleForAction("getSecret")->getSecret("magento/some/secret")); // stepKey: fillBefore + $this->getModuleForAction("click")->click(PersistedObjectHandler::getInstance()->retrieveEntityField('create2', 'key2', 'suite')); // stepKey: click + print("Entering Action Group [return1] ActionGroupReturningValueActionGroup"); + $grabProducts1Return1 = $this->getModuleForAction("grabMultiple")->grabMultiple("selector"); // stepKey: grabProducts1Return1 + $this->getModuleForAction("assertCount")->assertCount(1, $grabProducts1Return1); // stepKey: assertCountReturn1 + $return1 = $this->getModuleForAction("return")->return($grabProducts1Return1); // stepKey: returnProducts1Return1 + print("Exiting Action Group [return1] ActionGroupReturningValueActionGroup"); + } catch (\Exception $exception) { + $this->preconditionFailure = $exception->getMessage(); + } + + // reset configuration and close session + $this->webDriver->_resetConfig(); + $this->webDriver->webDriver->close(); + $this->webDriver->webDriver = null; + + print sprintf(self::$HOOK_EXECUTION_END, "before"); + } + } + + public function _after(\Codeception\Event\TestEvent $e) + { + $this->executePostConditions($e); + } + + private function executePostConditions(\Codeception\Event\TestEvent $e) + { + if ($this->currentTestRun == $this->testCount) { + print sprintf(self::$HOOK_EXECUTION_INIT, "after"); + + try { + // Find out if Test in Suite failed, will cause potential failures in suite after + $cest = $e->getTest(); + + //Access private TestResultObject to find stack and if there are any errors (as opposed to failures) + $testResultObject = call_user_func(\Closure::bind( + function () use ($cest) { + return $cest->getResultAggregator(); + }, + $cest + )); + $errors = $testResultObject->errors(); + + if (!empty($errors)) { + foreach ($errors as $error) { + if ($error->getTest()->getTestMethod() == $cest->getName()) { + // Do not attempt to run _after if failure was in the _after block + // Try to run _after but catch exceptions to prevent them from overwriting original failure. + print("LAST TEST IN SUITE FAILED, TEST AFTER MAY NOT BE SUCCESSFUL\n"); + } + } + } + if ($this->webDriver->webDriver != null) { + $this->webDriver->_restart(); + } else { + $this->webDriver->_initializeSession(); + } + print("Entering Action Group [return2] ExtendedActionGroupReturningValueActionGroup"); + $grabProducts1Return2 = $this->getModuleForAction("grabMultiple")->grabMultiple("selector"); // stepKey: grabProducts1Return2 + $this->getModuleForAction("assertCount")->assertCount(1, $grabProducts1Return2); // stepKey: assertCountReturn2 + $return2 = $this->getModuleForAction("return")->return($grabProducts1Return2); // stepKey: returnProducts1Return2 + $grabProducts2Return2 = $this->getModuleForAction("grabMultiple")->grabMultiple("otherSelector"); // stepKey: grabProducts2Return2 + $this->getModuleForAction("assertCount")->assertCount(2, $grabProducts2Return2); // stepKey: assertSecondCountReturn2 + $return2 = $this->getModuleForAction("return")->return($grabProducts2Return2); // stepKey: returnProducts2Return2 + print("Exiting Action Group [return2] ExtendedActionGroupReturningValueActionGroup"); + PersistedObjectHandler::getInstance()->deleteEntity( + "create1", + "suite" + ); + PersistedObjectHandler::getInstance()->deleteEntity( + "create2", + "suite" + ); + PersistedObjectHandler::getInstance()->deleteEntity( + "create3", + "suite" + ); + $this->getModuleForAction("deleteEntityByUrl")->deleteEntityByUrl("deleteThis"); // stepKey: deleteThis + $this->getModuleForAction("fillSecretField")->fillSecretField("#fill", $this->getModuleForAction("getSecret")->getSecret("magento/some/secret")); // stepKey: fillAfter + $cli2 = $this->getModuleForAction("magentoCLISecret")->magentoCLISecret($this->getModuleForAction("getSecret")->getSecret("magento/some/secret") . "-some/data-" . $this->getModuleForAction("getSecret")->getSecret("magento/some/secret"), 60); // stepKey: cli2 + print($cli2); // stepKey: cli2 + } catch (\Exception $exception) { + print $exception->getMessage(); + } + + PersistedObjectHandler::getInstance()->clearSuiteObjects(); + + $this->closeSession($this->webDriver); + + print sprintf(self::$HOOK_EXECUTION_END, "after"); + } + } + + /** + * Close session method closes current session. + * If config 'close_all_sessions' is set to 'true' all sessions will be closed. + * + * return void + */ + private function closeSession(): void + { + $webDriverConfig = $this->webDriver->_getConfig(); + $this->webDriver->_closeSession(); + if (isset($webDriverConfig['close_all_sessions']) && $webDriverConfig['close_all_sessions'] === "true") { + $wdHost = sprintf( + '%s://%s:%s%s', + $webDriverConfig['protocol'], + $webDriverConfig['host'], + $webDriverConfig['port'], + $webDriverConfig['path'] + ); + $availableSessions = RemoteWebDriver::getAllSessions($wdHost); + foreach ($availableSessions as $session) { + try { + $chromeOptions = new ChromeOptions(); + $capabilities = DesiredCapabilities::chrome(); + $remoteWebDriver = RemoteWebDriver::createBySessionID($session['id'], $wdHost, + $webDriverConfig['connection_timeout'], $webDriverConfig['request_timeout'], true, $capabilities); + $remoteWebDriver->quit(); + } catch (\Exception $exception) { + print("Failed trying to quit WebDriver session. Exception message: " . $exception->getMessage() . " Test execution will continue." . PHP_EOL); + // Session already closed so nothing to do + } + } + } + } + + /** + * Return the module for an action. + * + * @param string $action + * @return Module + * @throws \Exception + */ + private function getModuleForAction($action) + { + $module = $this->moduleContainer->moduleForAction($action); + if ($module === null) { + throw new TestFrameworkException('Invalid action "' . $action . '"' . PHP_EOL); + } + return $module; + } + + /** + * Counts how many tests in group. + * + * @return integer + */ + private function getTestCount() + { + $config = $this->getGlobalConfig(); + if (empty($config['groups']) || empty($config['groups'][self::$group])) { + return $this->testCount; + } + $pathToGroupDir = TESTS_BP . DIRECTORY_SEPARATOR . array_first($config['groups'][self::$group]); + $pathToGroupCests = $pathToGroupDir . "*Cest.php"; + + $files = glob($pathToGroupCests); + if (is_array($files)) { + $qty = count($files); + print('In a group "' . self::$group . '" suite executor found ' . $qty . ' tests.' . PHP_EOL); + return $qty; + } + + return $this->testCount; + } +} \ No newline at end of file diff --git a/dev/tests/verification/Resources/ArgumentWithSameNameAsElement.txt b/dev/tests/verification/Resources/ArgumentWithSameNameAsElement.txt index 4bf9332b7..a5e24c0a1 100644 --- a/dev/tests/verification/Resources/ArgumentWithSameNameAsElement.txt +++ b/dev/tests/verification/Resources/ArgumentWithSameNameAsElement.txt @@ -18,17 +18,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ArgumentWithSameNameAsElementCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $I->createEntity("createPersonParam", "hook", "ReplacementPerson", [], []); // stepKey: createPersonParam $I->comment("Entering Action Group [beforeGroup] FunctionalActionGroup"); $I->fillField("#foo", "myData1"); // stepKey: fillField1BeforeGroup $I->fillField("#bar", "myData2"); // stepKey: fillField2BeforeGroup $I->comment("Exiting Action Group [beforeGroup] FunctionalActionGroup"); + $I->comment('[END BEFORE HOOK]'); } /** @@ -37,10 +44,15 @@ class ArgumentWithSameNameAsElementCest */ public function _after(AcceptanceTester $I) { + $I->comment('[START AFTER HOOK]'); $I->comment("Entering Action Group [afterGroup] FunctionalActionGroup"); $I->fillField("#foo", "myData1"); // stepKey: fillField1AfterGroup $I->fillField("#bar", "myData2"); // stepKey: fillField2AfterGroup $I->comment("Exiting Action Group [afterGroup] FunctionalActionGroup"); + $I->comment('[END AFTER HOOK]'); + if ($this->isSuccess) { + unlink(__FILE__); + } } /** @@ -56,7 +68,6 @@ class ArgumentWithSameNameAsElementCest * @Severity(level = SeverityLevel::CRITICAL) * @Features({"TestModule"}) * @Stories({"MQE-433"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -68,4 +79,10 @@ class ArgumentWithSameNameAsElementCest $I->seeElement("#element .John"); // stepKey: see2ActionGroup1 $I->comment("Exiting Action Group [actionGroup1] FunctionalActionGroupWithTrickyArgument"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/AssertTest.txt b/dev/tests/verification/Resources/AssertTest.txt index c0cf932ae..75c5266bd 100644 --- a/dev/tests/verification/Resources/AssertTest.txt +++ b/dev/tests/verification/Resources/AssertTest.txt @@ -17,18 +17,35 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class AssertTestCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $I->createEntity("createData1", "hook", "ReplacementPerson", [], []); // stepKey: createData1 + $I->comment('[END BEFORE HOOK]'); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } } /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -43,8 +60,9 @@ class AssertTestCest $I->comment("asserts without variable replacement"); $I->assertArrayHasKey("apple", ['orange' => 2, 'apple' => 1], "pass"); // stepKey: assertArrayHasKey $I->assertArrayNotHasKey("kiwi", ['orange' => 2, 'apple' => 1], "pass"); // stepKey: assertArrayNotHasKey - $I->assertArraySubset([1, 2], [1, 2, 3, 5], "pass"); // stepKey: assertArraySubset $I->assertContains("ab", ['item1' => 'a', 'item2' => 'ab'], "pass"); // stepKey: assertContains + $I->assertStringContainsString("apple", "apple", "pass"); // stepKey: assertStringContainsString + $I->assertStringContainsStringIgnoringCase("Banana", "banana", "pass"); // stepKey: assertStringContainsStringIgnoringCase $I->assertCount(2, ['a', 'b'], "pass"); // stepKey: assertCount $I->assertEmpty([], "pass"); // stepKey: assertEmpty $I->assertEquals($text, "Copyright © 2013-2017 Magento, Inc. All rights reserved.", "pass"); // stepKey: assertEquals1 @@ -56,17 +74,15 @@ class AssertTestCest $I->assertGreaterOrEquals(2, 5, "pass"); // stepKey: assertGreaterOrEquals $I->assertGreaterThan(2, 5, "pass"); // stepKey: assertGreaterthan $I->assertGreaterThanOrEqual(2, 5, "pass"); // stepKey: assertGreaterThanOrEqual - $I->assertInternalType("string", "xyz", "pass"); // stepKey: assertInternalType1 - $I->assertInternalType("int", 21, "pass"); // stepKey: assertInternalType2 - $I->assertInternalType("string", $text, "pass"); // stepKey: assertInternalType3 $I->assertLessOrEquals(5, 2, "pass"); // stepKey: assertLessOrEquals $I->assertLessThan(5, 2, "pass"); // stepKey: assertLessThan $I->assertLessThanOrEqual(5, 2, "pass"); // stepKey: assertLessThanOrEquals - $I->assertNotContains("bc", ['item1' => 'a', 'item2' => 'ab'], "pass"); // stepKey: assertNotContains1 - $I->assertNotContains("bc", $text, "pass"); // stepKey: assertNotContains2 + $I->assertNotContains("bc", ['item1' => 'a', 'item2' => 'ab'], "pass"); // stepKey: assertNotContains + $I->assertStringNotContainsString("apple", "banana", "pass"); // stepKey: assertStringNotContainsString + $I->assertStringNotContainsStringIgnoringCase("apple", "banana", "pass"); // stepKey: assertStringNotContainsStringIgnoringCase $I->assertNotEmpty([1, 2], "pass"); // stepKey: assertNotEmpty1 $I->assertNotEmpty($text, "pass"); // stepKey: assertNotEmpty2 - $I->assertNotEquals(2, 5, "pass", 0); // stepKey: assertNotEquals + $I->assertNotEquals(2, 5, "pass"); // stepKey: assertNotEquals $I->assertNotNull("abc", "pass"); // stepKey: assertNotNull1 $I->assertNotNull($text, "pass"); // stepKey: assertNotNull2 $I->assertNotRegExp("/foo/", "bar", "pass"); // stepKey: assertNotRegExp @@ -80,7 +96,6 @@ class AssertTestCest $I->comment("asserts backward compatible"); $I->assertArrayHasKey("apple", ['orange' => 2, 'apple' => 1], "pass"); // stepKey: assertArrayHasKeyBackwardCompatible $I->assertArrayNotHasKey("kiwi", ['orange' => 2, 'apple' => 1], "pass"); // stepKey: assertArrayNotHasKeyBackwardCompatible - $I->assertArraySubset([1, 2], [1, 2, 3, 5], "pass"); // stepKey: assertArraySubsetBackwardCompatible $I->assertContains("ab", ['item1' => 'a', 'item2' => 'ab'], "pass"); // stepKey: assertContainsBackwardCompatible $I->assertCount(2, ['a', 'b'], "pass"); // stepKey: assertCountBackwardCompatible $I->assertEmpty([], "pass"); // stepKey: assertEmptyBackwardCompatible @@ -92,9 +107,6 @@ class AssertTestCest $I->assertGreaterOrEquals(2, 5, "pass"); // stepKey: assertGreaterOrEqualsBackwardCompatible $I->assertGreaterThan(2, 5, "pass"); // stepKey: assertGreaterThanBackwardCompatible $I->assertGreaterThanOrEqual(2, 5, "pass"); // stepKey: assertGreaterThanOrEqualBackwardCompatible - $I->assertInternalType("string", "xyz", "pass"); // stepKey: assertInternalType1BackwardCompatible - $I->assertInternalType("int", 21, "pass"); // stepKey: assertInternalType2BackwardCompatible - $I->assertInternalType("string", $text, "pass"); // stepKey: assertInternalType3BackwardCompatible $I->assertLessOrEquals(5, 2, "pass"); // stepKey: assertLessOrEqualBackwardCompatibles $I->assertLessThan(5, 2, "pass"); // stepKey: assertLessThanBackwardCompatible $I->assertLessThanOrEqual(5, 2, "pass"); // stepKey: assertLessThanOrEqualBackwardCompatible @@ -102,7 +114,7 @@ class AssertTestCest $I->assertNotContains("bc", $text, "pass"); // stepKey: assertNotContains2BackwardCompatible $I->assertNotEmpty([1, 2], "pass"); // stepKey: assertNotEmpty1BackwardCompatible $I->assertNotEmpty($text, "pass"); // stepKey: assertNotEmpty2BackwardCompatible - $I->assertNotEquals(2, 5, "pass", 0); // stepKey: assertNotEqualsBackwardCompatible + $I->assertNotEquals(2, 5, "pass"); // stepKey: assertNotEqualsBackwardCompatible $I->assertNotNull("abc", "pass"); // stepKey: assertNotNull1BackwardCompatible $I->assertNotNull($text, "pass"); // stepKey: assertNotNull2BackwardCompatible $I->assertNotRegExp("/foo/", "bar", "pass"); // stepKey: assertNotRegExpBackwardCompatible @@ -126,8 +138,6 @@ class AssertTestCest $I->assertEquals($I->retrieveEntityField('createData1', 'lastname', 'test'), $I->retrieveEntityField('createData1', 'lastname', 'test'), "pass"); // stepKey: assert5 $I->comment("array type that use created data"); $I->comment("array type that use created data"); - $I->assertArraySubset([$I->retrieveEntityField('createData1', 'lastname', 'test'), $I->retrieveEntityField('createData1', 'firstname', 'test')], [$I->retrieveEntityField('createData1', 'lastname', 'test'), $I->retrieveEntityField('createData1', 'firstname', 'test'), "1"], "pass"); // stepKey: assert9 - $I->assertArraySubset([$I->retrieveEntityField('createData2', 'firstname', 'test'), $I->retrieveEntityField('createData2', 'lastname', 'test')], [$I->retrieveEntityField('createData2', 'firstname', 'test'), $I->retrieveEntityField('createData2', 'lastname', 'test'), "1"], "pass"); // stepKey: assert10 $I->assertArrayHasKey("lastname", ['lastname' => $I->retrieveEntityField('createData1', 'lastname', 'test'), 'firstname' => $I->retrieveEntityField('createData1', 'firstname', 'test')], "pass"); // stepKey: assert3 $I->assertArrayHasKey("lastname", ['lastname' => $I->retrieveEntityField('createData2', 'lastname', 'test'), 'firstname' => $I->retrieveEntityField('createData2', 'firstname', 'test')], "pass"); // stepKey: assert4 $I->comment("this section can only be generated and cannot run"); @@ -152,5 +162,25 @@ class AssertTestCest $I->assertElementContainsAttribute("#username", "value", $I->retrieveEntityField('createData1', 'firstname', 'test')); // stepKey: assertElementContainsAttribute8 $I->comment("assert entity resolution"); $I->assertEquals("John", "Doe", "pass"); // stepKey: assertEqualsEntity + $I->assertEqualsWithDelta(10.0000, 10.0000, 1, "pass"); // stepKey: a1 + $I->assertNotEqualsWithDelta(10.0000, 12.0000, 1, "pass"); // stepKey: a2 + $I->assertEqualsCanonicalizing(["4", "2", "1", "3"], ["1", "2", "3", "4"], "pass"); // stepKey: a3 + $I->assertNotEqualsCanonicalizing(["5", "8", "7", "9"], ["1", "2", "3", "4"], "pass"); // stepKey: a4 + $I->assertEqualsIgnoringCase("Cat", "cat", "pass"); // stepKey: a5 + $I->assertNotEqualsIgnoringCase("Cat", "Dog", "pass"); // stepKey: a6 + $I->comment("assertions.md examples"); + $I->assertElementContainsAttribute(".admin__menu-overlay", "style", "color: #333;"); // stepKey: assertElementContainsAttribute + $I->assertStringContainsString("Buy 5 for $5.00 each and save 50%", $DropDownTierPriceTextProduct1); // stepKey: assertDropDownTierPriceTextProduct1 + $I->assertEmpty("$grabSearchButtonAttribute"); // stepKey: assertSearchButtonEnabled + $I->assertGreaterThanOrEqual($getOrderStatusFirstRow, $getOrderStatusSecondRow); // stepKey: checkStatusSortOrderAsc + $I->assertNotEquals($grabTotalBefore, $grabTotalAfter); // stepKey: assertNotEqualsExample + $I->assertNotRegExp('/placeholder\/thumbnail\.jpg/', $getSimpleProductThumbnail); // stepKey: simpleThumbnailIsNotDefault + $I->assertRegExp("#var\s+adminAnalyticsMetadata\s+=\s+{\s+(\"[\w_]+\":\s+\"[^\"]*?\",\s+)*?(\"[\w_]+\":\s+\"[^\"]*?\"\s+)};#s", $pageSource, "adminAnalyticsMetadata object is invalid"); // stepKey: validateadminAnalyticsMetadata + } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; } } diff --git a/dev/tests/verification/Resources/BasicActionGroupTest.txt b/dev/tests/verification/Resources/BasicActionGroupTest.txt index 95c31b813..5412afa70 100644 --- a/dev/tests/verification/Resources/BasicActionGroupTest.txt +++ b/dev/tests/verification/Resources/BasicActionGroupTest.txt @@ -18,24 +18,41 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class BasicActionGroupTestCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $I->createEntity("createPersonParam", "hook", "ReplacementPerson", [], []); // stepKey: createPersonParam $I->comment("Entering Action Group [beforeGroup] FunctionalActionGroup"); $I->fillField("#foo", "myData1"); // stepKey: fillField1BeforeGroup $I->fillField("#bar", "myData2"); // stepKey: fillField2BeforeGroup $I->comment("Exiting Action Group [beforeGroup] FunctionalActionGroup"); + $I->comment('[END BEFORE HOOK]'); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } } /** * @Severity(level = SeverityLevel::CRITICAL) * @Features({"TestModule"}) * @Stories({"MQE-433"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -49,4 +66,10 @@ class BasicActionGroupTestCest $I->comment("Exiting Action Group [actionGroup1] FunctionalActionGroup"); $I->click("loginButton"); // stepKey: step6 } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/BasicFunctionalTest.txt b/dev/tests/verification/Resources/BasicFunctionalTest.txt index 7f7e25c60..d4631b706 100644 --- a/dev/tests/verification/Resources/BasicFunctionalTest.txt +++ b/dev/tests/verification/Resources/BasicFunctionalTest.txt @@ -19,13 +19,20 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class BasicFunctionalTestCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $I->amOnPage("/beforeUrl"); // stepKey: beforeAmOnPageKey + $I->comment('[END BEFORE HOOK]'); } /** @@ -34,7 +41,12 @@ class BasicFunctionalTestCest */ public function _after(AcceptanceTester $I) { + $I->comment('[START AFTER HOOK]'); $I->amOnPage("/afterUrl"); // stepKey: afterAmOnPageKey + $I->comment('[END AFTER HOOK]'); + if ($this->isSuccess) { + unlink(__FILE__); + } } /** @@ -50,7 +62,6 @@ class BasicFunctionalTestCest * @Severity(level = SeverityLevel::CRITICAL) * @Features({"TestModule"}) * @Stories({"MQE-305"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -112,8 +123,11 @@ class BasicFunctionalTestCest $date->setTimezone(new \DateTimeZone("UTC")); $generateDateKey2 = $date->format("H:i:s"); + $getOtp = $I->getOTP(); // stepKey: getOtp + $getOtpWithInput = $I->getOTP("someInput"); // stepKey: getOtpWithInput $grabAttributeFromKey1 = $I->grabAttributeFrom(".functionalTestSelector", "someInput"); // stepKey: grabAttributeFromKey1 $grabCookieKey1 = $I->grabCookie("grabCookieInput", ['domain' => 'www.google.com']); // stepKey: grabCookieKey1 + $grabCookieAttributesKey1 = $I->grabCookieAttributes("grabCookieAttributesInput", ['domain' => 'www.google.com']); // stepKey: grabCookieAttributesKey1 $grabFromCurrentUrlKey1 = $I->grabFromCurrentUrl("/grabCurrentUrl"); // stepKey: grabFromCurrentUrlKey1 $grabMultipleKey1 = $I->grabMultiple(".functionalTestSelector"); // stepKey: grabMultipleKey1 $grabTextFromKey1 = $I->grabTextFrom(".functionalTestSelector"); // stepKey: grabTextFromKey1 @@ -138,7 +152,7 @@ class BasicFunctionalTestCest $I->moveForward(); // stepKey: moveForwardKey1 $I->moveMouseOver(".functionalTestSelector"); // stepKey: moveMouseOverKey1 $I->openNewTab(); // stepKey: openNewTabKey1 - $I->pauseExecution(); // stepKey: pauseExecutionKey1 + $I->pause(true); // stepKey: pauseKey1 $I->pressKey("#page", "a"); // stepKey: pressKey1 $I->pressKey("#page", ['ctrl', 'a'],'new'); // stepKey: pressKey2 $I->pressKey("#page", ['shift', '111'],'1','x'); // stepKey: pressKey3 @@ -183,4 +197,10 @@ class BasicFunctionalTestCest $I->waitForJS("someJsFunction", 30); // stepKey: waitForJSKey1 $I->waitForText("someInput", 30, ".functionalTestSelector"); // stepKey: waitForText1 } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/BasicMergeTest.txt b/dev/tests/verification/Resources/BasicMergeTest.txt index 8842d3d99..df625aa57 100644 --- a/dev/tests/verification/Resources/BasicMergeTest.txt +++ b/dev/tests/verification/Resources/BasicMergeTest.txt @@ -20,14 +20,21 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class BasicMergeTestCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $I->amOnPage("/beforeUrl"); // stepKey: before1 $I->see("#before2"); // stepKey: before2 + $I->comment('[END BEFORE HOOK]'); } /** @@ -36,7 +43,12 @@ class BasicMergeTestCest */ public function _after(AcceptanceTester $I) { + $I->comment('[START AFTER HOOK]'); $I->amOnPage("/afterUrl1"); // stepKey: after1 + $I->comment('[END AFTER HOOK]'); + if ($this->isSuccess) { + unlink(__FILE__); + } } /** @@ -52,7 +64,6 @@ class BasicMergeTestCest * @Severity(level = SeverityLevel::CRITICAL) * @Features({"TestModule"}) * @Stories({"MQE-433"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -75,4 +86,10 @@ class BasicMergeTestCest $I->comment("Exiting Action Group [step8Merge] FunctionalActionGroupWithData"); $I->click("#step10MergedInResult"); // stepKey: step10 } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/CharacterReplacementTest.txt b/dev/tests/verification/Resources/CharacterReplacementTest.txt index 24d8e2ab1..bf10c94fc 100644 --- a/dev/tests/verification/Resources/CharacterReplacementTest.txt +++ b/dev/tests/verification/Resources/CharacterReplacementTest.txt @@ -17,9 +17,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class CharacterReplacementTestCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -35,4 +50,10 @@ class CharacterReplacementTestCest $I->click("#`~!@#$%^&*()-_=+{}[]|\;:\".,></?() .`~!@#$%^&*()-_=+{}[]|\;:\".,></?()"); // stepKey: allChars5 $I->click("#words, and, commas, and, spaces .words, and, commas, and, spaces"); // stepKey: allChars6 } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ChildExtendedTestAddHooks.txt b/dev/tests/verification/Resources/ChildExtendedTestAddHooks.txt index 72e924954..01fa652d8 100644 --- a/dev/tests/verification/Resources/ChildExtendedTestAddHooks.txt +++ b/dev/tests/verification/Resources/ChildExtendedTestAddHooks.txt @@ -19,13 +19,20 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ChildExtendedTestAddHooksCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $I->amOnPage("/beforeUrl"); // stepKey: beforeAmOnPageKey + $I->comment('[END BEFORE HOOK]'); } /** @@ -34,7 +41,12 @@ class ChildExtendedTestAddHooksCest */ public function _after(AcceptanceTester $I) { + $I->comment('[START AFTER HOOK]'); $I->amOnPage("/afterUrl"); // stepKey: afterAmOnPageKey + $I->comment('[END AFTER HOOK]'); + if ($this->isSuccess) { + unlink(__FILE__); + } } /** @@ -50,7 +62,6 @@ class ChildExtendedTestAddHooksCest * @Severity(level = SeverityLevel::MINOR) * @Features({"TestModule"}) * @Stories({"Parent"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -58,4 +69,10 @@ class ChildExtendedTestAddHooksCest public function ChildExtendedTestAddHooks(AcceptanceTester $I) { } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ChildExtendedTestMerging.txt b/dev/tests/verification/Resources/ChildExtendedTestMerging.txt index 7900ca9a0..8b78c5a5e 100644 --- a/dev/tests/verification/Resources/ChildExtendedTestMerging.txt +++ b/dev/tests/verification/Resources/ChildExtendedTestMerging.txt @@ -19,15 +19,22 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ChildExtendedTestMergingCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $I->amOnPage("/firstUrl"); // stepKey: firstBeforeAmOnPageKey $I->amOnPage("/beforeUrl"); // stepKey: beforeAmOnPageKey $I->amOnPage("/lastUrl"); // stepKey: lastBefore + $I->comment('[END BEFORE HOOK]'); } /** @@ -36,7 +43,12 @@ class ChildExtendedTestMergingCest */ public function _after(AcceptanceTester $I) { + $I->comment('[START AFTER HOOK]'); $I->amOnPage("/afterUrl"); // stepKey: afterAmOnPageKey + $I->comment('[END AFTER HOOK]'); + if ($this->isSuccess) { + unlink(__FILE__); + } } /** @@ -52,7 +64,6 @@ class ChildExtendedTestMergingCest * @Severity(level = SeverityLevel::TRIVIAL) * @Features({"TestModule"}) * @Stories({"Child"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -64,4 +75,10 @@ class ChildExtendedTestMergingCest $I->comment("After Comment"); $I->comment("Last Comment"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ChildExtendedTestNoParent.txt b/dev/tests/verification/Resources/ChildExtendedTestNoParent.txt index be775aba9..1bd027399 100644 --- a/dev/tests/verification/Resources/ChildExtendedTestNoParent.txt +++ b/dev/tests/verification/Resources/ChildExtendedTestNoParent.txt @@ -20,17 +20,33 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ChildExtendedTestNoParentCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Severity(level = SeverityLevel::CRITICAL) * @Features({"TestModule"}) * @Stories({"Child"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception */ public function ChildExtendedTestNoParent(AcceptanceTester $I, \Codeception\Scenario $scenario) { + unlink(__FILE__); $scenario->skip("This test is skipped due to the following issues:No issues have been specified."); } } diff --git a/dev/tests/verification/Resources/ChildExtendedTestRemoveAction.txt b/dev/tests/verification/Resources/ChildExtendedTestRemoveAction.txt index d3017e010..859bc8226 100644 --- a/dev/tests/verification/Resources/ChildExtendedTestRemoveAction.txt +++ b/dev/tests/verification/Resources/ChildExtendedTestRemoveAction.txt @@ -19,13 +19,20 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ChildExtendedTestRemoveActionCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $I->amOnPage("/beforeUrl"); // stepKey: beforeAmOnPageKey + $I->comment('[END BEFORE HOOK]'); } /** @@ -34,7 +41,12 @@ class ChildExtendedTestRemoveActionCest */ public function _after(AcceptanceTester $I) { + $I->comment('[START AFTER HOOK]'); $I->amOnPage("/afterUrl"); // stepKey: afterAmOnPageKey + $I->comment('[END AFTER HOOK]'); + if ($this->isSuccess) { + unlink(__FILE__); + } } /** @@ -50,7 +62,6 @@ class ChildExtendedTestRemoveActionCest * @Severity(level = SeverityLevel::CRITICAL) * @Features({"TestModule"}) * @Stories({"Child"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -58,4 +69,10 @@ class ChildExtendedTestRemoveActionCest public function ChildExtendedTestRemoveAction(AcceptanceTester $I) { } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ChildExtendedTestRemoveHookAction.txt b/dev/tests/verification/Resources/ChildExtendedTestRemoveHookAction.txt index 0988e5d71..d48fb9743 100644 --- a/dev/tests/verification/Resources/ChildExtendedTestRemoveHookAction.txt +++ b/dev/tests/verification/Resources/ChildExtendedTestRemoveHookAction.txt @@ -19,6 +19,11 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ChildExtendedTestRemoveHookActionCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception @@ -33,7 +38,12 @@ class ChildExtendedTestRemoveHookActionCest */ public function _after(AcceptanceTester $I) { + $I->comment('[START AFTER HOOK]'); $I->amOnPage("/afterUrl"); // stepKey: afterAmOnPageKey + $I->comment('[END AFTER HOOK]'); + if ($this->isSuccess) { + unlink(__FILE__); + } } /** @@ -49,7 +59,6 @@ class ChildExtendedTestRemoveHookActionCest * @Severity(level = SeverityLevel::CRITICAL) * @Features({"TestModule"}) * @Stories({"Child"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -58,4 +67,10 @@ class ChildExtendedTestRemoveHookActionCest { $I->comment("Parent Comment"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ChildExtendedTestReplace.txt b/dev/tests/verification/Resources/ChildExtendedTestReplace.txt index 9d0a65a3b..acb8eced9 100644 --- a/dev/tests/verification/Resources/ChildExtendedTestReplace.txt +++ b/dev/tests/verification/Resources/ChildExtendedTestReplace.txt @@ -19,13 +19,20 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ChildExtendedTestReplaceCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $I->amOnPage("/beforeUrl"); // stepKey: beforeAmOnPageKey + $I->comment('[END BEFORE HOOK]'); } /** @@ -34,7 +41,12 @@ class ChildExtendedTestReplaceCest */ public function _after(AcceptanceTester $I) { + $I->comment('[START AFTER HOOK]'); $I->amOnPage("/afterUrl"); // stepKey: afterAmOnPageKey + $I->comment('[END AFTER HOOK]'); + if ($this->isSuccess) { + unlink(__FILE__); + } } /** @@ -50,7 +62,6 @@ class ChildExtendedTestReplaceCest * @Severity(level = SeverityLevel::TRIVIAL) * @Features({"TestModule"}) * @Stories({"Child"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -59,4 +70,10 @@ class ChildExtendedTestReplaceCest { $I->comment("Different Input"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ChildExtendedTestReplaceHook.txt b/dev/tests/verification/Resources/ChildExtendedTestReplaceHook.txt index c40b46fa0..db6912e95 100644 --- a/dev/tests/verification/Resources/ChildExtendedTestReplaceHook.txt +++ b/dev/tests/verification/Resources/ChildExtendedTestReplaceHook.txt @@ -19,13 +19,20 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ChildExtendedTestReplaceHookCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $I->amOnPage("/slightlyDifferentBeforeUrl"); // stepKey: beforeAmOnPageKey + $I->comment('[END BEFORE HOOK]'); } /** @@ -34,7 +41,12 @@ class ChildExtendedTestReplaceHookCest */ public function _after(AcceptanceTester $I) { + $I->comment('[START AFTER HOOK]'); $I->amOnPage("/afterUrl"); // stepKey: afterAmOnPageKey + $I->comment('[END AFTER HOOK]'); + if ($this->isSuccess) { + unlink(__FILE__); + } } /** @@ -50,7 +62,6 @@ class ChildExtendedTestReplaceHookCest * @Severity(level = SeverityLevel::TRIVIAL) * @Features({"TestModule"}) * @Stories({"Child"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -59,4 +70,10 @@ class ChildExtendedTestReplaceHookCest { $I->comment("Parent Comment"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/DataActionsTest.txt b/dev/tests/verification/Resources/DataActionsTest.txt index 84a94174a..982c680b2 100644 --- a/dev/tests/verification/Resources/DataActionsTest.txt +++ b/dev/tests/verification/Resources/DataActionsTest.txt @@ -17,30 +17,53 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class DataActionsTestCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { - $I->createEntity("createdInBefore", "hook", "entity", [], []); // stepKey: createdInBefore + $I->comment('[START BEFORE HOOK]'); $I->updateEntity("createdInBefore", "hook", "entity",[]); // stepKey: updateInBefore $I->deleteEntity("createdInBefore", "hook"); // stepKey: deleteInBefore + $I->comment('[END BEFORE HOOK]'); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } } /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception */ public function DataActionsTest(AcceptanceTester $I) { - $I->createEntity("createdInTest", "test", "entity", [], []); // stepKey: createdInTest + $I->waitForElementClickable(".functionalTestSelector"); // stepKey: waitForElementClickable $I->updateEntity("createdInTest", "test", "entity",[]); // stepKey: updateInTest $I->deleteEntity("createdInTest", "test"); // stepKey: deleteInTest $I->updateEntity("createdInBefore", "test", "entity",[]); // stepKey: updatedDataOutOfScope $I->deleteEntity("createdInBefore", "test"); // stepKey: deleteDataOutOfScope + $I->rapidClick("#functionalTestSelector", "50"); // stepKey: rapidClickTest + } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; } } diff --git a/dev/tests/verification/Resources/DataReplacementTest.txt b/dev/tests/verification/Resources/DataReplacementTest.txt index b75d93f0d..940504c63 100644 --- a/dev/tests/verification/Resources/DataReplacementTest.txt +++ b/dev/tests/verification/Resources/DataReplacementTest.txt @@ -17,9 +17,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class DataReplacementTestCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -89,4 +104,10 @@ class DataReplacementTestCest $I->dontSeeInSource("#element"); // stepKey: htmlReplace35 $I->dontSeeInSource("StringBefore #element StringAfter"); // stepKey: htmlReplace36 } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/DeprecatedEntitiesTest.txt b/dev/tests/verification/Resources/DeprecatedEntitiesTest.txt index 489cb66ea..cc205c438 100644 --- a/dev/tests/verification/Resources/DeprecatedEntitiesTest.txt +++ b/dev/tests/verification/Resources/DeprecatedEntitiesTest.txt @@ -17,9 +17,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class DeprecatedEntitiesTestCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -31,4 +46,10 @@ class DeprecatedEntitiesTestCest $I->comment("Exiting Action Group [deprecatedActionGroup] DeprecatedActionGroup"); $I->amOnPage("/test.html"); // stepKey: amOnPage } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/DeprecatedTest.txt b/dev/tests/verification/Resources/DeprecatedTest.txt index 845d13912..7e34131d5 100644 --- a/dev/tests/verification/Resources/DeprecatedTest.txt +++ b/dev/tests/verification/Resources/DeprecatedTest.txt @@ -17,9 +17,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class DeprecatedTestCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -31,4 +46,10 @@ class DeprecatedTestCest $I->comment("Exiting Action Group [deprecatedActionGroup] DeprecatedActionGroup"); $I->amOnPage("/test.html"); // stepKey: amOnPage } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ExecuteInSeleniumTest.txt b/dev/tests/verification/Resources/ExecuteInSeleniumTest.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/dev/tests/verification/Resources/ExecuteJsEscapingTest.txt b/dev/tests/verification/Resources/ExecuteJsEscapingTest.txt index ad3696e00..8718d188b 100644 --- a/dev/tests/verification/Resources/ExecuteJsEscapingTest.txt +++ b/dev/tests/verification/Resources/ExecuteJsEscapingTest.txt @@ -17,9 +17,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ExecuteJsEscapingTestCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -32,4 +47,10 @@ class ExecuteJsEscapingTestCest $hookPersistedDataNotEscaped = $I->executeJS("return " . $I->retrieveEntityField('persisted', 'data', 'test')); // stepKey: hookPersistedDataNotEscaped $addNewAttributeForRule = $I->executeJS("document.querySelector('entity option[value=" . $I->retrieveEntityField('productAttribute', 'attribute_code', 'test') . "]').setAttribute('selected', 'selected')"); // stepKey: addNewAttributeForRule } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ExtendParentDataTest.txt b/dev/tests/verification/Resources/ExtendParentDataTest.txt index 70cb6b70b..93ea3b754 100644 --- a/dev/tests/verification/Resources/ExtendParentDataTest.txt +++ b/dev/tests/verification/Resources/ExtendParentDataTest.txt @@ -17,9 +17,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ExtendParentDataTestCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -33,4 +48,10 @@ class ExtendParentDataTestCest $I->searchAndMultiSelectOption("#selector", [msq("extendParentData") . "prename"]); // stepKey: originalPre $I->searchAndMultiSelectOption("#selector", ["postnameExtend" . msq("extendParentData")]); // stepKey: secondUniquePre } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ExtendedActionGroup.txt b/dev/tests/verification/Resources/ExtendedActionGroup.txt index 9ed588df4..2d2373c0d 100644 --- a/dev/tests/verification/Resources/ExtendedActionGroup.txt +++ b/dev/tests/verification/Resources/ExtendedActionGroup.txt @@ -17,9 +17,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ExtendedActionGroupCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -34,4 +49,10 @@ class ExtendedActionGroupCest $I->assertCount(8000, $grabProductsActionGroup); // stepKey: assertSecondCountActionGroup $I->comment("Exiting Action Group [actionGroup] extendTestActionGroup"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ExtendedActionGroupReturningValueTest.txt b/dev/tests/verification/Resources/ExtendedActionGroupReturningValueTest.txt new file mode 100644 index 000000000..4c19fea5e --- /dev/null +++ b/dev/tests/verification/Resources/ExtendedActionGroupReturningValueTest.txt @@ -0,0 +1,61 @@ +<?php +namespace Magento\AcceptanceTest\_default\Backend; + +use Magento\FunctionalTestingFramework\AcceptanceTester; +use \Codeception\Util\Locator; +use Yandex\Allure\Adapter\Annotation\Features; +use Yandex\Allure\Adapter\Annotation\Stories; +use Yandex\Allure\Adapter\Annotation\Title; +use Yandex\Allure\Adapter\Annotation\Description; +use Yandex\Allure\Adapter\Annotation\Parameter; +use Yandex\Allure\Adapter\Annotation\Severity; +use Yandex\Allure\Adapter\Model\SeverityLevel; +use Yandex\Allure\Adapter\Annotation\TestCaseId; + +/** + * @Title("[NO TESTCASEID]: Extended ActionGroup Returning Value Test") + * @Description("<h3>Test files</h3>verification/TestModule/Test/ActionGroupTest/ExtendedActionGroupReturningValueTest.xml<br>") + */ +class ExtendedActionGroupReturningValueTestCest +{ + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + + /** + * @Severity(level = SeverityLevel::CRITICAL) + * @Features({"TestModule"}) + * @param AcceptanceTester $I + * @return void + * @throws \Exception + */ + public function ExtendedActionGroupReturningValueTest(AcceptanceTester $I) + { + $I->comment("Entering Action Group [actionGroupReturningValue] ActionGroupReturningValueActionGroup"); + $grabProducts1ActionGroupReturningValue = $I->grabMultiple("selector"); // stepKey: grabProducts1ActionGroupReturningValue + $I->assertCount(99, $grabProducts1ActionGroupReturningValue); // stepKey: assertCountActionGroupReturningValue + $actionGroupReturningValue = $I->return($grabProducts1ActionGroupReturningValue); // stepKey: returnProducts1ActionGroupReturningValue + $I->comment("Exiting Action Group [actionGroupReturningValue] ActionGroupReturningValueActionGroup"); + $I->comment("Entering Action Group [actionGroupWithStringUsage1] actionGroupWithStringUsage"); + $I->see($actionGroupReturningValue, "#element .{$actionGroupReturningValue}"); // stepKey: see1ActionGroupWithStringUsage1 + $I->comment("Exiting Action Group [actionGroupWithStringUsage1] actionGroupWithStringUsage"); + } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } +} diff --git a/dev/tests/verification/Resources/ExtendedChildActionGroupReturningValueTest.txt b/dev/tests/verification/Resources/ExtendedChildActionGroupReturningValueTest.txt new file mode 100644 index 000000000..a25ba031d --- /dev/null +++ b/dev/tests/verification/Resources/ExtendedChildActionGroupReturningValueTest.txt @@ -0,0 +1,64 @@ +<?php +namespace Magento\AcceptanceTest\_default\Backend; + +use Magento\FunctionalTestingFramework\AcceptanceTester; +use \Codeception\Util\Locator; +use Yandex\Allure\Adapter\Annotation\Features; +use Yandex\Allure\Adapter\Annotation\Stories; +use Yandex\Allure\Adapter\Annotation\Title; +use Yandex\Allure\Adapter\Annotation\Description; +use Yandex\Allure\Adapter\Annotation\Parameter; +use Yandex\Allure\Adapter\Annotation\Severity; +use Yandex\Allure\Adapter\Model\SeverityLevel; +use Yandex\Allure\Adapter\Annotation\TestCaseId; + +/** + * @Title("[NO TESTCASEID]: Extended Child ActionGroup Returning Value Test") + * @Description("<h3>Test files</h3>verification/TestModule/Test/ActionGroupTest/ExtendedChildActionGroupReturningValueTest.xml<br>") + */ +class ExtendedChildActionGroupReturningValueTestCest +{ + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + + /** + * @Severity(level = SeverityLevel::CRITICAL) + * @Features({"TestModule"}) + * @param AcceptanceTester $I + * @return void + * @throws \Exception + */ + public function ExtendedChildActionGroupReturningValueTest(AcceptanceTester $I) + { + $I->comment("Entering Action Group [extendedActionGroupReturningValue] ExtendedActionGroupReturningValueActionGroup"); + $grabProducts1ExtendedActionGroupReturningValue = $I->grabMultiple("selector"); // stepKey: grabProducts1ExtendedActionGroupReturningValue + $I->assertCount(99, $grabProducts1ExtendedActionGroupReturningValue); // stepKey: assertCountExtendedActionGroupReturningValue + $extendedActionGroupReturningValue = $I->return($grabProducts1ExtendedActionGroupReturningValue); // stepKey: returnProducts1ExtendedActionGroupReturningValue + $grabProducts2ExtendedActionGroupReturningValue = $I->grabMultiple("otherSelector"); // stepKey: grabProducts2ExtendedActionGroupReturningValue + $I->assertCount(8000, $grabProducts2ExtendedActionGroupReturningValue); // stepKey: assertSecondCountExtendedActionGroupReturningValue + $extendedActionGroupReturningValue = $I->return($grabProducts2ExtendedActionGroupReturningValue); // stepKey: returnProducts2ExtendedActionGroupReturningValue + $I->comment("Exiting Action Group [extendedActionGroupReturningValue] ExtendedActionGroupReturningValueActionGroup"); + $I->comment("Entering Action Group [actionGroupWithStringUsage1] actionGroupWithStringUsage"); + $I->see($extendedActionGroupReturningValue, "#element .{$extendedActionGroupReturningValue}"); // stepKey: see1ActionGroupWithStringUsage1 + $I->comment("Exiting Action Group [actionGroupWithStringUsage1] actionGroupWithStringUsage"); + } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } +} diff --git a/dev/tests/verification/Resources/ExtendedChildTestInSuiteCest.txt b/dev/tests/verification/Resources/ExtendedChildTestInSuiteCest.txt index d1f74abe1..c46076353 100644 --- a/dev/tests/verification/Resources/ExtendedChildTestInSuiteCest.txt +++ b/dev/tests/verification/Resources/ExtendedChildTestInSuiteCest.txt @@ -19,13 +19,20 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ExtendedChildTestInSuiteCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $I->amOnPage("/beforeUrl"); // stepKey: beforeAmOnPageKey + $I->comment('[END BEFORE HOOK]'); } /** @@ -34,7 +41,12 @@ class ExtendedChildTestInSuiteCest */ public function _after(AcceptanceTester $I) { + $I->comment('[START AFTER HOOK]'); $I->amOnPage("/afterUrl"); // stepKey: afterAmOnPageKey + $I->comment('[END AFTER HOOK]'); + if ($this->isSuccess) { + unlink(__FILE__); + } } /** @@ -50,7 +62,6 @@ class ExtendedChildTestInSuiteCest * @Severity(level = SeverityLevel::TRIVIAL) * @Features({"TestModule"}) * @Stories({"ExtendedChildTestInSuite"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -59,4 +70,10 @@ class ExtendedChildTestInSuiteCest { $I->comment("Different Input"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ExtendedChildTestNotInSuite.txt b/dev/tests/verification/Resources/ExtendedChildTestNotInSuite.txt index d9f8a72e6..38ba62faf 100644 --- a/dev/tests/verification/Resources/ExtendedChildTestNotInSuite.txt +++ b/dev/tests/verification/Resources/ExtendedChildTestNotInSuite.txt @@ -18,13 +18,20 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ExtendedChildTestNotInSuiteCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $I->amOnPage("/beforeUrl"); // stepKey: beforeAmOnPageKey + $I->comment('[END BEFORE HOOK]'); } /** @@ -33,7 +40,12 @@ class ExtendedChildTestNotInSuiteCest */ public function _after(AcceptanceTester $I) { + $I->comment('[START AFTER HOOK]'); $I->amOnPage("/afterUrl"); // stepKey: afterAmOnPageKey + $I->comment('[END AFTER HOOK]'); + if ($this->isSuccess) { + unlink(__FILE__); + } } /** @@ -49,7 +61,6 @@ class ExtendedChildTestNotInSuiteCest * @Severity(level = SeverityLevel::TRIVIAL) * @Features({"TestModule"}) * @Stories({"ExtendedChildTestNotInSuite"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -58,4 +69,10 @@ class ExtendedChildTestNotInSuiteCest { $I->comment("Different Input"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ExtendedParameterArrayTest.txt b/dev/tests/verification/Resources/ExtendedParameterArrayTest.txt index d09e9a0b6..21d711c28 100644 --- a/dev/tests/verification/Resources/ExtendedParameterArrayTest.txt +++ b/dev/tests/verification/Resources/ExtendedParameterArrayTest.txt @@ -16,9 +16,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ExtendParentDataTestCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -32,4 +47,10 @@ class ExtendParentDataTestCest $I->searchAndMultiSelectOption("#selector", [msq("extendParentData") . "prename"]); $I->searchAndMultiSelectOption("#selector", ["postnameExtend" . msq("extendParentData")]); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ExtendedRemoveActionGroup.txt b/dev/tests/verification/Resources/ExtendedRemoveActionGroup.txt index b593a9231..809fd2874 100644 --- a/dev/tests/verification/Resources/ExtendedRemoveActionGroup.txt +++ b/dev/tests/verification/Resources/ExtendedRemoveActionGroup.txt @@ -17,9 +17,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ExtendedRemoveActionGroupCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -29,4 +44,10 @@ class ExtendedRemoveActionGroupCest $I->comment("Entering Action Group [actionGroup] extendRemoveTestActionGroup"); $I->comment("Exiting Action Group [actionGroup] extendRemoveTestActionGroup"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ExtendingSkippedTest.txt b/dev/tests/verification/Resources/ExtendingSkippedTest.txt index 553131988..522ba7620 100644 --- a/dev/tests/verification/Resources/ExtendingSkippedTest.txt +++ b/dev/tests/verification/Resources/ExtendingSkippedTest.txt @@ -20,44 +20,21 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; class ExtendingSkippedTestCest { /** - * @param AcceptanceTester $I - * @throws \Exception - */ - public function _before(AcceptanceTester $I) - { - $I->amOnPage("/beforeUrl"); // stepKey: beforeAmOnPageKey - } - - /** - * @param AcceptanceTester $I - * @throws \Exception - */ - public function _after(AcceptanceTester $I) - { - $I->amOnPage("/afterUrl"); // stepKey: afterAmOnPageKey - } - - /** - * @param AcceptanceTester $I - * @throws \Exception - */ - public function _failed(AcceptanceTester $I) - { - $I->saveScreenshot(); // stepKey: saveScreenshot - } + * @var bool + */ + private $isSuccess = false; /** * @Severity(level = SeverityLevel::CRITICAL) * @Features({"TestModule"}) * @Stories({"Child"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception */ - public function ExtendingSkippedTest(AcceptanceTester $I) + public function ExtendingSkippedTest(AcceptanceTester $I, \Codeception\Scenario $scenario) { - $I->comment("text"); - $I->comment("child"); + unlink(__FILE__); + $scenario->skip("This test is skipped due to the following issues:\nParentTestIsSkipped"); } } diff --git a/dev/tests/verification/Resources/GroupSkipGenerationTest.txt b/dev/tests/verification/Resources/GroupSkipGenerationTest.txt new file mode 100644 index 000000000..034307e37 --- /dev/null +++ b/dev/tests/verification/Resources/GroupSkipGenerationTest.txt @@ -0,0 +1,55 @@ +<?php +namespace Magento\AcceptanceTest\_default\Backend; + +use Magento\FunctionalTestingFramework\AcceptanceTester; +use \Codeception\Util\Locator; +use Yandex\Allure\Adapter\Annotation\Features; +use Yandex\Allure\Adapter\Annotation\Stories; +use Yandex\Allure\Adapter\Annotation\Title; +use Yandex\Allure\Adapter\Annotation\Description; +use Yandex\Allure\Adapter\Annotation\Parameter; +use Yandex\Allure\Adapter\Annotation\Severity; +use Yandex\Allure\Adapter\Model\SeverityLevel; +use Yandex\Allure\Adapter\Annotation\TestCaseId; + +/** + * @Title("[NO TESTCASEID]: GroupSkipGenerationTestTitle") + * @Description("GroupSkipGenerationTestDescription<h3>Test files</h3>verification/TestModule/Test/GroupSkipGenerationTest.xml<br>") + * @group skip + */ +class GroupSkipGenerationTestCest +{ + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + + /** + * @Stories({"GroupSkipGenerationTestStory"}) + * @Severity(level = SeverityLevel::MINOR) + * @Features({"TestModule"}) + * @param AcceptanceTester $I + * @return void + * @throws \Exception + */ + public function GroupSkipGenerationTest(AcceptanceTester $I) + { + } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } +} diff --git a/dev/tests/verification/Resources/HookActionsTest.txt b/dev/tests/verification/Resources/HookActionsTest.txt index 0b77325c1..6ff14a09c 100644 --- a/dev/tests/verification/Resources/HookActionsTest.txt +++ b/dev/tests/verification/Resources/HookActionsTest.txt @@ -17,15 +17,17 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class HookActionsTestCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { - $I->createEntity("sampleCreateBefore", "hook", "sampleCreatedEntity", [], []); // stepKey: sampleCreateBefore - $I->deleteEntity("sampleCreateBefore", "hook"); // stepKey: sampleDeleteBefore - $I->createEntity("sampleCreateForAfter", "hook", "sampleCreatedEntity", [], []); // stepKey: sampleCreateForAfter } /** @@ -34,8 +36,12 @@ class HookActionsTestCest */ public function _after(AcceptanceTester $I) { - $I->createEntity("sampleCreateAfter", "hook", "sampleCreatedEntity", [], []); // stepKey: sampleCreateAfter + $I->comment('[START AFTER HOOK]'); $I->deleteEntity("sampleCreateForAfter", "hook"); // stepKey: sampleDeleteAfter + $I->comment('[END AFTER HOOK]'); + if ($this->isSuccess) { + unlink(__FILE__); + } } /** @@ -49,7 +55,6 @@ class HookActionsTestCest /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -57,4 +62,10 @@ class HookActionsTestCest public function HookActionsTest(AcceptanceTester $I) { } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/LocatorFunctionTest.txt b/dev/tests/verification/Resources/LocatorFunctionTest.txt index 76c48a921..f26f2aa4f 100644 --- a/dev/tests/verification/Resources/LocatorFunctionTest.txt +++ b/dev/tests/verification/Resources/LocatorFunctionTest.txt @@ -17,9 +17,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class LocatorFunctionTestCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -40,4 +55,10 @@ class LocatorFunctionTestCest $I->click(Locator::contains("string1", $I->retrieveEntityField('data', 'key1', 'test'))); // stepKey: TwoParamMix2 $I->click(Locator::contains("John", $I->retrieveEntityField('data', 'key1', 'test'))); // stepKey: TwoParamMix3 } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/MagentoCliTest.txt b/dev/tests/verification/Resources/MagentoCliTest.txt new file mode 100644 index 000000000..3efc73c01 --- /dev/null +++ b/dev/tests/verification/Resources/MagentoCliTest.txt @@ -0,0 +1,59 @@ +<?php +namespace Magento\AcceptanceTest\_default\Backend; + +use Magento\FunctionalTestingFramework\AcceptanceTester; +use \Codeception\Util\Locator; +use Yandex\Allure\Adapter\Annotation\Features; +use Yandex\Allure\Adapter\Annotation\Stories; +use Yandex\Allure\Adapter\Annotation\Title; +use Yandex\Allure\Adapter\Annotation\Description; +use Yandex\Allure\Adapter\Annotation\Parameter; +use Yandex\Allure\Adapter\Annotation\Severity; +use Yandex\Allure\Adapter\Model\SeverityLevel; +use Yandex\Allure\Adapter\Annotation\TestCaseId; + +/** + * @Description("<h3>Test files</h3>verification/TestModule/Test/BasicFunctionalTest/MagentoCliTest.xml<br>") + */ +class MagentoCliTestCest +{ + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + + /** + * @Features({"TestModule"}) + * @param AcceptanceTester $I + * @return void + * @throws \Exception + */ + public function MagentoCliTest(AcceptanceTester $I) + { + $magentoCli1 = $I->magentoCLI("maintenance:enable", 45, "\"stuffHere\""); // stepKey: magentoCli1 + $I->comment($magentoCli1); + $magentoCli2 = $I->magentoCLI("maintenance:enable", 120, "\"stuffHere\""); // stepKey: magentoCli2 + $I->comment($magentoCli2); + $magentoCli3 = $I->magentoCLISecret("config:set somePath " . $I->getSecret("someKey"), 45); // stepKey: magentoCli3 + $I->comment($magentoCli3); // stepKey: magentoCli3 + $magentoCli4 = $I->magentoCLISecret("config:set somePath " . $I->getSecret("someKey"), 120); // stepKey: magentoCli4 + $I->comment($magentoCli4); // stepKey: magentoCli4 + } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } +} diff --git a/dev/tests/verification/Resources/MergeMassViaInsertAfter.txt b/dev/tests/verification/Resources/MergeMassViaInsertAfter.txt index 1a94b90a6..60cfdb410 100644 --- a/dev/tests/verification/Resources/MergeMassViaInsertAfter.txt +++ b/dev/tests/verification/Resources/MergeMassViaInsertAfter.txt @@ -17,9 +17,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class MergeMassViaInsertAfterCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -33,4 +48,10 @@ class MergeMassViaInsertAfterCest $I->click("#mergeThree"); // stepKey: clickThree $I->fillField("#baz", "baz"); // stepKey: fillField3 } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/MergeMassViaInsertBefore.txt b/dev/tests/verification/Resources/MergeMassViaInsertBefore.txt index 12b073bfb..eb6be579b 100644 --- a/dev/tests/verification/Resources/MergeMassViaInsertBefore.txt +++ b/dev/tests/verification/Resources/MergeMassViaInsertBefore.txt @@ -17,9 +17,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class MergeMassViaInsertBeforeCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -33,4 +48,10 @@ class MergeMassViaInsertBeforeCest $I->fillField("#bar", "bar"); // stepKey: fillField2 $I->fillField("#baz", "baz"); // stepKey: fillField3 } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/MergeSkip.txt b/dev/tests/verification/Resources/MergeSkip.txt index d08f52b56..498aa1054 100644 --- a/dev/tests/verification/Resources/MergeSkip.txt +++ b/dev/tests/verification/Resources/MergeSkip.txt @@ -17,15 +17,20 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class MergeSkipCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception */ public function MergeSkip(AcceptanceTester $I, \Codeception\Scenario $scenario) { + unlink(__FILE__); $scenario->skip("This test is skipped due to the following issues:\nIssue5"); } } diff --git a/dev/tests/verification/Resources/MergedActionGroupReturningValueTest.txt b/dev/tests/verification/Resources/MergedActionGroupReturningValueTest.txt new file mode 100644 index 000000000..68fec4cfa --- /dev/null +++ b/dev/tests/verification/Resources/MergedActionGroupReturningValueTest.txt @@ -0,0 +1,94 @@ +<?php +namespace Magento\AcceptanceTest\_default\Backend; + +use Magento\FunctionalTestingFramework\AcceptanceTester; +use \Codeception\Util\Locator; +use Yandex\Allure\Adapter\Annotation\Features; +use Yandex\Allure\Adapter\Annotation\Stories; +use Yandex\Allure\Adapter\Annotation\Title; +use Yandex\Allure\Adapter\Annotation\Description; +use Yandex\Allure\Adapter\Annotation\Parameter; +use Yandex\Allure\Adapter\Annotation\Severity; +use Yandex\Allure\Adapter\Model\SeverityLevel; +use Yandex\Allure\Adapter\Annotation\TestCaseId; + +/** + * @group functional + * @Description("<h3>Test files</h3>verification/TestModule/Test/MergeFunctionalTest/MergedActionGroupReturningValueTest.xml<br>") + */ +class MergedActionGroupReturningValueTestCest +{ + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _before(AcceptanceTester $I) + { + $I->comment('[START BEFORE HOOK]'); + $I->createEntity("createPersonParam", "hook", "ReplacementPerson", [], []); // stepKey: createPersonParam + $I->comment("Entering Action Group [beforeGroup] FunctionalActionGroup"); + $I->fillField("#foo", "myData1"); // stepKey: fillField1BeforeGroup + $I->fillField("#bar", "myData2"); // stepKey: fillField2BeforeGroup + $I->comment("Exiting Action Group [beforeGroup] FunctionalActionGroup"); + $I->comment('[END BEFORE HOOK]'); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + $I->comment('[START AFTER HOOK]'); + $I->comment("Entering Action Group [afterGroup] FunctionalActionGroup"); + $I->fillField("#foo", "myData1"); // stepKey: fillField1AfterGroup + $I->fillField("#bar", "myData2"); // stepKey: fillField2AfterGroup + $I->comment("Exiting Action Group [afterGroup] FunctionalActionGroup"); + $I->comment('[END AFTER HOOK]'); + if ($this->isSuccess) { + unlink(__FILE__); + } + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _failed(AcceptanceTester $I) + { + $I->saveScreenshot(); // stepKey: saveScreenshot + } + + /** + * @Severity(level = SeverityLevel::CRITICAL) + * @Features({"TestModule"}) + * @Stories({"MQE-433"}) + * @param AcceptanceTester $I + * @return void + * @throws \Exception + */ + public function MergedActionGroupReturningValueTest(AcceptanceTester $I) + { + $I->amOnPage("/someUrl"); // stepKey: step1 + $I->comment("Entering Action Group [actionGroupWithReturnValue1] MergeActionGroupReturningValueActionGroup"); + $I->amOnPage("/Jane/Dane.html"); // stepKey: amOnPage1ActionGroupWithReturnValue1 + $I->click(".merge .Jane"); // stepKey: myMergedClickActionGroupWithReturnValue1 + $grabMultiple1ActionGroupWithReturnValue1 = $I->grabMultiple("#foo"); // stepKey: grabMultiple1ActionGroupWithReturnValue1 + $actionGroupWithReturnValue1 = $I->return($grabMultiple1ActionGroupWithReturnValue1); // stepKey: returnValueActionGroupWithReturnValue1 + $I->comment("Exiting Action Group [actionGroupWithReturnValue1] MergeActionGroupReturningValueActionGroup"); + $I->comment("Entering Action Group [actionGroupWithStringUsage1] actionGroupWithStringUsage"); + $I->see($actionGroupWithReturnValue1, "#element .{$actionGroupWithReturnValue1}"); // stepKey: see1ActionGroupWithStringUsage1 + $I->comment("Exiting Action Group [actionGroupWithStringUsage1] actionGroupWithStringUsage"); + } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } +} diff --git a/dev/tests/verification/Resources/MergedActionGroupTest.txt b/dev/tests/verification/Resources/MergedActionGroupTest.txt index c8401874d..a4abf2820 100644 --- a/dev/tests/verification/Resources/MergedActionGroupTest.txt +++ b/dev/tests/verification/Resources/MergedActionGroupTest.txt @@ -18,17 +18,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class MergedActionGroupTestCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $I->createEntity("createPersonParam", "hook", "ReplacementPerson", [], []); // stepKey: createPersonParam $I->comment("Entering Action Group [beforeGroup] FunctionalActionGroup"); $I->fillField("#foo", "myData1"); // stepKey: fillField1BeforeGroup $I->fillField("#bar", "myData2"); // stepKey: fillField2BeforeGroup $I->comment("Exiting Action Group [beforeGroup] FunctionalActionGroup"); + $I->comment('[END BEFORE HOOK]'); } /** @@ -37,10 +44,15 @@ class MergedActionGroupTestCest */ public function _after(AcceptanceTester $I) { + $I->comment('[START AFTER HOOK]'); $I->comment("Entering Action Group [afterGroup] FunctionalActionGroup"); $I->fillField("#foo", "myData1"); // stepKey: fillField1AfterGroup $I->fillField("#bar", "myData2"); // stepKey: fillField2AfterGroup $I->comment("Exiting Action Group [afterGroup] FunctionalActionGroup"); + $I->comment('[END AFTER HOOK]'); + if ($this->isSuccess) { + unlink(__FILE__); + } } /** @@ -56,7 +68,6 @@ class MergedActionGroupTestCest * @Severity(level = SeverityLevel::CRITICAL) * @Features({"TestModule"}) * @Stories({"MQE-433"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -70,4 +81,10 @@ class MergedActionGroupTestCest $I->click(".merge .Dane"); // stepKey: myMergedClickActionGroupForMerge $I->comment("Exiting Action Group [actionGroupForMerge] FunctionalActionGroupForMerge"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/MergedReferencesTest.txt b/dev/tests/verification/Resources/MergedReferencesTest.txt index 74f780aac..40c9b3cdb 100644 --- a/dev/tests/verification/Resources/MergedReferencesTest.txt +++ b/dev/tests/verification/Resources/MergedReferencesTest.txt @@ -19,13 +19,20 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class MergedReferencesTestCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $I->amOnPage("/beforeUrl"); // stepKey: before1 + $I->comment('[END BEFORE HOOK]'); } /** @@ -34,7 +41,12 @@ class MergedReferencesTestCest */ public function _after(AcceptanceTester $I) { + $I->comment('[START AFTER HOOK]'); $I->amOnPage("/afterUrl"); // stepKey: after1 + $I->comment('[END AFTER HOOK]'); + if ($this->isSuccess) { + unlink(__FILE__); + } } /** @@ -50,7 +62,6 @@ class MergedReferencesTestCest * @Severity(level = SeverityLevel::CRITICAL) * @Features({"TestModule"}) * @Stories({"MQE-433"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -60,4 +71,10 @@ class MergedReferencesTestCest $I->fillField("#merge", "merged"); // stepKey: fillField1 $I->fillField("#newElement", "newField"); // stepKey: fillField2 } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/MultipleActionGroupsTest.txt b/dev/tests/verification/Resources/MultipleActionGroupsTest.txt index 46673f473..02ffcdf84 100644 --- a/dev/tests/verification/Resources/MultipleActionGroupsTest.txt +++ b/dev/tests/verification/Resources/MultipleActionGroupsTest.txt @@ -18,17 +18,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class MultipleActionGroupsTestCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $I->createEntity("createPersonParam", "hook", "ReplacementPerson", [], []); // stepKey: createPersonParam $I->comment("Entering Action Group [beforeGroup] FunctionalActionGroup"); $I->fillField("#foo", "myData1"); // stepKey: fillField1BeforeGroup $I->fillField("#bar", "myData2"); // stepKey: fillField2BeforeGroup $I->comment("Exiting Action Group [beforeGroup] FunctionalActionGroup"); + $I->comment('[END BEFORE HOOK]'); } /** @@ -37,10 +44,15 @@ class MultipleActionGroupsTestCest */ public function _after(AcceptanceTester $I) { + $I->comment('[START AFTER HOOK]'); $I->comment("Entering Action Group [afterGroup] FunctionalActionGroup"); $I->fillField("#foo", "myData1"); // stepKey: fillField1AfterGroup $I->fillField("#bar", "myData2"); // stepKey: fillField2AfterGroup $I->comment("Exiting Action Group [afterGroup] FunctionalActionGroup"); + $I->comment('[END AFTER HOOK]'); + if ($this->isSuccess) { + unlink(__FILE__); + } } /** @@ -56,7 +68,6 @@ class MultipleActionGroupsTestCest * @Severity(level = SeverityLevel::CRITICAL) * @Features({"TestModule"}) * @Stories({"MQE-433"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -80,4 +91,10 @@ class MultipleActionGroupsTestCest $I->see("#element .John"); // stepKey: see1ActionGroupWithDataOverride2 $I->comment("Exiting Action Group [actionGroupWithDataOverride2] FunctionalActionGroupWithData"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/PageReplacementTest.txt b/dev/tests/verification/Resources/PageReplacementTest.txt index cda119999..097a8be02 100644 --- a/dev/tests/verification/Resources/PageReplacementTest.txt +++ b/dev/tests/verification/Resources/PageReplacementTest.txt @@ -17,9 +17,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class PageReplacementTestCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -35,8 +50,14 @@ class PageReplacementTestCest $I->amOnPage("/John/StringLiteral2.html"); // stepKey: twoParamPageStringData $I->amOnPage("/John/" . $I->retrieveEntityField('datakey', 'firstname', 'test') . ".html"); // stepKey: twoParamPageDataPersist $I->amOnPage("/" . $I->retrieveEntityField('datakey', 'firstname', 'test') . "/StringLiteral2.html"); // stepKey: twoParamPagePersistString - $I->amOnPage("/" . getenv("MAGENTO_BACKEND_NAME") . "/backend"); // stepKey: onAdminPage - $I->amOnPage("/" . getenv("MAGENTO_BACKEND_NAME") . "/StringLiteral/page.html"); // stepKey: oneParamAdminPageString + $I->amOnPage((getenv("MAGENTO_BACKEND_BASE_URL") ? rtrim(getenv("MAGENTO_BACKEND_BASE_URL"), "/") : "") . "/" . getenv("MAGENTO_BACKEND_NAME") . "/backend"); // stepKey: onAdminPage + $I->amOnPage((getenv("MAGENTO_BACKEND_BASE_URL") ? rtrim(getenv("MAGENTO_BACKEND_BASE_URL"), "/") : "") . "/" . getenv("MAGENTO_BACKEND_NAME") . "/StringLiteral/page.html"); // stepKey: oneParamAdminPageString $I->amOnUrl("http://myFullUrl.com/"); // stepKey: onExternalPage } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ParameterArrayTest.txt b/dev/tests/verification/Resources/ParameterArrayTest.txt index 8918bb619..1439dacdc 100644 --- a/dev/tests/verification/Resources/ParameterArrayTest.txt +++ b/dev/tests/verification/Resources/ParameterArrayTest.txt @@ -17,9 +17,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ParameterArrayTestCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -50,4 +65,10 @@ class ParameterArrayTestCest $I->pressKey("#selector", ['ctrl', 'a'],'new', 1, ['ctrl'], ['shift', 'ctrl', 'del'], 0, [$I->retrieveEntityField('simpleDataKey', 'name', 'test'), $I->retrieveEntityField('simpleDataKey', 'name', 'test')]); // stepKey: pressKey005 $I->pressKey("#selector", ['ctrl', 'a'],'new', 1, ['ctrl'], ['shift', 'ctrl', 'del'], [msq("simpleParamData") . "prename", "postname" . msq("simpleParamData")]); // stepKey: pressKey006 } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/ParentExtendedTest.txt b/dev/tests/verification/Resources/ParentExtendedTest.txt index 5606b2ac3..a06efd830 100644 --- a/dev/tests/verification/Resources/ParentExtendedTest.txt +++ b/dev/tests/verification/Resources/ParentExtendedTest.txt @@ -19,13 +19,20 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class ParentExtendedTestCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $I->amOnPage("/beforeUrl"); // stepKey: beforeAmOnPageKey + $I->comment('[END BEFORE HOOK]'); } /** @@ -34,7 +41,12 @@ class ParentExtendedTestCest */ public function _after(AcceptanceTester $I) { + $I->comment('[START AFTER HOOK]'); $I->amOnPage("/afterUrl"); // stepKey: afterAmOnPageKey + $I->comment('[END AFTER HOOK]'); + if ($this->isSuccess) { + unlink(__FILE__); + } } /** @@ -50,7 +62,6 @@ class ParentExtendedTestCest * @Severity(level = SeverityLevel::MINOR) * @Features({"TestModule"}) * @Stories({"Parent"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -59,4 +70,10 @@ class ParentExtendedTestCest { $I->comment("Parent Comment"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/PersistedAndXmlEntityArguments.txt b/dev/tests/verification/Resources/PersistedAndXmlEntityArguments.txt index d49b99306..35eb126cd 100644 --- a/dev/tests/verification/Resources/PersistedAndXmlEntityArguments.txt +++ b/dev/tests/verification/Resources/PersistedAndXmlEntityArguments.txt @@ -17,9 +17,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class PersistedAndXmlEntityArgumentsCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -30,4 +45,10 @@ class PersistedAndXmlEntityArgumentsCest $I->seeInCurrentUrl("/" . $I->retrieveEntityField('persistedInTest', 'urlKey', 'test') . ".html?___store=" . msq("uniqueData") . "John"); // stepKey: checkUrlAfterGroup $I->comment("Exiting Action Group [afterGroup] FunctionalActionGroupWithXmlAndPersistedData"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/PersistedReplacementTest.txt b/dev/tests/verification/Resources/PersistedReplacementTest.txt index eb1c9995d..0763d5da2 100644 --- a/dev/tests/verification/Resources/PersistedReplacementTest.txt +++ b/dev/tests/verification/Resources/PersistedReplacementTest.txt @@ -17,18 +17,35 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class PersistedReplacementTestCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $I->createEntity("createData1", "hook", "ReplacementPerson", [], []); // stepKey: createData1 + $I->comment('[END BEFORE HOOK]'); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } } /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -59,4 +76,10 @@ class PersistedReplacementTestCest $I->dontSeeInSource("StringBefore " . $I->retrieveEntityField('createData1', 'firstname', 'test') . " StringAfter"); // stepKey: htmlReplace11 $I->dontSeeInSource("#" . getenv("MAGENTO_BASE_URL") . "#" . $I->retrieveEntityField('createdData', 'firstname', 'test')); // stepKey: htmlReplace12 } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/PersistenceActionGroupAppendingTest.txt b/dev/tests/verification/Resources/PersistenceActionGroupAppendingTest.txt index 426003bc2..4901e3341 100644 --- a/dev/tests/verification/Resources/PersistenceActionGroupAppendingTest.txt +++ b/dev/tests/verification/Resources/PersistenceActionGroupAppendingTest.txt @@ -17,24 +17,41 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class PersistenceActionGroupAppendingTestCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $I->comment("Entering Action Group [ACTIONGROUPBEFORE] DataPersistenceAppendingActionGroup"); - $I->createEntity("createDataACTIONGROUPBEFORE", "hook", "entity", [], []); // stepKey: createDataACTIONGROUPBEFORE + $I->createEntity("createDataACTIONGROUPBEFORE", "hook", "DefaultPerson", [], []); // stepKey: createDataACTIONGROUPBEFORE $I->updateEntity("createDataACTIONGROUPBEFORE", "hook", "newEntity",[]); // stepKey: updateDataACTIONGROUPBEFORE $I->deleteEntity("createDataACTIONGROUPBEFORE", "hook"); // stepKey: deleteDataACTIONGROUPBEFORE $I->getEntity("getDataACTIONGROUPBEFORE", "hook", "someEneity", [], null); // stepKey: getDataACTIONGROUPBEFORE $I->comment($I->retrieveEntityField('createData', 'field', 'hook')); $I->comment("Exiting Action Group [ACTIONGROUPBEFORE] DataPersistenceAppendingActionGroup"); + $I->comment('[END BEFORE HOOK]'); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } } /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -42,11 +59,17 @@ class PersistenceActionGroupAppendingTestCest public function PersistenceActionGroupAppendingTest(AcceptanceTester $I) { $I->comment("Entering Action Group [ACTIONGROUP] DataPersistenceAppendingActionGroup"); - $I->createEntity("createDataACTIONGROUP", "test", "entity", [], []); // stepKey: createDataACTIONGROUP + $I->createEntity("createDataACTIONGROUP", "test", "DefaultPerson", [], []); // stepKey: createDataACTIONGROUP $I->updateEntity("createDataACTIONGROUP", "test", "newEntity",[]); // stepKey: updateDataACTIONGROUP $I->deleteEntity("createDataACTIONGROUP", "test"); // stepKey: deleteDataACTIONGROUP $I->getEntity("getDataACTIONGROUP", "test", "someEneity", [], null); // stepKey: getDataACTIONGROUP $I->comment($I->retrieveEntityField('createDataACTIONGROUP', 'field', 'test')); $I->comment("Exiting Action Group [ACTIONGROUP] DataPersistenceAppendingActionGroup"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/PersistenceCustomFieldsTest.txt b/dev/tests/verification/Resources/PersistenceCustomFieldsTest.txt index 41251d2fb..5a3270922 100644 --- a/dev/tests/verification/Resources/PersistenceCustomFieldsTest.txt +++ b/dev/tests/verification/Resources/PersistenceCustomFieldsTest.txt @@ -17,48 +17,51 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class PersistenceCustomFieldsTestCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $createData1Fields['firstname'] = "Mac"; - $createData1Fields['lastname'] = "Doe"; + $createData1Fields['lastname'] = "Bar"; $I->createEntity("createData1", "hook", "DefaultPerson", [], $createData1Fields); // stepKey: createData1 - $createData2Fields['firstname'] = $I->retrieveEntityField('createData1', 'firstname', 'hook'); - $I->createEntity("createData2", "hook", "uniqueData", ["createData1"], $createData2Fields); // stepKey: createData2 + $I->comment('[END BEFORE HOOK]'); + } + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } } /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception */ public function PersistenceCustomFieldsTest(AcceptanceTester $I) { - $createdDataFields['favoriteIndex'] = "1"; - $createdDataFields['middlename'] = "Kovacs"; - $I->createEntity("createdData", "test", "simpleData", [], $createdDataFields); // stepKey: createdData $createdData3Fields['firstname'] = "Takeshi"; $createdData3Fields['lastname'] = "Kovacs"; $I->createEntity("createdData3", "test", "UniquePerson", ["createdData"], $createdData3Fields); // stepKey: createdData3 - $I->comment("Entering Action Group [createdAG] PersistenceActionGroup"); - $createDataAG1CreatedAGFields['firstname'] = "string1"; - $I->createEntity("createDataAG1CreatedAG", "test", "simpleData", [], $createDataAG1CreatedAGFields); // stepKey: createDataAG1CreatedAG - $createDataAG2CreatedAGFields['firstname'] = "Jane"; - $I->createEntity("createDataAG2CreatedAG", "test", "simpleData", [], $createDataAG2CreatedAGFields); // stepKey: createDataAG2CreatedAG - $createDataAG3CreatedAGFields['firstname'] = $I->retrieveEntityField('createdData3', 'firstname', 'test'); - $I->createEntity("createDataAG3CreatedAG", "test", "simpleData", [], $createDataAG3CreatedAGFields); // stepKey: createDataAG3CreatedAG - $I->comment("Exiting Action Group [createdAG] PersistenceActionGroup"); - $I->comment("Entering Action Group [AGKEY] DataPersistenceSelfReferenceActionGroup"); - $I->createEntity("createData1AGKEY", "test", "entity1", [], []); // stepKey: createData1AGKEY - $I->createEntity("createData2AGKEY", "test", "entity2", [], []); // stepKey: createData2AGKEY - $createData3AGKEYFields['key1'] = $I->retrieveEntityField('createData1AGKEY', 'field', 'test'); - $createData3AGKEYFields['key2'] = $I->retrieveEntityField('createData2AGKEY', 'field', 'test'); - $I->createEntity("createData3AGKEY", "test", "entity3", [], $createData3AGKEYFields); // stepKey: createData3AGKEY - $I->comment("Exiting Action Group [AGKEY] DataPersistenceSelfReferenceActionGroup"); + } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; } } diff --git a/dev/tests/verification/Resources/SectionReplacementTest.txt b/dev/tests/verification/Resources/SectionReplacementTest.txt index 109c198eb..586280197 100644 --- a/dev/tests/verification/Resources/SectionReplacementTest.txt +++ b/dev/tests/verification/Resources/SectionReplacementTest.txt @@ -17,9 +17,24 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class SectionReplacementTestCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -63,4 +78,10 @@ class SectionReplacementTestCest $I->click("(//div[@data-role='slide'])[1]/a[@data-element='link'][contains(@href,'')]"); // stepKey: selectorParamWithEmptyString $I->click("(//div[@data-role='slide'])[1]/a[@data-element='link'][contains(@href,' ')]"); // stepKey: selectorParamWithASpace } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/SkippedTest.txt b/dev/tests/verification/Resources/SkippedTest.txt index 7dd9ba280..2e7decf9d 100644 --- a/dev/tests/verification/Resources/SkippedTest.txt +++ b/dev/tests/verification/Resources/SkippedTest.txt @@ -18,17 +18,22 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class SkippedTestCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @Stories({"skipped"}) * @Severity(level = SeverityLevel::MINOR) * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception */ public function SkippedTest(AcceptanceTester $I, \Codeception\Scenario $scenario) { + unlink(__FILE__); $scenario->skip("This test is skipped due to the following issues:\nSkippedValue"); } } diff --git a/dev/tests/verification/Resources/SkippedTestTwoIssues.txt b/dev/tests/verification/Resources/SkippedTestTwoIssues.txt index 0087ba92d..2deaa9beb 100644 --- a/dev/tests/verification/Resources/SkippedTestTwoIssues.txt +++ b/dev/tests/verification/Resources/SkippedTestTwoIssues.txt @@ -18,17 +18,22 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class SkippedTestTwoIssuesCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @Stories({"skippedMultiple"}) * @Severity(level = SeverityLevel::MINOR) * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception */ public function SkippedTestTwoIssues(AcceptanceTester $I, \Codeception\Scenario $scenario) { + unlink(__FILE__); $scenario->skip("This test is skipped due to the following issues:\nSkippedValue\nSecondSkippedValue"); } } diff --git a/dev/tests/verification/Resources/SkippedTestWithHooks.txt b/dev/tests/verification/Resources/SkippedTestWithHooks.txt index 6b2682c49..0299eb67a 100644 --- a/dev/tests/verification/Resources/SkippedTestWithHooks.txt +++ b/dev/tests/verification/Resources/SkippedTestWithHooks.txt @@ -18,17 +18,22 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class SkippedTestWithHooksCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @Stories({"skippedWithHooks"}) * @Severity(level = SeverityLevel::MINOR) * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception */ public function SkippedTestWithHooks(AcceptanceTester $I, \Codeception\Scenario $scenario) { + unlink(__FILE__); $scenario->skip("This test is skipped due to the following issues:\nSkippedValue"); } } diff --git a/dev/tests/verification/Resources/SkippedTestNoIssues.txt b/dev/tests/verification/Resources/SkippedTestWithIssueMustGetSkippedWithoutErrorExitCode.txt similarity index 65% rename from dev/tests/verification/Resources/SkippedTestNoIssues.txt rename to dev/tests/verification/Resources/SkippedTestWithIssueMustGetSkippedWithoutErrorExitCode.txt index 387ada571..f35d7ca74 100644 --- a/dev/tests/verification/Resources/SkippedTestNoIssues.txt +++ b/dev/tests/verification/Resources/SkippedTestWithIssueMustGetSkippedWithoutErrorExitCode.txt @@ -13,23 +13,27 @@ use Yandex\Allure\Adapter\Model\SeverityLevel; use Yandex\Allure\Adapter\Annotation\TestCaseId; /** - * @Title("[NO TESTCASEID]: skippedNoIssuesTest") - * @Description("<h3>Test files</h3>verification/TestModule/Test/SkippedTest/SkippedTestNoIssues.xml<br>") - * @group skip + * @Title("[NO TESTCASEID]: skippedTest") + * @Description("<h3>Test files</h3>verification/TestModule/Test/SkippedTest/SkippedTestWithIssueMustGetSkippedWithoutErrorExitCode.xml<br>") */ -class SkippedTestNoIssuesCest +class SkippedTestWithIssueMustGetSkippedWithoutErrorExitCodeCest { /** - * @Stories({"skippedNo"}) + * @var bool + */ + private $isSuccess = false; + + /** + * @Stories({"skipped"}) * @Severity(level = SeverityLevel::MINOR) * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception */ - public function SkippedTestNoIssues(AcceptanceTester $I, \Codeception\Scenario $scenario) + public function SkippedTestWithIssueMustGetSkippedWithoutErrorExitCode(AcceptanceTester $I, \Codeception\Scenario $scenario) { - $scenario->skip("This test is skipped due to the following issues:\nNo issues have been specified."); + unlink(__FILE__); + $scenario->skip("This test is skipped due to the following issues:\nSkippedValue"); } } diff --git a/dev/tests/verification/Resources/StaticChecks/mftf-deprecated-entity-usage-checks.txt b/dev/tests/verification/Resources/StaticChecks/mftf-deprecated-entity-usage-checks.txt new file mode 100644 index 000000000..63e025470 --- /dev/null +++ b/dev/tests/verification/Resources/StaticChecks/mftf-deprecated-entity-usage-checks.txt @@ -0,0 +1,23 @@ + +File "/verification/DeprecationCheckModule/Test/DeprecationCheckTest.xml" contains: + - Deprecated Page(s): + "DeprecationCheckPage" in /verification/DeprecationCheckModule/Page/DeprecationCheckPage.xml + - Deprecated ActionGroup(s): + "DeprecationCheckDeprecatedActionGroup" in /verification/DeprecationCheckModule/ActionGroup/DeprecationCheckDeprecatedActionGroup.xml + - Deprecated Data(s): + "DeprecationCheckData" in /verification/DeprecationCheckModule/Data/DeprecationCheckData.xml + - "DeprecationCheckData" references deprecated: + "type1" in metadata xml file + + +File "/verification/DeprecationCheckModule/ActionGroup/DeprecationCheckActionGroup.xml" contains: + - Deprecated Section(s): + "DeprecationCheckSection" in /verification/DeprecationCheckModule/Section/DeprecationCheckSection.xml + - Deprecated Element(s): + "DeprecationCheckSection.deprecationCheckElement" in /verification/DeprecationCheckModule/Section/DeprecationCheckSection.xml + + +File "/verification/DeprecationCheckModule/Suite/deprecationCheckSuite.xml" contains: + - Deprecated Test(s): + "DeprecationCheckDeprecatedTest" in /verification/DeprecationCheckModule/Test/DeprecationCheckDeprecatedTest.xml + diff --git a/dev/tests/verification/Resources/StaticChecks/mftf-pause-action-usage-checks.txt b/dev/tests/verification/Resources/StaticChecks/mftf-pause-action-usage-checks.txt new file mode 100644 index 000000000..fd042a24c --- /dev/null +++ b/dev/tests/verification/Resources/StaticChecks/mftf-pause-action-usage-checks.txt @@ -0,0 +1,30 @@ + +File "/verification/PauseCheckModule/ActionGroup/ActionGroupWithMultiplePausesActionGroup.xml" +contains pause action(s): + + ActionGroupWithMultiplePausesActionGroup has pause action at stepKey(s): pauseAfterFillField1, pauseAfterFillField2, pauseAfterFillField3 + +File "/verification/PauseCheckModule/ActionGroup/ActionGroupWithPauseActionGroup.xml" +contains pause action(s): + + ActionGroupWithPauseActionGroup has pause action at stepKey(s): pauseAfterFillField2 + +File "/verification/PauseCheckModule/Test/TestWithMultiplePauseActionsTest.xml" +contains pause action(s): + + TestWithMultiplePauseActionsTest has pause action at stepKey(s): pauseBeforeAmOnPageKey, pauseAfterStep2, pauseAfterAmOnPageKey + +File "/verification/PauseCheckModule/Test/TestWithPauseActionTest.xml" +contains pause action(s): + + TestWithPauseActionTest has pause action at stepKey(s): pauseAfterStep3 + +File "/verification/PauseCheckModule/Suite/suiteWithMultiplePauseActionsSuite.xml" +contains pause action(s): + + suiteWithMultiplePauseActionsSuite has pause action at stepKey(s): pauseCreate1, pauseFillAfter + +File "/verification/PauseCheckModule/Suite/suiteWithPauseActionSuite.xml" +contains pause action(s): + + suiteWithPauseActionSuite has pause action at stepKey(s): pauseSuite diff --git a/dev/tests/verification/Resources/XmlCommentedActionGroupTest.txt b/dev/tests/verification/Resources/XmlCommentedActionGroupTest.txt index 0eb546335..c406af2d5 100644 --- a/dev/tests/verification/Resources/XmlCommentedActionGroupTest.txt +++ b/dev/tests/verification/Resources/XmlCommentedActionGroupTest.txt @@ -18,10 +18,25 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class XmlCommentedActionGroupTestCest { + /** + * @var bool + */ + private $isSuccess = false; + + /** + * @param AcceptanceTester $I + * @throws \Exception + */ + public function _after(AcceptanceTester $I) + { + if ($this->isSuccess) { + unlink(__FILE__); + } + } + /** * @Severity(level = SeverityLevel::CRITICAL) * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -33,4 +48,10 @@ class XmlCommentedActionGroupTestCest $I->see("John", "#element .test1"); // stepKey: seeFirstNameActionGroup $I->comment("Exiting Action Group [actionGroup] XmlCommentedActionGroup"); } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/XmlCommentedTest.txt b/dev/tests/verification/Resources/XmlCommentedTest.txt index 47d984d9e..26563b7f4 100644 --- a/dev/tests/verification/Resources/XmlCommentedTest.txt +++ b/dev/tests/verification/Resources/XmlCommentedTest.txt @@ -18,15 +18,22 @@ use Yandex\Allure\Adapter\Annotation\TestCaseId; */ class XmlCommentedTestCest { + /** + * @var bool + */ + private $isSuccess = false; + /** * @param AcceptanceTester $I * @throws \Exception */ public function _before(AcceptanceTester $I) { + $I->comment('[START BEFORE HOOK]'); $I->comment("< > & \$abc \" abc ' <click stepKey=\"click\" userInput=\"\$\$createDataHook.firstname\$\$\" selector=\"#id_1\">/"); $I->amOnPage("/beforeUrl"); // stepKey: beforeAmOnPageKey $I->comment("< > & \$abc \" abc ' <click stepKey=\"click\" userInput=\"\$\$createDataHook.firstname\$\$\" selector=\"#id_2\">/"); + $I->comment('[END BEFORE HOOK]'); } /** @@ -35,9 +42,14 @@ class XmlCommentedTestCest */ public function _after(AcceptanceTester $I) { + $I->comment('[START AFTER HOOK]'); $I->comment("< > & \$abc \" abc ' <click stepKey=\"click\" userInput=\"\$\$createDataHook.firstname\$\$\" selector=\"#id_1\">/"); $I->amOnPage("/afterUrl"); // stepKey: afterAmOnPageKey $I->comment("< > & \$abc \" abc ' <click stepKey=\"click\" userInput=\"\$\$createDataHook.firstname\$\$\" selector=\"#id_2\">/"); + $I->comment('[END AFTER HOOK]'); + if ($this->isSuccess) { + unlink(__FILE__); + } } /** @@ -52,7 +64,6 @@ class XmlCommentedTestCest /** * @Severity(level = SeverityLevel::CRITICAL) * @Features({"TestModule"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -70,4 +81,10 @@ class XmlCommentedTestCest $I->comment("<cancelPopup stepKey=\"cancelPopupKey1\"/>"); $I->checkOption(".functionalTestSelector"); // stepKey: checkOptionKey1 } + + public function _passed(AcceptanceTester $I) + { + // Test passed successfully. + $this->isSuccess = true; + } } diff --git a/dev/tests/verification/Resources/functionalSuiteHooks.txt b/dev/tests/verification/Resources/functionalSuiteHooks.txt index 908cff8aa..946bf456e 100644 --- a/dev/tests/verification/Resources/functionalSuiteHooks.txt +++ b/dev/tests/verification/Resources/functionalSuiteHooks.txt @@ -2,8 +2,17 @@ namespace Group; +use Facebook\WebDriver\Remote\RemoteWebDriver; +use Facebook\WebDriver\Remote\DesiredCapabilities; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\PersistedObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; +use Magento\FunctionalTestingFramework\Module\MagentoWebDriver; +use Magento\FunctionalTestingFramework\Module\MagentoAssert; +use Magento\FunctionalTestingFramework\Module\MagentoActionProxies; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Codeception\Lib\ModuleContainer; +use Codeception\Module; +use Facebook\WebDriver\Chrome\ChromeOptions; /** * Group class is Codeception Extension which is allowed to handle to all internal events. @@ -22,9 +31,15 @@ class functionalSuiteHooks extends \Codeception\GroupObject private $currentTestRun = 0; private static $HOOK_EXECUTION_INIT = "\n/******** Beginning execution of functionalSuiteHooks suite %s block ********/\n"; private static $HOOK_EXECUTION_END = "\n/******** Execution of functionalSuiteHooks suite %s block complete ********/\n"; + /** @var MagentoWebDriver */ + private $webDriver; + /** @var ModuleContainer */ + private $moduleContainer; public function _before(\Codeception\Event\TestEvent $e) { + $this->webDriver = $this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver'); + $this->moduleContainer = $this->webDriver->getModuleContainer(); // increment test count per execution $this->currentTestRun++; $this->executePreConditions(); @@ -35,45 +50,59 @@ class functionalSuiteHooks extends \Codeception\GroupObject } } - private function executePreConditions() { if ($this->currentTestRun == 1) { + $this->testCount = $this->getTestCount(); + print sprintf(self::$HOOK_EXECUTION_INIT, "before"); try { - $webDriver = $this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver'); - - // close any open sessions - if ($webDriver->webDriver != null) { - $webDriver->webDriver->close(); - $webDriver->webDriver = null; + if ($this->webDriver->webDriver != null) { + $this->webDriver->_restart(); + } else { + $this->webDriver->_initializeSession(); } - - // initialize the webdriver session - $webDriver->_initializeSession(); - $webDriver->amOnPage("some.url"); // stepKey: before - $createFields['someKey'] = "dataHere"; + $this->getModuleForAction("amOnPage")->amOnPage("some.url"); // stepKey: before + $createOneFields['someKey'] = "dataHere"; + PersistedObjectHandler::getInstance()->createEntity( + "createOne", + "suite", + "createEntityOne", + [], + $createOneFields + ); PersistedObjectHandler::getInstance()->createEntity( - "create", + "createTwo", "suite", - "createThis", - $createFields + "createEntityTwo", + ["createEntityOne"] ); - $webDriver->click(PersistedObjectHandler::getInstance()->retrieveEntityField('create', 'data', 'suite')); // stepKey: clickWithData + PersistedObjectHandler::getInstance()->createEntity( + "createThree", + "suite", + "createEntityThree", + [] + ); + PersistedObjectHandler::getInstance()->createEntity( + "createFour", + "suite", + "createEntityFour", + ["createEntityTwo", "createEntityThree"] + ); + $this->getModuleForAction("click")->click(PersistedObjectHandler::getInstance()->retrieveEntityField('createTwo', 'data', 'suite')); // stepKey: clickWithData print("Entering Action Group [AC] actionGroupWithTwoArguments"); - $webDriver->see("John", msq("uniqueData") . "John"); // stepKey: seeFirstNameAC + $this->getModuleForAction("see")->see("John", msq("uniqueData") . "John"); // stepKey: seeFirstNameAC print("Exiting Action Group [AC] actionGroupWithTwoArguments"); - - // reset configuration and close session - $this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver')->_resetConfig(); - $webDriver->webDriver->close(); - $webDriver->webDriver = null; - } catch (\Exception $exception) { $this->preconditionFailure = $exception->getMessage(); } + // reset configuration and close session + $this->webDriver->_resetConfig(); + $this->webDriver->webDriver->close(); + $this->webDriver->webDriver = null; + print sprintf(self::$HOOK_EXECUTION_END, "before"); } } @@ -83,7 +112,6 @@ class functionalSuiteHooks extends \Codeception\GroupObject $this->executePostConditions($e); } - private function executePostConditions(\Codeception\Event\TestEvent $e) { if ($this->currentTestRun == $this->testCount) { @@ -96,7 +124,7 @@ class functionalSuiteHooks extends \Codeception\GroupObject //Access private TestResultObject to find stack and if there are any errors (as opposed to failures) $testResultObject = call_user_func(\Closure::bind( function () use ($cest) { - return $cest->getTestResultObject(); + return $cest->getResultAggregator(); }, $cest )); @@ -104,34 +132,106 @@ class functionalSuiteHooks extends \Codeception\GroupObject if (!empty($errors)) { foreach ($errors as $error) { - if ($error->failedTest()->getTestMethod() == $cest->getName()) { + if ($error->getTest()->getTestMethod() == $cest->getName()) { // Do not attempt to run _after if failure was in the _after block // Try to run _after but catch exceptions to prevent them from overwriting original failure. print("LAST TEST IN SUITE FAILED, TEST AFTER MAY NOT BE SUCCESSFUL\n"); } } } - $webDriver = $this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver'); - - // close any open sessions - if ($webDriver->webDriver != null) { - $webDriver->webDriver->close(); - $webDriver->webDriver = null; + if ($this->webDriver->webDriver != null) { + $this->webDriver->_restart(); + } else { + $this->webDriver->_initializeSession(); } - - // initialize the webdriver session - $webDriver->_initializeSession(); - $webDriver->amOnPage("some.url"); // stepKey: after - $webDriver->deleteEntityByUrl("deleteThis"); // stepKey: delete + $this->getModuleForAction("amOnPage")->amOnPage("some.url"); // stepKey: after + $this->getModuleForAction("deleteEntityByUrl")->deleteEntityByUrl("deleteThis"); // stepKey: delete print("Entering Action Group [AC] actionGroupWithTwoArguments"); - $webDriver->see("John", msq("uniqueData") . "John"); // stepKey: seeFirstNameAC + $this->getModuleForAction("see")->see("John", msq("uniqueData") . "John"); // stepKey: seeFirstNameAC print("Exiting Action Group [AC] actionGroupWithTwoArguments"); } catch (\Exception $exception) { print $exception->getMessage(); } PersistedObjectHandler::getInstance()->clearSuiteObjects(); + + $this->closeSession($this->webDriver); + print sprintf(self::$HOOK_EXECUTION_END, "after"); } } -} + + /** + * Close session method closes current session. + * If config 'close_all_sessions' is set to 'true' all sessions will be closed. + * + * return void + */ + private function closeSession(): void + { + $webDriverConfig = $this->webDriver->_getConfig(); + $this->webDriver->_closeSession(); + if (isset($webDriverConfig['close_all_sessions']) && $webDriverConfig['close_all_sessions'] === "true") { + $wdHost = sprintf( + '%s://%s:%s%s', + $webDriverConfig['protocol'], + $webDriverConfig['host'], + $webDriverConfig['port'], + $webDriverConfig['path'] + ); + $availableSessions = RemoteWebDriver::getAllSessions($wdHost); + foreach ($availableSessions as $session) { + try { + $chromeOptions = new ChromeOptions(); + $capabilities = DesiredCapabilities::chrome(); + $remoteWebDriver = RemoteWebDriver::createBySessionID($session['id'], $wdHost, + $webDriverConfig['connection_timeout'], $webDriverConfig['request_timeout'], true, $capabilities); + $remoteWebDriver->quit(); + } catch (\Exception $exception) { + print("Failed trying to quit WebDriver session. Exception message: " . $exception->getMessage() . " Test execution will continue." . PHP_EOL); + // Session already closed so nothing to do + } + } + } + } + + /** + * Return the module for an action. + * + * @param string $action + * @return Module + * @throws \Exception + */ + private function getModuleForAction($action) + { + $module = $this->moduleContainer->moduleForAction($action); + if ($module === null) { + throw new TestFrameworkException('Invalid action "' . $action . '"' . PHP_EOL); + } + return $module; + } + + /** + * Counts how many tests in group. + * + * @return integer + */ + private function getTestCount() + { + $config = $this->getGlobalConfig(); + if (empty($config['groups']) || empty($config['groups'][self::$group])) { + return $this->testCount; + } + $pathToGroupDir = TESTS_BP . DIRECTORY_SEPARATOR . array_first($config['groups'][self::$group]); + $pathToGroupCests = $pathToGroupDir . "*Cest.php"; + + $files = glob($pathToGroupCests); + if (is_array($files)) { + $qty = count($files); + print('In a group "' . self::$group . '" suite executor found ' . $qty . ' tests.' . PHP_EOL); + return $qty; + } + + return $this->testCount; + } +} \ No newline at end of file diff --git a/dev/tests/verification/Resources/functionalSuiteWithComments.txt b/dev/tests/verification/Resources/functionalSuiteWithComments.txt index 0e15b7909..a5a130345 100644 --- a/dev/tests/verification/Resources/functionalSuiteWithComments.txt +++ b/dev/tests/verification/Resources/functionalSuiteWithComments.txt @@ -2,8 +2,17 @@ namespace Group; +use Facebook\WebDriver\Remote\RemoteWebDriver; +use Facebook\WebDriver\Remote\DesiredCapabilities; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\PersistedObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; +use Magento\FunctionalTestingFramework\Module\MagentoWebDriver; +use Magento\FunctionalTestingFramework\Module\MagentoAssert; +use Magento\FunctionalTestingFramework\Module\MagentoActionProxies; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Codeception\Lib\ModuleContainer; +use Codeception\Module; +use Facebook\WebDriver\Chrome\ChromeOptions; /** * Group class is Codeception Extension which is allowed to handle to all internal events. @@ -22,9 +31,15 @@ class functionalSuiteWithComments extends \Codeception\GroupObject private $currentTestRun = 0; private static $HOOK_EXECUTION_INIT = "\n/******** Beginning execution of functionalSuiteWithComments suite %s block ********/\n"; private static $HOOK_EXECUTION_END = "\n/******** Execution of functionalSuiteWithComments suite %s block complete ********/\n"; + /** @var MagentoWebDriver */ + private $webDriver; + /** @var ModuleContainer */ + private $moduleContainer; public function _before(\Codeception\Event\TestEvent $e) { + $this->webDriver = $this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver'); + $this->moduleContainer = $this->webDriver->getModuleContainer(); // increment test count per execution $this->currentTestRun++; $this->executePreConditions(); @@ -35,47 +50,43 @@ class functionalSuiteWithComments extends \Codeception\GroupObject } } - private function executePreConditions() { if ($this->currentTestRun == 1) { + $this->testCount = $this->getTestCount(); + print sprintf(self::$HOOK_EXECUTION_INIT, "before"); try { - $webDriver = $this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver'); - - // close any open sessions - if ($webDriver->webDriver != null) { - $webDriver->webDriver->close(); - $webDriver->webDriver = null; + if ($this->webDriver->webDriver != null) { + $this->webDriver->_restart(); + } else { + $this->webDriver->_initializeSession(); } - - // initialize the webdriver session - $webDriver->_initializeSession(); print("Comment in Before"); - $webDriver->amOnPage("some.url"); // stepKey: before + $this->getModuleForAction("amOnPage")->amOnPage("some.url"); // stepKey: before $createFields['someKey'] = "dataHere"; PersistedObjectHandler::getInstance()->createEntity( "create", "suite", "createThis", - $createFields + [], + $createFields ); print("<click stepKey=\"comment with element\" userInput=\"helloworld\"/>"); - $webDriver->click(PersistedObjectHandler::getInstance()->retrieveEntityField('create', 'data', 'suite')); // stepKey: clickWithData + $this->getModuleForAction("click")->click(PersistedObjectHandler::getInstance()->retrieveEntityField('create', 'data', 'suite')); // stepKey: clickWithData print("Entering Action Group [AC] actionGroupWithTwoArguments"); - $webDriver->see("John", msq("uniqueData") . "John"); // stepKey: seeFirstNameAC + $this->getModuleForAction("see")->see("John", msq("uniqueData") . "John"); // stepKey: seeFirstNameAC print("Exiting Action Group [AC] actionGroupWithTwoArguments"); - - // reset configuration and close session - $this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver')->_resetConfig(); - $webDriver->webDriver->close(); - $webDriver->webDriver = null; - } catch (\Exception $exception) { $this->preconditionFailure = $exception->getMessage(); } + // reset configuration and close session + $this->webDriver->_resetConfig(); + $this->webDriver->webDriver->close(); + $this->webDriver->webDriver = null; + print sprintf(self::$HOOK_EXECUTION_END, "before"); } } @@ -85,7 +96,6 @@ class functionalSuiteWithComments extends \Codeception\GroupObject $this->executePostConditions($e); } - private function executePostConditions(\Codeception\Event\TestEvent $e) { if ($this->currentTestRun == $this->testCount) { @@ -98,7 +108,7 @@ class functionalSuiteWithComments extends \Codeception\GroupObject //Access private TestResultObject to find stack and if there are any errors (as opposed to failures) $testResultObject = call_user_func(\Closure::bind( function () use ($cest) { - return $cest->getTestResultObject(); + return $cest->getResultAggregator(); }, $cest )); @@ -106,30 +116,102 @@ class functionalSuiteWithComments extends \Codeception\GroupObject if (!empty($errors)) { foreach ($errors as $error) { - if ($error->failedTest()->getTestMethod() == $cest->getName()) { + if ($error->getTest()->getTestMethod() == $cest->getName()) { // Do not attempt to run _after if failure was in the _after block // Try to run _after but catch exceptions to prevent them from overwriting original failure. print("LAST TEST IN SUITE FAILED, TEST AFTER MAY NOT BE SUCCESSFUL\n"); } } } - $webDriver = $this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver'); - - // close any open sessions - if ($webDriver->webDriver != null) { - $webDriver->webDriver->close(); - $webDriver->webDriver = null; + if ($this->webDriver->webDriver != null) { + $this->webDriver->_restart(); + } else { + $this->webDriver->_initializeSession(); } - - // initialize the webdriver session - $webDriver->_initializeSession(); print("afterBlock"); } catch (\Exception $exception) { print $exception->getMessage(); } PersistedObjectHandler::getInstance()->clearSuiteObjects(); + + $this->closeSession($this->webDriver); + print sprintf(self::$HOOK_EXECUTION_END, "after"); } } -} + + /** + * Close session method closes current session. + * If config 'close_all_sessions' is set to 'true' all sessions will be closed. + * + * return void + */ + private function closeSession(): void + { + $webDriverConfig = $this->webDriver->_getConfig(); + $this->webDriver->_closeSession(); + if (isset($webDriverConfig['close_all_sessions']) && $webDriverConfig['close_all_sessions'] === "true") { + $wdHost = sprintf( + '%s://%s:%s%s', + $webDriverConfig['protocol'], + $webDriverConfig['host'], + $webDriverConfig['port'], + $webDriverConfig['path'] + ); + $availableSessions = RemoteWebDriver::getAllSessions($wdHost); + foreach ($availableSessions as $session) { + try { + $chromeOptions = new ChromeOptions(); + $capabilities = DesiredCapabilities::chrome(); + $remoteWebDriver = RemoteWebDriver::createBySessionID($session['id'], $wdHost, + $webDriverConfig['connection_timeout'], $webDriverConfig['request_timeout'], true, $capabilities); + $remoteWebDriver->quit(); + } catch (\Exception $exception) { + print("Failed trying to quit WebDriver session. Exception message: " . $exception->getMessage() . " Test execution will continue." . PHP_EOL); + // Session already closed so nothing to do + } + } + } + } + + /** + * Return the module for an action. + * + * @param string $action + * @return Module + * @throws \Exception + */ + private function getModuleForAction($action) + { + $module = $this->moduleContainer->moduleForAction($action); + if ($module === null) { + throw new TestFrameworkException('Invalid action "' . $action . '"' . PHP_EOL); + } + return $module; + } + + /** + * Counts how many tests in group. + * + * @return integer + */ + private function getTestCount() + { + $config = $this->getGlobalConfig(); + if (empty($config['groups']) || empty($config['groups'][self::$group])) { + return $this->testCount; + } + $pathToGroupDir = TESTS_BP . DIRECTORY_SEPARATOR . array_first($config['groups'][self::$group]); + $pathToGroupCests = $pathToGroupDir . "*Cest.php"; + + $files = glob($pathToGroupCests); + if (is_array($files)) { + $qty = count($files); + print('In a group "' . self::$group . '" suite executor found ' . $qty . ' tests.' . PHP_EOL); + return $qty; + } + + return $this->testCount; + } +} \ No newline at end of file diff --git a/dev/tests/verification/TestModule/ActionGroup/BasicActionGroup/ActionGroupReturningValueActionGroup.xml b/dev/tests/verification/TestModule/ActionGroup/BasicActionGroup/ActionGroupReturningValueActionGroup.xml new file mode 100644 index 000000000..ddb93c0c1 --- /dev/null +++ b/dev/tests/verification/TestModule/ActionGroup/BasicActionGroup/ActionGroupReturningValueActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="ActionGroupReturningValueActionGroup"> + <arguments> + <argument name="count" type="string"/> + </arguments> + <grabMultiple selector="selector" stepKey="grabProducts1"/> + <assertCount stepKey="assertCount"> + <expectedResult type="int">{{count}}</expectedResult> + <actualResult type="variable">grabProducts1</actualResult> + </assertCount> + <return value="{$grabProducts1}" stepKey="returnProducts1"/> + </actionGroup> +</actionGroups> diff --git a/dev/tests/verification/TestModule/ActionGroup/BasicActionGroup/ActionGroupWithCreateDataActionGroup.xml b/dev/tests/verification/TestModule/ActionGroup/BasicActionGroup/ActionGroupWithCreateDataActionGroup.xml index 9f3a11a4f..7bf83c230 100644 --- a/dev/tests/verification/TestModule/ActionGroup/BasicActionGroup/ActionGroupWithCreateDataActionGroup.xml +++ b/dev/tests/verification/TestModule/ActionGroup/BasicActionGroup/ActionGroupWithCreateDataActionGroup.xml @@ -8,8 +8,7 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="actionGroupWithCreateData"> - <createData entity="ApiCategory" stepKey="createCategory"/> - <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <createData entity="TestData" stepKey="createConfigProduct"> <requiredEntity createDataKey="createCategory"/> </createData> </actionGroup> diff --git a/dev/tests/verification/TestModule/ActionGroup/BasicActionGroup/ExtendedActionGroupReturningValueActionGroup.xml b/dev/tests/verification/TestModule/ActionGroup/BasicActionGroup/ExtendedActionGroupReturningValueActionGroup.xml new file mode 100644 index 000000000..18933a2f2 --- /dev/null +++ b/dev/tests/verification/TestModule/ActionGroup/BasicActionGroup/ExtendedActionGroupReturningValueActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="ExtendedActionGroupReturningValueActionGroup" extends="ActionGroupReturningValueActionGroup"> + <arguments> + <argument name="otherCount" type="string"/> + </arguments> + <grabMultiple selector="otherSelector" stepKey="grabProducts2"/> + <assertCount stepKey="assertSecondCount"> + <expectedResult type="int">{{otherCount}}</expectedResult> + <actualResult type="variable">grabProducts2</actualResult> + </assertCount> + <return value="{$grabProducts2}" stepKey="returnProducts2"/> + </actionGroup> +</actionGroups> diff --git a/dev/tests/verification/TestModule/ActionGroup/FunctionalActionGroup/FunctionActionGroupWithStepKeyReferencesActionGroup.xml b/dev/tests/verification/TestModule/ActionGroup/FunctionalActionGroup/FunctionActionGroupWithStepKeyReferencesActionGroup.xml index 8d36773e8..a9275c23b 100644 --- a/dev/tests/verification/TestModule/ActionGroup/FunctionalActionGroup/FunctionActionGroupWithStepKeyReferencesActionGroup.xml +++ b/dev/tests/verification/TestModule/ActionGroup/FunctionalActionGroup/FunctionActionGroupWithStepKeyReferencesActionGroup.xml @@ -8,7 +8,7 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="FunctionActionGroupWithStepKeyReferences"> - <createData entity="simpleData" stepKey="createSimpleData"/> + <createData entity="TestData" stepKey="createSimpleData"/> <grabTextFrom selector=".class" stepKey="grabTextData"/> <fillField stepKey="fill1" selector=".{$grabTextData}" userInput="$createSimpleData.field$"/> <comment userInput="Invocation stepKey will not be appended in non stepKey instances" stepKey="comment1"/> @@ -18,16 +18,16 @@ <executeJS function="{$action3}" stepKey="action3"/> <magentoCLI command="{$action4}" arguments=""stuffHere"" stepKey="action4"/> <generateDate date="{$action5}" format="H:i:s" stepKey="action5"/> - <formatMoney userInput="{$action6}" stepKey="action6"/> + <formatCurrency userInput="{$action6}" locale="en_CA" currency="USD" stepKey="action6"/> <deleteData createDataKey="{$action7}" stepKey="action7"/> <getData entity="{$action8}" stepKey="action8"/> <updateData entity="{$action9}" stepKey="action9" createDataKey="1"/> - <createData entity="{$action10}" stepKey="action10"/> <grabAttributeFrom selector="{$action11}" userInput="someInput" stepKey="action11"/> <grabCookie userInput="{$action12}" parameterArray="['domain' => 'www.google.com']" stepKey="action12"/> <grabFromCurrentUrl regex="{$action13}" stepKey="action13"/> <grabMultiple selector="{$action14}" stepKey="action14"/> <grabTextFrom selector="{$action15}" stepKey="action15"/> <grabValueFrom selector="{$action16}" stepKey="action16"/> + <grabCookieAttributes userInput="{$action17}" parameterArray="['domain' => 'www.google.com']" stepKey="action17"/> </actionGroup> </actionGroups> diff --git a/dev/tests/verification/TestModule/ActionGroup/FunctionalActionGroup/FunctionalActionGroupWithReturnValueActionGroup.xml b/dev/tests/verification/TestModule/ActionGroup/FunctionalActionGroup/FunctionalActionGroupWithReturnValueActionGroup.xml new file mode 100644 index 000000000..77900efd3 --- /dev/null +++ b/dev/tests/verification/TestModule/ActionGroup/FunctionalActionGroup/FunctionalActionGroupWithReturnValueActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="FunctionalActionGroupWithReturnValueActionGroup"> + <grabTextFrom selector="#foo" stepKey="grabTextFrom1"/> + <return value="{$grabTextFrom1}" stepKey="return"/> + </actionGroup> +</actionGroups> diff --git a/dev/tests/verification/TestModule/ActionGroup/FunctionalActionGroup/MergeActionGroupReturningValueActionGroup.xml b/dev/tests/verification/TestModule/ActionGroup/FunctionalActionGroup/MergeActionGroupReturningValueActionGroup.xml new file mode 100644 index 000000000..83afbce0c --- /dev/null +++ b/dev/tests/verification/TestModule/ActionGroup/FunctionalActionGroup/MergeActionGroupReturningValueActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="MergeActionGroupReturningValueActionGroup"> + <arguments> + <argument name="myArg"/> + </arguments> + <fillField userInput="Please delete me" selector="#delete" stepKey="deleteMe"/> + <amOnPage url="{{SamplePage.url(myArg.firstname,myArg.lastname)}}" stepKey="amOnPage1"/> + <grabMultiple selector="#foo" stepKey="grabMultiple1"/> + <return value="{$grabMultiple1}" stepKey="returnValue"/> + </actionGroup> +</actionGroups> diff --git a/dev/tests/verification/TestModule/ActionGroup/MergeFunctionalActionGroup/MergeActionGroupReturningValueActionGroup.xml b/dev/tests/verification/TestModule/ActionGroup/MergeFunctionalActionGroup/MergeActionGroupReturningValueActionGroup.xml new file mode 100644 index 000000000..d952fdddf --- /dev/null +++ b/dev/tests/verification/TestModule/ActionGroup/MergeFunctionalActionGroup/MergeActionGroupReturningValueActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="MergeActionGroupReturningValueActionGroup"> + <click stepKey="myMergedClick" selector=".merge .{{myArg.firstname}}" after="amOnPage1"/> + <remove keyForRemoval="deleteMe"/> + </actionGroup> +</actionGroups> diff --git a/dev/tests/verification/TestModule/ActionGroup/PersistenceActionGroup/DataPersistenceAppendingActionGroup.xml b/dev/tests/verification/TestModule/ActionGroup/PersistenceActionGroup/DataPersistenceAppendingActionGroup.xml index e5ee7ce4f..2fa5cb0aa 100644 --- a/dev/tests/verification/TestModule/ActionGroup/PersistenceActionGroup/DataPersistenceAppendingActionGroup.xml +++ b/dev/tests/verification/TestModule/ActionGroup/PersistenceActionGroup/DataPersistenceAppendingActionGroup.xml @@ -8,7 +8,7 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="DataPersistenceAppendingActionGroup"> - <createData entity="entity" stepKey="createData"/> + <createData entity="DefaultPerson" stepKey="createData"/> <updateData entity="newEntity" createDataKey="createData" stepKey="updateData"/> <deleteData createDataKey="createData" stepKey="deleteData"/> <getData entity="someEneity" stepKey="getData"/> diff --git a/dev/tests/verification/TestModule/ActionGroup/PersistenceActionGroup/PersistenceActionGroup.xml b/dev/tests/verification/TestModule/ActionGroup/PersistenceActionGroup/PersistenceActionGroup.xml index c164e13c8..2a7b4873d 100644 --- a/dev/tests/verification/TestModule/ActionGroup/PersistenceActionGroup/PersistenceActionGroup.xml +++ b/dev/tests/verification/TestModule/ActionGroup/PersistenceActionGroup/PersistenceActionGroup.xml @@ -13,13 +13,13 @@ <argument name="arg2"/> <argument name="arg3"/> </arguments> - <createData entity="simpleData" stepKey="createDataAG1"> + <createData entity="DefaultPerson" stepKey="createDataAG1"> <field key="firstname">{{arg1}}</field> </createData> - <createData entity="simpleData" stepKey="createDataAG2"> + <createData entity="DefaultPerson" stepKey="createDataAG2"> <field key="firstname">{{arg2}}</field> </createData> - <createData entity="simpleData" stepKey="createDataAG3"> + <createData entity="DefaultPerson" stepKey="createDataAG3"> <field key="firstname">{{arg3}}</field> </createData> </actionGroup> diff --git a/dev/tests/verification/TestModule/ActionGroup/XmlDuplicateActionGroup.xml b/dev/tests/verification/TestModule/ActionGroup/XmlDuplicateActionGroup.xml index 90f7090b0..14cbd836f 100644 --- a/dev/tests/verification/TestModule/ActionGroup/XmlDuplicateActionGroup.xml +++ b/dev/tests/verification/TestModule/ActionGroup/XmlDuplicateActionGroup.xml @@ -82,14 +82,16 @@ <executeJS function="1" stepKey="execJS2"/> <fillField stepKey="fill1"/> <fillField stepKey="fill21"/> - <formatMoney stepKey="frmtmoney1"/> - <formatMoney stepKey="frmtmoney12"/> + <formatCurrency userInput="1234.567890" locale="de_DE" currency="EUR" stepKey="frmtmoney1"/> + <formatCurrency userInput="1234.567890" locale="de_DE" currency="USD" stepKey="frmtmoney12"/> <getData entity="1" stepKey="getdata1"/> <getData entity="1" stepKey="getdata12"/> <grabAttributeFrom selector="1" stepKey="grabattribute1"/> <grabAttributeFrom selector="1" stepKey="grabattribute12"/> <grabCookie stepKey="grabcookie1"/> <grabCookie stepKey="grabcookie12"/> + <grabCookieAttributes stepKey="grabcookieattributes1"/> + <grabCookieAttributes stepKey="grabcookieattributes12"/> <grabFromCurrentUrl stepKey="grabfromcurl1"/> <grabFromCurrentUrl stepKey="grabfromcurl12"/> <grabMultiple selector="1" stepKey="grabmulti1"/> @@ -122,8 +124,8 @@ <openNewTab stepKey="newtab12"/> <parseFloat stepKey="parsefloat1"/> <parseFloat stepKey="parsefloat12"/> - <pauseExecution stepKey="pause1"/> - <pauseExecution stepKey="pause12"/> + <pause stepKey="pause1"/> + <pause stepKey="pause12"/> <pressKey selector="1" stepKey="press1"/> <pressKey selector="1" stepKey="press12"/> <reloadPage stepKey="reload1"/> diff --git a/dev/tests/verification/TestModule/Data/DefaultPerson.xml b/dev/tests/verification/TestModule/Data/DefaultPerson.xml new file mode 100644 index 000000000..d513b1fe1 --- /dev/null +++ b/dev/tests/verification/TestModule/Data/DefaultPerson.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="DefaultPerson" deprecated="Default Person"> + <data key="firstname">test</data> + <data key="lastname"> bar</data> + </entity> +</entities> diff --git a/dev/tests/verification/TestModule/Data/PersistedReplacementData.xml b/dev/tests/verification/TestModule/Data/PersistedReplacementData.xml index 514b5eec6..7213fa6b9 100644 --- a/dev/tests/verification/TestModule/Data/PersistedReplacementData.xml +++ b/dev/tests/verification/TestModule/Data/PersistedReplacementData.xml @@ -22,4 +22,8 @@ <data key="lastName">Dane</data> <data key="mergedField">unmerged</data> </entity> + <entity name="SecretData" type="SecretData"> + <data key="key1">some/data</data> + <data key="key2">{{_CREDS.magento/some/secret}}</data> + </entity> </entities> diff --git a/dev/tests/verification/TestModule/Data/TestData.xml b/dev/tests/verification/TestModule/Data/TestData.xml new file mode 100644 index 000000000..bbd9bdaf6 --- /dev/null +++ b/dev/tests/verification/TestModule/Data/TestData.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="TestData" deprecated="Test Data"> + <data key="field">test</data> + </entity> +</entities> diff --git a/dev/tests/verification/TestModule/Data/UniquePerson.xml b/dev/tests/verification/TestModule/Data/UniquePerson.xml new file mode 100644 index 000000000..5027b1d2a --- /dev/null +++ b/dev/tests/verification/TestModule/Data/UniquePerson.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="UniquePerson" deprecated="Unique Person"> + <requiredEntity type="createdData">Qty_1000</requiredEntity> + <data key="firstname">test</data> + <data key="lastname"> bar</data> + </entity> +</entities> diff --git a/dev/tests/verification/TestModule/Data/entity1.xml b/dev/tests/verification/TestModule/Data/entity1.xml new file mode 100644 index 000000000..c000223c2 --- /dev/null +++ b/dev/tests/verification/TestModule/Data/entity1.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="entity1" deprecated="Entity one"> + </entity> +</entities> diff --git a/dev/tests/verification/TestModule/Data/entity2.xml b/dev/tests/verification/TestModule/Data/entity2.xml new file mode 100644 index 000000000..0e1455f2d --- /dev/null +++ b/dev/tests/verification/TestModule/Data/entity2.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="entity2" > + </entity> +</entities> diff --git a/dev/tests/verification/TestModule/Suite/ActionsInDifferentModulesSuite.xml b/dev/tests/verification/TestModule/Suite/ActionsInDifferentModulesSuite.xml new file mode 100644 index 000000000..e3553308d --- /dev/null +++ b/dev/tests/verification/TestModule/Suite/ActionsInDifferentModulesSuite.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Suite/etc/suiteSchema.xsd"> + <suite name="ActionsInDifferentModulesSuite"> + <include> + <test name="IncludeActionsInDifferentModulesTest"/> + </include> + <before> + <magentoCLI command="{{SecretData.key2}}" stepKey="cli"/> + <createData entity="SecretData" stepKey="create1"> + <field key="someKey">dataHere</field> + </createData> + <createData entity="SecretData" stepKey="create2"/> + <createData entity="SecretData" stepKey="create3"> + <requiredEntity createDataKey="create1"/> + <requiredEntity createDataKey="create2"/> + </createData> + <fillField selector="#fill" userInput="{{SecretData.key2}}+{{SecretData.key2}}" stepKey="fillBefore"/> + <click userInput="$create2.key2$" stepKey="click"/> + <actionGroup ref="ActionGroupReturningValueActionGroup" stepKey="return1"> + <argument name="count" value="1"/> + </actionGroup> + </before> + <after> + <actionGroup ref="ExtendedActionGroupReturningValueActionGroup" stepKey="return2"> + <argument name="count" value="1"/> + <argument name="otherCount" value="2"/> + </actionGroup> + <deleteData createDataKey="create1" stepKey="delete1"/> + <deleteData createDataKey="create2" stepKey="delete2"/> + <deleteData createDataKey="create3" stepKey="delete3"/> + <deleteData url="deleteThis" stepKey="deleteThis"/> + <fillField selector="#fill" userInput="{{SecretData.key2}}" stepKey="fillAfter"/> + <magentoCLI command="{{SecretData.key2}}-{{SecretData.key1}}-{{SecretData.key2}}" stepKey="cli2"/> + </after> + </suite> +</suites> diff --git a/dev/tests/verification/TestModule/Suite/functionalSuiteHooks.xml b/dev/tests/verification/TestModule/Suite/functionalSuiteHooks.xml index 4fc459e7d..b4dca7f8d 100644 --- a/dev/tests/verification/TestModule/Suite/functionalSuiteHooks.xml +++ b/dev/tests/verification/TestModule/Suite/functionalSuiteHooks.xml @@ -13,10 +13,18 @@ </include> <before> <amOnPage url="some.url" stepKey="before"/> - <createData entity="createThis" stepKey="create"> + <createData entity="createEntityOne" stepKey="createOne"> <field key="someKey">dataHere</field> </createData> - <click stepKey="clickWithData" userInput="$create.data$"/> + <createData entity="createEntityTwo" stepKey="createTwo"> + <requiredEntity createDataKey="createEntityOne"/> + </createData> + <createData entity="createEntityThree" stepKey="createThree"/> + <createData entity="createEntityFour" stepKey="createFour"> + <requiredEntity createDataKey="createEntityTwo"/> + <requiredEntity createDataKey="createEntityThree"/> + </createData> + <click stepKey="clickWithData" userInput="$createTwo.data$"/> <actionGroup ref="actionGroupWithTwoArguments" stepKey="AC"> <argument name="somePerson" value="simpleData"/> <argument name="anotherPerson" value="uniqueData"/> diff --git a/dev/tests/verification/TestModule/Test/ActionGroupFunctionalTest/ActionGroupReturningValueTest.xml b/dev/tests/verification/TestModule/Test/ActionGroupFunctionalTest/ActionGroupReturningValueTest.xml new file mode 100644 index 000000000..ade1f51db --- /dev/null +++ b/dev/tests/verification/TestModule/Test/ActionGroupFunctionalTest/ActionGroupReturningValueTest.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="ActionGroupReturningValueTest"> + <annotations> + <severity value="CRITICAL"/> + <group value="functional"/> + <features value="Action Group Functional Cest"/> + <stories value="MQE-433"/> + </annotations> + <before> + <createData entity="ReplacementPerson" stepKey="createPersonParam"/> + <actionGroup ref="FunctionalActionGroup" stepKey="beforeGroup"/> + </before> + <amOnPage url="/someUrl" stepKey="step1"/> + <actionGroup ref="FunctionalActionGroupWithReturnValueActionGroup" stepKey="actionGroupWithReturnValue1"/> + <actionGroup ref="actionGroupWithStringUsage" stepKey="actionGroupWithStringUsage1"> + <argument name="someArgument" value="{$actionGroupWithReturnValue1}"/> + </actionGroup> + <after> + <actionGroup ref="FunctionalActionGroup" stepKey="afterGroup"/> + </after> + </test> +</tests> diff --git a/dev/tests/verification/TestModule/Test/ActionGroupTest/ExtendedActionGroupReturningValueTest.xml b/dev/tests/verification/TestModule/Test/ActionGroupTest/ExtendedActionGroupReturningValueTest.xml new file mode 100644 index 000000000..68042344c --- /dev/null +++ b/dev/tests/verification/TestModule/Test/ActionGroupTest/ExtendedActionGroupReturningValueTest.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="ExtendedActionGroupReturningValueTest"> + <annotations> + <severity value="CRITICAL"/> + <title value="Extended ActionGroup Returning Value Test"/> + </annotations> + <actionGroup ref="ActionGroupReturningValueActionGroup" stepKey="actionGroupReturningValue"> + <argument name="count" value="99"/> + </actionGroup> + <actionGroup ref="actionGroupWithStringUsage" stepKey="actionGroupWithStringUsage1"> + <argument name="someArgument" value="{$actionGroupReturningValue}"/> + </actionGroup> + </test> +</tests> diff --git a/dev/tests/verification/TestModule/Test/ActionGroupTest/ExtendedChildActionGroupReturningValueTest.xml b/dev/tests/verification/TestModule/Test/ActionGroupTest/ExtendedChildActionGroupReturningValueTest.xml new file mode 100644 index 000000000..ecd46c3c4 --- /dev/null +++ b/dev/tests/verification/TestModule/Test/ActionGroupTest/ExtendedChildActionGroupReturningValueTest.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="ExtendedChildActionGroupReturningValueTest"> + <annotations> + <severity value="CRITICAL"/> + <title value="Extended Child ActionGroup Returning Value Test"/> + </annotations> + <actionGroup ref="ExtendedActionGroupReturningValueActionGroup" stepKey="extendedActionGroupReturningValue"> + <argument name="count" value="99"/> + <argument name="otherCount" value="8000"/> + </actionGroup> + <actionGroup ref="actionGroupWithStringUsage" stepKey="actionGroupWithStringUsage1"> + <argument name="someArgument" value="{$extendedActionGroupReturningValue}"/> + </actionGroup> + </test> +</tests> diff --git a/dev/tests/verification/TestModule/Test/AssertTest.xml b/dev/tests/verification/TestModule/Test/AssertTest.xml index d17b60fae..03d9ca0c7 100644 --- a/dev/tests/verification/TestModule/Test/AssertTest.xml +++ b/dev/tests/verification/TestModule/Test/AssertTest.xml @@ -31,14 +31,18 @@ <expectedResult type="string">kiwi</expectedResult> <actualResult type="const">['orange' => 2, 'apple' => 1]</actualResult> </assertArrayNotHasKey> - <assertArraySubset stepKey="assertArraySubset" message="pass"> - <expectedResult type="const">[1, 2]</expectedResult> - <actualResult type="const">[1, 2, 3, 5]</actualResult> - </assertArraySubset> <assertContains stepKey="assertContains" message="pass"> <expectedResult type="string">ab</expectedResult> <actualResult type="const">['item1' => 'a', 'item2' => 'ab']</actualResult> </assertContains> + <assertStringContainsString stepKey="assertStringContainsString" message="pass"> + <expectedResult type="string">apple</expectedResult> + <actualResult type="string">apple</actualResult> + </assertStringContainsString> + <assertStringContainsStringIgnoringCase stepKey="assertStringContainsStringIgnoringCase" message="pass"> + <expectedResult type="string">Banana</expectedResult> + <actualResult type="string">banana</actualResult> + </assertStringContainsStringIgnoringCase> <assertCount stepKey="assertCount" message="pass"> <expectedResult type="int">2</expectedResult> <actualResult type="const">['a', 'b']</actualResult> @@ -79,18 +83,6 @@ <expectedResult type="int">2</expectedResult> <actualResult type="int">5</actualResult> </assertGreaterThanOrEqual> - <assertInternalType stepKey="assertInternalType1" message="pass"> - <expectedResult type="string">string</expectedResult> - <actualResult type="string">xyz</actualResult> - </assertInternalType> - <assertInternalType stepKey="assertInternalType2" message="pass"> - <expectedResult type="string">int</expectedResult> - <actualResult type="int">21</actualResult> - </assertInternalType> - <assertInternalType stepKey="assertInternalType3" message="pass"> - <expectedResult type="string">string</expectedResult> - <actualResult type="variable">text</actualResult> - </assertInternalType> <assertLessOrEquals stepKey="assertLessOrEquals" message="pass"> <expectedResult type="int">5</expectedResult> <actualResult type="int">2</actualResult> @@ -103,21 +95,25 @@ <expectedResult type="int">5</expectedResult> <actualResult type="int">2</actualResult> </assertLessThanOrEqual> - <assertNotContains stepKey="assertNotContains1" message="pass"> + <assertNotContains stepKey="assertNotContains" message="pass"> <expectedResult type="string">bc</expectedResult> <actualResult type="const">['item1' => 'a', 'item2' => 'ab']</actualResult> </assertNotContains> - <assertNotContains stepKey="assertNotContains2" message="pass"> - <expectedResult type="string">bc</expectedResult> - <actualResult type="variable">text</actualResult> - </assertNotContains> + <assertStringNotContainsString stepKey="assertStringNotContainsString" message="pass"> + <expectedResult type="string">apple</expectedResult> + <actualResult type="string">banana</actualResult> + </assertStringNotContainsString> + <assertStringNotContainsStringIgnoringCase stepKey="assertStringNotContainsStringIgnoringCase" message="pass"> + <expectedResult type="string">apple</expectedResult> + <actualResult type="string">banana</actualResult> + </assertStringNotContainsStringIgnoringCase> <assertNotEmpty stepKey="assertNotEmpty1" message="pass"> <actualResult type="const">[1, 2]</actualResult> </assertNotEmpty> <assertNotEmpty stepKey="assertNotEmpty2" message="pass"> <actualResult type="variable">text</actualResult> </assertNotEmpty> - <assertNotEquals stepKey="assertNotEquals" message="pass" delta=""> + <assertNotEquals stepKey="assertNotEquals" message="pass"> <expectedResult type="int">2</expectedResult> <actualResult type="int">5</actualResult> </assertNotEquals> @@ -165,10 +161,6 @@ <expectedResult type="string">kiwi</expectedResult> <actualResult type="const">['orange' => 2, 'apple' => 1]</actualResult> </assertArrayNotHasKey> - <assertArraySubset stepKey="assertArraySubsetBackwardCompatible" message="pass"> - <actualResult type="const">[1, 2, 3, 5]</actualResult> - <expectedResult type="const">[1, 2]</expectedResult> - </assertArraySubset> <assertContains stepKey="assertContainsBackwardCompatible" message="pass"> <expectedResult type="string">ab</expectedResult> <actualResult type="const">['item1' => 'a', 'item2' => 'ab']</actualResult> @@ -209,18 +201,6 @@ <actualResult type="int">5</actualResult> <expectedResult type="int">2</expectedResult> </assertGreaterThanOrEqual> - <assertInternalType stepKey="assertInternalType1BackwardCompatible" message="pass"> - <actualResult type="string">xyz</actualResult> - <expectedResult type="string">string</expectedResult> - </assertInternalType> - <assertInternalType stepKey="assertInternalType2BackwardCompatible" message="pass"> - <actualResult type="int">21</actualResult> - <expectedResult type="string">int</expectedResult> - </assertInternalType> - <assertInternalType stepKey="assertInternalType3BackwardCompatible" message="pass"> - <actualResult type="variable">$text</actualResult> - <expectedResult type="string">string</expectedResult> - </assertInternalType> <assertLessOrEquals stepKey="assertLessOrEqualBackwardCompatibles" message="pass"> <actualResult type="int">2</actualResult> <expectedResult type="int">5</expectedResult> @@ -247,7 +227,7 @@ <assertNotEmpty stepKey="assertNotEmpty2BackwardCompatible" message="pass"> <actualResult type="variable">text</actualResult> </assertNotEmpty> - <assertNotEquals stepKey="assertNotEqualsBackwardCompatible" message="pass" delta=""> + <assertNotEquals stepKey="assertNotEqualsBackwardCompatible" message="pass"> <actualResult type="int">5</actualResult> <expectedResult type="int">2</expectedResult> </assertNotEquals> @@ -326,14 +306,6 @@ <!-- array type that use created data --> <comment stepKey="c3" userInput="array type that use created data"/> - <assertArraySubset stepKey="assert9" message="pass"> - <expectedResult type="array">[$$createData1.lastname$$, $$createData1.firstname$$]</expectedResult> - <actualResult type="array">[$$createData1.lastname$$, $$createData1.firstname$$, 1]</actualResult> - </assertArraySubset> - <assertArraySubset stepKey="assert10" message="pass"> - <expectedResult type="array">[$createData2.firstname$, $createData2.lastname$]</expectedResult> - <actualResult type="array">[$createData2.firstname$, $createData2.lastname$, 1]</actualResult> - </assertArraySubset> <assertArrayHasKey stepKey="assert3" message="pass"> <expectedResult type="string">lastname</expectedResult> <actualResult type="array">['lastname' => $$createData1.lastname$$, 'firstname' => $$createData1.firstname$$]</actualResult> @@ -403,5 +375,58 @@ <expectedResult type="string">{{simpleData.firstname}}</expectedResult> <actualResult type="string">{{simpleData.lastname}}</actualResult> </assertEquals> + + <assertEqualsWithDelta stepKey="a1" message="pass" delta="1"> + <expectedResult type="const">10.0000</expectedResult> + <actualResult type="const">10.0000</actualResult> + </assertEqualsWithDelta> + <assertNotEqualsWithDelta stepKey="a2" message="pass" delta="1"> + <expectedResult type="const">10.0000</expectedResult> + <actualResult type="const">12.0000</actualResult> + </assertNotEqualsWithDelta> + <assertEqualsCanonicalizing stepKey="a3" message="pass"> + <expectedResult type="array">[4, 2, 1, 3]</expectedResult> + <actualResult type="array">[1, 2, 3, 4]</actualResult> + </assertEqualsCanonicalizing> + <assertNotEqualsCanonicalizing stepKey="a4" message="pass"> + <expectedResult type="array">[5, 8, 7, 9]</expectedResult> + <actualResult type="array">[1, 2, 3, 4]</actualResult> + </assertNotEqualsCanonicalizing> + <assertEqualsIgnoringCase stepKey="a5" message="pass"> + <expectedResult type="string">Cat</expectedResult> + <actualResult type="string">cat</actualResult> + </assertEqualsIgnoringCase> + <assertNotEqualsIgnoringCase stepKey="a6" message="pass"> + <expectedResult type="string">Cat</expectedResult> + <actualResult type="string">Dog</actualResult> + </assertNotEqualsIgnoringCase> + + <!-- assertions.md examples --> + <assertElementContainsAttribute stepKey="assertElementContainsAttribute"> + <expectedResult selector=".admin__menu-overlay" attribute="style" type="string">color: #333;</expectedResult> + </assertElementContainsAttribute> + <assertStringContainsString stepKey="assertDropDownTierPriceTextProduct1"> + <expectedResult type="string">Buy 5 for $5.00 each and save 50%</expectedResult> + <actualResult type="variable">DropDownTierPriceTextProduct1</actualResult> + </assertStringContainsString> + <assertEmpty stepKey="assertSearchButtonEnabled"> + <actualResult type="string">$grabSearchButtonAttribute</actualResult> + </assertEmpty> + <assertGreaterThanOrEqual stepKey="checkStatusSortOrderAsc"> + <actualResult type="const">$getOrderStatusSecondRow</actualResult> + <expectedResult type="const">$getOrderStatusFirstRow</expectedResult> + </assertGreaterThanOrEqual> + <assertNotEquals stepKey="assertNotEqualsExample"> + <actualResult type="string">{$grabTotalAfter}</actualResult> + <expectedResult type="string">{$grabTotalBefore}</expectedResult> + </assertNotEquals> + <assertNotRegExp stepKey="simpleThumbnailIsNotDefault"> + <actualResult type="const">$getSimpleProductThumbnail</actualResult> + <expectedResult type="const">'/placeholder\/thumbnail\.jpg/'</expectedResult> + </assertNotRegExp> + <assertRegExp message="adminAnalyticsMetadata object is invalid" stepKey="validateadminAnalyticsMetadata"> + <expectedResult type="string">#var\s+adminAnalyticsMetadata\s+=\s+{\s+("[\w_]+":\s+"[^"]*?",\s+)*?("[\w_]+":\s+"[^"]*?"\s+)};#s</expectedResult> + <actualResult type="variable">$pageSource</actualResult> + </assertRegExp> </test> </tests> diff --git a/dev/tests/verification/TestModule/Test/BasicFunctionalTest/BasicFunctionalTest.xml b/dev/tests/verification/TestModule/Test/BasicFunctionalTest/BasicFunctionalTest.xml index ea2a48e9e..fa2b5e247 100644 --- a/dev/tests/verification/TestModule/Test/BasicFunctionalTest/BasicFunctionalTest.xml +++ b/dev/tests/verification/TestModule/Test/BasicFunctionalTest/BasicFunctionalTest.xml @@ -68,8 +68,11 @@ <fillField selector=".functionalTestSelector" userInput="0" stepKey="fillFieldKey2" /> <generateDate date="Now" format="H:i:s" stepKey="generateDateKey"/> <generateDate date="Now" format="H:i:s" stepKey="generateDateKey2" timezone="UTC"/> + <getOTP stepKey="getOtp"/> + <getOTP stepKey="getOtpWithInput" userInput="someInput"/> <grabAttributeFrom selector=".functionalTestSelector" userInput="someInput" stepKey="grabAttributeFromKey1" /> <grabCookie userInput="grabCookieInput" parameterArray="['domain' => 'www.google.com']" stepKey="grabCookieKey1" /> + <grabCookieAttributes userInput="grabCookieAttributesInput" parameterArray="['domain' => 'www.google.com']" stepKey="grabCookieAttributesKey1" /> <grabFromCurrentUrl regex="/grabCurrentUrl" stepKey="grabFromCurrentUrlKey1" /> <grabMultiple selector=".functionalTestSelector" stepKey="grabMultipleKey1" /> <grabTextFrom selector=".functionalTestSelector" stepKey="grabTextFromKey1" /> @@ -87,7 +90,7 @@ <moveForward stepKey="moveForwardKey1"/> <moveMouseOver selector=".functionalTestSelector" stepKey="moveMouseOverKey1"/> <openNewTab stepKey="openNewTabKey1"/> - <pauseExecution stepKey="pauseExecutionKey1"/> + <pause stepKey="pauseKey1"/> <pressKey selector="#page" userInput="a" stepKey="pressKey1"/> <pressKey selector="#page" parameterArray="[['ctrl','a'],'new']" stepKey="pressKey2"/> <pressKey selector="#page" parameterArray="[['shift','111'],'1','x']" stepKey="pressKey3"/> diff --git a/dev/tests/verification/TestModule/Test/BasicFunctionalTest/MagentoCliTest.xml b/dev/tests/verification/TestModule/Test/BasicFunctionalTest/MagentoCliTest.xml new file mode 100644 index 000000000..23914943a --- /dev/null +++ b/dev/tests/verification/TestModule/Test/BasicFunctionalTest/MagentoCliTest.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="MagentoCliTest"> + <magentoCLI command="maintenance:enable" arguments=""stuffHere"" stepKey="magentoCli1"/> + <magentoCLI command="maintenance:enable" arguments=""stuffHere"" timeout="120" stepKey="magentoCli2"/> + <magentoCLI command="config:set somePath {{_CREDS.someKey}}" stepKey="magentoCli3"/> + <magentoCLI command="config:set somePath {{_CREDS.someKey}}" timeout="120" stepKey="magentoCli4"/> + </test> +</tests> diff --git a/dev/tests/verification/TestModule/Test/DataActionsTest.xml b/dev/tests/verification/TestModule/Test/DataActionsTest.xml index b75bc98f8..6f36803ae 100644 --- a/dev/tests/verification/TestModule/Test/DataActionsTest.xml +++ b/dev/tests/verification/TestModule/Test/DataActionsTest.xml @@ -10,16 +10,14 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="DataActionsTest"> <before> - <createData entity="entity" stepKey="createdInBefore"/> <updateData entity="entity" createDataKey="createdInBefore" stepKey="updateInBefore"/> <deleteData createDataKey="createdInBefore" stepKey="deleteInBefore"/> </before> - - <createData entity="entity" stepKey="createdInTest"/> + <waitForElementClickable selector=".functionalTestSelector" time="30" stepKey="waitForElementClickable" /> <updateData entity="entity" createDataKey="createdInTest" stepKey="updateInTest"/> <deleteData createDataKey="createdInTest" stepKey="deleteInTest"/> - <updateData entity="entity" createDataKey="createdInBefore" stepKey="updatedDataOutOfScope"/> <deleteData createDataKey="createdInBefore" stepKey="deleteDataOutOfScope"/> + <rapidClick selector="#functionalTestSelector" count="50" stepKey="rapidClickTest"/> </test> </tests> diff --git a/dev/tests/verification/TestModule/Test/SkippedTest/SkippedTestNoIssues.xml b/dev/tests/verification/TestModule/Test/GroupSkipGenerationTest.xml similarity index 64% rename from dev/tests/verification/TestModule/Test/SkippedTest/SkippedTestNoIssues.xml rename to dev/tests/verification/TestModule/Test/GroupSkipGenerationTest.xml index 02f800536..35cf74d33 100644 --- a/dev/tests/verification/TestModule/Test/SkippedTest/SkippedTestNoIssues.xml +++ b/dev/tests/verification/TestModule/Test/GroupSkipGenerationTest.xml @@ -7,11 +7,11 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="SkippedTestNoIssues"> + <test name="GroupSkipGenerationTest"> <annotations> - <stories value="skippedNo"/> - <title value="skippedNoIssuesTest"/> - <description value=""/> + <stories value="GroupSkipGenerationTestStory"/> + <title value="GroupSkipGenerationTestTitle"/> + <description value="GroupSkipGenerationTestDescription"/> <severity value="AVERAGE"/> <group value="skip"/> </annotations> diff --git a/dev/tests/verification/TestModule/Test/HookActionsTest.xml b/dev/tests/verification/TestModule/Test/HookActionsTest.xml index 0a26256d0..b1350a0a7 100644 --- a/dev/tests/verification/TestModule/Test/HookActionsTest.xml +++ b/dev/tests/verification/TestModule/Test/HookActionsTest.xml @@ -10,12 +10,8 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="HookActionsTest"> <before> - <createData entity="sampleCreatedEntity" stepKey="sampleCreateBefore"/> - <deleteData createDataKey="sampleCreateBefore" stepKey="sampleDeleteBefore"/> - <createData entity="sampleCreatedEntity" stepKey="sampleCreateForAfter"/> </before> <after> - <createData entity="sampleCreatedEntity" stepKey="sampleCreateAfter"/> <deleteData createDataKey="sampleCreateForAfter" stepKey="sampleDeleteAfter"/> </after> </test> diff --git a/dev/tests/verification/TestModule/Test/MergeFunctionalTest/MergedActionGroupReturningValueTest.xml b/dev/tests/verification/TestModule/Test/MergeFunctionalTest/MergedActionGroupReturningValueTest.xml new file mode 100644 index 000000000..baf7c0ded --- /dev/null +++ b/dev/tests/verification/TestModule/Test/MergeFunctionalTest/MergedActionGroupReturningValueTest.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="MergedActionGroupReturningValueTest"> + <annotations> + <severity value="CRITICAL"/> + <group value="functional"/> + <features value="Action Group Functional Cest"/> + <stories value="MQE-433"/> + </annotations> + <before> + <createData entity="ReplacementPerson" stepKey="createPersonParam"/> + <actionGroup ref="FunctionalActionGroup" stepKey="beforeGroup"/> + </before> + <amOnPage url="/someUrl" stepKey="step1"/> + <actionGroup ref="MergeActionGroupReturningValueActionGroup" stepKey="actionGroupWithReturnValue1"> + <argument name="myArg" value="DefaultPerson"/> + </actionGroup> + <actionGroup ref="actionGroupWithStringUsage" stepKey="actionGroupWithStringUsage1"> + <argument name="someArgument" value="{$actionGroupWithReturnValue1}"/> + </actionGroup> + <after> + <actionGroup ref="FunctionalActionGroup" stepKey="afterGroup"/> + </after> + </test> +</tests> diff --git a/dev/tests/verification/TestModule/Test/PersistenceCustomFieldsTest.xml b/dev/tests/verification/TestModule/Test/PersistenceCustomFieldsTest.xml index 77a85060b..867dd35ff 100644 --- a/dev/tests/verification/TestModule/Test/PersistenceCustomFieldsTest.xml +++ b/dev/tests/verification/TestModule/Test/PersistenceCustomFieldsTest.xml @@ -12,27 +12,13 @@ <before> <createData entity="DefaultPerson" stepKey="createData1"> <field key="firstname">Mac</field> - <field key="lastname">{{simpleData.lastname}}</field> - </createData> - <createData entity="uniqueData" stepKey="createData2"> - <requiredEntity createDataKey="createData1"/> - <field key="firstname">$$createData1.firstname$$</field> + <field key="lastname">Bar</field> </createData> </before> - <createData entity="simpleData" stepKey="createdData"> - <field key="favoriteIndex">1</field> - <field key="middlename">Kovacs</field> - </createData> <createData entity="UniquePerson" stepKey="createdData3"> <requiredEntity createDataKey="createdData"/> <field key="firstname">Takeshi</field> <field key="lastname">Kovacs</field> </createData> - <actionGroup ref="PersistenceActionGroup" stepKey="createdAG"> - <argument name="arg1" value="string1"/> - <argument name="arg2" value="DefaultPerson.firstname"/> - <argument name="arg3" value="$createdData3.firstname$"/> - </actionGroup> - <actionGroup ref="DataPersistenceSelfReferenceActionGroup" stepKey="AGKEY"/> </test> </tests> diff --git a/dev/tests/verification/TestModule/Test/SampleSuiteTest/IncludeActionsInDifferentModulesTest.xml b/dev/tests/verification/TestModule/Test/SampleSuiteTest/IncludeActionsInDifferentModulesTest.xml new file mode 100644 index 000000000..1be662bbe --- /dev/null +++ b/dev/tests/verification/TestModule/Test/SampleSuiteTest/IncludeActionsInDifferentModulesTest.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="IncludeActionsInDifferentModulesTest"> + <createData entity="SecretData" stepKey="create"> + <field key="someKey">dataHere</field> + </createData> + <fillField selector="#fill" userInput="{{SecretData.key2}}+$create.key2$" stepKey="fill"/> + <magentoCLI command="create.key2$" stepKey="cli"/> + <actionGroup ref="ActionGroupReturningValueActionGroup" stepKey="return"> + <argument name="count" value="3"/> + </actionGroup> + </test> +</tests> diff --git a/dev/tests/verification/TestModule/Test/SkippedTest/SkippedTestWithIssueMustGetSkippedWithoutErrorExitCode.xml b/dev/tests/verification/TestModule/Test/SkippedTest/SkippedTestWithIssueMustGetSkippedWithoutErrorExitCode.xml new file mode 100644 index 000000000..b44cd6d81 --- /dev/null +++ b/dev/tests/verification/TestModule/Test/SkippedTest/SkippedTestWithIssueMustGetSkippedWithoutErrorExitCode.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="SkippedTestWithIssueMustGetSkippedWithoutErrorExitCode"> + <annotations> + <stories value="skipped"/> + <title value="skippedTest"/> + <description value=""/> + <severity value="AVERAGE"/> + <skip> + <issueId value="SkippedValue"/> + </skip> + </annotations> + <before> + <createData entity="ReplacementPerson" stepKey="createPersonParam"/> + <actionGroup ref="FunctionalActionGroup" stepKey="beforeGroup"/> + </before> + <createData entity="DefaultPerson" stepKey="createPerson"/> + <actionGroup ref="FunctionalActionGroupWithData" stepKey="actionGroupWithPersistedData1"> + <argument name="person" value="$createPerson$"/> + </actionGroup> + <after> + <actionGroup ref="FunctionalActionGroup" stepKey="afterGroup" after ="notFound"/> + </after> + </test> +</tests> diff --git a/dev/tests/verification/TestModule/Test/XmlDuplicateTest/XmlDuplicateTest.xml b/dev/tests/verification/TestModule/Test/XmlDuplicateTest/XmlDuplicateTest.xml index 6df962ffa..01d7bc88c 100644 --- a/dev/tests/verification/TestModule/Test/XmlDuplicateTest/XmlDuplicateTest.xml +++ b/dev/tests/verification/TestModule/Test/XmlDuplicateTest/XmlDuplicateTest.xml @@ -85,14 +85,16 @@ <executeJS function="1" stepKey="execJS2"/> <fillField stepKey="fill1"/> <fillField stepKey="fill21"/> - <formatMoney stepKey="frmtmoney1"/> - <formatMoney stepKey="frmtmoney12"/> + <formatCurrency userInput="1234.567890" locale="de_DE" currency="EUR" stepKey="frmtmoney1"/> + <formatCurrency userInput="1234.567890" locale="en_US" currency="EUR" stepKey="frmtmoney12"/> <getData entity="1" stepKey="getdata1"/> <getData entity="1" stepKey="getdata12"/> <grabAttributeFrom selector="1" stepKey="grabattribute1"/> <grabAttributeFrom selector="1" stepKey="grabattribute12"/> <grabCookie stepKey="grabcookie1"/> <grabCookie stepKey="grabcookie12"/> + <grabCookieAttributes stepKey="grabcookieattributes1"/> + <grabCookieAttributes stepKey="grabcookieattributes12"/> <grabFromCurrentUrl stepKey="grabfromcurl1"/> <grabFromCurrentUrl stepKey="grabfromcurl12"/> <grabMultiple selector="1" stepKey="grabmulti1"/> @@ -125,8 +127,8 @@ <openNewTab stepKey="newtab12"/> <parseFloat stepKey="parsefloat1"/> <parseFloat stepKey="parsefloat12"/> - <pauseExecution stepKey="pause1"/> - <pauseExecution stepKey="pause12"/> + <pause stepKey="pause1"/> + <pause stepKey="pause12"/> <pressKey selector="1" stepKey="press1"/> <pressKey selector="1" stepKey="press12"/> <reloadPage stepKey="reload1"/> @@ -297,14 +299,16 @@ <executeJS function="1" stepKey="execJS2"/> <fillField stepKey="fill1"/> <fillField stepKey="fill21"/> - <formatMoney stepKey="frmtmoney1"/> - <formatMoney stepKey="frmtmoney12"/> + <formatCurrency userInput="1234.567890" locale="de_DE" currency="EUR" stepKey="frmtmoney1"/> + <formatCurrency userInput="1234.567890" locale="en_US" currency="EUR" stepKey="frmtmoney12"/> <getData entity="1" stepKey="getdata1"/> <getData entity="1" stepKey="getdata12"/> <grabAttributeFrom selector="1" stepKey="grabattribute1"/> <grabAttributeFrom selector="1" stepKey="grabattribute12"/> <grabCookie stepKey="grabcookie1"/> <grabCookie stepKey="grabcookie12"/> + <grabCookieAttributes stepKey="grabcookieattributes1"/> + <grabCookieAttributes stepKey="grabcookieattributes12"/> <grabFromCurrentUrl stepKey="grabfromcurl1"/> <grabFromCurrentUrl stepKey="grabfromcurl12"/> <grabMultiple selector="1" stepKey="grabmulti1"/> @@ -337,8 +341,8 @@ <openNewTab stepKey="newtab12"/> <parseFloat stepKey="parsefloat1"/> <parseFloat stepKey="parsefloat12"/> - <pauseExecution stepKey="pause1"/> - <pauseExecution stepKey="pause12"/> + <pause stepKey="pause1"/> + <pause stepKey="pause12"/> <pressKey selector="1" stepKey="press1"/> <pressKey selector="1" stepKey="press12"/> <reloadPage stepKey="reload1"/> @@ -508,14 +512,16 @@ <executeJS function="1" stepKey="execJS2"/> <fillField stepKey="fill1"/> <fillField stepKey="fill21"/> - <formatMoney stepKey="frmtmoney1"/> - <formatMoney stepKey="frmtmoney12"/> + <formatCurrency userInput="1234.567890" locale="de_DE" currency="EUR" stepKey="frmtmoney1"/> + <formatCurrency userInput="1234.567890" locale="en_US" currency="EUR" stepKey="frmtmoney12"/> <getData entity="1" stepKey="getdata1"/> <getData entity="1" stepKey="getdata12"/> <grabAttributeFrom selector="1" stepKey="grabattribute1"/> <grabAttributeFrom selector="1" stepKey="grabattribute12"/> <grabCookie stepKey="grabcookie1"/> <grabCookie stepKey="grabcookie12"/> + <grabCookieAttributes stepKey="grabcookieattributes1"/> + <grabCookieAttributes stepKey="grabcookieattributes12"/> <grabFromCurrentUrl stepKey="grabfromcurl1"/> <grabFromCurrentUrl stepKey="grabfromcurl12"/> <grabMultiple selector="1" stepKey="grabmulti1"/> @@ -548,8 +554,8 @@ <openNewTab stepKey="newtab12"/> <parseFloat stepKey="parsefloat1"/> <parseFloat stepKey="parsefloat12"/> - <pauseExecution stepKey="pause1"/> - <pauseExecution stepKey="pause12"/> + <pause stepKey="pause1"/> + <pause stepKey="pause12"/> <pressKey selector="1" stepKey="press1"/> <pressKey selector="1" stepKey="press12"/> <reloadPage stepKey="reload1"/> diff --git a/dev/tests/verification/Tests/ActionGroupWithReturnGenerationTest.php b/dev/tests/verification/Tests/ActionGroupWithReturnGenerationTest.php new file mode 100644 index 000000000..e369a797d --- /dev/null +++ b/dev/tests/verification/Tests/ActionGroupWithReturnGenerationTest.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace tests\verification\Tests; + +use tests\util\MftfTestCase; + +class ActionGroupWithReturnGenerationTest extends MftfTestCase +{ + /** + * Test generation of a test referencing an action group that returns a value. + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testActionGroupReturningValue() + { + $this->generateAndCompareTest('ActionGroupReturningValueTest'); + } + /** + * Test generation of a test referencing a merged action group that returns a value. + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testMergedActionGroupReturningValue() + { + $this->generateAndCompareTest('MergedActionGroupReturningValueTest'); + } + /** + * Test generation of a test referencing an extended action group that returns a value. + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testExtendedActionGroupReturningValue() + { + $this->generateAndCompareTest('ExtendedActionGroupReturningValueTest'); + } + /** + * Test generation of a test referencing an extending child action group that returns a value. + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testExtendedChildActionGroupReturningValue() + { + $this->generateAndCompareTest('ExtendedChildActionGroupReturningValueTest'); + } +} diff --git a/dev/tests/verification/Tests/BasicCestGenerationTest.php b/dev/tests/verification/Tests/BasicCestGenerationTest.php index 39802c50d..a900edbd3 100644 --- a/dev/tests/verification/Tests/BasicCestGenerationTest.php +++ b/dev/tests/verification/Tests/BasicCestGenerationTest.php @@ -59,4 +59,17 @@ public function testWithXmlComments() { $this->generateAndCompareTest('XmlCommentedTest'); } + + /** + * Tests magentoCLI and magentoCLISecret commands with env 'MAGENTO_CLI_WAIT_TIMEOUT' set + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testMagentoCli() + { + putenv("MAGENTO_CLI_WAIT_TIMEOUT=45"); + $this->generateAndCompareTest('MagentoCliTest'); + putenv("MAGENTO_CLI_WAIT_TIMEOUT"); + } } diff --git a/dev/tests/verification/Tests/GroupSkipGenerationTest.php b/dev/tests/verification/Tests/GroupSkipGenerationTest.php new file mode 100644 index 000000000..ad0ba6ec0 --- /dev/null +++ b/dev/tests/verification/Tests/GroupSkipGenerationTest.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace tests\verification\Tests; + +use tests\util\MftfTestCase; + +class GroupSkipGenerationTest extends MftfTestCase +{ + /** + * Tests group skip test generation + * + * @throws \Exception + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException + */ + public function testGroupSkipGenerationTest() + { + $this->generateAndCompareTest('GroupSkipGenerationTest'); + } +} diff --git a/dev/tests/verification/Tests/ResilientGenerationTest.php b/dev/tests/verification/Tests/ResilientGenerationTest.php new file mode 100644 index 000000000..9bb2b9479 --- /dev/null +++ b/dev/tests/verification/Tests/ResilientGenerationTest.php @@ -0,0 +1,286 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace tests\verification\Tests; + +use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; +use Magento\FunctionalTestingFramework\Suite\SuiteGenerator; +use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; +use Magento\FunctionalTestingFramework\Util\Filesystem\DirSetupUtil; +use Magento\FunctionalTestingFramework\Util\Manifest\TestManifestFactory; +use Magento\FunctionalTestingFramework\Util\TestGenerator; +use Symfony\Component\Yaml\Yaml; +use tests\unit\Util\TestLoggingUtil; +use tests\util\MftfTestCase; + +class ResilientGenerationTest extends MftfTestCase +{ + const RESOURCES_DIR = TESTS_BP . DIRECTORY_SEPARATOR . 'verification' . DIRECTORY_SEPARATOR . 'Resources'; + const CONFIG_YML_FILE = TESTS_BP . DIRECTORY_SEPARATOR . SuiteGenerator::YAML_CODECEPTION_CONFIG_FILENAME; + const GENERATE_RESULT_DIR = TESTS_BP . + DIRECTORY_SEPARATOR . + "verification" . + DIRECTORY_SEPARATOR . + "_generated" . + DIRECTORY_SEPARATOR; + + /** + * Exception group names and log messages + * + * @var string[][] + */ + private static $exceptionGrpLogs = [ + 'NotGenerateHookBeforeSuite' => [ + '/Suite NotGenerateHookBeforeSuite is not defined in xml or is invalid./' + ], + 'NotGenerateHookAfterSuite' => [ + '/Suite NotGenerateHookAfterSuite is not defined in xml or is invalid./' + ], + 'NotGenerateEmptySuite' => [ + '/Suite NotGenerateEmptySuite is not defined in xml or is invalid./' + ], + ]; + + /** + * Set up for testing + */ + public static function setUpBeforeClass(): void + { + // destroy _generated if it exists + if (file_exists(self::GENERATE_RESULT_DIR)) { + DirSetupUtil::rmdirRecursive(self::GENERATE_RESULT_DIR); + } + } + + public function setUp(): void + { + // copy config yml file to test dir + $fileSystem = new \Symfony\Component\Filesystem\Filesystem(); + $fileSystem->copy( + realpath( + FW_BP + . DIRECTORY_SEPARATOR + . 'etc' + . DIRECTORY_SEPARATOR + . 'config' + . DIRECTORY_SEPARATOR + . 'codeception.dist.yml' + ), + self::CONFIG_YML_FILE + ); + + TestLoggingUtil::getInstance()->setMockLoggingUtil(); + + $property = new \ReflectionProperty(SuiteGenerator::class, "instance"); + $property->setAccessible(true); + $property->setValue(null, null); + + $property = new \ReflectionProperty(DirSetupUtil::class, "DIR_CONTEXT"); + $property->setAccessible(true); + $property->setValue(null, []); + + $property = new \ReflectionProperty(SuiteObjectHandler::class, "instance"); + $property->setAccessible(true); + $property->setValue(null, null); + + $property = new \ReflectionProperty(TestObjectHandler::class, "testObjectHandler"); + $property->setAccessible(true); + $property->setValue(null, null); + } + + /** + * Test resilient generate all tests + */ + public function testGenerateAllTests() + { + $testManifest = TestManifestFactory::makeManifest('default', []); + TestGenerator::getInstance(null, [])->createAllTestFiles($testManifest); + + // Validate tests have been generated + $dirContents = array_diff( + scandir(self::GENERATE_RESULT_DIR . DIRECTORY_SEPARATOR . 'default'), + ['..', '.'] + ); + + foreach ($dirContents as $dirContent) { + $this->assertStringStartsNotWith( + 'NotGenerate', + $dirContent, + "string {$dirContent} should not contains \"NotGenerate\"" + ); + } + } + + /** + * Test resilient generate all suites + */ + public function testGenerateAllSuites() + { + $testManifest = TestManifestFactory::makeManifest('', []); + SuiteGenerator::getInstance()->generateAllSuites($testManifest); + + foreach (SuiteTestReferences::$data as $groupName => $expectedContents) { + if (substr($groupName, 0, 11) !== 'NotGenerate') { + // Validate Yaml file updated + $yml = Yaml::parse(file_get_contents(self::CONFIG_YML_FILE)); + $this->assertArrayHasKey($groupName, $yml['groups']); + + $suiteResultBaseDir = self::GENERATE_RESULT_DIR . + DIRECTORY_SEPARATOR . + $groupName . + DIRECTORY_SEPARATOR; + + // Validate suite and tests have been generated + $dirContents = array_diff(scandir($suiteResultBaseDir), ['..', '.']); + + foreach ($expectedContents as $expectedFile) { + $this->assertTrue(in_array($expectedFile, $dirContents)); + } + } else { + $dirContents = array_diff(scandir(self::GENERATE_RESULT_DIR), ['..', '.']); + + // Validate suite is not generated + $this->assertFalse(in_array($groupName, $dirContents)); + } + } + } + + /** + * Test resilient generate some suites + */ + public function testGenerateSomeSuites() + { + $suites = [ + 'PartialGenerateForIncludeSuite', + 'PartialGenerateNoExcludeSuite', + 'NotGenerateHookBeforeSuite', + 'NotGenerateHookAfterSuite', + ]; + + foreach ($suites as $groupName) { + $expectedContents = SuiteTestReferences::$data[$groupName]; + if (!in_array($groupName, array_keys(self::$exceptionGrpLogs))) { + SuiteGenerator::getInstance()->generateSuite($groupName); + } else { + // Validate exception is thrown + $this->assertExceptionRegex( + \Exception::class, + self::$exceptionGrpLogs[$groupName], + function () use ($groupName) { + SuiteGenerator::getInstance()->generateSuite($groupName); + } + ); + } + + if (substr($groupName, 0, 11) !== 'NotGenerate') { + // Validate Yaml file updated + $yml = Yaml::parse(file_get_contents(self::CONFIG_YML_FILE)); + $this->assertArrayHasKey($groupName, $yml['groups']); + + $suiteResultBaseDir = self::GENERATE_RESULT_DIR . + DIRECTORY_SEPARATOR . + $groupName . + DIRECTORY_SEPARATOR; + + // Validate tests have been generated + $dirContents = array_diff(scandir($suiteResultBaseDir), ['..', '.']); + + foreach ($expectedContents as $expectedFile) { + $this->assertTrue(in_array($expectedFile, $dirContents)); + } + } else { + $dirContents = array_diff(scandir(self::GENERATE_RESULT_DIR), ['..', '.']); + + // Validate suite is not generated + $this->assertFalse(in_array($groupName, $dirContents)); + } + + // Validate log message + if (substr($groupName, 0, 11) !== 'NotGenerate' + && !in_array($groupName, array_keys(self::$exceptionGrpLogs))) { + $type = 'info'; + $message = '/suite generated/'; + $context = [ + 'suite' => $groupName, + 'relative_path' => "_generated" . DIRECTORY_SEPARATOR . $groupName + ]; + } else { + $type = 'error'; + $message = self::$exceptionGrpLogs[$groupName][0]; + $context = []; + } + TestLoggingUtil::getInstance()->validateMockLogStatmentRegex($type, $message, $context); + } + } + + /** + * Test generate an empty suite + */ + public function testGenerateEmptySuites() + { + $groupName = 'NotGenerateEmptySuite'; + + // Validate exception is thrown + $this->assertExceptionRegex( + \Exception::class, + self::$exceptionGrpLogs[$groupName], + function () use ($groupName) { + SuiteGenerator::getInstance()->generateSuite($groupName); + } + ); + + // Validate suite is not generated + $dirContents = array_diff(scandir(self::GENERATE_RESULT_DIR), ['..', '.']); + $this->assertFalse(in_array($groupName, $dirContents)); + + // Validate log message + $type = 'error'; + $message = self::$exceptionGrpLogs[$groupName][0]; + $context = []; + + TestLoggingUtil::getInstance()->validateMockLogStatmentRegex($type, $message, $context); + } + + /** + * + * revert any changes made to config.yml + * remove _generated directory + */ + public function tearDown(): void + { + DirSetupUtil::rmdirRecursive(self::GENERATE_RESULT_DIR); + + // delete config yml file from test dir + $fileSystem = new \Symfony\Component\Filesystem\Filesystem(); + $fileSystem->remove( + self::CONFIG_YML_FILE + ); + + $property = new \ReflectionProperty(SuiteGenerator::class, "instance"); + $property->setAccessible(true); + $property->setValue(null, null); + + $property = new \ReflectionProperty(DirSetupUtil::class, "DIR_CONTEXT"); + $property->setAccessible(true); + $property->setValue(null, []); + + $property = new \ReflectionProperty(SuiteObjectHandler::class, "instance"); + $property->setAccessible(true); + $property->setValue(null, null); + + $property = new \ReflectionProperty(TestObjectHandler::class, "testObjectHandler"); + $property->setAccessible(true); + $property->setValue(null, null); + } + + /** + * Remove yml if created during tests and did not exist before + */ + public static function tearDownAfterClass(): void + { + TestLoggingUtil::getInstance()->clearMockLoggingUtil(); + } +} diff --git a/dev/tests/verification/Tests/SchemaValidationTest.php b/dev/tests/verification/Tests/SchemaValidationTest.php index 92442bd37..e4e5aea40 100644 --- a/dev/tests/verification/Tests/SchemaValidationTest.php +++ b/dev/tests/verification/Tests/SchemaValidationTest.php @@ -6,8 +6,8 @@ namespace tests\verification\Tests; use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use ReflectionProperty; use tests\util\MftfTestCase; -use AspectMock\Test as AspectMock; class SchemaValidationTest extends MftfTestCase { @@ -19,7 +19,11 @@ class SchemaValidationTest extends MftfTestCase */ public function testInvalidTestSchema() { - AspectMock::double(MftfApplicationConfig::class, ['getDebugLevel' => MftfApplicationConfig::LEVEL_DEVELOPER]); + $config = MftfApplicationConfig::getConfig(); + $property = new ReflectionProperty(MftfApplicationConfig::class, 'debugLevel'); + $property->setAccessible(true); + $property->setValue($config, MftfApplicationConfig::LEVEL_DEVELOPER); + $testFile = ['testFile.xml' => "<tests><test name='testName'><annotations>a</annotations></test></tests>"]; $expectedError = TESTS_MODULE_PATH . DIRECTORY_SEPARATOR . @@ -32,11 +36,13 @@ public function testInvalidTestSchema() } /** - * After method functionality - * @return void + * @inheritdoc */ - protected function tearDown() + protected function tearDown(): void { - AspectMock::clean(); + $config = MftfApplicationConfig::getConfig(); + $property = new ReflectionProperty(MftfApplicationConfig::class, 'debugLevel'); + $property->setAccessible(true); + $property->setValue($config, MftfApplicationConfig::LEVEL_DEFAULT); } } diff --git a/dev/tests/verification/Tests/SecretCredentialDataTest.php b/dev/tests/verification/Tests/SecretCredentialDataTestCest.php similarity index 71% rename from dev/tests/verification/Tests/SecretCredentialDataTest.php rename to dev/tests/verification/Tests/SecretCredentialDataTestCest.php index d1ef43745..acbcd1488 100644 --- a/dev/tests/verification/Tests/SecretCredentialDataTest.php +++ b/dev/tests/verification/Tests/SecretCredentialDataTestCest.php @@ -4,20 +4,9 @@ * See COPYING.txt for license details. */ -namespace Magento\AcceptanceTest\_default\Backend; +namespace Magento\FunctionalTestingFramework\Tests\Verification; use Magento\FunctionalTestingFramework\AcceptanceTester; -use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; -use Magento\FunctionalTestingFramework\DataGenerator\Handlers\PersistedObjectHandler; -use \Codeception\Util\Locator; -use Yandex\Allure\Adapter\Annotation\Features; -use Yandex\Allure\Adapter\Annotation\Stories; -use Yandex\Allure\Adapter\Annotation\Title; -use Yandex\Allure\Adapter\Annotation\Description; -use Yandex\Allure\Adapter\Annotation\Parameter; -use Yandex\Allure\Adapter\Annotation\Severity; -use Yandex\Allure\Adapter\Model\SeverityLevel; -use Yandex\Allure\Adapter\Annotation\TestCaseId; /** */ @@ -25,7 +14,6 @@ class SecretCredentialDataTestCest { /** * @Features({"AdminNotification"}) - * @Parameter(name = "AcceptanceTester", value="$I") * @param AcceptanceTester $I * @return void * @throws \Exception @@ -62,14 +50,14 @@ public function secretCredentialDataTest(AcceptanceTester $I) $I->fillField("#username", "Hardcoded"); // stepKey: fillFieldUsingHardCodedData1 $I->fillSecretField("#username", $I->getSecret("carriers_dhl_id_eu")); - // stepKey: fillFieldUsingSecretCredData1 + // stepKey: fillFieldUsingSecretCredData1 $magentoCliUsingHardcodedData1 = $I->magentoCLI("config:set cms/wysiwyg/enabled 0"); - // stepKey: magentoCliUsingHardcodedData1 + // stepKey: magentoCliUsingHardcodedData1 $I->comment($magentoCliUsingHardcodedData1); $magentoCliUsingSecretCredData1 = $I->magentoCLI("config:set cms/wysiwyg/enabled " . $I->getSecret("payment_authorizenet_login")); - // stepKey: magentoCliUsingSecretCredData1 + // stepKey: magentoCliUsingSecretCredData1 $I->comment($magentoCliUsingSecretCredData1); } } diff --git a/dev/tests/verification/Tests/SkippedGenerationTest.php b/dev/tests/verification/Tests/SkippedGenerationTest.php index 2d259e02c..b2d1c8fdc 100644 --- a/dev/tests/verification/Tests/SkippedGenerationTest.php +++ b/dev/tests/verification/Tests/SkippedGenerationTest.php @@ -43,13 +43,13 @@ public function testMultipleSkippedIssuesGeneration() } /** - * Tests skipped test generation with no specified issues. Will be deprecated after MFTF 3.0.0 + * Tests skipped test doesnt fail to generate when there is issue in test * * @throws \Exception * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException */ - public function testSkippedNoIssueGeneration() + public function testSkippedTestMustNotFailToGenerateWithErrorWhenThereIsIssueWithAnyOfTheStepsAsTheTestIsSkipped() { - $this->generateAndCompareTest('SkippedTestNoIssues'); + $this->generateAndCompareTest('SkippedTestWithIssueMustGetSkippedWithoutErrorExitCode'); } } diff --git a/dev/tests/verification/Tests/StaticCheck/DeprecationStaticCheckTest.php b/dev/tests/verification/Tests/StaticCheck/DeprecationStaticCheckTest.php new file mode 100644 index 000000000..6b034adb1 --- /dev/null +++ b/dev/tests/verification/Tests/StaticCheck/DeprecationStaticCheckTest.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace tests\verification\Tests; + +use Magento\FunctionalTestingFramework\StaticCheck\DeprecatedEntityUsageCheck; +use Magento\FunctionalTestingFramework\StaticCheck\StaticChecksList; +use ReflectionProperty; +use Symfony\Component\Console\Input\InputInterface; +use tests\util\MftfStaticTestCase; +use ReflectionClass; + +class DeprecationStaticCheckTest extends MftfStaticTestCase +{ + const LOG_FILE = self::STATIC_RESULTS_DIR . + DIRECTORY_SEPARATOR . + DeprecatedEntityUsageCheck::ERROR_LOG_FILENAME . + '.txt'; + + const TEST_MODULE_PATH = TESTS_MODULE_PATH . + DIRECTORY_SEPARATOR . + 'DeprecationCheckModule'. + DIRECTORY_SEPARATOR; + + /** + * test static-check DeprecatedEntityUsageCheck. + * + * @throws \Exception + */ + public function testDeprecatedEntityUsageCheck() + { + $staticCheck = new DeprecatedEntityUsageCheck(); + + $input = $this->mockInputInterface(self::TEST_MODULE_PATH); + $reflectionClass = new ReflectionClass(StaticChecksList::class); + $reflectionClass->setStaticPropertyValue('errorFilesPath', self::STATIC_RESULTS_DIR); + /** @var InputInterface $input */ + $staticCheck->execute($input); + + $this->assertTrue(file_exists(self::LOG_FILE)); + $this->assertFileEquals( + self::RESOURCES_PATH. + DIRECTORY_SEPARATOR . + DeprecatedEntityUsageCheck::ERROR_LOG_FILENAME . + ".txt", + self::LOG_FILE + ); + } + + /** + * @inheritdoc + */ + public function tearDown(): void + { + $reflectionClass = new ReflectionClass(StaticChecksList::class); + $reflectionClass->setStaticPropertyValue('errorFilesPath', null); + } +} diff --git a/dev/tests/verification/Tests/StaticCheck/PauseActionStaticCheckTest.php b/dev/tests/verification/Tests/StaticCheck/PauseActionStaticCheckTest.php new file mode 100644 index 000000000..70ab480c4 --- /dev/null +++ b/dev/tests/verification/Tests/StaticCheck/PauseActionStaticCheckTest.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace tests\verification\Tests; + +use Exception; +use Magento\FunctionalTestingFramework\StaticCheck\PauseActionUsageCheck; +use Magento\FunctionalTestingFramework\StaticCheck\StaticChecksList; +use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; +use ReflectionProperty; +use Symfony\Component\Console\Input\InputInterface; +use tests\util\MftfStaticTestCase; +use ReflectionClass; + +class PauseActionStaticCheckTest extends MftfStaticTestCase +{ + const LOG_FILE = self::STATIC_RESULTS_DIR . + DIRECTORY_SEPARATOR . + PauseActionUsageCheck::ERROR_LOG_FILENAME . + '.txt'; + + const TEST_MODULE_PATH = TESTS_MODULE_PATH . + DIRECTORY_SEPARATOR . + 'PauseCheckModule'. + DIRECTORY_SEPARATOR; + + /** + * test static-check PauseActionUsageCheck. + * + * @throws Exception + */ + public function testPauseActionUsageCheck() + { + $staticCheck = new PauseActionUsageCheck(); + + $input = $this->mockInputInterface(self::TEST_MODULE_PATH); + $reflectionClass = new ReflectionClass(StaticChecksList::class); + $reflectionClass->setStaticPropertyValue('errorFilesPath', self::STATIC_RESULTS_DIR); + + /** @var InputInterface $input */ + $staticCheck->execute($input); + + $this->assertTrue(file_exists(self::LOG_FILE)); + $this->assertFileEquals( + self::RESOURCES_PATH. + DIRECTORY_SEPARATOR . + PauseActionUsageCheck::ERROR_LOG_FILENAME . + ".txt", + self::LOG_FILE + ); + } + + /** + * @inheritdoc + */ + public static function tearDownAfterClass(): void + { + $reflectionClass = new ReflectionClass(StaticChecksList::class); + $reflectionClass->setStaticPropertyValue('errorFilesPath', null); + } +} diff --git a/dev/tests/verification/Tests/SuiteGenerationTest.php b/dev/tests/verification/Tests/SuiteGenerationTest.php index 485ef6411..ea25048b4 100644 --- a/dev/tests/verification/Tests/SuiteGenerationTest.php +++ b/dev/tests/verification/Tests/SuiteGenerationTest.php @@ -10,10 +10,10 @@ use Magento\FunctionalTestingFramework\Suite\SuiteGenerator; use Magento\FunctionalTestingFramework\Util\Filesystem\DirSetupUtil; use Magento\FunctionalTestingFramework\Util\Manifest\DefaultTestManifest; -use Magento\FunctionalTestingFramework\Util\Manifest\ParallelTestManifest; +use Magento\FunctionalTestingFramework\Util\Manifest\ParallelByTimeTestManifest; +use Magento\FunctionalTestingFramework\Util\Manifest\ParallelByGroupTestManifest; use Magento\FunctionalTestingFramework\Util\Manifest\TestManifestFactory; use Magento\FunctionalTestingFramework\Util\Path\FilePathFormatter; -use PHPUnit\Util\Filesystem; use Symfony\Component\Yaml\Yaml; use tests\unit\Util\TestLoggingUtil; use tests\util\MftfTestCase; @@ -39,7 +39,7 @@ class SuiteGenerationTest extends MftfTestCase /** * Set up config.yml for testing */ - public static function setUpBeforeClass() + public static function setUpBeforeClass(): void { // destroy _generated if it exists if (file_exists(self::GENERATE_RESULT_DIR)) { @@ -47,7 +47,7 @@ public static function setUpBeforeClass() } } - public function setUp() + public function setUp(): void { // copy config yml file to test dir $fileSystem = new \Symfony\Component\Filesystem\Filesystem(); @@ -74,12 +74,7 @@ public function testSuiteGeneration1() { $groupName = 'functionalSuite1'; - $expectedContents = [ - 'additionalTestCest.php', - 'additionalIncludeTest2Cest.php', - 'IncludeTest2Cest.php', - 'IncludeTestCest.php' - ]; + $expectedContents = SuiteTestReferences::$data[$groupName]; // Generate the Suite SuiteGenerator::getInstance()->generateSuite($groupName); @@ -113,27 +108,22 @@ public function testSuiteGeneration1() /** * Test generation of parallel suite groups */ - public function testSuiteGenerationParallel() + public function testSuiteGenerationParallelByTime() { $groupName = 'functionalSuite1'; $expectedGroups = [ - 'functionalSuite1_0', - 'functionalSuite1_1', - 'functionalSuite1_2', - 'functionalSuite1_3' + 'functionalSuite1_0_G', + 'functionalSuite1_1_G', + 'functionalSuite1_2_G', + 'functionalSuite1_3_G' ]; - $expectedContents = [ - 'additionalTestCest.php', - 'additionalIncludeTest2Cest.php', - 'IncludeTest2Cest.php', - 'IncludeTestCest.php' - ]; + $expectedContents = SuiteTestReferences::$data[$groupName]; //createParallelManifest - /** @var ParallelTestManifest $parallelManifest */ - $parallelManifest = TestManifestFactory::makeManifest("parallel", ["functionalSuite1" => []]); + /** @var ParallelByTimeTestManifest $parallelManifest */ + $parallelManifest = TestManifestFactory::makeManifest("parallelByTime", ["functionalSuite1" => []]); // Generate the Suite $parallelManifest->createTestGroups(1); @@ -168,6 +158,57 @@ public function testSuiteGenerationParallel() } } + /** + * Test generation of parallel suite groups + */ + public function testSuiteGenerationParallelByGroup() + { + $groupName = 'functionalSuite1'; + + $expectedGroups = [ + 'functionalSuite1_0_G', + 'functionalSuite1_1_G', + ]; + + $expectedContents = SuiteTestReferences::$data[$groupName]; + + //createParallelManifest + /** @var ParallelByGroupTestManifest $parallelManifest */ + $parallelManifest = TestManifestFactory::makeManifest("parallelByGroup", ["functionalSuite1" => []]); + + // Generate the Suite + $parallelManifest->createTestGroups(2); + SuiteGenerator::getInstance()->generateAllSuites($parallelManifest); + + // Validate log message (for final group) and add group name for later deletion + $expectedGroup = $expectedGroups[count($expectedGroups)-1] ; + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'info', + "suite generated", + ['suite' => $expectedGroup, 'relative_path' => "_generated" . DIRECTORY_SEPARATOR . $expectedGroup] + ); + + self::$TEST_GROUPS[] = $groupName; + + // Validate Yaml file updated + $yml = Yaml::parse(file_get_contents(self::CONFIG_YML_FILE)); + $this->assertEquals(array_intersect($expectedGroups, array_keys($yml['groups'])), $expectedGroups); + + foreach ($expectedGroups as $expectedFolder) { + $suiteResultBaseDir = self::GENERATE_RESULT_DIR . + DIRECTORY_SEPARATOR . + $expectedFolder . + DIRECTORY_SEPARATOR; + + // Validate tests have been generated + $dirContents = array_diff(scandir($suiteResultBaseDir), ['..', '.']); + + //Validate two test has been added to each group since lines are set to 1 + $this->assertEquals(2, count($dirContents)); + $this->assertContains(array_values($dirContents)[0], $expectedContents); + } + } + /** * Test hook groups generated during suite generation */ @@ -175,9 +216,7 @@ public function testSuiteGenerationHooks() { $groupName = 'functionalSuiteHooks'; - $expectedContents = [ - 'IncludeTestCest.php' - ]; + $expectedContents = SuiteTestReferences::$data[$groupName]; // Generate the Suite SuiteGenerator::getInstance()->generateSuite($groupName); @@ -235,12 +274,7 @@ public function testSuiteGenerationSingleRun() //using functionalSuite2 to avoid directory caching $groupName = 'functionalSuite2'; - $expectedContents = [ - 'additionalTestCest.php', - 'additionalIncludeTest2Cest.php', - 'IncludeTest2Cest.php', - 'IncludeTestCest.php' - ]; + $expectedContents = SuiteTestReferences::$data[$groupName]; //createParallelManifest /** @var DefaultTestManifest $parallelManifest */ @@ -294,9 +328,7 @@ public function testSuiteGenerationWithExtends() { $groupName = 'suiteExtends'; - $expectedFileNames = [ - 'ExtendedChildTestInSuiteCest' - ]; + $expectedFileNames = SuiteTestReferences::$data[$groupName]; // Generate the Suite SuiteGenerator::getInstance()->generateSuite($groupName); @@ -321,10 +353,12 @@ public function testSuiteGenerationWithExtends() $dirContents = array_diff(scandir($suiteResultBaseDir), ['..', '.']); foreach ($expectedFileNames as $expectedFileName) { - $this->assertTrue(in_array($expectedFileName . ".php", $dirContents)); + $this->assertTrue(in_array($expectedFileName, $dirContents)); $this->assertFileEquals( - self::RESOURCES_PATH . DIRECTORY_SEPARATOR . $expectedFileName . ".txt", - $suiteResultBaseDir . $expectedFileName . ".php" + self::RESOURCES_PATH . DIRECTORY_SEPARATOR + . substr($expectedFileName, 0, strlen($expectedFileName)-4) + . ".txt", + $suiteResultBaseDir . $expectedFileName ); } } @@ -336,9 +370,64 @@ public function testSuiteCommentsGeneration() { $groupName = 'functionalSuiteWithComments'; - $expectedContents = [ - 'IncludeTestCest.php' - ]; + $expectedContents = SuiteTestReferences::$data[$groupName]; + + // Generate the Suite + SuiteGenerator::getInstance()->generateSuite($groupName); + + // Validate log message and add group name for later deletion + TestLoggingUtil::getInstance()->validateMockLogStatement( + 'info', + "suite generated", + ['suite' => $groupName, 'relative_path' => "_generated" . DIRECTORY_SEPARATOR . $groupName] + ); + self::$TEST_GROUPS[] = $groupName; + + // Validate Yaml file updated + $yml = Yaml::parse(file_get_contents(self::CONFIG_YML_FILE)); + $this->assertArrayHasKey($groupName, $yml['groups']); + + $suiteResultBaseDir = self::GENERATE_RESULT_DIR . + DIRECTORY_SEPARATOR . + $groupName . + DIRECTORY_SEPARATOR; + + // Validate tests have been generated + $dirContents = array_diff(scandir($suiteResultBaseDir), ['..', '.']); + + foreach ($expectedContents as $expectedFile) { + $this->assertTrue(in_array($expectedFile, $dirContents)); + } + + //assert group file created and contains correct contents + $groupFile = PROJECT_ROOT . + DIRECTORY_SEPARATOR . + "src" . + DIRECTORY_SEPARATOR . + "Magento" . + DIRECTORY_SEPARATOR . + "FunctionalTestingFramework" . + DIRECTORY_SEPARATOR . + "Group" . + DIRECTORY_SEPARATOR . + $groupName . + ".php"; + + $this->assertTrue(file_exists($groupFile)); + $this->assertFileEquals( + self::RESOURCES_PATH . DIRECTORY_SEPARATOR . $groupName . ".txt", + $groupFile + ); + } + + /** + * Test suite generation with actions from different modules + */ + public function testSuiteGenerationActionsInDifferentModules() + { + $groupName = 'ActionsInDifferentModulesSuite'; + + $expectedContents = SuiteTestReferences::$data[$groupName]; // Generate the Suite SuiteGenerator::getInstance()->generateSuite($groupName); @@ -392,7 +481,7 @@ public function testSuiteCommentsGeneration() * revert any changes made to config.yml * remove _generated directory */ - public function tearDown() + public function tearDown(): void { DirSetupUtil::rmdirRecursive(self::GENERATE_RESULT_DIR); @@ -401,12 +490,16 @@ public function tearDown() $fileSystem->remove( self::CONFIG_YML_FILE ); + + $property = new \ReflectionProperty(DirSetupUtil::class, "DIR_CONTEXT"); + $property->setAccessible(true); + $property->setValue(null, []); } /** * Remove yml if created during tests and did not exist before */ - public static function tearDownAfterClass() + public static function tearDownAfterClass(): void { TestLoggingUtil::getInstance()->clearMockLoggingUtil(); } diff --git a/dev/tests/verification/Tests/SuiteTestReferences.php b/dev/tests/verification/Tests/SuiteTestReferences.php new file mode 100644 index 000000000..83481cabf --- /dev/null +++ b/dev/tests/verification/Tests/SuiteTestReferences.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace tests\verification\Tests; + +class SuiteTestReferences +{ + /** + * Array of suite to tests that the suite contains + * + * @var array + */ + public static $data = [ + 'functionalSuite1' => [ + 'additionalTestCest.php', + 'additionalIncludeTest2Cest.php', + 'IncludeTest2Cest.php', + 'IncludeTestCest.php' + ], + 'functionalSuiteHooks' => [ + 'IncludeTestCest.php' + ], + 'functionalSuite2' => [ + 'additionalTestCest.php', + 'additionalIncludeTest2Cest.php', + 'IncludeTest2Cest.php', + 'IncludeTestCest.php' + ], + 'suiteExtends' => [ + 'ExtendedChildTestInSuiteCest.php' + ], + 'functionalSuiteWithComments' => [ + 'IncludeTestCest.php' + ], + 'ActionsInDifferentModulesSuite' => [ + 'IncludeActionsInDifferentModulesTestCest.php' + ], + 'suiteWithMultiplePauseActionsSuite' => [ + 'additionalTestCest.php', + 'ExcludeTest2Cest.php', + 'IncludeTest2Cest.php' + ], + 'suiteWithPauseActionSuite' => [ + 'additionalTestCest.php', + 'ExcludeTest2Cest.php', + 'IncludeTest2Cest.php' + ], + 'PartialGenerateForIncludeSuite' => [ + 'IncludeTestCest.php' + ], + 'PartialGenerateNoExcludeSuite' => [ + 'IncludeTestCest.php' + ], + 'NotGenerateHookBeforeSuite' => [ + ], + 'NotGenerateHookAfterSuite' => [ + ], + 'deprecationCheckSuite' => [ + 'DeprecationCheckDeprecatedTestCest.php', + 'DeprecationCheckTestCest.php' + ], + 'NotGenerateEmptySuite' => [ + ], + ]; +} diff --git a/dev/tests/verification/Tests/XmlDuplicateGenerationTest.php b/dev/tests/verification/Tests/XmlDuplicateGenerationTest.php index e9f65b9aa..5dc71cb8c 100644 --- a/dev/tests/verification/Tests/XmlDuplicateGenerationTest.php +++ b/dev/tests/verification/Tests/XmlDuplicateGenerationTest.php @@ -9,7 +9,7 @@ use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; use tests\util\MftfTestCase; -class XmlDuplicateGerationTest extends MftfTestCase +class XmlDuplicateGenerationTest extends MftfTestCase { const XML_DUPLICATE_TEST = 'XmlDuplicateTest'; const XML_DUPLICATE_ACTIONGROUP = 'xmlDuplicateActionGroup'; diff --git a/docs/best-practices.md b/docs/best-practices.md deleted file mode 100644 index c0499fb12..000000000 --- a/docs/best-practices.md +++ /dev/null @@ -1,180 +0,0 @@ -# Best practices - -Check out our best practices below to ensure you are getting the absolute most out of the Magento Functional Testing Framework. - -## Action group - -1. [Action group] names should be sufficiently descriptive to inform a test writer of what the action group does and when it should be used. - Add additional explanation in annotations if needed. -2. Provide default values for the arguments that apply to your most common case scenarios. -3. One `<actionGroup>` tag is allowed per action group XML file. - -## `actionGroups` vs `extends` - -Use an action group to wrap a set of actions to reuse them multiple times. - -Use an [extension] when a test or action group needs to be repeated with the exception of a few steps. - -### When to use `extends` - -Use `extends` in your new test or action group when at least one of the following conditions is applicable to your case: - -1. You want to keep the original test without any modifications. -2. You want to create a new test that follows the same path as the original test. -3. You want a new action group that behaves similarly to the existing action group, but you do not want to change the functionality of the original action group. - -### When to avoid `extends` - -Do not use `extends` in the following conditions: - -1. You want to change the functionality of the test or action group and do not need to run the original version. -2. You plan to merge the base test or action group. - -The following pattern is used when merging with `extends`: - -1. The original test is merged. -2. The extended test is created from the merged original test. -3. The extended test is merged. - -## Annotation - -1. Use [annotations] in a test. -2. Update your annotations correspondingly when updating tests. - -## Data entity - -1. Keep your testing instance clean. - Remove data after the test if the test required creating any data. - Use a corresponding [`<deleteData>`] test step in your [`<after>`] block when using a [`<createData>`] action in a [`<before>`] block. -2. Make specific data entries under test to be unique. - Enable data uniqueness where data values are required to be unique in a database by test design. - Use `unique=”suffix”` or `unique=”prefix”` to append or prepend a unique value to the [entity] attribute. - This ensures that tests using the entity can be repeated. -3. Do not modify existing data entity fields or merge additional data fields without complete understanding and verifying the usage of existing data in tests. - Create a new data entity for your test if you are not sure. - -## Naming conventions - -### File names - -Name files according to the following patterns to make searching in future more easy: - -<!-- {% raw %} --> - -#### Test file name - -Format: {_Admin_ or _Storefront_}{Functionality}_Test.xml_, where Functionality briefly describes the testing functionality. - -Example: _StorefrontCreateCustomerTest.xml_. - -#### Action Group file name - -Format: {_Admin_ or _Storefront_}{Action Group Summary}ActionGroup.xml`, where Action Group Summary describes with a few words what we can expect from it. - -Example: _AdminCreateStoreActionGroup.xml_ - -#### Section file name - -Format: {_Admin_ or _Storefront_}{UI Description}_Section.xml_, where UI Description briefly describes the testing UI. - -Example: _AdminNavbarSection.xml_. - -#### Data file name - -Format: {Type}_Data.xml_, where Type represents the entity type. - -<!-- {% endraw %} --> - -Example: _ProductData.xml_. - -### Object names - -Use the _Foo.camelCase_ naming convention, which is similar to _Classes_ and _classProperties_ in PHP. - -#### Upper case - -Use an upper case first letter for: - -- File names. Example: _StorefrontCreateCustomerTest.xml_ -- Test name attributes. Example: `<test name="TestAllTheThingsTest">` -- Data entity names. Example: `<entity name="OutOfStockProduct">` -- Page name. Example: `<page name="AdminLoginPage">` -- Section name. Example: `<section name="AdminCategorySidebarActionSection">` -- Action group name. Example: `<actionGroup name="LoginToAdminActionGroup">` - -#### Lower case - -Use a lower case first letter for: - -- Data keys. Example: `<data key="firstName">` -- Element names. Examples: `<element name="confirmDeleteButton"/>` -- Step keys. For example: `<click selector="..." stepKey="clickLogin"/>` - -## Page object - -1. One `<page>` tag is allowed per page XML file. -2. Use [parameterized selectors] for constructing a selector when test-specific or runtime-generated information is needed. -Do not use them for static elements. - -<span class="color:red"> -BAD: -</span> - -<!-- {% raw %} --> - -``` xml -<element name="relatedProductSectionText" type="text" selector=".fieldset-wrapper.admin__fieldset-section[data-index='{{productType}}']" parameterized="true"/> -``` - -<!-- {% endraw %} --> - -<span class="color:green"> -GOOD: -</span> - -Define these three elements and reference them by name in the tests. - -``` xml -<element name="relatedProductSectionText" type="text" selector=".fieldset-wrapper.admin__fieldset-section[data-index='related']"/> -<element name="upSellProductSectionText" type="text" selector=".fieldset-wrapper.admin__fieldset-section[data-index='upsell']"/> -<element name="crossSellProductSectionText" type="text" selector=".fieldset-wrapper.admin__fieldset-section[data-index='crosssell']"/> -``` - -## Test - -1. Use actions such as [`<waitForElementVisible>`], [`<waitForLoadingMaskToDisappear>`], and [`<waitForElement>`] to wait the exact time required for the test step. - Try to avoid using the [`<wait>`] action, because it forces the test to wait for the time you specify. You may not need to wait so long to proceed. -1. Keep your tests short and granular for target testing, easier reviews, and easier merge conflict resolution. - It also helps you to identify the cause of test failure. -1. Use comments to keep tests readable and maintainable: - - Keep the inline `<!-- XML comments -->` and [`<comment>`] tags up to date. - It helps to inform the reader of what you are testing and to yield a more descriptive Allure report. - - Explain in comments unclear or tricky test steps. -1. Refer to [sections] instead of writing selectors. -1. One `<test>` tag is allowed per test XML file. - -## Test step merging order - -When setting a [merging] order for a test step, do not depend on steps from Magento modules that could be disabled by an application. - -For example, when you write a test step to create a gift card product, set your test step **after** simple product creation and let the MFTF handle the merge order. -Since the configurable product module could be disabled, this approach is more reliable than setting the test step **before** creating a configurable product. - -<!-- Link definitions --> - -[`<after>`]: test/actions.html#before-and-after -[`<before>`]: test/actions.html#before-and-after -[`<comment>`]: test/actions.html#comment -[`<createData>`]: test/actions.html#createdata -[`<deleteData>`]: test/actions.html#deletedata -[`<wait>`]: test/actions.html#wait -[`<waitForElement>`]: test/actions.html#waitforelement -[`<waitForElementVisible>`]: test/actions.html#waitforelementvisible -[`<waitForLoadingMaskToDisappear>`]: test/actions.html#waitforloadingmasktodisappear -[Action group]: test/action-groups.html -[annotations]: test/annotations.html -[entity]: data.html -[extension]: extending.html -[merging]: merging.html -[parameterized selectors]: section/parameterized-selectors.html -[sections]: section.html diff --git a/docs/commands/codeception.md b/docs/commands/codeception.md deleted file mode 100644 index 6a96a89c7..000000000 --- a/docs/commands/codeception.md +++ /dev/null @@ -1,85 +0,0 @@ -# CLI commands: vendor/bin/codecept - -<div class="bs-callout bs-callout-warning" markdown="1"> -We do not recommend using Codeception commands directly as they can break the MFTF basic workflow. -All the Codeception commands you need are wrapped using the [mftf tool][]. - -To run the Codeception testing framework commands directly, change your directory to the `<Magento root>`. -</div> - -## Usage examples - -Run all the generated tests: - -```bash -vendor/bin/codecept run functional -c dev/tests/acceptance/codeception.yml -``` - -Run all tests without the `<group value="skip"/>` [annotation][]: - -```bash -vendor/bin/codecept run functional --skip-group skip -c dev/tests/acceptance/codeception.yml -``` - -Run all tests with the `<group value="example"/>` [annotation][] but with no `<group value="skip"/>`: - -```bash -vendor/bin/codecept run functional --group example --skip-group skip -c dev/tests/acceptance/codeception.yml -``` - -## `codecept run` - -`codecept run` runs the test suites: - -```bash -vendor/bin/codecept run -``` - -<div class="bs-callout bs-callout-info"> -The following documentation corresponds to Codeception 2.3.8. -</div> - -```bash -Full reference: - -Arguments: - suite suite to be tested - test test to be run - -Options: - -o, --override=OVERRIDE Override config values (multiple values allowed) - --config (-c) Use custom path for config - --report Show output in compact style - --html Generate html with results (default: "report.html") - --xml Generate JUnit XML Log (default: "report.xml") - --tap Generate Tap Log (default: "report.tap.log") - --json Generate Json Log (default: "report.json") - --colors Use colors in output - --no-colors Force no colors in output (useful to override config file) - --silent Only outputs suite names and final results - --steps Show steps in output - --debug (-d) Show debug and scenario output - --coverage Run with code coverage (default: "coverage.serialized") - --coverage-html Generate CodeCoverage HTML report in path (default: "coverage") - --coverage-xml Generate CodeCoverage XML report in file (default: "coverage.xml") - --coverage-text Generate CodeCoverage text report in file (default: "coverage.txt") - --coverage-phpunit Generate CodeCoverage PHPUnit report in file (default: "coverage-phpunit") - --no-exit Do not finish with exit code - --group (-g) Groups of tests to be executed (multiple values allowed) - --skip (-s) Skip selected suites (multiple values allowed) - --skip-group (-x) Skip selected groups (multiple values allowed) - --env Run tests in selected environments. (multiple values allowed, environments can be merged with ',') - --fail-fast (-f) Stop after first failure - --help (-h) Display this help message. - --quiet (-q) Do not output any message. - --verbose (-v|vv|vvv) Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug - --version (-V) Display this application version. - --ansi Force ANSI output. - --no-ansi Disable ANSI output. - --no-interaction (-n) Do not ask any interactive question. -``` - -<!-- Link definitions --> - -[mftf tool]: mftf.md -[annotation]: ../test/annotations.md \ No newline at end of file diff --git a/docs/commands/mftf.md b/docs/commands/mftf.md deleted file mode 100644 index 6d2dedfd1..000000000 --- a/docs/commands/mftf.md +++ /dev/null @@ -1,523 +0,0 @@ -# CLI commands: vendor/bin/mftf - -The Magento Functional Testing Framework (MFTF) introduces the command line interface (CLI) tool `vendor/bin/mftf` to facilitate your interaction with the framework. - -Note that `mftf` commands replace the `robo` commands that were used in previous releases. - -## Command format - -In the project root directory (where you have installed the framework as a composer dependency), run commands using the following format: - -```bash -vendor/bin/mftf command [options] [<arguments>] [--remove|-r] -``` - -## Useful commands - -Use the following commands to run commonly performed tasks. - -### Apply the configuration parameters - -```bash -vendor/bin/mftf build:project -``` - -### Upgrade the project - -```bash -vendor/bin/mftf build:project --upgrade -``` - -Upgrades all installed MFTF tests after a major MFTF upgrade. - -### Generate all tests - -```bash -vendor/bin/mftf generate:tests -``` - -### Generate tests by test name - -```bash -vendor/bin/mftf generate:tests AdminLoginTest StorefrontPersistedCustomerLoginTest -``` - -### Generate test by test and suite name - -```bash -vendor/bin/mftf generate:tests LoginSuite:AdminLoginTest -``` - -### Generate and run the tests for a specified group - -```bash -vendor/bin/mftf run:group product -r -``` - -This command cleans up the previously generated tests; generates and runs tests for the product group (where `group="product"`). - -### Generate and run particular tests - -```bash -vendor/bin/mftf run:test AdminLoginTest StorefrontPersistedCustomerLoginTest -r -``` - -This command cleans up the previously generated tests; generates and runs the `LoginAsAdminTest` and `LoginAsCustomerTest` tests. - -### Generate and run particular test in a specific suite's context - -```bash -vendor/bin/mftf run:test LoginSuite:AdminLoginTest -r -``` - -This command cleans up previously generated tests; generates and run `AdminLoginTest` within the context of the `LoginSuite`. - -### Generate and run a testManifest.txt file - -```bash -vendor/bin/mftf run:manifest path/to/your/testManifest.txt -``` - -This command runs all tests specified in a `testManifest.txt` file. When you generate tests, a `testManifest.txt` file is also generated for you. You can pass this file directly to the `run:manifest` command and it will execute all listed tests. You can also create your own file in the same format to execute a subset of tests. Note: This command does not generate tests. - -### Generate and run previously failed tests - -```bash -vendor/bin/mftf run:failed -``` - -This command cleans up the previously generated tests; generates and runs the tests listed in `dev/tests/acceptance/tests/_output/failed`. -For more details about `failed`, refer to [Reporting][]. - -## Reference - -### `build:project` - -#### Description - -Clone the example configuration files and build the Codeception project. - -#### Usage - -```bash -vendor/bin/mftf build:project [--upgrade] [config_param_options] -``` - -#### Options - -| Option | Description | -| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `-u`, `--upgrade` | Upgrades all installed MFTF tests according to requirements of the last major release. Specifying this flag upgrades only those tests in the default location. Example: `build:project --upgrade`. | - -You can include options to set configuration parameter values for your environment since the project build process also [sets up the environment][setup]. - -```bash -vendor/bin/mftf build:project --MAGENTO_BASE_URL=http://magento.local/ --MAGENTO_BACKEND_NAME=admin214365 -``` - -### `doctor` - -#### Description - -Diagnose MFTF configuration and setup. Currently this command will check the following: -- Verify admin credentials are valid. Allowing MFTF authenticates and runs API requests to Magento through cURL -- Verify that Selenium is up and running and available for MFTF -- Verify that new session of browser can open Magento admin and store front urls -- Verify that MFTF can run MagentoCLI commands - -#### Usage - -```bash -vendor/bin/mftf doctor -``` - -#### Options - -### `generate:tests` - -#### Description - -Perform XML schema validation and generate PHP code from the tests defined in XML files. -The path is set in the `TESTS_MODULE_PATH` [configuration] parameter. - -#### Usage - -```bash -vendor/bin/mftf generate:tests [option] [<test name>] [<test name>] [--remove] -``` - -#### Options - -| Option | Description| -| ---| --- | -| `--config=[<default> or <singleRun> or <parallel>]` | Creates a single manifest file with a list of all tests. The default location is `tests/functional/Magento/FunctionalTest/_generated/testManifest.txt`.<br/> You can split the list into multiple groups using `--config=parallel`; the groups will be generated in `_generated/groups/` like `_generated/groups/group1.txt, group2.txt, ...`.<br/> Available values: `default` (default), `singleRun`(same as `default`), and `parallel`.<br/> Example: `generate:tests --config=parallel`. | -| `--filter` | Option to filter tests to be generated.<br/>Template: '<filterName>:<filterValue>'.<br/>Existing filter types: severity.<br/>Existing severity values: BLOCKER, CRITICAL, MAJOR, AVERAGE, MINOR.<br/>Example: --filter=severity:CRITICAL| -| `--force` | Forces test generation, regardless of the module merge order defined in the Magento instance. Example: `generate:tests --force`. | -| `-i,--time` | Set time in minutes to determine the group size when `--config=parallel` is used. The __default value__ is `10`. Example: `generate:tests --config=parallel --time=15`| -| `--tests` | Defines the test configuration as a JSON string.| -| `--allow-skipped` | Allows MFTF to generate and run tests marked with `<skip>.`| -| `--debug` | Performs schema validations on XML files. <br/> DEFAULT: `generate:tests` implicitly performs schema validation on merged files. It does not indicate the file name where the error is encountered. <br/> DEVELOPER: `--debug` performs per-file validation and returns additional debug information (such as the filename where an error occurred) when test generation fails because of an invalid XML schema. This option takes extra processing time. Use it after test generation has failed once.<br/>| -| `-r,--remove`| Removes the existing generated suites and tests cleaning up the `_generated` directory before the actual run. For example, `generate:tests SampleTest --remove` cleans up the entire `_generated` directory and generates `SampleTest` only.| - -#### Examples of the JSON configuration - -The configuration to generate a single test with no suites: - -```json -{ - "tests":[ - "general_test1" //Generate the "general_test1" test. - ], - "suites": null -} -``` - -The configuration to generate a single test in the suite: - -```json -{ - "tests": null, // No tests outside the suite configuration will be generated. - "suites":{ - "sample":[ // The suite that contains the test. - "suite_test1" // The test to be generated. - ] - } -} -``` - -Complex configuration to generate a few non-suite tests, a single test in a suite, and an entire suite: - -```json -{ - "tests":[ - "general_test1", - "general_test2", - "general_test3" - ], - "suites":{ //Go to suites. - "sample":[ //Go to the "sample" suite. - "suite_test1" //Generate the "suite_test1" test. - ], - "sample2":[] //Generate all tests in the "sample2" suite. - } -} -``` - -The command that encodes this complex configuration: - -```bash -vendor/bin/mftf generate:tests --tests '{"tests":["general_test1","general_test2","general_test3"],"suites":{"sample":["suite_test1"],"sample2":null}}' -``` - -Note that the strings must be escaped and surrounded in quotes. - -### `generate:suite` - -#### Description - -Generates one or more suites based on XML declarations. - -#### Usage - -```bash -vendor/bin/mftf generate:suite <suite name> [<suite name>] [--remove] -``` - -#### Options - -| Option | Description | -| --- | --- | -| `-r,--remove` | Removes the existing generated suites and tests cleaning up the `_generated` directory before the actual run. For example, `vendor/bin/mftf generate:suite WYSIWYG --remove` cleans up the entire `_generated` directory and generates `WYSIWYG` only. | - -#### Example - -```bash -vendor/bin/mftf generate:suite suite1 suite2 -``` - -### `generate:urn-catalog` - -#### Description - -Generates a URN catalog, enabling PhpStorm to recognize and highlight URNs. -It also enables auto-completion in PhpStorm. - -#### Usage - -```bash -vendor/bin/mftf generate:urn-catalog [--force] [<path to the directory with misc.xml>] -``` - -`misc.xml` is typically located in `<project root>/.idea/`. - -#### Options - -| Option | Description | -| ------------- | --------------------------------------------------------------------- | -| `-f, --force` | Creates the `misc.xml` file if it does not exist in the given `path`. | - -#### Example - -```bash -vendor/bin/mftf generate:urn-catalog .idea/ -``` - -### `reset` - -#### Description - -Cleans any configuration files and generated artifacts from the environment. -The `.env` file is not affected. - -#### Usage - -```bash -vendor/bin/mftf reset [--hard] -``` - -#### Options - -| Option | Description | -| -------- | ------------------------------------------ | -| `--hard` | Forces a reset of the configuration files. | - -#### Example - -```bash -vendor/bin/mftf reset --hard -``` - -### `run:group` - -Generates and executes the listed groups of tests using Codeception. - -#### Usage - -```bash -vendor/bin/mftf run:group [--skip-generate|--remove] [--] <group1> [<group2>] -``` - -#### Options - -| Option | Description | -| --------------------- | --------------------------------------------------------------------------------------------------------- | -| `-k, --skip-generate` | Skips generating from the source XML. Instead, the command executes previously-generated groups of tests. | -| `-r, --remove` | Removes previously generated suites and tests before the actual generation and run. | -| `--debug` | Performs schema validations on XML files. `run:group` implicitly performs schema validation on merged files. It does not indicate the file name where the error is encountered. `--debug` performs per-file validation and returns additional debug information (such as the filename where an error occurred).| - -#### Examples - -Clean up after the last test run; generate from XML and execute the tests with the annotations `group="group1"` and `group="group2"`: - -```bash -vendor/bin/mftf -r -- run:group group1 group2 -``` - -Execute previously generated tests with the annotations `group="group1"` and `group="group2"`, skipping the regeneration of the test: - -```bash -vendor/bin/mftf run:group -k -- group1 group2 -``` - -### `run:test` - -Generates and executes tests by name using Codeception. - -#### Usage - -```bash -vendor/bin/mftf run:test [--skip-generate|--remove] [--] <name1> [<name2>] -``` - -#### Options - -| Option | Description | -|-----------------------|-----------------------------------------------------------------------------------------------------------| -| `-k, --skip-generate` | Skips generating from the source XML. Instead, the command executes previously-generated groups of tests. | -| `-r, --remove` | Remove previously generated suites and tests. | -| `--debug` | Performs schema validations on XML files. `run:test` implicitly performs schema validation on merged files. It does not indicate the file name where the error is encountered. `--debug` performs per-file validation and returns additional debug information (such as the filename where an error occurred).| - -#### Examples - -Generate the `LoginCustomerTest` and `StorefrontCreateCustomerTest` tests from XML and execute all the generated tests: - -```bash -vendor/bin/mftf run:test LoginCustomerTest StorefrontCreateCustomerTest -``` - -### `run:manifest` - -Runs a testManifest.txt file. - -This command runs all tests specified in a testManifest.xml file. It does not generate tests for you. You must do that as first. - -#### Usage - -```bash -vendor/bin/mftf run:manifest path/to/your/testManifest.txt -``` - -#### Example testManifest.xml file - -Each line should contain either: one test path or one group (-g) reference. - -``` -tests/functional/tests/MFTF/_generated/default/AdminLoginTestCest.php --g PaypalTestSuite -tests/functional/tests/MFTF/_generated/default/SomeOtherTestCest.php -tests/functional/tests/MFTF/_generated/default/ThirdTestCest.php --g SomeOtherSuite -``` - -### `run:failed` - -Regenerates and reruns tests that previously failed. - -This command cleans up previously generated tests. It generates and runs the tests listed in `dev/tests/acceptance/tests/_output/failed`. -For more details about `failed`, refer to [Reporting][]. - -#### Usage - -```bash -vendor/bin/mftf run:failed -``` -#### Options - -| Option | Description | -|-----------------------|-----------------------------------------------------------------------------------------------------------| -| `--debug` | Performs schema validations on XML files. `run:failed` implicitly performs schema validation on merged files. It does not indicate the file name where the error is encountered. `--debug` performs per-file validation and returns additional debug information (such as the filename where an error occurred). Use it after test run has failed once.| - -#### Examples - -Run the tests that failed in the previous run: - -```bash -vendor/bin/mftf run:failed -``` - -### `setup:env` - -Updates the [configuration] parameter values in the [`.env`] file. -Creates the `.env` file if it does not exist. - -#### Usage - -```bash -vendor/bin/mftf setup:env [config_param_option1=<value>] [config_param_option2=<value>] -``` - -`config_param` is a configuration parameter from the `.env` file. -The command consumes the parameters in a format of options assigned with values, for example `--MAGENTO_BASE_URL=http://magento.local/`. -If you specify a parameter that the `.env` file does not contain, the command returns an error. - -You can also update configuration parameter values when you use the [`build:project`][build] command. - -#### Examples - -To change values for the `MAGENTO_BASE_URL` and `BROWSER`: - -```bash -vendor/bin/mftf setup:env --MAGENTO_BASE_URL=http://magento.local/ --BROWSER=firefox -``` - -To create a `.env` file with example parameters: - -```bash -vendor/bin/mftf setup:env -``` - -The example parameters are taken from the `etc/config/.env.example` file. - -### `static-checks` - -Runs all or specific MFTF static-checks on the test codebase that MFTF is currently attached to. -If no script name argument is specified, all existing static check scripts will run. - -#### Usage - -```bash -vendor/bin/mftf static-checks [<names>]... -``` - -#### Examples - -To check what existing static check scripts are available - -```bash -vendor/bin/mftf static-checks --help -``` - -To run all existing static check scripts - -```bash -vendor/bin/mftf static-checks -``` - -To run specific static check scripts - -```bash -vendor/bin/mftf static-checks testDependencies -``` -```bash -vendor/bin/mftf static-checks actionGroupArguments -``` -```bash -vendor/bin/mftf static-checks testDependencies actionGroupArguments -``` - -#### Existing static checks - -* Test Dependency: Checks that test dependencies do not violate Magento module's composer dependencies. -* Action Group Unused Arguments: Checks that action groups do not have unused arguments. - -### `upgrade:tests` - -When the path argument is specified, this `upgrade` command applies all the major version MFTF upgrade scripts to a `Test Module` in the given path. -Otherwise, it will apply all the major version MFTF upgrade scripts to all installed test components. - -`Test Module` should have the directory structure of ActionGroup, Data, Metadata, Page, Section, Test, and Suite. - -**Note**: - -The upgrade scripts are meant to be used for Test Modules under source code control. If you have old versions of test modules under vendor, those test modules will get upgraded - -#### Usage - -```bash -vendor/bin/mftf upgrade:tests [<path>] -``` - -`<path>` is the path to a MFTF `Test Module` that needs to be upgraded. -The command searches recursively for any `*.xml` files to upgrade. - -#### Examples - -To upgrade all installed MFTF tests: - -```bash -vendor/bin/mftf upgrade:tests -``` - -To upgrade all test components inside modules in the `dev/tests/acceptance/tests/` directory: - -```bash -vendor/bin/mftf upgrade:tests /Users/user/magento2/dev/tests/acceptance/tests/ -``` - -To upgrade all test components inside the `Catalog` module: - -```bash -vendor/bin/mftf upgrade:tests /Users/user/magento2/app/code/Magento/Catalog/Test/Mftf/ -``` - -<!-- LINK DEFINITIONS --> - -[configuration]: ../configuration.md -[Reference]: #reference -[build]: #buildproject -[setup]: #setupenv -[Reporting]: ../reporting.md - -<!-- Abbreviations --> - -*[MFTF]: Magento Functional Testing Framework diff --git a/docs/configuration.md b/docs/configuration.md index 9466f2bcc..de7fd5859 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,12 +1,12 @@ # Configuration The `*.env` file provides additional configuration for the Magento Functional Testing Framework (MFTF). -To run the MFTF on your Magento instance, specify the basic configuration values. +To run MFTF on your Magento instance, specify the basic configuration values. Advanced users can create custom configurations based on requirements and environment. ## Basic configuration -These basic configuration values are __required__ and must be set by the user before the MFTF can function correctly. +These basic configuration values are __required__ and must be set by the user before MFTF can function correctly. ### MAGENTO_BASE_URL @@ -32,29 +32,29 @@ Example: MAGENTO_BACKEND_NAME=admin_12346 ``` -### MAGENTO_ADMIN_USERNAME +### MAGENTO_BACKEND_BASE_URL -The username that tests can use to access the Magento Admin page +(Optional) If you are running the Admin Panel on a separate domain, specify this value: Example: ```conf -MAGENTO_ADMIN_USERNAME=admin +MAGENTO_BACKEND_BASE_URL=https://admin.magento2.test ``` -### MAGENTO_ADMIN_PASSWORD +### MAGENTO_ADMIN_USERNAME -The password that tests will use to log in to the Magento Admin page. +The username that tests can use to access the Magento Admin page Example: ```conf -MAGENTO_ADMIN_PASSWORD=1234reTyt%$7 +MAGENTO_ADMIN_USERNAME=admin ``` ## Advanced configuration -Depending on the environment you use, you may need to configure the MFTF more precisely by setting more configuration parameters then for basic configuration. +Depending on the environment you use, you may need to configure MFTF more precisely by setting additional configuration parameters. This section describes available configuration parameters and their default values (where applicable). ### DEFAULT_TIMEZONE @@ -163,8 +163,8 @@ MAGENTO_RESTAPI_SERVER_PORT=5000 ### \*_BP Settings to override base paths for the framework. -You can use it when the MFTF is applied as a separate tool. -For example, when you need to place the MFTF and the Magento codebase in separate projects. +You can use it when MFTF is applied as a separate tool. +For example, when you need to place MFTF and the Magento codebase in separate projects. ```conf MAGENTO_BP @@ -211,18 +211,18 @@ The path to where the MFTF modules mirror Magento modules. Example: ```conf -TESTS_MODULE_PATH=~/magento2/dev/tests/acceptance/tests/functional/Magento/FunctionalTest +TESTS_MODULE_PATH=~/magento2/dev/tests/acceptance/tests/functional/Magento ``` -### MODULE_WHITELIST +### MODULE_ALLOWLIST Use for a new module. -When adding a new directory at `Magento/FunctionalTest`, add the directory name to `MODULE_WHITELIST` to enable the MFTF to process it. +When adding a new directory at `tests/functional/Magento`, add the directory name to `MODULE_ALLOWLIST` to enable MFTF to process it. Example: ```conf -MODULE_WHITELIST=Magento_Framework,Magento_ConfigurableProductWishlist,Magento_ConfigurableProductCatalogSearch +MODULE_ALLOWLIST=Magento_Framework,Magento_ConfigurableProductWishlist,Magento_ConfigurableProductCatalogSearch ``` ### MAGENTO_CLI_COMMAND_PATH @@ -235,7 +235,7 @@ It points to `MAGENTO_BASE_URL` + `dev/tests/acceptance/utils/command.php` Modify the default value: - for non-default Magento installation -- when use a subdirectory in the `MAGENTO_BASE_URL` +- when using a subdirectory in the `MAGENTO_BASE_URL` Example: `dev/tests/acceptance/utils/command.php` @@ -299,6 +299,20 @@ Example: CREDENTIAL_AWS_SECRETS_MANAGER_PROFILE=default ``` +### VERBOSE_ARTIFACTS + +Determines if passed tests should still have all their Allure artifacts. These artifacts include `.txt` attachments for `dontSee` actions and `createData` actions. + +If enabled, all tests will have all of their normal Allure artifacts. + +If disabled, passed tests will have their Allure artifacts trimmed. Failed tests will still contain all their artifacts. + +This is set `false` by default. + +```conf +VERBOSE_ARTIFACTS=true +``` + ### ENABLE_BROWSER_LOG Enables addition of browser logs to Allure steps @@ -307,14 +321,24 @@ Enables addition of browser logs to Allure steps ENABLE_BROWSER_LOG=true ``` -### BROWSER_LOG_BLACKLIST +### SELENIUM_CLOSE_ALL_SESSIONS + +Forces MFTF to close all Selenium sessions after running a suite. + +Use this if you're having issues with sessions hanging in an MFTF suite. + +```conf +SELENIUM_CLOSE_ALL_SESSIONS=true +``` -Blacklists types of browser log entries from appearing in Allure steps. +### BROWSER_LOG_BLOCKLIST + +Blocklists types of browser log entries from appearing in Allure steps. Denoted in browser log entry as `"SOURCE": "type"`. ```conf -BROWSER_LOG_BLACKLIST=other,console-api +BROWSER_LOG_BLOCKLIST=other,console-api ``` ### WAIT_TIMEOUT @@ -325,9 +349,88 @@ Global MFTF configuration for the default amount of time (in seconds) that a tes WAIT_TIMEOUT=30 ``` +### ENABLE_PAUSE + +Enables the ability to pause test execution at any point, and enter an interactive shell where you can try commands in action. +When pause is enabled, MFTF will generate pause() command in _failed() hook so that test will pause execution when failed. + +```conf +ENABLE_PAUSE=true +``` + +### REMOTE_STORAGE_AWSS3_DRIVER + +The remote storage driver. To enable AWS S3, use `aws-s3`. + +Example: + +```conf +REMOTE_STORAGE_AWSS3_DRIVER=aws-s3 +``` + +### REMOTE_STORAGE_AWSS3_REGION + +The region of S3 bucket. + +Example: + +```conf +REMOTE_STORAGE_AWSS3_REGION=us-west-2 +``` + +### REMOTE_STORAGE_AWSS3_BUCKET + +The name of S3 bucket. + +Example: + +```conf +REMOTE_STORAGE_AWSS3_BUCKET=my-test-bucket +``` + +### REMOTE_STORAGE_AWSS3_PREFIX + +The optional prefix inside S3 bucket. + +Example: + +```conf +REMOTE_STORAGE_AWSS3_PREFIX=local +``` + +### REMOTE_STORAGE_AWSS3_ACCESS_KEY + +The optional access key for the S3 bucket. + +Example: + +```conf +REMOTE_STORAGE_AWSS3_ACCESS_KEY=access-key +``` + +### REMOTE_STORAGE_AWSS3_SECRET_KEY + +The optional secret key for the S3 bucket. + +Example: + +```conf +REMOTE_STORAGE_AWSS3_SECRET_KEY=secret-key +``` + +### MAGENTO_ADMIN_WEBAPI_TOKEN_LIFETIME + +The lifetime (in seconds) of Magento Admin WebAPI token; if token is older than this value a refresh attempt will be made just before the next WebAPI call. + +Example: + +```conf +MAGENTO_ADMIN_WEBAPI_TOKEN_LIFETIME=10800 +``` + <!-- Link definitions --> [`MAGENTO_CLI_COMMAND_PATH`]: #magento_cli_command_path [generateDate]: test/actions.md#generatedate [mftf]: commands/mftf.md -[timezones]: http://php.net/manual/en/timezones.php +[timezones]: https://php.net/manual/en/timezones.php diff --git a/docs/credentials.md b/docs/credentials.md deleted file mode 100644 index 402030985..000000000 --- a/docs/credentials.md +++ /dev/null @@ -1,280 +0,0 @@ -# Credentials - -When you test functionality that involves external services such as UPS, FedEx, PayPal, or SignifyD, -use the MFTF credentials feature to hide sensitive [data][] like integration tokens and API keys. - -Currently the MFTF supports three types of credential storage: - -- **.credentials file** -- **HashiCorp Vault** -- **AWS Secrets Manager** - -## Configure File Storage - -The MFTF creates a sample file for credentials during [initial setup][]: `magento2/dev/tests/acceptance/.credentials.example`. -The file contains an example list of keys for fields that can require credentials. - -### Create `.credentials` - -To make the MFTF process the file with credentials, in the command line, navigate to `magento2/dev/tests/acceptance/` and rename `.credentials.example` to `.credentials`. - -```bash -cd dev/tests/acceptance/ -``` - -```bash -cp .credentials.example .credentials -``` - -### Add `.credentials` to `.gitignore` - -Verify that the file is excluded from tracking by `.gitignore` (unless you need this behavior): - -```bash -git check-ignore .credentials -``` - -The command outputs the path if the file is excluded: - -```terminal -.credentials -``` - -### Define sensitive data in the `.credentials` file - -Open the `.credentials` file and, for Magento core credentials, uncomment the fields you want to use and add your values: - -```conf -... -# Credentials for the USPS service -magento/carriers_usps_userid=usps_test_user -magento/carriers_usps_password=Lmgxvrq89uPwECeV - -# Credentials for the DHL service -#magento/carriers_dhl_id_us=dhl_test_user -#magento/carriers_dhl_password_us=Mlgxv3dsagVeG -.... -``` - -Or add new key/value pairs for your own credentials. The keys use the following format: - -```conf -<vendor>/<key_name>=<key_value> -``` - -<div class="bs-callout bs-callout-info" markdown="1"> -The `/` symbol is not supported in a `key_name` other than the one after your vendor or extension name. -</div> - -Otherwise you are free to use any other `key_name` you like, as they are merely the keys to reference from your tests. - -```conf -# Credentials for the MyAwesome service -vendor/my_awesome_service_token=rRVSVnh3cbDsVG39oTMz4A -``` - -## Configure Vault Storage - -Hashicorp vault secures, stores, and tightly controls access to data in modern computing. -It provides advanced data protection for your testing credentials. - -The MFTF works with both `vault enterprise` and `vault open source` that use `KV Version 2` secret engine. - -### Install vault CLI - -Download and install vault CLI tool if you want to run or develop MFTF tests locally. [Download Vault][Download Vault] - -### Authenticate to vault via vault CLI - -Authenticate to vault server via the vault CLI tool: [Login Vault][Login Vault]. - -```bash -vault login -method -path -``` - -**Do not** use `-no-store` command option, as the MFTF will rely on the persisted token in the token helper (usually the local filesystem) for future API requests. - -### Store secrets in vault - -The MFTF uses the `KV Version 2` secret engine for secret storage. -More information for working with `KV Version 2` can be found in [Vault KV2][Vault KV2]. - -#### Secrets path and key convention - -The path and key for secret data must follow the format: - -```conf -<SECRETS_BASE_PATH>/mftf/<VENDOR>/<SECRET_KEY> -``` - -```conf -# Secret path and key for carriers_usps_userid -secret/mftf/magento/carriers_usps_userid - -# Secret path and key for carriers_usps_password -secret/mftf/magento/carriers_usps_password -``` - -#### Write secrets to vault - -You can use vault CLI or API to write secret data (credentials, etc) to vault. Here is a CLI example: - -```bash -vault kv put secret/mftf/magento/carriers_usps_userid carriers_usps_userid=usps_test_user -vault kv put secret/mftf/magento/carriers_usps_password carriers_usps_password=Lmgxvrq89uPwECeV -``` - -### Setup MFTF to use vault - -Add vault configuration environment variables [`CREDENTIAL_VAULT_ADDRESS`][] and [`CREDENTIAL_VAULT_SECRET_BASE_PATH`][] -from `etc/config/.env.example` in `.env`. -Set values according to your vault server configuration. - -```conf -# Default vault dev server -CREDENTIAL_VAULT_ADDRESS=http://127.0.0.1:8200 -CREDENTIAL_VAULT_SECRET_BASE_PATH=secret -``` - -## Configure AWS Secrets Manager - -AWS Secrets Manager offers secret management that supports: -- Secret rotation with built-in integration for Amazon RDS, Amazon Redshift, and Amazon DocumentDB -- Fine-grained policies and permissions -- Audit secret rotation centrally for resources in the AWS Cloud, third-party services, and on-premises - -### Prerequisites - -#### Use AWS Secrets Manager from your own AWS account - -- An AWS account with Secrets Manager service -- An IAM user with AWS Secrets Manager access permission - -#### Use AWS Secrets Manager in CI/CD - -- AWS account ID where the AWS Secrets Manager service is hosted -- Authorized CI/CD EC2 instances with AWS Secrets Manager service access IAM role attached - -### Store secrets in AWS Secrets Manager - -#### Secrets format - -`Secret Name` and `Secret Value` are two key pieces of information for creating a secret. - -`Secret Value` can be either plaintext or key/value pairs in JSON format. - -`Secret Name` must use the following format: - -```conf -mftf/<VENDOR>/<YOUR/SECRET/KEY> -``` - -`Secret Value` can be stored in two different formats: plaintext or key/value pairs. - -For plaintext format, `Secret Value` can be any string you want to secure. - -For key/value pairs format, `Secret Value` is a key/value pair with `key` the same as `Secret Name` without `mftf/<VENDOR>/` prefix, which is `<YOUR/SECRET/KEY>`, and value can be any string you want to secure. - -##### Create Secrets using AWS CLI - -```bash -aws secretsmanager create-secret --name "mftf/magento/shipping/carriers_usps_userid" --description "Carriers USPS user id" --secret-string "1234567" -``` - -##### Create Secrets using AWS Console - -- Sign in to the AWS Secrets Manager console -- Choose Store a new secret -- In the Select secret type section, specify "Other type of secret" -- For `Secret Name`, `Secret Key` and `Secret Value` field, for example, to save the same secret in key/value JSON format, you should use - -```conf -# Secret Name -mftf/magento/shipping/carriers_usps_userid - -# Secret Key -shipping/carriers_usps_userid - -# Secret Value -1234567 -``` - -### Setup MFTF to use AWS Secrets Manager - -To use AWS Secrets Manager, the AWS region to connect to is required. You can set it through environment variable [`CREDENTIAL_AWS_SECRETS_MANAGER_REGION`][] in `.env`. - -MFTF uses the recommended [Default Credential Provider Chain][credential chain] to establish connection to AWS Secrets Manager service. -You can setup credentials according to [Default Credential Provider Chain][credential chain] and there is no MFTF specific setup required. -Optionally, however, you can explicitly set AWS profile through environment variable [`CREDENTIAL_AWS_SECRETS_MANAGER_PROFILE`][] in `.env`. - -```conf -# Sample AWS Secrets Manager configuration -CREDENTIAL_AWS_SECRETS_MANAGER_REGION=us-east-1 -CREDENTIAL_AWS_SECRETS_MANAGER_PROFILE=default -``` - -### Optionally set CREDENTIAL_AWS_ACCOUNT_ID environment variable - -In case AWS credentials cannot resolve to a valid AWS account, full AWS KMS ([Key Management Service][]) key ARN ([Amazon Resource Name][]) is required. -You will also need to set `CREDENTIAL_AWS_ACCOUNT_ID` environment variable so that MFTF can construct the full ARN. This is mostly used for CI/CD. - -```bash -export CREDENTIAL_AWS_ACCOUNT_ID=<Your_12_Digits_AWS_Account_ID> -``` - -## Configure multiple credential storage - -It is possible and sometimes useful to setup and use multiple credential storage at the same time. -In this case, the MFTF tests are able to read secret data at runtime from all storage options, in this case MFTF use the following precedence: - -``` -.credentials File > HashiCorp Vault > AWS Secrets Manager -``` -<!-- {% raw %} --> - -## Use credentials in a test - -Credentials can be used in actions: [`fillField`][], [`magentoCLI`][], and [`createData`][]. - -Define the value as a reference to the corresponding key in the credentials file or vault such as `{{_CREDS.my_data_key}}`: - -- `_CREDS` is an environment constant pointing to the `.credentials` file -- `my_data_key` is a key in the the `.credentials` file or vault that contains the value to be used in a test step - - for File Storage, ensure your key contains the vendor prefix, which is `vendor/my_data_key` - -For example, to reference secret data in the [`fillField`][] action, use the `userInput` attribute using a typical File Storage: - -```xml -<fillField stepKey="FillApiToken" selector=".api-token" userInput="{{_CREDS.vendor/my_data_key}}" /> -``` - -<!-- {% endraw %} --> - -## Implementation details - -The generated tests do not contain credentials values. -The MFTF dynamically retrieves, encrypts, and decrypts the sensitive data during test execution. -Decrypted credentials do not appear in the console, error logs, or [test reports][]. -The decrypted values are only available in the `.credentials` file or within vault. - -<div class="bs-callout bs-callout-info"> -The MFTF tests delivered with Magento application do not use credentials and do not cover external services, because of sensitivity of the data. -</div> - -<!-- Link definitions --> -[`fillField`]: test/actions.md#fillfield -[`magentoCLI`]: test/actions.md#magentocli -[`createData`]: test/actions.md#createdata -[data]: data.md -[initial setup]: getting-started.md -[test reports]: reporting.md -[Download Vault]: https://www.hashicorp.com/products/vault/ -[Login Vault]: https://www.vaultproject.io/docs/commands/login.html -[Vault KV2]: https://www.vaultproject.io/docs/secrets/kv/kv-v2.html -[`CREDENTIAL_VAULT_ADDRESS`]: configuration.md#credential_vault_address -[`CREDENTIAL_VAULT_SECRET_BASE_PATH`]: configuration.md#credential_vault_secret_base_path -[credential chain]: https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/guide_credentials.html -[`CREDENTIAL_AWS_SECRETS_MANAGER_PROFILE`]: configuration.md#credential_aws_secrets_manager_profile -[`CREDENTIAL_AWS_SECRETS_MANAGER_REGION`]: configuration.md#credential_aws_secrets_manager_region -[Key Management Service]: https://aws.amazon.com/kms/ -[Amazon Resource Name]: https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html \ No newline at end of file diff --git a/docs/data.md b/docs/data.md deleted file mode 100644 index fa4c90dcf..000000000 --- a/docs/data.md +++ /dev/null @@ -1,288 +0,0 @@ -# Input testing data - -The MFTF enables you to specify and use `<data>` entities defined in XML. Default `<data>` entities are provided for use and as templates for entity creation and manipulation. -The following diagram shows the XML structure of an MFTF data object: - -![MFTF Data Object](img/data-dia.svg) - -<!-- {% raw %} --> - -## Supply data to test by reference to a data entity - -Test steps requiring `<data>` input in an action, like filling a field with a string, may reference an attribute from a data entity: - -```xml -userInput="{{SimpleSubCategory.name}}" -``` - -In this example: - -* `SimpleSubCategory` is an entity name. -* `name` is a `<data>` key of the entity. The corresponding value will be assigned to `userInput` as a result. - -### Environmental data - -```xml -userInput="{{_ENV.MAGENTO_ADMIN_USERNAME}}" -``` - -In this example: - -* `_ENV` is a reference to the `dev/tests/acceptance/.env` file, where basic environment variables are set. -* `MAGENTO_ADMIN_USERNAME` is a name of an environment variable. - The corresponding value will be assigned to `userInput` as a result. - -### Sensitive data - -```xml -userInput="{{_CREDS.my_secret_token}}" -``` - -In this example: - -* `_CREDS` is a constant to reference to the `dev/tests/acceptance/.credentials` file, where sensitive data and secrets are stored for use in a test. -* `MY_SECRET_TOKEN` is the name of a key in the credentials variable. - The corresponding value of the credential will be assigned to `userInput` as a result. -* The decrypted values are only available in the `.credentials` file in which they are stored. - -Learn more in [Credentials][]. - -## Persist a data entity as a prerequisite of a test {#persist-data} - -A test can specify an entity to be persisted (created in the database) so that the test actions could operate on the existing known data. - -Example of referencing `data` in a test: - -```xml -userInput="$createCustomer.email$" -``` - -In this example: - -* `createCustomer` is a step key of the corresponding test step that creates an entity. -* `email` is a data key of the entity. - The corresponding value will be assigned to `userInput` as a result. - -<div class="bs-callout bs-callout-info"> -As of MFTF 2.3.6, you no longer need to differentiate between scopes (a test, a hook, or a suite) for persisted data when referencing it in tests. -</div> - -The MFTF now stores the persisted data and attempts to retrieve it using the combination of `stepKey` and the scope of where it has been called. -The current scope is preferred, then widening to _test > hook > suite_ or _hook > test > suite_. - -This emphasizes the practice for the `stepKey` of `createData` to be descriptive and unique, as a duplicated `stepKey` in both a `<test>` and `<before>` prefers the `<test>` data. - -## Use data returned by test actions - -A test can also reference data that was returned as a result of [test actions][], like the action `<grabValueFrom selector="someSelector" stepKey="grabStepKey>`. - -Further in the test, the data grabbed by the `someSelector` selector can be referenced using the `stepKey` value. In this case, it is `grabStepKey`. - -The following example shows the usage of `grabValueFrom` in testing, where the returned value is used by action's `stepKey`: - -```xml -<grabValueFrom selector="someSelector" stepKey="grabStepKey"/> -<fillField selector=".functionalTestSelector" userInput="{$grabStepKey}" stepKey="fillFieldKey1"/> -``` - -The following is an example of the `Magento/Catalog/Test/Mftf/ActionGroup/AssertDiscountsPercentageOfProductsActionGroup.xml` test: - -```xml -<grabValueFrom selector="{{AdminProductFormAdvancedPricingSection.productTierPricePercentageValuePriceInput('0')}}" stepKey="grabProductTierPriceInput"/> -<assertEquals stepKey="assertProductTierPriceInput"> - <expectedResult type="string">{{amount}}</expectedResult> - <actualResult type="string">$grabProductTierPriceInput</actualResult> -</assertEquals> -``` - -## Hard-coded data input - -The data to operate against can be included as literals in a test. Hard-coded data input can be useful in assertions. - -See also [Actions][]. - -```xml -userInput="We'll email you an order confirmation with details and tracking info." -``` - -## Format - -The format of `<data>` is: - -```xml -<?xml version="1.0" encoding="UTF-8"?> - -<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> - <entity name="" type=""> - <data key=""></data> - </entity> - <entity name="" type=""> - <data key="" unique=""></data> - <var key="" entityType="" entityKey=""/> - </entity> -</entities> -``` - -## Principles - -The following conventions apply to MFTF `<data>`: - -* A `<data>` file may contain multiple data entities. -* Camel case is used for `<data>` elements. The name represents the `<data>` type. For example, a file with customer data is `CustomerData.xml`. A file for simple product would be `SimpleProductData.xml`. -* Camel case is used for the entity name. -* The file name must have the suffix `Data.xml`. - -## Example - -Example (`.../Catalog/Data/CategoryData.xml` file): - -```xml -<?xml version="1.0" encoding="UTF-8"?> - -<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> - <entity name="_defaultCategory" type="category"> - <data key="name" unique="suffix">simpleCategory</data> - <data key="name_lwr" unique="suffix">simplecategory</data> - <data key="is_active">true</data> - </entity> - <entity name="SimpleSubCategory" type="category"> - <data key="name" unique="suffix">SimpleSubCategory</data> - <data key="name_lwr" unique="suffix">simplesubcategory</data> - <data key="is_active">true</data> - <data key="include_in_menu">true</data> - </entity> -</entities> -``` - -This example declares two `<data>` entities: `_defaultCategory` and `SimpleSubCategory`. They set the data required for [category creation][]. - -All entities that have the same name will be merged during test generation. Both entities are of the `category` type. - -`_defaultCategory` sets three data fields: - -* `name` defines the category name as `simpleCategory` with a unique suffix. Example: `simpleCategory598742365`. -* `name_lwr` defines the category name in lowercase format with a unique suffix. Example: `simplecategory697543215`. -* `is_active` sets the enable category to `true`. - -`SimpleSubCategory` sets four data fields: - -* `name` that defines the category name with a unique suffix. Example: `SimpleSubCategory458712365`. -* `name_lwr` that defines the category name in lowercase format with a unique suffix. Example: `simplesubcategory753698741`. -* `is_active` sets the enable category to `true`. -* `include_in_menu` that sets the include in the menu to `true`. - -The following is an example of a call in test: - -```xml -<fillField selector="{{AdminCategoryBasicFieldSection.categoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="enterCategoryName"/> -``` - -<!-- {% endraw %} --> - -This action inputs data from the `name` of the `_defaultCategory` entity (for example, `simpleCategory598742365`) into the field with the locator defined in the selector of the `categoryNameInput` element of the `AdminCategoryBasicFieldSection`. - -You can also call data from the xml definition of a `data` tag directly: - -```xml -<entity name="NewAdminUser" type="user"> - <data key="username" unique="suffix">admin</data> - <data key="current_password">{{AnotherUser.current_password}}</data> <!-- Data from another entity --> - <data key="current_password">{{_ENV.MAGENTO_ADMIN_PASSWORD}}</data> <!-- ENV file reference --> -</entity> -``` - -## Reference - -### entities {#entities-tag} - -`<entities>` is an element that contains all `<entity>` elements. - -### entity {#entity-tag} - -`<entity>` is an element that contains `<data>` elements. - -Attributes|Type|Use|Description ----|---|---|--- -`name`|string|optional|Name of the `<entity>`. -`type`|string|optional|Node containing the exact name of `<entity>` type. Used later to find specific Persistence Layer Model class. `type` in `<data>` can be whatever the user wants; There are no constraints. It is important when persisting data, depending on the `type` given, as it will try to match a metadata definition with the operation being done. Example: A `myCustomer` entity with `type="customer"`, calling `<createData entity="myCustomer"/>`, will try to find a metadata entry with the following attributes: `<operation dataType="customer" type="create">`. -`deprecated`|string|optional|Used to warn about the future deprecation of the data entity. String will appear in Allure reports and console output at runtime. - -`<entity>` may contain one or more [`<data>`][], [`<var>`][], [`<required-entities>`][], or [`<array>`][] elements in any sequence. - -### data {#data-tag} - -`<data>` is an element containing a data/value pair. - -Attributes|Type|Use|Description ----|---|---|--- -`key`|string|optional|Key attribute of data/value pair. -`unique`|enum: `"prefix"`, `"suffix"`|optional|Add suite or test wide unique sequence as "prefix" or "suffix" to the data value if specified. - -### var {#var-tag} - -`<var>` is an element that can be used to grab a key value from another entity. For example, when creating a customer with the `<createData>` action, the server responds with the auto-incremented ID of that customer. Use `<var>` to access that ID and use it in another data entity. - -Attributes|Type|Use|Description ----|---|---|--- -`key`|string|optional|Key attribute of this entity to assign a value to. -`entityType`|string|optional|Type attribute of referenced entity. -`entityKey`|string|optional|Key attribute of the referenced entity from which to get a value. -`unique`|--|--|*This attribute hasn't been implemented yet.* - -### requiredEntity {#requiredentity-tag} - -`<requiredEntity>` is an element that specifies the parent/child relationship between complex types. - -Example: You have customer address info. To specify that relationship: - -```xml -<entity name="CustomerEntity" type="customer"> - ... - <requiredEntity type="address">AddressEntity</requiredEntity> - ... -</entity> -``` - -Attributes|Type|Use|Description ----|---|---|--- -`type`|string|optional|Type attribute of `<requiredEntity>`. - -### array {#array-tag} - -`<array>` is an element that contains a reference to an array of values. - -Example: - -```xml -<entity name="AddressEntity" type="address"> - ... - <array key="street"> - <item>7700 W Parmer Ln</item> - <item>Bld D</item> - </array> - ... -</entity> -``` - -Attributes|Type|Use|Description ----|---|---|--- -`key`|string|required|Key attribute of this entity in which to assign a value. - -`<array>` may contain [`<item>`][] elements. - -### item {#item-tag} - -`<item>` is an individual piece of data to be passed in as part of the parent `<array>` type. - -<!-- Link Definitions --> -[`<array>`]: #array-tag -[`<data>`]: #data-tag -[`<item>`]: #item-tag -[`<required-entities>`]: #requiredentity-tag -[`<var>`]: #var-tag -[Actions]: ./test/actions.md -[category creation]: http://docs.magento.com/m2/ce/user_guide/catalog/category-create.html -[Credentials]: ./credentials.md -[test actions]: ./test/actions.md#actions-returning-a-variable diff --git a/docs/debugging.md b/docs/debugging.md deleted file mode 100644 index be17e952a..000000000 --- a/docs/debugging.md +++ /dev/null @@ -1,37 +0,0 @@ -# Debugging - -Debugging within the Magento Functional Testing Framework is helpful in identifying test bugs by allowing you to pause execution so that you may: - -- Examine the page. -- Check returned data and other variables being used during run-time. - -This is straightforward to do once you create a basic Debug Configuration. - -## Prerequisites - -- [Xdebug][] -- PHPUnit configured for use in [PHPStorm][] - -## Creating Debug Configuration with PHPStorm - -1. If not already installed, download the Codeception Framework plugin for PHPStorm (`PhpStorm->Preferences->Plugins`). -1. Click `Edit Configurations` on the configuration dropdown. -1. Click `+` and select `Codeception` from the available types. -1. Change `Test Scope` to `Type` and select `functional` from the `Type:` dropdown. -1. Find the `Custom Working Directory` option and set the path to your `dev/tests/acceptance/` directory. - -If you get a warning `Path to Codeception for local machine is not configured.`: - -1. Click `Fix`, then `+`, and select `Codeception Local`. -1. Click `...` and locate `/vendor/bin/codecept` in your Magento installation folder. - -The easiest method of tagging a test for debugging is the following: - -- In your Debug configuration, locate `Test Runner options:` and set `--group testDebug`. -- When you want to debug a test you are working on, simply add `<group value="testDebug"/>` to the annotations. Be sure to remove this after done debugging. - -Your Debug Configuration should now be able to run your test and pause execution on any breakpoints you have set in the generated `.php` file under the `_generated` folder. - -<!-- Link definitions --> -[Xdebug]: https://xdebug.org/docs/install -[PHPStorm]: https://www.jetbrains.com/phpstorm/ diff --git a/docs/extending.md b/docs/extending.md deleted file mode 100644 index 064ed3208..000000000 --- a/docs/extending.md +++ /dev/null @@ -1,369 +0,0 @@ -# Extending - -There are cases when you need to create many tests that are very similar to each other. -For example, only one or two parameters (for example, URL) might vary between tests. -To avoid copy-pasting and to save some time the Magento Functional Testing Framework (MFTF) enables you to extend test components such as [test], [data], and [action group]. -You can create or update any component of the parent body in your new test/action group/entity. - -* A test starting with `<test name="SampleTest" extends="ParentTest">` creates a test `SampleTest` that takes body of existing test `ParentTest` and adds to it the body of `SampleTest`. -* An action group starting with `<actionGroup name="SampleActionGroup" extends="ParentActionGroup">` creates an action group based on the `ParentActionGroup`, but with the changes specified in `SampleActionGroup`. -* An entity starting with `<entity name="SampleEntity" extends="ParentEntity">` creates an entity `SampleEntity` that is equivalent to merging the `SampleEntity` with the `ParentEntity`. - -Specify needed variations for a parent object and produce a copy of the original that incorporates the specified changes (the "delta"). - -<div class="bs-callout bs-callout-info"> -Unlike merging, the parent test (or action group) will still exist after the test generation. -</div> - -## Extending tests - -### Update a test step - -<!-- {% raw %} --> - -__Use case__: Create two similar tests with different `url` (`"{{AdminCategoryPage.url}}"` and `"{{OtherCategoryPage.url}}"`) in a test step. - -> Test with "extends": - -```xml -<tests> - <test name="AdminCategoryTest"> - <annotations> - ... - </annotations> - ...(several steps) - <amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToAdminCategory"/> - ...(several steps) - </test> - <test name="OtherCategoryTest" extends="AdminCategoryTest"> - <annotations> - ... - </annotations> - <amOnPage url="{{OtherCategoryPage.url}}" stepKey="navigateToAdminCategory"/> - </test> -</tests> -``` - -> Test without "extends": - -```xml -<tests> - <test name="AdminCategoryTest"> - <annotations> - ... - </annotations> - ...(several steps) - <amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToAdminCategory"/> - ...(several steps) - </test> - <test name="OtherCategoryTest"> - <annotations> - ... - </annotations> - ...(several steps) - <amOnPage url="{{OtherCategoryPage.url}}" stepKey="navigateToAdminCategory"/> - ...(several steps) - </test> -</tests> -``` - -### Add a test step - -__Use case__: Create two similar tests where the second test contains two additional steps: - -* `checkOption` before `click` (`stepKey="clickLogin"`) -* `seeInCurrentUrl` after `click` in the `LogInAsAdminTest` test (in the `.../Backend/Test/LogInAsAdminTest.xml` file) - -> Tests with "extends": - -```xml -<tests> - <test name="LogInAsAdminTest"> - <amOnPage url="{{AdminLoginPage}}" stepKey="navigateToAdmin"/> - <fillField selector="{{AdminLoginFormSection.username}}" userInput="admin" stepKey="fillUsername"/> - <fillField selector="{{AdminLoginFormSection.password}}" userInput="password" stepKey="fillPassword"/> - <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> - <see userInput="Lifetime Sales" stepKey="seeLifetimeSales"/> - </test> - <test name="AlternativeLogInAsAdminTest" extends="LogInAsAdminTest"> - <checkOption selector="{{AdminLoginFormSection.rememberMe}}" stepKey="checkRememberMe" before="clickLogin"/> - <seeInCurrentUrl url="admin/admin/dashboard/" stepKey="seeAdminUrl" after="clickLogin"/> - </test> -</tests> -``` - -> Tests without "extends": - -```xml -<tests> - <test name="LogInAsAdminTest"> - <amOnPage url="{{AdminLoginPage}}" stepKey="navigateToAdmin"/> - <fillField selector="{{AdminLoginFormSection.username}}" userInput="admin" stepKey="fillUsername"/> - <fillField selector="{{AdminLoginFormSection.password}}" userInput="password" stepKey="fillPassword"/> - <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> - <see userInput="Lifetime Sales" stepKey="seeLifetimeSales"/> - </test> - <test name="AlternativeLogInAsAdminTest"> - <amOnPage url="{{AdminLoginPage}}" stepKey="navigateToAdmin"/> - <fillField selector="{{AdminLoginFormSection.username}}" userInput="admin" stepKey="fillUsername"/> - <fillField selector="{{AdminLoginFormSection.password}}" userInput="password" stepKey="fillPassword"/> - <checkOption selector="{{AdminLoginFormSection.rememberMe}}" stepKey="checkRememberMe"/> - <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> - <seeInCurrentUrl url="admin/admin/dashboard/" stepKey="seeAdminUrl"/> - <see userInput="Lifetime Sales" stepKey="seeLifetimeSales"/> - </test> -</tests> -``` - -### Update a test annotation - -__Use case__: Create two similar tests where the second one contains two additional actions in the `before` hook: - -* `checkOption` before `click` (`stepKey="clickLogin"`) -* `seeInCurrentUrl` after `click` in the `LogInAsAdminTest` test (in the `.../Backend/Test/LogInAsAdminTest.xml` file) - -> Tests with "extends": - -```xml -<tests> - <test name="LogInAsAdminTest"> - <before> - <amOnPage url="{{AdminLoginPage}}" stepKey="navigateToAdmin"/> - <fillField selector="{{AdminLoginFormSection.username}}" userInput="admin" stepKey="fillUsername"/> - <fillField selector="{{AdminLoginFormSection.password}}" userInput="password" stepKey="fillPassword"/> - <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> - </before> - <see userInput="Lifetime Sales" stepKey="seeLifetimeSales"/> - </test> - <test name="AlternativeLogInAsAdminTest" extends="LogInAsAdminTest"> - <before> - <checkOption selector="{{AdminLoginFormSection.rememberMe}}" stepKey="checkRememberMe" before="clickLogin"/> - <seeInCurrentUrl url="admin/admin/dashboard/" stepKey="seeAdminUrl" after="clickLogin"/> - </before> - </test> -</tests> -``` - -> Tests without "extends": - -```xml -<tests> - <test name="LogInAsAdminTest"> - <before> - <amOnPage url="{{AdminLoginPage}}" stepKey="navigateToAdmin"/> - <fillField selector="{{AdminLoginFormSection.username}}" userInput="admin" stepKey="fillUsername"/> - <fillField selector="{{AdminLoginFormSection.password}}" userInput="password" stepKey="fillPassword"/> - <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> - </before> - <see userInput="Lifetime Sales" stepKey="seeLifetimeSales"/> - </test> - <test name="AlternativeLogInAsAdminTest"> - <before> - <amOnPage url="{{AdminLoginPage}}" stepKey="navigateToAdmin"/> - <fillField selector="{{AdminLoginFormSection.username}}" userInput="admin" stepKey="fillUsername"/> - <fillField selector="{{AdminLoginFormSection.password}}" userInput="password" stepKey="fillPassword"/> - <checkOption selector="{{AdminLoginFormSection.rememberMe}}" stepKey="checkRememberMe"/> - <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> - <seeInCurrentUrl url="admin/admin/dashboard/" stepKey="seeAdminUrl"/> - </before> - <see userInput="Lifetime Sales" stepKey="seeLifetimeSales"/> - </test> -</tests> -``` - -## Extending action groups - -Extend an [action group] to add or update [actions] in your module. - -### Update an action - -__Use case__: The `CountProductA` test counts the particular product. -Modify the action group to use another product. - -> Action groups with "extends": - -```xml -<actionGroups> - <actionGroup name="CountProductA"> - <arguments> - <argument name="count" type="string"/> - </arguments> - <grabMultiple selector="selectorForProductA" stepKey="grabProducts"/> - <assertCount stepKey="assertCount"> - <expectedResult type="int">{{count}}</expectedResult> - <actualResult type="variable">grabProducts</actualResult> - </assertCount> - </actionGroup> - - <actionGroup name="CountProductB" extends="CountProductA"> - <grabMultiple selector="selectorForProductB" stepKey="grabProducts"/> - </actionGroup> -</actionGroups> -``` - -> Action groups without "extends": - -```xml -<actionGroups> - <actionGroup name="CountProductA"> - <arguments> - <argument name="count" type="string"/> - </arguments> - <grabMultiple selector="selectorForProductA" stepKey="grabProducts"/> - <assertCount stepKey="assertCount"> - <expectedResult type="int">{{count}}</expectedResult> - <actualResult type="variable">grabProducts</actualResult> - </assertCount> - </actionGroup> - - <actionGroup name="CountProductB"> - <arguments> - <argument name="count" type="string"/> - </arguments> - <grabMultiple selector="selectorForProductB" stepKey="grabProducts"/> - <assertCount stepKey="assertCount"> - <expectedResult type="int">{{count}}</expectedResult> - <actualResult type="variable">grabProducts</actualResult> - </assertCount> - </actionGroup> -</actionGroups> -``` - -### Add an action - -__Use case__: The `GetProductCount` action group returns the count of products. -Add a new test `VerifyProductCount` that asserts the count of products: - -> Action groups with "extends": - -```xml -<actionGroups> - <actionGroup name="GetProductCount"> - <arguments> - <argument name="productSelector" type="string"/> - </arguments> - <grabMultiple selector="{{productSelector}}" stepKey="grabProducts"/> - </actionGroup> - - <actionGroup name="VerifyProductCount" extends="GetProductCount"> - <arguments> - <argument name="count" type="string"/> - </arguments> - <assertCount stepKey="assertCount" after="grabProducts"> - <expectedResult type="int">{{count}}</expectedResult> - <actualResult type="variable">grabProducts</actualResult> - </assertCount> - </actionGroup> -</actionGroups> -``` - -> Action groups without "extends": - -```xml -<actionGroups> - <actionGroup name="GetProductCount"> - <arguments> - <argument name="productSelector" type="string"/> - </arguments> - <grabMultiple selector="{{productSelector}}" stepKey="grabProducts"/> - </actionGroup> - - <actionGroup name="VerifyProductCount"> - <arguments> - <argument name="count" type="string"/> - <argument name="productSelector" type="string"/> - </arguments> - <grabMultiple selector="{{productSelector}}" stepKey="grabProducts"/> - <assertCount stepKey="assertCount"> - <expectedResult type="int">{{count}}</expectedResult> - <actualResult type="variable">grabProducts</actualResult> - </assertCount> - </actionGroup> -</actionGroups> -``` - -<!-- {% endraw %} --> - -## Extending data - -Extend data to reuse entities in your module. - -### Update a data entry - -__Use case__: Create an entity named `DivPanelGreen`, which is similar to the `DivPanel` entity, except that it is green. - -> Entities with "extends": - -```xml -<entities> - <entity name="DivPanel"> - <data key="divColor">Red</data> - <data key="divSize">80px</data> - <data key="divWidth">100%</data> - </entity> - <entity name="DivPanelGreen" extends="DivPanel"> - <data key="divColor">Green</data> - </entity> -</entities> -``` - -> Entities without "extends": - -```xml -<entities> - <entity name="DivPanel"> - <data key="divColor">Red</data> - <data key="divSize">80px</data> - <data key="divWidth">100%</data> - </entity> - <entity name="DivPanelGreen" extends="DivPanel"> - <data key="divColor">Green</data> - <data key="divSize">80px</data> - <data key="divWidth">100%</data> - </entity> -</entities> -``` - -### Add a data entry - -__Use case__: Create an entity named `DivPanelGreen`, which is similar to the `DivPanel` entity, except that it has a specific panel color. - -> Entities with "extends": - -```xml -<entities> - <entity name="DivPanel"> - <data key="divColor">Red</data> - <data key="divSize">80px</data> - <data key="divWidth">100%</data> - </entity> - <entity name="DivPanelGreen" extends="DivPanel"> - <data key="divColor">#000000</data> - <data key="AttributeHidden">True</data> - </entity> -</entities> -``` - -> Entities without "extends": - -```xml -<entities> - <entity name="DivPanel"> - <data key="divColor">Red</data> - <data key="divSize">80px</data> - <data key="divWidth">100%</data> - </entity> - <entity name="DivPanelGreen" extends="DivPanel"> - <data key="divColor">#000000</data> - <data key="divSize">80px</data> - <data key="divWidth">100%</data> - <data key="AttributeHidden">True</data> - </entity> -</entities> -``` - -<!-- Link definitions --> -[test]: ./test.md -[data]: ./data.md -[action group]: ./test/action-groups.md -[actions]: ./test/actions.md diff --git a/docs/getting-started.md b/docs/getting-started.md deleted file mode 100644 index 89d228df9..000000000 --- a/docs/getting-started.md +++ /dev/null @@ -1,359 +0,0 @@ -# Getting started - -<div class="bs-callout bs-callout-info" markdown="1"> -[Find your MFTF version][] of the MFTF. -The latest Magento 2.3.x release supports MFTF 2.5.3. -The latest Magento 2.2.x release supports MFTF 2.4.5. -</div> - -## Prepare environment {#prepare-environment} - -Make sure that you have the following software installed and configured on your development environment: - -- [PHP version supported by the Magento instance under test][php] -- [Composer 1.3 or later][composer] -- [Java 1.8 or later][java] -- [Selenium Server Standalone 3.1 or later][selenium server] and [ChromeDriver 2.33 or later][chrome driver] or other webdriver in the same directory - -<div class="bs-callout bs-callout-tip" markdown="1"> -[PhpStorm] supports [Codeception test execution][], which is helpful when debugging. -</div> - -## Install Magento {#install-magento} - -Use instructions below to install Magento. - -### Step 1. Clone the `magento2` source code repository {#clone-magento} - -```bash -git clone https://github.com/magento/magento2.git -``` - -or - -```bash -git clone git@github.com:magento/magento2.git -``` - -### Step 2. Install dependencies {#install-dependencies} - -Checkout the Magento version that you are going to test. - -```bash -cd magento2/ -``` - -```bash -git checkout 2.3-develop -``` - -Install the Magento application. - -```bash -composer install -``` - -## Prepare Magento {#prepare-magento} - -Configure the following settings in Magento as described below. - -### WYSIWYG settings {#wysiwyg-settings} - -A Selenium web driver cannot enter data to fields with WYSIWYG. - -To disable the WYSIWYG and enable the web driver to process these fields as simple text areas: - -1. Log in to the Magento Admin as an administrator. -2. Navigate to **Stores** > Settings > **Configuration** > **General** > **Content Management**. -3. In the WYSIWYG Options section set the **Enable WYSIWYG Editor** option to **Disabled Completely**. -4. Click **Save Config**. - -or via command line: - -```bash -bin/magento config:set cms/wysiwyg/enabled disabled -``` - -Clean the cache after changing the configuration values: - -```bash -bin/magento cache:clean config full_page -``` - -<div class="bs-callout bs-callout-tip"> -When you want to test the WYSIWYG functionality, re-enable WYSIWYG in your test suite. -</div> - -### Security settings {#security-settings} - -To enable the **Admin Account Sharing** setting, to avoid unpredictable logout during a testing session, and disable the **Add Secret Key in URLs** setting, to open pages using direct URLs: - -1. Navigate to **Stores** > Settings > **Configuration** > **Advanced** > **Admin** > **Security**. -2. Set **Admin Account Sharing** to **Yes**. -3. Set **Add Secret Key to URLs** to **No**. -4. Click **Save Config**. - -or via command line: - -```bash -bin/magento config:set admin/security/admin_account_sharing 1 -``` - -```bash -bin/magento config:set admin/security/use_form_key 0 -``` - -Clean the cache after changing the configuration values: - -```bash -bin/magento cache:clean config full_page -``` - -### Webserver configuration {#web-server-configuration} - -The MFTF does not support executing CLI commands if your web server points to `<MAGE_ROOT_DIR>/pub` directory as recommended in the [Installation Guide][Installation Guide docroot]. For the MFTF to execute the CLI commands, the web server must point to the Magento root directory. - -### Nginx settings {#nginx-settings} - -If the Nginx Web server is used on your development environment, then **Use Web Server Rewrites** setting in **Stores** > Settings > **Configuration** > **General** > **Web** > **Search Engine Optimization** must be set to **Yes**. - -To be able to run Magento command line commands in tests, add the following location block to the Nginx configuration file in the Magento root directory: - -```conf -location ~* ^/dev/tests/acceptance/utils($|/) { - root $MAGE_ROOT; - location ~ ^/dev/tests/acceptance/utils/command.php { - fastcgi_pass fastcgi_backend; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - include fastcgi_params; - } -} -``` - -## Set up an embedded MFTF {#setup-framework} - -This is the default setup of the MFTF that you would need to cover your Magento project with functional tests. -It installs the framework using an existing Composer dependency such as `magento/magento2-functional-testing-framework`. -If you want to set up the MFTF as a standalone tool, refer to [Set up a standalone MFTF][]. - -Install the MFTF. - -```bash -composer install -``` - -### Step 1. Build the project {#build-project} - -In the Magento project root, run: - -```bash -vendor/bin/mftf build:project -``` - -If you use PhpStorm, generate a URN catalog: - -```bash -vendor/bin/mftf generate:urn-catalog .idea/ -``` - -If the file does not exist, add the `--force` option to create it: - -```bash -vendor/bin/mftf generate:urn-catalog --force .idea/ -``` - -See [`generate:urn-catalog`][] for more details. - -<div class="bs-callout bs-callout-tip" markdown="1"> -You can simplify command entry by adding the absolute path to the `vendor/bin` directory path to your PATH environment variable. -After adding the path, you can run `mftf` without having to include `vendor/bin`. -</div> - -### Step 2. Edit environmental settings {#environment-settings} - -In the `magento2/dev/tests/acceptance/` directory, edit the `.env` file to match your system. - -```bash -vim dev/tests/acceptance/.env -``` - -Specify the following parameters, which are required to launch tests: - -- `MAGENTO_BASE_URL` must contain a domain name of the Magento instance that will be tested. - Example: `MAGENTO_BASE_URL=http://magento.test` - -- `MAGENTO_BACKEND_NAME` must contain the relative path for the Admin area. - Example: `MAGENTO_BACKEND_NAME=admin` - -- `MAGENTO_ADMIN_USERNAME` must contain the username required for authorization in the Admin area. - Example: `MAGENTO_ADMIN_USERNAME=admin` - -- `MAGENTO_ADMIN_PASSWORD` must contain the user password required for authorization in the Admin area. - Example: `MAGENTO_ADMIN_PASSWORD=123123q` - -<div class="bs-callout bs-callout-info" markdown="1"> -If the `MAGENTO_BASE_URL` contains a subdirectory like `http://magento.test/magento2ce`, specify `MAGENTO_CLI_COMMAND_PATH`. -</div> - -Learn more about environmental settings in [Configuration][]. - -### Step 3. Enable the Magento CLI commands - -In the `magento2/dev/tests/acceptance` directory, run the following command to enable the MFTF to send Magento CLI commands to your Magento instance. - - ```bash -cp dev/tests/acceptance/.htaccess.sample dev/tests/acceptance/.htaccess -``` - -### Step 4. Generate and run tests {#run-tests} - -To run tests, you need a running Selenium server and [`mftf`][] commands. - -#### Run the Selenium server {#selenium-server} - -Run the Selenium server in terminal. -For example, the following commands run the Selenium server for Google Chrome: - -```bash -cd <path_to_directory_with_selenium_server_and_webdriver>/ -``` - -```bash -java -Dwebdriver.chrome.driver=chromedriver -jar selenium-server-standalone-3.14.0.jar -``` - -#### Generate and run all tests {#run-all-tests} - -```bash -vendor/bin/mftf generate:tests -``` - -```bash -cd dev/tests/acceptance -``` - -```bash -vendor/bin/codecept run functional -c dev/tests/acceptance/codeception.yml -``` - -See more commands in [`codecept`][]. - -#### Run a simple test {#run-test} - -To clean up the previously generated tests, and then generate and run a single test `AdminLoginTest`, run: - -```bash -vendor/bin/mftf run:test AdminLoginTest --remove -``` - -See more commands in [`mftf`][]. - -### Step 5. Generate reports {#reports} - -During testing, the MFTF generates test reports in CLI. -You can generate visual representations of the report data using [Allure Framework][]. -To view the reports in GUI: - -- [Install Allure][] -- Run the tool to serve the artifacts in `dev/tests/acceptance/tests/_output/allure-results/`: - -```bash -allure serve dev/tests/acceptance/tests/_output/allure-results/ -``` - -Learn more about Allure in the [official documentation][allure docs]. - -## Set up a standalone MFTF - -The MFTF is a root level Magento dependency, but it is also available for use as a standalone application. -You may want to use a standalone application when you develop for or contribute to MFTF, which facilitates debugging and tracking changes. -These guidelines demonstrate how to set up and run Magento acceptance functional tests using standalone MFTF. - -### Prerequisites - -This installation requires a local instance of the Magento application. -The MFTF uses the [tests from Magento modules][mftf tests] as well as the `app/autoload.php` file. - -### Step 1. Clone the MFTF repository - -If you develop or contribute to the MFTF, it makes sense to clone your fork of the MFTF repository. -For contribution guidelines, refer to the [Contribution Guidelines for the Magento Functional Testing Framework][contributing]. - -### Step 2. Install the MFTF - -```bash -cd magento2-functional-testing-framework -``` - -```bash -composer install -``` - -### Step 3. Build the project - -```bash -bin/mftf build:project -``` - -### Step 4. Edit environment settings - -In the `dev/.env` file, define the [basic configuration][] and [`MAGENTO_BP`][] parameters. - -### Step 5. Enable the Magento CLI commands {#add-cli-commands} - -Copy the `etc/config/command.php` file into your Magento installation at `<magento root directory>/dev/tests/acceptance/utils/`. -Create the `utils/` directory, if you didn't find it. - -### Step 6. Remove the MFTF package dependency in Magento - -The MFTF uses the Magento `app/autoload.php` file to read Magento modules. -The MFTF dependency in Magento supersedes the standalone registered namespaces unless it is removed at a Composer level. - -```bash -composer remove magento/magento2-functional-testing-framework --dev -d <path to the Magento root directory> -``` - -### Step 7. Run a simple test - -Generate and run a single test that will check your logging to the Magento Admin functionality: - -```bash -bin/mftf run:test AdminLoginTest -``` - -You can find the generated test at `dev/tests/functional/tests/MFTF/_generated/default/`. - -### Step 8. Generate Allure reports - -The standalone MFTF generates Allure reports at `dev/tests/_output/allure-results/`. -Run the Allure server pointing to this directory: - -```bash -allure serve dev/tests/_output/allure-results/ -``` - -<!-- Link definitions --> - -[`codecept`]: commands/codeception.html -[`generate:urn-catalog`]: commands/mftf.html#generateurn-catalog -[`MAGENTO_BP`]: configuration.html#magento_bp -[`mftf`]: commands/mftf.html -[allure docs]: https://docs.qameta.io/allure/ -[Allure Framework]: http://allure.qatools.ru/ -[basic configuration]: configuration.html#basic-configuration -[chrome driver]: https://sites.google.com/a/chromium.org/chromedriver/downloads -[Codeception Test execution]: https://blog.jetbrains.com/phpstorm/2017/03/codeception-support-comes-to-phpstorm-2017-1/ -[composer]: https://getcomposer.org/download/ -[Configuration]: configuration.html -[contributing]: https://github.com/magento/magento2-functional-testing-framework/blob/develop/.github/CONTRIBUTING.md -[install Allure]: https://github.com/allure-framework/allure2#download -[java]: http://www.oracle.com/technetwork/java/javase/downloads/index.html -[mftf tests]: introduction.html#mftf-tests -[php]: https://devdocs.magento.com/guides/v2.3/install-gde/system-requirements-tech.html#php -[PhpStorm]: https://www.jetbrains.com/phpstorm/ -[selenium server]: https://www.seleniumhq.org/download/ -[Set up a standalone MFTF]: #set-up-a-standalone-mftf -[test suite]: suite.html -[Find your MFTF version]: introduction.html#find-your-mftf-version -[Installation Guide docroot]: https://devdocs.magento.com/guides/v2.3/install-gde/tutorials/change-docroot-to-pub.html diff --git a/docs/guides/action-groups.md b/docs/guides/action-groups.md deleted file mode 100644 index 30c531e4e..000000000 --- a/docs/guides/action-groups.md +++ /dev/null @@ -1,82 +0,0 @@ -# Action Group Best Practices - -We strive to write tests using only action groups. Fortunately, we have built up a large set of action groups to get started. We can make use of them and extend them for our own specific needs. In some cases, we may never even need to write action groups of our own. We may be able to simply chain together calls to existing action groups to implement our new test case. - -## Why use Action Groups? - -Action groups simplify maintainability by reducing duplication. Because they are re-usable building blocks, odds are that they are already made use of by existing tests in the Magento codebase. This proves their stability through real-world use. Take for example, the action group named `LoginAsAdmin`: - -```xml -<actionGroup name="LoginAsAdmin"> - <annotations> - <description>Login to Backend Admin using provided User Data. PLEASE NOTE: This Action Group does NOT validate that you are Logged In.</description> - </annotations> - <arguments> - <argument name="adminUser" type="entity" defaultValue="DefaultAdminUser"/> - </arguments> - - <amOnPage url="{{AdminLoginPage.url}}" stepKey="navigateToAdmin"/> - <fillField selector="{{AdminLoginFormSection.username}}" userInput="{{adminUser.username}}" stepKey="fillUsername"/> - <fillField selector="{{AdminLoginFormSection.password}}" userInput="{{adminUser.password}}" stepKey="fillPassword"/> - <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> - <closeAdminNotification stepKey="closeAdminNotification"/> -</actionGroup> -``` - -Logging in to the admin panel is one of the most used action groups. It is used around 1,500 times at the time of this writing. - -Imagine if this was not an action group and instead we were to copy and paste these 5 actions every time. In that scenario, if a small change was needed, it would require a lot of work. But with the action group, we can make the change in one place. - -## How to extend action groups - -Again using `LoginAsAdmin` as our example, we trim away metadata to clearly reveal that this action group performs 5 actions: - -```xml -<actionGroup name="LoginAsAdmin"> - ... - <amOnPage url="{{AdminLoginPage.url}}" .../> - <fillField selector="{{AdminLoginFormSection.username}}" .../> - <fillField selector="{{AdminLoginFormSection.password}}" .../> - <click selector="{{AdminLoginFormSection.signIn}}" .../> - <closeAdminNotification .../> -</actionGroup> -``` - -This works against the standard Magento admin panel login page. Bu imagine we are working on a Magento extension that adds a CAPTCHA field to the login page. If we create and activate this extension and then run all existing tests, we can expect almost everything to fail because the CAPTCHA field is left unfilled. - -We can overcome this by making use of MFTF's extensibility. All we need to do is to provide a "merge" that modifies the existing `LoginAsAdmin` action group. Our merge file will look like: - -```xml -<actionGroup name="LoginAsAdmin"> - <fillField selector="{{CaptchaSection.captchaInput}}" before="signIn" .../> -</actionGroup> -``` - -Because the name of this merge is also `LoginAsAdmin`, the two get merged together and an additional step happens everytime this action group is used. - -To continue this example, imagine someone else is working on a 'Two-Factor Authentication' extension and they also provide a merge for the `LoginAsAdmin` action group. Their merge looks similar to what we have already seen. The only difference is that this time we fill a different field: - -```xml -<actionGroup name="LoginAsAdmin"> - <fillField selector="{{TwoFactorSection.twoFactorInput}}" before="signIn" .../> -</actionGroup> -``` - -Bringing it all together, our resulting `LoginAsAdmin` action group becomes this: - -```xml -<actionGroup name="LoginAsAdmin"> - ... - <amOnPage url="{{AdminLoginPage.url}}" .../> - <fillField selector="{{AdminLoginFormSection.username}}" .../> - <fillField selector="{{AdminLoginFormSection.password}}" .../> - <fillField selector="{{CaptchaSection.captchaInput}}" .../> - <fillField selector="{{TwoFactorSection.twoFactorInput}}" .../> - <click selector="{{AdminLoginFormSection.signIn}}" .../> - <closeAdminNotification .../> -</actionGroup> -``` - -No one file contains this exact content as above, but instead all three files come together to form this action group. - -This extensibility can be applied in many ways. We can use it to affect existing Magento entities such as tests, action groups, and data. Not so obvious is that this tehcnique can be used within your own entities to make them more maintainable as well. diff --git a/docs/guides/git-vs-composer-install.md b/docs/guides/git-vs-composer-install.md deleted file mode 100644 index fd9006cc1..000000000 --- a/docs/guides/git-vs-composer-install.md +++ /dev/null @@ -1,83 +0,0 @@ -# Git vs Composer installation of Magento with MFTF - -Depending on how you plan to use Magnto code, there are different options for installing Magento. - -## GitHub Installation - -If you are contributing a pull request to the Magento 2 codebase, download Magento 2 from our GitHub repository. Contribution to the codebase is done using the 'fork and pull' model where contributors maintain their own fork of the repo. This repo is then used to submit a pull request to the base repo. - -Install guide: [GitHub Installation][] - -## Composer based Installation - -A Composer install downloads released packages of Magento 2 from the composer repo [https://repo.magento.com](https://repo.magento.com). - -All Magento modules and their MFTF tests are put under `<vendor>` directory, for convenience of 3rd party developers. With this setup, you can keep your custom modules separate from core modules. You can also develop modules in a separate VCS repository and add them to your `composer.json` which installs them into the `vendor` directory. - -Install guide: [Composer based Installation][] - -## MFTF Installation - -After installing your Magento project in either of the above ways, the composer dependency `magento/magento2-functional-testing-framework` downloads and installs MFTF. MFTF is embedded in your Magento 2 installation and will cover your project with functional tests. - -If you want to contribute a pull request into MFTF codebase, you will need to install MFTF in the [Standalone][] mode. - -## Managing modules - Composer vs GitHub - -### Via GitHub - -Cloning the Magento 2 git repository is a way of installing where you do not have to worry about matching your codebase with production. Your version control system generally holds and manages your `app/code` folder and you can do manual, ad-hoc development here. - -### Via Composer - -Magento advocates the use of composer for managing modules. When you install a module through composer, it is added to `vendor/<vendor-name>/<module>`. - -When developing your own module or adding MFTF tests to a module, you should not edit in `vendor` because a composer update could overwrite your changes. Instead, overwrite a module under `vendor` by adding files or cloning your module-specific Git repo to `app/code/<vendor-name>/<module>`. - -To distribute the module and its tests, you can initialize a git repo and create a [composer package][]. In this way others will be able to download and install your module and access your tests as a composer package, in their `<vendor>` folder. - -## MFTF test materials location - -- For GitHub installations, MFTF test materials are located in `<magento_root>/app/code/<vendor_name>/<module_name>/Test/Mftf/`. This is the directory for new tests or to maintain existing ones. -- For Composer-based installations, MFTF test materials are located at `<magento_root>/vendor/<vendor_name>/<module_name>/Test/Mftf/`. This is the directory to run tests fetched by Composer. - -The file structure under both paths is the same: - -```tree -<Path> -├── ActionGroup -│   └── ... -├── Data -│   └── ... -├── Metadata -│   └── ... -├── Page -│   └── ... -├── Section -│   └── ... -├── Suite -│   └── ... -└── Test - └── ... -``` - -## How ModuleResolver reads modules - -With either type of installation, all tests and test data are read and merged by MFTF's ModuleResolver in this order: - -1. `<magento_root>/app/code/<vendor_name>/<module_name>/Test/Mftf/` -1. `<magento_root>/vendor/<vendor_name>/<module_name>/Test/Mftf/` -1. `<magento_root>/dev/tests/acceptance/tests/functional/<vendor_name>/<module_name>/` - -## Conclusion - -There is no difference between having the test materials in `app/code` or in `/vendor`: it works the same. Composer-based installs may benefit teams when there is a need to match file systems in `development` and `production`. - -If you are a contributing developer with an understanding of Git and Composer commands, you can choose the GitHub installation method instead. - -<!-- Link definitions --> - -[Composer based Installation]: https://devdocs.magento.com/guides/v2.3/install-gde/composer.html -[GitHub Installation]: https://devdocs.magento.com/guides/v2.3/install-gde/prereq/dev_install.html -[Standalone]: ../getting-started.html#set-up-a-standalone-mftf -[composer package]: https://devdocs.magento.com/guides/v2.3/extension-dev-guide/package/package_module.html diff --git a/docs/guides/selectors.md b/docs/guides/selectors.md deleted file mode 100644 index d1441865a..000000000 --- a/docs/guides/selectors.md +++ /dev/null @@ -1,346 +0,0 @@ -# How To write good selectors - -Selectors are the atomic unit of test writing. They fit into the hierarchy like this: MFTF tests make use of action groups > which are made up of actions > which interact with page objects > which contain elements > which are specified by selectors. Because they are fundamental building blocks, we must take care when writing them. - -## What is a selector? - -A "selector" works like an address to an element in the Document Object Model (DOM). It specifies page elements and allows MFTF to interact with them. -By 'element' we mean things such as input fields, buttons, tables, divs, etc. -By 'interact' we mean actions such as click, fill field, etc. - -Selectors live inside of MFTF page objects and are meant to be highly re-usable amongst all tests. They can be written in either CSS or XPath. - -## Why are good selectors important? - -Good selectors are important because they are the most re-used component of functional testing. They are the lowest building blocks of tests; the foundation. If they are unstable then everything else built on top of them will inherit that instability. - -## How do I write good selectors? - -We could cover this subject with an infinite amount of documentation and some lessons only come from experience. This guide explains some DOs and DONTs to help you along the way towards selector mastery. - -### Inspecting the DOM - -To write a selector you need to be able to see the DOM and find the element within it. Fortunately you do not have to look at the entire DOM every time. Nor do you have to read it from top to bottom. Instead you can make use of your browsers built-in developer tools or go a step further and try out some popular browser extensions. - -See these links for more information about built-in browser developer tools: - -* [Chrome Developer Tools](https://developers.google.com/web/tools/chrome-devtools/) -* [Firefox Developer Tools](https://developer.mozilla.org/en-US/docs/Tools) - -See these links for common browser addons that may offer advantages over browser developer tools: - -* [Live editor for CSS, Less & Sass - Magic CSS](https://chrome.google.com/webstore/detail/live-editor-for-css-less/ifhikkcafabcgolfjegfcgloomalapol?hl=en) -* [XPath Helper](https://chrome.google.com/webstore/detail/xpath-helper/hgimnogjllphhhkhlmebbmlgjoejdpjl?hl=en) - -### CSS vs XPath - -There are similarities and differences between CSS and XPath. Both are powerful and complex in ways that are outside of the scope of this document. -In general: - -* CSS is more stable, easier to read, and easier to maintain (typically). -* XPath provides several powerful tools and it has been around the longest so it is well documented. -* XPath can be less stable and potentially unsupported by certain actions in Selenium. - -### Priority - -The best and most simple selector will always be to use an element ID: `#some-id-here`. If only we were so lucky to have this every time. - -When writing selectors, you should prioritize finding in this order: - -1. ID, name, class, or anything else that is unique to the element -2. Complex CSS selectors -3. XPath selectors -4. If none of the above work for you, then the last resort is to ask a developer to add a unique ID or class to the element you are trying to select. - -We suggest the use of CSS selectors above XPath selectors when possible. - -### Writing proper selectors - -There are correct ways of writing selectors and incorrect ways. These suggestions will help you write better selectors. - -#### Incorrect - copy selector/xpath - -DO NOT right click on an element in your browser developer tools and select "Copy selector" or "Copy XPath" and simply use that as your selector. These auto-generated selectors are prime examples of what not to do. - -These are bad: - -```css -#html-body > section > div > div > div > div -``` - -```xpath -//*[@id='html-body']/section/div/div/div/div -``` - -Both include unnecessary hierarchical details. As written, we are looking for a `div` inside of a `div` inside of a `div` inside of... you get the picture. If an application developer adds another `div` parent tomorrow, for whatever reason, this selector will break. Furthermore, when reading it, it is not clear what the intended target is. It may also grab other elements that were not intended. - -#### Do not be too general - -DO NOT make your selectors too generic. If a selector is too generic, there is a high probability that it will match multiple elements on the page. Maybe not today, but perhaps tomorrow when the application being tested changes. - -These are bad: - -```html -input[name*='firstname'] -``` - -The `*=` means `contains`. The selector is saying 'find an input whose name contains the string "firstname"'. But if a future change adds a new element to the page whose name also contains "firstname", then this selector will match two elements and that is bad. - -```css -.add -``` - -Similarly here, this will match all elements which contains the class `.add`. This is brittle and susceptible to breaking when new elements/styles are added to the page. - -#### Avoid being too specific - -DO NOT make your selectors too specific either. If a selector is too specific, there is a high probability that it will break due to even minor changes to the application being tested. - -These are bad: - -```css -#container .dashboard-advanced-reports .dashboard-advanced-reports-description .dashboard-advanced-reports-title -``` - -This selector is too brittle. It would break very easily if an application developer does something as simple as adding a parent container for style reasons. - -```xpath -//*[@id='container']/*[@class='dashboard-advanced-reports']/*[@class='dashboard-advanced-reports-description']/*[@class='dashboard-advanced-reports-title'] -``` - -This is the same selector as above, but represented in XPath instead of CSS. It is brittle for the same reasons. - -#### XPath selectors should not use @attribute="foo" - -This XPath is fragile. It would fail if the attribute was `attribute="foo bar"`. Instead you should use `contains(@attribute, "foo")` where @attribute is any valid attribute such as @text or @class. - -#### CSS and XPath selectors should avoid making use of hardcoded indices - -Hardcoded values are by definition not flexible. A hardcoded index may change if new code is introduced. Instead, parameterize the selector. - -GOOD: .foo:nth-of-type({{index}}) - -BAD: .foo:nth-of-type(1) - -GOOD: button[contains(@id, "foo")][{{index}}] - -BAD: button[contains(@id, "foo")][1] - -GOOD: #actions__{{index}}__aggregator - -BAD: #actions__1__aggregator - -#### CSS and XPath selectors MUST NOT reference the @data-bind attribute - -The @data-bind attribute is used by KnockoutJS, a framework Magento uses to create dynamic Javascript pages. Since this @data-bind attribute is tied to a specific framework, it should not be used for selectors. If Magento decides to use a different framework then these @data-bind selectors would break. - -#### Use isolation - -You should think in terms of "isolation" when writing new selectors. - -For example, say you have a login form that contains a username field, a password field, and a 'Sign In' button. First isolate the parent element. Perhaps it's `#login-form`. Then target the child element under that parent element: `.sign-in-button` The result is `#login-form .sign-in-button`. - -Using isolation techniques reduces the amount of DOM that needs to be processed. This makes the selector both accurate and efficient. - -#### Use advanced notation - -If you need to interact with the parent element but it is too generic, and the internal contents are unique then you need to: - -1. Target the unique internal contents first. -1. Then jump to the parent element using `::parent`. - -Imagine you want to find a table row that contains the string "Jerry Seinfeld". You can use the following XPath selector: - -```xpath -//div[contains(text(), 'Jerry Seinfeld')]/parent::td/parent::tr -``` - -Note in this instance that CSS does not have an equivalent to `::parent`, so XPath is a better choice. - -### CSS Examples - -Examples of common HTML elements and the corresponding selector to find that element in the DOM: - -Type|HTML|Selector ----|---|--- -IDs|`<div id="idname"/>`|`#idname` -Classes|`<div class="classname"/>`|`.classname` -HTML Tags|`<div/>`|`div` -HTML Tag & ID|`<div id="idname"/>`|`div#idname` -HTML Tag & Class|`<div class="classname"/>`|`div.classname` -ID & Class|`<div id="idname" class="classname"/>`|`#idname.classname` -HTML Tag & ID & Class|`<div id="idname" class="classname"/>`|`div#idname.classname` - -Examples of common CSS selector operators and their purpose: - -Symbol|Name|Purpose|Selector ----|---|---|--- -`*`|Universal Selector|Allows you to select ALL ELEMENTS on the Page. Wild Card.|`*` -Whitespace|Descendant Combinator|Allows you to combine 2 or more selectors.|`#idname .classname` -`>`|Child Combinator|Allows you to select the top-level elements THAT FOLLOWS another specified element.|`#idname > .classname` -`+`|Adjacent Sibling Combinator|Allows you to select an element THAT FOLLOWS DIRECTLY AFTER another specified element.|`#idname + .classname` -`~`|General Sibling Combinator|Allows you to select an element THAT FOLLOWS (directly or indirectly) another specified element.|`#idname ~ .classname` - -Examples of CSS attribute operators and their purpose: - -Symbol|Purpose|Example ----|---|--- -`=`|Returns all elements that CONTAIN the EXACT string in the value.|`[attribute='value']` -`*=`|Returns all elements that CONTAINS the substring in the value.|`[attribute*='value']` -`~=`|Returns all elements that CONTAINS the given words delimited by spaces in the value.|`[attribute~='value']` -`$=`|Returns all elements that ENDS WITH the substring in the value.|`[attribute$='value']` -`^=`|Returns all elements that BEGIN EXACTLY WITH the substring in the value.|`[attribute^='value']` -`!=`|Returns all elements that either DOES NOT HAVE the given attribute or the value of the attribute is NOT EQUAL to the value.|`[attribute!='value']` - -### XPath Examples - -#### `/` vs `//` - -The absolute XPath selector is a single forward slash `/`. It is used to provide a direct path to the element from the root element. - -WARNING: The `/` selector is brittle and should be used sparingly. - -Here is an example of what NOT to do, but this demonstrates how the selector works: - -```xpath -/html/body/div[2]/div/div[2]/div[1]/div[2]/form/div/input -``` - -In the BAD example above, we are specifying a very precise path to an input element in the DOM, starting from the very top of the document. - -Similarly, the relative XPath selector is a double forward slash `//`. It is used to start searching for an element anywhere in the DOM starting from the specified element. If no element is defined, the entire DOM is searched. - -Example: - -```xpath -//div[@class=’form-group’]//input[@id='user-message'] -``` - -In the `GOOD` example above, all `<div class='form-group'/>` elements in the DOM are matched first. Then all `<input id='user-message'/>` with `<div class='form-group'/>` as one of its parents are matched. The parent does not have to immediately precede it since it uses another double forward slash `//`. - -#### Parent Selectors - -The parent selector (`..`) allows you to jump to the parent element. - -Example #1: - -Given this HTML: - -```html -<tr> - <td> - <div>Unique Value</div> - </td> -</tr> -``` - -We can locate the `<tr>` element with this selector: - -```xpath -//*[text()='Unique Value']/../.. -``` - -Example #2: - -Given this HTML: - -```html -<tr> - <td> - <a href=“#”>Edit</a> - </td> - <td> - <div>Unique Value</div> - </td> -</tr> -``` - -We can locate the `<a>` element with this selector: - -```xpath -//div[text()='Unique Value']/../..//a -``` - -#### Attribute Selectors - -Attribute selectors allow you to select elements that match a specific attribute value. - -Examples: - -Attribute|HTML|Selector ----|---|--- -id|`<div id='idname'/>`|`//*[@id='idname']` -class|`<div class='classname'/>`|`//*[@class='classname']` -type|`<button type='submit'/>`|`//*[@type='submit']` -value|`<input value='value'/>`|`//*[@value='value']` -href|`<a href='https://google.com'/>`|`//*[@href='https://google.com']` -src|`<img src='/img.png'/>`|`//*[@src='/img.png']` - -#### `contains()` Selector - -The `contains()` selector allows you to select an element that contains an attribute value string. - -Examples: - -Attribute|HTML|Selector ----|---|--- -`text()`|`<p>Hello World!</p>`|`[contains(text(), 'Hello')]` -`@id`|`<div id='idname1234abcd'/>`|`[contains(@id, 'idname')]` -`@class`|`<div class='classname1 classname2'/>`|`[contains(@class, 'classname1')]` -`@name`|`<input name='inputname'/>`|`[contains(@name, 'name')]` -`@value`|`<input value='value'/>`|`[contains(@value, 'value')]` -`@href`|`<a href='https://google.com'/>`|`[contains(@href, 'google.com')]` - -#### `text()` Selector - -The `text()` selector allows you to select an element that contains a specific string. - -Examples: - -Type|HTML|Selector ----|---|--- -Exact Match|`<p>Hello World!!</p>`|`//p[text()='Hello World!!']` -Substring Match|`<p>Hello World!!</p>`|`//p[contains(text(), 'Hello')]` - -#### `starts-with()` Selector - -The `starts-with()` selector allows you to select an element whose attribute or text starts with a search string. - -Examples: - -Attribute|HTML|Selector ----|---|--- -`@id`|`<div id='unique_id_abcd1234'/>`|`//*[starts-with(@id, 'unique_id')]` -`@class`|`<div class='unique_class_abcd1234'/>`|`//*[starts-with(@class, 'unique_class')]` -`@href`|`<a href='https://www.google.com/'/>`|`//a[starts-with(@href, 'https://')]` -`text()`|`<p>Hello World!</p>`|`//p[starts-with(text(), 'Hello ')]` - -#### `ends-with()` Selector - -The `ends-with()` selector allows you to select an element whose attribute or text ends with a search string. - -Examples: - -Attribute|HTML|Selector ----|---|--- -`@id`|`<div id='abcd1234_unique_id'/>`|`//*[ends-with(@id, 'unique_id')]` -`@class`|`<div class='abcd1234_unique_class'/>`|`//*[ends-with(@class, 'unique_class')]` -`@href`|`<a href='https://www.google.com'/>`|`//a[ends-with(@href, 'google.com')]` -`text()`|`<p>Hello World!</p>`|`//p[ends-with(text(), 'World!')]` - -### Translating Between CSS and XPath - -Most of the time it is possible to translate from CSS to XPath and vice versa. Here are some examples: - -Type|CSS|XPath ----|---|--- -IDs|`#idname`|`//*[@id='idname']` -Classes|`.classname`|`//*[@class='classname']` -HTML Tags|`div`|`//div` -HTML Tag & ID|`div#idname`|`//div[@id='idname']` -HTML Tag & Class|`div.classname`|`//div[@class='classname']` -Universal|`*`|`//*` -Descendant|`#idname .classname`|`//*[@id='idname']//*[@class='classname']` -Child|`#idname > .classname`|`//*[@id='idname']/*[@class='classname']` -Adjacent Sibling|`#idname + .classname`|`//*[@id='idname']/following-sibling::*[@class='classname'][1]` -General Sibling|`#idname ~ .classname`|`//*[@id='idname']/following-sibling::*[@class='classname']` diff --git a/docs/guides/test-isolation.md b/docs/guides/test-isolation.md deleted file mode 100644 index 474867ff2..000000000 --- a/docs/guides/test-isolation.md +++ /dev/null @@ -1,119 +0,0 @@ -# Test Isolation - -Because MFTF is a framework for testing a highly customizable and ever changing application, MFTF tests need to be properly isolated. - -## What is test isolation? - -Test isolation refers to a test that does not leave behind any data or configuration changes in the Magento instance. - -An MFTF test is considered fully isolated if: - -1. It does not leave data behind. -1. It does not leave Magento configured in a different state than when the test started. -1. It does not affect a following test's outcome. -1. It does not rely on an irregular configuration to start its preconditions. - -### Deleting versus restoring - -In the above list, points 1 and 2 refer to leaving things behind during test execution. This means you are either deleting or restoring entities in Magento after your test's execution. - -Some examples of entities to be deleted include: - -1. Products -2. Categories -3. Rules (Price, Related Products) - -The list of entities to restore is much simpler: - -1. Application Configuration - -The distinction above is because MFTF tests expect the environment to be in a completely clean state, outside of a test or suite's preconditions. Data must be cleaned up and any application configuration must go back to the default. - -## Why is isolation important? - -As mentioned above, isolation is important because poor isolation can lead to other test failures. For a test to be useful, you must have high confidence in the test's outcome, and by introducing test isolation issues it can invalidate a test's result. - -## How can I achieve test isolation? - -This is difficult to do given how large the Magento application is, but a systematic approach can ensure a high level of confidence in you test's isolation. - -### Cleaning up data - -If your test creates any data via `<createData>` then a subsequent `<deleteData>` action *must* exist in the test's `<after>` block. - -This includes both `<createData>` actions in the test's `<before>` as well as in the test body. - -```xml -<test name="SampleTest"> - <before> - <createData entity="SimpleSubCategory" stepKey="category"/> - </before> - <after> - <deleteData createDataKey="category" stepKey="deleteCategory"/> - <deleteData createDataKey="entityCreatedDuringWorkflow" stepKey="deleteCategory"/> - </after> - ... - <createData entity="SimpleSubCategory" stepKey="entityCreatedDuringWorkflow"/> - ... -</test> -``` - -Other test data can be more difficult to detect, and requires an understanding of what the test does in its workflow. - -```xml -<test name="AdminAddImageForCategoryTest"> - <before> - <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin"/> - </before> - <after> - <actionGroup ref="DeleteCategory" stepKey="DeleteCategory"> - <argument name="categoryEntity" value="SimpleSubCategory"/> - </actionGroup> - <actionGroup ref="logout" stepKey="logout"/> - </after> - <!-- Go to create a new category with image --> - <actionGroup ref="goToCreateCategoryPage" stepKey="goToCreateCategoryPage"/> - ... -</test> -``` - -Note that the test contains a context setting comment describing the workflow; this is very helpful in determining that a new category will be created, which will need to be cleaned up in the test `<after>` block. - -### Cleaning up configuration - -Similarly, configuration changes can be easily identified by `<magentoCLI>` actions. - -```xml -<test name="AddOutOfStockProductToCompareListTest"> - <before> - <magentoCLI command="config:set cataloginventory/options/show_out_of_stock 0" stepKey="displayOutOfStockNo"/> - ... - </before> - <after> - <magentoCLI command="config:set cataloginventory/options/show_out_of_stock 1" stepKey="displayOutOfStockNo"/> - ... - </after> - ... -</test> -``` - -Configuration changes can also be done via `<createData>` actions, but that is not recommended as it is much easier to identify `<magentoCLI>` commands. - -A test's workflow can also alter the application's configuration, and much like data cleanup, this can only be identified by understanding a test's workflow: - -```xml -<test name="AdminMoveProductBetweenCategoriesTest"> - ... - <!-- Enable `Use Categories Path for Product URLs` on Stores -> Configuration -> Catalog -> Catalog -> Search Engine Optimization --> - <amOnPage url="{{AdminCatalogSearchConfigurationPage.url}}" stepKey="onConfigPage"/> - <waitForPageLoad stepKey="waitForLoading"/> - <conditionalClick selector="{{AdminCatalogSearchEngineConfigurationSection.searchEngineOptimization}}" dependentSelector="{{AdminCatalogSearchEngineConfigurationSection.openedEngineOptimization}}" visible="false" stepKey="clickEngineOptimization"/> - <uncheckOption selector="{{AdminCatalogSearchEngineConfigurationSection.systemValueUseCategoriesPath}}" stepKey="uncheckDefault"/> - <selectOption userInput="Yes" selector="{{AdminCatalogSearchEngineConfigurationSection.selectUseCategoriesPatForProductUrls}}" stepKey="selectYes"/> - <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> - <waitForPageLoad stepKey="waitForSaving"/> - ... -</test> -``` - -One thing to note, unless a test is specifically testing the configuration page's frontend capabilities, configuring the application should always be done with a `<magentoCLI>` action. diff --git a/docs/guides/test-modularity.md b/docs/guides/test-modularity.md deleted file mode 100644 index ed182313e..000000000 --- a/docs/guides/test-modularity.md +++ /dev/null @@ -1,88 +0,0 @@ -# Test Modularity - -One of MFTF's most distinguishing functionalities is the framework's modularity. - -## What is test modularity - -Within MFTF, test modularity can refer to two different concepts: - -### Test material merging - -Test material merging is covered extensively in the [merging] topic, so it will not be our focus in this guide. - -### Modular test materials - -This refers to test materials being correctly owned by the right Magento module, and for tests to have references to only what their parent Magento module has a dependency on. - -Since MFTF queries the Magento instance for enabled modules, MFTF test materials are included or excluded from the merging process dynamically, making proper ownership and dependencies a must. - -Consider the following scenario: - -* TestA in ModuleA is using materials form ModuleB -* In Magento, I now disable ModuleB -* TestA will try to use ModuleB materials, which are no longer being read by MFTF since the Magento instance has it disable - -Since TestA's dependencies are out of sync with ModuleA, the tests are no longer properly modular. - -## Why is test modularity important? - -This concept is important simply because without proper modularity, tests or test materials may be incorrectly merged in (or left out), leading to the the test itself being out of sync with the Magento instance. - -For example, in a situation where an extension drastically alters the login process (for instance: two factor authentication), the only way the tests will be able to pass is if the test materials are correctly nested in the extension. - -## How can I achieve test modularity? - -Test modularity can be challenging, depending on the breadth of the changes being introduced in a module. - -### Determine test material ownership - -This is should be the first step when creating new test materials. We will use the `New Product` page as an example. - -#### Intuitive reasoning - -The easiest way to do this has limited application, but some times it is fairly obvious where test material comes from due to nomenclature or functionality. - -The following `<select>` for `Tax Class` clearly belongs to the `Tax` module: - -```xml -<select class="admin__control-select" name="product[tax_class_id]"/> -``` - -This approach will work on getting the quickest ownership, but it is fairly obvious that it may be necessary to double check. - -#### Deduction - -This is the next step up in difficulty from the above method, as it involves searching through the Magento codebase. - -Take the `Add Attribute` button for example. The button has an `id="addAttribute"` and since we know Magento uses XML to declare much of its layout/CSS properties we can start by searching only `*.xml` files. - -Searching through the codebase for `"addAttribute"` in `xml` files leads to four different files: - -```terminal -app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerActivitiesConfigureSection.xml -app/code/Magento/GiftRegistry/Test/Mftf/Section/AdminGiftRegistrySection.xml -app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeActionGroup.xml -app/code/Magento/Catalog/view/adminhtml/ui_component/product_form.xml -``` - -The first three are clearly MFTF test materials, which leaves us with the final file, and the line below - -```xml -<button name="addAttribute" class="Magento\Catalog\Block\Adminhtml\Product\Edit\Button\AddAttribute"/> -``` - -This means we can safely assume `Add Attribute` button belongs to `Catalog` based on the above class namespace and filepath. - -This kind of deduction is more involved, but it much more likely to give you the true source of the element. - -### Use bin/mftf static-checks - -For tests to be fully modular, an MFTF test must have the same dependencies as its parent module. This is quite difficult to do by hand, and requires checking of every `{{test.material}}` call and any other references to MFTF test materials in a test. - -The `static-checks` command includes a test material ownership check that should help suss out these kind of dependency issues. - -See [mftf commands] for more information. - -<!-- Link definitions --> -[merging]: ../merging.md -[mftf commands]: ../commands/mftf.md diff --git a/docs/guides/using-suites.md b/docs/guides/using-suites.md deleted file mode 100644 index 99413cb78..000000000 --- a/docs/guides/using-suites.md +++ /dev/null @@ -1,101 +0,0 @@ -# Using suites - -With an increasing number of MFTF tests, it is important to have a mechanism to organize and consolidate them for ease-of-use. - -### What is a suite? - -A suite is a collection of MFTF tests that are intended to test specific behaviors of Magento. It may contain initialization and clean up steps common to the included test cases. It allows you to include, exclude and/or group tests with preconditions and post conditions. -You can create a suite referencing tests, test groups and modules. - -### How is a suite defined? - -A suite should be created under `<magento2 root>/dev/tests/acceptance/tests/_suite` if it has cross-module references. If a suite references only a single module, it should be created under `<module>/Test/Mftf/Suite`. The generated tests for each suite are grouped into their own directory under `<magento2 root>/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/_generated/`. - -### What is the format of a suite? - -A suite is comprised of blocks: - -* `<before>` : executes precondition once per suite run. -* `<after>` : executes postcondition once per suite run. -* `<include>`: includes specific tests/groups/modules in the suite. -* `<exclude>`: excludes specific tests/groups/modules from the suite. - -```xml -<?xml version="1.0" encoding="UTF-8"?> - -<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Suite/etc/suiteSchema.xsd"> - <suite name=""> - <before> - </before> - <after> - </after> - <include> - <test name=""/> - <group name=""/> - <module name="" file=""/> - </include> - <exclude> - <test name=""/> - <group name=""/> - <module name="" file=""/> - </exclude> - </suite> -</suites> -``` - -### Example - -```xml -<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Suite/etc/suiteSchema.xsd"> - <suite name="WYSIWYGDisabledSuite"> - <before> - <magentoCLI stepKey="disableWYSIWYG" command="config:set cms/wysiwyg/enabled disabled" /> - </before> - <after> - <magentoCLI stepKey="enableWYSIWYG" command="config:set cms/wysiwyg/enabled enabled" /> - </after> - <include> - <module name="Catalog"/> - </include> - <exclude> - <test name="WYSIWYGIncompatibleTest"/> - </exclude> - </suite> -</suites> -``` - -This example declares a suite with name `WYSIWYGDisabledSuite`: - -* Disables WYSIWYG of the Magento instance before running the tests. -* Runs all tests from the `Catalog` module, except `WYSIWYGIncompatibleTest` -* Returns the Magento instance back to its original state, by enabling WYSIWYG at the end of testing. - -### Using MFTF suite commands - -* Generate all tests within a suite. - - ```bash - vendor/bin/mftf generate:suite <suiteName> [<suiteName>] - ``` -* Run all tests within suite. - - ```bash - vendor/bin/mftf run:group <suiteName> [<suiteName>] - ``` -* Generates any combination of suites and tests. - - ```bash - vendor/bin/mftf generate:tests --tests '{"tests":["testName1","testName2"],"suites":{"suite1":["suite_test1"],"suite2":null}}' - ``` - -### Run specific tests within a suite - -If a test is referenced in a suite, it can be run in the suite's context with MFTF `run` command. If a test is referenced in multiple suites, the `run` command will run the test multiple times in all contexts. - -```bash -vendor/bin/mftf run:test <testName> [<testName>] -``` - -### When to use suites? - -Suites are a great way to organize tests which need the Magento environment to be configured in a specific way as a pre-requisite. The conditions are executed once per suite which optimizes test execution time. If you wish to categorize tests solely based on functionality, use group tags instead. diff --git a/docs/img/action-groups-dia.svg b/docs/img/action-groups-dia.svg deleted file mode 100644 index 853420d54..000000000 --- a/docs/img/action-groups-dia.svg +++ /dev/null @@ -1,516 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:xlink="http://www.w3.org/1999/xlink" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="643" - height="114" - version="1.1" - id="svg152" - sodipodi:docname="action-groups-dia.svg" - inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"> - <metadata - id="metadata158"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - </cc:Work> - </rdf:RDF> - </metadata> - <defs - id="defs156" /> - <sodipodi:namedview - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1" - objecttolerance="10" - gridtolerance="10" - guidetolerance="10" - inkscape:pageopacity="0" - inkscape:pageshadow="2" - inkscape:window-width="1920" - inkscape:window-height="1017" - id="namedview154" - showgrid="false" - inkscape:zoom="1.2492586" - inkscape:cx="149.25073" - inkscape:cy="58.965954" - inkscape:window-x="3278" - inkscape:window-y="-8" - inkscape:window-maximized="1" - inkscape:current-layer="svg152" /> - <a - href="actions.html" - title="any actions" - id="a3851"> - <g - id="g242"> - <path - d="M404 25 L498 25 L504 31 L504 41 L498 47 L404 47 L398 41 L398 31 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="path2" /> - <text - x="451" - y="36" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text4">actionTypeTags</text> - <line - x1="401" - y1="44" - x2="404" - y2="41" - style="stroke:rgb(0,0,0);stroke-width:2" - id="line6" /> - <path - d="M404 41 L406 43 L407 38 L402 39 L404 41 Z" - style="fill:rgb(0,0,0)" - id="path8" /> - <rect - x="499" - y="31" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect10" /> - <line - x1="501" - y1="36" - x2="507" - y2="36" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line12" /> - <line - x1="504" - y1="33" - x2="504" - y2="39" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line14" /> - </g> - </a> - <a - id="a3924" - href="#argument-tag" - title="zero or more <argument> elements"> - <g - id="g233"> - <rect - x="566" - y="80" - width="70" - height="22" - style="fill:#ffffff;stroke:#000000;stroke-width:1;stroke-dasharray:4, 1" - id="rect16" /> - <rect - x="563" - y="77" - width="70" - height="22" - style="fill:#ffffff;stroke:#000000;stroke-width:1;stroke-dasharray:4, 1" - id="rect18" /> - <text - x="598" - y="88" - style="font-weight:bold;font-size:10.66666698px;font-family:Arial;dominant-baseline:central;text-anchor:middle;fill:#000000" - id="text20">argument</text> - <text - x="623" - y="109" - style="font-size:9.59999943px;font-family:Arial;dominant-baseline:central;text-anchor:end;fill:#000000" - id="text22">0..∞</text> - </g> - </a> - <line - x1="543" - y1="88" - x2="563" - y2="88" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line24" /> - <a - id="a3914" - title="contains a sequence of"> - <g - id="g227"> - <path - d="M509 78 L532 78 L538 84 L538 92 L532 98 L509 98 L503 92 L503 84 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="path26" /> - <line - x1="506" - y1="88" - x2="535" - y2="88" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line28" /> - <ellipse - cx="515" - cy="88" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse30" /> - <ellipse - cx="520" - cy="88" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse32" /> - <ellipse - cx="525" - cy="88" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse34" /> - <rect - x="533" - y="83" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect36" /> - <line - x1="535" - y1="88" - x2="541" - y2="88" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line38" /> - </g> - </a> - <line - x1="483" - y1="88" - x2="503" - y2="88" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line40" /> - <a - id="a3907" - href="#arguments-tag" - title="wrapping element <arguments>"> - <g - id="g218"> - <rect - x="398" - y="77" - width="80" - height="22" - style="fill:#ffffff;stroke:#000000;stroke-width:1" - id="rect42" /> - <text - x="438" - y="88" - style="font-weight:bold;font-size:10.66666698px;font-family:Arial;dominant-baseline:central;text-anchor:middle;fill:#000000" - id="text44">arguments</text> - <rect - x="473" - y="83" - width="10" - height="10" - style="fill:#ffffff;stroke:#000000;stroke-width:1" - id="rect46" /> - <line - x1="475" - y1="88" - x2="481" - y2="88" - style="stroke:#000000;stroke-width:1" - id="line48" /> - </g> - </a> - <line - x1="388" - y1="36" - x2="398" - y2="36" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line50" /> - <line - x1="388" - y1="88" - x2="398" - y2="88" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line52" /> - <line - x1="388" - y1="36" - x2="388" - y2="88" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line54" /> - <a - id="a3833" - title="optionally contains muiliple times one of the following nodes"> - <g - id="g212"> - <line - x1="378" - y1="62" - x2="388" - y2="62" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line56" /> - <path - d="M347 55 L370 55 L376 61 L376 69 L370 75 L347 75 L341 69 L341 61 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="path58" /> - <path - d="M344 52 L367 52 L373 58 L373 66 L367 72 L344 72 L338 66 L338 58 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="path60" /> - <line - x1="343" - y1="62" - x2="347" - y2="62" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line62" /> - <line - x1="347" - y1="62" - x2="351" - y2="58" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line64" /> - <line - x1="359" - y1="58" - x2="363" - y2="58" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line66" /> - <line - x1="359" - y1="62" - x2="367" - y2="62" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line68" /> - <line - x1="359" - y1="66" - x2="363" - y2="66" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line70" /> - <line - x1="363" - y1="58" - x2="363" - y2="66" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line72" /> - <ellipse - cx="355" - cy="58" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse74" /> - <ellipse - cx="355" - cy="62" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse76" /> - <ellipse - cx="355" - cy="66" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse78" /> - <text - x="368" - y="82" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text80">0..∞</text> - <rect - x="368" - y="57" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect82" /> - <line - x1="370" - y1="62" - x2="376" - y2="62" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line84" /> - </g> - </a> - <line - x1="318" - y1="62" - x2="338" - y2="62" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line86" /> - <a - id="a82" - href="#actiongroup-tag" - title="one or more <actionGroup> elements"> - <g - id="g195"> - <rect - x="227" - y="54" - width="89" - height="22" - style="fill:#ffffff;stroke:#000000;stroke-width:1" - id="rect88" /> - <rect - x="224" - y="51" - width="89" - height="22" - style="fill:#ffffff;stroke:#000000;stroke-width:1" - id="rect90" /> - <text - x="268.5" - y="62" - style="font-weight:bold;font-size:10.66666698px;font-family:Arial;dominant-baseline:central;text-anchor:middle;fill:#000000" - id="text92">actionGroup</text> - <text - x="308" - y="83" - style="font-size:9.59999943px;font-family:Arial;dominant-baseline:central;text-anchor:end;fill:#000000" - id="text94">1..∞</text> - <rect - x="308" - y="57" - width="10" - height="10" - style="fill:#ffffff;stroke:#000000;stroke-width:1" - id="rect96" /> - <line - x1="310" - y1="62" - x2="316" - y2="62" - style="stroke:#000000;stroke-width:1" - id="line98" /> - </g> - </a> - <line - x1="204" - y1="62" - x2="224" - y2="62" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line100" /> - <line - x1="146.40559" - y1="62" - x2="166.40559" - y2="62" - style="stroke:#000000;stroke-width:1;stroke-linecap:round" - id="line126" /> - <a - id="a106" - title="Root element <actionGroups>" - href="#actiongroups-tag"> - <g - transform="translate(-10)" - id="g104"> - <line - x1="124.97701" - y1="62" - x2="146.40559" - y2="62" - style="stroke:#000000;stroke-width:1.03509831;stroke-linecap:round" - id="line142" /> - <rect - x="59.977016" - y="51" - width="95.357147" - height="22" - style="fill:#ffffff;stroke:#000000;stroke-width:1.03509831" - id="rect144" /> - <text - x="102.44111" - y="63.59021" - style="font-weight:bold;font-size:11.041049px;font-family:Arial;dominant-baseline:central;text-anchor:middle;fill:#000000;stroke-width:1.03509831" - id="text146" - transform="scale(1.0350984,0.96609173)">actionGroups</text> - <rect - x="147.83417" - y="57" - width="10.714286" - height="10" - style="fill:#ffffff;stroke:#000000;stroke-width:1.03509831" - id="rect148" /> - <line - x1="149.97702" - y1="62" - x2="156.40558" - y2="62" - style="stroke:#000000;stroke-width:1.03509831" - id="line150" /> - </g> - </a> - <a - id="a3823" - title="contains a sequence of"> - <g - id="g227-6" - transform="translate(-339.59441,-26)"> - <path - d="m 509,78 h 23 l 6,6 v 8 l -6,6 h -23 l -6,-6 v -8 z" - style="fill:#ffffff;stroke:#000000;stroke-width:1" - id="path26-0" - inkscape:connector-curvature="0" /> - <line - x1="506" - y1="88" - x2="535" - y2="88" - style="stroke:#000000;stroke-width:1" - id="line28-3" /> - <circle - cx="515" - cy="88" - id="ellipse30-8" - r="2" /> - <circle - cx="520" - cy="88" - id="ellipse32-3" - r="2" /> - <circle - cx="525" - cy="88" - id="ellipse34-4" - r="2" /> - <rect - x="533" - y="83" - width="10" - height="10" - style="fill:#ffffff;stroke:#000000;stroke-width:1" - id="rect36-0" /> - <line - x1="535" - y1="88" - x2="541" - y2="88" - style="stroke:#000000;stroke-width:1" - id="line38-2" /> - </g> - </a> -</svg> diff --git a/docs/img/catalogCategoryRepository-operations.png b/docs/img/catalogCategoryRepository-operations.png deleted file mode 100644 index e6fa806bb..000000000 Binary files a/docs/img/catalogCategoryRepository-operations.png and /dev/null differ diff --git a/docs/img/data-dia.svg b/docs/img/data-dia.svg deleted file mode 100644 index fbaf7d8ec..000000000 --- a/docs/img/data-dia.svg +++ /dev/null @@ -1,489 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:xlink="http://www.w3.org/1999/xlink" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="496" - height="218" - version="1.1" - id="svg176" - sodipodi:docname="data-dia.svg" - inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"> - <metadata - id="metadata182"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - </cc:Work> - </rdf:RDF> - </metadata> - <defs - id="defs180" /> - <sodipodi:namedview - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1" - objecttolerance="10" - gridtolerance="10" - guidetolerance="10" - inkscape:pageopacity="0" - inkscape:pageshadow="2" - inkscape:window-width="1920" - inkscape:window-height="1017" - id="namedview178" - showgrid="false" - inkscape:zoom="1.6195026" - inkscape:cx="156.50572" - inkscape:cy="97.15766" - inkscape:window-x="3278" - inkscape:window-y="-8" - inkscape:window-maximized="1" - inkscape:current-layer="svg176" /> - <text - x="338" - y="57" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text8">0..∞</text> - <a - id="a3925" - href="#data-tag" - title="zero or more <data> elements"> - <g - id="g195"> - <rect - x="307" - y="28" - width="44" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect2" /> - <rect - x="304" - y="25" - width="44" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect4" /> - <text - x="326" - y="36" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text6">data</text> - </g> - </a> - <text - x="332" - y="109" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text24">0..∞</text> - <a - id="a3935" - href="#var-tag" - title="zero or more <var> elements"> - <g - id="g204"> - <rect - x="307" - y="80" - width="38" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect18" /> - <rect - x="304" - y="77" - width="38" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect20" /> - <text - x="323" - y="88" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text22">var</text> - </g> - </a> - <text - x="389" - y="161" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text40">0..∞</text> - <a - id="a3945" - href="#requiredentity-tag" - title="zero or more <requiredEntity> elements"> - <g - id="g213"> - <rect - x="307" - y="132" - width="95" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect34" /> - <rect - x="304" - y="129" - width="95" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect36" /> - <text - x="351.5" - y="140" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text38">requiredEntity</text> - </g> - </a> - <text - x="476" - y="213" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text56">0..∞</text> - <a - id="a3963" - href="#item-tag" - title="zero or more <item> elements"> - <g - id="g229"> - <rect - x="445" - y="184" - width="44" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect50" /> - <rect - x="442" - y="181" - width="44" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect52" /> - <text - x="464" - y="192" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text54">item</text> - </g> - </a> - <line - x1="417.84494" - y1="192" - x2="442" - y2="192" - style="stroke:#000000;stroke-width:1.09897804;stroke-linecap:round" - id="line66" /> - <a - id="a3968" - title="a sequence of the following elements"> - <g - id="g3966"> - <path - inkscape:connector-curvature="0" - d="m 388,182 h 23 l 6,6 v 8 l -6,6 h -23 l -6,-6 v -8 z" - style="fill:#ffffff;stroke:#000000;stroke-width:1" - id="path68" /> - <line - x1="385" - y1="192" - x2="414" - y2="192" - style="stroke:#000000;stroke-width:1" - id="line70" /> - <circle - r="2" - cx="394" - cy="192" - id="ellipse72" /> - <circle - r="2" - cx="399" - cy="192" - id="ellipse74" /> - <circle - r="2" - cx="404" - cy="192" - id="ellipse76" /> - </g> - </a> - <line - x1="360.46375" - y1="192" - x2="382" - y2="192" - style="stroke:#000000;stroke-width:1.03769612;stroke-linecap:round" - id="line82" /> - <text - x="352" - y="213" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text90">0..∞</text> - <a - id="a99" - href="#array-tag" - title="zero or more <array> elements"> - <g - id="g3864"> - <rect - x="307" - y="184" - width="53" - height="22" - style="fill:#ffffff;stroke:#000000;stroke-width:1;stroke-dasharray:4, 1" - id="rect84" /> - <rect - x="304" - y="181" - width="53" - height="22" - style="fill:#ffffff;stroke:#000000;stroke-width:1;stroke-dasharray:4, 1" - id="rect86" /> - <text - x="330.5" - y="192" - style="font-weight:bold;font-size:10.66666698px;font-family:Arial;dominant-baseline:central;text-anchor:middle;fill:#000000" - id="text88">array</text> - </g> - </a> - <line - x1="294" - y1="36" - x2="304" - y2="36" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line96" /> - <line - x1="294" - y1="88" - x2="304" - y2="88" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line98" /> - <line - x1="294" - y1="140" - x2="304" - y2="140" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line100" /> - <line - x1="294" - y1="192" - x2="304" - y2="192" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line102" /> - <line - x1="294" - y1="36" - x2="294" - y2="192" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line104" /> - <line - x1="282.8219" - y1="114" - x2="294" - y2="114" - style="stroke:#000000;stroke-width:1.05726469;stroke-linecap:round" - id="line106" /> - <a - id="a3984" - title="that contains one of the following nodes one or times"> - <g - id="g3959"> - <g - id="g3922"> - <path - inkscape:connector-curvature="0" - id="path110" - style="fill:#ffffff;stroke:#000000;stroke-width:1" - d="m 250,104 h 23 l 6,6 v 8 l -6,6 h -23 l -6,-6 v -8 z" /> - <path - id="path108" - style="fill:#ffffff;stroke:#000000;stroke-width:1" - d="m 253,107 h 23 l 6,6 v 8 l -6,6 h -23 l -6,-6 v -8 z" - inkscape:connector-curvature="0" /> - <line - id="line112" - style="stroke:#000000;stroke-width:1" - y2="114" - x2="253" - y1="114" - x1="249" /> - <line - id="line114" - style="stroke:#000000;stroke-width:1" - y2="110" - x2="257" - y1="114" - x1="253" /> - <line - id="line116" - style="stroke:#000000;stroke-width:1" - y2="110" - x2="269" - y1="110" - x1="265" /> - <line - id="line118" - style="stroke:#000000;stroke-width:1" - y2="114" - x2="273" - y1="114" - x1="265" /> - <line - id="line120" - style="stroke:#000000;stroke-width:1" - y2="118" - x2="269" - y1="118" - x1="265" /> - <line - id="line122" - style="stroke:#000000;stroke-width:1" - y2="118" - x2="269" - y1="110" - x1="269" /> - <circle - id="ellipse124" - cy="110" - cx="261" - r="2" /> - <circle - id="ellipse126" - cy="114" - cx="261" - r="2" /> - <circle - id="ellipse128" - cy="118" - cx="261" - r="2" /> - <text - id="text130" - style="font-size:9.59999943px;font-family:Arial;dominant-baseline:central;text-anchor:end;fill:#000000" - y="134" - x="274">1..∞</text> - </g> - </g> - </a> - <line - x1="222.68196" - y1="114" - x2="244" - y2="114" - style="stroke:#000000;stroke-width:1.03242517;stroke-linecap:round" - id="line136" /> - <rect - x="167" - y="106" - width="55" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect138" /> - <a - id="a231" - href="#entity-tag" - title="zero or more <entity> elements"> - <g - id="g186"> - <rect - x="164" - y="103" - width="55" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect140" /> - <text - x="191.5" - y="114" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text142">entity</text> - </g> - </a> - <text - x="214" - y="135" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text144">0..∞</text> - <line - x1="136" - y1="114" - x2="164" - y2="114" - style="stroke:#000000;stroke-width:1.18321598;stroke-linecap:round" - id="line150" /> - <a - id="a3976" - title="that contains a sequence of"> - <g - id="g3944"> - <path - d="m 110,104 h 23 l 6,6 v 8 l -6,6 h -23 l -6,-6 v -8 z" - style="fill:#ffffff;stroke:#000000;stroke-width:1" - id="path152" - inkscape:connector-curvature="0" /> - <line - x1="107" - y1="114" - x2="136" - y2="114" - style="stroke:#000000;stroke-width:1" - id="line154" /> - <circle - cx="116" - cy="114" - id="ellipse156" - r="2" /> - <circle - cx="121" - cy="114" - id="ellipse158" - r="2" /> - <circle - cx="126" - cy="114" - id="ellipse160" - r="2" /> - </g> - </a> - <line - x1="79.899483" - y1="114" - x2="104" - y2="114" - style="stroke:#000000;stroke-width:1.09773672;stroke-linecap:round" - id="line166" /> - <a - id="a104" - title="Root element <entities>" - href="#entities-tag"> - <g - id="g102"> - <rect - x="20" - y="103" - width="59" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect168" /> - <text - x="49.5" - y="114" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text170">entities</text> - </g> - </a> -</svg> diff --git a/docs/img/issue.png b/docs/img/issue.png deleted file mode 100644 index 1dcf78ce2..000000000 Binary files a/docs/img/issue.png and /dev/null differ diff --git a/docs/img/metadata-dia.svg b/docs/img/metadata-dia.svg deleted file mode 100644 index 0ce0fc27a..000000000 --- a/docs/img/metadata-dia.svg +++ /dev/null @@ -1,1050 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:xlink="http://www.w3.org/1999/xlink" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="561" - height="478" - version="1.1" - id="svg318" - sodipodi:docname="metadata-dia.svg" - inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"> - <metadata - id="metadata324"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - </cc:Work> - </rdf:RDF> - </metadata> - <defs - id="defs322" /> - <sodipodi:namedview - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1" - objecttolerance="10" - gridtolerance="10" - guidetolerance="10" - inkscape:pageopacity="0" - inkscape:pageshadow="2" - inkscape:window-width="1920" - inkscape:window-height="1017" - id="namedview320" - showgrid="false" - inkscape:zoom="1.3964619" - inkscape:cx="235.1642" - inkscape:cy="256.75664" - inkscape:window-x="3278" - inkscape:window-y="-8" - inkscape:window-maximized="1" - inkscape:current-layer="svg318" /> - <a - id="a4301" - href="#field-tag" - title="zero or more <field> elements"> - <g - id="g417"> - <rect - x="491" - y="28" - width="44" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect2" /> - <rect - x="488" - y="25" - width="44" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect4" /> - <text - x="510" - y="36" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text6">field</text> - <text - x="522" - y="57" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text8">0..∞</text> - </g> - </a> - <a - id="a4308" - title="zero or more <array> elements" - href="#array-tag"> - <g - id="g442"> - <rect - x="491" - y="80" - width="53" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect18" /> - <rect - x="488" - y="77" - width="53" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect20" /> - <text - x="514.5" - y="88" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text22">array</text> - <text - x="536" - y="109" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text24">0..∞</text> - <rect - x="536" - y="83" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect26" /> - <line - x1="538" - y1="88" - x2="544" - y2="88" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line28" /> - <line - x1="541" - y1="85" - x2="541" - y2="91" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line30" /> - </g> - </a> - <line - x1="478" - y1="36" - x2="488" - y2="36" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line46" /> - <line - x1="478" - y1="88" - x2="488" - y2="88" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line48" /> - <a - id="a4318" - href="#object-tag" - title="zero or more <object> elements"> - <g - id="g452"> - <rect - x="491" - y="132" - width="58" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect32" /> - <rect - x="488" - y="129" - width="58" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect34" /> - <text - x="517" - y="140" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text36">object</text> - <text - x="541" - y="161" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text38">0..∞</text> - <rect - x="541" - y="135" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect40" /> - <line - x1="543" - y1="140" - x2="549" - y2="140" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line42" /> - <line - x1="546" - y1="137" - x2="546" - y2="143" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line44" /> - <line - x1="478" - y1="140" - x2="488" - y2="140" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line50" /> - </g> - </a> - <line - x1="478" - y1="36" - x2="478" - y2="140" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line52" /> - <line - x1="468" - y1="88" - x2="478" - y2="88" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line54" /> - <a - id="a4284" - title="that may contain zero or more times one of the following nodes"> - <g - id="g433"> - <path - d="M437 81 L460 81 L466 87 L466 95 L460 101 L437 101 L431 95 L431 87 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="path56" /> - <path - d="M434 78 L457 78 L463 84 L463 92 L457 98 L434 98 L428 92 L428 84 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="path58" /> - <line - x1="433" - y1="88" - x2="437" - y2="88" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line60" /> - <line - x1="437" - y1="88" - x2="441" - y2="84" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line62" /> - <line - x1="449" - y1="84" - x2="453" - y2="84" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line64" /> - <line - x1="449" - y1="88" - x2="457" - y2="88" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line66" /> - <line - x1="449" - y1="92" - x2="453" - y2="92" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line68" /> - <line - x1="453" - y1="84" - x2="453" - y2="92" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line70" /> - <ellipse - cx="445" - cy="84" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse72" /> - <ellipse - cx="445" - cy="88" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse74" /> - <ellipse - cx="445" - cy="92" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse76" /> - <text - x="458" - y="108" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text78">0..∞</text> - <rect - x="458" - y="83" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect80" /> - <line - x1="460" - y1="88" - x2="466" - y2="88" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line82" /> - </g> - </a> - <line - x1="408" - y1="88" - x2="428" - y2="88" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line84" /> - <a - id="a4227" - title="zero or more <field> elements" - href="#field-tag"> - <g - id="g387"> - <rect - x="348" - y="184" - width="44" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect98" /> - <rect - x="345" - y="181" - width="44" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect100" /> - <text - x="367" - y="192" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text102">field</text> - <text - x="379" - y="213" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text104">0..∞</text> - </g> - </a> - <a - id="a4262" - title="zero or one <value> element" - href="#value-tag"> - <g - id="g466"> - <rect - x="483" - y="285" - width="49" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect128" /> - <text - x="507.5" - y="296" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text130">value</text> - </g> - </a> - <a - id="a4329" - title="zero or more <object> elements" - href="#object-tag"> - <g - id="g462"> - <rect - x="486" - y="236" - width="58" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect114" /> - <rect - x="483" - y="233" - width="58" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect116" /> - <text - x="512" - y="244" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text118">object</text> - <text - x="536" - y="265" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text120">0..∞</text> - <rect - x="536" - y="239" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect122" /> - <line - x1="538" - y1="244" - x2="544" - y2="244" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line124" /> - <line - x1="541" - y1="241" - x2="541" - y2="247" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line126" /> - <line - x1="473" - y1="244" - x2="483" - y2="244" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line140" /> - </g> - </a> - <line - x1="473" - y1="296" - x2="483" - y2="296" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line142" /> - <line - x1="473" - y1="244" - x2="473" - y2="296" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line144" /> - <line - x1="463" - y1="270" - x2="473" - y2="270" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line146" /> - <a - id="a4267" - title="that may contain zero or more times one of the following nodes"> - <g - id="g482"> - <path - d="M432 263 L455 263 L461 269 L461 277 L455 283 L432 283 L426 277 L426 269 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="path148" /> - <path - d="M429 260 L452 260 L458 266 L458 274 L452 280 L429 280 L423 274 L423 266 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="path150" /> - <line - x1="428" - y1="270" - x2="432" - y2="270" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line152" /> - <line - x1="432" - y1="270" - x2="436" - y2="266" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line154" /> - <line - x1="444" - y1="266" - x2="448" - y2="266" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line156" /> - <line - x1="444" - y1="270" - x2="452" - y2="270" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line158" /> - <line - x1="444" - y1="274" - x2="448" - y2="274" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line160" /> - <line - x1="448" - y1="266" - x2="448" - y2="274" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line162" /> - <ellipse - cx="440" - cy="266" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse164" /> - <ellipse - cx="440" - cy="270" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse166" /> - <ellipse - cx="440" - cy="274" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse168" /> - <text - x="453" - y="290" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text170">0..∞</text> - <rect - x="453" - y="265" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect172" /> - <line - x1="455" - y1="270" - x2="461" - y2="270" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line174" /> - </g> - </a> - <line - x1="403" - y1="270" - x2="423" - y2="270" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line176" /> - <a - id="a4234" - title="zero or more <array> elements" - href="#array-tag"> - <g - id="g395"> - <rect - x="348" - y="262" - width="53" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect178" /> - <rect - x="345" - y="259" - width="53" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect180" /> - <text - x="371.5" - y="270" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text182">array</text> - <text - x="393" - y="291" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text184">0..∞</text> - <rect - x="393" - y="265" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect186" /> - <line - x1="395" - y1="270" - x2="401" - y2="270" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line188" /> - </g> - </a> - <a - id="a4243" - href="#header-tag" - title="zero or more <header> elements"> - <g - id="g400"> - <rect - x="348" - y="340" - width="57" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect190" /> - <rect - x="345" - y="337" - width="57" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect192" /> - <text - x="373.5" - y="348" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text194">header</text> - </g> - </a> - <text - x="392" - y="369" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text196">0..∞</text> - <a - id="a4249" - title="zero or more <param> elements" - href="#param-tag"> - <g - id="g406"> - <rect - x="348" - y="392" - width="54" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect206" /> - <rect - x="345" - y="389" - width="54" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect208" /> - <text - x="372" - y="400" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text210">param</text> - <text - x="389" - y="421" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text212">0..∞</text> - </g> - </a> - <a - id="a4216" - href="#object-tag" - title="zero or more <object> elements"> - <g - id="g381"> - <g - id="g346"> - <rect - id="rect86" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - height="22" - width="58" - y="80" - x="348" /> - <rect - id="rect88" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - height="22" - width="58" - y="77" - x="345" /> - <text - id="text90" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - y="88" - x="374">object</text> - <text - id="text92" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - y="109" - x="398">0..∞</text> - <rect - id="rect94" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - height="10" - width="10" - y="83" - x="398" /> - <line - id="line96" - style="stroke:rgb(0,0,0);stroke-width:1" - y2="88" - x2="406" - y1="88" - x1="400" /> - </g> - <line - x1="335" - y1="88" - x2="345" - y2="88" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line234" /> - </g> - </a> - <line - x1="335" - y1="192" - x2="345" - y2="192" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line236" /> - <line - x1="335" - y1="270" - x2="345" - y2="270" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line238" /> - <line - x1="335" - y1="348" - x2="345" - y2="348" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line240" /> - <line - x1="335" - y1="400" - x2="345" - y2="400" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line242" /> - <a - id="a4256" - title="one <contentType> element" - href="#contentType-tag"> - <g - id="g411"> - <rect - x="345" - y="441" - width="84" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect222" /> - <text - x="387" - y="452" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text224">contentType</text> - <line - x1="335" - y1="452" - x2="345" - y2="452" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line244" /> - </g> - </a> - <line - x1="335" - y1="88" - x2="335" - y2="452" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line246" /> - <line - x1="325" - y1="244" - x2="335" - y2="244" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line248" /> - <a - id="a4199" - title="that may contain zero or more times one of the following nodes"> - <g - id="g371"> - <path - d="M294 237 L317 237 L323 243 L323 251 L317 257 L294 257 L288 251 L288 243 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="path250" /> - <path - d="M291 234 L314 234 L320 240 L320 248 L314 254 L291 254 L285 248 L285 240 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="path252" /> - <line - x1="290" - y1="244" - x2="294" - y2="244" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line254" /> - <line - x1="294" - y1="244" - x2="298" - y2="240" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line256" /> - <line - x1="306" - y1="240" - x2="310" - y2="240" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line258" /> - <line - x1="306" - y1="244" - x2="314" - y2="244" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line260" /> - <line - x1="306" - y1="248" - x2="310" - y2="248" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line262" /> - <line - x1="310" - y1="240" - x2="310" - y2="248" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line264" /> - <ellipse - cx="302" - cy="240" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse266" /> - <ellipse - cx="302" - cy="244" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse268" /> - <ellipse - cx="302" - cy="248" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse270" /> - <text - x="315" - y="264" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text272">0..∞</text> - <rect - x="315" - y="239" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect274" /> - <line - x1="317" - y1="244" - x2="323" - y2="244" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line276" /> - </g> - </a> - <line - x1="265" - y1="244" - x2="285" - y2="244" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line278" /> - <a - id="a4190" - title="zero or more <operation> elements" - href="#operation-tag"> - <g - id="g338"> - <rect - x="188" - y="236" - width="75" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect280" /> - <rect - x="185" - y="233" - width="75" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect282" /> - <text - x="222.5" - y="244" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text284">operation</text> - <text - x="255" - y="265" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text286">0..∞</text> - <rect - x="255" - y="239" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect288" /> - <line - x1="257" - y1="244" - x2="263" - y2="244" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line290" /> - </g> - </a> - <line - x1="165" - y1="244" - x2="185" - y2="244" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line292" /> - <a - id="a4180" - title="that contains a sequence of"> - <g - id="g355"> - <path - d="M131 234 L154 234 L160 240 L160 248 L154 254 L131 254 L125 248 L125 240 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="path294" /> - <line - x1="128" - y1="244" - x2="157" - y2="244" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line296" /> - <ellipse - cx="137" - cy="244" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse298" /> - <ellipse - cx="142" - cy="244" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse300" /> - <ellipse - cx="147" - cy="244" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse302" /> - <rect - x="155" - y="239" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect304" /> - <line - x1="157" - y1="244" - x2="163" - y2="244" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line306" /> - </g> - </a> - <line - x1="105" - y1="244" - x2="125" - y2="244" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line308" /> - <a - id="a484" - href="#operations-tag" - xlink:arcrole="The <operations> element"> - <g - id="g330"> - <rect - x="20" - y="233" - width="80" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect310" /> - <text - x="60" - y="244" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text312">operations</text> - <rect - x="95" - y="239" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect314" /> - <line - x1="97" - y1="244" - x2="103" - y2="244" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line316" /> - </g> - </a> -</svg> diff --git a/docs/img/mftf-fork.gif b/docs/img/mftf-fork.gif deleted file mode 100644 index 6ea138cce..000000000 Binary files a/docs/img/mftf-fork.gif and /dev/null differ diff --git a/docs/img/page-dia.svg b/docs/img/page-dia.svg deleted file mode 100644 index 668891db0..000000000 --- a/docs/img/page-dia.svg +++ /dev/null @@ -1,290 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:xlink="http://www.w3.org/1999/xlink" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="370" - height="62" - version="1.1" - id="svg78" - sodipodi:docname="page-dia.svg" - inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"> - <metadata - id="metadata84"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - </cc:Work> - </rdf:RDF> - </metadata> - <defs - id="defs82" /> - <sodipodi:namedview - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1" - objecttolerance="10" - gridtolerance="10" - guidetolerance="10" - inkscape:pageopacity="0" - inkscape:pageshadow="2" - inkscape:window-width="1920" - inkscape:window-height="1017" - id="namedview80" - showgrid="false" - inkscape:zoom="1.5351351" - inkscape:cx="91.410446" - inkscape:cy="18.181373" - inkscape:window-x="3278" - inkscape:window-y="-8" - inkscape:window-maximized="1" - inkscape:current-layer="svg78" /> - <rect - x="304" - y="28" - width="59" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect2" /> - <a - href="#section-tag" - id="a8" - title="zero or more <secion> elements"> - <rect - x="301" - y="25" - width="59" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect4" /> - <text - x="330.5" - y="36" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text6">section</text> - </a> - <text - x="350" - y="57" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text10">0..∞</text> - <line - x1="281" - y1="36" - x2="301" - y2="36" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line16" /> - <a - id="a3770" - title="that contains a sequence of"> - <g - id="g3768"> - <path - d="M247 26 L270 26 L276 32 L276 40 L270 46 L247 46 L241 40 L241 32 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="path18" /> - <line - x1="244" - y1="36" - x2="273" - y2="36" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line20" /> - <ellipse - cx="253" - cy="36" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse22" /> - <ellipse - cx="258" - cy="36" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse24" /> - <ellipse - cx="263" - cy="36" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse26" /> - <rect - x="271" - y="31" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect28" /> - <line - x1="273" - y1="36" - x2="279" - y2="36" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line30" /> - </g> - </a> - <line - x1="221" - y1="36" - x2="241" - y2="36" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line32" /> - <rect - x="167" - y="28" - width="52" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect34" /> - <a - href="#page-tag" - id="a40" - title="one or more <page> elements"> - <rect - x="164" - y="25" - width="52" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect36" /> - <text - x="190" - y="36" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text38">page</text> - </a> - <text - x="211" - y="57" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text42">1..∞</text> - <rect - x="211" - y="31" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect48" /> - <line - x1="213" - y1="36" - x2="219" - y2="36" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line50" /> - <line - x1="144" - y1="36" - x2="164" - y2="36" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line52" /> - <a - id="a3751" - title="that contains a sequence of"> - <g - id="g3749"> - <path - d="M110 26 L133 26 L139 32 L139 40 L133 46 L110 46 L104 40 L104 32 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="path54" /> - <line - x1="107" - y1="36" - x2="136" - y2="36" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line56" /> - <ellipse - cx="116" - cy="36" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse58" /> - <ellipse - cx="121" - cy="36" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse60" /> - <ellipse - cx="126" - cy="36" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse62" /> - <rect - x="134" - y="31" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect64" /> - <line - x1="136" - y1="36" - x2="142" - y2="36" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line66" /> - </g> - </a> - <line - x1="84" - y1="36" - x2="104" - y2="36" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line68" /> - <a - id="a46" - href="#pages-tag" - title="Root element <pages>"> - <g - id="g44"> - <rect - x="20" - y="25" - width="59" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect70" /> - <text - x="49.5" - y="36" - style="font-weight:bold;font-size:10.66666698px;font-family:Arial;dominant-baseline:central;text-anchor:middle;fill:#000000" - id="text72">pages</text> - <rect - x="74" - y="31" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect74" /> - <line - x1="76" - y1="36" - x2="82" - y2="36" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line76" /> - </g> - </a> -</svg> diff --git a/docs/img/pull-request.png b/docs/img/pull-request.png deleted file mode 100644 index c492350d8..000000000 Binary files a/docs/img/pull-request.png and /dev/null differ diff --git a/docs/img/section-dia.svg b/docs/img/section-dia.svg deleted file mode 100644 index 1ee7611af..000000000 --- a/docs/img/section-dia.svg +++ /dev/null @@ -1,296 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:xlink="http://www.w3.org/1999/xlink" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="385" - height="62" - version="1.1" - id="svg74" - sodipodi:docname="section-dia.svg" - inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"> - <metadata - id="metadata80"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - </cc:Work> - </rdf:RDF> - </metadata> - <defs - id="defs78" /> - <sodipodi:namedview - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1" - objecttolerance="10" - gridtolerance="10" - guidetolerance="10" - inkscape:pageopacity="0" - inkscape:pageshadow="2" - inkscape:window-width="1920" - inkscape:window-height="1017" - id="namedview76" - showgrid="false" - inkscape:zoom="2.0864242" - inkscape:cx="154.86083" - inkscape:cy="15.327771" - inkscape:window-x="3278" - inkscape:window-y="-8" - inkscape:window-maximized="1" - inkscape:current-layer="svg74" /> - <rect - x="316" - y="28" - width="62" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect2" /> - <a - id="a3803" - href="#element-tag" - title="one or more <element> elements"> - <g - id="g88"> - <rect - x="313" - y="25" - width="62" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect4" /> - <text - x="344" - y="36" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text6">element</text> - </g> - </a> - <text - x="365" - y="57" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text8">1..∞</text> - <line - x1="293" - y1="36" - x2="313" - y2="36" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line14" /> - <a - id="a3777" - title="that contains a sequence of"> - <g - id="g3775"> - <path - d="M259 26 L282 26 L288 32 L288 40 L282 46 L259 46 L253 40 L253 32 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="path16" /> - <line - x1="256" - y1="36" - x2="285" - y2="36" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line18" /> - <ellipse - cx="265" - cy="36" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse20" /> - <ellipse - cx="270" - cy="36" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse22" /> - <ellipse - cx="275" - cy="36" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse24" /> - <rect - x="283" - y="31" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect26" /> - <line - x1="285" - y1="36" - x2="291" - y2="36" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line28" /> - </g> - </a> - <line - x1="233" - y1="36" - x2="253" - y2="36" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line30" /> - <rect - x="167" - y="28" - width="64" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect32" /> - <a - id="a109" - href="#section-tag" - title="one or more <section> elements"> - <g - id="g84"> - <path - inkscape:connector-curvature="0" - id="rect34" - d="m 164,25 c 21.33333,0 42.66667,0 64,0 0,7.333333 0,14.666667 0,22 -21.33333,0 -42.66667,0 -64,0 0,-7.333333 0,-14.666667 0,-22 z" - style="fill:#ffffff;stroke:#000000;stroke-width:1" /> - <path - inkscape:connector-curvature="0" - id="text36" - d="m 177.5625,37.363255 c 1.52529,-0.935044 2.52281,1.779504 3.69792,0.04687 -1.41244,-0.537747 -5.23048,-1.866312 -2.60417,-3.849609 1.13374,-1.247589 5.42381,0.888062 2.93622,1.363663 -0.52908,-0.06221 -2.59658,-1.343691 -2.32164,0.136988 1.91588,0.03374 5.07512,1.94106 2.46713,3.697591 -1.42333,0.629818 -3.75169,0.397163 -4.17546,-1.395508 z m 9.65625,-0.182291 c 2.44842,-0.276007 0.71062,2.155886 -0.94792,1.885417 -2.98868,0.278515 -3.66937,-4.498438 -1.14974,-5.585938 1.95032,-1.411312 4.8677,2.271182 3.08252,3.117187 -1.03835,0 -2.07671,0 -3.11507,0 -0.18497,1.277388 1.81333,2.021528 2.13021,0.583334 z m 0.0833,-1.479167 c 0.38377,-2.44698 -3.53646,-0.483165 -1.6359,0 0.5453,0 1.0906,0 1.6359,0 z m 7.47396,-0.65625 c -1.52854,0.994728 -2.51116,-1.881488 -3.55989,0.273438 -0.96483,1.83752 1.70256,3.656455 2.33125,1.525753 2.64318,-0.225852 0.0544,2.909802 -1.55668,2.20968 -3.17146,-0.115705 -3.1548,-5.664414 0.0212,-5.757325 1.21388,-0.172487 2.50213,0.483367 2.76416,1.748454 z m 3.65104,-1.635417 c 0.4678,1.281947 -0.63769,0.990621 -1,1.442464 0.17471,0.96121 -0.40103,2.406694 0.40625,3.000245 1.95951,0.06826 -0.55728,2.393732 -1.625,0.739583 -0.45945,-1.271518 -0.16723,-2.680854 -0.25,-4.015625 -0.97874,0.476369 -0.99812,-1.649412 0,-1.166667 -0.31505,-1.053184 0.48134,-1.467007 1.31127,-1.861536 0.54105,-0.0085 -0.30244,1.868393 0.43328,1.861536 0.2414,0 0.4828,0 0.7242,0 z m 1.02604,-0.75 c -0.20685,-1.071939 0.0909,-1.639223 1.24109,-1.354166 0.51629,0.342671 0.34899,1.883179 -0.74283,1.354166 -0.16609,0 -0.33217,0 -0.49826,0 z m 0,6.281251 c 0,-1.843751 0,-3.687501 0,-5.531251 0.96324,-0.107657 1.81965,-0.108492 1.46355,1.103188 0,1.476021 0,2.952042 0,4.428063 -0.48785,0 -0.9757,0 -1.46355,0 z m 2.6198,-2.843751 c -0.18497,-2.633509 3.5807,-3.821658 5.09424,-1.784912 1.49262,1.743986 0.16168,4.91742 -2.23487,4.753663 -1.65062,0.06716 -2.99523,-1.308404 -2.85937,-2.968751 z m 1.5,0.07813 c -0.0314,3.170045 4.26045,0.891586 2.3125,-1.260416 -0.96403,-1.094803 -2.49485,0.0058 -2.3125,1.260416 z m 10.39062,2.765626 c -0.96323,0.107657 -1.81964,0.108491 -1.46354,-1.103189 -0.16753,-1.109612 0.46733,-2.707337 -0.63151,-3.396812 -1.96808,-0.190926 -1.40078,2.230498 -1.48307,3.511676 0.38258,1.199876 -0.60098,1.033596 -1.46354,0.988325 0,-1.843751 0,-3.687501 0,-5.531251 1.63308,-0.62997 1.077,1.516748 2.17708,0.109375 2.22452,-1.125947 3.22655,1.30944 2.86458,3.087564 0,0.778104 0,1.556208 0,2.334312 z" - style="font-weight:bold;font-size:10.66666698px;font-family:Arial;dominant-baseline:central;text-anchor:middle;fill:#000000" /> - </g> - </a> - <text - x="223" - y="57" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text38">1..∞</text> - <rect - x="223" - y="31" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect44" /> - <line - x1="225" - y1="36" - x2="231" - y2="36" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line46" /> - <line - x1="144" - y1="36" - x2="164" - y2="36" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line48" /> - <a - id="a3757" - title="that contains a sequence of"> - <g - id="g3755"> - <path - d="M110 26 L133 26 L139 32 L139 40 L133 46 L110 46 L104 40 L104 32 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="path50" /> - <line - x1="107" - y1="36" - x2="136" - y2="36" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line52" /> - <ellipse - cx="116" - cy="36" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse54" /> - <ellipse - cx="121" - cy="36" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse56" /> - <a - id="a3744"> - <ellipse - id="ellipse58" - style="rgb(0,0,0)" - ry="2" - rx="2" - cy="36" - cx="126" /> - </a> - <rect - x="134" - y="31" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect60" /> - <line - x1="136" - y1="36" - x2="142" - y2="36" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line62" /> - </g> - </a> - <line - x1="84" - y1="36" - x2="104" - y2="36" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line64" /> - <a - id="a48" - title="Root element <sections>"> - <g - id="g46"> - <rect - x="20" - y="25" - width="59" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect66" /> - <text - x="49.5" - y="36" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text68">sections</text> - <rect - x="74" - y="31" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect70" /> - <line - x1="76" - y1="36" - x2="82" - y2="36" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line72" /> - </g> - </a> -</svg> diff --git a/docs/img/switching-the-base.png b/docs/img/switching-the-base.png deleted file mode 100644 index 412ab1624..000000000 Binary files a/docs/img/switching-the-base.png and /dev/null differ diff --git a/docs/img/test-dia.svg b/docs/img/test-dia.svg deleted file mode 100644 index fcf2628da..000000000 --- a/docs/img/test-dia.svg +++ /dev/null @@ -1,1228 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:xlink="http://www.w3.org/1999/xlink" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="601" - height="322" - version="1.1" - id="svg330" - sodipodi:docname="test-dia.svg" - inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"> - <metadata - id="metadata336"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - </cc:Work> - </rdf:RDF> - </metadata> - <defs - id="defs334" /> - <sodipodi:namedview - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1" - objecttolerance="10" - gridtolerance="10" - guidetolerance="10" - inkscape:pageopacity="0" - inkscape:pageshadow="2" - inkscape:window-width="1920" - inkscape:window-height="1017" - id="namedview332" - showgrid="false" - inkscape:zoom="1.890183" - inkscape:cx="272.85453" - inkscape:cy="165.81687" - inkscape:window-x="3278" - inkscape:window-y="-8" - inkscape:window-maximized="1" - inkscape:current-layer="svg330" /> - <g - id="g398"> - <path - id="path2" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - d="M354 25 L436 25 L442 31 L442 41 L436 47 L354 47 L348 41 L348 31 Z" /> - <text - id="text4" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - y="36" - x="395">testTypeTags</text> - <line - id="line6" - style="stroke:rgb(0,0,0);stroke-width:2" - y2="41" - x2="354" - y1="44" - x1="351" /> - <path - id="path8" - style="fill:rgb(0,0,0)" - d="M354 41 L356 43 L357 38 L352 39 L354 41 Z" /> - <rect - id="rect10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - height="10" - width="10" - y="31" - x="437" /> - <line - id="line12" - style="stroke:rgb(0,0,0);stroke-width:1" - y2="36" - x2="445" - y1="36" - x1="439" /> - <line - id="line14" - style="stroke:rgb(0,0,0);stroke-width:1" - y2="39" - x2="442" - y1="33" - x1="442" /> - </g> - <g - id="g429"> - <path - id="path16" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - d="M498 77 L580 77 L586 83 L586 93 L580 99 L498 99 L492 93 L492 83 Z" /> - <text - id="text18" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - y="88" - x="539">testTypeTags</text> - <line - id="line20" - style="stroke:rgb(0,0,0);stroke-width:2" - y2="93" - x2="498" - y1="96" - x1="495" /> - <path - id="path22" - style="fill:rgb(0,0,0)" - d="M498 93 L500 95 L501 90 L496 91 L498 93 Z" /> - <rect - id="rect24" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - height="10" - width="10" - y="83" - x="581" /> - <line - id="line26" - style="stroke:rgb(0,0,0);stroke-width:1" - y2="88" - x2="589" - y1="88" - x1="583" /> - <line - id="line28" - style="stroke:rgb(0,0,0);stroke-width:1" - y2="91" - x2="586" - y1="85" - x1="586" /> - </g> - <line - x1="472" - y1="88" - x2="492" - y2="88" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line30" /> - <a - id="a4301" - title="that may contain any number of the following nodes in any order"> - <g - id="g420"> - <path - d="M441 81 L464 81 L470 87 L470 95 L464 101 L441 101 L435 95 L435 87 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="path32" /> - <path - d="M438 78 L461 78 L467 84 L467 92 L461 98 L438 98 L432 92 L432 84 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="path34" /> - <line - x1="437" - y1="88" - x2="441" - y2="88" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line36" /> - <line - x1="441" - y1="88" - x2="445" - y2="84" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line38" /> - <line - x1="453" - y1="84" - x2="457" - y2="84" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line40" /> - <line - x1="453" - y1="88" - x2="461" - y2="88" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line42" /> - <line - x1="453" - y1="92" - x2="457" - y2="92" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line44" /> - <line - x1="457" - y1="84" - x2="457" - y2="92" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line46" /> - <ellipse - cx="449" - cy="84" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse48" /> - <ellipse - cx="449" - cy="88" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse50" /> - <ellipse - cx="449" - cy="92" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse52" /> - <text - x="462" - y="108" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text54">0..∞</text> - <rect - x="462" - y="83" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect56" /> - <line - x1="464" - y1="88" - x2="470" - y2="88" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line58" /> - </g> - </a> - <line - x1="412" - y1="88" - x2="432" - y2="88" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line60" /> - <a - id="a4270" - href="#before-tag" - title="zero or one <before> element"> - <g - id="g404"> - <rect - x="348" - y="77" - width="59" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect62" /> - <text - x="377.5" - y="88" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text64">before</text> - <rect - x="402" - y="83" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect66" /> - <line - x1="404" - y1="88" - x2="410" - y2="88" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line68" /> - </g> - </a> - <g - id="g438"> - <path - id="path70" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - d="M489 129 L571 129 L577 135 L577 145 L571 151 L489 151 L483 145 L483 135 Z" /> - <text - id="text72" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - y="140" - x="530">testTypeTags</text> - <line - id="line74" - style="stroke:rgb(0,0,0);stroke-width:2" - y2="145" - x2="489" - y1="148" - x1="486" /> - <path - id="path76" - style="fill:rgb(0,0,0)" - d="M489 145 L491 147 L492 142 L487 143 L489 145 Z" /> - <rect - id="rect78" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - height="10" - width="10" - y="135" - x="572" /> - <line - id="line80" - style="stroke:rgb(0,0,0);stroke-width:1" - y2="140" - x2="580" - y1="140" - x1="574" /> - <line - id="line82" - style="stroke:rgb(0,0,0);stroke-width:1" - y2="143" - x2="577" - y1="137" - x1="577" /> - </g> - <line - x1="463" - y1="140" - x2="483" - y2="140" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line84" /> - <a - id="a4318" - title="that may contain any number of the following nodes in any order"> - <g - id="g460"> - <path - d="M432 133 L455 133 L461 139 L461 147 L455 153 L432 153 L426 147 L426 139 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="path86" /> - <path - d="M429 130 L452 130 L458 136 L458 144 L452 150 L429 150 L423 144 L423 136 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="path88" /> - <line - x1="428" - y1="140" - x2="432" - y2="140" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line90" /> - <line - x1="432" - y1="140" - x2="436" - y2="136" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line92" /> - <line - x1="444" - y1="136" - x2="448" - y2="136" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line94" /> - <line - x1="444" - y1="140" - x2="452" - y2="140" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line96" /> - <line - x1="444" - y1="144" - x2="448" - y2="144" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line98" /> - <line - x1="448" - y1="136" - x2="448" - y2="144" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line100" /> - <ellipse - cx="440" - cy="136" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse102" /> - <ellipse - cx="440" - cy="140" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse104" /> - <ellipse - cx="440" - cy="144" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse106" /> - <text - x="453" - y="160" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text108">0..∞</text> - <rect - x="453" - y="135" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect110" /> - <line - x1="455" - y1="140" - x2="461" - y2="140" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line112" /> - </g> - </a> - <line - x1="403" - y1="140" - x2="423" - y2="140" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line114" /> - <a - id="a4277" - href="#after-tag" - title="zero or one <after> element"> - <g - id="g444"> - <rect - x="348" - y="129" - width="50" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect116" /> - <text - x="373" - y="140" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text118">after</text> - <rect - x="393" - y="135" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect120" /> - <line - x1="395" - y1="140" - x2="401" - y2="140" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line122" /> - </g> - </a> - <a - id="a4335" - title="zero or one <annotations> element" - href="#annotations-tag"> - <g - id="g467"> - <rect - x="348" - y="181" - width="86" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect124" /> - <text - x="391" - y="192" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text126">annotations</text> - <rect - x="429" - y="187" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect128" /> - <line - x1="431" - y1="192" - x2="437" - y2="192" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line130" /> - <line - x1="434" - y1="189" - x2="434" - y2="195" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line132" /> - </g> - </a> - <line - x1="338" - y1="36" - x2="348" - y2="36" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line134" /> - <line - x1="338" - y1="88" - x2="348" - y2="88" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line136" /> - <line - x1="338" - y1="140" - x2="348" - y2="140" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line138" /> - <line - x1="338" - y1="192" - x2="348" - y2="192" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line140" /> - <line - x1="338" - y1="36" - x2="338" - y2="192" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line142" /> - <line - x1="328" - y1="114" - x2="338" - y2="114" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line144" /> - <a - id="a4284" - title="that may contain any number of the following nodes in any order"> - <g - id="g389"> - <path - d="M297 107 L320 107 L326 113 L326 121 L320 127 L297 127 L291 121 L291 113 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="path146" /> - <path - d="M294 104 L317 104 L323 110 L323 118 L317 124 L294 124 L288 118 L288 110 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="path148" /> - <line - x1="293" - y1="114" - x2="297" - y2="114" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line150" /> - <line - x1="297" - y1="114" - x2="301" - y2="110" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line152" /> - <line - x1="309" - y1="110" - x2="313" - y2="110" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line154" /> - <line - x1="309" - y1="114" - x2="317" - y2="114" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line156" /> - <line - x1="309" - y1="118" - x2="313" - y2="118" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line158" /> - <line - x1="313" - y1="110" - x2="313" - y2="118" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line160" /> - <ellipse - cx="305" - cy="110" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse162" /> - <ellipse - cx="305" - cy="114" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse164" /> - <ellipse - cx="305" - cy="118" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse166" /> - <text - x="318" - y="134" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text168">0..∞</text> - <rect - x="318" - y="109" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect170" /> - <line - x1="320" - y1="114" - x2="326" - y2="114" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line172" /> - </g> - </a> - <line - x1="268" - y1="114" - x2="288" - y2="114" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line174" /> - <a - id="a4261" - title="one or more <test> elements" - href="#test-tag"> - <g - id="g373"> - <rect - x="220" - y="106" - width="46" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect176" /> - <rect - x="217" - y="103" - width="46" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect178" /> - <text - x="240" - y="114" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text180">test</text> - <text - x="258" - y="135" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text182">1..∞</text> - <rect - x="258" - y="109" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect184" /> - <line - x1="260" - y1="114" - x2="266" - y2="114" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line186" /> - </g> - </a> - <line - x1="197" - y1="114" - x2="217" - y2="114" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line188" /> - <a - id="a4205" - title="that contains one of the following nodes"> - <g - id="g365"> - <path - d="M163 104 L186 104 L192 110 L192 118 L186 124 L163 124 L157 118 L157 110 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="path190" /> - <line - x1="162" - y1="114" - x2="166" - y2="114" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line192" /> - <line - x1="166" - y1="114" - x2="170" - y2="110" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line194" /> - <line - x1="178" - y1="110" - x2="182" - y2="110" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line196" /> - <line - x1="178" - y1="114" - x2="186" - y2="114" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line198" /> - <line - x1="178" - y1="118" - x2="182" - y2="118" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line200" /> - <line - x1="182" - y1="110" - x2="182" - y2="118" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line202" /> - <ellipse - cx="174" - cy="110" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse204" /> - <ellipse - cx="174" - cy="114" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse206" /> - <ellipse - cx="174" - cy="118" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse208" /> - <rect - x="187" - y="109" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect210" /> - <line - x1="189" - y1="114" - x2="195" - y2="114" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line212" /> - </g> - </a> - <line - x1="137" - y1="114" - x2="157" - y2="114" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line214" /> - <a - id="a4195" - title="that contains a sequence of the following nodes"> - <g - id="g351"> - <path - d="M103 104 L126 104 L132 110 L132 118 L126 124 L103 124 L97 118 L97 110 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="path216" /> - <line - x1="100" - y1="114" - x2="129" - y2="114" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line218" /> - <ellipse - cx="109" - cy="114" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse220" /> - <ellipse - cx="114" - cy="114" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse222" /> - <ellipse - cx="119" - cy="114" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse224" /> - <rect - x="127" - y="109" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect226" /> - <line - x1="129" - y1="114" - x2="135" - y2="114" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line228" /> - </g> - </a> - <line - x1="77" - y1="114" - x2="97" - y2="114" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line230" /> - <a - id="a499" - href="#tests-tag" - title="Root element <tests>"> - <g - id="g342"> - <rect - x="20" - y="103" - width="52" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect232" /> - <text - x="46" - y="114" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text234">tests</text> - <rect - x="67" - y="109" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect236" /> - <line - x1="69" - y1="114" - x2="75" - y2="114" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line238" /> - </g> - </a> - <a - href="test/actions.html" - title="one of <action> elements" - target="_blank" - id="a4355"> - <g - id="g497"> - <path - d="M205 233 L299 233 L305 239 L305 249 L299 255 L205 255 L199 249 L199 239 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="path240" /> - <text - x="252" - y="244" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text242">actionTypeTags</text> - <line - x1="202" - y1="252" - x2="205" - y2="249" - style="stroke:rgb(0,0,0);stroke-width:2" - id="line244" /> - <path - d="M205 249 L207 251 L208 246 L203 247 L205 249 Z" - style="fill:rgb(0,0,0)" - id="path246" /> - <rect - x="300" - y="239" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect248" /> - <line - x1="302" - y1="244" - x2="308" - y2="244" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line250" /> - <line - x1="305" - y1="241" - x2="305" - y2="247" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line252" /> - </g> - </a> - <a - id="a4350" - href="#argument-tag" - title="one <argument> element"> - <g - id="g471"> - <rect - x="373" - y="285" - width="70" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect254" /> - <text - x="408" - y="296" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text256">argument</text> - </g> - </a> - <line - x1="353" - y1="296" - x2="373" - y2="296" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line258" /> - <a - id="a4220" - title="that may contain multiple sequences of the following node"> - <g - id="g482"> - <path - d="M322 289 L345 289 L351 295 L351 303 L345 309 L322 309 L316 303 L316 295 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="path260" /> - <path - d="M319 286 L342 286 L348 292 L348 300 L342 306 L319 306 L313 300 L313 292 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="path262" /> - <line - x1="316" - y1="296" - x2="345" - y2="296" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line264" /> - <ellipse - cx="325" - cy="296" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse266" /> - <ellipse - cx="330" - cy="296" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse268" /> - <ellipse - cx="335" - cy="296" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse270" /> - <text - x="343" - y="316" - style="font-family:Arial;font-size:7.2pt;fill:rgb(0,0,0);text-anchor:end;dominant-baseline:central" - id="text272">0..∞</text> - <rect - x="343" - y="291" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect274" /> - <line - x1="345" - y1="296" - x2="351" - y2="296" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line276" /> - </g> - </a> - <line - x1="293" - y1="296" - x2="313" - y2="296" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line278" /> - <a - id="a4343" - href="#actiongroup-tag" - target="" - title="zero or one <actionGroup> elements"> - <g - id="g488"> - <rect - x="199" - y="285" - width="89" - height="22" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1;stroke-dasharray:4,1" - id="rect280" /> - <text - x="243.5" - y="296" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text282">actionGroup</text> - <rect - x="283" - y="291" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect284" /> - <line - x1="285" - y1="296" - x2="291" - y2="296" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line286" /> - </g> - </a> - <line - x1="189" - y1="244" - x2="199" - y2="244" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line288" /> - <line - x1="189" - y1="296" - x2="199" - y2="296" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line290" /> - <line - x1="189" - y1="244" - x2="189" - y2="296" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line292" /> - <line - x1="179" - y1="270" - x2="189" - y2="270" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line294" /> - <a - id="a4246" - title="that contains one of the following nodes"> - <g - id="g4244"> - <path - d="M145 260 L168 260 L174 266 L174 274 L168 280 L145 280 L139 274 L139 266 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="path296" /> - <line - x1="144" - y1="270" - x2="148" - y2="270" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line298" /> - <line - x1="148" - y1="270" - x2="152" - y2="266" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line300" /> - <line - x1="160" - y1="266" - x2="164" - y2="266" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line302" /> - <line - x1="160" - y1="270" - x2="168" - y2="270" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line304" /> - <line - x1="160" - y1="274" - x2="164" - y2="274" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line306" /> - <line - x1="164" - y1="266" - x2="164" - y2="274" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line308" /> - <ellipse - cx="156" - cy="266" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse310" /> - <ellipse - cx="156" - cy="270" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse312" /> - <ellipse - cx="156" - cy="274" - rx="2" - ry="2" - style="rgb(0,0,0)" - id="ellipse314" /> - <rect - x="169" - y="265" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect316" /> - <line - x1="171" - y1="270" - x2="177" - y2="270" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line318" /> - </g> - </a> - <line - x1="119" - y1="270" - x2="139" - y2="270" - style="stroke:rgb(0,0,0);stroke-width:1;stroke-linecap:round" - id="line320" /> - <path - d="M26 259 L108 259 L114 265 L114 275 L108 281 L26 281 L20 275 L20 265 Z" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="path322" /> - <text - x="67" - y="270" - style="font-family:Arial;font-size:8pt;fill:rgb(0,0,0);font-weight:bold;text-anchor:middle;dominant-baseline:central" - id="text324">testTypeTags</text> - <rect - x="109" - y="265" - width="10" - height="10" - style="fill:rgb(255,255,255);stroke:rgb(0,0,0);stroke-width:1" - id="rect326" /> - <line - x1="111" - y1="270" - x2="117" - y2="270" - style="stroke:rgb(0,0,0);stroke-width:1" - id="line328" /> -</svg> diff --git a/docs/img/trouble-chrome232.png b/docs/img/trouble-chrome232.png deleted file mode 100644 index 1dc44f6a4..000000000 Binary files a/docs/img/trouble-chrome232.png and /dev/null differ diff --git a/docs/introduction.md b/docs/introduction.md deleted file mode 100644 index b68cca819..000000000 --- a/docs/introduction.md +++ /dev/null @@ -1,144 +0,0 @@ -# Introduction to the Magento Functional Testing Framework - -<div class="bs-callout bs-callout-info" markdown="1"> -These are the docs for the latest MFTF release. -To find older documentation, please refer to the [docs folder] of your desired release in Github. -</div> - -[Find your MFTF version][] of the MFTF. - -The Magento Functional Testing Framework (MFTF) aims to replace the [Functional Testing Framework] in future releases. -MFTF improves: - -- **Traceability** for clear logging and reporting capabilities. -- **Modularity** to run tests based on installed modules and extensions. -- **Customizability** for existing tests. -- **Readability** using clear and declarative XML test steps. -- **Maintainability** based on simple test creation and overall structure. - -Because MFTF tests are written in XML, you no longer need to learn PHP to write tests. - -<div class="bs-callout bs-callout-info" markdown="1"> -We are actively developing functional tests. -Refer to `<magento_root>/app/code/<vendor_name>/<module_name>/Test/Mftf/` for examples. -Check out the [MFTF Test Migration][] repo. -</div> - -## Audience - -This MFTF guide is intended for Magento developers and software engineers, such as QA specialists, PHP developers, and system integrators. - -## Goals - -The purpose of MFTF is to: - -- Facilitate functional testing and minimize the effort it takes to perform regression testing. -- Make it easier to support the extension and customization of tests via XML merging. - -## Scope - -MFTF will enable you to: - -- Test user interactions with web applications in testing. -- Write functional tests located in `<magento_root>/app/code/<vendor_name>/<module_name>/Test/Mftf/`. -- Cover basic functionality using out-of-the-box tests. You can test extended functionality using custom tests. -- Automate regression testing. - -## Use cases - -As a Magento developer, test changes, such as extended search functionality, a new form attribute, or new product tags. - -As a software engineer, perform regression testing before release to ensure that Magento works as expected with new functionality. - -## Find your MFTF version - -There are two options to find out your MFTF version: - -- using the MFTF CLI -- using the Composer CLI - -### MFTF CLI - -```bash -cd <magento_root>/ -``` - -```bash -vendor/bin/mftf --version -``` - -### Composer CLI - -```bash -cd <magento_root>/ -``` - -```bash -composer show magento/magento2-functional-testing-framework -``` - -## Contents of dev/tests/acceptance - -```tree -tests - _data // Additional files required for tests (e.g. pictures, CSV files for import/export, etc.) - _output // The directory is generated during test run. It contains testing reports. - _suite // Test suites. - _bootstrap.php // The script that executes essential initialization routines. - functional.suite.dist.yml // The Codeception functional test suite configuration (generated while running 'bin/mftf build:project') -utils // The test-running utilities. -.env.example // Example file for environmental settings. -.credentials.example // Example file for credentials to be used by the third party integrations (generated while running 'bin/mftf build:project'; should be filled with the appropriate credentials in the corresponding sandboxes). -.gitignore // List of files ignored by git. -.htaccess.sample // Access settings for the Apache web server to perform the Magento CLI commands. -codeception.dist.yml // Codeception configuration (generated while running 'bin/mftf build:project') -``` - -## MFTF output - -- Generated PHP Codeception tests -- Codeception results and console logs -- Screenshots and HTML failure report -- Allure formatted XML results -- Allure report dashboard of results - -## MFTF tests - -The MFTF supports three different locations for storing the tests and test artifacts: -- `<magento_root>/app/code/<vendor_name>/<module_name>/Test/Mftf/` is the directory to create new tests. -- `<magento_root>/vendor/<vendor_name>/<module_name>/Test/Mftf/` is the directory with the out of the box tests (fetched by the Composer). -- `<magento_root>/dev/tests/acceptance/tests/functional/<vendor_name>/<module_name>/` is used to store tests that depend on multiple modules. - -All tests and test data from these locations are merged in the order indicated in the above list. - -Directories immediately following the above paths will use the same format, and sub-directories under each category are supported. - -```tree -<Path> -├── ActionGroup -│   └── ... -├── Data -│   └── ... -├── Metadata -│   └── ... -├── Page -│   └── ... -├── Section -│   └── ... -├── Suite -│   └── ... -└── Test - └── ... -``` - -## MFTF on Github - -Follow the [MFTF project] and [contribute on Github]. - -<!-- Link definitions --> -[contribute on Github]: https://github.com/magento/magento2-functional-testing-framework/blob/master/.github/CONTRIBUTING.md -[Functional Testing Framework]: https://devdocs.magento.com/guides/v2.3/mtf/mtf_introduction.html -[MFTF project]: https://github.com/magento/magento2-functional-testing-framework -[Find your MFTF version]: #find-your-mftf-version -[MFTF Test Migration]: https://github.com/magento/magento-functional-tests-migration -[docs folder]: https://github.com/magento/magento2-functional-testing-framework/tree/master/docs diff --git a/docs/merge_points/extend-action-groups.md b/docs/merge_points/extend-action-groups.md deleted file mode 100644 index 95d6cff90..000000000 --- a/docs/merge_points/extend-action-groups.md +++ /dev/null @@ -1,102 +0,0 @@ -# Extend action groups - -Extending an action group doesn't affect the existing action group. - -In this example we add a `<click>` command to check the checkbox that our extension added with a new action group for the simple product creation form. - -## Starting action group - -<!-- {% raw %} --> - -```xml -<actionGroup name="FillAdminSimpleProductForm"> - <arguments> - <argument name="category"/> - <argument name="simpleProduct"/> - </arguments> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> - <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickAddSimpleProduct"/> - <fillField userInput="{{simpleProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> - <fillField userInput="{{simpleProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillSKU"/> - <fillField userInput="{{simpleProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> - <fillField userInput="{{simpleProduct.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> - <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[{{category.name}}]" stepKey="searchAndSelectCategory"/> - <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> - <fillField userInput="{{simpleProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> - <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="assertSaveMessageSuccess"/> - <seeInField userInput="{{simpleProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="assertFieldName"/> - <seeInField userInput="{{simpleProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="assertFieldSku"/> - <seeInField userInput="{{simpleProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="assertFieldPrice"/> - <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSectionAssert"/> - <seeInField userInput="{{simpleProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="assertFieldUrlKey"/> -</actionGroup> -``` - -## File to merge - -```xml -<actionGroup name="FillAdminSimpleProductFormWithMyExtension" extends="FillAdminSimpleProductForm"> - <!-- This will be added after the step "fillQuantity" on line 12 in the above test. --> - <click selector="{{MyExtensionSection.myCheckbox}}" stepKey="clickMyCheckbox" after="fillQuantity"/> -</actionGroup> -``` - -## Resultant action group - -Note that there are now two action groups below. - -```xml -<actionGroup name="FillAdminSimpleProductForm"> - <arguments> - <argument name="category"/> - <argument name="simpleProduct"/> - </arguments> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> - <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickAddSimpleProduct"/> - <fillField userInput="{{simpleProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> - <fillField userInput="{{simpleProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillSKU"/> - <fillField userInput="{{simpleProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> - <fillField userInput="{{simpleProduct.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> - <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[{{category.name}}]" stepKey="searchAndSelectCategory"/> - <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> - <fillField userInput="{{simpleProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> - <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="assertSaveMessageSuccess"/> - <seeInField userInput="{{simpleProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="assertFieldName"/> - <seeInField userInput="{{simpleProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="assertFieldSku"/> - <seeInField userInput="{{simpleProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="assertFieldPrice"/> - <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSectionAssert"/> - <seeInField userInput="{{simpleProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="assertFieldUrlKey"/> -</actionGroup> -<actionGroup name="FillAdminSimpleProductFormWithMyExtension"> - <arguments> - <argument name="category"/> - <argument name="simpleProduct"/> - </arguments> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> - <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickAddSimpleProduct"/> - <fillField userInput="{{simpleProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> - <fillField userInput="{{simpleProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillSKU"/> - <fillField userInput="{{simpleProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> - <fillField userInput="{{simpleProduct.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> - - <click selector="{{MyExtensionSection.myCheckbox}}" stepKey="clickMyCheckbox"/> - - <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[{{category.name}}]" stepKey="searchAndSelectCategory"/> - <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> - <fillField userInput="{{simpleProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> - <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="assertSaveMessageSuccess"/> - <seeInField userInput="{{simpleProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="assertFieldName"/> - <seeInField userInput="{{simpleProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="assertFieldSku"/> - <seeInField userInput="{{simpleProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="assertFieldPrice"/> - <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSectionAssert"/> - <seeInField userInput="{{simpleProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="assertFieldUrlKey"/> -</actionGroup> -``` - -<!-- {% endraw %} --> \ No newline at end of file diff --git a/docs/merge_points/extend-data.md b/docs/merge_points/extend-data.md deleted file mode 100644 index ebac16108..000000000 --- a/docs/merge_points/extend-data.md +++ /dev/null @@ -1,70 +0,0 @@ -# Extend data entities - -Extending a data entity does not affect the existing data entity. - -In this example we update the quantity to 1001 and add a new piece of data relevant to our extension. Unlike merging, this will _not_ affect any other tests that use this data entity. - -## Starting entity - -```xml -<entity name="SimpleProduct" type="product"> - <data key="sku" unique="suffix">SimpleProduct</data> - <data key="type_id">simple</data> - <data key="attribute_set_id">4</data> - <data key="name" unique="suffix">SimpleProduct</data> - <data key="price">123.00</data> - <data key="visibility">4</data> - <data key="status">1</data> - <data key="quantity">1000</data> - <data key="urlKey" unique="suffix">simpleproduct</data> - <data key="weight">1</data> - <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> - <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> -</entity> -``` - -## File to merge - -```xml -<entity name="ExtensionProduct" type="product" extends="SimpleProduct"> - <!-- myExtensionData will simply be added to the product and quantity will be changed to 1001. --> - <data key="quantity">1001</data> - <data key="myExtensionData">dataHere</data> -</entity> -``` - -## Resultant entity - -Note that there are now two data entities below. - -```xml -<entity name="SimpleProduct" type="product"> - <data key="sku" unique="suffix">SimpleProduct</data> - <data key="type_id">simple</data> - <data key="attribute_set_id">4</data> - <data key="name" unique="suffix">SimpleProduct</data> - <data key="price">123.00</data> - <data key="visibility">4</data> - <data key="status">1</data> - <data key="quantity">1000</data> - <data key="urlKey" unique="suffix">simpleproduct</data> - <data key="weight">1</data> - <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> - <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> -</entity> -<entity name="ExtensionProduct" type="product"> - <data key="sku" unique="suffix">SimpleProduct</data> - <data key="type_id">simple</data> - <data key="attribute_set_id">4</data> - <data key="name" unique="suffix">SimpleProduct</data> - <data key="price">123.00</data> - <data key="visibility">4</data> - <data key="status">1</data> - <data key="quantity">1001</data> - <data key="urlKey" unique="suffix">simpleproduct</data> - <data key="weight">1</data> - <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> - <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> - <data key="myExtensionData">dataHere</data> -</entity> -``` diff --git a/docs/merge_points/extend-tests.md b/docs/merge_points/extend-tests.md deleted file mode 100644 index 6aab50764..000000000 --- a/docs/merge_points/extend-tests.md +++ /dev/null @@ -1,145 +0,0 @@ -# Extend tests - -Tests can be extended to cover the needs of your extension. - -In this example, we add an action group to a new copy of the original test for our extension. - -## Starting test - -```xml -<test name="AdminCreateSimpleProductTest"> - <annotations> - <features value="Catalog"/> - <stories value="Create a Simple Product via Admin"/> - <title value="Admin should be able to create a Simple Product"/> - <description value="Admin should be able to create a Simple Product"/> - <severity value="CRITICAL"/> - <testCaseId value="MAGETWO-23414"/> - <group value="product"/> - </annotations> - <before> - <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> - </before> - <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> - <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> - </after> - - <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> - <actionGroup ref="FillAdminSimpleProductForm" stepKey="fillProductFieldsInAdmin"> - <argument name="category" value="$$createPreReqCategory$$"/> - <argument name="simpleProduct" value="_defaultProduct"/> - </actionGroup> - <actionGroup ref="AssertProductInStorefrontCategoryPage" stepKey="assertProductInStorefront1"> - <argument name="category" value="$$createPreReqCategory$$"/> - <argument name="product" value="_defaultProduct"/> - </actionGroup> - <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="assertProductInStorefront2"> - <argument name="product" value="_defaultProduct"/> - </actionGroup> -</test> -``` - -## File to merge - -```xml -<test name="AdminCreateSimpleProductExtensionTest" extends="AdminCreateSimpleProductTest"> - <!-- Since this is its own test you need the annotations block --> - <annotations> - <features value="Catalog"/> - <stories value="Create a Simple Product via Admin"/> <!-- you should leave this the same since it is part of the same group --> - <title value="Admin should be able to create a Simple Product with my extension"/> - <description value="Admin should be able to create a Simple Product with my extension via the product grid"/> - <severity value="CRITICAL"/> - <testCaseId value="Extension/Github Issue Number"/> - <group value="product"/> - </annotations> - <!-- This will be added after the step "fillProductFieldsInAdmin" on line 20 in the above test. --> - <actionGroup ref="AddMyExtensionData" stepKey="extensionField" after="fillProductFieldsInAdmin"> - <argument name="extensionData" value="_myData"/> - </actionGroup> - - <!-- This will be added after the step "assertProductInStorefront2" on line 28 in the above test. --> - <actionGroup ref="AssertMyExtensionDataExists" stepKey="assertExtensionInformation" after="assertProductInStorefront2"> - <argument name="extensionData" value="_myData"/> - </actionGroup> -</test> -``` - -## Resultant test - -Note that there are now two tests below. - -```xml -<test name="AdminCreateSimpleProductTest"> - <annotations> - <features value="Catalog"/> - <stories value="Create a Simple Product via Admin"/> - <title value="Admin should be able to create a Simple Product"/> - <description value="Admin should be able to create a Simple Product"/> - <severity value="CRITICAL"/> - <testCaseId value="MAGETWO-23414"/> - <group value="product"/> - </annotations> - <before> - <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> - </before> - <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> - <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> - </after> - - <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> - <actionGroup ref="FillAdminSimpleProductForm" stepKey="fillProductFieldsInAdmin"> - <argument name="category" value="$$createPreReqCategory$$"/> - <argument name="simpleProduct" value="_defaultProduct"/> - </actionGroup> - <actionGroup ref="AssertProductInStorefrontCategoryPage" stepKey="assertProductInStorefront1"> - <argument name="category" value="$$createPreReqCategory$$"/> - <argument name="product" value="_defaultProduct"/> - </actionGroup> - <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="assertProductInStorefront2"> - <argument name="product" value="_defaultProduct"/> - </actionGroup> -</test> -<test name="AdminCreateSimpleProductExtensionTest"> - <annotations> - <features value="Catalog"/> - <stories value="Create a Simple Product via Admin"/> - <title value="Admin should be able to create a Simple Product with my extension"/> - <description value="Admin should be able to create a Simple Product with my extension via the product grid"/> - <severity value="CRITICAL"/> - <testCaseId value="Extension/Github Issue Number"/> - <group value="product"/> - </annotations> - <before> - <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> - </before> - <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> - <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> - </after> - - <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> - <actionGroup ref="FillAdminSimpleProductForm" stepKey="fillProductFieldsInAdmin"> - <argument name="category" value="$$createPreReqCategory$$"/> - <argument name="simpleProduct" value="_defaultProduct"/> - </actionGroup> - - <actionGroup ref="AddMyExtensionData" stepKey="extensionField"> - <argument name="extensionData" value="_myData"/> - </actionGroup> - - <actionGroup ref="AssertProductInStorefrontCategoryPage" stepKey="assertProductInStorefront1"> - <argument name="category" value="$$createPreReqCategory$$"/> - <argument name="product" value="_defaultProduct"/> - </actionGroup> - <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="assertProductInStorefront2"> - <argument name="product" value="_defaultProduct"/> - </actionGroup> - - <actionGroup ref="AssertMyExtensionDataExists" stepKey="assertExtensionInformation"> - <argument name="extensionData" value="_myData"/> - </actionGroup> -</test> -``` diff --git a/docs/merge_points/introduction.md b/docs/merge_points/introduction.md deleted file mode 100644 index af03dade6..000000000 --- a/docs/merge_points/introduction.md +++ /dev/null @@ -1,39 +0,0 @@ -# Merge Points for testing extensions in MFTF - -The Magento Functional Testing Framework (MFTF) allows great flexibility when writing XML tests for extensions. -All parts of tests can be used, reused, and merged to best suit your needs and cut down on needless duplication. - -Extension developers can utilitze these merge points to leverage existing tests and modify just the parts needed to test their extension. For instance, if your extension adds a form field to a Catalog admin page, you can modify the existing Catalog tests and add actions, data, etc as needed to test the custom field. -This topic shows how to merge and reuse test elements when testing extensions. - -## Merging - -Follow the links below for an example of how to merge: - -- [Merge Action Groups][] -- [Merge Data][] -- [Merge Pages][] -- [Merge Sections][] -- [Merge Tests][] - -## Extending - -Only Test, Action Group, and Data objects in the MFTF Framework can be extended. -Extending can be very useful for extension developers since it will not affect existing tests. - -Consult [when to use Extends][] to use extends when deciding whether to merge or extend. - -- [Extend Action Groups][] -- [Extend Data][] -- [Extend Tests][] - -<!-- Link definitions --> -[when to use Extends]: ../best-practices.md#when-to-use-extends -[Merge Action Groups]: merge-action-groups.md -[Merge Data]: merge-data.md -[Merge Pages]: merge-pages.md -[Merge Sections]: merge-sections.md -[Merge Tests]: merge-tests.md -[Extend Action Groups]: extend-action-groups.md -[Extend Data]: extend-data.md -[Extend Tests]: extend-tests.md \ No newline at end of file diff --git a/docs/merge_points/merge-action-groups.md b/docs/merge_points/merge-action-groups.md deleted file mode 100644 index 3a4f70ab1..000000000 --- a/docs/merge_points/merge-action-groups.md +++ /dev/null @@ -1,78 +0,0 @@ -# Merge action groups - -An action group is a set of individual actions working together as a group. -These action groups can be shared between tests and they also be modified to your needs. - -In this example we add a `<click>` command to check the checkbox that our extension adds to the simple product creation form. - -## Starting test - -<!-- {% raw %} --> - -```xml -<actionGroup name="FillAdminSimpleProductForm"> - <arguments> - <argument name="category"/> - <argument name="simpleProduct"/> - </arguments> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> - <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickAddSimpleProduct"/> - <fillField userInput="{{simpleProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> - <fillField userInput="{{simpleProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillSKU"/> - <fillField userInput="{{simpleProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> - <fillField userInput="{{simpleProduct.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> - <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[{{category.name}}]" stepKey="searchAndSelectCategory"/> - <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> - <fillField userInput="{{simpleProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> - <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="assertSaveMessageSuccess"/> - <seeInField userInput="{{simpleProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="assertFieldName"/> - <seeInField userInput="{{simpleProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="assertFieldSku"/> - <seeInField userInput="{{simpleProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="assertFieldPrice"/> - <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSectionAssert"/> - <seeInField userInput="{{simpleProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="assertFieldUrlKey"/> -</actionGroup> -``` - -## File to merge - -```xml -<actionGroup name="FillAdminSimpleProductForm"> - <!-- This will be added after the step "fillQuantity" in the above test. --> - <click selector="{{MyExtensionSection.myCheckbox}}" stepKey="clickMyCheckbox" after="fillQuantity"/> -</actionGroup> -``` - -## Resultant test - -```xml -<actionGroup name="FillAdminSimpleProductForm"> - <arguments> - <argument name="category"/> - <argument name="simpleProduct"/> - </arguments> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> - <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickAddSimpleProduct"/> - <fillField userInput="{{simpleProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> - <fillField userInput="{{simpleProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillSKU"/> - <fillField userInput="{{simpleProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> - <fillField userInput="{{simpleProduct.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> - <!-- Merged line here --> - <click selector="{{MyExtensionSection.myCheckbox}}" stepKey="clickMyCheckbox"/> - - <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[{{category.name}}]" stepKey="searchAndSelectCategory"/> - <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> - <fillField userInput="{{simpleProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> - <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="assertSaveMessageSuccess"/> - <seeInField userInput="{{simpleProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="assertFieldName"/> - <seeInField userInput="{{simpleProduct.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="assertFieldSku"/> - <seeInField userInput="{{simpleProduct.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="assertFieldPrice"/> - <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSectionAssert"/> - <seeInField userInput="{{simpleProduct.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="assertFieldUrlKey"/> -</actionGroup> -``` - -<!-- {% endraw %} --> \ No newline at end of file diff --git a/docs/merge_points/merge-data.md b/docs/merge_points/merge-data.md deleted file mode 100644 index b3342cc46..000000000 --- a/docs/merge_points/merge-data.md +++ /dev/null @@ -1,56 +0,0 @@ -# Merge data - -Data objects can be merged to cover the needs of your extension. - -In this example we update the `quantity` to `1001` and add a new piece of data relevant to our extension. This will affect all other tests that use this data. - -## Starting entity - -```xml -<entity name="SimpleProduct" type="product"> - <data key="sku" unique="suffix">SimpleProduct</data> - <data key="type_id">simple</data> - <data key="attribute_set_id">4</data> - <data key="name" unique="suffix">SimpleProduct</data> - <data key="price">123.00</data> - <data key="visibility">4</data> - <data key="status">1</data> - <data key="quantity">1000</data> - <data key="urlKey" unique="suffix">simpleproduct</data> - <data key="weight">1</data> - <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> - <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> -</entity> -``` - -## File to merge - -```xml -<entity name="SimpleProduct" type="product"> - <!-- myExtensionData will simply be added to the product and quantity will be changed to 1001. --> - <data key="quantity">1001</data> - <data key="myExtensionData">dataHere</data> -</entity> -``` - -## Resultant entity - -```xml -<entity name="SimpleProduct" type="product"> - <data key="sku" unique="suffix">SimpleProduct</data> - <data key="type_id">simple</data> - <data key="attribute_set_id">4</data> - <data key="name" unique="suffix">SimpleProduct</data> - <data key="price">123.00</data> - <data key="visibility">4</data> - <data key="status">1</data> - <!-- Quantity updated --> - <data key="quantity">1001</data> - <data key="urlKey" unique="suffix">simpleproduct</data> - <data key="weight">1</data> - <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> - <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> - <!-- Data key merged --> - <data key="myExtensionData">dataHere</data> -</entity> -``` \ No newline at end of file diff --git a/docs/merge_points/merge-pages.md b/docs/merge_points/merge-pages.md deleted file mode 100644 index c7a3e8fa8..000000000 --- a/docs/merge_points/merge-pages.md +++ /dev/null @@ -1,50 +0,0 @@ -# Merge pages - -Sections can be merged into pages to cover your extension. - -In this example we add a section that may be relevant to our extension to the list of sections underneath one page. - -## Starting page - -```xml -<page name="AdminCategoryPage" url="catalog/category/" area="admin" module="Magento_Catalog"> - <section name="AdminCategorySidebarActionSection"/> - <section name="AdminCategoryMainActionsSection"/> - <section name="AdminCategorySidebarTreeSection"/> - <section name="AdminCategoryBasicFieldSection"/> - <section name="AdminCategorySEOSection"/> - <section name="AdminCategoryProductsSection"/> - <section name="AdminCategoryProductsGridSection"/> - <section name="AdminCategoryModalSection"/> - <section name="AdminCategoryMessagesSection"/> - <section name="AdminCategoryContentSection"/> -</page> -``` - -## File to merge - -```xml -<page name="AdminCategoryPage" url="catalog/category/" area="admin" module="Magento_Catalog"> - <!-- myExtensionSection will simply be added to the page --> - <section name="MyExtensionSection"/> -</page> -``` - -## Resultant page - -```xml -<page name="AdminCategoryPage"> - <section name="AdminCategorySidebarActionSection"/> - <section name="AdminCategoryMainActionsSection"/> - <section name="AdminCategorySidebarTreeSection"/> - <section name="AdminCategoryBasicFieldSection"/> - <section name="AdminCategorySEOSection"/> - <section name="AdminCategoryProductsSection"/> - <section name="AdminCategoryProductsGridSection"/> - <section name="AdminCategoryModalSection"/> - <section name="AdminCategoryMessagesSection"/> - <section name="AdminCategoryContentSection"/> - <!-- New section merged --> - <section name="MyExtensionSection"/> -</page> -``` \ No newline at end of file diff --git a/docs/merge_points/merge-sections.md b/docs/merge_points/merge-sections.md deleted file mode 100644 index dbd6e3828..000000000 --- a/docs/merge_points/merge-sections.md +++ /dev/null @@ -1,47 +0,0 @@ -# Merge sections - -Sections can be merged together to cover your extension. - -In this example we add another selector to the section on the products page section. - -## Starting section - -<!-- {% raw %} --> - -```xml -<section name="ProductsPageSection"> - <element name="addProductButton" type="button" selector="//button[@id='add_new_product-button']"/> - <element name="checkboxForProduct" type="button" selector="//*[contains(text(),'{{args}}')]/parent::td/preceding-sibling::td/label[@class='data-grid-checkbox-cell-inner']" parameterized="true"/> - <element name="actions" type="button" selector="//div[@class='col-xs-2']/div[@class='action-select-wrap']/button[@class='action-select']"/> - <element name="delete" type="button" selector="//*[contains(@class,'admin__data-grid-header-row row row-gutter')]//*[text()='Delete']"/> - <element name="ok" type="button" selector="//button[@data-role='action']//span[text()='OK']"/> - <element name="deletedSuccessMessage" type="button" selector="//*[@class='message message-success success']"/> -</section> -``` - -## File to merge - -```xml -<section name="ProductsPageSection"> - <!-- myExtensionElement will simply be added to the page --> - <element name="myExtensionElement" type="button" selector="input.myExtension"/> -</section> -``` - -## Resultant section - -```xml -<section name="ProductsPageSection"> - <element name="addProductButton" type="button" selector="//button[@id='add_new_product-button']"/> - <element name="checkboxForProduct" type="button" selector="//*[contains(text(),'{{args}}')]/parent::td/preceding-sibling::td/label[@class='data-grid-checkbox-cell-inner']" parameterized="true"/> - <element name="actions" type="button" selector="//div[@class='col-xs-2']/div[@class='action-select-wrap']/button[@class='action-select']"/> - <element name="delete" type="button" selector="//*[contains(@class,'admin__data-grid-header-row row row-gutter')]//*[text()='Delete']"/> - <element name="ok" type="button" selector="//button[@data-role='action']//span[text()='OK']"/> - <element name="deletedSuccessMessage" type="button" selector="//*[@class='message message-success success']"/> - <!-- New element merged --> - <element name="myExtensionElement" type="button" selector="input.myExtension"/> -</section> -</page> -``` - -<!-- {% endraw %} --> \ No newline at end of file diff --git a/docs/merge_points/merge-tests.md b/docs/merge_points/merge-tests.md deleted file mode 100644 index 3958410e9..000000000 --- a/docs/merge_points/merge-tests.md +++ /dev/null @@ -1,102 +0,0 @@ -# Merge tests - -Tests can be merged to create a new test that covers new extension capabilities. - -In this example we add an action group that modifies the original test to interact with our extension sending in data we created. - -## Starting test - -```xml -<test name="AdminCreateSimpleProductTest"> - <annotations> - <features value="Catalog"/> - <stories value="Create a Simple Product via Admin"/> - <title value="Admin should be able to create a Simple Product"/> - <description value="Admin should be able to create a Simple Product"/> - <severity value="CRITICAL"/> - <testCaseId value="MAGETWO-23414"/> - <group value="product"/> - </annotations> - <before> - <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> - </before> - <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> - <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> - </after> - - <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> - <actionGroup ref="FillAdminSimpleProductForm" stepKey="fillProductFieldsInAdmin"> - <argument name="category" value="$$createPreReqCategory$$"/> - <argument name="simpleProduct" value="_defaultProduct"/> - </actionGroup> - <actionGroup ref="AssertProductInStorefrontCategoryPage" stepKey="assertProductInStorefront1"> - <argument name="category" value="$$createPreReqCategory$$"/> - <argument name="product" value="_defaultProduct"/> - </actionGroup> - <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="assertProductInStorefront2"> - <argument name="product" value="_defaultProduct"/> - </actionGroup> -</test> -``` - -## File to merge - -```xml -<test name="AdminCreateSimpleProductTest"> - <!-- This will be added after the step "fillProductFieldsInAdmin" in the above test. --> - <actionGroup ref="AddMyExtensionData" stepKey="extensionField" after="fillProductFieldsInAdmin"> - <argument name="extensionData" value="_myData"/> - </actionGroup> - - <!-- This will be added after the step "assertProductInStorefront2" in the above test. --> - <actionGroup ref="AssertMyExtensionDataExists" stepKey="assertExtensionInformation" after="assertProductInStorefront2"> - <argument name="extensionData" value="_myData"/> - </actionGroup> -</test> -``` - -## Resultant test - -```xml -<test name="AdminCreateSimpleProductTest"> - <annotations> - <features value="Catalog"/> - <stories value="Create a Simple Product via Admin"/> - <title value="Admin should be able to create a Simple Product"/> - <description value="Admin should be able to create a Simple Product"/> - <severity value="CRITICAL"/> - <testCaseId value="MAGETWO-23414"/> - <group value="product"/> - </annotations> - <before> - <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> - </before> - <after> - <amOnPage url="admin/admin/auth/logout/" stepKey="amOnLogoutPage"/> - <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> - </after> - - <actionGroup ref="LoginAsAdmin" stepKey="loginAsAdmin1"/> - <actionGroup ref="FillAdminSimpleProductForm" stepKey="fillProductFieldsInAdmin"> - <argument name="category" value="$$createPreReqCategory$$"/> - <argument name="simpleProduct" value="_defaultProduct"/> - </actionGroup> - <!-- First merged action group --> - <actionGroup ref="AddMyExtensionData" stepKey="extensionField"> - <argument name="extensionData" value="_myData"/> - </actionGroup> - - <actionGroup ref="AssertProductInStorefrontCategoryPage" stepKey="assertProductInStorefront1"> - <argument name="category" value="$$createPreReqCategory$$"/> - <argument name="product" value="_defaultProduct"/> - </actionGroup> - <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="assertProductInStorefront2"> - <argument name="product" value="_defaultProduct"/> - </actionGroup> - <!-- Second merged action group --> - <actionGroup ref="AssertMyExtensionDataExists" stepKey="assertExtensionInformation"> - <argument name="extensionData" value="_myData"/> - </actionGroup> -</test> -``` \ No newline at end of file diff --git a/docs/merging.md b/docs/merging.md deleted file mode 100644 index e4b91fb5f..000000000 --- a/docs/merging.md +++ /dev/null @@ -1,572 +0,0 @@ -# Merging - -The MFTF allows you to merge test components defined in XML files, such as: - -- [`<tests>`][] -- [`<pages>`][] -- [`<sections>`][] -- [`<data>`][] -- [`<action groups>`][] - -You can create, delete, or update the component. -It is useful for supporting rapid test creation for extensions and customizations. - -You can specify needed changes to an existing file and merge them to produce a modification of the original that incorporates the specified changes (the "delta"). - -Merging operates at the XML tag level, triggered by our parser when there are two (or more) entities with the same name. -Your update (XML node with changes) must have the same attribute `name` as its base node (the target object to be changed). - -For example: - -- All tests with `<test name="SampleTest>` will be merged into one. -- All pages with `<page name="SamplePage>` will be merged into one. -- All sections with `<section name="SampleAction">` will be merged into one. -- All data entities with `<entity name="sampleData" type="sample">` will be merged into one. -- All action groups with `<actionGroup name="selectNotLoggedInCustomerGroup">` will be merged into one. - -Although a file name does not influence merging, we recommend using the same file names in merging updates. -This makes it easier to search later on. - -## Add a test - -You cannot add another [`<test>`][tests] using merging functionality. -To add a `<test>`, create a new `*Test.xml` file or add a `<test>` node to an existing `*Test.xml` file. - -## Remove a test - -Tests cannot be removed while merging. -If a [`<test>`][tests] must be skipped due to a module completely invalidating a function, you can add the test to the `skip` group. - -Learn more about running tests with different options using [`mftf`] or [`codecept`] commands. - -### Example - -Skip the `AdminLoginTest` test in the `.../Backend/Test/AdminLoginTest.xml` file while merging with the `.../Foo/Test/AdminLoginTest.xml` file: - -<!-- {% raw %} --> - -```xml -<tests ...> - <test name="AdminLoginTest"> - <annotations> - <features value="Admin Login"/> - <stories value="Login on the Admin Login page"/> - <title value="You should be able to log into the Magento Admin backend."/> - <description value="You should be able to log into the Magento Admin backend."/> - <severity value="CRITICAL"/> - <testCaseId value="MAGETWO-71572"/> - <group value="example"/> - <group value="login"/> - </annotations> - <amOnPage url="{{AdminLoginPage.url}}" stepKey="amOnAdminLoginPage"/> - <fillField selector="{{AdminLoginFormSection.username}}" userInput="{{_ENV.MAGENTO_ADMIN_USERNAME}}" stepKey="fillUsername"/> - <fillField selector="{{AdminLoginFormSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="fillPassword"/> - <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickOnSignIn"/> - <closeAdminNotification stepKey="closeAdminNotification"/> - <seeInCurrentUrl url="{{AdminLoginPage.url}}" stepKey="seeAdminLoginUrl"/> - </test> -</tests> -``` - -Create the `.../Foo/Test/AdminLoginTest.xml` file: - -```xml -<tests ...> - <test name="AdminLoginTest"> - <annotations> - <skip> - <issueId value="Issue#"/> - </skip> - </annotations> - </test> -</tests> -``` - -The `AdminLoginTest` result corresponds to: - -```xml -<test name="AdminLoginTest"> - <annotations> - <features value="Admin Login"/> - <stories value="Login on the Admin Login page"/> - <title value="You should be able to log into the Magento Admin backend."/> - <description value="You should be able to log into the Magento Admin backend."/> - <severity value="CRITICAL"/> - <testCaseId value="MAGETWO-71572"/> - <group value="example"/> - <group value="login"/> - <skip> - <issueId value="Issue#"/> - </skip> - </annotations> - <amOnPage url="{{AdminLoginPage.url}}" stepKey="amOnAdminLoginPage"/> - <fillField selector="{{AdminLoginFormSection.username}}" userInput="{{_ENV.MAGENTO_ADMIN_USERNAME}}" stepKey="fillUsername"/> - <fillField selector="{{AdminLoginFormSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="fillPassword"/> - <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickOnSignIn"/> - <closeAdminNotification stepKey="closeAdminNotification"/> - <seeInCurrentUrl url="{{AdminLoginPage.url}}" stepKey="seeAdminLoginUrl"/> -</test> -``` - -## Update a test - -Any change must include information about where it should be inserted relative to other test steps in the scope of the test. - -This is done using the `before` or `after` element. -See the previous examples. - -### Add a test step - -**Use case**: Add `checkOption` before `click` (`stepKey="clickLogin"`) and add `seeInCurrentUrl` after the `click` in the `LogInAsAdminTest` test (in the `.../Backend/Test/LogInAsAdminTest.xml` file) while merging with the `.../Foo/Test/LogInAsAdminTest.xml` file: - -```xml -<tests ...> - <test name="LogInAsAdminTest"> - <amOnPage url="{{AdminLoginPage}}" stepKey="navigateToAdmin"/> - <fillField selector="{{AdminLoginFormSection.username}}" userInput="admin" stepKey="fillUsername"/> - <fillField selector="{{AdminLoginFormSection.password}}" userInput="password" stepKey="fillPassword"/> - <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> - <see userInput="Lifetime Sales" stepKey="seeLifetimeSales"/> - </test> -</tests> -``` - -Create the `.../Foo/Test/LogInAsAdminTest.xml` file: - -```xml -<tests ...> - <test name="LogInAsAdminTest"> - <checkOption selector="{{AdminLoginFormSection.rememberMe}}" stepKey="checkRememberMe" before="clickLogin"/> - <seeInCurrentUrl url="admin/admin/dashboard/" stepKey="seeAdminUrl" after="clickLogin"/> - </test> -</tests> -``` - -The `LogInAsAdminTest` result corresponds to: - -```xml -<test name="LogInAsAdminTest"> - <amOnPage url="{{AdminLoginPage}}" stepKey="navigateToAdmin"/> - <fillField selector="{{AdminLoginFormSection.username}}" userInput="admin" stepKey="fillUsername"/> - <fillField selector="{{AdminLoginFormSection.password}}" userInput="password" stepKey="fillPassword"/> - <checkOption selector="{{AdminLoginFormSection.rememberMe}}" stepKey="checkRememberMe"/> - <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> - <seeInCurrentUrl url="admin/admin/dashboard/" stepKey="seeAdminUrl"/> - <see userInput="Lifetime Sales" stepKey="seeLifetimeSales"/> -</test> -``` - -### Remove a test step - -**Use case**: Remove `see` (`stepKey="seeLifetimeSales"`) from the `LogInAsAdminTest` test (in the `.../Backend/Test/LogInAsAdminTest.xml` file) while merging with the `.../Foo/Test/LogInAsAdminTest.xml` file: - -```xml -<tests ...> - <test name="LogInAsAdminTest"> - <amOnPage url="{{AdminLoginPage}}" stepKey="navigateToAdmin"/> - <fillField selector="{{AdminLoginFormSection.username}}" userInput="admin" stepKey="fillUsername"/> - <fillField selector="{{AdminLoginFormSection.password}}" userInput="password" stepKey="fillPassword"/> - <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> - <see userInput="Lifetime Sales" stepKey="seeLifetimeSales"/> - </test> -</tests> -``` - -Create the `.../Foo/Test/LogInAsAdminTest.xml` file: - -```xml -<tests ...> - <test name="LogInAsAdminTest"> - <remove keyForRemoval="seeLifetimeSales"/> - </test> -</tests> -``` - -The `LogInAsAdminTest` result corresponds to: - -```xml -<test name="LogInAsAdminTest"> - <amOnPage url="{{AdminLoginPage}}" stepKey="navigateToAdmin"/> - <fillField selector="{{AdminLoginFormSection.username}}" userInput="admin" stepKey="fillUsername"/> - <fillField selector="{{AdminLoginFormSection.password}}" userInput="password" stepKey="fillPassword"/> - <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> -</test> -``` - -### Update a test step - -**Use case**: Change selector in `fillField` (`stepKey="fillPassword"`) of the `LogInAsAdminTest` test (in the `.../Backend/Test/LogInAsAdminTest.xml` file) while merging with the `.../Foo/Test/LogInAsAdminTest.xml` file: - -```xml -<tests ...> - <test name="LogInAsAdminTest"> - <amOnPage url="{{AdminLoginPage}}" stepKey="navigateToAdmin"/> - <fillField selector="{{AdminLoginFormSection.username}}" userInput="admin" stepKey="fillUsername"/> - <fillField selector="{{AdminLoginFormSection.password}}" userInput="password" stepKey="fillPassword"/> - <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> - <see userInput="Lifetime Sales" stepKey="seeLifetimeSales"/> - </test> -</tests> -``` - -Create the `.../Foo/Test/LogInAsAdminTest.xml` file: - -```xml -<tests ...> - <test name="LogInAsAdminTest"> - <fillField selector="{{AdminLoginFormSection.wrong-password}}" userInput="password" stepKey="fillPassword"/> - </test> -</tests> -``` - -The `LogInAsAdminTest` result corresponds to: - -```xml -<test name="LogInAsAdminTest"> - <amOnPage url="{{AdminLoginPage}}" stepKey="navigateToAdmin"/> - <fillField selector="{{AdminLoginFormSection.username}}" userInput="admin" stepKey="fillUsername"/> - <fillField selector="{{AdminLoginFormSection.wrong-password}}" userInput="password" stepKey="fillPassword"/> - <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> - <see userInput="Lifetime Sales" stepKey="seeLifetimeSales"/> -</test> -``` - -### Add several test steps {#insert-after} - -**Use case**: Add several actions after the `click` (`stepKey="clickLogin"`) in the `LogInAsAdminTest` test (in the `.../Backend/Test/LogInAsAdminTest.xml` file) while merging with the `.../Foo/Test/LogInAsAdminTest.xml` file: - -```xml -<tests ...> - <test name="LogInAsAdminTest"> - <amOnPage url="{{AdminLoginPage}}" stepKey="navigateToAdmin"/> - <fillField selector="{{AdminLoginFormSection.username}}" userInput="admin" stepKey="fillUsername"/> - <fillField selector="{{AdminLoginFormSection.password}}" userInput="password" stepKey="fillPassword"/> - <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> - <see userInput="Lifetime Sales" stepKey="seeLifetimeSales"/> - </test> -</tests> -``` - -Create the `.../Foo/Test/LogInAsAdminTest.xml` file: - -```xml -<tests ...> - <test name="LogInAsAdminTest" insertAfter="clickLogin"> - <checkOption selector="{{AdminLoginFormSection.rememberMe}}" stepKey="checkRememberMe"/> - <seeInCurrentUrl url="admin/admin/dashboard/" stepKey="seeAdminUrl"/> - </test> -</tests> -``` - -The `LogInAsAdminTest` result corresponds to: - -```xml -<test name="LogInAsAdminTest"> - <amOnPage url="{{AdminLoginPage}}" stepKey="navigateToAdmin"/> - <fillField selector="{{AdminLoginFormSection.username}}" userInput="admin" stepKey="fillUsername"/> - <fillField selector="{{AdminLoginFormSection.password}}" userInput="password" stepKey="fillPassword"/> - <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> - <checkOption selector="{{AdminLoginFormSection.rememberMe}}" stepKey="checkRememberMe"/> - <seeInCurrentUrl url="admin/admin/dashboard/" stepKey="seeAdminUrl"/> - <see userInput="Lifetime Sales" stepKey="seeLifetimeSales"/> -</test> -``` - -## Merge action groups - -Merging action groups allows you to extend existing tests by reusing existing action groups, while customizing them for your specific needs. - -### Use case - -Here is an action group for selecting `customerGroup` in the `Cart Price Rules` section. -The controls change drastically in the B2B version, so it was abstracted to an action group so that it may be easily changed if B2B is enabled. - -> Action group for selecting `customerGroup` in the `Cart Price Rules` section: - -```xml -<actionGroup name="selectNotLoggedInCustomerGroup"> - <selectOption selector="{{AdminCartPriceRulesFormSection.customerGroups}}" userInput="NOT LOGGED IN" stepKey="selectCustomerGroup"/> -</actionGroup> -``` - -> B2B Merge file - -```xml -<!-- name matches --> -<actionGroup name="selectNotLoggedInCustomerGroup"> - <!-- removes the original action --> - <remove keyForRemoval="selectCustomerGroup"/> - <!-- adds in sequence of actions to be performed instead--> - <click selector="{{AdminCartPriceRulesFormSection.customerGroups}}" stepKey="expandCustomerGroups"/> - <fillField selector="{{AdminCartPriceRulesFormSection.customerGroupsInput}}" userInput="NOT LOGGED IN" stepKey="fillCustomerGroups"/> - <click selector="{{AdminCartPriceRulesFormSection.customerGroupsFirstResult}}" stepKey="selectNotLoggedInGroup"/> - <click selector="{{AdminCartPriceRulesFormSection.customerGroupsDoneBtn}}" stepKey="closeMultiSelect"/> -</actionGroup> -``` - -## Merge pages - -Use [page] merging to add or remove [sections] in your module. - -To merge [pages][page], the `page name` must be the same as in the base module. -`page name` is set in the `module` attribute. - -### Add a section - -**Use case**: The `FooBackend` module extends the `Backend` module and requires use of two new sections that must be covered with tests. -Add `BaseBackendSection` and `AnotherBackendSection` to the `BaseBackendPage` (`.../Backend/Page/BaseBackendPage.xml` file): - -```xml -<pages ...> - <page name="BaseBackendPage" url="admin" area="admin" module="Magento_Backend"> - <section name="BaseBackendSection"/> - <section name="AnotherBackendSection"/> - </page> -</pages> -``` - -Create the `.../FooBackend/Page/BaseBackendPage.xml` file: - -```xml -<pages ...> - <page name="BaseBackendPage" url="admin" area="admin" module="Magento_Backend"> - <section name="NewExtensionSection"/> - </page> -</pages> -``` - -The `BaseBackendPage` result corresponds to: - -```xml -<page name="BaseBackendPage" url="admin" area="admin" module="Magento_Backend"> - - <section name="BaseBackendSection"/> - <section name="AnotherBackendSection"/> - <section name="NewExtensionSection"/> -</page> -``` - -### Remove a section - -**Use case**: The `FooBackend` module extends the `Backend` module and requires deletion of the `AnotherBackendSection` section (the `.../Backend/Page/BaseBackendPage.xml` file): - -```xml -<page name="BaseBackendPage" url="admin" area="admin" module="Magento_Backend"> - <section name="BaseBackendSection"/> - <section name="AnotherBackendSection"/> -</page> -``` - -Create the `.../FooBackend/Page/BaseBackendPage.xml` file: - -```xml -<page name="BaseBackendPage" url="admin" area="admin" module="Magento_Backend"> - <section name="AnotherBackendSection" remove="true"/> -</page> -``` - -The `BaseBackendPage` result corresponds to: - -```xml -<page name="BaseBackendPage" url="admin" area="admin" module="Magento_Backend"> - <section name="BaseBackendSection"/> -</page> -``` - -## Merge sections - -Use merging to add, remove, or update [elements] in sections. - -All sections with the same _file name_, _section name_, and _element name_ are merged during test generation. - -### Add an element - -**Use case**: The `FooBackend` module extends the `Backend` module and requires a new `mergeElement` element in the `AdminLoginFormSection`. -Add `mergeElement` to the `AdminLoginFormSection`: - -```xml -<sections ...> - <section name="AdminLoginFormSection"> - <element name="username" type="input" selector="#username"/> - <element name="password" type="input" selector="#login"/> - <element name="signIn" type="button" selector=".actions .action-primary" timeout="30"/> - </section> -</sections> -``` - -Create the `.../FooBackend/Section/AdminLoginFormSection.xml` file: - -```xml -<sections ...> - <section name="AdminLoginFormSection"> - <element name="mergeElement" type="input" selector="#selector"/> - </section> -</sections> -``` - -The `AdminLoginFormSection` result corresponds to: - -```xml -<section name="AdminLoginFormSection"> - <element name="username" type="input" selector="#username"/> - <element name="password" type="input" selector="#login"/> - <element name="signIn" type="button" selector=".actions .action-primary" timeout="30"/> - <element name="mergeElement" type="input" selector="#selector"/> -</section> -``` - -### Remove an element - -**Use case**: The `FooBackend` module extends the `Backend` module and requires deletion of `username` in the `AdminLoginFormSection`. -Remove `username` from the `AdminLoginFormSection`: - -```xml -<sections ...> - <section name="AdminLoginFormSection"> - <element name="username" type="input" selector="#username"/> - <element name="password" type="input" selector="#login"/> - <element name="signIn" type="button" selector=".actions .action-primary" timeout="30"/> - </section> -</sections> -``` - -Create the `.../FooBackend/Section/AdminLoginFormSection.xml` file: - -```xml -<sections ...> - <section name="AdminLoginFormSection"> - <element name="username" type="input" remove="true"/> - </section> -</sections> -``` - -The `AdminLoginFormSection` result corresponds to: - -```xml -<section name="AdminLoginFormSection"> - <element name="password" type="input" selector="#login"/> - <element name="signIn" type="button" selector=".actions .action-primary" timeout="30"/> -</section> -``` - -### Update an element - -**Use case**: The `FooBackend` module extends the `Backend` module and requires change of a selector in `username` in the `AdminLoginFormSection`. -Update `username` in the `AdminLoginFormSection` (the `.../Backend/Section/AdminLoginFormSection.xml` file): - -```xml -<sections ...> - <section name="AdminLoginFormSection"> - <element name="username" type="input" selector="#username"/> - <element name="password" type="input" selector="#login"/> - <element name="signIn" type="button" selector=".actions .action-primary" timeout="30"/> - </section> -</sections> -``` - -Create the `.../FooBackend/Section/AdminLoginFormSection.xml` file: - -```xml -<sections ...> - <section name="AdminLoginFormSection"> - <element name="username" type="input" selector="#newSelector"/> - </section> -</sections> -``` - -The `AdminLoginFormSection` result corresponds to: - -```xml -<section name="AdminLoginFormSection"> - <element name="username" type="input" selector="#newSelector"/> - <element name="password" type="input" selector="#login"/> - <element name="signIn" type="button" selector=".actions .action-primary" timeout="30"/> -</section> -``` - -## Merge data - -You can add or update `<data>` elements within an `<entity>`. -Removal of individual `<data>` tags is not supported. - -Entities and data with the same _file name_, _entity name and type_, and _data key_ are merged during test generation. - -### Add data - -**Use case**: The `FooSample` module extends the `Sample` module and requires a new data item `thirdField` in the `_defaultSample` entity. -Add `<data key="thirdField">field3</data>` to the `_defaultSample` (the `.../Sample/Data/SampleData.xml` file): - -```xml -<entities ...> - <entity name="_defaultSample" type="testData"> - <data key="firstField">field1</data> - <data key="secondField">field2</data> - </entity> -</entities> -``` - -Create the `.../FooSample/Data/SampleData.xml` file: - -```xml -<entities ...> - <entity name="sampleData" type="testData"> - <data key="thirdField">field3</data> - </entity> -</entities> -``` - -The `_defaultSample` result corresponds to: - -```xml -<entity name="_defaultSample" type="testData"> - <data key="firstField">field1</data> - <data key="secondField">field2</data> - <data key="thirdField">field3</data> -</entity> -``` - -### Update data - -**Use case**: The `FooSample` module extends the `Sample` module and requires changing the `firstField` data item in the `_defaultSample` entity. -Change `firstField` to `<data key="firstField">overrideField</data>` in the `_defaultSample` (the `.../Sample/Data/SampleData.xml` file): - -```xml -<entities ...> - <entity name="_defaultSample" type="testData"> - <data key="firstField">field1</data> - <data key="secondField">field2</data> - </entity> -</entity> -``` - -Create the `.../FooSample/Data/SampleData.xml` file: - -```xml -<entities ...> - <entity name="_defaultSample" type="testData"> - <data key="firstField">overrideField</data> - </entity> -</entity> -``` - -The `_defaultSample` results corresponds to: - -```xml -<entity name="_defaultSample" type="testData"> - <data key="firstField">overrideField</data> - <data key="secondField">field2</data> -</entity> -``` - -<!-- {% endraw %} --> - -<!-- Link definitions --> - -[`codecept`]: ./commands/codeception.md -[`mftf`]: ./commands/mftf.md -[`<data>`]: ./data.md -[elements]: ./section.md#element-tag -[`<pages>`]: ./page.md -[`<sections>`]: ./section.md -[`<tests>`]: ./test.md -[`<action groups>`]: ./test/action-groups.md diff --git a/docs/metadata.md b/docs/metadata.md deleted file mode 100644 index 3f6032b57..000000000 --- a/docs/metadata.md +++ /dev/null @@ -1,587 +0,0 @@ -# Metadata - -In this topic we talk about handling entities that you need in your tests (such as categories, products, wish lists, and similar) using the MFTF. -Using data handling actions like [`createData`], [`deleteData`], [`updateData`], and [`getData`], you are able to create, delete, update, and read entities for your tests. -The framework enables you to send HTTP requests with these statically defined data entities: - -- [Sending a REST API request][rest request] -- [Handling a REST API response][rest response] -- [Sending an HTML form encoded in URL][html form] - -You have probably noticed that some modules in acceptance functional tests contain a directory, which is called `Metadata`. - -Example of a module with _Metadata_: - -```tree -Wishlist -├── Data -├── Metadata -├── Page -├── Section -└── Test -``` - -This directory contains XML files with metadata required to create a valid request to handle an entity defined in `dataType`. -A metadata file contains a list of operations with different types (defined in `type`). -Each [operation] includes: - -- The set of adjustments for processing a request in [attributes][operation], and in some cases, a response (see `successRegex`, `returnRegex` and `returnIndex` in [reference details][operation]). -- The type of body content encoding in [contentType]. -- The body of the request represented as a tree of objects, arrays, and fields. - -When a test step requires handling the specified data entity, the MFTF performs the following steps: - -- Reads input data (`<data/>`) and the type (the `type` attribute) of the specified [entity]. -- Searches the metadata operation for the `dataType` that matches the entity's `type`. For example, `<entity type="product">` matches `<operation dataType="product"`. -- Forms a request of the operation and the input data of the entity according to matching metadata. -- Stores a response and provides access to its data using MFTF variables syntax in XML. - -The following diagram demonstrates the XML structure of a metadata file: -![Structure of metadata](img/metadata-dia.svg) - -## Format {#format} - -```xml -<?xml version="1.0" encoding="UTF-8"?> -<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> - <operation name="" - dataType="" - type="" - auth="" - url="" - method=""> - <contentType></contentType> - <object key="" dataType=""> - <field key="">integer</field> - <field key="">string</field> - <field key="">boolean</field> - <array key=""> - <value>string</value> - </array> - </object> - </operation> -</operations> -``` - -## Principles {#principles} - -1. A `dataType` value must match the `type` value of the corresponding entity. -2. A file name should be PascalCase and end with `Meta.xml`. - Example: `ProductAttributeMeta.xml`. -3. A metadata file may contain different types of operations (`type`) with the same data entity (`dataType`). - -Example: - - ```xml - <operations> - <operation type="create" dataType="category"> - ... - </operation> - <operation type="update" dataType="category"> - ... - </operation> - <operation type="delete" dataType="category"> - ... - </operation> - <operation type="get" dataType="category"> - ... - </operation> - </operations> - ``` - -## Handling entities using REST API {#handling-with-api} - -### Sending a REST API request - -The MFTF allows you to handle basic CRUD operations with an object using [Magento REST API][api reference] requests. -To convert a request to the MFTF format, wrap the corresponding REST API request into XML tags according to the [Reference documentation][reference]. - -- GET is used for retrieving data from objects. -- POST is used for creating new objects. -- PUT is used for updating objects. -- DELETE is used for deleting objects. - -This is an example of how to handle a category using REST API operations provided by the `catalogCategoryRepositoryV1` service. - -![REST API operations provided by catalogCategoryRepositoryV1][catalogCategoryRepositoryV1 image] - -The above screenshot from the [Magento REST API Reference][api reference] demonstrates a list of available operations to: - -- Delete a category by its identifier (`method="DELETE"`) -- Get information about a category by its ID (`method="GET"`) -- [Create a new category] (`method="POST"`) -- Update category data by its ID (`method="PUT"`) - -We assume that our `.env` file sets `MAGENTO_BASE_URL=https://example.com/` and `MAGENTO_BACKEND_NAME=admin`. - -#### Create a simple category {#create-object-as-adminOauth} - -Let's see what happens when you create a category: - -```xml -<createData entity="_defaultCategory" stepKey="createPreReqCategory"/> -``` - -The MFTF searches in the _Data_ directory an entity with `<entity name="_defaultCategory">` and reads `type` of the entity. -If there are more than one entity with the same name, all of the entities are merged. - -_Catalog/Data/CategoryData.xml_: - -```xml -<entity name="_defaultCategory" type="category"> - <data key="name" unique="suffix">simpleCategory</data> - <data key="name_lwr" unique="suffix">simplecategory</data> - <data key="is_active">true</data> -</entity> -``` - -Here, `type` is equal to `"category"`, which instructs the MFTF to search an operation with `dataType="category"`. -Since the action is __to create__ a category, the MFTF will also search for operation with `type="create"` in _Metadata_ for `dataType="category"`. - -_Catalog/Metadata/CategoryMeta.xml_: - -```xml -<operation name="CreateCategory" dataType="category" type="create" auth="adminOauth" url="/V1/categories" method="POST"> - <contentType>application/json</contentType> - <object key="category" dataType="category"> - <field key="parent_id">integer</field> - <field key="name">string</field> - <field key="is_active">boolean</field> - <field key="position">integer</field> - <field key="level">integer</field> - <field key="children">string</field> - <field key="created_at">string</field> - <field key="updated_at">string</field> - <field key="path">string</field> - <field key="include_in_menu">boolean</field> - <array key="available_sort_by"> - <value>string</value> - </array> - <field key="extension_attributes">empty_extension_attribute</field> - <array key="custom_attributes"> - <value>custom_attribute</value> - </array> - </object> -</operation> -``` - -(The corresponding operation is provided by _catalogCategoryRepositoryV1_ in [REST API][api reference].) - -The following is encoded in `<operation>`: - -- `name="CreateCategory"` defines a descriptive name of the operation, which is used for merging if needed. -- `dataType="category"` defines a relation with data entities with input data for a Category (`<entity type="category">`). -- `auth="adminOauth"` defines OAuth authorization, which is required for the Admin area. -- `url="/V1/categories"` defines a routing URL to the corresponding service class. - (The request will be sent to `https://example.com/rest/V1/categories` if `MAGENTO_BASE_URL=https://example.com/` and `MAGENTO_BACKEND_NAME=admin` are set in the _acceptance/.env_ configuration file.) -- `method="POST"` defines a POST method of the HTTP request. - -`<contentType>application/json</contentType>` defines a content type of the REST API request, which is set as `application/json` here. - -The parameter that declares a body of the request is _catalogCategoryRepositoryV1SavePostBody_. -Using the [Reference], we can trace how the JSON request was converted into XML representation. - -<div class="bs-callout bs-callout-info"> -Comments in the example below are used to demonstrate relation between JSON request and MFTF metadata in XML. -JSON does not support comments. -</div> - -Model schema for _catalogCategoryRepositoryV1SavePostBody_ with XML representation of _Catalog/Metadata/CategoryMeta.xml_ in comments: - -```json -{ // XML representation in the MFTF metadata format (see 'Catalog/Metadata/CategoryMeta.xml') - "category": { // <object key="category" dataType="category"> - "id": 0, // Skipped, because Category ID is not available on UI when you create a new category. - "parent_id": 0, // <field key="parent_id">integer</field> - "name": "string", // <field key="name">string</field> - "is_active": true, // <field key="is_active">boolean</field> - "position": 0, // <field key="position">integer</field> - "level": 0, // <field key="level">integer</field> - "children": "string", // <field key="children">string</field> - "created_at": "string", // <field key="created_at">string</field> - "updated_at": "string", // <field key="updated_at">string</field> - "path": "string", // <field key="path">string</field> - "available_sort_by": [ // <array key="available_sort_by"> - "string" // <value>string</value> - ], // </array> - "include_in_menu": true, // <field key="include_in_menu">boolean</field> - "extension_attributes": {}, // <field key="extension_attributes">empty_extension_attribute</field>, where 'empty_extension_attribute' is a reference to operation with 'dataType="empty_extension_attribute"' (see 'Catalog/Metadata/EmptyExtensionAttributeMeta.xml') - "custom_attributes": [ // <array key="custom_attributes"> - { // <value>custom_attribute</value>, where 'custom_attribute' is a reference to operation with 'dataType="custom_attribute"' (see 'Catalog/Metadata/CustomAttributeMeta.xml') - "attribute_code": "string", - "value": "string" - } - ] // </array> - } // </object> -} -``` - -So, the body of a REST API request that creates a simple category is the following: - -```json -{ // XML representation of input data used to create a simple category ("Catalog/Data/CategoryData.xml") - "category": { // <entity name="_defaultCategory" type="category"> - "name": "simpleCategory_0986576456", // <data key="name" unique="suffix">simpleCategory</data> - "is_active": true // <data key="is_active">true</data> - } // </entity> -} -``` - -#### Create an object as a guest {#create-object-as-anonymous} - -The corresponding test step is: - -```xml -<createData entity="guestCart" stepKey="createGuestCart"/> -``` - -The MFTF searches in the _Data_ directory an entity with `<entity name="guestCart">` and reads `type`. - -_Quote/Data/GuestCartData.xml_: - -```xml -<entity name="guestCart" type="guestCart"> -</entity> -``` - -`type="guestCart"` points to the operation with `dataType=guestCart"` and `type="create"` in the _Metadata_ directory. - -_Catalog/Data/CategoryData.xml_: - -```xml -<operation name="CreateGuestCart" dataType="guestCart" type="create" auth="anonymous" url="/V1/guest-carts" method="POST"> - <contentType>application/json</contentType> -</operation> -``` - -As a result, the MFTF sends an unauthorized POST request with an empty body to the `https://example.com/rest/V1/guest-carts` and stores the single string response that the endpoint returns. - -### Handling a REST API response {#rest-response} - -There are cases when you need to reuse the data that Magento responded with to your POST request. - -Let's see how to handle data after you created a category with custom attributes: - -```xml -<createData entity="customizedCategory" stepKey="createPreReqCategory"/> -``` - -The MFTF receives the corresponding JSON response and enables you to reference its data using a variable of format: - -**$** _stepKey_ **.** _JsonKey_ **$** - -Example: - -```xml -$createPreReqCategory.id$ -``` - -And for a custom attribute: - -**$** _stepKey_ **.custom_attributes[** _attribute key_ **]$** - -Example: - -```xml -$createPreReqCategory.custom_attributes[is_anchor]$ -``` - -The following example of response in JSON demonstrates how to reference data on the root level and as data in custom attributes: - -```json -{ - "id": 7, //$createPreReqCategory.id$ - "parent_id": 2, //$createPreReqCategory.parent_id$ - "name": "simpleCategory_0986576456", //$createPreReqCategory.is_active$ - "is_active": true, - "position": 5, - "level": 2, - "children": "", - "created_at": "2018-05-08 14:27:18", - "updated_at": "2018-05-08 14:27:18", - "path": "1/2/7", - "available_sort_by": [], - "include_in_menu": true, - "custom_attributes": [ - { - "attribute_code": "is_anchor", - "value": "1" //$createPreReqCategory.custom_attributes[is_anchor]$ - }, - { - "attribute_code": "path", - "value": "1/2/7" //$createPreReqCategory.custom_attributes[path]$ - }, - { - "attribute_code": "children_count", - "value": "0" - }, - { - "attribute_code": "url_key", - "value": "simplecategory5af1b41cd58fb4" //$createPreReqCategory.custom_attributes[url_key]$ - }, - { - "attribute_code": "url_path", - "value": "simplecategory5af1b41cd58fb4" - } - ], -} -} -``` - -## Handling entities using HTML forms {#using-html-forms} - -For cases when REST API is not applicable, you may use [HTML forms] (when all object parameters are encoded in a URL as `key=name` attributes). -There are two different attributes to split access to different areas: - -- `auth="adminFormKey"` is used for objects in an Admin area. -- `auth="customerFormKey"` is used for objects in a storefront. - -You are able to create assurances with `successRegex`, and, optionally, return values with `returnRegex`. You can also use `returnIndex` when `returnRegex` matches multiple values. - -### Create an object in Admin {#create-object-as-adminFormKey} - -The `CreateStoreGroup` operation is used to persist a store group: - -Source file is _Store/Metadata/StoreGroupMeta.xml_: - -```xml -<operation name="CreateStoreGroup" dataType="group" type="create" auth="adminFormKey" url="/admin/system_store/save" method="POST" successRegex="/messages-message-success/" > - <contentType>application/x-www-form-urlencoded</contentType> - <object dataType="group" key="group"> - <field key="group_id">string</field> - <field key="name">string</field> - <field key="code">string</field> - <field key="root_category_id">integer</field> - <field key="website_id">integer</field> - </object> - <field key="store_action">string</field> - <field key="store_type">string</field> -</operation> -``` - -The operation is called when `<createData>` (`type="create"`) points to a data entity of type `"group"` (`dataType="group"`). -It sends a POST request (`method="POST"`) to `http://example.com/admin/system_store/save` (`url="/admin/system_store/save"`) that is authorized for the Admin area (`auth="adminFormKey"`). -The request contains HTML form data encoded in the [application/x-www-form-urlencoded] content type (`<contentType>application/x-www-form-urlencoded</contentType>`). -If the returned HTML code contains the `messages-message-success` string, it is resolved as successful. - -The operation enables you to assign the following form fields: - -- `group/group_id` -- `group/name` -- `group/code` -- `group/root_category_id` -- `group/website_id` -- `store_action` -- `store_type` - -### Create an object in storefront {#create-object-as-customerFormKey} - -The MFTF uses the `CreateWishlist` operation to create a wish list on storefront: - -Source file is _Wishlist/Metadata/WishlistMeta.xml_ - -```xml -<operation name="CreateWishlist" dataType="wishlist" type="create" auth="customerFormKey" url="/wishlist/index/add/" method="POST" successRegex="" returnRegex="~\/wishlist_id\/(\d*?)\/~" > - <contentType>application/x-www-form-urlencoded</contentType> - <field key="product">integer</field> - <field key="customer_email">string</field> - <field key="customer_password">string</field> -</operation> -``` - -The operation is used when `<createData>` (`type="create"`) points to a data entity of type `"wishlist"` (`dataType="wishlist"`). -It sends a POST request (`method="POST"`) to `http://example.com/wishlist/index/add/` (`url="wishlist/index/add/"`) as a customer (`auth="customerFormKey"`). -The request contains HTML form data encoded in the [application/x-www-form-urlencoded] content type (`<contentType>application/x-www-form-urlencoded</contentType>`). -If the returned HTML code contains a string that matches the regular expression `~\/wishlist_id\/(\d*?)\/~`, it returns the matching value. - -The operation assigns three form fields: - -- `product` -- `customer_email` -- `customer_password` - -## Reference - -### operations {#operations-tag} - -Root element that points to the corresponding XML Schema. - -### operation {#operation-tag} - -| Attribute | Type | Use | Description | -|-----------------|------------------------------------------------------------------------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------| -| `name` | string | optional | Name of the operation. | -| `dataType` | string | required | Data type of the operation. It refers to a `type` attribute of data entity that will be used as a source of input data. | -| `type` | Possible values: `create`, `delete`, `update`, `get`. | required | Type of operation. | -| \*`url` | string | optional | A routing URL of the operation. Example value: `"/V1/categories"`. | -| \*\*`auth` | Possible values: `adminOath`, `adminFormKey`, `customerFormKey`, `anonymous` | optional | Type of authorization of the operation. | -| `method` | string | optional | HTTP method of the operation. Possible values: `POST`, `DELETE`, `PUT`, `GET`. | -| `successRegex` | string | optional | Determines if the operation was successful. Parses the HTML body in response and asserts if the value assigned to the `successRegex` exists. | -| `returnRegex` | string | optional | Determines if the response contains the matching value to return. | -| `returnIndex` | string | optional | Specifies index at which the value will be returned when `returnRegex` matches multiple values | -| `removeBackend` | boolean | optional | Removes backend name from requested URL. Applicable when `auth="adminFormKey"`. | -| `filename` | string | optional | | -|`deprecated`|string|optional|Used to warn about the future deprecation of the test. String will appear in Allure reports and console output at runtime.| - -- \*`url` - full URL is a concatenation of _ENV.baseUrl_ + `/rest/` + _url_. - To reuse data of a required entity or returned response use a field key wrapped in curly braces such as `{sku}`. - When the data to reuse is of a different type, declare also the type of data such as `{product.sku}`. - Example: `"/V1/products/{product.sku}/media/{id}"`. - -- \*\*`auth` - available values: - - - `adminOath` is used for REST API persistence in the Admin area with [OAuth-based authentication][oauth]. - - `adminFormKey` is used for HTML form persistence in the Admin area. - - `customerFormKey` is used for HTML form persistence in the Customer area. - - `anonymous` is used for REST API persistence without authorization. - -### contentType {#contentType-tag} - -Sets one of the following operation types: - -- `application/json` is used for REST API operations. -- `application/x-www-form-urlencoded` is used for HTML form operations. - -### object {#object-tag} - -Representation of a complex entity that may contain fields, arrays, and objects. -An object must match the [entity] of the same `type`. - -| Attribute | Type | Use | Description | -| ---------- | ------- | -------- | ---------------------------------------------------------------------------------------------- | -| `key` | string | optional | Name of the object. | -| `dataType` | string | required | Type of the related [entity]. | -| `required` | boolean | optional | Determines if the object is required or not. It must match the Magento REST API specification. | - -### field {#field-tag} - -Representation of HTML form or REST API fields. - -| Attribute | Type | Use | Description | -| ---------- | ------- | -------- | --------------------------------------------------------------------------------------------- | -| `key` | string | required | Name of the field. It must match the field name of the related [entity]. | -| `type` | string | optional | Type of the value. It may contain a primitive type or the type of another operation. | -| `required` | boolean | optional | Determines if the field is required or not. It must match the Magento REST API specification. | - -### array {#array-tag} - -Representation of an array. - -| Attribute | Type | Use | Description | -| --------- | ------ | -------- | ------------------ | -| `key` | string | required | Name of the array. | - -It contains one or more `value` elements. - -### value {#value-tag} - -Declares a data type for items within `<array>`. - -#### Example of an array with value of a primitive data type - -Metadata declaration of the operation schema: - -```xml -<array key="tax_rate_ids"> - <value>integer</value> -</array> -``` - -The value can contain one or more items. - -Data entity with the corresponding assignment: - -```xml -<array key="tax_rate_ids"> - <item>1</item> - <item>2</item> -</array> -``` - -- Resulted JSON request: - -```json -"tax_rate_ids": - [ - "item": 1, - "item": 2 - ] -``` - -#### Example of an array containing data entities - -```xml -<array key="product_options"> - <value>product_option</value> -</array> -``` - -The value declares the `product_options` array that contains one or more entities of the `product_option` data type. - -#### Example of an array containing a particular data field - -```xml -<array key="tax_rate_ids"> - <value>tax_rate.id</value> -</array> -``` - -The value declares the `tax_rate_ids` array that contains one or more `id` fields of the `tax_rate` data type entity. - -### header {#header-tag} - -An additional parameter in REST API request. - -| Attribute | Type | Use | Description | -| --------- | ------ | -------- | ---------------------------- | -| `param` | string | required | A REST API header parameter. | - -```xml -<header param="status">available</header> -``` - -### param {#param-tag} - -An additional parameter in URL. - -| Attribute | Type | Use | Description | -| --------- | ------ | -------- | -------------------------- | -| `key` | string | required | Name of the URL parameter. | - -Example: - -```xml -<param key="status">someValue</param> -``` - -<!-- LINK DEFINITIONS --> - -[actions]: test/actions.md -[api reference]: https://devdocs.magento.com/guides/v2.3/get-started/bk-get-started-api.html -[application/x-www-form-urlencoded]: https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1 -{:target="_blank"} -[catalogCategoryRepositoryV1 image]: img/catalogCategoryRepository-operations.png -[catalogCategoryRepositoryV1SavePostBody]: #catalogCategoryRepositoryV1SavePostBody -[contentType]: #contentType-tag -[Create a new category]: #create-object-as-adminOauth -[createData]: test/actions.md#createdata -[delete a category by its ID]: #delete-object-as-adminOauth -[deleteData]: test/actions.md#deletedata -[entity]: data.md#entity-tag -[get information about a category by its ID]: #get-object-as-adminOauth -[getData]: test/actions.md#getdata -[HTML forms]: https://www.w3.org/TR/html401/interact/forms.html -{:target="\_blank"} -[oauth]: https://devdocs.magento.com/guides/v2.3/get-started/authentication/gs-authentication-oauth.html -{:target="\_blank"} -[operation]: #operation-tag -[reference]: #reference -[rest request]: #handling-with-api -[html form]: #using-html-forms -[update category data by its ID]: #update-object-as-adminOauth -[updateData]: test/actions.md#updatedata -[rest response]: #rest-response - -*[CRUD]: Create Read Update Delete -*[MFTF]: Magento Functional Testing Framework diff --git a/docs/mftf-tests-packaging.md b/docs/mftf-tests-packaging.md deleted file mode 100644 index 8a37b8013..000000000 --- a/docs/mftf-tests-packaging.md +++ /dev/null @@ -1,58 +0,0 @@ -<style> -.mftf-dl { - margin-bottom: 2.5em; -} -dl dt{ - font-weight:400; -} -</style> - -# MFTF functional test modules and packaging - -## MFTF predefined test module paths -The Magento Functional Testing Framework can run tests from predefined paths and custom paths. The predefined paths are: -``` -app/code/<Vendor>/<Module>/Test/Mftf -dev/tests/acceptance/tests/functional/<Vendor>/<TestModule> -vendor/<Vendor>/<Module>/Test/Mftf -vendor/<Vendor>/<TestModule> -``` - -To support future service isolation, Test module in `dev/tests/acceptance/tests/functional/<Vendor>/<TestModule>` and -`vendor/<Vendor>/<TestModule>` must define the module type as `magento2-functional-test-module` in its `composer.json` file. -No `composer.json` file is required for tests in `app/code/<Vendor>/<Module>/Test/Mftf` and `vendor/<Vendor>/<Module>/Test/Mftf` -as they are part of the Magento modules. - -Test module for a specific Magento module can only be in one of the paths. - -## Test module composer.json format - -Test module `composer.json` file should use type `magento2-functional-test-module`. - -Test module `composer.json` file should define Magento module dependencies in suggests block. -MFTF will recognize the dependency if the suggest message of a module specifies `type` using `magento2-module` and `name` -using module name registered with Magento. - -Here is an example `composer.json` file for the test module `dev/tests/acceptance/tests/functional/Magento/ConfigurableProductCatalogSearch`: - -```json -{ - "name": "magento/module-configurable-product-catalog-search-functional-test", - "description": "MFTF test module for Magento_ConfigurableProduct and Magento_CatalogSearch", - "type": "magento2-functional-test-module", - "config": { - "sort-packages": true - }, - "require": { - "magento/magento2-functional-testing-framework": ">=2.5" - }, - "suggest": { - "magento/module-configurable-product": "type: magento2-module, name: Magento_ConfigurableProduct, version: *", - "magento/module-catalog-search": "type: magento2-module, name: Magento_CatalogSearch, version: *" - }, - "license": [ - "OSL-3.0", - "AFL-3.0" - ] -} -``` \ No newline at end of file diff --git a/docs/page.md b/docs/page.md deleted file mode 100644 index 41afd274f..000000000 --- a/docs/page.md +++ /dev/null @@ -1,174 +0,0 @@ -# Page structure - -The MFTF uses a modified concept of [PageObjects], which models the testing areas of your page as objects within the code. -This reduces occurrences of duplicated code and enables you to fix things quickly, in one place, when things change. -You define the contents of a page, for reference in a [`<test>`], at both the [`<page>`] and [`<section>`] level. - -The `pageObject` lists the URL of the `page` and the `sections` that it contains. -You can reuse `sections` to define the elements on a page that are exercisable, while also ensuring a single source of truth to help maintain consistency. - -Avoid hard-coded location selectors from tests to increase the maintainability and readability of the tests, as well as improve the test execution output and logging. - -Two types of pages are available: - -<!-- {% raw %} --> - -- Page with `url` declared as a constant string or [explicit page] - In a test it is called in a format like `{{NameOfPage.url}}`, where `NameOfPage` is a value of `name` in the corresponding page declaration `*.xml` file. -- Page with `url` declared as a string with one or more variables or [parameterized page] -- In a test it is called using a format like `{{NameOfPage.url(var1, var2, ...)}}`, where `var1, var2` etc. are parameters that will be substituted in the `url` of the corresponding `<page>` declaration. - -The following diagram shows the XML structure of an MFTF page: - -![XML Structure of MFTF Page](img/page-dia.svg) - -## Format - -The format of `<page>` is: - -```xml -<?xml version="1.0" encoding="UTF-8"?> - -<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> - <page name="" url="" module="" area=""> - <section name=""/> - <section name=""/> - </page> -</pages> -``` - -## Principles - -The following conventions apply to MFTF pages: - -- `<page>` name is the same as the file name. -- `<page>` name must be alphanumeric. -- `*Page.xml` is stored in the _Page_ directory of a module. -- The name format is `{Admin|Storefront}{PageDescription}Page.xml`. -- One `<page>` tag is allowed per page XML file. - -The `.url` attribute is required when using the page for [actions] that require the URL argument. - -## Page examples - -These examples demonstrate explicit and parameterized pages and include informative explanations. - -### Explicit page - -Example (_Catalog/Page/AdminCategoryPage.xml_ file): - -```xml -<?xml version="1.0" encoding="UTF-8"?> - -<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../..dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> - <page name="AdminCategoryPage" url="catalog/category/" module="Magento_Catalog" area="admin"> - <section name="AdminCategorySidebarActionSection"/> - <section name="AdminCategorySidebarTreeSection"/> - <section name="AdminCategoryBasicFieldSection"/> - <section name="AdminCategorySEOSection"/> - </page> -</pages> -``` - -In this example, the _Catalog/Page/AdminCategoryPage.xml_ file declares a page with the name `AdminCategoryPage`. -It will be merged with the other `AdminCategoryPage` pages from other modules. - -The corresponding web page is generated by the Magento Catalog module and is called by the `baseUrl` + `backendName` + `catalog/category/` URl. - -The `AdminCategoryPage` declares four [sections][section]: - -- `AdminCategorySidebarActionSection` - located in the `Catalog/Section/AdminCategorySidebarActionSection.xml` file -- `AdminCategorySidebarTreeSection` - located in the `Catalog/Section/AdminCategorySidebarTreeSection.xml` file -- `AdminCategoryBasicFieldSection` - located in the `Catalog/Section/AdminCategoryBasicFieldSection.xml` file -- `AdminCategorySEOSection` - located in the `Catalog/Section/AdminCategorySEOSection.xml` file - -The following is an example of a call in test: - -```xml -<amOnPage url="{{AdminCategoryPage.url}}" stepKey="navigateToAdminCategory"/> -``` - -### Parameterized page - -Example (_Catalog/Page/StorefrontCategoryPage.xml_ file): - -```xml -<?xml version="1.0" encoding="UTF-8"?> - -<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="../../../../../..dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Page/etc/PageObject.xsd"> - <page name="StorefrontCategoryPage" url="/{{var1}}.html" module="Magento_Catalog" parameterized="true" area="storefront"> - <section name="StorefrontCategoryMainSection"/> - </page> -</pages> -``` - -This example shows the page with the name `StorefrontCategoryPage`. -It will be merged with the other `StorefrontCategoryPage` pages from other modules. - -The following is an example of a call in test: - -```xml -<amOnPage url="{{StorefrontCategoryPage.url($$createPreReqCategory.name$$)}}" stepKey="navigateToCategoryPage"/> -``` - -The `StorefrontCategoryPage` page is declared as parameterized, where the `url` contains a `{{var1}}` parameter. - -The corresponding web page is generated by the Magento Catalog module and is called by the `baseUrl`+`/$$createPreReqCategory.name$$.html` URl. - -`{{var1}}` is substituted with the `name` of the previously created category in the `createPreReqCategory` action. - -See also [`<createData>`]. - -**** - -The `StorefrontCategoryPage` page declares only the `StorefrontCategoryMainSection` [section], which is located in the `Catalog/Section/StorefrontCategoryMainSection.xml` file. - -## Elements reference - -There are several XML elements that are used in `<page>` in the MFTF. - -### pages {#pages-tag} - -`<pages>` are elements that point to the corresponding XML Schema location. -It contains one or more `<page>` elements. - -### page {#page-tag} - -`<page>` contains a sequence of UI sections in a page. - -Attributes|Type|Use|Description ----|---|---|--- -`name`|string|required|Unique page name identifier. -`url`|string|required|URL path (excluding the base URL) for the page. Use parameterized notation (`{{var1}}`) for replaceable parameters, such as the edit page for a persisted entity that is based on an ID or a name. -`module`|string|required|Name of the module to which the page belongs. The name must be prefixed with a vendor name. It corresponds to the parent directory where the module with tests is stored. Example: `"Magento_Catalog"`. -`area`|string|required|The area where this page lives. Three possible values: `admin` prepends `BACKEND_NAME` to `url`, `storefront` does not prepend anything to `url`, `external` flags the page for use with `amOnUrl`. The `url` provided must be a full URL, such as `http://myFullUrl.com/`, instead of the URL for a Magento page. -`parameterized`|boolean |optional|Include and set to `"true"` if the `url` for this page has parameters that need to be replaced for proper use. -`remove`|boolean|optional|The default value is `"false"`. Set to `"true"` to remove this element during parsing. -`deprecated`|string|optional|Used to warn about the future deprecation of the data entity. String will appear in Allure reports and console output at runtime. - -`<page>` may contain several [`<section>`] elements. - -<!-- {% endraw %} --> - -### section {#section-tag} - -`<section>` contains the sequence of UI elements. -A section is a reusable piece or part of a page. - -Attributes|Type|Use|Description ----|---|---|--- -`name`|string|required|Unique section name identifier. -`remove`|boolean|optional|The default value is `"false"`. Set to `"true"` to remove this element during parsing. - -<!-- Link definitions --> -[`<createData>`]: test/actions.md#createdata -[`<page>`]: #page-tag -[`<section>`]: #section-tag -[`<test>`]: test.md -[actions]: test/actions.md -[explicit page]: #explicit-page -[PageObjects]: https://github.com/SeleniumHQ/selenium/wiki/PageObjects -[parameterized page]: #parameterized-page -[section]: section.md diff --git a/docs/reporting.md b/docs/reporting.md deleted file mode 100644 index 59a8617ba..000000000 --- a/docs/reporting.md +++ /dev/null @@ -1,349 +0,0 @@ -# Reporting - -The Magento Functional Testing Framework provides two types of reporting: - -- Inline reporting that you can view in the terminal as you run [`mftf`][mftf] or [`codecept`][codecept] CLI commands. -- HTML reports that you can view using the [Allure Framework][] after a test run completes. - -When you run a test, MFTF copies all reporting artifacts to the `dev/tests/acceptance/tests/_output` subdirectory in the Magento root directory. -The directory contains: - -- `allure-results/` that is a directory generated and served by the Allure Framework. -- `failed` that is a text file containing relative paths to failed tests after the last test run. - The paths are relative to `dev/tests/acceptance/`. -- `.html` and `.png` files that are screenshots of fails in HTML and PNG formats. - To cleanup the `_output/` directory, remove them manually. - -The `mftf` tool logs output continuously to the `dev/tests/acceptance/mftf.log` file. - -## Command line - -MFTF reports about its progress during test run when you run the `mftf` CLI tool with [`run:test`][] or [`run:group`][] commands. - -The report can contain three main parts: - -- Pre-run checks: - - Environment check, such as PHP warnings, etc. - - XML test validation like deprecation warnings such as missing required components in XML tests. -- Codeception report which is the progress report for each test. -- Total results of the test run such as number of tests, assertions, and failures. - -To manage the level of verbosity, use `-v` or `--verbose` flag in the `mftf` commands. -To enable verbosity using the `codecept` commands, refer to the Codeception [Console Commands][codeception]. - -The following sections demonstrate an example interpretation of a complete log separated into chunks. - -### Pre-run check report - -First, MFTF returns `DEPRECATION` warnings alerting you that required test components are missing in XML test definitions. - -```terminal -DEPRECATION: Test AdminFilteringCategoryProductsUsingScopeSelectorTest is missing required annotations.{"testName":"AdminFilteringCategoryProductsUsingScopeSelectorTest","missingAnnotations":"stories"} -DEPRECATION: Test AdminAbleToShipPartiallyInvoicedItemsTest is missing required annotations.{"testName":"AdminAbleToShipPartiallyInvoicedItemsTest","missingAnnotations":"stories"} -DEPRECATION: Test AdminRemoveProductWeeeAttributeOptionTest is missing required annotations.{"testName":"AdminRemoveProductWeeeAttributeOptionTest","missingAnnotations":"stories"} -Generate Tests Command Run -``` - -`Generate Tests Command Run` indicates that test generation is finished and tests are able to be executed. - -### Test execution report - -A test execution report is generated by Codeception and includes configuration information, scenario execution steps, and PASSED/FAIL verdict after each test completion. - -#### General information - -The general information can be useful for MFTF contributors, but can be ignored by a test writer. - -Let's consider the general part of the following test execution report: - -```terminal -==== Redirecting to Composer-installed version in vendor/codeception ==== -Codeception PHP Testing Framework v2.3.9 -Powered by PHPUnit 6.5.13 by Sebastian Bergmann and contributors. - -Magento\FunctionalTestingFramework.functional Tests (2) ------------------------ -Modules: \Magento\FunctionalTestingFramework\Module\MagentoWebDriver, \Magento\FunctionalTestingFramework\Helper\Acceptance, \Magento\FunctionalTestingFramework\Helper\MagentoFakerData, \Magento\FunctionalTestingFramework\Module\MagentoRestDriver, PhpBrowser, \Magento\FunctionalTestingFramework\Module\MagentoSequence, \Magento\FunctionalTes -``` - -After the test generation command (mentioned in the previous section), MFTF delegates control to the `vendor/codeception` tool, which is the `Codeception PHP Testing Framework` of version `2.3.9` that uses `PHPUnit` of version `6.5.13`. - -The tool runs `2 Tests` using the configuration defined in the `functional` suite under the `Magento\FunctionalTestingFramework` namespace. -The corresponding configuration file is `acceptance/tests/functional.suite.yml`. -It enables `Modules: \Magento\FunctionalTestingFramework\Module\MagentoWebDriver, \Magento\FunctionalTestingFramework\Helper\Acceptance, \Magento\FunctionalTestingFramework\Helper\MagentoFakerData, \Magento\FunctionalTestingFramework\Module\MagentoRestDriver, PhpBrowser, \Magento\FunctionalTestingFramework\Module\MagentoSequence, ...` - -#### Passed tests - -The next chunk of the log reports about test execution of the first test: - -```terminal -AdminLoginTestCest: Admin login test -Signature: Magento\AcceptanceTest\_default\Backend\AdminLoginTestCest:AdminLoginTest -Test: tests/functional/Magento/FunctionalTest/_generated/default/AdminLoginTestCest.php:AdminLoginTest -Scenario -- -I am on page "/admin/admin" -I fill field "#username","admin" -I fill field "#login","123123q" -I click ".actions .action-primary" -I wait for page load 30 -I close admin notification -I see in current url "/admin/admin" -PASSED -``` - -The running test is `AdminLoginTestCest`, which is `Admin login test` (this text is generated from the test name but with the `Cest` part excluded). -Its test signature is `Magento\AcceptanceTest\_default\Backend\AdminLoginTestCest:AdminLoginTest` that matches a `className:methodName` format using namespaces. -A path to the corresponding `Test` is `tests/functional/Magento/FunctionalTest/_generated/default/AdminLoginTestCest.php:AdminLoginTest` (relative to the `acceptance/` directory). - -`Scenario` lists the tests steps as they run during test execution, ending with the successful test verdict `PASSED`. -It means that all test steps were processed as expected. - -#### Failed tests - -The second test fails with the following report: - -```terminal -AdminMenuNavigationWithSecretKeysTestCest: Admin menu navigation with secret keys test -Signature: Magento\AcceptanceTest\_default\Backend\AdminMenuNavigationWithSecretKeysTestCest:AdminMenuNavigationWithSecretKeysTest -Test: tests/functional/Magento/FunctionalTest/_generated/default/AdminMenuNavigationWithSecretKeysTestCest.php:AdminMenuNavigationWithSecretKeysTest -Scenario -- -I magento cli "config:set admin/security/use_form_key 1" -Value was saved. -I magento cli "cache:clean config full_page" -Cleaned cache types: -config -full_page -I am on page "/admin/admin" -I wait for page load -I fill field "#username","admin" -I fill field "#login","123123q" -I click ".actions .action-primary" -I wait for page load 30 -I close admin notification -I click "//li[@id='menu-magento-backend-stores']" -I wait for loading mask to disappear -I click "#nav li[data-ui-id='menu-magento-config-system-config']" -I wait for page load -I see current url matches "~\/admin\/system_config\/~" -I see "#something" -I save screenshot -FAIL - -I magento cli "config:set admin/security/use_form_key 0" -Value was saved. -I magento cli "cache:clean config full_page" -Cleaned cache types: -config -full_page -I am on page "/admin/admin/auth/logout/" --------------------------------------------------------------------------------- -``` - -The general test details and scenario has the same format as in the Passed test. -The interesting part starts near the `FAIL` line. - -```terminal -I see "#something" -I save screenshot -FAIL -``` - -When a test step fails, MFTF always saves a screenshot of the web page with the failing state immediately after the failure occurs. -`I save screenshot` follows the failing test step `I see "#something"` in our case. - -A screenshot of the fail goes at the `acceptance/tests/_output` directory in both PNG and HTML formats: - -- `Magento.AcceptanceTest._default.Backend.AdminMenuNavigationWithSecretKeysTestCest.AdminMenuNavigationWithSecretKeysTest.fail.html` -- `Magento.AcceptanceTest._default.Backend.AdminMenuNavigationWithSecretKeysTestCest.AdminMenuNavigationWithSecretKeysTest.fail.png` - -The file name encodes: - -- `Magento` namespace -- with the `AcceptanceTest` test type -- generated as a part of the `_default` suite -- defined at the `Magento_Backend` module -- implemented in the `AdminMenuNavigationWithSecretKeysTestCest` PHP class -- with the `AdminMenuNavigationWithSecretKeysTest` test name -- and execution status `fail` - -Actions after `FAIL` are run as a part of the [`after`][] hook of the test. - -### Test result report - -After MFTF completed test execution, it generates a general report about test results along with detailed information about each fail. - -```terminal --------------------------------------------------------------------------------- -DEPRECATION: Calling the "Symfony\Component\BrowserKit\Client::getInternalResponse()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0. /Users/.../magento2ce/vendor/symfony/browser-kit/Client.php:208 - -Time: 52.43 seconds, Memory: 16.00MB - -There was 1 failure: ---------- -``` - -First you see warnings and deprecations. -The `DEPRECATION` here is thrown by an MFTF dependency (Symfony) that is out of the scope for test writers and should be considered by MFTF contributors. -If you encounter this type of reporting, [report an issue][]. - -Then, MFTF reports that the test run took 52.43 seconds using 16 MB of system RAM. -And, finally, that there was `1 failure`. - -Next, the report provides details about the test failure. - -```terminal ---------- -1) AdminMenuNavigationWithSecretKeysTestCest: Admin menu navigation with secret keys test -Test tests/functional/Magento/FunctionalTest/_generated/default/AdminMenuNavigationWithSecretKeysTestCest.php:AdminMenuNavigationWithSecretKeysTest -Step See "#something" -Fail Failed asserting that on page /admin/admin/system_config/index/key/678b7ba922c.../ ---> DASHBOARD -SALES -CATALOG -CUSTOMERS -MARKETING -CONTENT -REPORTS -STORES -SYSTEM -FIND PARTNERS & EXTENSIONS -Configuration -admin -1 -Store View: Default Config -What is this? -Save Config -Country Options -State Options -Locale Options -Store Information -Store Name -Store Phone Number -Store Hours of Operation -Countr -[Content too long to display. See complete response in '/Users/dmytroshevtsov/Projects/vagrant/vagrant-magento/magento2ce/dev/tests/acceptance/tests/_output/' directory] ---> contains "#something". - -Scenario Steps: - -23. $I->amOnPage("/admin/admin/auth/logout/") at tests/functional/Magento/FunctionalTest/_generated/default/AdminMenuNavigationWithSecretKeysTestCest.php:54 -22. // Cleaned cache types: -config -full_page -21. $I->magentoCLI("cache:clean config full_page") at tests/functional/Magento/FunctionalTest/_generated/default/AdminMenuNavigationWithSecretKeysTestCest.php:52 -20. // Value was saved. -19. $I->magentoCLI("config:set admin/security/use_form_key 0") at tests/functional/Magento/FunctionalTest/_generated/default/AdminMenuNavigationWithSecretKeysTestCest.php:50 -18. $I->saveScreenshot() at tests/functional/Magento/FunctionalTest/_generated/default/AdminMenuNavigationWithSecretKeysTestCest.php:63 -``` - -- `1) AdminMenuNavigationWithSecretKeysTestCest: Admin menu navigation with secret keys test` - the failed Codeception test is *AdminMenuNavigationWithSecretKeysTestCest*. It references to the PHP class that implemented the failed test. - -- `Test tests/functional/Magento/FunctionalTest/_generated/default/AdminMenuNavigationWithSecretKeysTestCest.php:AdminMenuNavigationWithSecretKeysTest` - the test is implemented in the *AdminMenuNavigationWithSecretKeysTest* test method of the *tests/functional/Magento/FunctionalTest/_generated/default/AdminMenuNavigationWithSecretKeysTestCest.php* file under `<magento root>/dev/tests/acceptance/`. - It matches the corresponding test defined in XML that is *AdminMenuNavigationWithSecretKeysTest* defined in `<test name="AdminMenuNavigationWithSecretKeysTest">...</test>` - -- `Step See "#something"` - the failing test step is the *see* action with the *#something* selector. It would correspond the `<see selector="#something" ... />` test step in the XML defined tests. - -- `Fail Failed asserting that on page /admin/admin/system_config/index/key/678b7ba922c.../` - the fail occurred on the web page `<MAGENTO_BASE_URL>/admin/admin/system_config/index/key/678b7ba922c.../`. - -```terminal ---> ... -[Content too long to display. See complete response in '/../../magento2/dev/tests/acceptance/tests/_output/' directory] ---> contains "#something". -``` - -The web page is too long to be reported in the CLI, and it is stored at *'/../../magento2/dev/tests/acceptance/tests/_output/'*. -Search the web page by test name *AdminMenuNavigationWithSecretKeysTest*. -The failing test assertion is that the web page contains *contains* a CSS locator *#something*. - -Finally, the report finishes with fairly self-descriptive lines. - -```terminal -FAILURES! -Tests: 2, Assertions: 3, Failures: 1. -``` - -MFTF encountered failures due to the last test run, that included *2* tests with *3* assertions. -*1* assertion fails. - -## Allure - -Each time you run tests, MFTF appends an XML file with results at the `tests/_output/allure-results/` directory. - -The official [Allure Test Report][] documentation is well-covered, so we'll list only the CLI commands that you would need for your day-to-day work. - -<div class="bs-callout bs-callout-info"> -The following commands are relative to the Magento installation directory. -</div> - -To generate the HTML Allure report in a temporary folder and open the report in your default web browser: - -```bash -allure serve dev/tests/acceptance/tests/_output/allure-results/ -``` - -To generate a report to the `allure-report/` at the current directory: - -```bash -allure generate dev/tests/acceptance/tests/_output/allure-result -``` - -To generate a report to a particular directory, use the `-o` option: - -```bash -allure generate dev/tests/acceptance/tests/_output/allure-result -o dev/tests/acceptance/tests/_output/allure-report -``` - -To launch the generated report in a web browser: - -```bash -allure open dev/tests/acceptance/tests/_output/allure-report -``` - -<div class="bs-callout bs-callout-info" markdown="1"> -By default, Allure generates reports in the `allure-report/` at the current directory. -For example, if you run the command without `-o` flag while you are in the `magento2/` directory, Allure will generate a report at the `magento2/allure-report/` directory. -</div> - -```bash -allure generate dev/tests/acceptance/tests/_output/allure-result -``` - -Example of file structure after the command run: - -```terminal -magento2 -├── allure-report -├── app -├── bin -├── dev -├── ... -``` - -And if you run the `open` command with no arguments while you are in the same directory (`magento2/`): - -```bash -allure open -``` - -Allure would attempt to open a generated report at the `magento2/allure-report/` directory.' -%} - -To clean up existing reports before generation (for example after getting new results), use the `--clean` flag: - -```bash -allure generate dev/tests/acceptance/tests/_output/allure-result --clean -``` - -Refer to the [Reporting section][] for more Allure CLI details. - -<!-- Link definitions --> - -[`after`]: test.md#after-tag -[`run:group`]: commands/mftf.md#rungroup -[`run:test`]: commands/mftf.md#runtest -[Allure Framework]: https://docs.qameta.io/allure/ -[Allure Test Report]: http://allure.qatools.ru/ -[codecept]: commands/codeception.md -[codeception]: https://codeception.com/docs/reference/Commands -[mftf]: commands/mftf.md -[report an issue]: https://github.com/magento/magento2-functional-testing-framework/blob/master/.github/CONTRIBUTING.md#report-an-issue -[Reporting section]: https://docs.qameta.io/allure/#_reporting diff --git a/docs/section.md b/docs/section.md deleted file mode 100644 index 421c98fdd..000000000 --- a/docs/section.md +++ /dev/null @@ -1,154 +0,0 @@ -# Section structure - -A `<section>` is a reusable part of a [`<page>`](./page.html) and is the standard file for defining UI elements on a page used in a test. - -A `<section>` can define: - -<!-- {% raw %} --> - -- An explicit element that has a selector equal to the constant string. Example: `selector="#add_root_category_button"` -- A parameterized element that contains substitutable values in the selector. Example: `selector="#element .{{var1}} .{{var2}}"`. - -## Substitutable values - -Substitutable values in the test can be of the following formats: - -- String literals (`stringLiteral`) -- References to a [data entity][] (XML data from the corresponding `.../Data/*.xml`) such as `entityName.Field`. -- Persisted data: - - `$persistedCreateDataKey.field$` for data created in the scope of a [test][] using the [`<createData>`][] action with `stepKey="persistedCreateDataKey"`. - - `$$persistedCreateDataKey.field$$` for data created in [before][] and [after][] hooks. Even though `<before>`and `<after>` are nested inside a [test][], persisted data is stored differently when it is done in a test hook. Therefore it must be accessed with a different notation. - -The following diagram shows the XML structure of an MFTF section: - -![XML Structure of MFTF section](img/section-dia.svg) - -## Format - -The format of a `<section>` is: - -```xml -<?xml version="1.0" encoding="UTF-8"?> - -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> - <section name=""> - <element name="" type="" selector="" /> - <element name="" type="" selector="" parameterized="true"/> - <element name="" type="" selector="" timeout=""/> - </section> -</sections> -``` - -## Principles - -The following conventions apply to MFTF sections: - -- `<section>` name must be alphanumeric. -- `*Section.xml` is stored in the _Section_ directory of a module. -- The name format is `{Admin|Storefront}{SectionDescription}Section.xml`. -- Camel case is used for `<section>` elements. -- One `<section>` tag is allowed per section XML file. - -## Example - -Example (`.../Catalog/Section/AdminCategorySidebarActionSection.xml` file): - -```xml -<?xml version="1.0" encoding="utf-8"?> - -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> - <section name="AdminCategorySidebarActionSection"> - <element name="addRootCategoryButton" type="button" selector="#add_root_category_button" timeout="30"/> - <element name="addSubcategoryButton" type="button" selector="#add_subcategory_button" timeout="30"/> - </section> -</sections> -``` - -This example uses a `AdminCategorySidebarActionSection` section. All sections with same name will be merged during test generation. - -The `AdminCategorySidebarActionSection` section declares two buttons: - -- `addRootCategoryButton` - button with a `#add_root_category_button` locator on the parent web page -- `addSubcategoryButton` - button with a `#add_subcategory_button` locator on the parent web page - -The following is an example of a call in test: - -```xml -<!-- Click on the button with locator "#add_subcategory_button" on the web page--> -<click selector="{{AdminCategorySidebarActionSection.addSubcategoryButton}}" stepKey="clickOnAddSubCategory"/> -``` - -## Elements reference - -### section {#section-tag} - -`<section>` contains the sequence of UI elements in a section of a [page][]. - -Attributes|Type|Use|Description ----|---|---|--- -`name`|string|required|Unique section name identifier. -`deprecated`|string|optional|Used to warn about the future deprecation of the section. String will appear in Allure reports and console output at runtime. -`remove`|boolean|optional|The default is `false`. Set to `true` to remove this element during parsing. - -### element {#element-tag} - -`<element>`is a UI element used in an [action][]. - -Attributes|Type|Use|Description ----|---|---|--- -`name`|string|required|The element name; Must be alphanumeric. -`type`|string|required|The type of the element. Possible values: `text`, `textarea`, `input`, `button`, `checkbox`, `radio`, `checkboxset`, `radioset`, `date`, `file`, `select`, `multiselect`, `wysiwyg`, `iframe`, `block`. -`selector`|string|optional|[XPath][] or [CSS][] selector of the element. -`locatorFunction`|string|optional|[Locator function][] declaration to be used in lieu of a selector. -`timeout`|string|optional|The timeout after interaction with the element (in seconds). The default is _none_. -`parameterized`|boolean|optional|Include and set to `true` if the `selector` for this element has parameters that need to be replaced for proper use. Learn more in [Parameterized selectors][]. -`deprecated`|string|optional|Used to warn about the future deprecation of the element. String will appear in Allure reports and console output at runtime. -`remove`|boolean|optional|The default is `false`. Set to `true` to remove this element during parsing. - -#### `timeout` attribute {#timeout-attribute} - -The attribute adds the [waitForPageLoad] action after a reference to the element in test. -The most usual use case is a test step with a button click action. - -**Use case**: Set a timeout of 30 seconds after clicking the `signIn` button. - -The section element code declaration containing the timeout attribute: - -> StorefrontSigninSection.xml - -```xml -... -<element name="signIn" type="button" selector="#signIn" timeout="30"/> -... -``` - -The test step that covers the use case: - -> StorefrontSigninTest.xml - -```xml -... -<click selector="{{StorefrontSigninSection.signIn}}" ../> -... -``` - -<!-- {% endraw %} --> - -Whenever the `signIn` button is used in a test, the MFTF will add a 30 second `waitForPageLoad` action immediately after the `click`. - -<!-- Link definitions --> - -[waitForPageLoad]: test/actions.html#waitforpageload -[data entity]: ./data.html -[test]: ./test.html#test-tag -[`<createData>`]: ./test/actions.html#createdata -[before]: ./test.html#before-tag -[after]: ./test.html#after-tag -[page]: ./page.html -[action]: ./test/actions.html -[XPath]: https://www.w3schools.com/xml/xpath_nodes.asp -[CSS]: https://www.w3schools.com/cssref/css_selectors.asp -[Locator function]: ./section/locator-functions.html -[Parameterized selectors]: ./section/parameterized-selectors.html diff --git a/docs/section/locator-functions.md b/docs/section/locator-functions.md deleted file mode 100644 index 1e3dffd24..000000000 --- a/docs/section/locator-functions.md +++ /dev/null @@ -1,45 +0,0 @@ -# Locator functions - -## Define Locator::functions in elements - - Codeception has a set of very useful [Locator functions][] that may be used by elements inside a [section][]. - -Declare an element with a `locatorFunction`: - -```xml -<element name="simpleLocator" type="button" locatorFunction="Locator::contains('label', 'Name')"/> -``` - -When using the `locatorFunction`, omit `Locator::` for code simplicity: - -```xml -<element name="simpleLocatorShorthand" type="button" locatorFunction="contains('label', 'Name')"/> -``` - -An element's `locatorFunction` can also be parameterized the same way as [parameterized selectors][]: - -<!-- {% raw %} --> - -```xml -<element name="simpleLocatorTwoParam" type="button" locatorFunction="contains({{arg1}}, {{arg2}})" parameterized="true"/> -``` - -An element cannot, however, have both a `selector` and a `locatorFunction`. - -## Call Elements that use locatorFunction - -Given the above element definitions, you call the elements in a test just like any other element. No special reference is required, as you are still just referring to an `element` inside a `section`: - -```xml -<test name="LocatorFuctionTest"> - <click selector="{{LocatorFunctionSection.simpleLocator}}" stepKey="SimpleLocator"/> - <click selector="{{LocatorFunctionSection.simpleLocatorTwoParam('string1', 'string2')}}" stepKey="TwoParamLiteral"/> -</test> -``` - -<!-- {% endraw %} --> - -<!-- Link Definitions --> -[Locator functions]: http://codeception.com/docs/reference/Locator -[section]: ../section.md -[parameterized selectors]: ./parameterized-selectors.md \ No newline at end of file diff --git a/docs/section/parameterized-selectors.md b/docs/section/parameterized-selectors.md deleted file mode 100644 index 58227948f..000000000 --- a/docs/section/parameterized-selectors.md +++ /dev/null @@ -1,155 +0,0 @@ -# Parameterized selectors - -Use the following examples to create and use parameterized selectors in the MFTF. - -## Set up a selector in section - -Create a new `<element/>` in a `<section></section>`, : - -```xml -<section name="SampleSection"> - <element name="" type="" selector=""/> -</section> -``` - -Add the attribute `parameterized="true"` to the `<element/>`: - -```xml -<section name="SampleSection"> - <element name="" type="" selector="" parameterized="true"/> -</section> -``` - -Add your selector in the `selector=""` attribute: - -```xml -<section name="SampleSection"> - <element name="" type="" selector="#element" parameterized="true"/> -</section> -``` - -<!-- {% raw %} --> - -### Selector with single variable - -For the parameterized part of the selector, add `{{var1}}` to represent the first piece of data that you want to replace: - -```xml -<section name="SampleSection"> - <element name="" type="" selector="#element .{{var1}}" parameterized="true"/> -</section> -``` - -Add a descriptive name in the `name=""` attribute: - -```xml -<section name="SampleSection"> - <element name="oneParamElement" type="" selector="#element .{{var1}}" parameterized="true"/> -</section> -``` - -Add the type of UI element that the `<element/>` represents in `type`: - -```xml -<section name="SampleSection"> - <element name="oneParamElement" type="text" selector="#element .{{var1}}" parameterized="true"/> -</section> -``` - -### Selector with multiple variables - -For the parameterized part of the selector, add `{{var1}}, {{var2}}, ..., {{varN}}` for each parameter that you need to pass in: - -```xml -<section name="SampleSection"> - <element name="threeParamElement" type="text" selector="#element .{{var1}} .{{var2}}" parameterized="true"/> -</section> -``` - -```xml -<section name="SampleSection"> - <element name="threeParamElement" type="text" selector="#element .{{var1}} .{{var2}}-{{var3}}" parameterized="true"/> -</section> -``` - -<div class="bs-callout bs-callout-info" markdown="1"> -There is no need to use sequential variables like `{{var1}}`, `{{var2}}`. Parameterized replacement reads variables and maps them to the test call of the element sequentially from left to right, meaning you can use a selector like `#element .{{categoryId}} .{{productId}}`." -</div> - -## Use a parameterized selector in a test - -Create a new [test][]: - -```xml -<test name="SampleTest"> - -</test> -``` - -Add an action: - -```xml -<test name="SampleTest"> - <click selector="" stepKey="click1"/> -</test> -``` - -Enter `"{{}}"` in the `selector=""` attribute: - -```xml -<test name="SampleTest"> - <click selector="{{}}" stepKey="click1"/> -</test> -``` - -Make a reference to the section that the element is assigned to inside the `{{}}`: - -```xml -<test name="SampleTest"> - <click selector="{{SampleSection}}" stepKey="click1"/> -</test> -``` - -Add name of a parameterized element, separated by `"."`, inside the `{{}}`: - -```xml -<test name="SampleTest"> - <click selector="{{SampleSection.threeParamElement}}" stepKey="click1"/> -</test> -``` - -Add a set of `"()"` following the parameterized element inside the `{{}}`: - -```xml -<test name="SampleTest"> - <click selector="{{SampleSection.threeParamElement()}}" stepKey="click1"/> -</test> -``` - -Add the first parameter, that you would like to pass to the selector, inside of the `()`: - -```xml -<test name="SampleTest"> - <click selector="{{SampleSection.threeParamElement(_defaultCategory.is_active)}}" stepKey="click1"/> -</test> -``` - -Add the second or third parameters, that you'd like to pass to the selector, separated by `, `: - -```xml -<test name="SampleTest"> - <click selector="{{SampleSection.threeParamElement(_defaultCategory.is_active,'StringLiteral',$createDataKey.id$)}}" stepKey="click1"/> -</test> -``` - -<!-- {% endraw %} --> - -Any data can be used in parameterized elements, as well as entered in test actions: - -* `_defaultCategory.is_active` is a reference to `<data key="is_active">` in `<entity name="_defaultCategory" ... ></entity>` in the corresponding `.../Data/*.xml`. -* `'StringLiteral'` is a literal. -* `$createDataKey.id$` is a reference to persisted data created in the `SampleTest1` within the `stepKey="createDataKey"` action. -* `{$variable}` is a reference to data returned by a test action, like `<grabValueFrom>`. - -<!-- Link Definitions --> -[test]: ../test.md \ No newline at end of file diff --git a/docs/selectors.md b/docs/selectors.md deleted file mode 100644 index 376140819..000000000 --- a/docs/selectors.md +++ /dev/null @@ -1,35 +0,0 @@ -## Selectors - -These guidelines should help you to write high quality selectors. - -### Selectors SHOULD be written in CSS instead of XPath whenever possible - -CSS is generally easier to read than XPath. For example, `//*[@id="foo"]` in XPath can be expressed as simply as `#foo` in CSS. -See this [XPath Cheatsheet](https://devhints.io/xpath) for more examples. - -### XPath selectors SHOULD NOT use `@attribute="foo"`. - -This would fail if the attribute was `attribute="foo bar"`. -Instead you SHOULD use `contains(@attribute, "foo")` where `@attribute` is any valid attribute such as `@text` or `@class`. - -### CSS and XPath selectors SHOULD be implemented in their most simple form - -* <span class="color:green">GOOD:</span> `#foo` -* <span class="color:red">BAD:</span> `button[contains(@id, "foo")]` - -### CSS and XPath selectors SHOULD avoid making use of hardcoded indices - -Instead you SHOULD parameterize the selector. - -* <span class="color:green">GOOD:</span> `.foo:nth-of-type({{index}})` -* <span class="color:red">BAD:</span> `.foo:nth-of-type(1)` - -* <span class="color:green">GOOD:</span> `button[contains(@id, "foo")][{{index}}]` -* <span class="color:red">BAD:</span> `button[contains(@id, "foo")][1]` - -* <span class="color:green">GOOD:</span> `#actions__{{index}}__aggregator` -* <span class="color:red">BAD:</span> `#actions__1__aggregator` - -### CSS and XPath selectors MUST NOT reference the `@data-bind` attribute - -The `@data-bind` attribute is used by KnockoutJS, a framework Magento uses to create dynamic Javascript pages. Since this `@data-bind` attribute is tied to a specific framework, it should not be used for selectors. If Magento decides to use a different framework then these `@data-bind` selectors would break. diff --git a/docs/suite.md b/docs/suite.md deleted file mode 100644 index 65bcedce0..000000000 --- a/docs/suite.md +++ /dev/null @@ -1,295 +0,0 @@ -# Suites - -Suites are essentially groups of tests that run in specific conditions (preconditions and postconditions). -They enable including, excluding, and grouping tests for a customized test run. -You can form suites using separate tests, groups, and modules. - -Each suite must be defined in the `<VendorName>/<ModuleName>/Test/Mftf/Suite` directory. - -The tests for each suite are generated in a separate directory under `<magento 2 root>/dev/tests/acceptance/tests/functional/Magento/FunctionalTest/_generated/`. -All tests that are not within a suite are generated in the _default_ suite at `.../Magento/FunctionalTest/_generated/default/`. - -<div class="bs-callout bs-callout-info"> - If a test is generated into at least one custom suite, it will not appear in the _default_ suite. -</div> - -## Format - -The format of a suite: - -```xml -<?xml version="1.0" encoding="UTF-8"?> -<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Suite/etc/suiteSchema.xsd"> - <suite name=""> - <before> - </before> - <after> - </after> - <include> - <test name=""/> - <group name=""/> - <module name="" file=""/> - </include> - <exclude> - <test name=""/> - <group name=""/> - <module name="" file=""/> - </exclude> - </suite> -</suites> -``` - -## Principles - -- A suite name: - - - must not match any existing group value. - For example, the suite `<suite name="ExampleTest">` will fail during test run if any test contains in annotations `<group value="ExampleTest">`. - - must not be `default` or `skip`. Tests that are not in any suite are generated under the `default` suite. - The suite name `skip` is synonymous to including a test in the `<group value="skip"/>`, which will be deprecated in MFTF 3.0.0. - - can contain letters, numbers, and underscores. - - should be upper camel case. - -- A suite must contain at least one `<include>`, or one `<exclude>`, or both. -- Using `<before>` in a suite, you must add the corresponding `<after>` to restore the initial state of your testing instance. -- One `<suite>` tag is allowed per suite XML file. - -## Conditions - -Using suites enables test writers to consolidate conditions that are shared between tests. -The code lives in one place and executes once per suite. - -- Set up preconditions and postconditions using [actions] in [`<before>`] and [`<after>`] correspondingly, just similar to use in a [test]. -- Clean up after suites just like after tests. - The MFTF enforces the presence of both `<before>` and `<after>` if either is present. - -## Test writing - -Since suites enable precondition consolidation, a common workflow for test writing is adding a new test to an existing suite. -Such test is generated in context of the suite that contains it. -You cannot isolate this test from preconditions of the suite; it cannot be used outside of the suite at the same time. - -There are several ways to generate and execute your new test in the context of a suite: - -- Edit the appropriate `suite.xml` to include your test only and run: - - ```bash - vendor/bin/mftf run:group <suiteName> - ``` - -- Temporarily add a group to your test like `<group value="foo">` and run: - - ```bash - vendor/bin/mftf run:group foo - ``` - -- To limit generation to your suite/test combination, run in conjunction with the above: - - ```bash - vendor/bin/mftf generate:suite <suite> - ``` - -- To generate any combination of suites and tests, use [`generate:tests`] with the `--tests` flag. - -## Examples - -### Enabling/disabling WYSIWYG in suite conditions - -<!-- {% raw %} --> - -```xml -<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Suite/etc/suiteSchema.xsd"> - <suite name="WYSIWYG"> - <before> - <actionGroup ref="LoginAsAdmin" stepKey="login"/> - <amOnPage url="admin/admin/system_config/edit/section/cms/" stepKey="navigateToConfigurationPage" /> - <waitForPageLoad stepKey="wait1"/> - <conditionalClick stepKey="expandWYSIWYGOptions" selector="{{ContentManagementSection.WYSIWYGOptions}}" dependentSelector="{{ContentManagementSection.CheckIfTabExpand}}" visible="true" /> - <waitForElementVisible selector="{{ContentManagementSection.EnableWYSIWYG}}" stepKey="waitForEnableWYSIWYGDropdown1" /> - <waitForElementVisible selector="{{ContentManagementSection.EnableSystemValue}}" stepKey="waitForUseSystemValueVisible"/> - <uncheckOption selector="{{ContentManagementSection.EnableSystemValue}}" stepKey="uncheckUseSystemValue"/> - <selectOption selector="{{ContentManagementSection.EnableWYSIWYG}}" userInput="Enabled by Default" stepKey="selectOption1"/> - <click selector="{{ContentManagementSection.WYSIWYGOptions}}" stepKey="collapseWYSIWYGOptions" /> - <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig" /> - </before> - <after> - <actionGroup ref="LoginAsAdmin" stepKey="login"/> - <actionGroup ref="DisabledWYSIWYG" stepKey="disable"/> - </after> - <include> - <group name="WYSIWYG"/> - </include> - </suite> -</suites> -``` - -<!-- {% endraw %} --> -This example declares a suite with the name `WYSIWYG`. -The suite enables WYSIWYG *before* running tests. -It performs the following steps: - -1. Log in to the backend. -2. Navigate to the **Configuration** page. -3. Enable **WYSIWYG** in the Magento instance. - -*After* the testing, the suite returns the Magento instance to the initial state disabling WYSIWYG: - -1. Log back in. -2. Disable **WYSIWYG** in the Magento instance. - -This suite includes all tests that contain the `<group value="WYSIWYG"/>` annotation. - -### Execute Magento CLI commands in suite conditions - -```xml -<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Suite/etc/suiteSchema.xsd"> - <suite name="Cache"> - <before> - <magentoCLI stepKey="disableCache" command="cache:disable"/> - </before> - <after> - <magentoCLI stepKey="enableCache" command="cache:enable"/> - </after> - <include> - <test name="SomeCacheRelatedTest"/> - <group name="CacheRelated"/> - </include> - </suite> -</suites> -``` - -This example declares a suite with the name `Cache`. - -Preconditions: - -1. It disables the Magento instance cache entirely before running the included tests. -2. After the testing, it re-enables the cache. - -The suite includes a specific test `SomeCacheRelatedTest` and every `<test>` that includes the `<group value="CacheRelated"/>` annotation. - -### Change Magento configurations in suite conditions - -```xml -<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../dev/tests/acceptance/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/Suite/etc/suiteSchema.xsd"> - <suite name="PaypalConfiguration"> - <before> - <createData entity="SamplePaypalConfig" stepKey="createSamplePaypalConfig"/> - </before> - <after> - <createData entity="DefaultPayPalConfig" stepKey="restoreDefaultPaypalConfig"/> - </after> - <include> - <module name="Catalog"/> - </include> - <exclude> - <test name="PaypalIncompatibleTest"/> - </exclude> - </suite> -</suites> -``` - -This example declares a suite with the name `PaypalConfiguration`: - -- `<before>` block persists a Paypal Configuration enabling all tests in this suite to run under the newly reconfigured Magento instance. -- `<after>` block deletes the persisted configuration, returning Magento to its initial state. -- The suite includes all tests from the `Catalog` module, except the `PaypalIncompatibleTest` test. - -## Elements reference - -### suites {#suites-tag} - -The root element for suites. - -### suite {#suite-tag} - -A set of "before" and "after" preconditions, and test filters to include and exclude tests in the scope of suite. - -Attributes|Type|Use|Description ----|---|---|--- -`name`|string|required|Unique suite name identifier. -`remove`|boolean|optional|Removing the suite during merging. - -It can contain `<before>`, `<after>`, `<include>`, and `<exclude>`. - -### before {#before-tag} - -A suite hook with preconditions that executes once before the suite tests. - -It may contain test steps with any [actions] and [action groups]. - -<div class="bs-callout bs-callout-info"> -Tests in the suite are not run and screenshots are not saved in case of a failure in the before hook. -To troubleshoot the failure, run the suite locally. -</div> - -### after {#after-tag} - -A suite hook with postconditions executed once after the suite tests. - -It may contain test steps with any [actions] and [action groups]. - -### include {#include-tag} - -A set of filters that you can use to specify which tests to include in the test suite. - -It may contain filters by: - -- test which names a specific `<test>`. -- group which refers to a declared `group` annotation. -- module which refers to `test` files under a specific Magento Module. - -The element can contain [`<test>`], [`<group>`], and [`<module>`]. - -### exclude {#exclude-tag} - -A set of filters that you can use to specify which tests to exclude in the test suite. - -There are two types of behavior: - -1. Applying filters to the included tests when the suite contains [`<include>`] filters. - The MFTF will exclude tests from the previously included set and generate the remaining tests in the suite. -2. Applying filters to all tests when the suite does not contain [`<include>`] filters. - The MFTF will generate all existing tests except the excluded. - In this case, the custom suite will contain all generated tests except excluded, and the _default_ suite will contain the excluded tests only. - -It may contain filters by: - -- test which names a specific `<test>`. -- group which refers to a declared `group` annotation. -- module which refers to `test` files under a specific Magento Module. - -The element may contain [`<test>`], [`<group>`], and [`<module>`]. - -### test {#test-tag} - -Attributes|Type|Use|Description ----|---|---|--- -`name`|string|required|Filtering a test by its name. -`remove`|boolean|optional|Removing the filter during merging. - -### group {#group-tag} - -Attributes|Type|Use|Description ----|---|---|--- -`name`|string|required|Filtering tests by the `<group>` annotation. -`remove`|boolean|optional|Removing the filter during merging. - -### module {#module-tag} - -Attributes|Type|Use|Description ----|---|---|--- -`name`|string|required|Filtering tests by their location in the corresponding module. -`file`|string|optional|Filtering a specific test file in the module. -`remove`|boolean|optional|Removing the filter during merging. - -<!-- Link definitions --> -[actions]: test/actions.md -[action groups]: test/action-groups.md -[`<after>`]: #after-tag -[`<before>`]: #before-tag -[`generate:tests`]: commands/mftf.md#generatetests -[test]: test.md -[`<test>`]: #test-tag -[`<group>`]: #group-tag -[`<module>`]: #module-tag -[`<include>`]: #include-tag diff --git a/docs/test-prep.md b/docs/test-prep.md deleted file mode 100644 index b344fcf9f..000000000 --- a/docs/test-prep.md +++ /dev/null @@ -1,407 +0,0 @@ -# Preparing a test for MFTF - -This tutorial demonstrates the process of converting a raw functional test into a properly abstracted test file, ready for publishing. - -## The abstraction process - -When first writing a test for a new piece of code such as a custom extension, it is likely that values are hardcoded for the specific testing environment while in development. To make the test more generic and easier for others to update and use, we need to abstract the test. -The general process: - -1. Convert the manual test to a working, hard-coded test. -1. Replace hardcoded selectors to a more flexible format such as [parameterized selectors][]. -1. Convert hardcoded form values and other data to data entities. -1. Convert [actions][] into [action groups][]. - -## The manual test - -Manual tests are just that: A series of manual steps to be run. - -```xml -<!-- Navigate to Catalog -> Products page (or just open by link) --> -<!-- Fill field "Name" with "Simple Product %unique_value%" --> -<!-- Fill field "SKU" with "simple_product_%unique_value%" --> -<!-- Fill field "Price" with "500.00" --> -<!-- Fill field "Quantity" with "100" --> -<!-- Fill field "Weight" with "100" --> -<!-- Click "Save" button --> -<!-- See success save message "You saved the product." --> -<!-- Navigate to Catalog -> Products page (or just open by link) --> -<!-- See created product is in grid --> -<!-- See "Name" in grid is valid --> -<!-- See "SKU" in grid is valid --> -<!-- See "Price" in grid is valid --> -<!-- See "Quantity" in grid is valid --> -<!-- Open Storefront Product Page and verify "Name", "SKU", "Price" --> -``` - -## The raw test - -This test works just fine. But it will only work if everything referenced in the test stays exactly the same. This neither reusable nor extensible. -Hardcoded selectors make it impossible to reuse sections in other action groups and tasks. They can also be brittle. If Magento happens to change a `class` or `id` on an element, the test will fail. - -Some data, like the SKU in this example, must be unique for every test run. Hardcoded values will fail here. [Data entities][] allow for `suffix` and `prefix` for ensuring unique data values. - -For our example, we have a test that creates a simple product. Note the hardcoded selectors, data values and lack of action groups. We will focus on the "product name". - -```xml -<?xml version="1.0" encoding="UTF-8"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="CreateSimpleProductTest"> - <annotations> - <features value="Catalog"/> - <stories value="Create Product"/> - <title value="Admin should be able to create simple product."/> - <description value="Admin should be able to create simple product."/> - <severity value="MAJOR"/> - <group value="Catalog"/> - <group value="alex" /> - </annotations> - <before> - <!-- Login to Admin panel --> - <amOnPage url="admin" stepKey="openAdminPanelPage" /> - <fillField selector="#username" userInput="admin" stepKey="fillLoginField" /> - <fillField selector="#login" userInput="123123q" stepKey="fillPasswordField" /> - <click selector="#login-form .action-login" stepKey="clickLoginButton" /> - </before> - <after> - <!-- Logout from Admin panel --> - </after> - - <!-- Navigate to Catalog -> Products page (or just open by link) --> - <amOnPage url="admin/catalog/product/index" stepKey="openProductGridPage" /> - - <!-- Click "Add Product" button --> - <click selector="#add_new_product-button" stepKey="clickAddProductButton" /> - <waitForPageLoad stepKey="waitForNewProductPageOpened" /> - - <!-- Fill field "Name" with "Simple Product %unique_value%" --> - -----><fillField selector="input[name='product[name]']" userInput="Simple Product 12412431" stepKey="fillNameField" /> - - <!-- Fill field "SKU" with "simple_product_%unique_value%" --> - <fillField selector="input[name='product[sku]']" userInput="simple-product-12412431" stepKey="fillSKUField" /> - - <!-- Fill field "Price" with "500.00" --> - <fillField selector="input[name='product[price]']" userInput="500.00" stepKey="fillPriceField" /> - - <!-- Fill field "Quantity" with "100" --> - <fillField selector="input[name='product[quantity_and_stock_status][qty]']" userInput="100" stepKey="fillQtyField" /> - - <!-- Fill field "Weight" with "100" --> - <fillField selector="input[name='product[weight]']" userInput="100" stepKey="fillWeightField" /> - - ... - </test> -</tests> -``` - -## Extract the CSS selectors - -First we will extract the hardcoded CSS selector values into variables. -For instance: `input[name='product[name]']` becomes `{{AdminProductFormSection.productName}}`. -In this example `AdminProductFormSection` refers to the `<section>` in the XML file which contains an `<element>` node named `productName`. This element contains the value of the selector that was previously hardcoded: `input[name='product[name]']` - -```xml -<?xml version="1.0" encoding="UTF-8"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="CreateSimpleProductTest"> - <annotations> - <features value="Catalog"/> - <stories value="Create Product"/> - <title value="Admin should be able to create simple product."/> - <description value="Admin should be able to create simple product."/> - <severity value="MAJOR"/> - <group value="Catalog"/> - <group value="alex" /> - </annotations> - <before> - <before> - <!-- Login to Admin panel --> - <amOnPage url="admin" stepKey="openAdminPanelPage" /> - <fillField selector="#username" userInput="admin" stepKey="fillLoginField" /> - <fillField selector="#login" userInput="123123q" stepKey="fillPasswordField" /> - <click selector="#login-form .action-login" stepKey="clickLoginButton" /> - </before> - <after> - <!-- Logout from Admin panel --> - </after> - - <!-- Navigate to Catalog -> Products page (or just open by link) --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductGridPage" /> - - <!-- Click "Add Product" button --> - <click selector="{{AdminProductGridActionSection.addProductBtn}}" stepKey="clickAddProductButton" /> - <waitForPageLoad stepKey="waitForNewProductPageOpened" /> - - <!-- Fill field "Name" with "Simple Product %unique_value%" --> - -----><fillField selector="{{AdminProductFormSection.productName}}" userInput="Simple Product 12412431" stepKey="fillNameField" /> - <!-- Fill field "SKU" with "simple_product_%unique_value%" --> - <fillField selector="{{AdminProductFormSection.productSku}}" userInput="simple-product-12412431" stepKey="fillSKUField" /> - - <!-- Fill field "Price" with "500.00" --> - <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="500.00" stepKey="fillPriceField" /> - - <!-- Fill field "Quantity" with "100" --> - <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="100" stepKey="fillQtyField" /> - - <!-- Fill field "Weight" with "100" --> - <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="100" stepKey="fillWeightField" /> - ... - </test> -</tests> -``` - -## The section file - -We abstract these selector values to a file named `AdminProductFormSection.xml` which is kept in the `Section` folder. -Within this file, there can be multiple `<section>` nodes which contains data for different parts of the test. -Here we are interested in `<section name="AdminProductFormSection">`, where we are keeping our extracted values from above. - -```xml -<?xml version="1.0" encoding="UTF-8"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> ---> <section name="AdminProductFormSection"> - <element name="attributeSet" type="select" selector="div[data-index='attribute_set_id'] .admin__field-control"/> - <element name="attributeSetFilter" type="input" selector="div[data-index='attribute_set_id'] .admin__field-control input" timeout="30"/> - <element name="attributeSetFilterResult" type="input" selector="div[data-index='attribute_set_id'] .action-menu-item._last" timeout="30"/> - <element name="attributeSetFilterResultByName" type="text" selector="//label/span[text() = '{{var}}']" timeout="30" parameterized="true"/> - -----><element name="productName" type="input" selector="input[name='product[name]']"/> - <element name="RequiredNameIndicator" type="text" selector=" return window.getComputedStyle(document.querySelector('._required[data-index=name]>.admin__field-label span'), ':after').getPropertyValue('content');"/> - <element name="RequiredSkuIndicator" type="text" selector=" return window.getComputedStyle(document.querySelector('._required[data-index=sku]>.admin__field-label span'), ':after').getPropertyValue('content');"/> - <element name="productSku" type="input" selector="input[name='product[sku]']"/> - <element name="enableProductAttributeLabel" type="text" selector="//span[text()='Enable Product']/parent::label"/> - <element name="enableProductAttributeLabelWrapper" type="text" selector="//span[text()='Enable Product']/parent::label/parent::div"/> - <element name="productStatus" type="checkbox" selector="input[name='product[status]']"/> - ... - </section> - <section name="ProductInWebsitesSection"> - <element name="sectionHeader" type="button" selector="div[data-index='websites']" timeout="30"/> - <element name="website" type="checkbox" selector="//label[contains(text(), '{{var1}}')]/parent::div//input[@type='checkbox']" parameterized="true"/> - </section> -``` - -## Data entities - -The hardcoded values of these form elements are abstracted to a "data entity" XML file. -We replace the hardcoded values with variables and the MFTF will do the variable substitution. - -```xml -<?xml version="1.0" encoding="UTF-8"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="CreateSimpleProductTest"> - <annotations> - <features value="Catalog"/> - <stories value="Create Product"/> - <title value="Admin should be able to create simple product."/> - <description value="Admin should be able to create simple product."/> - <severity value="MAJOR"/> - <group value="Catalog"/> - <group value="alex" /> - </annotations> - <before> - <!-- Login to Admin panel --> - <amOnPage url="admin" stepKey="openAdminPanelPage" /> - <fillField selector="#username" userInput="admin" stepKey="fillLoginField" /> - <fillField selector="#login" userInput="123123q" stepKey="fillPasswordField" /> - <click selector="#login-form .action-login" stepKey="clickLoginButton" /> - </before> - <after> - <!-- Logout from Admin panel --> - </after> - - <!-- Navigate to Catalog -> Products page (or just open by link) --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductGridPage" /> - - <!-- Click "Add Product" button --> - <click selector="{{AdminProductGridActionSection.addProductBtn}}" stepKey="clickAddProductButton" /> - <waitForPageLoad stepKey="waitForNewProductPageOpened" /> - - <!-- Fill field "Name" with "Simple Product %unique_value%" --> - ----><fillField selector="{{AdminProductFormSection.productName}}" userInput="{{_defaultProduct.name}}" stepKey="fillNameField" /> - - <!-- Fill field "SKU" with "simple_product_%unique_value%" --> - <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{_defaultProduct.sku}}" stepKey="fillSKUField" /> - - <!-- Fill field "Price" with "500.00" --> - <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{_defaultProduct.price}}" stepKey="fillPriceField" /> - - <!-- Fill field "Quantity" with "100" --> - <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{_defaultProduct.quantity}}" stepKey="fillQtyField" /> - - <!-- Fill field "Weight" with "100" --> - <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{_defaultProduct.weight}}" stepKey="fillWeightField" /> - - ... - </test> -</tests> -``` - -One of the reasons that we abstract things is so that they are more flexible and reusable. In this case, we can leverage this flexibility and use an existing data file. For this scenario, we are using [this file](https://raw.githubusercontent.com/magento-pangolin/magento2/MageTestFest/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml). - -Data entities are important because this is where a `suffix` or `prefix` can be defined. This ensures that data values can be unique for every test run. - -Notice that the `<entity>` name is `_defaultProduct` as referenced above. Within this entity is the `name` value. - -```xml -<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> - <entity name="_defaultProduct" type="product"> - <data key="sku" unique="suffix">testSku</data> - <data key="type_id">simple</data> - <data key="attribute_set_id">4</data> - <data key="visibility">4</data> - ---> <data key="name" unique="suffix">testProductName</data> - <data key="price">123.00</data> - <data key="urlKey" unique="suffix">testurlkey</data> - <data key="status">1</data> - <data key="quantity">100</data> - <data key="weight">1</data> - <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> - <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> - </entity> -``` - -The `unique="suffix"` attribute appends a random numeric string to the end of the actual data string. This ensures that unique values are used for each test run. -See [Input testing data][] for more information. - -## Convert actions to action groups - -Action groups are sets of steps that are run together. Action groups are designed to break up multiple individual steps into logical groups. For example: logging into the admin panel requires ensuring the login form exists, filling in two form fields and clicking the **Submit** button. These can be bundled into a single, reusable "LoginAsAdmin" action group that can be applied to any other test. This leverages existing code and prevents duplication of effort. We recommend that all steps in a test be within an action group. - -Using action groups can be very useful when testing extensions. -Extending the example above, assume the first extension adds a new field to the admin log in, a Captcha for example. -The second extension we are testing needs to log in AND get past the Captcha. - -1. The admin login is encapsulated in an action group. -2. The Captcha extension properly extends the `LoginAsAdmin` capture group using the `merge` functionality. -3. Now the second extension can call the `LoginAsAdmin` action group and because of the `merge`, it will automatically include the Captcha field. - -In this case, the action group is both reusable and extensible! - -We further abstract the test by putting these action groups in their own file: ['app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml'](https://raw.githubusercontent.com/magento-pangolin/magento2/e5671d84aa63cad772fbba757005b3d89ddb79d9/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductActionGroup.xml) - -To create an action group, take the steps and put them within an `<actionGroup>` element. Note that the `<argument>` node defines the `_defaultProduct` data entity that is required for the action group. - -```xml -<?xml version="1.0" encoding="UTF-8"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> -<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> -<!--Fill main fields in create product form--> - <actionGroup name="fillMainProductForm"> - <arguments> - <argument name="product" defaultValue="_defaultProduct"/> - </arguments> - -----><fillField selector="{{AdminProductFormSection.productName}}" userInput="{{product.name}}" stepKey="fillProductName"/> - <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{product.sku}}" stepKey="fillProductSku"/> - <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{product.price}}" stepKey="fillProductPrice"/> - <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{product.quantity}}" stepKey="fillProductQty"/> - <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{product.status}}" stepKey="selectStockStatus"/> - <selectOption selector="{{AdminProductFormSection.productWeightSelect}}" userInput="This item has weight" stepKey="selectWeight"/> - <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{product.weight}}" stepKey="fillProductWeight"/> - <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> - <fillField userInput="{{product.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> - </actionGroup> -``` - -Note how the `<argument>` node takes in the `_defaultProduct` data entity and renames it to `product`, which is then used for the `userInput` values. - -Now we can reference this action group within our test (and any other test). - -```xml -<?xml version="1.0" encoding="UTF-8"?> -<!-- -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="CreateSimpleProductTest"> - <annotations> - <features value="Catalog"/> - <stories value="Create Product"/> - <title value="Admin should be able to create simple product."/> - <description value="Admin should be able to create simple product."/> - <severity value="MAJOR"/> - <group value="Catalog"/> - <group value="alex" /> - </annotations> - <before> - <!-- Login to Admin panel --> - <actionGroup ref="LoginAsAdmin" stepKey="loginToAdminPanel" /> - </before> - <after> - <!-- Logout from Admin panel --> - </after> - - <!-- Navigate to Catalog -> Products page (or just open by link) --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductGridPage" /> - <waitForPageLoad stepKey="waitForProductGridPageLoaded" /> - - <!-- Click "Add Product" button --> - <actionGroup ref="goToCreateProductPage" stepKey="goToCreateProductPage" /> - -----><fillField selector="{{AdminProductFormSection.productName}}" userInput="{{product.name}}" stepKey="fillProductName"/> - <actionGroup ref="fillMainProductForm" stepKey="fillProductForm"> - <argument name="product" value="_defaultProduct" /> - </actionGroup> - - <!-- See success save message "You saved the product." --> - <actionGroup ref="saveProductForm" stepKey="clickSaveOnProductForm" /> - - <actionGroup ref="AssertProductInGridActionGroup" stepKey="assertProductInGrid" /> - - <!-- Open Storefront Product Page and verify "Name", "SKU", "Price" --> - <actionGroup ref="AssertProductInStorefrontProductPage" stepKey="assertProductInStorefrontProductPage"> - <argument name="product" value="_defaultProduct" /> - </actionGroup> - </test> -</tests> -``` - -A well written test will end up being a set of action groups. -The finished test is fully abstracted in such a way that it is short and readable and importantly, the abstracted data and action groups can be used again. - -<!-- Link Definitions --> -[actions]: https://devdocs.magento.com/mftf/docs/test/actions.html -[action groups]: https://devdocs.magento.com/mftf/docs/test/action-groups.html -[Data entities]: https://devdocs.magento.com/mftf/docs/data.html -[Input testing data]: https://devdocs.magento.com/mftf/docs/data.html -[parameterized selectors]: https://devdocs.magento.com/mftf/docs/section/parameterized-selectors.html diff --git a/docs/test.md b/docs/test.md deleted file mode 100644 index 444f6cf6a..000000000 --- a/docs/test.md +++ /dev/null @@ -1,146 +0,0 @@ -# Test - -Test cases in the Magento Functional Testing Framework (MFTF) are defined in XML as [`<tests>`]. -`<tests>` is a [Codeception test container][Codeception] that contains multiple individual tests with test metadata and before and after actions. - -MFTF `<tests>` is considered a sequence of actions with associated parameters. -Any failed [assertion] within a test constitutes a failed test. - -<div class="bs-callout bs-callout-info" markdown="1"> - `<before>` and `<after>` hooks are not global within `<tests>`. -They only apply to the `<test>` in which they are declared. -The steps in `<after>` are run in both successful **and** failed test runs. -</div> - -The following diagram shows the structure of an MFTF test case: - -![Structure of MFTF test case](img/test-dia.svg) - -## Format - -The format of `<tests>` is: - -```xml -<?xml version="1.0" encoding="UTF-8"?> - -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="" insertBefore="" insertAfter=""> - <annotations> - <!-- TEST ANNOTATIONS --> - </annotations> - <before> - <!-- ACTIONS AND ACTION GROUPS PERFORMED BEFORE THE TEST --> - </before> - <after> - <!-- ACTIONS AND ACTION GROUPS PERFORMED AFTER THE TEST --> - </after> - <!-- TEST ACTIONS, ACTION GROUPS, AND ASSERTIONS--> - </test> -</tests> -``` - -## Principles - -The following conventions apply to MFTF tests: - -* All names within the framework are in the CamelCase format. -* `<test>` name must be alphanumeric. -* Each action and action group has its own identifier `<stepKey>` for reference purposes. -* A test may have any number of [assertions][assertion] at any point within the `<test>`. -* If `<test>` is included in `<suite>`, it **cannot be generated in isolation** to the rest of the contents of the suite (see [suites] for details). -* One `<test>` tag is allowed per test XML file. - -## Elements reference - -There are several XML elements that are used in `<tests>` in the MFTF. - -### tests {#tests-tag} - -`<tests>` is a container for multiple tests. It is a group of test methods that define test flows within a test case. - -`<tests>` must contain at least one [`<test>`]. - -### test {#test-tag} - -`<test>` is a set of steps, including [actions] and [assertions][assertion]. It is a sequence of test steps that define test flow within a test method. - -Attribute|Type|Use|Description ----|---|---|--- -`name`|string|optional|The test identifier. -`remove`|boolean|optional|Set `true` to remove the test when merging. -`insertBefore`|string|optional| This option is used for [merging]. It enables you to add all test actions contained in the original test into a test with the same name BEFORE the test step with `stepKey` that you assigned in `insertBefore`. -`insertAfter`|string|optional| Set `stepKey` of the test step after which you want to insert the test when [merging]. -`deprecated`|string|optional|Used to warn about the future deprecation of the test. String will appear in Allure reports and console output at runtime. -`extends`|string|optional|A name of the parent test to [extend]. - -`<test>` may also contain [`<annotations>`], [`<before>`], [`<after>`], any [action][actions], or [`<actionGroup>`]. - -### annotations {#annotations-tag} - -[Annotations] are supported by both [Codeception] and [Allure]. - -Codeception annotations typically provide metadata and are able to influence test selection. -Allure annotations provide metadata for reporting. - -### before {#before-tag} - -`<before>` wraps the steps to perform before the [`<test>`]. - -`<before>` may contain these child elements: - -* Any [`<action>`][actions] -* [`<actionGroup>`] - -### after {#after-tag} - -`<after>` wraps the steps to perform after the [`<test>`]. -The steps are run in both successful **and** failed test runs. - -`<after>` may contain: - -* Any [`<action>`][actions] -* [`<actionGroup>`] - -### actionGroup {#actiongroup-tag} - -`<actionGroup>` calls a corresponding [action group]. - -Attribute|Type|Use|Description ----|---|---|--- -`ref`|string|required|References the required action group by its `name`. -`stepKey`|string|required| Identifies the element within `<test>`. -`before`|string|optional| `<stepKey>` of an action or action group that must be executed next while merging. -`after`|string|optional| `<stepKey>` of an action or action group that must be executed one step before the current one while merging. - -`<actionGroup>` may contain [`<argument>`]. - -### argument {#argument-tag} - -`<argument>` sets an argument that is used in the parent [`<actionGroup>`]. - -Attribute|Type|Use ----|---|--- -`name`|string|optional| Name of the argument. -`value`|string|optional| Value of the argument. - -See [Action groups][action group] for more information. - -<!-- Link definitions --> - -[`<actionGroup>`]: #actiongroup-tag -[`<after>`]: #after-tag -[`<annotations>`]: #annotations-tag -[`<argument>`]: #argument-tag -[`<before>`]: #before-tag -[`<test>`]: #test-tag -[`<tests>`]: #tests-tag -[action group]: ./test/action-groups.md -[actions]: ./test/actions.md -[Allure]: https://github.com/allure-framework/ -[Annotations]: ./test/annotations.md -[assertion]: ./test/assertions.md -[Codeception]: https://codeception.com/docs/07-AdvancedUsage -[extend]: extending.md -[merging]: ./merging.md#insert-after -[suites]: ./suite.md diff --git a/docs/test/action-groups.md b/docs/test/action-groups.md deleted file mode 100644 index 70af0621a..000000000 --- a/docs/test/action-groups.md +++ /dev/null @@ -1,278 +0,0 @@ -# Action groups - -In the MFTF, you can re-use a group of [actions][], such as logging in as an administrator or a customer, declared in an XML file when you need to perform the same sequence of actions multiple times. - -The following diagram shows the structure of an MFTF action group: - -![Structure of MFTF action group](../img/action-groups-dia.svg) - -## Principles - -{% raw %} - -The following conventions apply to MFTF action groups: - -- All action groups are declared in XML files and stored in the `<module>/Test/Mftf/ActionGroup/` directory. -- Every file name ends with `ActionGroup` suffix. For exampe `LoginAsAdminActionGroup.xml`. -- Action group name should be the same as file name without extension. -- One `<actionGroup>` tag is allowed per action group XML file. - -The XML format for the `actionGroups` declaration is: - -```xml -<?xml version="1.0" encoding="UTF-8"?> - -<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name=""> - <arguments> - <argument name=""/> - <argument name="" defaultValue=""/> - <argument name="" defaultValue="" type=""/> - </arguments> - </actionGroup> -</actionGroups> -``` - -## Example - -These examples build a declaration for a group of actions that grant authorization to the Admin area, and use the declaration in a test. - -The _Magento/Backend/Test/Mftf/ActionGroup/LoginAsAdminActionGroup.xml_ `<actionGroup>` relates to the functionality of the _Magento_Backend_ module. - -In [test][], the name and identifier of the `<actionGroup>` is used as a reference in the `ref` parameter, such as `ref="LoginAsAdminActionGroup"`. - -### Create an action group declaration - -To create the `<actionGroup>` declaration: - -1. Begin with a template for the `<actionGroup>`: - - ```xml - <?xml version="1.0" encoding="UTF-8"?> - - <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="{Action Group Name}"> - - </actionGroup> - </actionGroups> - ``` - -1. Add actions to the `actionGroup` arguments: - - ```xml - <actionGroup name="LoginAsAdminActionGroup"> - <fillField stepKey="fillUsername" selector="#username" userInput="{{adminUser.username}}" /> - <fillField stepKey="fillPassword" selector="#password" userInput="{{adminUser.password}}" /> - <click stepKey="click" selector="#login" /> - </actionGroup> - ``` - -1. The `userInput` variable must contain a data value for test. - Add a default data value for the variable to use in the most common cases. - For this example, the default value is `_defaultAdmin`. - - ```xml - <argument name="adminUser" defaultValue="_defaultAdmin"/> - ``` - -1. The following example shows the complete declaration: - - ```xml - <?xml version="1.0" encoding="UTF-8"?> - - <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="LoginAsAdmin"> - <annotations> - <description>Login to Backend Admin using provided User Data. PLEASE NOTE: This Action Group does NOT validate that you are Logged In.</description> - </annotations> - <arguments> - <argument name="adminUser" type="entity" defaultValue="DefaultAdminUser"/> - </arguments> - - <amOnPage url="{{AdminLoginPage.url}}" stepKey="navigateToAdmin"/> - <fillField selector="{{AdminLoginFormSection.username}}" userInput="{{adminUser.username}}" stepKey="fillUsername"/> - <fillField selector="{{AdminLoginFormSection.password}}" userInput="{{adminUser.password}}" stepKey="fillPassword"/> - <click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> - <closeAdminNotification stepKey="closeAdminNotification"/> - </actionGroup> - </actionGroups> - ``` - -### Use the declaration in a test - -In this test example, we want to add the following set of actions: - -```xml -<fillField selector="{{AdminLoginFormSection.username}}" userInput="{{adminUser.username}}" stepKey="fillUsername"/> -<fillField selector="{{AdminLoginFormSection.password}}" userInput="{{adminUser.password}}" stepKey="fillPassword"/> -<click selector="{{AdminLoginFormSection.signIn}}" stepKey="clickLogin"/> -``` - -Instead of adding this set of actions, use the _LoginAsAdminActionGroup_ `<actionGroup>` declaration in tests: - -1. Reference the `LoginAsAdminActionGroup` action group: - - ```xml - <actionGroup stepKey="loginToAdminPanel" ref="LoginAsAdminActionGroup"/> - ``` - -1. Update the argument name/value pair to `adminUser` and `CustomAdminUser`: - - ```xml - <actionGroup stepKey="loginToAdminPanel" ref="LoginAsAdminActionGroup"> - <argument name="adminUser" value="CustomAdminUser"/> - </actionGroup> - ``` - -## Data type usage - -By default, an [`argument`][] expects an entire `entity` when the `type` value is not defined. -There are cases when you use a string instead of a whole entity. - -For example, the following defines the replacement argument `relevantString` using a primitive data type: - -```xml -<actionGroup name="fillExample"> - <arguments> - <argument name="relevantString" defaultValue="defaultString" type="string"/> - </arguments> - <fillField stepKey="fillField1" selector="#input" userInput="{{relevantString}}"/> - <click stepKey="clickSave" selector="#save"/> - <see stepKey="seeItWorked" selector="#outputArea" userInput="{{relevantString}}"/> - <click stepKey="clickParameterizedSelector" selector="{{SomeSection.parameterizedElement(relevantString)}}"/> -</actionGroup> -``` - -The `string` argument type provides a method to pass a single piece of data to the `<actionGroup>`during a test instead of passing an entire entity. - -### Explicitly define the argument value - -```xml -<actionGroup stepKey="fillWithStringLiteral" ref="fillExample"> - <argument name="relevantString" value="overrideString"/> -</actionGroup> -``` - -### Use persisted data references to define the argument value - -```xml -<actionGroup stepKey="fillWithStringLiteral" ref="fillExample"> - <argument name="relevantString" value="$persistedData.field1$"/> -</actionGroup> -``` - -The `relevantString` argument value points to the data [created][] in the `stepKey="persistedData"` test step. -`field1` is a data key of the required data string. -Even with the `persistedData` data entity, the MFTF interprets the `$persistedData.field1$` value as a string. - -### Define the argument value based on data entity resolution - -The argument value points to a piece of data defined in a `data.xml` file. -The `field1` data contains the required string. -MFTF resolves `{{myCustomEntity.field1}}` the same as it would in a `selector` or `userInput` attribute. - -```xml -<actionGroup stepKey="fillWithXmlData" ref="fillExample"> - <argument name="relevantString" value="{{myCustomEntity.field1}}"/> -</actionGroup> -``` - -## Optimizing action group structures - -Structuring properly an action group increases code reusability and readability. - -Starting with an action group such as: - -```xml -<actionGroup name="CreateCategory"> - <arguments> - <argument name="categoryEntity" defaultValue="_defaultCategory"/> - </arguments> - <seeInCurrentUrl url="{{AdminCategoryPage.url}}" stepKey="seeOnCategoryPage"/> - <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategory"/> - <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Category" stepKey="seeCategoryPageTitle"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{categoryEntity.name}}" stepKey="enterCategoryName"/> - <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSEO"/> - <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{categoryEntity.name_lwr}}" stepKey="enterURLKey"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> - <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccess"/> - <seeInTitle userInput="{{categoryEntity.name}}" stepKey="seeNewCategoryPageTitle"/> - <seeElement selector="{{AdminCategorySidebarTreeSection.categoryInTree(categoryEntity.name)}}" stepKey="seeCategoryInTree"/> -</actionGroup> -``` - -It can be reworked into more manageable pieces, as below. These smaller steps are easier to read, update, and reuse. -* GoToCategoryGridAndAddNewCategory - ```xml - <actionGroup name="GoToCategoryGridAndAddNewCategory"> - <seeInCurrentUrl url="{{AdminCategoryPage.url}}" stepKey="seeOnCategoryPage"/> - <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategory"/> - <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Category" stepKey="seeCategoryPageTitle"/> - </actionGroup> - ``` -* FillInBasicCategoryFields - ```xml - <actionGroup name="FillInBasicCategoryFields"> - <arguments> - <argument name="categoryEntity" defaultValue="_defaultCategory"/> - </arguments> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{categoryEntity.name}}" stepKey="enterCategoryName"/> - <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSEO"/> - <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{categoryEntity.name_lwr}}" stepKey="enterURLKey"/> - </actionGroup> - ``` -* SaveAndVerifyCategoryCreation - ```xml - <actionGroup name="SaveAndVerifyCategoryCreation"> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> - <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccess"/> - <seeInTitle userInput="{{categoryEntity.name}}" stepKey="seeNewCategoryPageTitle"/> - <seeElement selector="{{AdminCategorySidebarTreeSection.categoryInTree(categoryEntity.name)}}" stepKey="seeCategoryInTree"/> - </actionGroup> - ``` - -<!-- {% endraw %} --> - -## Elements reference - -### actionGroups {#actiongroups-tag} - -The `<actionGroups>` element is a root element that contains XML configuration attributes. - -Attribute|Value|Description ----|---|--- -`xmlns:xsi`|`"http://www.w3.org/2001/XMLSchema-instance"`|Tells the XML parser to validate this document against a schema. -`xsi:noNamespaceSchemaLocation`|`"urn:magento:mftf:Test/etc/actionGroupSchema.xsd"`|Relative path to the corresponding schema. - -It may contain one or more `<actionGroup>`. - -### actionGroup {#actiongroup-tag} - -Attribute|Type|Use|Description ----|---|---|--- -`name`|string|required|Identifier of the action group. -`extends`|string|optional|Identifies the action group to extend. -`deprecated`|string|optional|Used to warn about the future deprecation of the actionGroup. String will appear in Allure reports and console output at runtime. - -It may contain `<arguments>`. - -### arguments {#arguments-tag} - -The `<arguments>` element is a wrapper for an array of `<argument>` elements. - -### argument {#argument-tag} - -Attribute|Type|Use|Description ----|---|---|--- -`name`|string|required|Identifier of an argument in the scope of the corresponding action group. -`defaultValue`|string|optional|Provides a default data value. -`type`|Possible values: `string`, `entity` (default).|optional|Defines the argument data type; Defaults to `entity`. - -<!-- Link Definitions --> -[actions]: ./actions.md -[test]: ../test.md -[`argument`]: #argument-tag -[created]: ../data.md#persist-data diff --git a/docs/test/actions.md b/docs/test/actions.md deleted file mode 100644 index 9cf3d43f6..000000000 --- a/docs/test/actions.md +++ /dev/null @@ -1,2465 +0,0 @@ -# Test actions - -Actions in the MFTF allow you to automate different scenarios of Magento user's actions. -They are mostly XML implementations of [Codeception actions](http://codeception.com/docs/modules/WebDriver#Actions). -Some actions drive browser elements, while others use REST APIs. - -## Common attributes - -All `<actions>` contain the following attributes that are useful for merging needs. - -### `stepKey` - -`stepKey` is a required attribute that stores a unique identifier of the action. - -Example test step of the `myAction` action with the `conditionalClickStep1` identifier: - -```xml -<myAction stepKey="conditionalClickStep1"/> -``` - -This step can be referenced within the test using `conditionalClickStep1`. - -The value format should met the following principles: - -* Must be unique within [`<test>`](../test.md#test-tag). -* Naming should be as descriptive as possible: - * Describe the action performed. - * Briefly describe the purpose. - * Describe which data is in use. -* Should be in camelCase with lowercase first letter. -* Should be the last attribute of an element. - -### `before` and `after` - -`before` and `after` are optional attributes that insert the action into the test while merging. The action will be executed before or after the one set in these attributes. The value here is the `stepKey` of reference action. - -Example with `before`: - -```xml -<myAction before="fillField" stepKey="conditionalClickStep1"/> -``` - -`myAction` will be executed before the action, which has `stepKey="fillField"`. - -Example with `after`: - -```xml -<myAction after="fillField" stepKey="seeResult"/> -``` - -`myAction` will be executed after the action, which has `stepKey="fillField"`. - -## Examples - -<!-- {% raw %} --> - -The following example contains four actions: - -1. [Open the Sign In page for a Customer](#example-step1). -2. [Enter a customer's email](#example-step2). -3. [Enter a customer's password](#example-step3). -4. [Click the Sign In button](#example-step4). - - ```xml - <amOnPage url="{{StorefrontCustomerSignInPage.url}}" stepKey="amOnSignInPage"/> - <fillField userInput="$$customer.email$$" selector="{{StorefrontCustomerSignInFormSection.emailField}}" stepKey="fillEmail"/> - <fillField userInput="$$customer.password$$" selector="{{StorefrontCustomerSignInFormSection.passwordField}}" stepKey="fillPassword"/> - <click selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}" stepKey="clickSignInAccountButton"/> - ``` - -### 1. Open the Sign In page for a customer {#example-step1} - -```xml -<amOnPage url="{{StorefrontCustomerSignInPage.url}}" stepKey="amOnSignInPage"/> -``` - -The Customer Sign In page is declared in the `.../Customer/Page/StorefrontCustomerSignInPage.xml` file. -The given relative URI is declared in `StorefrontCustomerSignInPage.url`. - -Source code (`StorefrontCustomerSignInPage.xml` ): - -```xml -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> - <page name="StorefrontCustomerSignInPage" url="/customer/account/login/" module="Magento_Customer"> - <section name="StorefrontCustomerSignInFormSection" /> - </page> -</config> -``` - -[`<amOnPage>`](#amonpage) is an action that opens a page for a given URI. It has a key `"amOnSignInPage"` that will be used as a reference for merging needs in other modules. -This action uses the `url` attribute value for the given relative URI to open a browser page. -Here, `url` contains a pointer to a `url` attribute of the `StorefrontCustomerSignInPage`. - -### 2. Enter a customer's email {#example-step2} - -```xml -<fillField userInput="$$customer.email$$" selector="{{StorefrontCustomerSignInFormSection.emailField}}" stepKey="fillEmail"/> -``` - -[`<fillField>`](#fillfield) fills a text field with the given string. - -The customer's email is stored in the `email` parameter of the `customer` entity created somewhere earlier in the test using a [`<createData>`](#createdata) tag. -`userInput` points to that data. - -`selector` points to the field where you enter the data. -A required selector is stored in the `emailField` element of the `StorefrontCustomerSignInFormSection` section. - -This section is declared in `.../Customer/Section/StorefrontCustomerSignInFormSection.xml` file: - -```xml -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> - <section name="StorefrontCustomerSignInFormSection"> - <element name="emailField" type="input" selector="#email"/> - <element name="passwordField" type="input" selector="#pass"/> - <element name="signInAccountButton" type="button" selector="#send2" timeout="30"/> - </section> -</config> -``` - -### 3. Enter a customer's password {#example-step3} - -```xml -<fillField userInput="$$customer.password$$" selector="{{StorefrontCustomerSignInFormSection.passwordField}}" stepKey="fillPassword"/> -``` - -This `<action>` is very similar to the `<action>` in a previous step. -The only difference is that different data is assigned to the attributes, which set a field with a password. - -### 4. Click the Sign In button {#example-step4} - -```xml -<click selector="{{StorefrontCustomerSignInFormSection.signInAccountButton}}" stepKey="clickSignInAccountButton"/> -``` - -<!-- {% endraw %} --> - -Here, [`<click>`](#click) performs a click on a button that can be found by the selector that is stored in the `signInAccountButton` of the `StorefrontCustomerSignInFormSection`. - -## Actions returning a variable - -The following test actions return a variable: - -* [grabAttributeFrom](#grabattributefrom) -* [grabCookie](#grabcookie) -* [grabFromCurrentUrl](#grabfromcurrenturl) -* [grabMultiple](#grabmultiple) -* [grabPageSource](#grabpagesource) -* [grabTextFrom](#grabtextfrom) -* [grabValueFrom](#grabvaluefrom) -* [executeJS](#executejs) - -Learn more in [Using data returned by test actions](../data.md#use-data-returned-by-test-actions). - -## Actions handling data entities - -The following test actions handle data entities using [metadata](../metadata.md): - -* [createData](#createdata) -* [deleteData](#deletedata) -* [updateData](#updatedata) -* [getData](#getdata) - -Learn more in [Handling a REST API response](../metadata.md#rest-response). - -## Actions specifying HTML values - -To use HTML in actions you must encode the HTML string. We recommend using [CyberChef](https://gchq.github.io/CyberChef/#recipe=To_HTML_Entity(false,'Numeric%20entities')). Using CyberChef or a similar tool is straightforward: enter in your HTML string, copy the encoded result, and paste that value into your MFTF test. - -For example, we want to ensure that this value is presented as a string and not rendered as a H1 tag: `<h1 class="login-header">` - -After passing `<h1 class="login-header">` through CyberChef we get `<h1 class="login-header">` which can be used in a test like: - -```xml -<dontSeeInSource html="<h1 class="login-header">" stepKey="dontSeeInSource"/> -``` - -## Reference - -The following list contains reference documentation about all action elements available in the MFTF. -If the description of an element does not include a link to Codeception analogue, it means that the action is developed by Magento for specific MFTF needs. - -### acceptPopup - -Accepts the current popup visible on the page. - -See [acceptPopup docs on codeception.com](http://codeception.com/docs/modules/WebDriver#acceptPopup). - -Attribute|Type|Use|Description ----|---|---|--- -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Accept the current popup visible on the page. --> -<acceptPopup stepKey="acceptPopup"/> -``` - -### amOnPage - -Opens the page by the URL relative to the one set in the `MAGENTO_BASE_URL` configuration variable. - -See [amOnPage docs on codeception.com](http://codeception.com/docs/modules/WebDriver#amOnPage). - -Attribute|Type|Use|Description ----|---|---|--- -`url`|string|optional| A path to the page relative to the `MAGENTO_BASE_URL`. -`stepKey`|string|required|A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Open the `(baseURL)/admin` page. --> -<amOnPage url="/admin" stepKey="goToLogoutPage"/> -``` - -### amOnSubdomain - -Takes the base URL and changes the subdomain. - -See [amOnSubdomain docs on codeception.com](http://codeception.com/docs/modules/WebDriver#amOnSubdomain). - -Attribute|Type|Use|Description ----|---|---|--- -`url`|string|optional| The name of the subdomain. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -Pre-condition: the current base URL is `https://www.magento.com`. - -```xml -<!-- Change the sub-domain to `https://devdocs.magento.com`. --> -<amOnSubdomain url="devdocs" stepKey="changeSubdomain"/> -<!-- Open the page `https://devdocs.magento.com` --> -<amOnPage url="/" stepKey="goToDataPage"/> -``` - -### amOnUrl - -Opens a page by the absolute URL. - -See [amOnUrl docs on codeception.com](http://codeception.com/docs/modules/WebDriver#amOnUrl). - -Attribute|Type|Use|Description ----|---|---|--- -`url`|string|optional| The absolute URL to be used in subsequent steps. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Set url to be used in the next steps to https://www.magento.com/ --> -<amOnUrl url="https://www.magento.com/" stepKey="amOnUrl"/> -``` - -### appendField - -See [appendField docs on codeception.com](http://codeception.com/docs/modules/WebDriver#appendField). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector used to identify the form field. -`userInput`|string|optional| Value to append to the form field. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Append the "Sample Text" string to the selected input element --> -<appendField userInput="Sample Text" selector="input#name" stepKey="appendSuffix"/> -``` - -### attachFile - -See [attachFile docs on codeception.com](http://codeception.com/docs/modules/WebDriver#attachFile). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional|The selector identifying the corresponding HTML element (`<input type="file">`). -`userInput`|string|optional|The name of attaching file. The file must be placed in the `tests/_data` directory. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Upload a file from the `tests/_data` directory with the `image.png` name to the selected input element. --> -<attachFile userInput="image.png" selector="input#imgUpload" stepKey="uploadFile"/> -``` - -### cancelPopup - -See [cancelPopup docs on codeception.com](http://codeception.com/docs/modules/WebDriver#cancelPopup). - -Attribute|Type|Use|Description ----|---|---|--- -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Cancel the current popup visible on the page. --> -<cancelPopup stepKey="cancelPopup"/> -``` - -### checkOption - -See [checkOption docs on codeception.com](http://codeception.com/docs/modules/WebDriver#checkOption). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Ensure the checkbox `<input type="checkbox" id="checkbox" ... >...</input>` is checked. --> -<checkOption selector="input#checkbox" stepKey="checkCheckbox"/> -``` - -### clearField - -Clears a text input field. -Equivalent to using [`<fillField>`](#fillfield) with an empty string. - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|required| The selector identifying the corresponding HTML element to be cleared. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Clear the selected field. --> -<clearField selector="input#name" stepKey="clearField"/> -``` - -### click - -See [click docs on codeception.com](http://codeception.com/docs/modules/WebDriver#click). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`selectorArray`|string|optional| Selects an element as a key value array. See [strict locator](http://codeception.com/docs/modules/WebDriver#locating-elements). -`userInput`|string|optional| Data to be sent with the click. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Click the selected button. --> -<click selector="button#clickable" stepKey="clickButton"/> -``` - -```xml -<!-- Click on the "<a href=...>Login</a>" link. --> -<click selectorArray="['link' => 'Login']" stepKey="clickButton2"/> -``` - -### clickWithLeftButton - -See [clickWithLeftButton docs on codeception.com](http://codeception.com/docs/modules/WebDriver#clickWithLeftButton). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`selectorArray`|string|optional| Selects an element as a key value array; See [strict locator]. -`x`|string|optional| The x-axis value in pixels for the click location. -`y`|string|optional| The y-axis value in pixels for the click location. -`stepKey`|string|required|A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Examples - -```xml -<!-- Left click on the center of the `<button id="clickable" />` element. --> -<clickWithLeftButton selector="button#clickable" stepKey="clickButton1"/> -``` - -```xml -<!-- Left click on the point that is 50 px from the top of the window and 50 px from the left of the window. --> -<clickWithLeftButton x="50" y="50" stepKey="clickButton2"/> -``` - -```xml -<!-- Left click on the point that is 50 px from the top and 50 px from the left of of the `<button id="clickable" />` element.. --> -<clickWithLeftButton selector="button#clickable" x="50" y="50" stepKey="clickButton3"/> -``` - -### clickWithRightButton - -See [clickWithRightButton docs on codeception.com](http://codeception.com/docs/modules/WebDriver#clickWithRightButton). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`selectorArray`|string|optional| Selects an element as a key value array; See [strict locator]. -`x`|string|optional| The x-axis value in pixels for the click location. -`y`|string|optional| The y-axis value in pixels for the click location. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Examples - -```xml -<!-- Right click on the center of the `<button id="clickable" />` element. --> -<clickWithRightButton selector="button#clickable" stepKey="clickButton1"/> -``` - -```xml -<!-- Right click on the point that is 50 px from the top of the window and 50 px from the left of the window. --> -<clickWithRightButton x="50" y="50" stepKey="clickButton2"/> -``` - -```xml -<!-- Right click on the point that is 50 px from the top and 50 px from the left of of the `<button id="clickable" />` element.. --> -<clickWithRightButton selector="button#clickable" x="50" y="50" stepKey="clickButton3"/> -``` - -### closeAdminNotification - -Remove from the DOM all elements with the CSS classes `.modal-popup` or `.modals-overlay`. - -Attribute|Type|Use|Description ----|---|---|--- -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Remove elements of the `.modal-popup` or `.modals-overlay` CSS classes. --> -<closeAdminNotification stepKey="closeAdminNotification"/> -``` - -### closeTab - -See [closeTab docs on codeception.com](http://codeception.com/docs/modules/WebDriver#closeTab). - -Attribute|Type|Use|Description ----|---|---|--- -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Close the active tab. --> -<closeTab stepKey="closeTab"/> -``` - -### comment - -Allows input of a string as a PHP code comment. -This tag is not executed. -It is intended to aid documentation and clarity of tests. - -Attribute|Type|Use|Description ----|---|---|--- -`userInput`|string|required| PHP comment that will be written in generated test file. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -```xml -<!-- Open the specified page and print a comment "I am on the login page" in the log during test execution. --> -<amOnPage url="/login" stepKey="goToLoginPage"/> -<comment userInput="I am on the login page" stepKey="loginPageComment"/> -``` - -### conditionalClick - -Conditionally clicks on an element if, and only if, another element is visible or not. - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the HTML element to be clicked. -`dependentSelector`|string|optional| The selector of the HTML element whose visibility is checked for to activate the click. -`visible`|boolean|optional| Determines whether the conditional click is activated by the element being visible or hidden. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Examples - -```xml -<!-- Click on the element with `id="foo"` if the element with `id="bar"` is visible. --> -<conditionalClick selector="#foo" dependentSelector="#bar" visible="true" stepKey="click1"/> -``` - -### createData - -Creates an entity (for example, a category or product). -To create an entity, the MFTF makes a `POST` request to the Magento API according to the [data](../data.md) and [metadata](../metadata.md) of the entity to be created. - -Attribute|Type|Use|Description ----|---|---|--- -`entity`|string|required| Type of entity to be created. -`storeCode`|string|optional| ID of the store within which the data is created. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -It can optionally contain one or more `requiredEntity` child elements. - -#### Example - -```xml -<!-- Create an entity with the "SampleProduct" name. --> -<createData entity="SampleProduct" stepKey="createSampleProduct"/> -``` - -#### requiredEntity - -Specify relationships amongst data to be created. -For example, a complex Product object may contain within it a pointer (an ID) to a complex Category object. - -##### Example - -```xml -<!-- Create an entity with the "SampleCategory" name. --> -<createData entity="SampleCategory" stepKey="createCategory"/> -<!-- Create the "SampleProduct" product in that category. --> -<createData entity="SampleProduct" stepKey="createProduct"> - <requiredEntity createDataKey="createCategory"/> -</createData> -``` - -Attribute|Type|Use|Description ----|---|---|--- -`createDataKey`|string|required| Name of the required entity. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### field - -Persists a custom field (as a part of the entity) overriding the matching declaration in static data. -This field is replaced at a top level only (nested values such as custom attributes or extension attributes are not replaced). - -Attribute|Type|Use|Description ----|---|---|--- -`key`|string|required| Name of the field to be replaced or added. - -##### Example - -To overwrite the `name` field in a particular product, specify a field element during its creation. - -```xml -<createData entity="SampleProduct" stepKey="createProduct"> - <field key="name">myCustomProductName</field> -</createData> -``` - -### deleteData - -Delete an entity that was previously created. - -Attribute|Type|Use|Description ----|---|---|--- -`createDataKey`|string|optional| Reference to `stepKey` of the `createData` action . -`url`|string|optional| REST API route to send a DELETE request. -`storeCode`|string|optional| ID of the store from which to delete the data. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Examples - -Delete the entity that was previously created using [`createData`](#createdata) in the scope of the [test](../test.md#test-tag). - -1. Create _SampleCategory_: - -```xml -<createData entity="SampleCategory" stepKey="createCategory"/> -``` - -1. Delete _SampleCategory_: - -```xml -<deleteData createDataKey="createCategory" stepKey="deleteCategory"/> -``` - -#### Example of existing data deletion - -Delete an entity using [REST API](https://devdocs.magento.com/redoc/2.3/) request to the corresponding route: - -```xml -<grabFromCurrentUrl regex="/^.+id\/([\d]+)/" stepKey="grabId"/> -<deleteData url="V1/categories/{$grabId}" stepKey="deleteCategory"/> -``` - -### dontSee - -See [the codeception.com documentation for more information about this action](http://codeception.com/docs/modules/WebDriver#dontSee). - -Attribute|Type|Use|Description ----|---|---|--- -`userInput`|string|optional| Value for the form field. -`selector`|string|optional| The selector identifying the corresponding HTML element. -`selectorArray`|string|optional| Array of selectors to evaluate. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Check that the page does not contain the `<h2 id="title">Sample title</h2>` element. --> -<dontSee userInput="Sample title" selector="h2#title" stepKey="dontSeeTitle"/> -``` - -### dontSeeCheckboxIsChecked - -See [dontSeeCheckboxIsChecked docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeCheckboxIsChecked). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Verify that the page does not contain the `<input type="checkbox" id="option1" ... >...</input>` element. --> -<dontSeeCheckboxIsChecked selector="input#option1" stepKey="checkboxNotChecked"/> -``` - -### dontSeeCookie - -See [dontSeeCookie docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeCookie). - -Attribute|Type|Use|Description ----|---|---|--- -`userInput`|string|optional| Value for the form field. -`parameterArray`|string|optional| Parameters to search for within the cookie. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Examples - -```xml -<!-- Verify that there is no cookie with the given name `cookie1`. --> -<dontSeeCookie userInput="cookie1" stepKey="cookie1NotPresent"/> -``` - -```xml -<!-- Verify that there is no cookie with the given name `cookie1` from the domain `www.example.com`. --> -<dontSeeCookie userInput="cookie1" parameterArray="['domainName' => '.example.com']" stepKey="dontSeeCookieInExampleDomain"/> -``` - -### dontSeeCurrentUrlEquals - -See [dontSeeCurrentUrlEquals docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeCurrentUrlEquals). - -Attribute|Type|Use|Description ----|---|---|--- -`url`|string|optional| URL to be compared with the current URL. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Verify that the relative URL of the current page does not match `/admin`. --> -<dontSeeCurrentUrlEquals url="/admin" stepKey="notOnAdminPage"/> -``` - -### dontSeeCurrentUrlMatches - -See [dontSeeCurrentUrlMatches docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeCurrentUrlMatches) - -Attribute|Type|Use|Description ----|---|---|--- -`regex`|string|optional| Regular expression against the current URI. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Verify that the relative URL of the current page does not match the `~$/users/(\d+)~` regular expression. --> -<dontSeeCurrentUrlMatches regex="~$/users/(\d+)~" stepKey="dontSeeCurrentUrlMatches"/> -``` - -### dontSeeElement - -See [dontSeeElement docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeElement). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`parameterArray`|string|optional| Parameters to search for within the selected element. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Verify that `<div id="box" ... >...</div>` is missing or invisible on the current page. --> -<dontSeeElement selector="div#box" stepKey="dontSeeBox"/> -``` - -### dontSeeElementInDOM - -See [dontSeeElementInDOM docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeElementInDOM). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`parameterArray`|string|optional| Array of parameters to search for within the selector. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Verify that `<div id="box" ... >...</div>` is completely missing on the current page. --> -<dontSeeElementInDOM selector="div#box" stepKey="dontSeeBoxInDOM"/> -``` - -### dontSeeInCurrentUrl - -See [dontSeeInCurrentUrl docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeInCurrentUrl). - -Attribute|Type|Use|Description ----|---|---|--- -`url`|string|optional| String to search for within the current URL. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Verify that the url of the current active tab does not contain the string "/users/". --> -<dontSeeInCurrentUrl url="/users/" stepKey="dontSeeInCurrentUrl"/> -``` - -### dontSeeInField - -See [dontSeeInField docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeInField). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`selectorArray`|string|optional| Array of selectors to be searched. -`userInput`|string|optional| Value for the form field. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Verify that `<input id="field" ... >...</input>` does not contain the text "Sample text". --> -<dontSeeInField userInput="Sample text" selector="input#field" stepKey="dontSeeInField1"/> -``` - -### dontSeeInFormFields - -See [dontSeeInFormFields docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeInFormFields). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`parameterArray`|string|optional| Array of name/value pairs of the form fields to check against. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Verify that `<form name="myform" ... >...</form>` with the input elements `<input name="input1">...</input>` and `<input name="input2">...</input>`, do not have the values of `value1` and `value2` respectively. --> -<dontSeeInFormFields selector="form[name=myform]" parameterArray="['input1' => 'value1', 'input2' => 'value2']" stepKey="dontSeeInFormFields"/> -``` - -### dontSeeInPageSource - -See [dontSeeInPageSource docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeInPageSource). - -Attribute|Type|Use|Description ----|---|---|--- -`html`|string|required| HTML code to be searched for within the page source. The value must be entity-encoded. See example. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Verify that the page source does not contain the raw source code `<h1 class="login-header">`. --> -<dontSeeInPageSource userInput="<h1 class="login-header">" stepKey="dontSeeInPageSource"/> -``` - -### dontSeeInSource - -See [dontSeeInSource docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeInSource). - -Attribute|Type|Use|Description ----|---|---|--- -`html`|string|required| HTML code to be searched for within the page source. The value must be entity-encoded. See example. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -You must encode the `html` using a tool such as [CyberChef](https://gchq.github.io/CyberChef/#recipe=To_HTML_Entity(false,'Numeric%20entities')). - -```xml -<!-- Verify that the page source does not contain the raw source code `<h1 class="login-header">`. --> -<dontSeeInSource html="<h1 class="login-header">" stepKey="dontSeeInSource"/> -``` - -### dontSeeInTitle - -See [dontSeeInTitle docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeInTitle). - -Attribute|Type|Use|Description ----|---|---|--- -`userInput`|string|optional| Value to be located in the page title. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Verify that the title of the current active window does not contain the text "Page Title". --> -<dontSeeInTitle userInput="Page Title" stepKey="dontSeeInTitle"/> -``` - -### dontSeeJsError - -Ensure that the current page does not have JavaScript errors. - -Attribute|Type|Use|Description ----|---|---|--- -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Verify there are no JavaScript errors in the current active window. --> -<dontSeeJsError stepKey="dontSeeJsError"/> -``` - -### dontSeeLink - -See [dontSeeLink docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeLink). - -Attribute|Type|Use|Description ----|---|---|--- -`userInput`|string|optional| Text of the link field to search for. -`url`|string|optional| Value of the href attribute to search for. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Examples - -```xml -<!-- Verify that there is no hyperlink tag on the page with the text "External link". --> -<dontSeeLink userInput="External link" stepKey="dontSeeLink"/> -``` - -```xml -<!-- Verify that there is no hyperlink tag with the text "External link" and the `href` attribute of `/admin`. --> -<dontSeeLink userInput="External link" url="/admin" stepKey="dontSeeAdminLink"/> -``` - -### dontSeeOptionIsSelected - -See [dontSeeOptionIsSelected docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dontSeeOptionIsSelected). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding select element. -`userInput`|string|optional| Name of the option to look for. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Verify that `<select id="myselect" ... >...</select>` does not have the option `option1` selected --> -<dontSeeOptionIsSelected userInput="option1" selector="select#myselect" stepKey="dontSeeOption1"/> -``` - -### doubleClick - -See [doubleClick docs on codeception.com](http://codeception.com/docs/modules/WebDriver#doubleClick). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Click the selected element twice in succession. --> -<doubleClick selector="button#mybutton" stepKey="doubleClickButton"/> -``` - -### dragAndDrop - -See [dragAndDrop docs on codeception.com](http://codeception.com/docs/modules/WebDriver#dragAndDrop). - -Attribute|Type|Use|Description ----|---|---|--- -`selector1`|string|optional|A selector for the HTML element to drag. -`selector2`|string|optional|A selector for the HTML element to drop onto. -`x`|int|optional| X offset applied to drag-and-drop destination. -`y`|int|optional| Y offset applied to drag-and-drop destination. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Examples - -```xml -<!-- Click and drag `<div id="block1" ... >...</div>` to the middle of `<div id="block2" ... >...</div>` --> -<dragAndDrop selector1="div#block1" selector2="div#block2" stepKey="dragAndDrop"/> -``` - -```xml -<!-- Click and drag `<div id="block1" ... >...</div>` to the middle of `<div id="block2" ... >...</div>` with a left offset of 50px and top offset of 50px. --> -<dragAndDrop selector1="#block1" selector2="#block2" x="50" y="50" stepKey="dragAndDrop"/> -``` - -### executeJS - -See [executeJS docs on codeception.com](http://codeception.com/docs/modules/WebDriver#executeJS). - -Attribute|Type|Use|Description ----|---|---|--- -`function`|string|optional| JavaScript to be executed. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Return the time in seconds since Unix Epoch (January 1, 1970) using the JavaScript Date() function. -To access this value, use `{$returnTime}` in later actions. --> -<executeJS function="return Math.floor(new Date() / 1000);" stepKey="returnTime"/> -``` - -To access this value you would use `{$returnTime}` in later actions. - -### fillField - -See [fillField docs on codeception.com](http://codeception.com/docs/modules/WebDriver#fillField). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`selectorArray`|string|optional| Array of name/value pairs with which to populate the form. -`userInput`|string|optional| Value for the form field. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Fill in `<input id="myfield" ... >...</input>` with the text "Sample text". --> -<fillField userInput="Sample text" selector="input#myfield" stepKey="fillField"/> -``` - -### formatMoney - -Attribute|Type|Use|Description ----|---|---|--- -`userInput`|string|optional| Value for the money form field. -`locale`|string|optional| The PHP locale value for the store. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -### generateDate - -Generates a date for use in `{$stepKey}` format in other test actions. - -Attribute|Type|Use|Description ----|---|---|--- -`date`|string|required| Date input to parse. Uses the same functionality as the PHP `strtotime()` function. -`format`|string|required| Format in which to save the given date. Uses the same formatting as the PHP `date()` function. -`timezone`|string|optional| Timezone to use when generating date, defaults to `America/Los_Angeles`. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Generate a date that is 1 minute after the current date using Pacific Standard Time. For example "07/11/2020 7:00 AM". -To access this value, use `{$generateDate}` in later actions. --> -<generateDate date="+1 minute" format="m/d/Y g:i A" stepKey="generateDate"/> -``` - -### getData - -Gets an entity (for example, a category), from the Magento API according to the data and metadata of the entity type that is requested. - -Attribute|Type|Use|Description ----|---|---|--- -`storeCode`|string|optional| Identifier of the store from which to get the data. -`index`|integer|optional| The index in the returned data array. -`entity`|string|required| Name of the entity from which to get the data. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Get the product attribute that was created using `<createData stepKey="productAttributeHandle" ... />`. --> -<getData entity="ProductAttributeOptionGetter" index="1" stepKey="getAttributeOption1Handle"> - <requiredEntity createDataKey="productAttributeHandle"/> -</getData> -``` - -The `ProductAttributeOptionGetter` entity must be defined in the corresponding [data `*.xml`](../data.md). - -This action can optionally contain one or more [requiredEntity](#requiredentity) child elements. - -### grabAttributeFrom - -See [grabAttributeFrom docs on codeception.com](http://codeception.com/docs/modules/WebDriver#grabAttributeFrom). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`userInput`|string|optional| Name of tag attribute to grab. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Grab the `title` attribute from `<input id="myinput" ... >...</input>`. -To access this value, use `{$grabAttributeFromInput}` in later actions. --> -<grabAttributeFrom userInput="title" selector="input#myinput" stepKey="grabAttributeFromInput"/> -``` - -### grabCookie - -See [grabCookie docs on codeception.com](http://codeception.com/docs/modules/WebDriver#grabCookie). - -Attribute|Type|Use|Description ----|---|---|--- -`userInput`|string|optional| Name of the cookie to grab. -`parameterArray`|string|optional| Array of cookie parameters to grab. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Examples - -```xml -<!-- Grab the cookie with the given name `cookie1`. -To access this value, use `{$grabCookie1}` in later actions. --> -<grabCookie userInput="cookie1" stepKey="grabCookie1"/> -``` - -```xml -<!-- Grab the cookie with the given name `cookie1` from the domain `www.example.com`. -To access this value, use `{$grabCookieExampleDomain}` in later actions. --> -<grabCookie userInput="cookie1" parameterArray="['domainName' => '.example.com']" stepKey="grabCookieExampleDomain"/> -``` - -### grabFromCurrentUrl - -See [grabFromCurrentUrl docs on codeception.com](http://codeception.com/docs/modules/WebDriver#grabFromCurrentUrl).. - -Attribute|Type|Use|Description ----|---|---|--- -`regex`|string|optional| Regular expression against the current URI. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Grab the text from the current URL that matches the regex expression `~$/user/(\d+)/~`. -To access this value, use `{$grabFromCurrentUrl}` in later actions. --> -<grabFromCurrentUrl regex="~$/user/(\d+)/~" stepKey="grabFromCurrentUrl"/> -``` - -### grabMultiple - -See [grabMultiple docs on codeception.com](http://codeception.com/docs/modules/WebDriver#grabMultiple).. - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`userInput`|string|optional| Name of the tag attribute to grab. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Examples - -```xml -<!-- Grab every element on the page with the class `myElement` and return them as an array. -To access this value, use `{$grabAllMyElements}` in later actions. --> -<grabMultiple selector="div.myElement" stepKey="grabAllMyElements"/> -``` - -```xml -<!-- Grab the `href` tag from every `a` element on the page and return them as an array. -To access this value, use `{$grabAllLinks}` in later actions. --> -<grabMultiple userInput="href" selector="a" stepKey="grabAllLinks"/> -``` - -### grabPageSource - -See [grabPageSource docs on codeception.com](http://codeception.com/docs/modules/WebDriver#grabPageSource). - -Attribute|Type|Use|Description ----|---|---|--- -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Store the page source code as text -To access this value, use `{$grabPageSource}` in later actions. --> -<grabPageSource stepKey="grabPageSource"/> -``` - -### grabTextFrom - -See [grabTextFrom docs on codeception.com](http://codeception.com/docs/modules/WebDriver#grabTextFrom). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Store the text currently displayed by the selected element. -To access this value, use `{$grabTitle}` in later actions. --> -<grabTextFrom selector="h2#title" stepKey="grabTitle"/> -``` - -### grabValueFrom - -See [grabValueFrom docs on codeception.com](https://codeception.com/docs/modules/WebDriver#grabValueFrom). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`selectorArray`|string|optional| Array of selectors for the form fields to be selected. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Store the value currently entered in <input id="name" ... >...</input>. -To access this value, use `{$grabInputName}` in later actions. --> -<grabValueFrom selector="input#name" stepKey="grabInputName"/> -``` - -### loadSessionSnapshot - -See [loadSessionSnapshot docs on codeception.com](http://codeception.com/docs/modules/WebDriver#loadSessionSnapshot). - -Attribute|Type|Use|Description ----|---|---|--- -`userInput`|string|optional| Name of saved cookie. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Load all cookies saved via `<saveSessionSnapshot name="savedSnapshot" ... />`. -To access this value, use the `loadSessionSnapshot` action --> -<loadSessionSnapshot userInput="savedSnapshot" stepKey="loadSnapshot"/> -``` - -### magentoCLI - -Specifies a CLI command to execute in a Magento environment. - -Attribute|Type|Use|Description ----|---|---|--- -`command`|string |optional| CLI command to be executed in Magento environment. -`arguments`|string |optional| Unescaped arguments to be passed in with the CLI command. -`timeout`|string|optional| Number of seconds CLI command can run without outputting anything. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Re-index all indices via the command line. --> -<magentoCLI command="indexer:reindex" stepKey="reindex"/> -``` - -### magentoCron - -Used to execute Magento Cron jobs. Groups may be provided optionally. Internal mechanism of `<magentoCron>` ensures that Cron Job of single group is ran with 60 seconds interval. - -Attribute|Type|Use|Description ----|---|---|--- -`groups`|string |optional| Run only specified groups of Cron Jobs -`arguments`|string |optional| Unescaped arguments to be passed in with the CLI command. -`timeout`|string|optional| Number of seconds CLI command can run without outputting anything. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example -```xml -<magentoCron stepKey="runStagingCronJobs" groups="staging"/> -<!-- No interval here --> -<magentoCron stepKey="runIndexCronJobs" groups="index"/> -<!-- 60 seconds interval takes place here --> -<magentoCron stepKey="runAllCronJobs"/> -``` - -### makeScreenshot - -See [makeScreenshot docs on codeception.com](http://codeception.com/docs/modules/WebDriver#makeScreenshot). - -Attribute|Type|Use|Description ----|---|---|--- -`userInput`|string|optional| Name of PNG file to be created. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -Note that the makeScreenshot action does not automatically add the screenshot to Allure reports. - -#### Example - -```xml -<!-- Take a screenshot of the page and save it to the directory `tests/_output/debug` under the name `example.png`. --> -<makeScreenshot userInput="example" stepKey="screenshotPage"/> -``` - -<div class="bs-callout bs-callout-info"> -This action does not add a screenshot to the Allure [report](../reporting.md).</div> - -### maximizeWindow - -See [maximizeWindow docs on codeception.com](http://codeception.com/docs/modules/WebDriver#maximizeWindow). - -Attribute|Type|Use|Description ----|---|---|--- -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Maximize the current window. --> -<maximizeWindow stepKey="maximizeWindow"/> -``` - -### moveBack - -See [moveBack docs on codeception.com](http://codeception.com/docs/modules/WebDriver#moveBack). - -Attribute|Type|Use|Description ----|---|---|--- -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Move back one page in history. --> -<moveBack stepKey="moveBack"/> -``` - -### moveForward - -See [moveForward docs on codeception.com](http://codeception.com/docs/modules/WebDriver#moveForward).. - -Attribute|Type|Use|Description ----|---|---|--- -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -```xml -<!-- Move forward one page in history. --> -<moveForward stepKey="moveForward"/> -``` - -### moveMouseOver - -See [moveMouseOver docs on codeception.com](http://codeception.com/docs/modules/WebDriver#moveMouseOver). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`selectorArray`|string|optional| Array of selectors. -`x`|string|optional| Number of pixels on the x-axis to offset from the selected element. -`y`|string|optional| Number of pixels on the y-axis to offset from the selected element. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Move the mouse cursor over the selected element. --> -<moveMouseOver selector="button#product1" stepKey="hoverOverProduct1"/> -``` - -```xml -<!-- Move the mouse cursor over the selected element with an offset of 50px from the top and 50px from the left. --> -<moveMouseOver selector="button#product1" x="50" y="50" stepKey="hoverOverProduct2"/> -``` - -### mSetLocale - -Attribute|Type|Use|Description ----|---|---|--- -`userInput`|string|optional| Value of the expected locale. -`locale`|string|optional| Number of the locale value to be set. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -### mResetLocale - -Attribute|Type|Use|Description ----|---|---|--- -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -### openNewTab - -See [openNewTab docs on codeception.com](http://codeception.com/docs/modules/WebDriver#openNewTab). - -Attribute|Type|Use|Description ----|---|---|--- -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Open and switch to a new browser tab. --> -<openNewTab stepKey="openNewTab"/> -``` - -### parseFloat - -Parses float number with thousands separator. - -Attribute|Type|Use|Description ----|---|---|--- -`userInput`|string|optional| Float value to be parsed. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -### pauseExecution - -See [pauseExecution docs on codeception.com](http://codeception.com/docs/modules/WebDriver#pauseExecution). - -Attribute|Type|Use|Description ----|---|---|--- -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Halt test execution until the `enter` key is pressed to continue. --> -<pauseExecution stepKey="pause"/> -``` - -### pressKey - -See [pressKey docs on codeception.com](http://codeception.com/docs/modules/WebDriver#pressKey). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`userInput`|string|optional| Key to be pressed. -`parameterArray`|string|optional| Array of keys to be pressed and functions to be run for the action. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Examples - -```xml -<!-- Press the `a` key within the selected area. --> -<pressKey userInput="a" selector="#targetElement" stepKey="pressA"/> -``` - -The `parameterArray` attribute value must begin with `[` and end with `]`. -To press more than one key at a time, wrap the keys in secondary `[]`. - -```xml -<!-- Press the delete within the selected area uses key constants from the WebDriverKeys class. --> -<pressKey selector="#targetElement" parameterArray="[['ctrl', 'a'], \Facebook\WebDriver\WebDriverKeys::DELETE]" stepKey="pressDelete"/> -``` - -### reloadPage - -See [reloadPage docs on codeception.com](http://codeception.com/docs/modules/WebDriver#reloadPage). - -Attribute|Type|Use|Description ----|---|---|--- -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Reload the current page. --> -<reloadPage stepKey="reloadPage"/> -``` - -### remove - -Removes action by its `stepKey`. - -Attribute|Type|Use|Description ----|---|---|--- -`keyForRemoval`|string|required| Set `stepKey` of the action you want to remove. - -#### Example - -```xml -<!-- Remove an action in the test with the stepKey of `stepKeyToRemove`. --> -<remove keyForRemoval="stepKeyToRemove"/> -``` - -### resetCookie - -See [resetCookie docs on codeception.com](http://codeception.com/docs/modules/WebDriver#resetCookie). - -Attribute|Type|Use|Description ----|---|---|--- -`userInput`|string|optional| Name of the cookie to be reset. -`parameterArray`|string|optional| Array of key/values to get reset within the cookie. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Examples - -```xml -<!-- Reset a cookie with the name `cookie1`. --> -<resetCookie userInput="cookie1" stepKey="resetCookie1"/> -``` - -```xml -<!-- Reset a cookie with the given name `cookie1` from the domain `www.example.com`. --> -<resetCookie userInput="cookie1" parameterArray="['domainName' => '.example.com']" stepKey="resetCookieExampleDomain"/> -``` - -### resizeWindow - -See [resizeWindow docs on codeception.com](http://codeception.com/docs/modules/WebDriver#resizeWindow). - -Attribute|Type|Use|Description ----|---|---|--- -`width`|string|optional| The new width of the window in pixels. -`height`|string|optional| The new height of the window in pixels. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Examples - -```xml -<!-- Resize the current window to a width of 800px and a height of 600px. --> -<resizeWindow width="800" height="600" stepKey="resizeWindow"/> -``` - -### saveSessionSnapshot - -See [saveSessionSnapshot docs on codeception.com](http://codeception.com/docs/modules/WebDriver#saveSessionSnapshot). - -Attribute|Type|Use|Description ----|---|---|--- -`userInput`|string|optional| Name of snapshot where cookies are to be saved. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Save all of the current cookies under the name `savedSnapshot`. --> -<saveSessionSnapshot userInput="savedSnapshot" stepKey="saveCurrentCookies"/> -``` - -### scrollTo - -See [scrollTo docs on codeception.com](http://codeception.com/docs/modules/WebDriver#scrollTo). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`selectorArray`|string|optional| Array of selectors to return. -`x`|string|optional| x offset of the element to be scrolled to. -`y`|string|optional| y offset of the element to be scrolled to. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Examples - -```xml -<!-- Move the page to the middle of the selected area. --> -<scrollTo selector="div#anchor" stepKey="scrollToAnchor"/> -``` - -```xml -<!-- Move the page to the middle of the selected area with an offset of 50px from the top and 50px from the left. --> -<scrollTo selector="div#anchor" x="50" y="50" stepKey="scrollToAnchor2"/> -``` - -### scrollToTopOfPage - -A convenience function that executes `window.scrollTo(0,0)` as JavaScript, thus returning to the top of the page. - -Attribute|Type|Use|Description ----|---|---|--- -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Examples - -```xml -<!-- Move the page to the uppermost, leftmost position. --> -<scrollToTopOfPage stepKey="scrollToTopOfPages"/> -``` - -### searchAndMultiSelectOption - -Search for and select options from a Magento multi-select drop-down menu. -For example, the drop-down menu you use to assign Products to Categories. - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|required|The selector of a multi select HTML element (drop-down menu). -`parameterArray`|array|required| Items to search and select in the selected drop-down menu. -`requiredAction`|boolean|optional|Clicks **Done** after selections if `true`. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Search and select for "Item 1" amd "Item 2" in the Magento multiselect element with the id of `multiSelect`. --> -<searchAndMultiSelectOption selector="#multiSelect" parameterArray="['Item 1', 'Item 2']" stepKey="searchAndMultiSelect1"/> -``` - -On this test step the MFTF: - -1. Searches for a drop-down HTML element that matches the `#stuff` selector. -2. Opens the drop-down menu. -3. Enters **Item 1** in a search field of the drop-down element. -4. Selects first element from the filtered results. -5. Enters **Item 2** in a search field of the drop-down element. -6. Selects first element from the filtered results. - -### see - -See [see docs on codeception.com](http://codeception.com/docs/modules/WebDriver#see). - -Attribute|Type|Use|Description ----|---|---|--- -`userInput`|string|optional| The text to be searched for within the selector. -`selector`|string|optional| The selector identifying the corresponding HTML element to be searched for. -`selectorArray`|string|optional| Array of selectors to be searched for. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Verify that the selected element contains the text "Sample title". --> -<see userInput="Sample title" selector="h2#title" stepKey="seeTitle"/> -``` - -### seeCheckboxIsChecked - -See [seeCheckboxIsChecked docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeCheckboxIsChecked). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Verify `<input type="checkbox" id="option1" ... >...</input>` is checked. --> -<seeCheckboxIsChecked selector="input#option1" stepKey="seeCheckboxChecked"/> -``` - -### seeCookie - -See [seeCookie docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeCookie). - -Attribute|Type|Use|Description ----|---|---|--- -`userInput`|string|optional| Name of the cookie to be searched for. -`parameterArray`|string|optional| Cookie parameters to be searched for. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Examples - -```xml -<!-- Verify that there is a cookie with the given name `cookie1`. --> -<seeCookie userInput="cookie1" stepKey="cookie1Present"/> -``` - -```xml -<!-- Verify that there is a cookie with the given name `cookie1` from the domain `www.example.com`. --> -<seeCookie userInput="cookie1" parameterArray="['domainName' => 'www.example.com']" stepKey="seeCookieInExampleDomain"/> -``` - -### seeCurrentUrlEquals - -See [seeCurrentUrlEquals docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeCurrentUrlEquals). - -Attribute|Type|Use|Description ----|---|---|--- -`url`|string|optional| The full URL to be searched for. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Verify that the relative URL of the current page matches `/admin`. --> -<seeCurrentUrlEquals url="/admin" stepKey="onAdminPage"/> -``` - -### seeCurrentUrlMatches - -See [seeCurrentUrlMatches docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeCurrentUrlMatches). - -Attribute|Type|Use|Description ----|---|---|--- -`regex`|string|optional| Regular expression against the current URI. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Verify that the relative URL of the current page matches the `~$/users/(\d+)~` regular expression. --> -<seeCurrentUrlMatches regex="~$/users/(\d+)~" stepKey="seeCurrentUrlMatches"/> -``` - -### seeElement - -See [seeElement docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeElement). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`selectorArray`|string|optional| Array of selectors to be searched for. -`parameterArray`|string|optional| Array of parameters to be searched for within the selector. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Verify that `<div id="box" ... >...</div>` is available and visible on the current page. --> -<seeElement selector="div#box" stepKey="seeBox"/> -``` - -### seeElementInDOM - -See [seeElementInDOM docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeElementInDOM). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`parameterArray`|string|optional| Array of parameters to be searched for within the selected element. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Verify that `<div id="box" ... >...</div>` is available on the current page. --> -<seeElementInDOM selector="div#box" stepKey="seeBoxInDOM"/> -``` - -### seeInCurrentUrl - -See [seeInCurrentUrl docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeInCurrentUrl). - -Attribute|Type|Use|Description ----|---|---|--- -`url`|string|optional| String to be searched for within the current URL. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Verify that the url of the current active tab contains the string "/users/". --> -<seeInCurrentUrl url="/users/" stepKey="seeInCurrentUrl"/> -``` - -### seeInField - -See [seeInField docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeInField). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`selectorArray`|string|optional| Array of selectors to be searched. -`userInput`|string|optional| Value to be searched for within the selected form field. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Verify that `<input id="field" ... >...</input>` contains the text "Sample text". --> -<seeInField userInput="Sample text" selector="input#field" stepKey="seeInField"/> -``` - -### seeInFormFields - -See [seeInFormFields docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeInFormFields). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`parameterArray`|string|optional| Array of parameters to be searched for within the selector. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Verify that `<form name="myform" ... >...</form>` with the input elements `<input name="input1">...</input>` and `<input name="input2">...</input>`, has the values of `value1` and `value2` respectively. --> -<seeInFormFields selector="form[name=myform]" parameterArray="['input1' => 'value1', 'input2' => 'value2']" stepKey="seeInFormFields"/> -``` - -### seeInPageSource - -See [seeInPageSource docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeInPageSource). - -Attribute|Type|Use|Description ----|---|---|--- -`html`|string|required| HTML code to be searched for within the page source. The value must be entity-encoded. See example. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -You must encode the `html` using a tool such as [CyberChef](https://gchq.github.io/CyberChef/#recipe=To_HTML_Entity(false,'Numeric%20entities')). - -```xml -<!-- Verify that the page source contains the raw source code `<h1 class="login-header">`. --> -<seeInPageSource html="<h1 class="login-header">" stepKey="seeInPageSource"/> -``` - -### seeInPopup - -See [seeInPopup docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeInPopup). - -Attribute|Type|Use|Description ----|---|---|--- -`userInput`|string|optional| String to be searched for within the popup. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Verify the current popup on the page contains the string "Sample text". --> -<seeInPopup userInput="Sample text" stepKey="seeInPopup"/> -``` - -### seeInSource - -See [seeInSource docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeInSource). - -Attribute|Type|Use|Description ----|---|---|--- -`html`|string|required| HTML code to be searched for within the page source. The value must be entity-encoded. See example. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -You must encode the `html` using a tool such as [CyberChef](https://gchq.github.io/CyberChef/#recipe=To_HTML_Entity(false,'Numeric%20entities')). - -```xml -<!-- Verify that the page source contains the raw source code `<h1 class="login-header">`. --> -<seeInSource html="<h1 class="login-header">" stepKey="seeInSource"/> -``` - -### seeInTitle - -See [seeInTitle docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeInTitle). - -Attribute|Type|Use|Description ----|---|---|--- -`userInput`|string|optional| String to be searched for within the current page title. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Verify that the title of the current active window contains the text "Page Title". --> -<seeInTitle userInput="Page Title" stepKey="seeInTitle"/> -``` - -### seeLink - -See [seeLink docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeLink). - -Attribute|Type|Use|Description ----|---|---|--- -`userInput`|string|optional| String to be searched for within the text of the link. -`url`|string|optional| Hyperlink to be searched. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Verify that there is a hyperlink tag on the page with the text "External link". --> -<seeLink userInput="External link" stepKey="seeLink"/> -``` - -```xml -<!-- Verify that there is a hyperlink tag with the text "External link" and the `href` attribute of `/admin`. --> -<seeLink userInput="External link" url="/admin" stepKey="seeAdminLink"/> -``` - -### seeNumberOfElements - -See [seeNumberOfElements docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeNumberOfElements). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`userInput`|string|optional| Number of instances of the specified selector to be found. -`parameterArray`|string|optional| Array of parameters to be searched for within the selector. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Examples - -```xml -<!-- Verify there are 10 `<div class="product" ... >...</div>` elements on the page. --> -<seeNumberOfElements userInput="10" selector="div.product" stepKey="seeTenProducts"/> -``` - -```xml -<!-- Verify there are between 5 and 10 `<div class="product" ... >...</div>` elements on the page. --> -<seeNumberOfElements parameterArray="[5, 10]" selector="div.product" stepKey="seeFiveToTenProducts"/> -``` - -### seeOptionIsSelected - -See [seeOptionIsSelected docs on codeception.com](http://codeception.com/docs/modules/WebDriver#seeOptionIsSelected). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`userInput`|string|optional| The name of the option that should be selected. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Verify that `<select id="myselect" ... >...</select>` has the option `option1` selected --> -<seeOptionIsSelected userInput="option1" selector="select#myselect" stepKey="seeOption1"/> -``` - -### selectOption - -See [selectOption docs on codeception.com](http://codeception.com/docs/modules/WebDriver#selectOption). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`userInput`|string|optional| The name of the option to be selected. -`parameterArray`|string|optional| Array of options to be selected. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Select `option1` from `<select id="mySelect" ... >...</select>`. --> -<selectOption userInput="option1" selector="select#mySelect" stepKey="selectOption1"/> -``` - -### selectMultipleOptions - -Selects all given options in the given Magento drop-down element. - -Attribute|Type|Use|Description ----|---|---|--- -`filterSelector`|string|required| The selector for the text filter field. -`optionSelector`|string|required| The selector used to select the corresponding options based on the filter field. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -It contains a child element `<array>` where you specify the options that must be selected using an array format like `['opt1', 'opt2']`. - -#### Example - -```xml -<!-- Select the options `opt1` and `opt2` from `<option class="option" ... >...</option>` and `<input class="filter" ...>...</input>` --> -<selectMultipleOptions filterSelector=".filter" optionSelector=".option" stepKey="selectMultipleOpts1"> - <array>['opt1', 'opt2']</array> -</selectMultipleOptions> -``` - -### setCookie - -See [setCookie docs on codeception.com](http://codeception.com/docs/modules/WebDriver#setCookie). - -Attribute|Type|Use|Description ----|---|---|--- -`userInput`|string|optional| The name of the cookie to be set. -`parameterArray`|string|optional| Array of name/value pairs to be set within the cookie. -`value`|string|optional| Value to be written to the cookie. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Examples - -```xml -<!-- Set a cookie with the name of `cookieName` and value of `cookieValue`. --> -<setCookie userInput="cookieName" value="cookieValue" stepKey="setCookie"/> -``` - -### submitForm - -See [submitForm docs on codeception.com](http://codeception.com/docs/modules/WebDriver#submitForm). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`parameterArray`|string|optional| An array of form field names and their corresponding values. -`button`|string|optional| Selector for the form submit button. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Examples - -```xml -<!-- Submit a value of `admin` for `<input name="username" ... >...</input>`, a value of `123123q` for `<input name="password" ... >...</input>` for the form `<form id="loginForm" ...>...</form>` and a submit button of `<button id="submit" ... >...</button>` --> -<submitForm selector="#loginForm" parameterArray="['username' => 'admin','password' => '123123q']" button="#submit" stepKey="submitForm"/> -``` - -### switchToIFrame - -See [switchToIFrame docs on codeception.com](http://codeception.com/docs/modules/WebDriver#switchToIFrame). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`userInput`|string|optional| The name of the IFrame to set focus to. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Set the focus to <iframe name="embeddedFrame" ... /> --> -<switchToIFrame userInput="embeddedFrame" stepKey="switchToIFrame"/> -``` - -### switchToNextTab - -See [switchToNextTab docs on codeception.com](http://codeception.com/docs/modules/WebDriver#switchToNextTab). - -Attribute|Type|Use|Description ----|---|---|--- -`userInput`|string|optional| Offset of the tab to open, usually a number. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Examples - -```xml -<!-- Switch to the next tab. --> -<switchToNextTab stepKey="switchToNextTab"/> -``` - -```xml -<!-- Switch to the third next tab. --> -<switchToNextTab userInput="3" stepKey="switchToThirdNextTab"/> -``` - -### switchToPreviousTab - -See [switchToPreviousTab docs on codeception.com](http://codeception.com/docs/modules/WebDriver#switchToPreviousTab). - -Attribute|Type|Use|Description ----|---|---|--- -`userInput`|string|optional| Number of tabs to go back. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Examples - -```xml -<!-- Switch to the previous tab. --> -<switchToPreviousTab stepKey="switchToPreviousTab"/> -``` - -```xml -<!-- Switch to the third previous tab. --> -<switchToPreviousTab userInput="3" stepKey="switchToThirdPreviousTab"/> -``` - -### switchToWindow - -See [switchToWindow docs on codeception.com](http://codeception.com/docs/modules/WebDriver#switchToWindow). - -Attribute|Type|Use|Description ----|---|---|--- -`userInput`|string|optional| The name of new window to be opened. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Switch to a window with the `name` parameter of `newWindow`. --> -<switchToWindow userInput="newWindow" stepKey="switchToWindow"/> -``` - -### typeInPopup - -See [typeInPopup docs on codeception.com](http://codeception.com/docs/modules/WebDriver#typeInPopup). - -Attribute|Type|Use|Description ----|---|---|--- -`userInput`|string|optional| String to be added to the current popup. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Type the text "Sample Text" into the current popup visible on the page. --> -<typeInPopup userInput="Sample Text" stepKey="typeInPopup"/> -``` - -### uncheckOption - -See [uncheckOption docs on codeception.com](http://codeception.com/docs/modules/WebDriver#uncheckOption). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Ensure the checkbox `<input type="checkbox" id="checkbox" ... >...</input>` is unchecked. --> -<uncheckOption selector="input#checkbox" stepKey="uncheckCheckbox"/> -``` - -### unselectOption - -See [unselectOption docs on codeception.com](http://codeception.com/docs/modules/WebDriver#unselectOption). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`userInput`|string|optional| The name of the option to deselect. -`parameterArray`|string|optional| Array of options to be deselected. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Deselect `option1` from `<select id="mySelect" ... >...</select>`. --> -<unselectOption userInput="option1" selector="select#myselect" stepKey="unselectOption1"/> -``` - -### updateData - -When you create a data entity using `createData`, you may need to update it later in the test. -The `updateData` action allows this. - -For example, to change the price of a product: - -```xml -<updateData entity="AdjustPriceProduct" createDataKey="productHandle" stepKey="updateProduct"/> -``` - -Where `AdjustPriceProduct` simply looks like this: - -```xml -<entity name="AdjustPriceProduct" type="product"> - <data key="price">321.00</data> -</entity> -``` - -Only the fields that you want to update are set. - -Attribute|Type|Use|Description ----|---|---|--- -`storeCode`|string|optional| ID of the store in which to apply the updated data. -`entity`|string|required| The name of the `updateData` entity being created. -`createDataKey`|string|required| Key of the data entity to be updated. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -This action can optionally contain one or more [requiredEntity](#requiredentity) child elements. - -### wait - -See [wait docs on codeception.com](http://codeception.com/docs/modules/WebDriver#wait). - -Attribute|Type|Use|Description ----|---|---|--- -`time`|string|optional| The number of seconds to wait. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Halt test execution for 10 seconds before continuing. --> -<wait time="10" stepKey="waitTenSeconds"/> -``` - -### waitForAjaxLoad - -Wait for all AJAX calls to finish. - -Attribute|Type|Use|Description ----|---|---|--- -`time`|string|optional| The number of seconds to wait for Ajax calls to finish. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Wait up to 30 seconds for all AJAX calls to finish before continuing. --> -<waitForAjaxLoad stepKey="waitForAjaxLoad"/> -``` - -### waitForElementChange - -See [waitForElementChange docs on codeception.com](http://codeception.com/docs/modules/WebDriver#waitForElementChange). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the HTML element to be changed. -`function`|string|optional| The function to be run after the element changes. -`time`|string|optional| The number of seconds to wait for the change. Default is 30. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Wait up to 30 seconds for `<div id="changedElement" ... >...</div>` to change to displayed before continuing. --> -<waitForElementChange selector="div#changedElement" function="function(\WebDriverElement $el) {return $el->isDisplayed();}" stepKey="waitForElementChange"/> -``` - -### waitForElement - -See [waitForElement docs on codeception.com](http://codeception.com/docs/modules/WebDriver#waitForElement). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`time`|string|optional| The number of seconds to wait for the element to appear. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Wait up to 30 seconds for `<div id="changedElement" ... >...</div>` to be appear on the page before continuing. --> -<waitForElement selector="#changedElement" stepKey="waitForElement"/> -``` - -### waitForElementNotVisible - -See [waitForElementNotVisible docs on codeception.com](http://codeception.com/docs/modules/WebDriver#waitForElementNotVisible). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`time`|string|optional| The number of seconds to wait for the element to become not visible. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Wait up to 30 seconds for `<div id="changedElement" ... >...</div>` to become non-visible on the page before continuing. --> -<waitForElementNotVisible selector="#changedElement" stepKey="waitForElementNotVisible"/> -``` - -### waitForElementVisible - -See [waitForElementVisible docs on codeception.com](http://codeception.com/docs/modules/WebDriver#waitForElementVisible). - -Attribute|Type|Use|Description ----|---|---|--- -`selector`|string|optional| The selector identifying the corresponding HTML element. -`time`|string|optional| The number of seconds to wait for the element to appear. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Wait up to 30 seconds for `<div id="changedElement" ... >...</div>` to become visible on the page before continuing. --> -<waitForElementVisible selector="#changedElement" stepKey="waitForElementVisible"/> -``` - -### waitForJS - -See [waitForJS docs on codeception.com](http://codeception.com/docs/modules/WebDriver#waitForJS). - -Attribute|Type|Use|Description ----|---|---|--- -`function`|string|optional| The function to be run after all JavaScript finishes. -`time`|string|optional| The number of seconds to wait for JavaScript to finish. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Wait for all jQuery AJAX requests to finish before continuing. --> -<waitForJS function="return $.active == 0;" stepKey="waitForJS"/> -``` - -### waitForLoadingMaskToDisappear - -Wait for all Magento loading overlays to disappear. - -<div class="bs-callout bs-callout-info"> -The CSS class for loading masks is not used consistently throughout Magento. -Therefore, this convenience function tries to wait for various specific selectors.</div> - -```config -# Wait for these classes to not be visible - -//div[contains(@class, "loading-mask")] -//div[contains(@class, "admin_data-grid-loading-mask")] -//div[contains(@class, "admin__data-grid-loading-mask")] -//div[contains(@class, "admin__form-loading-mask")] -//div[@data-role="spinner"] -``` - -Attribute|Type|Use|Description ----|---|---|--- -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Wait up to 30 seconds for all Magento loading overlays to disappear before continuing. --> -<waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> -``` - -### waitForPageLoad - -Wait for AJAX, Magento loading overlays, and `document.readyState == "complete"`. - -Attribute|Type|Use|Description ----|---|---|--- -`time`|string|optional| Number of seconds to wait for the page to load. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Wait up to 30 seconds for the current page to fully load before continuing. --> -<waitForPageLoad stepKey="waitForPageLoad"/> -``` - -### waitForPwaElementNotVisible - -Waits up to the given `time` for a PWA Element to disappear from the screen. - -Attribute|Type|Use|Description ----|---|---|--- -`time`|string|optional| Number of seconds to wait for the element to disappear. -`selector`|string|required| The selector identifying the corresponding HTML element. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Wait for the PWA element to disappear. --> -<waitForPwaElementNotVisible time="1" stepKey="waitForPwaElementNotVisible"/> -``` - -### waitForPwaElementVisible - -Waits up to the given 'time' for a PWA Element to appear on the screen. - -Attribute|Type|Use|Description ----|---|---|--- -`time`|string|optional| Number of seconds to wait for the selected element. -`selector`|string|required| The selector identifying the corresponding HTML element. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Wait for the selected element to appear. --> -<waitForPwaElementVisible stepKey="waitForPwaElementVisible"/> -``` - -### waitForText - -See [waitForText docs on codeception.com](http://codeception.com/docs/modules/WebDriver#waitForText). - -Attribute|Type|Use|Description ----|---|---|--- -`userInput`|string|optional| The string to wait for. -`time`|string|optional| The number of seconds to wait for the text to appear. -`selector`|string|optional| The selector identifying the corresponding HTML element. -`stepKey`|string|required| A unique identifier of the action. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of preceding action. - -#### Example - -```xml -<!-- Wait for text "Sample Text" to appear in the selected area before continuing. --> -<waitForText userInput="Sample Text" selector="div#page" stepKey="waitForText"/> -``` diff --git a/docs/test/annotations.md b/docs/test/annotations.md deleted file mode 100644 index f30b8104e..000000000 --- a/docs/test/annotations.md +++ /dev/null @@ -1,233 +0,0 @@ -# Annotations - - -Annotations are essentially comments in the code. In PHP, they all are marked by a preceding `@` symbol. - -Within [tests], annotations are contained within their own node. - -## Principles - -The following conventions apply to annotations in the Magento Functional Testing Framework (MFTF): - -- All annotations are within an `<annotations>` element. -- Each element within corresponds to a supported annotation type. -- There is no distinction made in XML between Codeception annotations and Allure annotations. -- Each annotation contains only one value. -If multiple annotation values are supported and required each value requires a separate annotation. -- Tests must contain all of the following annotations: stories, title, description, severity. - -Recommended use cases of the annotation types: - -- [stories] - report grouping, a set of tests that verify a story. -- [title] - description of the test purpose. -- [group] - general functionality grouping. -- [description] - description of how the test achieves the purpose defined in the title. -- [skip] - a label for the test to be skipped during generation (for example, an incomplete test blocked by an issue) - -## Example - -```xml -<annotations> - <stories value="Category Creation"/> - <title value="Create a Category via Admin"/> - <description value="Test logs into admin backend and creates a category."/> - <severity value="CRITICAL"/> - <group value="category"/> -</annotations> -``` - -## Reference - -### description - -The `<description>` element is an implementation of a [`@Description`] Allure tag; Metadata for report. - -Attribute|Type|Use ----|---|-- -`value`|string|required - -#### Example - -```xml -<description value="Add Catalog via Admin"/> -``` - -### features - -The `<features>` element is an implementation of a [`@Features`] Allure tag. - -`<features>` sets a string that will be displayed as a feature within the Allure report. Tests under the same feature are grouped together in the report. - -Attribute|Type|Use ----|---|-- -`value`|string|required - -#### Example - -```xml -<features value="Catalog"/> -<features value="Add/Edit"/> -``` - -### group - -The `<group>` element is an implementation of a [`@group`] Codeception tag. - -`<group>` specifies a string to identify and collect tests together. -Any test can be a part of multiple groups. -The purpose of grouping is to create a set of test for a functionality or purpose, such as all cart tests or all slow tests and run them together locally. - -<div class="bs-callout bs-callout-warning" markdown="1"> -Group values cannot collide with [suite][] names. -</div> - -<div class="bs-callout bs-callout-tip" markdown="1"> -Add `<skip>` to the test to skip it during test run. -</div> - -Attribute|Type|Use|Definition ----|---|---|--- -`value`|string|required|A value that is used to group tests. It should be lower case. `skip` is reserved to ignore content of the test and generate an empty test. - -#### Example - -```xml -<group value="category"/> -``` - -### return - -The `<return>` element is an implementation of a [`@return`] Codeception tag. -It specifies what is returned from a test execution. - -Attribute|Type|Use ----|---|-- -`value`|string|required - -#### Example - -```xml -<return value="void"/> -``` - -### severity - -The `<return>` element is an implementation of a [`@Severity`] Allure tag; Metadata for report. - -Attribute|Type|Use|Acceptable values ----|---|---|--- -`value`|string|required|`MINOR`, `AVERAGE`, `MAJOR`, `BLOCKER`, `CRITICAL` - -#### Example - -```xml -<severity value="CRITICAL"/> -``` - -### skip - -Use the `<skip>` element to skip a test. -It contains one or more child elements `<issueId>` to specify one or more issues that cause the test skipping. - -#### issueId - -This element under `<skip>` is required at least once and contains references to issues that cause the test to be skipped. - -Attribute|Type|Use ----|---|-- -`value`|string|required - -#### Example - -```xml -<skip> - <issueId value="#117"/> - <issueId value="MC-345"/> -</skip> -``` - -### stories - -The `<stories>` element is an implementation of a [`@Stories`] Allure tag. -It has the same functionality as [features], within the Story report group. - -Attribute|Type|Use ----|---|-- -`value`|string|required - -#### Example - -```xml -<stories value="Add Catalog"/> -<stories value="Edit Catalog"/> -``` - -### testCaseId - -The `<testCaseId>` element is an implementation of a [`@TestCaseId`] Allure tag. -It specifies a ZephyrId for a test. - -This tag is prefixed to a title of the test annotation to make the test title unique in Allure. - -If the linkage is set up correctly in the Allure config, the test will have a hyperlink to the Zephyr test case in the report. - -Learn more about [setup instructions in Allure]. - -Attribute|Type|Use ----|---|-- -`value`|string|required - -#### Example - -```xml -<testCaseId value="#"/> -``` - -### title - -The `<title>` element is an implementation of [`@Title`] Allure tag; Metadata for report. - -Attribute|Type|Use ----|---|-- -`value`|string|required - -#### Example - -```xml -<title value="Add Catalog"/> -``` - -### useCaseId - -The `<useCaseId>` element is an implementation of a `@UseCaseId` custom tag. It specifies the use case ID for a test and is ignored by Allure configuration at the moment, as Allure implementation is not complete. - -Attribute|Type|Use ----|---|-- -`value`|string|required - -#### Example - -```xml -<useCaseId value="USECASE-1"/> -``` - -<!-- Link definitions --> - -[`@Description`]: https://devhub.io/zh/repos/allure-framework-allure-phpunit#extended-test-class-or-test-method-description -[`@Features`]: https://devhub.io/zh/repos/allure-framework-allure-phpunit#map-test-classes-and-test-methods-to-features-and-stories -[`@group`]: http://codeception.com/docs/07-AdvancedUsage#Groups -[`@return`]: http://codeception.com/docs/07-AdvancedUsage#Examples -[`@Severity`]: https://devhub.io/zh/repos/allure-framework-allure-phpunit#set-test-severity -[`@Stories`]: https://devhub.io/zh/repos/allure-framework-allure-phpunit#map-test-classes-and-test-methods-to-features-and-stories -[`@TestCaseId`]: https://github.com/allure-framework/allure1/wiki/Test-Case-ID -[`@Title`]: https://devhub.io/zh/repos/allure-framework-allure-phpunit#human-readable-test-class-or-test-method-title -[description]: #description -[features]: #features -[group]: #group -[setup instructions in Allure]: https://github.com/allure-framework/allure1/wiki/Test-Case-ID -[severity]: #severity -[stories]: #stories -[suite]: ../suite.md -[tests]: ../test.md -[title]: #title -[skip]: #skip diff --git a/docs/test/assertions.md b/docs/test/assertions.md deleted file mode 100644 index dfe4c3d4b..000000000 --- a/docs/test/assertions.md +++ /dev/null @@ -1,462 +0,0 @@ -# Assertions - - -Assertions serve to pass or fail the [test](../test.md#test-tag) if a condition is not met. These assertions will look familiar to you if you've used any other testing framework, like PHPUnit. - -All assertions contain the same [common actions attributes](./actions.md#common-attributes): `stepKey`, `before`, and `after`. - -Most assertions contain a `message` attribute that specifies the text of an informational message to help you identify the cause of the failure. - -## Principles - -The [principles for actions](../test.md#principles) are also applicable to assertions. - -Assertion actions have nested self-descriptive elements, `<expectedResult>` and `<actualResult>`. These elements contain a result type and a value: - -* `type` - * `const` (default) - * `int` - * `float` - * `bool` - * `string` - * `variable` - * `array` -* `value` - -If `variable` is used, the test transforms the corresponding value to `$variable`. Use the `stepKey` of a test, that returns the value you want to use, in assertions: - -`actual="stepKeyOfGrab" actualType="variable"` - -To use variables embedded in a string in `expected` and `actual` of your assertion, use the `{$stepKey}` format: - -`actual="A long assert string {$stepKeyOfGrab} with an embedded variable reference." actualType="variable"` - -## Example - -The following example shows a common test that gets text from a page and asserts that it matches what we expect to see. If it does not, the test will fail at the assert step. - -```xml -<!-- Grab a value from the page using any grab action --> -<grabTextFrom selector="#elementId" stepKey="stepKeyOfGrab"/> - -<!-- Ensure that the value we grabbed matches our expectation --> -<assertEquals message="This is an optional human readable hint that will be shown in the logs if this assert fails." stepKey="assertEquals1"> - <expectedResult type="string">Some String</expectedResult> - <actualResult type="string">A long assert string {$stepKeyOfGrab} with an embedded variable reference.</actualResult> -</assertEquals> -``` - -## Elements reference - -### assertElementContainsAttribute - -Example: - -```xml -<assertElementContainsAttribute stepKey="assertElementContainsAttribute"> - <expectedResult selector=".admin__menu-overlay" attribute="style" type="string">color: #333;</expectedResult> -</assertElementContainsAttribute> -``` - -Attribute|Type|Use|Description ----|---|---|--- -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertArrayIsSorted - -The `<assertArrayIsSorted>` asserts that the array is sorted according to a specified sort order, ascending or descending. - -Example: - -```xml -<assertArrayIsSorted sortOrder="asc" stepKey="assertSorted"> - <array>[1,2,3,4,5,6,7]</array> -</assertArrayIsSorted> -``` - -Attribute|Type|Use|Description ----|---|---|--- -`sortOrder`|Possible values: `asc`, `desc`|required| A sort order to assert on array values. -`stepKey`|string|required| A unique identifier of the test step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -It contains an `<array>` child element that specifies an array to be asserted for proper sorting. -It must be in typical array format like `[1,2,3,4,5]` or `[alpha, brontosaurus, zebra]`. - -### assertArrayHasKey - -See [assertArrayHasKey docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertArrayHasKey) - -Attribute|Type|Use|Description ----|---|---|---`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertArrayNotHasKey - -See [assertArrayNotHasKey docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertArrayNotHasKey). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertArraySubset - -See [assertArraySubset docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertArraySubset). - -Attribute|Type|Use|Description ----|---|---|--- -`strict`|boolean|optional| -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertContains - -See [assertContains docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertContains). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertCount - -See [assertCount docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertCount). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertEmpty - -See [assertEmpty docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertEmpty). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertEquals - -See [assertEquals docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertEquals). - -Attribute|Type|Use|Description ----|---|---|--- -`delta`|string|optional| -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertFalse - -See [assertFalse docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertFalse). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertFileExists - -See [assertFileExists docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertFileExists). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertFileNotExists - -See [assertFileNotExists docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertFileNotExists). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertGreaterOrEquals - -See [assertGreaterOrEquals docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertGreaterOrEquals). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertGreaterThan - -See [assertGreaterThan docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertGreaterThan). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertGreaterThanOrEqual - -See [assertGreaterThanOrEqual docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertGreaterThanOrEqual). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertInstanceOf - -See [assertInstanceOf docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertInstanceOf). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertInternalType - -See [assertInternalType docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertInternalType). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertIsEmpty - -See [assertIsEmpty docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertIsEmpty). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertLessOrEquals - -See [assertLessOrEquals docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertLessOrEquals). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertLessThan - -See [assertLessThan docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertLessThan). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertLessThanOrEqual - -See [assertLessThanOrEqual docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertLessThanOrEqual). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertNotContains - -See [assertNotContains docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertNotContains). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertNotEmpty - -See [assertNotEmpty docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertNotEmpty). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertNotEquals - -See [assertNotEquals docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertNotEquals). - -Attribute|Type|Use|Description ----|---|---|--- -`delta`|string|optional| -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertNotInstanceOf - -See [assertNotInstanceOf docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertNotInstanceOf). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertNotNull - -See [assertNotNull docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertNotNull). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertNotRegExp - -See [assertNotRegExp docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertNotRegExp). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertNotSame - -See [assertNotSame docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertNotSame). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertNull - -See [assertNull docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertNull). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertRegExp - -See [assertRegExp docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertRegExp). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertSame - -See [assertSame docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertSame). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertStringStartsNotWith - -See [assertStringStartsNotWith docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertStringStartsNotWith). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertStringStartsWith - -See [assertStringStartsWith docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertStringStartsWith). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### assertTrue - -See [assertTrue docs on codeception.com](http://codeception.com/docs/modules/Asserts#assertTrue). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|optional|Text of informational message about a cause of failure. -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### expectException - -See [expectException docs on codeception.com](http://codeception.com/docs/modules/WebDriver#expectException). - -Attribute|Type|Use|Description ----|---|---|--- -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. - -### fail - -See [fail docs on codeception.com](http://codeception.com/docs/modules/WebDriver#fail). - -Attribute|Type|Use|Description ----|---|---|--- -`message`|string|required| -`stepKey`|string|required| A unique identifier of the text step. -`before`|string|optional| `stepKey` of action that must be executed next. -`after`|string|optional| `stepKey` of the preceding action. diff --git a/docs/tips-tricks.md b/docs/tips-tricks.md deleted file mode 100644 index a2300c3bf..000000000 --- a/docs/tips-tricks.md +++ /dev/null @@ -1,423 +0,0 @@ -# Tips and Tricks - -Sometimes, little changes can make a big difference in your project. Here are some test writing tips to keep everything running smoothly. - -## Actions and action groups - -### Use parameterized selectors in action groups with argument references - -Clarity and readability are important factors in good test writing. -Having to parse through unreadable code can be time consuming. Save time by writing clearly. -The good example clearly shows what the selector arguments refer to. -In the bad example we see two parameters being passed into the selector with little clue as to their purpose. - -**Why?** The next person maintaining the test or extending it may not be able to understand what the parameters are referencing. - -<span style="color:green"> -Good -</span> - -<!-- {% raw %} --> - -```xml -<test> - <actionGroup ref="VerifyOptionInProductStorefront" stepKey="verifyConfigurableOption" after="AssertProductInStorefrontProductPage"> - <argument name="attributeCode" value="$createConfigProductAttribute.default_frontend_label$"/> - <argument name="optionName" value="$createConfigProductAttributeOption1.option[store_labels][1][label]$"/> - </actionGroup> -</test> - -<actionGroup name="VerifyOptionInProductStorefront"> - <arguments> - <argument name="attributeCode" type="string"/> - <argument name="optionName" type="string"/> - </arguments> - <seeElement selector="{{StorefrontProductInfoMainSection.attributeOptionByAttributeID(attributeCode, optionName)}}" stepKey="verifyOptionExists"/> -</actionGroup> -``` - -<span style="color:red"> -Bad -</span> - -```xml -<test> - <seeElement selector="{{StorefrontProductInfoMainSection.attributeOptionByAttributeID($createConfigProductAttribute.default_frontend_label$, $createConfigProductAttributeOption1.option[store_labels][1][label]$)}}" stepKey="verifyOptionExists"/> -</test> -``` - -### Perform the most critical actions first in the `<after>` block - -Perform non-browser driving actions first. These are more likely to succeed as no UI is involved. -In the good example, `magentoCLI` and `deleteData` are run first to ensure a proper state. -In the bad example, we perform some heavy UI steps first. - -**Why?** If something goes wrong there, then the critical `magentoCLI` commands may not get a chance to run, leaving Magento configured incorrectly for any upcoming tests. - -<span style="color:green"> -Good: -</span> - -```xml -<after> - <magentoCLI command="indexer:set-mode" arguments="schedule" stepKey="setIndexerMode"/> - <magentoCLI command="config:set catalog/frontend/flat_catalog_category 0" stepKey="setFlatCatalogCategory"/> - <deleteData createDataKey="category" stepKey="deleteCategory"/> - <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> - <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> - <argument name="customStore" value="customStoreEN"/> - </actionGroup> - <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewFr"> - <argument name="customStore" value="customStoreFR"/> - </actionGroup> - <actionGroup ref="logout" stepKey="logout"/> -</after> -``` - -<span style="color:red"> -Bad: -</span> - -```xml -<after> - <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> - <argument name="customStore" value="customStoreEN"/> - </actionGroup> - <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewFr"> - <argument name="customStore" value="customStoreFR"/> - </actionGroup> - <deleteData createDataKey="category" stepKey="deleteCategory"/> - <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> - <magentoCLI command="config:set catalog/frontend/flat_catalog_category 0" stepKey="setFlatCatalogCategory"/> - <magentoCLI command="indexer:set-mode" arguments="schedule" stepKey="setIndexerMode"/> - <actionGroup ref="logout" stepKey="logout"/> -</after> -``` - -### When to use see vs. seeElement - -Use `see` and `seeElement` wisely. -If you need to see some element and verify that the text inside is shown correctly, use the `see` action. -If you need to verify that element present on page, use `seeElement`. -But never use `seeElement` and build a xPath which contains the expected text. - -**Why?** For `see` it will output something similar to this: -`Failed asserting that any element by #some_selector contains text "some_text"` -And for `seeElement` it will output something like this: -`Element by #some_selector is not visible.` -There is a subtle distinction: The first is a failure but it is the desired result: a 'positive failure'. -The second is a proper result of the action. - -<span style="color:green"> -Good: -</span> - -```xml -<see selector="//div[@data-element='content']//p" userInput="SOME EXPECTED TEXT" stepKey="seeSlide1ContentStorefront"/> -``` - -<span style="color:red"> -Bad: -</span> - -```xml -<seeElement selector="//div[@data-element='content']//p[.='SOME EXPECTED TEXT']" stepKey="seeSlide1ContentStorefront"/> -``` - -### Always specify a default value for action group arguments - -Whenever possible, specify a `defaultValue` for action group arguments. - -<span style="color:green"> -GOOD: -</span> - -```xml -<actionGroup name="StorefrontAssertProductImagesOnProductPageActionGroup"> - <arguments> - <argument name="productImage" type="string" defaultValue="Magento_Catalog/images/product/placeholder/image.jpg" /> - </arguments> - <waitForElementNotVisible selector="{{StorefrontProductMediaSection.gallerySpinner}}" stepKey="waitGallerySpinnerDisappear" /> - <seeElement selector="{{StorefrontProductMediaSection.gallery}}" stepKey="seeProductGallery" /> - <seeElement selector="{{StorefrontProductMediaSection.productImage(productImage)}}" stepKey="seeProductImage" /> - <click selector="{{StorefrontProductMediaSection.productImage(productImage)}}" stepKey="openFullscreenImage" /> - <waitForPageLoad stepKey="waitForGalleryLoaded" /> - <seeElement selector="{{StorefrontProductMediaSection.productImageFullscreen(productImage)}}" stepKey="seeFullscreenProductImage" /> - <click selector="{{StorefrontProductMediaSection.closeFullscreenImage}}" stepKey="closeFullScreenImage" /> - <waitForPageLoad stepKey="waitForGalleryDisappear" /> -</actionGroup> -``` - -<span style="color:red"> -BAD: -</span> - -```xml -<actionGroup name="StorefrontAssertProductImagesOnProductPageActionGroup"> - <arguments> - <argument name="productImage" type="string" /> - </arguments> - <waitForElementNotVisible selector="{{StorefrontProductMediaSection.gallerySpinner}}" stepKey="waitGallerySpinnerDisappear" /> - <seeElement selector="{{StorefrontProductMediaSection.gallery}}" stepKey="seeProductGallery" /> - <seeElement selector="{{StorefrontProductMediaSection.productImage(productImage)}}" stepKey="seeProductImage" /> - <click selector="{{StorefrontProductMediaSection.productImage(productImage)}}" stepKey="openFullscreenImage" /> - <waitForPageLoad stepKey="waitForGalleryLoaded" /> - <seeElement selector="{{StorefrontProductMediaSection.productImageFullscreen(productImage)}}" stepKey="seeFullscreenProductImage" /> - <click selector="{{StorefrontProductMediaSection.closeFullscreenImage}}" stepKey="closeFullScreenImage" /> - <waitForPageLoad stepKey="waitForGalleryDisappear" /> -</actionGroup> -``` - -### Build tests from action groups - -Build your tests using action groups, even if an action group contains a single action. - -**Why?** For extension developers, this will make it easier to extend or customize tests. -Extending a single action group will update all tests that use this group. -This improves maintainability as multiple instances of a failure can be fixed with a single action group update. - -<span style="color:green"> -GOOD: -</span> - -```xml -<test name="NavigateClamberWatchEntityTest"> - <annotations> - <!--some annotations--> - </annotations> - - <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> - <argument name="productUrl" value="{{ClamberWatch.url_key}}" /> - </actionGroup> - <actionGroup ref="StorefrontAssertProductNameOnProductPageActionGroup" stepKey="assertProductName"> - <argument name="productName" value="{{ClamberWatch.name}}" /> - </actionGroup> - <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="assertProductSku"> - <argument name="productSku" value="{{ClamberWatch.sku}}" /> - </actionGroup> - <actionGroup ref="StorefrontAssertProductPriceOnProductPageActionGroup" stepKey="assertProductPrice"> - <argument name="productPrice" value="{{ClamberWatch.price}}" /> - </actionGroup> - <actionGroup ref="StorefrontAssertProductImagesOnProductPageActionGroup" stepKey="assertProductImage"> - <argument name="productImage" value="{{ClamberWatch.image}}" /> - </actionGroup> -</test> -``` - -<span style="color:red"> -BAD: -</span> - -```xml -<test name="NavigateClamberWatchEntityTest"> - <annotations> - <!--some annotations--> - </annotations> - - <amOnPage url="{{StorefrontProductPage.url(ClamberWatch.url_key)}}" stepKey="openProductPage"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{ClamberWatch.name}}" stepKey="seeProductName" /> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{ClamberWatch.sku}}" stepKey="seeProductSku" /> - <see selector="{{StorefrontProductInfoMainSection.price}}" userInput="{{ClamberWatch.price}}" stepKey="seeProductPrice" /> - <waitForElementNotVisible selector="{{StorefrontProductMediaSection.gallerySpinner}}" stepKey="waitGallerySpinnerDisappear" /> - <seeElement selector="{{StorefrontProductMediaSection.gallery}}" stepKey="seeProductGallery" /> - <seeElement selector="{{StorefrontProductMediaSection.productImage(ClamberWatch.productImage)}}" stepKey="seeProductImage" /> - <click selector="{{StorefrontProductMediaSection.productImage(ClamberWatch.productImage)}}" stepKey="openFullscreenImage" /> - <waitForPageLoad stepKey="waitForGalleryLoaded" /> - <seeElement selector="{{StorefrontProductMediaSection.productImageFullscreen(ClamberWatch.productImage)}}" stepKey="seeFullscreenProductImage" /> -</test> -``` - -### Use descriptive stepKey names - -Make `stepKeys` values as descriptive as possible. -Do not use numbers to make a `stepKey` unique. - -**Why?** This helps with readability and clarity. - -<span style="color:green"> -GOOD: -</span> - -```xml -<click selector="{{StorefrontNavigationSection.topCategory(SimpleSubCategory.name)}}" stepKey="clickSimpleSubCategoryLink" /> -<waitForPageLoad stepKey="waitForSimpleSubCategoryPageLoad" /> -<click selector="{{StorefrontCategoryMainSection.productLinkByHref(SimpleProduct.urlKey)}}" stepKey="clickSimpleProductLink" /> -<waitForPageLoad stepKey="waitForSimpleProductPageLoad" /> - -<!-- Perform some actions / Assert product page --> - -<click selector="{{StorefrontNavigationSection.topCategory(CustomCategory.name)}}" stepKey="clickCustomCategoryLink" /> -<waitForPageLoad stepKey="waitForCustomCategoryPageLoad" /> -<click selector="{{StorefrontCategoryMainSection.productLinkByHref(CustomSimpleProduct.urlKey)}}" stepKey="clickCustomSimpleProductLink" /> -<waitForPageLoad stepKey="waitForCustomSimpleProductPageLoad" /> -``` - -<span style="color:red"> -BAD: -</span> - -```xml -<click selector="{{StorefrontNavigationSection.topCategory(SimpleSubCategory.name)}}" stepKey="clickCategoryLink1" /> -<waitForPageLoad stepKey="waitForPageLoad1" /> -<click selector="{{StorefrontCategoryMainSection.productLinkByHref(SimpleProduct.urlKey)}}" stepKey="clickProductLink1" /> -<waitForPageLoad stepKey="waitForPageLoad2" /> - -<!-- Perform some actions / Assert product page --> - -<click selector="{{StorefrontNavigationSection.topCategory(CustomCategory.name)}}" stepKey="clickCategoryLink2" /> -<waitForPageLoad stepKey="waitForPageLoad3" /> -<click selector="{{StorefrontCategoryMainSection.productLinkByHref(CustomSimpleProduct.urlKey)}}" stepKey="clickProductLink2" /> -<waitForPageLoad stepKey="waitForPageLoad4" /> -``` - -**Exception:** - -Use numbers within `stepKeys` when order is important, such as with testing sort order. - -```xml -<createData entity="BasicMsiStock1" stepKey="createCustomStock1"/> -<createData entity="BasicMsiStock2" stepKey="createCustomStock2"/> -<createData entity="BasicMsiStock3" stepKey="createCustomStock3"/> -<createData entity="BasicMsiStock4" stepKey="createCustomStock4"/> -``` - -## Selectors - -### Use contains() around text() - -When possible, use `contains(text(), 'someTextHere')` rather than `text()='someTextHere'`. -`contains()` ignores whitespace while `text()` accounts for it. - -**Why?** -If you are comparing text within a selector and have an unexpected space, or a blank line above or below the string, `text()` will fail while the `contains(text())` format will catch it. -In this scenario `text()` is more exacting. Use it when you need to be very precise about what is getting compared. - -<span style="color:green"> -GOOD: -</span> - -`//span[contains(text(), 'SomeTextHere')]` - -<span style="color:red"> -BAD: -</span> - -`//span[text()='SomeTextHere']` - -### Build selectors in proper order - -When building selectors for form elements, start with the parent context of the form element. -Then specify the element `name` attribute in your selector to ensure the correct element is targeted. -To build a selector for an input, use the pattern: `{{section_selector}} {{input_selector}}` or for a button: `{{section_selector}} {{button_selector}}` - -**Why?** Traversing the DOM takes a finite amount of time and reducing the scope of the selector makes the selector lookup as efficient as possible. - -Example: - -```xml -<div class="admin__field _required" data-bind="css: $data.additionalClasses, attr: {'data-index': index}, visible: visible" data-index="name"> - <div class="admin__field-label" data-bind="visible: $data.labelVisible"> - <span data-bind="attr: {'data-config-scope': $data.scopeLabel}, i18n: label" data-config-scope="[STORE VIEW]">Product Name</span> - </div> - <div class="admin__field-control" data-bind="css: {'_with-tooltip': $data.tooltip, '_with-reset': $data.showFallbackReset && $data.isDifferedFromDefault}"> - <input class="admin__control-text" type="text" name="product[name]" aria-describedby="notice-EXNI71H" id="EXNI71H" maxlength="255" data-bind=" - attr: { - name: inputName, - placeholder: placeholder, - maxlength: 255}"/> - </div> -</div> -``` - -<span style="color:green"> -GOOD: -</span> - -```xml -<element name="productName" type="input" selector="*[data-index='product-details'] input[name='product[name]']"/> -``` - -<span style="color:red"> -BAD: -</span> - -```xml -<element name="productName" type="input" selector=".admin__field[data-index=name] input"/> -``` - -## General tips - -### Use data references to avoid hardcoded values - -If you need to run a command such as `<magentoCLI command="config:set" />`, do not hardcode paths and values to the command. -Rather, create an appropriate `ConfigData.xml` file, which contains the required parameters for running the command. -It will simplify the future maintanence of tests. - - <span style="color:green"> -GOOD: -</span> - -```xml -<magentoCLI command="config:set {{StorefrontCustomerCaptchaLength3ConfigData.path}} {{StorefrontCustomerCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> -``` - - <span style="color:red"> -BAD: -</span> - -```xml -<magentoCLI command="config:set customer/captcha/length 3" stepKey="setCaptchaLength" /> -``` - -For example: -[This test][] refers to this [Data file][]. - -### Use descriptive variable names - -Use descriptive variable names to increase readability. -**Why?** It makes the code easier to follow and update. - - <span style="color:green"> -GOOD: -</span> - -```xml -<element name="storeName" type="checkbox" selector="//label[contains(text(),'{{storeName}}')]" parameterized="true"/> -``` - -<span style="color:red"> -BAD: -</span> - -```xml -<element name="storeName" type="checkbox" selector="//label[contains(text(),'{{var1}}')]" parameterized="true"/> -``` - -### Use proper checkbox actions - -When working with input type `checkbox`, do not use the `click` action; use `checkOption` or `uncheckOption` instead. -**Why?** A click does not make it clear what the ending state will be; it will simply toggle the current state. Using the proper actions will ensure the expected state of the checkbox. - -<span style="color:green"> -GOOD: -</span> - -```xml -<checkOption selector="{{ProductInWebsitesSection.website('Second Website')}}" stepKey="selectSecondWebsite"/> -<uncheckOption selector="{{ProductInWebsitesSection.website('Second Website')}}" stepKey="unselectSecondWebsite"/> -``` - -<span style="color:red"> -BAD: -</span> - -```xml -<click selector="{{ProductInWebsitesSection.website('Second Website')}}" stepKey="selectSecondWebsite"/> -<click selector="{{ProductInWebsitesSection.website('Second Website')}}" stepKey="unselectSecondWebsite"/> -``` - -<!--{% endraw %}--> - -<!-- Link Definitions --> -[This test]: https://github.com/magento/magento2/blob/2.3/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaRegisterNewCustomerTest.xml#L24 -[Data file]: https://github.com/magento/magento2/blob/2.3/app/code/Magento/Captcha/Test/Mftf/Data/CaptchaConfigData.xml diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md deleted file mode 100644 index d2a7dcabe..000000000 --- a/docs/troubleshooting.md +++ /dev/null @@ -1,65 +0,0 @@ -# Troubleshooting - -Having a little trouble with the MFTF? See some common errors and fixes below. - -## AcceptanceTester class issues - -If you see the following error: - -```terminal -AcceptanceTester class doesn't exist in suite folder. -Run the 'build' command to generate it -``` - -### Reason - -Something went wrong during the `mftf build:project` command that prevented the creation of the AcceptanceTester class. - -### Solution - -This issue is fixed in the MFTF 2.5.0. - -In versions of the MFTF lower than 2.5.0 you should: - -1. Open the functional.suite.yml file at: - - ```terminal - <magento root directory>/dev/tests/acceptance/tests/functional.suite.yml - ``` -1. Add quotation marks (`"`) around these values: - - 1. `%SELENIUM_HOST%` - 1. `%SELENIUM_PORT%` - 1. `%SELENIUM_PROTOCOL%` - 1. `%SELENIUM_PATH%` - -1. Run the `vendor/bin/mftf build:project` command again. -1. You should see the AcceptanceTester class is created at: - - ```terminal - <magento root directory>/vendor/magento/magento2-functional-testing-framework/src/Magento/FunctionalTestingFramework/AcceptanceTester.php - ``` - -## WebDriver issues - -Troubleshoot your WebDriver issues on various browsers. - -### PhantomJS - -You are unable to upload file input using the MFTF actions and are seeing the following exception: - -```terminal -[Facebook\WebDriver\Exception\NoSuchDriverException] -No active session with ID e56f9260-b366-11e7-966b-db3e6f35d8e1 -``` - -#### Reason - -Use of PhantomJS is not supported by the MFTF. - -#### Solution - -For headless browsing, the [Headless Chrome][]{:target="\_blank"} has better compatibility with the MFTF. - -<!-- Link Definitions --> -[Headless Chrome]: https://developers.google.com/web/updates/2017/04/headless-chrome diff --git a/docs/versioning.md b/docs/versioning.md deleted file mode 100644 index ecb8438c5..000000000 --- a/docs/versioning.md +++ /dev/null @@ -1,68 +0,0 @@ -# MFTF versioning schema - -This document describes the versioning policy for the Magento Functional Testing Framework (MFTF), including the version numbering schema. - -## Backward compatibility - -In this context, backward compatibility means that when changes are made to the MFTF, all existing tests still run normally. -If a modification to MFTF forces tests to be changed, this is a backward incompatible change. - -## Find your MFTF version number - -To find the version of MFTF that you are using, run the Magento CLI command: - -```bash -cd <magento_root>/ -vendor/bin/mftf --version -``` - -## Versioning Policy - -MFTF versioning policy follows [Semantic Versioning](https://semver.org/) guidelines. - -### 3-component version numbers - -Version numbering schemes help users to understand the scope of the changes made a new release. - -```tree -X.Y.Z -| | | -| | +-- Backward Compatible changes (Patch release - bug fixes, small additions) -| +---- Backward Compatible changes (Minor release - small new features, bug fixes) -+------ Backward Incompatible changes (Major release - new features and/or major changes) -``` - -For example: - -- Magento 2 ships with MFTF version 2.3.9 -- A patch is added to fix a bug: 2.3.10 (Increment Z = backward compatible change) -- New action command added: 2.4.0 (Increment Y, set Z to 0 = backward compatible change) -- New action added: 2.4.1 (Increment Z = backward compatible change) -- Major new features added to MFTF to support changes in Magento codebase: 3.0.0. (Increment X, reset Y and Z to 0 = backward incompatible change) - -### Z release - patch - -Patch version **Z** MUST be incremented for a release that introduces only backward compatible changes. - -### Y release - minor - -Minor version **Y** MUST be incremented for a release that introduces new, backward compatible features. -It MUST be incremented if any test or test entity is marked as deprecated. -It MAY include patch level changes. Patch version MUST be reset to 0 when minor version is incremented. - -### X release - major - -Major version **X** MUST be incremented for a release that introduces backward incompatible changes. -A major release can also include minor and patch level changes. -You must reset the patch and minor version to 0 when you change the major version. - -## Magento 2 compatibility - -This table lists the version of the MFTF that was released with a particular version of Magento. - -|Magento version| MFTF version| -|---|---| -| 2.3.2 | 2.3.14 | -| 2.3.1 | 2.3.13 | -| 2.3.0 | 2.3.9 | -| 2.2.8 | 2.3.13 | diff --git a/etc/config/.credentials.example b/etc/config/.credentials.example index 429e9d19f..fe6dd19a9 100644 --- a/etc/config/.credentials.example +++ b/etc/config/.credentials.example @@ -1,3 +1,6 @@ +magento/tfa/OTP_SHARED_SECRET +magento/MAGENTO_ADMIN_PASSWORD + #magento/carriers_fedex_account= #magento/carriers_fedex_meter_number= #magento/carriers_fedex_key= @@ -72,4 +75,3 @@ #magento/payment_paypal_alternative_payment_methods_express_checkout_us_express_checkout_required_express_checkout_required_express_checkout_api_password= #magento/payment_paypal_alternative_payment_methods_express_checkout_us_express_checkout_required_express_checkout_required_express_checkout_api_signature= -#magento/fraud_protection_signifyd_api_key= \ No newline at end of file diff --git a/etc/config/.env.example b/etc/config/.env.example index 349d7da9c..b3ec2ad41 100644 --- a/etc/config/.env.example +++ b/etc/config/.env.example @@ -10,7 +10,6 @@ MAGENTO_BASE_URL=http://devdocs.magento.com/ #*** Set the Admin Username and Password for your Magento instance ***# MAGENTO_BACKEND_NAME=admin MAGENTO_ADMIN_USERNAME=admin -MAGENTO_ADMIN_PASSWORD=123123q #*** Path to CLI entry point and command parameter name. Uncomment and change if folder structure differs from standard Magento installation #MAGENTO_CLI_COMMAND_PATH=dev/tests/acceptance/utils/command.php @@ -21,9 +20,12 @@ MAGENTO_ADMIN_PASSWORD=123123q #SELENIUM_PORT=4444 #SELENIUM_PROTOCOL=http #SELENIUM_PATH=/wd/hub +SELENIUM_CLOSE_ALL_SESSIONS=true #*** Browser for running tests, default chrome. Uncomment and change if you want to run tests on another browser (ex. firefox). BROWSER=chrome +WINDOW_WIDTH=1920 +WINDOW_HEIGHT=1080 #*** Uncomment and set host & port if your dev environment needs different value other than MAGENTO_BASE_URL for Rest API Requests ***# #MAGENTO_RESTAPI_SERVER_HOST=restapi.magento.com @@ -47,7 +49,7 @@ BROWSER=chrome #DEFAULT_TIMEZONE=America/Los_Angeles #*** These properties impact the modules loaded into MFTF, you can point to your own full path, or a custom set of modules located with the core set -MODULE_WHITELIST=Magento_Framework,ConfigurableProductWishlist,ConfigurableProductCatalogSearch +MODULE_ALLOWLIST=Magento_Framework,ConfigurableProductWishlist,ConfigurableProductCatalogSearch #CUSTOM_MODULE_PATHS= #*** Bool property which allows the user to toggle debug output during test execution @@ -57,12 +59,25 @@ MODULE_WHITELIST=Magento_Framework,ConfigurableProductWishlist,ConfigurableProdu #ALLOW_SKIPPED=true #*** Default timeout for wait actions -#WAIT_TIMEOUT=30 +WAIT_TIMEOUT=60 -#*** Uncomment and set to enable browser log entries on actions in Allure. Blacklist is used to filter logs of a specific "source" +#*** Default timeout for 'magentoCLI' and 'magentoCLISecret' command +MAGENTO_CLI_WAIT_TIMEOUT=60 + +#*** Uncomment and set to enable all tests, regardless of passing status, to have all their Allure artifacts. +#VERBOSE_ARTIFACTS=true + +#*** Uncomment and set to enable browser log entries on actions in Allure. Blocklist is used to filter logs of a specific "source" #ENABLE_BROWSER_LOG=true -#BROWSER_LOG_BLACKLIST=other +BROWSER_LOG_BLOCKLIST=other + +#*** Uncomment and set to true to use Codeception's interactive pause functionality +#ENABLE_PAUSE=true #*** Elastic Search version used for test ***# ELASTICSEARCH_VERSION=7 + +#*** Lifetime (in seconds) of Magento Admin WebAPI Token; if token is older than this value a refresh attempt will be made just before the next WebAPI call ***# +#MAGENTO_ADMIN_WEBAPI_TOKEN_LIFETIME=10800 + #*** End of .env ***# diff --git a/etc/config/codeception.dist.yml b/etc/config/codeception.dist.yml index 30697dc8f..54858dd6a 100755 --- a/etc/config/codeception.dist.yml +++ b/etc/config/codeception.dist.yml @@ -7,6 +7,7 @@ paths: data: tests/_data support: src/Magento/FunctionalTestingFramework envs: etc/_envs + output: tests/_output settings: silent: true colors: true @@ -15,9 +16,9 @@ extensions: enabled: - Magento\FunctionalTestingFramework\Codeception\Subscriber\Console - Magento\FunctionalTestingFramework\Extension\TestContextExtension - - Magento\FunctionalTestingFramework\Allure\Adapter\MagentoAllureAdapter + - Qameta\Allure\Codeception\AllureCodeception config: - Magento\FunctionalTestingFramework\Allure\Adapter\MagentoAllureAdapter: + Qameta\Allure\Codeception\AllureCodeception: deletePreviousResults: false outputDirectory: allure-results ignoredAnnotations: @@ -27,4 +28,4 @@ extensions: Magento\FunctionalTestingFramework\Extension\TestContextExtension: driver: \Magento\FunctionalTestingFramework\Module\MagentoWebDriver params: - - .env \ No newline at end of file + - .env diff --git a/etc/config/command.php b/etc/config/command.php index e3b8f1191..861d016e8 100644 --- a/etc/config/command.php +++ b/etc/config/command.php @@ -18,13 +18,13 @@ // Token returned will be null if the token we passed in is invalid $tokenFromMagento = $tokenModel->loadByToken($tokenPassedIn)->getToken(); - if (!empty($tokenFromMagento) && ($tokenFromMagento == $tokenPassedIn)) { + if (!empty($tokenFromMagento) && ($tokenFromMagento === $tokenPassedIn)) { $php = PHP_BINDIR ? PHP_BINDIR . '/php' : 'php'; $magentoBinary = $php . ' -f ../../../../bin/magento'; $valid = validateCommand($magentoBinary, $command); if ($valid) { $fullCommand = escapeshellcmd($magentoBinary . " $command" . " $arguments"); - $process = new Symfony\Component\Process\Process($fullCommand); + $process = Symfony\Component\Process\Process::fromShellCommandline($fullCommand); $process->setIdleTimeout($timeout); $process->setTimeout(0); $idleTimeout = false; @@ -46,18 +46,16 @@ $idleTimeout = true; } - if (checkForFilePath($output)) { - $output = "CLI output suppressed, filepath detected in output."; - } - $exitCode = $process->getExitCode(); - if ($exitCode == 0 || $idleTimeout) { + if ($process->isSuccessful() || $idleTimeout) { http_response_code(202); } else { http_response_code(500); } - echo $output; + + // Suppress file paths from output + echo suppressFilePaths($output); } else { http_response_code(403); echo "Given command not found valid in Magento CLI Command list."; @@ -115,11 +113,21 @@ function trimAfterWhitespace($string) } /** - * Detects file path in string. + * Suppress file paths in string. * @param string $string - * @return boolean + * @return string */ -function checkForFilePath($string) +function suppressFilePaths(string $string): string { - return preg_match('/\/[\S]+\//', $string); + // Match file paths on both *nix and Windows system + $filePathPattern = '~(?:[A-Za-z]:[\\\/]|\\\\|\/)\S+~'; + $replacement = '[suppressed_path]'; + + preg_match_all($filePathPattern, $string, $matches); + if (!empty($matches)) { + foreach ($matches[0] as $match) { + $string = str_replace($match, $replacement, $string); + } + } + return $string; } diff --git a/etc/config/functional.suite.dist.yml b/etc/config/functional.suite.dist.yml index ffbb60990..25cad8342 100644 --- a/etc/config/functional.suite.dist.yml +++ b/etc/config/functional.suite.dist.yml @@ -7,7 +7,7 @@ # Perform tests in browser using the WebDriver or PhpBrowser. # If you need both WebDriver and PHPBrowser tests - create a separate suite. -class_name: AcceptanceTester +actor: AcceptanceTester namespace: Magento\FunctionalTestingFramework modules: enabled: @@ -16,6 +16,7 @@ modules: - \Magento\FunctionalTestingFramework\Module\MagentoAssert - \Magento\FunctionalTestingFramework\Module\MagentoActionProxies - Asserts + - \Magento\FunctionalTestingFramework\Helper\HelperContainer config: \Magento\FunctionalTestingFramework\Module\MagentoWebDriver: url: "%MAGENTO_BASE_URL%" @@ -23,7 +24,7 @@ modules: backend_name: "%MAGENTO_BACKEND_NAME%" browser: 'chrome' restart: true - window_size: 1280x1024 + window_size: 1920x1080 username: "%MAGENTO_ADMIN_USERNAME%" password: "%MAGENTO_ADMIN_PASSWORD%" pageload_timeout: "%WAIT_TIMEOUT%" @@ -33,6 +34,8 @@ modules: port: "%SELENIUM_PORT%" protocol: "%SELENIUM_PROTOCOL%" path: "%SELENIUM_PATH%" + close_all_sessions: "%SELENIUM_CLOSE_ALL_SESSIONS%" capabilities: - chromeOptions: - args: ["--window-size=1280,1024", "--disable-extensions", "--enable-automation", "--disable-gpu", "--enable-Passthrough"] + unhandledPromptBehavior: "ignore" + chromeOptions: + args: ["--no-sandbox", "--window-size=1920,1080", "--disable-extensions", "--enable-automation", "--disable-gpu", "--enable-Passthrough", "--disable-dev-shm-usage", "--disable-component-update", "--disable-features=OptimizationHints","--disable-background-networking","--disable-domain-reliability","--disable-breakpad"] diff --git a/etc/di.xml b/etc/di.xml index 1dd91896a..a3dfa1292 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -8,7 +8,7 @@ <!-- Entity value gets replaced in Dom.php before reading $xml --> <!DOCTYPE config [ - <!ENTITY commonTestActions "acceptPopup|actionGroup|amOnPage|amOnUrl|amOnSubdomain|appendField|assertArrayIsSorted|assertArraySubset|assertElementContainsAttribute|attachFile|cancelPopup|checkOption|clearField|click|clickWithLeftButton|clickWithRightButton|closeAdminNotification|closeTab|comment|conditionalClick|createData|deleteData|updateData|getData|dontSee|dontSeeJsError|dontSeeCheckboxIsChecked|dontSeeCookie|dontSeeCurrentUrlEquals|dontSeeCurrentUrlMatches|dontSeeElement|dontSeeElementInDOM|dontSeeInCurrentUrl|dontSeeInField|dontSeeInFormFields|dontSeeInPageSource|dontSeeInSource|dontSeeInTitle|dontSeeLink|dontSeeOptionIsSelected|doubleClick|dragAndDrop|entity|executeJS|fillField|formatMoney|generateDate|grabAttributeFrom|grabCookie|grabFromCurrentUrl|grabMultiple|grabPageSource|grabTextFrom|grabValueFrom|loadSessionSnapshot|loginAsAdmin|magentoCLI|magentoCron|makeScreenshot|maximizeWindow|moveBack|moveForward|moveMouseOver|mSetLocale|mResetLocale|openNewTab|pauseExecution|parseFloat|pressKey|reloadPage|resetCookie|submitForm|resizeWindow|saveSessionSnapshot|scrollTo|scrollToTopOfPage|searchAndMultiSelectOption|see|seeCheckboxIsChecked|seeCookie|seeCurrentUrlEquals|seeCurrentUrlMatches|seeElement|seeElementInDOM|seeInCurrentUrl|seeInField|seeInFormFields|seeInPageSource|seeInPopup|seeInSource|seeInTitle|seeLink|seeNumberOfElements|seeOptionIsSelected|selectOption|setCookie|submitForm|switchToIFrame|switchToNextTab|switchToPreviousTab|switchToWindow|typeInPopup|uncheckOption|unselectOption|wait|waitForAjaxLoad|waitForElement|waitForElementChange|waitForElementNotVisible|waitForElementVisible|waitForPwaElementNotVisible|waitForPwaElementVisible|waitForJS|waitForLoadingMaskToDisappear|waitForPageLoad|waitForText|assertArrayHasKey|assertArrayNotHasKey|assertArraySubset|assertContains|assertCount|assertEmpty|assertEquals|assertFalse|assertFileExists|assertFileNotExists|assertGreaterOrEquals|assertGreaterThan|assertGreaterThanOrEqual|assertInstanceOf|assertInternalType|assertIsEmpty|assertLessOrEquals|assertLessThan|assertLessThanOrEqual|assertNotContains|assertNotEmpty|assertNotEquals|assertNotInstanceOf|assertNotNull|assertNotRegExp|assertNotSame|assertNull|assertRegExp|assertSame|assertStringStartsNotWith|assertStringStartsWith|assertTrue|expectException|fail|dontSeeFullUrlEquals|dontSee|dontSeeFullUrlMatches|dontSeeInFullUrl|seeFullUrlEquals|seeFullUrlMatches|seeInFullUrl|grabFromFullUrl|helper"> + <!ENTITY commonTestActions "acceptPopup|actionGroup|amOnPage|amOnUrl|amOnSubdomain|appendField|assertArrayIsSortasserted|assertElementContainsAttribute|attachFile|cancelPopup|checkOption|clearField|click|clickWithLeftButton|clickWithRightButton|closeAdminNotification|closeTab|comment|conditionalClick|createData|deleteData|updateData|getData|dontSee|dontSeeJsError|dontSeeCheckboxIsChecked|dontSeeCookie|dontSeeCurrentUrlEquals|dontSeeCurrentUrlMatches|dontSeeElement|dontSeeElementInDOM|dontSeeInCurrentUrl|dontSeeInField|dontSeeInFormFields|dontSeeInPageSource|dontSeeInSource|dontSeeInTitle|dontSeeLink|dontSeeOptionIsSelected|doubleClick|dragAndDrop|entity|executeJS|fillField|formatCurrency|generateDate|getOTP|grabAttributeFrom|grabCookie|grabCookieAttributes|grabFromCurrentUrl|grabMultiple|grabPageSource|grabTextFrom|grabValueFrom|return|loadSessionSnapshot|loginAsAdmin|magentoCLI|magentoCron|makeScreenshot|maximizeWindow|moveBack|moveForward|moveMouseOver|mSetLocale|mResetLocale|openNewTab|pause|parseFloat|pressKey|reloadPage|resetCookie|submitForm|resizeWindow|saveSessionSnapshot|scrollTo|scrollToTopOfPage|searchAndMultiSelectOption|see|seeCheckboxIsChecked|seeCookie|seeCurrentUrlEquals|seeCurrentUrlMatches|seeElement|seeElementInDOM|seeInCurrentUrl|seeInField|seeInFormFields|seeInPageSource|seeInPopup|seeInSource|seeInTitle|seeLink|seeNumberOfElements|seeOptionIsSelected|selectOption|setCookie|submitForm|switchToIFrame|switchToNextTab|switchToPreviousTab|switchToWindow|typeInPopup|uncheckOption|unselectOption|wait|waitForAjaxLoad|waitForElement|waitForElementChange|waitForElementNotVisible|waitForElementVisible|waitForPwaElementNotVisible|waitForPwaElementVisible|waitForElementClickable|waitForJS|waitForLoadingMaskToDisappear|waitForPageLoad|waitForText|assertArrayHasKey|assertArrayNotHasKey|assertContains|assertStringContainsString|assertStringContainsStringIgnoringCase|assertCount|assertEmpty|assertEquals|assertFalse|assertFileExists|assertFileNotExists|assertGreaterOrEquals|assertGreaterThan|assertGreaterThanOrEqual|assertInstanceOf|assertIsEmpty|assertLessOrEquals|assertLessThan|assertLessThanOrEqual|assertNotContains|assertStringNotContainsString|assertStringNotContainsStringIgnoringCase|assertNotEmpty|assertNotEquals|assertNotInstanceOf|assertNotNull|assertNotRegExp|assertNotSame|assertNull|assertRegExp|assertSame|assertStringStartsNotWith|assertStringStartsWith|assertTrue|expectException|fail|dontSeeFullUrlEquals|dontSee|dontSeeFullUrlMatches|dontSeeInFullUrl|seeFullUrlEquals|seeFullUrlMatches|seeInFullUrl|grabFromFullUrl|helper|assertEqualsWithDelta|assertEqualsCanonicalizing|assertEqualsIgnoringCase|assertNotEqualsWithDelta|assertNotEqualsCanonicalizing|assertNotEqualsIgnoringCase"> ]> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../src/Magento/FunctionalTestingFramework/ObjectManager/etc/config.xsd"> @@ -157,6 +157,7 @@ <argument name="idAttributes" xsi:type="array"> <item name="/entities/entity" xsi:type="string">name</item> <item name="/entities/entity/(data|array)" xsi:type="string">key</item> + <item name="/entities/entity/array/item" xsi:type="string">name</item> <item name="/entities/entity/requiredEntity" xsi:type="string">type</item> </argument> <argument name="mergeablePaths" xsi:type="array"> diff --git a/src/Magento/FunctionalTestingFramework/Allure/Adapter/MagentoAllureAdapter.php b/src/Magento/FunctionalTestingFramework/Allure/Adapter/MagentoAllureAdapter.php index e9e1c344c..1c4766f56 100644 --- a/src/Magento/FunctionalTestingFramework/Allure/Adapter/MagentoAllureAdapter.php +++ b/src/Magento/FunctionalTestingFramework/Allure/Adapter/MagentoAllureAdapter.php @@ -5,13 +5,18 @@ */ namespace Magento\FunctionalTestingFramework\Allure\Adapter; +use Codeception\Codecept; +use Codeception\Test\Cest; use Codeception\Step\Comment; use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; use Magento\FunctionalTestingFramework\Test\Objects\ActionGroupObject; use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; use Magento\FunctionalTestingFramework\Util\TestGenerator; +use Yandex\Allure\Adapter\Model\Failure; +use Yandex\Allure\Adapter\Model\Provider; use Yandex\Allure\Adapter\Model\Status; use Yandex\Allure\Adapter\Model\Step; +use Yandex\Allure\Adapter\Allure; use Yandex\Allure\Codeception\AllureCodeception; use Yandex\Allure\Adapter\Event\StepStartedEvent; use Yandex\Allure\Adapter\Event\StepFinishedEvent; @@ -19,9 +24,11 @@ use Yandex\Allure\Adapter\Event\TestCaseFailedEvent; use Yandex\Allure\Adapter\Event\TestCaseFinishedEvent; use Yandex\Allure\Adapter\Event\TestCaseBrokenEvent; +use Yandex\Allure\Adapter\Event\AddAttachmentEvent; use Codeception\Event\FailEvent; use Codeception\Event\SuiteEvent; use Codeception\Event\StepEvent; +use Codeception\Event\TestEvent; /** * Class MagentoAllureAdapter @@ -35,6 +42,12 @@ class MagentoAllureAdapter extends AllureCodeception { const STEP_PASSED = "passed"; + + /** + * @var array + */ + private $options = []; + /** * Test files cache. * @@ -56,7 +69,7 @@ class MagentoAllureAdapter extends AllureCodeception */ private function getGroup() { - if ($this->options['groups'] != null) { + if ($this->options['groups'] !== null) { return $this->options['groups'][0]; } return null; @@ -72,7 +85,7 @@ public function suiteBefore(SuiteEvent $suiteEvent) { $changeSuiteEvent = $suiteEvent; - if ($this->getGroup() != null) { + if ($this->getGroup() !== null) { $suite = $suiteEvent->getSuite(); $suiteName = ($suite->getName()) . "\\" . $this->sanitizeGroupName($this->getGroup()); @@ -114,6 +127,7 @@ private function sanitizeGroupName($group) // if we can't find this group in the generated suites we have to assume that the group was split for generation $groupNameSplit = explode("_", $group); array_pop($groupNameSplit); + array_pop($groupNameSplit); $originalName = implode("_", $groupNameSplit); // confirm our original name is one of the existing suite names otherwise just return the original group name @@ -176,7 +190,6 @@ public function stepBefore(StepEvent $stepEvent) // Strip control characters so that report generation does not fail $stepName = preg_replace('/[[:cntrl:]]/', '', $stepName); - $this->emptyStep = false; $this->getLifecycle()->fire(new StepStartedEvent($stepName)); } @@ -186,7 +199,7 @@ public function stepBefore(StepEvent $stepEvent) * @throws \Yandex\Allure\Adapter\AllureException * @return void */ - public function stepAfter(StepEvent $stepEvent = null) + public function stepAfter(?StepEvent $stepEvent = null) { // Simply return if step is INVISIBLE_STEP_ACTIONS if ($this->atInvisibleSteps) { @@ -245,11 +258,17 @@ public function testError(FailEvent $failEvent) /** * Override of parent method, polls stepStorage for testcase and formats it according to actionGroup nesting. - * + * @param TestEvent $testEvent + * @throws \Yandex\Allure\Adapter\AllureException * @return void */ - public function testEnd() + public function testEnd(TestEvent $testEvent) { + $test = $this->getLifecycle()->getTestCaseStorage()->get(); + // update testClass label to consolidate re-try reporting + $this->formatAllureTestClassName($test); + // Peek top of testCaseStorage to check of failure + $testFailed = $test->getFailure(); // Pops top of stepStorage, need to add it back in after processing $rootStep = $this->getLifecycle()->getStepStorage()->pollLast(); $formattedSteps = []; @@ -257,6 +276,7 @@ public function testEnd() $actionGroupStepKey = null; foreach ($rootStep->getSteps() as $step) { + $this->removeAttachments($step, $testFailed); $stepKey = str_replace($actionGroupStepKey, '', $step->getName()); if ($stepKey !== '[]' && $stepKey !== null) { $step->setName($stepKey); @@ -309,9 +329,28 @@ function () use ($rootStep, $formattedSteps) { $this->getLifecycle()->getStepStorage()->put($rootStep); + $this->addAttachmentEvent($testEvent); + $this->getLifecycle()->fire(new TestCaseFinishedEvent()); } + /** + * Fire add attachment event + * @param TestEvent $testEvent + * @throws \Yandex\Allure\Adapter\AllureException + * @return void + */ + private function addAttachmentEvent(TestEvent $testEvent) + { + // attachments supported since Codeception 3.0 + if (version_compare(Codecept::VERSION, '3.0.0') > -1 && $testEvent->getTest() instanceof Cest) { + $artifacts = $testEvent->getTest()->getMetadata()->getReports(); + foreach ($artifacts as $name => $artifact) { + Allure::lifecycle()->fire(new AddAttachmentEvent($artifact, $name, null)); + } + } + } + /** * Reads action group stepKey from step. * @@ -354,4 +393,60 @@ private function retrieveStepKey($stepLine) return $stepKey; } + + /** + * Removes attachments from step depending on MFTF configuration + * @param Step $step + * @param Failure $testFailed + * @return void + */ + private function removeAttachments($step, $testFailed) + { + //Remove Attachments if verbose flag is not true AND test did not fail + if (getenv('VERBOSE_ARTIFACTS') !== "true" && $testFailed === null) { + foreach ($step->getAttachments() as $index => $attachment) { + $step->removeAttachment($index); + unlink(Provider::getOutputDirectory() . DIRECTORY_SEPARATOR . $attachment->getSource()); + } + } + } + + /** + * Format testClass label to consolidate re-try reporting for groups split for parallel execution + * @param TestCase $test + * @return void + */ + private function formatAllureTestClassName($test) + { + if ($this->getGroup() !== null) { + foreach ($test->getLabels() as $label) { + if ($label->getName() === 'testClass') { + $originalTestClass = $this->sanitizeTestClassLabel($label->getValue()); + call_user_func(\Closure::bind( + function () use ($label, $originalTestClass) { + $label->value = $originalTestClass; + }, + null, + $label + )); + break; + } + } + } + } + + /** + * Function which sanitizes testClass label for split group runs + * @param string $testClass + * @return string + */ + private function sanitizeTestClassLabel($testClass) + { + $originalTestClass = $testClass; + $originalGroupName = $this->sanitizeGroupName($this->getGroup()); + if ($originalGroupName !== $this->getGroup()) { + $originalTestClass = str_replace($this->getGroup(), $originalGroupName, $testClass); + } + return $originalTestClass; + } } diff --git a/src/Magento/FunctionalTestingFramework/Allure/AllureHelper.php b/src/Magento/FunctionalTestingFramework/Allure/AllureHelper.php index 58bbc55b8..6e6498e62 100644 --- a/src/Magento/FunctionalTestingFramework/Allure/AllureHelper.php +++ b/src/Magento/FunctionalTestingFramework/Allure/AllureHelper.php @@ -3,44 +3,79 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\FunctionalTestingFramework\Allure; -use Magento\FunctionalTestingFramework\Allure\Event\AddUniqueAttachmentEvent; -use Yandex\Allure\Adapter\Allure; -use Yandex\Allure\Adapter\Event\AddAttachmentEvent; +use Qameta\Allure\Allure; +use Qameta\Allure\Io\DataSourceInterface; class AllureHelper { /** - * Adds attachment to the current step + * Adds attachment to the current step. + * * @param mixed $data * @param string $caption - * @throws \Yandex\Allure\Adapter\AllureException + * * @return void */ - public static function addAttachmentToCurrentStep($data, $caption) + public static function addAttachmentToCurrentStep($data, $caption): void { - Allure::lifecycle()->fire(new AddUniqueAttachmentEvent($data, $caption)); + if (!is_string($data)) { + try { + $data = serialize($data); + } catch (\Exception $exception) { + throw new \Exception($data->getMessage()); + } + } + if (@file_exists($data) && is_file($data)) { + Allure::attachmentFile($caption, $data); + } else { + Allure::attachment($caption, $data); + } } /** * Adds Attachment to the last executed step. * Use this when adding attachments outside of an $I->doSomething() step/context. + * * @param mixed $data * @param string $caption + * * @return void */ - public static function addAttachmentToLastStep($data, $caption) + public static function addAttachmentToLastStep($data, $caption): void { - $rootStep = Allure::lifecycle()->getStepStorage()->getLast(); - $trueLastStep = array_last($rootStep->getSteps()); - - if ($trueLastStep == null) { - // Nothing to attach to; do not fire off allure event - return; + if (!is_string($data)) { + $data = serialize($data); + } + if (@file_exists($data) && is_file($data)) { + Allure::attachmentFile($caption, $data); + } else { + Allure::attachment($caption, $data); } - - $attachmentEvent = new AddUniqueAttachmentEvent($data, $caption); - $attachmentEvent->process($trueLastStep); + } + + /** + * @param DataSourceInterface $dataSource + * @param string $name + * @param string|null $type + * @param string|null $fileExtension + * @return void + */ + public static function doAddAttachment( + DataSourceInterface $dataSource, + string $name, + ?string $type = null, + ?string $fileExtension = null, + ): void { + $attachment = Allure::getConfig() + ->getResultFactory() + ->createAttachment() + ->setName($name) + ->setType($type) + ->setFileExtension($fileExtension); + Allure::getLifecycle()->addAttachment($attachment, $dataSource); } } diff --git a/src/Magento/FunctionalTestingFramework/Allure/Event/AddUniqueAttachmentEvent.php b/src/Magento/FunctionalTestingFramework/Allure/Event/AddUniqueAttachmentEvent.php index bf16e5b49..af0ed2c52 100644 --- a/src/Magento/FunctionalTestingFramework/Allure/Event/AddUniqueAttachmentEvent.php +++ b/src/Magento/FunctionalTestingFramework/Allure/Event/AddUniqueAttachmentEvent.php @@ -1,36 +1,37 @@ <?php - /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\FunctionalTestingFramework\Allure\Event; +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; use Symfony\Component\Mime\MimeTypes; use Yandex\Allure\Adapter\AllureException; use Yandex\Allure\Adapter\Event\AddAttachmentEvent; -const DEFAULT_FILE_EXTENSION = 'txt'; -const DEFAULT_MIME_TYPE = 'text/plain'; - class AddUniqueAttachmentEvent extends AddAttachmentEvent { - /** - * @var string - */ - private $type; + private const DEFAULT_FILE_EXTENSION = 'txt'; + private const DEFAULT_MIME_TYPE = 'text/plain'; /** - * Near copy of parent function, added uniqid call for filename to prevent buggy allure behavior - * @param string $filePathOrContents + * Near copy of parent function, added uniqid call for filename to prevent buggy allure behavior. + * + * @param mixed $filePathOrContents * @param string $type + * * @return string * @throws AllureException */ - public function getAttachmentFileName($filePathOrContents, $type) + public function getAttachmentFileName($filePathOrContents, $type): string { $filePath = $filePathOrContents; - if (!file_exists($filePath) || !is_file($filePath)) { + + if (!is_string($filePath) || !file_exists($filePath) || !is_file($filePath)) { //Save contents to temporary file $filePath = tempnam(sys_get_temp_dir(), 'allure-attachment'); if (!file_put_contents($filePath, $filePathOrContents)) { @@ -40,13 +41,11 @@ public function getAttachmentFileName($filePathOrContents, $type) if (!isset($type)) { $type = $this->guessFileMimeType($filePath); - $this->type = $type; } - $fileExtension = $this->guessFileExtension($type); - $fileSha1 = uniqid(sha1_file($filePath)); $outputPath = parent::getOutputPath($fileSha1, $fileExtension); + if (!$this->copyFile($filePath, $outputPath)) { throw new AllureException("Failed to copy attachment from $filePath to $outputPath."); } @@ -56,51 +55,52 @@ public function getAttachmentFileName($filePathOrContents, $type) /** * Copies file from one path to another. Wrapper for mocking in unit test. + * * @param string $filePath * @param string $outputPath + * * @return boolean + * @throws TestFrameworkException */ - private function copyFile($filePath, $outputPath) + private function copyFile(string $filePath, string $outputPath): bool { + if (MftfApplicationConfig::getConfig()->getPhase() === MftfApplicationConfig::UNIT_TEST_PHASE) { + return true; + } return copy($filePath, $outputPath); } /** - * Copy of parent private function + * Copy of parent private function. + * * @param string $filePath + * * @return string */ - private function guessFileMimeType($filePath) + private function guessFileMimeType(string $filePath): string { $type = MimeTypes::getDefault()->guessMimeType($filePath); + if (!isset($type)) { - return DEFAULT_MIME_TYPE; + return self::DEFAULT_MIME_TYPE; } return $type; } /** - * Copy of parent private function + * Copy of parent private function. + * * @param string $mimeType + * * @return string */ - private function guessFileExtension($mimeType) + private function guessFileExtension(string $mimeType): string { $candidate = MimeTypes::getDefault()->getExtensions($mimeType); + if (empty($candidate)) { - return DEFAULT_FILE_EXTENSION; + return self::DEFAULT_FILE_EXTENSION; } return reset($candidate); } - - /** - * Copy of parent private function - * @param string $sha1 - * @param string $extension - * @return string - */ - public function getOutputFileName($sha1, $extension) - { - return $sha1 . '-attachment.' . $extension; - } } diff --git a/src/Magento/FunctionalTestingFramework/Code/Reader/ClassReader.php b/src/Magento/FunctionalTestingFramework/Code/Reader/ClassReader.php index bc29d47fc..f3e67e35b 100644 --- a/src/Magento/FunctionalTestingFramework/Code/Reader/ClassReader.php +++ b/src/Magento/FunctionalTestingFramework/Code/Reader/ClassReader.php @@ -13,6 +13,7 @@ class ClassReader implements ClassReaderInterface * @param string $className * @return array|null * @throws \ReflectionException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function getConstructor($className) { @@ -24,9 +25,13 @@ public function getConstructor($className) /** @var $parameter \ReflectionParameter */ foreach ($constructor->getParameters() as $parameter) { try { + $paramType = $parameter->getType(); + $name = ($paramType && method_exists($paramType, 'isBuiltin') && !$paramType->isBuiltin()) + ? new \ReflectionClass($paramType->getName()) + : null; $result[] = [ $parameter->getName(), - $parameter->getClass() !== null ? $parameter->getClass()->getName() : null, + $name !== null ? $name->getName() : null, !$parameter->isOptional(), $parameter->isOptional() ? ($parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null) diff --git a/src/Magento/FunctionalTestingFramework/Codeception/Module/Sequence.php b/src/Magento/FunctionalTestingFramework/Codeception/Module/Sequence.php new file mode 100644 index 000000000..a7d763cfc --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Codeception/Module/Sequence.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\FunctionalTestingFramework\Codeception\Module; + +use Codeception\Module; +use Codeception\Exception\ModuleException; +use Codeception\TestInterface; + +/** + * Class Sequence + * Implemented here as a replacement for codeception/module-sequence due to PHP 8.4 deprecation errors. + * This class can be removed when PHP 8.4 compatibility is updated in codeception/module-sequence. + */ +class Sequence extends Module +{ + /** + * @var array<int|string,string> + */ + public static array $hash = [];// phpcs:ignore + + /** + * @var array<int|string,string> + */ + public static array $suiteHash = [];// phpcs:ignore + + /** + * @var string + */ + public static string $prefix = '';// phpcs:ignore + + /** + * @var array<string, string> + */ + protected array $config = ['prefix' => '{id}_'];// phpcs:ignore + + /** + * Initialise method + * @return void + */ + public function _initialize(): void + { + static::$prefix = $this->config['prefix']; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * after method + * @return void + */ + public function _after(TestInterface $test): void + { + self::$hash = []; + } + + /** + * after suite method + * @return void + */ + public function _afterSuite(): void + { + self::$suiteHash = []; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Codeception/Subscriber/Console.php b/src/Magento/FunctionalTestingFramework/Codeception/Subscriber/Console.php index 90f04abae..fc80483ec 100644 --- a/src/Magento/FunctionalTestingFramework/Codeception/Subscriber/Console.php +++ b/src/Magento/FunctionalTestingFramework/Codeception/Subscriber/Console.php @@ -70,9 +70,11 @@ public function __construct($extensionOptions = [], $options = []) * @return void * @throws \Exception */ - public function startTest(TestEvent $e) + public function startTest(TestEvent $e): void { - $test = $e->getTest()->getTestClass(); + $test = $e->getTest(); + $testReflection = new \ReflectionClass($test); + try { $testReflection = new \ReflectionClass($test); $isDeprecated = preg_match_all(self::DEPRECATED_NOTICE, $testReflection->getDocComment(), $match); @@ -99,7 +101,7 @@ public function startTest(TestEvent $e) * @param StepEvent $e * @return void */ - public function beforeStep(StepEvent $e) + public function beforeStep(StepEvent $e): void { if ($this->silent or !$this->steps or !$e->getTest() instanceof ScenarioDriven) { return; @@ -119,7 +121,7 @@ public function beforeStep(StepEvent $e) } $metaStep = $e->getStep()->getMetaStep(); - if ($metaStep and $this->metaStep != $metaStep) { + if ($metaStep and $this->metaStep !== $metaStep) { $this->message(' ' . $metaStep->getPrefix()) ->style('bold') ->append($metaStep->__toString()) @@ -158,11 +160,11 @@ public function afterStep(StepEvent $e) */ private function printStepKeys(Step $step) { - if ($step instanceof Comment and $step->__toString() == '') { + if ($step instanceof Comment and $step->__toString() === '') { return; // don't print empty comments } - $stepKey = $this->retrieveStepKey($step->getLine()); + $stepKey = $this->retrieveStepKey($step); $isActionGroup = (strpos($step->__toString(), ActionGroupObject::ACTION_GROUP_CONTEXT_START) !== false); if ($isActionGroup) { @@ -193,7 +195,7 @@ private function printStepKeys(Step $step) $stepString = str_replace( [ActionGroupObject::ACTION_GROUP_CONTEXT_START, ActionGroupObject::ACTION_GROUP_CONTEXT_END], '', - $step->toString(150) + $step->toString(1000) ); $msg->append(OutputFormatter::escape($stepString)); @@ -220,13 +222,14 @@ private function message($string = '') /** * Reading stepKey from file. * - * @param string $stepLine + * @param Step $step * @return string|null */ - private function retrieveStepKey($stepLine) + private function retrieveStepKey(Step $step) { $stepKey = null; - list($filePath, $stepLine) = explode(":", $stepLine); + $stepLine = $step->getLineNumber(); + $filePath = $step->getFilePath(); $stepLine = $stepLine - 1; if (!array_key_exists($filePath, $this->testFiles)) { diff --git a/src/Magento/FunctionalTestingFramework/Composer/ComposerInstall.php b/src/Magento/FunctionalTestingFramework/Composer/ComposerInstall.php index f10c0b418..151fdc86e 100644 --- a/src/Magento/FunctionalTestingFramework/Composer/ComposerInstall.php +++ b/src/Magento/FunctionalTestingFramework/Composer/ComposerInstall.php @@ -50,7 +50,7 @@ public function isInstalledPackageOfType($packageName, $packageType) { /** @var CompletePackageInterface $package */ foreach ($this->getLocker()->getLockedRepository()->getPackages() as $package) { - if (($package->getName() == $packageName) && ($package->getType() == $packageType)) { + if (($package->getName() === $packageName) && ($package->getType() === $packageType)) { return true; } } @@ -67,7 +67,7 @@ public function getInstalledTestPackages() $packages = []; /** @var CompletePackageInterface $package */ foreach ($this->getLocker()->getLockedRepository()->getPackages() as $package) { - if ($package->getType() == self::TEST_MODULE_PACKAGE_TYPE) { + if ($package->getType() === self::TEST_MODULE_PACKAGE_TYPE) { $packages[$package->getName()] = [ self::PACKAGE_NAME => $package->getName(), self::PACKAGE_TYPE => $package->getType(), diff --git a/src/Magento/FunctionalTestingFramework/Composer/ComposerPackage.php b/src/Magento/FunctionalTestingFramework/Composer/ComposerPackage.php index 0a6bfdca2..5e2d21c15 100644 --- a/src/Magento/FunctionalTestingFramework/Composer/ComposerPackage.php +++ b/src/Magento/FunctionalTestingFramework/Composer/ComposerPackage.php @@ -116,7 +116,7 @@ public function getSuggestedMagentoModules() */ public function isMftfTestPackage() { - return ($this->getType() == self::TEST_MODULE_PACKAGE_TYPE) ? true : false; + return $this->getType() === self::TEST_MODULE_PACKAGE_TYPE; } /** diff --git a/src/Magento/FunctionalTestingFramework/Config/Converter.php b/src/Magento/FunctionalTestingFramework/Config/Converter.php index cf61805dc..14b96907e 100644 --- a/src/Magento/FunctionalTestingFramework/Config/Converter.php +++ b/src/Magento/FunctionalTestingFramework/Config/Converter.php @@ -91,7 +91,7 @@ protected function convertXml($elements) foreach ($elements as $element) { if ($element instanceof \DOMElement) { - if ($element->getAttribute('remove') == 'true') { + if ($element->getAttribute('remove') === 'true') { // Remove element continue; } @@ -119,7 +119,7 @@ protected function convertXml($elements) } elseif (!empty($elementData)) { $result[$element->nodeName][] = $elementData; } - } elseif ($element->nodeType == XML_TEXT_NODE && trim($element->nodeValue) != '') { + } elseif ($element->nodeType === XML_TEXT_NODE && trim($element->nodeValue) !== '') { return ['value' => $element->nodeValue]; } } @@ -156,9 +156,9 @@ protected function getElementKey(\DOMElement $element) protected function isKeyAttribute(\DOMElement $element, \DOMAttr $attribute) { if (isset($this->idAttributes[$element->nodeName])) { - return $attribute->name == $this->idAttributes[$element->nodeName]; + return $attribute->name === $this->idAttributes[$element->nodeName]; } else { - return $attribute->name == self::NAME_ATTRIBUTE; + return $attribute->name === self::NAME_ATTRIBUTE; } } @@ -174,7 +174,7 @@ protected function getAttributes(\DOMElement $element) if ($element->hasAttributes()) { /** @var \DomAttr $attribute */ foreach ($element->attributes as $attribute) { - if (trim($attribute->nodeValue) != '' && !$this->isKeyAttribute($element, $attribute)) { + if (trim($attribute->nodeValue) !== '' && !$this->isKeyAttribute($element, $attribute)) { $attributes[$attribute->nodeName] = $this->castNumeric($attribute->nodeValue); } } diff --git a/src/Magento/FunctionalTestingFramework/Config/Converter/Dom/Flat.php b/src/Magento/FunctionalTestingFramework/Config/Converter/Dom/Flat.php index 001a811d9..742ef64f3 100644 --- a/src/Magento/FunctionalTestingFramework/Config/Converter/Dom/Flat.php +++ b/src/Magento/FunctionalTestingFramework/Config/Converter/Dom/Flat.php @@ -41,7 +41,7 @@ protected function getNodeAttributes(\DOMNode $node) $attributes = $node->attributes ?: []; /** @var \DOMNode $attribute */ foreach ($attributes as $attribute) { - if ($attribute->nodeType == XML_ATTRIBUTE_NODE) { + if ($attribute->nodeType === XML_ATTRIBUTE_NODE) { $result[$attribute->nodeName] = $attribute->nodeValue; } } @@ -77,7 +77,7 @@ public function convert(\DOMNode $source, $basePath = '') $value = []; /** @var \DOMNode $node */ foreach ($source->childNodes as $node) { - if ($node->nodeType == XML_ELEMENT_NODE) { + if ($node->nodeType === XML_ELEMENT_NODE) { $nodeName = $node->nodeName; $nodePath = $basePath . '/' . $nodeName; @@ -107,8 +107,8 @@ public function convert(\DOMNode $source, $basePath = '') } else { $value[$nodeName] = $nodeData; } - } elseif ($node->nodeType == XML_CDATA_SECTION_NODE - || ($node->nodeType == XML_TEXT_NODE && trim($node->nodeValue) != '') + } elseif ($node->nodeType === XML_CDATA_SECTION_NODE + || ($node->nodeType === XML_TEXT_NODE && trim($node->nodeValue) !== '') ) { $value = $node->nodeValue; break; diff --git a/src/Magento/FunctionalTestingFramework/Config/Data.php b/src/Magento/FunctionalTestingFramework/Config/Data.php index a34ec1bf3..e9b9c377d 100644 --- a/src/Magento/FunctionalTestingFramework/Config/Data.php +++ b/src/Magento/FunctionalTestingFramework/Config/Data.php @@ -56,7 +56,7 @@ public function merge(array $config) * @param null|mixed $default * @return array|mixed|null */ - public function get($path = null, $default = null) + public function get(mixed $path = null, mixed $default = null) { if ($path === null) { return $this->data; @@ -94,7 +94,7 @@ public function setFileName($fileName) * @param string|null $scope * @return void */ - public function load($scope = null) + public function load(?string $scope = null) { $this->merge( $this->reader->read($scope) diff --git a/src/Magento/FunctionalTestingFramework/Config/DataInterface.php b/src/Magento/FunctionalTestingFramework/Config/DataInterface.php index 6af873001..80636c2e4 100644 --- a/src/Magento/FunctionalTestingFramework/Config/DataInterface.php +++ b/src/Magento/FunctionalTestingFramework/Config/DataInterface.php @@ -26,7 +26,7 @@ public function merge(array $config); * @param mixed|null $default * @return mixed|null */ - public function get($key = null, $default = null); + public function get(mixed $key = null, mixed $default = null); // @codingStandardsIgnoreEnd /** @@ -35,7 +35,7 @@ public function get($key = null, $default = null); * @param string|null $scope * @return void */ - public function load($scope = null); + public function load(?string $scope = null); /** * Set name of the config file diff --git a/src/Magento/FunctionalTestingFramework/Config/Dom.php b/src/Magento/FunctionalTestingFramework/Config/Dom.php index 6d10811ca..91de64ae1 100644 --- a/src/Magento/FunctionalTestingFramework/Config/Dom.php +++ b/src/Magento/FunctionalTestingFramework/Config/Dom.php @@ -207,7 +207,7 @@ protected function replaceNodeValue($parentPath, \DOMElement $node, \DOMElement */ protected function isTextNode($node) { - return $node->childNodes->length == 1 && $node->childNodes->item(0) instanceof \DOMText; + return $node->childNodes->length === 1 && $node->childNodes->item(0) instanceof \DOMText; } /** @@ -246,8 +246,8 @@ protected function getNodePathByParent(\DOMElement $node, $parentPath) $idAttribute = $this->nodeMergingConfig->getIdAttribute($path); if ($idAttribute) { foreach (explode('|', $idAttribute) as $idAttributeValue) { - if ($value = $node->getAttribute($idAttributeValue)) { - $path .= "[@{$idAttributeValue}='{$value}']"; + if ($node->hasAttribute($idAttributeValue)) { + $path .= "[@{$idAttributeValue}='" . $node->getAttribute($idAttributeValue) . "']"; break; } } @@ -273,7 +273,7 @@ protected function getMatchedNode($nodePath) $node = null; if ($matchedNodes->length > 1) { throw new \Exception("More than one node matching the query: {$nodePath}"); - } elseif ($matchedNodes->length == 1) { + } elseif ($matchedNodes->length === 1) { $node = $matchedNodes->item(0); } return $node; diff --git a/src/Magento/FunctionalTestingFramework/Config/Dom/ArrayNodeConfig.php b/src/Magento/FunctionalTestingFramework/Config/Dom/ArrayNodeConfig.php index 5f03450dc..d224bb882 100644 --- a/src/Magento/FunctionalTestingFramework/Config/Dom/ArrayNodeConfig.php +++ b/src/Magento/FunctionalTestingFramework/Config/Dom/ArrayNodeConfig.php @@ -64,7 +64,7 @@ public function __construct( public function isNumericArray($nodeXpath) { foreach ($this->numericArrays as $pathPattern) { - if ($this->nodePathMatcher->match($pathPattern, $nodeXpath)) { + if ($this->nodePathMatcher->pathMatch($pathPattern, $nodeXpath)) { return true; } } @@ -84,7 +84,7 @@ public function getAssocArrayKeyAttribute($nodeXpath) } foreach ($this->assocArrays as $pathPattern => $keyAttribute) { - if ($this->nodePathMatcher->match($pathPattern, $nodeXpath)) { + if ($this->nodePathMatcher->pathMatch($pathPattern, $nodeXpath)) { return $keyAttribute; } } diff --git a/src/Magento/FunctionalTestingFramework/Config/Dom/NodeMergingConfig.php b/src/Magento/FunctionalTestingFramework/Config/Dom/NodeMergingConfig.php index e4231c8ec..f02b1dc5c 100644 --- a/src/Magento/FunctionalTestingFramework/Config/Dom/NodeMergingConfig.php +++ b/src/Magento/FunctionalTestingFramework/Config/Dom/NodeMergingConfig.php @@ -44,7 +44,7 @@ public function __construct(NodePathMatcher $nodePathMatcher, array $idAttribute public function getIdAttribute($nodeXpath) { foreach ($this->idAttributes as $pathPattern => $idAttribute) { - if ($this->nodePathMatcher->match($pathPattern, $nodeXpath)) { + if ($this->nodePathMatcher->pathMatch($pathPattern, $nodeXpath)) { return $idAttribute; } } diff --git a/src/Magento/FunctionalTestingFramework/Config/Dom/NodePathMatcher.php b/src/Magento/FunctionalTestingFramework/Config/Dom/NodePathMatcher.php index 2857bbea6..b7c3f9185 100644 --- a/src/Magento/FunctionalTestingFramework/Config/Dom/NodePathMatcher.php +++ b/src/Magento/FunctionalTestingFramework/Config/Dom/NodePathMatcher.php @@ -17,7 +17,7 @@ class NodePathMatcher * @param string $xpathSubject Example: '/some[@attr="value"]/static/ns:path'. * @return boolean */ - public function match($pathPattern, $xpathSubject) + public function pathMatch($pathPattern, $xpathSubject) { $pathSubject = $this->simplifyXpath($xpathSubject); $pathPattern = '#^' . $pathPattern . '$#'; diff --git a/src/Magento/FunctionalTestingFramework/Config/FileResolver/Mask.php b/src/Magento/FunctionalTestingFramework/Config/FileResolver/Mask.php index e3f29f954..7db1514af 100644 --- a/src/Magento/FunctionalTestingFramework/Config/FileResolver/Mask.php +++ b/src/Magento/FunctionalTestingFramework/Config/FileResolver/Mask.php @@ -28,7 +28,7 @@ class Mask implements FileResolverInterface * * @param ModuleResolver|null $moduleResolver */ - public function __construct(ModuleResolver $moduleResolver = null) + public function __construct(?ModuleResolver $moduleResolver = null) { if ($moduleResolver) { $this->moduleResolver = $moduleResolver; diff --git a/src/Magento/FunctionalTestingFramework/Config/FileResolver/Module.php b/src/Magento/FunctionalTestingFramework/Config/FileResolver/Module.php index 7cff19c87..18432c876 100644 --- a/src/Magento/FunctionalTestingFramework/Config/FileResolver/Module.php +++ b/src/Magento/FunctionalTestingFramework/Config/FileResolver/Module.php @@ -27,7 +27,7 @@ class Module implements FileResolverInterface * @param ModuleResolver|null $moduleResolver * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function __construct(ModuleResolver $moduleResolver = null) + public function __construct(?ModuleResolver $moduleResolver = null) { $this->moduleResolver = ModuleResolver::getInstance(); } diff --git a/src/Magento/FunctionalTestingFramework/Config/FileResolver/Root.php b/src/Magento/FunctionalTestingFramework/Config/FileResolver/Root.php index 0ff6e4a77..230586852 100644 --- a/src/Magento/FunctionalTestingFramework/Config/FileResolver/Root.php +++ b/src/Magento/FunctionalTestingFramework/Config/FileResolver/Root.php @@ -32,6 +32,19 @@ public function get($filename, $scope) . DIRECTORY_SEPARATOR . '*.xml' ); + // include root suite dir when running standalone version + $altPath = FilePathFormatter::format(MAGENTO_BP) . 'dev/tests/acceptance'; + + if (realpath($altPath) && ($altPath !== TESTS_BP)) { + $paths = array_merge( + $paths, + glob( + FilePathFormatter::format($altPath) . self::ROOT_SUITE_DIR + . DIRECTORY_SEPARATOR . '*.xml' + ) + ); + } + // Then merge this path into the module based paths // Since we are sharing this code with Module based resolution we will unnecessarily glob against modules in the // dev/tests dir tree, however as we plan to migrate to app/code this will be a temporary unneeded check. diff --git a/src/Magento/FunctionalTestingFramework/Config/MftfApplicationConfig.php b/src/Magento/FunctionalTestingFramework/Config/MftfApplicationConfig.php index 5181e8dc9..7685ac919 100644 --- a/src/Magento/FunctionalTestingFramework/Config/MftfApplicationConfig.php +++ b/src/Magento/FunctionalTestingFramework/Config/MftfApplicationConfig.php @@ -100,23 +100,15 @@ private function __construct( $this->phase = $phase; $this->verboseEnabled = $verboseEnabled; - - //TODO: overriding pipeline config, to be removed for MFTF 3.0.0 - if (strtolower($debugLevel) === 'none') { - $debugLevel = self::LEVEL_DEFAULT; - } - - if (isset($debugLevel) && !in_array(strtolower($debugLevel), self::MFTF_DEBUG_LEVEL)) { + if (!in_array(strtolower($debugLevel), self::MFTF_DEBUG_LEVEL)) { throw new TestFrameworkException("{$debugLevel} is not a debug level. Use 'DEFAULT' or 'DEVELOPER'"); } switch (strtolower($debugLevel)) { - case self::LEVEL_DEVELOPER: case self::LEVEL_DEFAULT: - $this->debugLevel = $debugLevel; + $this->debugLevel = self::LEVEL_DEFAULT; break; - case null: + default: $this->debugLevel = self::LEVEL_DEVELOPER; - break; } $this->allowSkipped = $allowSkipped; $this->filterList = new FilterList($filters); @@ -143,7 +135,7 @@ public static function create( $allowSkipped = false, $filters = [] ) { - if (self::$MFTF_APPLICATION_CONTEXT == null) { + if (self::$MFTF_APPLICATION_CONTEXT === null) { self::$MFTF_APPLICATION_CONTEXT = new MftfApplicationConfig( $forceGenerate, @@ -167,7 +159,7 @@ public static function getConfig() // TODO explicitly set this with AcceptanceTester or MagentoWebDriver // during execution we cannot guarantee the use of the robofile so we return the default application config, // we don't want to set the application context in case the user explicitly does so at a later time. - if (self::$MFTF_APPLICATION_CONTEXT == null) { + if (self::$MFTF_APPLICATION_CONTEXT === null) { return new MftfApplicationConfig(); } @@ -202,7 +194,7 @@ public function verboseEnabled() */ public function getDebugLevel() { - return $this->debugLevel ?? getenv('MFTF_DEBUG'); + return $this->debugLevel; } /** diff --git a/src/Magento/FunctionalTestingFramework/Config/Reader.php b/src/Magento/FunctionalTestingFramework/Config/Reader.php index 0c996f14c..b627dec79 100644 --- a/src/Magento/FunctionalTestingFramework/Config/Reader.php +++ b/src/Magento/FunctionalTestingFramework/Config/Reader.php @@ -48,9 +48,9 @@ public function __construct( $this->fileName = $fileName; $this->idAttributes = array_replace($this->idAttributes, $idAttributes); $this->schemaFile = $schemaLocator->getSchema(); - $this->isValidated = $validationState->isValidated(); - $this->perFileSchema = $schemaLocator->getPerFileSchema() && - $this->isValidated ? $schemaLocator->getPerFileSchema() : null; + $isValidated = $validationState->isValidated(); + $this->perFileSchema = $schemaLocator->getPerFileSchema() && $isValidated ? + $schemaLocator->getPerFileSchema() : null; $this->domDocumentClass = $domDocumentClass; $this->defaultScope = $defaultScope; } diff --git a/src/Magento/FunctionalTestingFramework/Config/Reader/Filesystem.php b/src/Magento/FunctionalTestingFramework/Config/Reader/Filesystem.php index ab7d64ea9..a9dc6785c 100644 --- a/src/Magento/FunctionalTestingFramework/Config/Reader/Filesystem.php +++ b/src/Magento/FunctionalTestingFramework/Config/Reader/Filesystem.php @@ -6,6 +6,7 @@ namespace Magento\FunctionalTestingFramework\Config\Reader; use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Exceptions\FastFailException; use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; /** @@ -123,7 +124,7 @@ public function __construct( * @param string|null $scope * @return array */ - public function read($scope = null) + public function read(?string $scope = null) { $scope = $scope ?: $this->defaultScope; $fileList = $this->fileResolver->get($this->fileName, $scope); @@ -147,7 +148,7 @@ protected function readFiles($fileList) /** @var \Magento\FunctionalTestingFramework\Config\Dom $configMerger */ $configMerger = null; $debugLevel = MftfApplicationConfig::getConfig()->getDebugLevel(); - foreach ($fileList as $key => $content) { + foreach ($fileList as $content) { //check if file is empty and continue to next if it is if (!$this->verifyFileEmpty($content, $fileList->getFilename())) { continue; @@ -158,7 +159,7 @@ protected function readFiles($fileList) } else { $configMerger->merge($content); } - if (strcasecmp($debugLevel, MftfApplicationConfig::LEVEL_DEVELOPER) == 0) { + if (strcasecmp($debugLevel, MftfApplicationConfig::LEVEL_DEVELOPER) === 0) { $this->validateSchema($configMerger, $fileList->getFilename()); } } catch (\Magento\FunctionalTestingFramework\Config\Dom\ValidationException $e) { @@ -209,7 +210,7 @@ protected function verifyFileEmpty($content, $fileName) { if (empty($content)) { if (MftfApplicationConfig::getConfig()->verboseEnabled()) { - LoggingUtil::getInstance()->getLogger(Filesystem::class)->warn( + LoggingUtil::getInstance()->getLogger(Filesystem::class)->warning( "XML File is empty.", ["File" => $fileName] ); @@ -227,7 +228,7 @@ protected function verifyFileEmpty($content, $fileName) * @throws \Exception * @return void */ - protected function validateSchema($configMerger, $filename = null) + protected function validateSchema($configMerger, ?string $filename = null) { if ($this->validationState->isValidationRequired()) { $errors = []; @@ -240,7 +241,7 @@ protected function validateSchema($configMerger, $filename = null) true ); } - throw new \Exception("Schema validation errors found in xml file(s)" . $filename); + throw new FastFailException("Schema validation errors found in xml file(s)" . $filename); } } } diff --git a/src/Magento/FunctionalTestingFramework/Config/Reader/MftfFilesystem.php b/src/Magento/FunctionalTestingFramework/Config/Reader/MftfFilesystem.php index 3740d683f..88a176a3c 100644 --- a/src/Magento/FunctionalTestingFramework/Config/Reader/MftfFilesystem.php +++ b/src/Magento/FunctionalTestingFramework/Config/Reader/MftfFilesystem.php @@ -42,7 +42,7 @@ public function readFiles($fileList) $configMerger->merge($content, $fileList->getFilename(), $exceptionCollector); } // run per file validation with generate:tests -d - if (strcasecmp($debugLevel, MftfApplicationConfig::LEVEL_DEVELOPER) == 0) { + if (strcasecmp($debugLevel, MftfApplicationConfig::LEVEL_DEVELOPER) === 0) { $this->validateSchema($configMerger, $fileList->getFilename()); } } catch (\Magento\FunctionalTestingFramework\Config\Dom\ValidationException $e) { @@ -52,7 +52,7 @@ public function readFiles($fileList) $exceptionCollector->throwException(); //run validation on merged file with generate:tests - if (strcasecmp($debugLevel, MftfApplicationConfig::LEVEL_DEFAULT) == 0) { + if (strcasecmp($debugLevel, MftfApplicationConfig::LEVEL_DEFAULT) === 0) { $this->validateSchema($configMerger); } diff --git a/src/Magento/FunctionalTestingFramework/Config/ReaderInterface.php b/src/Magento/FunctionalTestingFramework/Config/ReaderInterface.php index 9e64a6715..9fc374f7b 100644 --- a/src/Magento/FunctionalTestingFramework/Config/ReaderInterface.php +++ b/src/Magento/FunctionalTestingFramework/Config/ReaderInterface.php @@ -19,5 +19,5 @@ interface ReaderInterface * @param string|null $scope * @return array */ - public function read($scope = null); + public function read(?string $scope = null); } diff --git a/src/Magento/FunctionalTestingFramework/Config/ValidationState.php b/src/Magento/FunctionalTestingFramework/Config/ValidationState.php index f08dc141a..d610f428a 100644 --- a/src/Magento/FunctionalTestingFramework/Config/ValidationState.php +++ b/src/Magento/FunctionalTestingFramework/Config/ValidationState.php @@ -37,6 +37,6 @@ public function __construct($appMode) */ public function isValidationRequired() { - return $this->appMode == 'developer'; // @todo + return $this->appMode === 'developer'; // @todo } } diff --git a/src/Magento/FunctionalTestingFramework/Console/BaseGenerateCommand.php b/src/Magento/FunctionalTestingFramework/Console/BaseGenerateCommand.php index f3becf935..0c209d8f2 100644 --- a/src/Magento/FunctionalTestingFramework/Console/BaseGenerateCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/BaseGenerateCommand.php @@ -8,12 +8,15 @@ namespace Magento\FunctionalTestingFramework\Console; +use Magento\FunctionalTestingFramework\Exceptions\FastFailException; use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; use Magento\FunctionalTestingFramework\Util\Path\FilePathFormatter; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Output\OutputInterface; use Magento\FunctionalTestingFramework\Util\Filesystem\DirSetupUtil; use Magento\FunctionalTestingFramework\Util\TestGenerator; @@ -21,9 +24,40 @@ use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; use Symfony\Component\Console\Style\SymfonyStyle; +/** + * Class BaseGenerateCommand + * @package Magento\FunctionalTestingFramework\Console + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class BaseGenerateCommand extends Command { const MFTF_NOTICES = "Placeholder text for MFTF notices\n"; + const CODECEPT_RUN = 'codecept:run'; + const CODECEPT_RUN_FUNCTIONAL = self::CODECEPT_RUN . ' functional '; + const CODECEPT_RUN_OPTION_NO_EXIT = ' --no-exit '; + const FAILED_FILE = 'failed'; + + /** + * Enable pause() + * + * @var boolean + */ + private $enablePause = null; + + /** + * Full path to '_output' dir + * + * @var string + */ + private $testsOutputDir = null; + + /** + * String contains all 'failed' tests + * + * @var string + */ + private $allFailed; /** * Console output style @@ -32,6 +66,13 @@ class BaseGenerateCommand extends Command */ protected $ioStyle = null; + /** + * Full path to 'failed' file + * + * @var string + */ + protected $testsFailedFile = null; + /** * Configures the base command. * @@ -87,7 +128,7 @@ protected function removeGeneratedDirectory(OutputInterface $output, bool $verbo * Returns an array of test configuration to be used as an argument for generation of tests * @param array $tests * @return false|string - * @throws \Magento\FunctionalTestingFramework\Exceptions\XmlException + * @throws FastFailException */ protected function getTestAndSuiteConfiguration(array $tests) { @@ -125,7 +166,8 @@ protected function getTestAndSuiteConfiguration(array $tests) * Returns an array of test configuration to be used as an argument for generation of tests * This function uses group or suite names for generation * @return false|string - * @throws \Magento\FunctionalTestingFramework\Exceptions\XmlException + * @throws FastFailException + * @throws TestFrameworkException */ protected function getGroupAndSuiteConfiguration(array $groupOrSuiteNames) { @@ -218,4 +260,133 @@ protected function showMftfNotices(OutputInterface $output) $output->writeln(self::MFTF_NOTICES); } } + + /** + * Return if pause() is enabled + * + * @return boolean + */ + protected function pauseEnabled() + { + if (null === $this->enablePause) { + if (getenv('ENABLE_PAUSE') === 'true') { + $this->enablePause = true; + } else { + $this->enablePause = false; + } + } + return $this->enablePause; + } + + /** + * Runs the bin/mftf codecept:run command and returns exit code + * + * @param string $commandStr + * @param OutputInterface $output + * @return integer + * @throws \Exception + */ + protected function codeceptRunTest(string $commandStr, OutputInterface $output) + { + $input = new StringInput($commandStr); + $command = $this->getApplication()->find(self::CODECEPT_RUN); + return $command->run($input, $output); + } + + /** + * Return tests _output directory + * + * @return string + * @throws TestFrameworkException + */ + protected function getTestsOutputDir() + { + if (!$this->testsOutputDir) { + $this->testsOutputDir = FilePathFormatter::format(TESTS_BP) . + "tests" . + DIRECTORY_SEPARATOR . + "_output" . + DIRECTORY_SEPARATOR; + } + + return $this->testsOutputDir; + } + + /** + * Save 'failed' tests + * + * @return void + */ + protected function appendRunFailed() + { + try { + if (!$this->testsFailedFile) { + $this->testsFailedFile = $this->getTestsOutputDir() . self::FAILED_FILE; + } + + if (file_exists($this->testsFailedFile)) { + // Save 'failed' tests + $contents = file_get_contents($this->testsFailedFile); + if ($contents !== false && !empty($contents)) { + $this->allFailed .= trim($contents) . PHP_EOL; + } + } + } catch (TestFrameworkException $e) { + } + } + + /** + * Apply 'allFailed' in 'failed' file + * + * @return void + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + protected function applyAllFailed() + { + try { + if (!$this->testsFailedFile) { + $this->testsFailedFile = $this->getTestsOutputDir() . self::FAILED_FILE; + } + + if (!empty($this->allFailed)) { + // Update 'failed' with content from 'allFailed' + if (file_exists($this->testsFailedFile)) { + rename($this->testsFailedFile, $this->testsFailedFile . '.copy'); + } + if (file_put_contents($this->testsFailedFile, $this->allFailed) === false + && file_exists($this->testsFailedFile . '.copy')) { + rename($this->testsFailedFile . '.copy', $this->testsFailedFile); + } + if (file_exists($this->testsFailedFile . '.copy')) { + unlink($this->testsFailedFile . '.copy'); + } + } + } catch (TestFrameworkException $e) { + } + } + /** + * Codeception creates default xml file with name report.xml . + * This function renames default file name with name of the test. + * + * @param string $xml + * @param string $fileName + * @param OutputInterface $output + * @return void + * @throws \Exception + */ + public function movingXMLFileFromSourceToDestination($xml, $fileName, $output) + { + if(!empty($xml) && file_exists($this->getTestsOutputDir().'report.xml')) { + if (!file_exists($this->getTestsOutputDir().'xml')) { + mkdir($this->getTestsOutputDir().'xml' , 0777, true); + } + $fileName = str_replace("Cest.php", "",$fileName); + $existingFileName = $this->getTestsOutputDir().'report.xml'; + $newFileName = $this->getTestsOutputDir().'xml/'.$fileName.'_report.xml'; + $output->writeln( "<info>".sprintf(" report.xml file is moved to ". + $this->getTestsOutputDir().'xml/'. ' location with the new name '.$fileName.'_report.xml')."</info>") ; + rename($existingFileName , $newFileName); + } + } + } diff --git a/src/Magento/FunctionalTestingFramework/Console/BuildProjectCommand.php b/src/Magento/FunctionalTestingFramework/Console/BuildProjectCommand.php index cc8d9776e..33cc4bc71 100644 --- a/src/Magento/FunctionalTestingFramework/Console/BuildProjectCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/BuildProjectCommand.php @@ -28,7 +28,8 @@ */ class BuildProjectCommand extends Command { - const DEFAULT_YAML_INLINE_DEPTH = 10; + private const SUCCESS_EXIT_CODE = 0; + public const DEFAULT_YAML_INLINE_DEPTH = 10; /** * Env processor manages .env files. @@ -65,11 +66,11 @@ protected function configure() * * @param InputInterface $input * @param OutputInterface $output - * @return void + * @return integer * @throws \Exception - * @SuppressWarnings(PHPMD.UnusedLocalVariable) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $resetCommand = new CleanProjectCommand(); $resetOptions = new ArrayInput([]); @@ -91,7 +92,7 @@ protected function execute(InputInterface $input, OutputInterface $output) // TODO can we just import the codecept symfony command? $codeceptBuildCommand = realpath(PROJECT_ROOT . '/vendor/bin/codecept') . ' build'; - $process = new Process($codeceptBuildCommand); + $process = Process::fromShellCommandline($codeceptBuildCommand); $process->setWorkingDirectory(TESTS_BP); $process->setIdleTimeout(600); $process->setTimeout(0); @@ -107,12 +108,13 @@ function ($type, $buffer) use ($output) { ); } - // Temporary enable upgrade at build time for testing - //if ($input->getOption('upgrade')) { + if ($input->getOption('upgrade')) { $upgradeCommand = new UpgradeTestsCommand(); $upgradeOptions = new ArrayInput([]); $upgradeCommand->run($upgradeOptions, $output); - //} + } + + return self::SUCCESS_EXIT_CODE; } /** diff --git a/src/Magento/FunctionalTestingFramework/Console/CleanProjectCommand.php b/src/Magento/FunctionalTestingFramework/Console/CleanProjectCommand.php index 641cfe1ed..6517d3ab1 100644 --- a/src/Magento/FunctionalTestingFramework/Console/CleanProjectCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/CleanProjectCommand.php @@ -18,6 +18,8 @@ class CleanProjectCommand extends Command { + private const SUCCESS_EXIT_CODE = 0; + /** * Configures the current command. * @@ -37,11 +39,11 @@ protected function configure() * * @param InputInterface $input * @param OutputInterface $output - * @return void + * @return integer * @throws \Symfony\Component\Console\Exception\LogicException * @throws TestFrameworkException */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $configFiles = [ // codeception.yml file for top level config @@ -97,5 +99,7 @@ protected function execute(InputInterface $input, OutputInterface $output) } $output->writeln('mftf files removed from filesystem.'); + + return self::SUCCESS_EXIT_CODE; } } diff --git a/src/Magento/FunctionalTestingFramework/Console/Codecept/CodeceptCommandUtil.php b/src/Magento/FunctionalTestingFramework/Console/Codecept/CodeceptCommandUtil.php new file mode 100644 index 000000000..1a469b179 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Console/Codecept/CodeceptCommandUtil.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\FunctionalTestingFramework\Console\Codecept; + +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Util\Path\FilePathFormatter; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\ArgvInput; + +class CodeceptCommandUtil +{ + const CODECEPTION_AUTOLOAD_FILE = PROJECT_ROOT . '/vendor/codeception/codeception/autoload.php'; + + /** + * Current working directory + * + * @var string + */ + private $cwd = null; + + // @codingStandardsIgnoreStart + /** + * Setup Codeception + * + * @param InputInterface $input + * @return void + */ + public function setup(InputInterface $input) + { + require_once realpath(self::CODECEPTION_AUTOLOAD_FILE); + + $tokens = preg_split('{\\s+}', $input->__toString()); + $tokens[0] = str_replace('codecept:', '', $tokens[0]); + \Closure::bind(function &(ArgvInput $input) use ($tokens) { + return $input->setTokens($tokens); + }, null, ArgvInput::class); + } + // @codingStandardsIgnoreEnd + + /** + * Save Codeception working directory + * + * @return void + * @throws TestFrameworkException + */ + public function setCodeceptCwd() + { + $this->cwd = getcwd(); + chdir(FilePathFormatter::format(TESTS_BP, false)); + } + + /** + * Restore current working directory + * + * @return void + */ + public function restoreCwd() + { + if ($this->cwd) { + chdir($this->cwd); + } + } +} diff --git a/src/Magento/FunctionalTestingFramework/Console/CodeceptRunCommand.php b/src/Magento/FunctionalTestingFramework/Console/CodeceptRunCommand.php new file mode 100644 index 000000000..35e5ddc36 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Console/CodeceptRunCommand.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\FunctionalTestingFramework\Console; + +use Codeception\Command\Run; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Magento\FunctionalTestingFramework\Console\Codecept\CodeceptCommandUtil; + +class CodeceptRunCommand extends Run +{ + /** + * Configures the current command + * + * @return void + */ + protected function configure():void + { + $this->setName('codecept:run') + ->setDescription( + "Wrapper command to vendor/bin/codecept:run. See https://codeception.com/docs/reference/Commands#Run" + ); + + parent::configure(); + } + + /** + * Executes the current command + * + * @param InputInterface $input + * @param OutputInterface $output + * @return integer + * @throws \Exception + */ + public function execute(InputInterface $input, OutputInterface $output): int + { + $commandUtil = new CodeceptCommandUtil(); + $commandUtil->setup($input); + $commandUtil->setCodeceptCwd(); + + try { + $exitCode = parent::execute($input, $output); + } catch (\Exception $e) { + throw new TestFrameworkException( + 'Make sure cest files are generated before running bin/mftf ' + . $this->getName() + . PHP_EOL + . $e->getMessage() + ); + } + + $commandUtil->restoreCwd(); + return $exitCode; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Console/CommandList.php b/src/Magento/FunctionalTestingFramework/Console/CommandList.php index f77c2c576..a31ebe175 100644 --- a/src/Magento/FunctionalTestingFramework/Console/CommandList.php +++ b/src/Magento/FunctionalTestingFramework/Console/CommandList.php @@ -30,11 +30,13 @@ public function __construct(array $commands = []) { $this->commands = [ 'build:project' => new BuildProjectCommand(), + 'codecept:run' => new CodeceptRunCommand(), 'doctor' => new DoctorCommand(), 'generate:suite' => new GenerateSuiteCommand(), 'generate:tests' => new GenerateTestsCommand(), 'generate:urn-catalog' => new GenerateDevUrnCommand(), 'reset' => new CleanProjectCommand(), + 'generate:failed' => new GenerateTestFailedCommand(), 'run:failed' => new RunTestFailedCommand(), 'run:group' => new RunTestGroupCommand(), 'run:manifest' => new RunManifestCommand(), diff --git a/src/Magento/FunctionalTestingFramework/Console/DoctorCommand.php b/src/Magento/FunctionalTestingFramework/Console/DoctorCommand.php index 6fc9afa55..d55aedd25 100644 --- a/src/Magento/FunctionalTestingFramework/Console/DoctorCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/DoctorCommand.php @@ -8,7 +8,7 @@ namespace Magento\FunctionalTestingFramework\Console; use Codeception\Configuration; -use Magento\FunctionalTestingFramework\Util\Path\UrlFormatter; +use Magento\FunctionalTestingFramework\DataTransport\Auth\WebApiAuth; use Symfony\Component\EventDispatcher\EventDispatcher; use Codeception\SuiteManager; use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; @@ -16,7 +16,6 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Magento\FunctionalTestingFramework\Util\ModuleResolver; use Magento\FunctionalTestingFramework\Module\MagentoWebDriver; use Magento\FunctionalTestingFramework\Module\MagentoWebDriverDoctor; use Symfony\Component\Console\Style\SymfonyStyle; @@ -125,7 +124,7 @@ private function checkAuthenticationToMagentoAdmin() $result = false; try { $this->ioStyle->text("Requesting API token for admin user through cURL ..."); - ModuleResolver::getInstance()->getAdminToken(); + WebApiAuth::getAdminToken(); $this->ioStyle->success('Successful'); $result = true; } catch (TestFrameworkException $e) { @@ -191,7 +190,7 @@ private function runMagentoWebDriverDoctor() // Disable MagentoWebDriver to avoid conflicts foreach ($settings['modules']['enabled'] as $index => $module) { - if ($module == $magentoWebDriver) { + if ($module === $magentoWebDriver) { unset($settings['modules']['enabled'][$index]); break; } @@ -199,7 +198,7 @@ private function runMagentoWebDriverDoctor() unset($settings['modules']['config'][$magentoWebDriver]); $dispatcher = new EventDispatcher(); - $suiteManager = new SuiteManager($dispatcher, self::SUITE, $settings); + $suiteManager = new SuiteManager($dispatcher, self::SUITE, $settings, []); try { $suiteManager->initialize(); $this->context = ['Successful']; diff --git a/src/Magento/FunctionalTestingFramework/Console/GenerateDevUrnCommand.php b/src/Magento/FunctionalTestingFramework/Console/GenerateDevUrnCommand.php index bca72421b..72529158b 100644 --- a/src/Magento/FunctionalTestingFramework/Console/GenerateDevUrnCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/GenerateDevUrnCommand.php @@ -19,13 +19,14 @@ class GenerateDevUrnCommand extends Command { + private const SUCCESS_EXIT_CODE = 0; /** * Argument for the path to IDE config file */ - const IDE_FILE_PATH_ARGUMENT = 'path'; + public const IDE_FILE_PATH_ARGUMENT = 'path'; - const PROJECT_PATH_IDENTIFIER = '$PROJECT_DIR$'; - const MFTF_SRC_PATH = 'src/Magento/FunctionalTestingFramework/'; + public const PROJECT_PATH_IDENTIFIER = '$PROJECT_DIR$'; + public const MFTF_SRC_PATH = 'src/Magento/FunctionalTestingFramework/'; /** * Configures the current command. @@ -54,17 +55,17 @@ protected function configure() * * @param InputInterface $input * @param OutputInterface $output - * @return void + * @return int * @throws \Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $miscXmlFilePath = $input->getArgument(self::IDE_FILE_PATH_ARGUMENT); $miscXmlFile = realpath($miscXmlFilePath); - $force = $input->getOption('force'); + $force = (bool) $input->getOption('force'); if ($miscXmlFile === false) { - if ($force == true) { + if ($force === true) { // create file and refresh realpath $xml = "<project version=\"4\"/>"; file_put_contents($miscXmlFilePath, $xml); @@ -117,6 +118,8 @@ protected function execute(InputInterface $input, OutputInterface $output) //Save output $dom->save($miscXmlFile); $output->writeln("MFTF URN mapping successfully added to {$miscXmlFile}."); + + return self::SUCCESS_EXIT_CODE; } /** diff --git a/src/Magento/FunctionalTestingFramework/Console/GenerateSuiteCommand.php b/src/Magento/FunctionalTestingFramework/Console/GenerateSuiteCommand.php index 8da3493aa..102930472 100644 --- a/src/Magento/FunctionalTestingFramework/Console/GenerateSuiteCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/GenerateSuiteCommand.php @@ -8,7 +8,9 @@ namespace Magento\FunctionalTestingFramework\Console; use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Exceptions\FastFailException; use Magento\FunctionalTestingFramework\Suite\SuiteGenerator; +use Magento\FunctionalTestingFramework\Util\GenerationErrorHandler; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -65,13 +67,34 @@ protected function execute(InputInterface $input, OutputInterface $output) $suites = $input->getArgument('suites'); + $generated = 0; foreach ($suites as $suite) { - SuiteGenerator::getInstance()->generateSuite($suite); - if ($output->isVerbose()) { - $output->writeLn("suite $suite generated"); + try { + SuiteGenerator::getInstance()->generateSuite($suite); + if ($output->isVerbose()) { + $output->writeLn("suite $suite generated"); + } + $generated++; + } catch (FastFailException $e) { + throw $e; + } catch (\Exception $e) { } } - $output->writeLn("Suites Generated"); + if (empty(GenerationErrorHandler::getInstance()->getAllErrors())) { + if ($generated > 0) { + $output->writeln("Suites Generated" . PHP_EOL); + return 0; + } + } else { + GenerationErrorHandler::getInstance()->printErrorSummary(); + if ($generated > 0) { + $output->writeln("Suites Generated (with errors)" . PHP_EOL); + return 1; + } + } + + $output->writeln("No Suite Generated" . PHP_EOL); + return 1; } } diff --git a/src/Magento/FunctionalTestingFramework/Console/GenerateTestFailedCommand.php b/src/Magento/FunctionalTestingFramework/Console/GenerateTestFailedCommand.php new file mode 100644 index 000000000..380270969 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Console/GenerateTestFailedCommand.php @@ -0,0 +1,186 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types = 1); + +namespace Magento\FunctionalTestingFramework\Console; + +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Util\Path\FilePathFormatter; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Process\Process; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Symfony\Component\Console\Input\InputOption; + +class GenerateTestFailedCommand extends BaseGenerateCommand +{ + /** + * Default Test group to signify not in suite + */ + const DEFAULT_TEST_GROUP = 'default'; + + /** + * Configures the current command. + * + * @return void + */ + protected function configure() + { + $this->setName('generate:failed') + ->setDescription('Generate a set of tests failed'); + + parent::configure(); + } + + /** + * Executes the current command. + * + * @param InputInterface $input + * @param OutputInterface $output + * @return integer + * @throws \Exception + * + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $force = $input->getOption('force'); + $debug = $input->getOption('debug') ?? MftfApplicationConfig::LEVEL_DEVELOPER; // for backward compatibility + $allowSkipped = $input->getOption('allow-skipped'); + $verbose = $output->isVerbose(); + + // Create Mftf Configuration + MftfApplicationConfig::create( + $force, + MftfApplicationConfig::EXECUTION_PHASE, + $verbose, + $debug, + $allowSkipped + ); + + $testsFailedFile = $this->getTestsOutputDir() . self::FAILED_FILE; + $testsReRunFile = $this->getTestsOutputDir() . "rerun_tests"; + $testConfiguration = $this->getFailedTestList($testsFailedFile, $testsReRunFile); + + if ($testConfiguration === null) { + // No failed tests found, no tests generated + $this->removeGeneratedDirectory($output, $verbose); + return 0; + } + + $command = $this->getApplication()->find('generate:tests'); + $args = [ + '--tests' => $testConfiguration, + '--force' => $force, + '--remove' => true, + '--debug' => $debug, + '--allow-skipped' => $allowSkipped, + '-v' => $verbose + ]; + $command->run(new ArrayInput($args), $output); + $output->writeln("Test Failed Generated, now run:failed command"); + return 0; + } + + /** + * Returns a json string of tests that failed on the last run + * + * @return string + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function getFailedTestList($testsFailedFile, $testsReRunFile) + { + $failedTestDetails = ['tests' => [], 'suites' => []]; + + $testList = $this->readFailedTestFile($testsFailedFile); + + if (!empty($testList)) { + foreach ($testList as $test) { + if (!empty($test)) { + $this->writeFailedTestToFile($test, $testsReRunFile); + $testInfo = explode(DIRECTORY_SEPARATOR, $test); + $testName = isset($testInfo[count($testInfo) - 1][1]) + ? explode(":", $testInfo[count($testInfo) - 1])[1] + : []; + $suiteName = isset($testInfo[count($testInfo) - 2]) + ? $testInfo[count($testInfo) - 2] + : []; + if ($suiteName === self::DEFAULT_TEST_GROUP) { + array_push($failedTestDetails['tests'], $testName); + } else { + $suiteName = $this->sanitizeSuiteName($suiteName); + $failedTestDetails['suites'] = array_merge_recursive( + $failedTestDetails['suites'], + [$suiteName => [$testName]] + ); + } + } + } + } + if (empty($failedTestDetails['tests']) & empty($failedTestDetails['suites'])) { + return null; + } + if (empty($failedTestDetails['tests'])) { + $failedTestDetails['tests'] = null; + } + if (empty($failedTestDetails['suites'])) { + $failedTestDetails['suites'] = null; + } + $testConfigurationJson = json_encode($failedTestDetails); + return $testConfigurationJson; + } + + /** + * Trim potential suite_parallel_0_G to suite_parallel + * + * @param string $suiteName + * @return string + */ + private function sanitizeSuiteName($suiteName) + { + $suiteNameArray = explode("_", $suiteName); + if (array_pop($suiteNameArray) === 'G') { + if (is_numeric(array_pop($suiteNameArray))) { + $suiteName = implode("_", $suiteNameArray); + } + } + return $suiteName; + } + + /** + * Returns an array of tests read from the failed test file in _output + * + * @param string $filePath + * @return array|boolean + */ + public function readFailedTestFile($filePath) + { + if (realpath($filePath)) { + return file($filePath, FILE_IGNORE_NEW_LINES); + } + return ""; + } + + /** + * Writes the test name to a file if it does not already exist + * + * @param string $test + * @param string $filePath + * @return void + */ + public function writeFailedTestToFile($test, $filePath) + { + if (file_exists($filePath)) { + if (strpos(file_get_contents($filePath), $test) === false) { + file_put_contents($filePath, "\n" . $test, FILE_APPEND); + } + } else { + file_put_contents($filePath, $test . "\n"); + } + } +} diff --git a/src/Magento/FunctionalTestingFramework/Console/GenerateTestsCommand.php b/src/Magento/FunctionalTestingFramework/Console/GenerateTestsCommand.php index b76d3ab3c..c1ac3aa6e 100644 --- a/src/Magento/FunctionalTestingFramework/Console/GenerateTestsCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/GenerateTestsCommand.php @@ -8,23 +8,63 @@ namespace Magento\FunctionalTestingFramework\Console; use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Exceptions\FastFailException; use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; +use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Suite\SuiteGenerator; use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; -use Magento\FunctionalTestingFramework\Util\Manifest\ParallelTestManifest; +use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; +use Magento\FunctionalTestingFramework\Util\GenerationErrorHandler; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; +use Magento\FunctionalTestingFramework\Util\Manifest\ParallelByTimeTestManifest; use Magento\FunctionalTestingFramework\Util\Manifest\TestManifestFactory; +use Magento\FunctionalTestingFramework\Util\Script\ScriptUtil; +use Magento\FunctionalTestingFramework\Util\Script\TestDependencyUtil; use Magento\FunctionalTestingFramework\Util\TestGenerator; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Finder\Finder; /** * @SuppressWarnings(PHPMD) */ class GenerateTestsCommand extends BaseGenerateCommand { + const PARALLEL_DEFAULT_TIME = 10; + const EXTENDS_REGEX_PATTERN = '/extends=["\']([^\'"]*)/'; + const ACTIONGROUP_REGEX_PATTERN = '/ref=["\']([^\'"]*)/'; + const TEST_DEPENDENCY_FILE_LOCATION_STANDALONE = 'dev/tests/_output/test-dependencies.json'; + const TEST_DEPENDENCY_FILE_LOCATION_EMBEDDED = 'dev/tests/acceptance/tests/_output/test-dependencies.json'; + + /** + * @var ScriptUtil + */ + private $scriptUtil; + + /** + * @var TestDependencyUtil + */ + private $testDependencyUtil; + + /** + * @var array + */ + private $moduleNameToPath; + + /** + * @var array + */ + private $moduleNameToComposerName; + + /** + * @var array + */ + private $flattenedDependencies; + /** * Configures the current command. * @@ -39,18 +79,31 @@ protected function configure() 'name', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'name(s) of specific tests to generate' - )->addOption("config", 'c', InputOption::VALUE_REQUIRED, 'default, singleRun, or parallel', 'default') - ->addOption( + )->addOption( + "config", + 'c', + InputOption::VALUE_REQUIRED, + 'default, singleRun, or parallel', + 'default' + )->addOption( 'time', 'i', InputOption::VALUE_REQUIRED, - 'Used in combination with a parallel configuration, determines desired group size (in minutes)', - 10 + 'Used in combination with a parallel configuration, determines desired group size (in minutes)' + . PHP_EOL . 'Option "--time" will be the default and the default value is ' + . self::PARALLEL_DEFAULT_TIME + . ' when neither "--time" nor "--groups" is specified' + )->addOption( + 'groups', + 'g', + InputOption::VALUE_REQUIRED, + 'Used in combination with a parallel configuration, determines desired number of groups' + . PHP_EOL . 'Options "--time" and "--groups" are mutually exclusive and only one should be used' )->addOption( 'tests', 't', InputOption::VALUE_REQUIRED, - 'A parameter accepting a JSON string used to determine the test configuration' + 'A parameter accepting a JSON string or JSON file path used to determine the test configuration' )->addOption( 'filter', null, @@ -59,7 +112,18 @@ protected function configure() . '<info>Template:</info> <filterName>:<filterValue>' . PHP_EOL . '<info>Existing filter types:</info> severity.' . PHP_EOL . '<info>Existing severity values:</info> BLOCKER, CRITICAL, MAJOR, AVERAGE, MINOR.' . PHP_EOL - . '<info>Example:</info> --filter=severity:CRITICAL' . PHP_EOL + . '<info>Example:</info> --filter=severity:CRITICAL' + . ' --filter=includeGroup:customer --filter=excludeGroup:customerAnalytics' . PHP_EOL + )->addOption( + 'path', + 'p', + InputOption::VALUE_REQUIRED, + 'path to a test names file.', + )->addOption( + 'log', + 'l', + InputOption::VALUE_REQUIRED, + 'Generate metadata files during test generation.', ); parent::configure(); @@ -72,8 +136,8 @@ protected function configure() * @param OutputInterface $output * @return void|integer * @throws TestFrameworkException - * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException - * @throws \Magento\FunctionalTestingFramework\Exceptions\XmlException + * @throws FastFailException + * @throws XmlException */ protected function execute(InputInterface $input, OutputInterface $output) { @@ -82,16 +146,26 @@ protected function execute(InputInterface $input, OutputInterface $output) $config = $input->getOption('config'); $json = $input->getOption('tests'); // for backward compatibility $force = $input->getOption('force'); - $time = $input->getOption('time') * 60 * 1000; // convert from minutes to milliseconds + $time = $input->getOption('time'); + //$time = $input->getOption('time') * 60 * 1000; // convert from minutes to milliseconds + $groups = $input->getOption('groups'); $debug = $input->getOption('debug') ?? MftfApplicationConfig::LEVEL_DEVELOPER; // for backward compatibility $remove = $input->getOption('remove'); $verbose = $output->isVerbose(); $allowSkipped = $input->getOption('allow-skipped'); + $log = $input->getOption('log'); $filters = $input->getOption('filter'); foreach ($filters as $filter) { list($filterType, $filterValue) = explode(':', $filter); $filterList[$filterType][] = $filterValue; } + + $path = $input->getOption('path'); + // check filepath is given for generate test file + if (!empty($path)) { + $tests = $this->generateTestFileFromPath($path); + } + // Set application configuration so we can references the user options in our framework try { MftfApplicationConfig::create( @@ -107,6 +181,10 @@ protected function execute(InputInterface $input, OutputInterface $output) return 1; } + if ($json !== null && is_file($json)) { + $json = file_get_contents($json); + } + if (!empty($tests)) { $json = $this->getTestAndSuiteConfiguration($tests); } @@ -116,9 +194,8 @@ protected function execute(InputInterface $input, OutputInterface $output) throw new TestFrameworkException("JSON could not be parsed: " . json_last_error_msg()); } - if ($config === 'parallel' && $time <= 0) { - // stop execution if the user has given us an invalid argument for time argument during parallel generation - throw new TestFrameworkException("time option cannot be less than or equal to 0"); + if ($config === 'parallel') { + list($config, $configNumber) = $this->parseConfigParallelOptions($time, $groups); } // Remove previous GENERATED_DIR if --remove option is used @@ -132,17 +209,37 @@ protected function execute(InputInterface $input, OutputInterface $output) // create our manifest file here $testManifest = TestManifestFactory::makeManifest($config, $testConfiguration['suites']); - TestGenerator::getInstance(null, $testConfiguration['tests'])->createAllTestFiles($testManifest); + try { + if (empty($tests) || !empty($testConfiguration['tests'])) { + // $testConfiguration['tests'] cannot be empty if $tests is not empty + TestGenerator::getInstance(null, $testConfiguration['tests'])->createAllTestFiles($testManifest); + } elseif (empty($testConfiguration['suites'])) { + throw new FastFailException( + !empty(GenerationErrorHandler::getInstance()->getAllErrors()) + ? + GenerationErrorHandler::getInstance()->getAllErrorMessages() + : + 'Invalid input' + ); + } + } catch (FastFailException $e) { + throw $e; + } catch (\Exception $e) { + } - if ($config == 'parallel') { - /** @var ParallelTestManifest $testManifest */ - $testManifest->createTestGroups($time); + if (strpos($config, 'parallel') !== false) { + $testManifest->createTestGroups($configNumber); } SuiteGenerator::getInstance()->generateAllSuites($testManifest); $testManifest->generate(); + + SuiteGenerator::getInstance()->generateTestgroupmembership($testManifest); } catch (\Exception $e) { + if (!empty(GenerationErrorHandler::getInstance()->getAllErrors())) { + GenerationErrorHandler::getInstance()->printErrorSummary(); + } $message = $e->getMessage() . PHP_EOL; $message .= !empty($filters) ? 'Filter(s): ' . implode(', ', $filters) . PHP_EOL : ''; $message .= !empty($tests) ? 'Test name(s): ' . implode(', ', $tests) . PHP_EOL : ''; @@ -152,7 +249,32 @@ protected function execute(InputInterface $input, OutputInterface $output) return 1; } - $output->writeln("Generate Tests Command Run"); + // check test dependencies log command + if (!empty($log)) { + if ($log === "testEntityJson") { + $this->getTestEntityJson($filterList ??[], $tests); + $testDependencyFileLocation = self::TEST_DEPENDENCY_FILE_LOCATION_EMBEDDED; + if (isset($_ENV['MAGENTO_BP'])) { + $testDependencyFileLocation = self::TEST_DEPENDENCY_FILE_LOCATION_STANDALONE; + } + $output->writeln( + "Test dependencies file created, Located in: " . $testDependencyFileLocation + ); + } else { + $output->writeln( + "Wrong parameter for log (-l) option, accepted parameter are: testEntityJson" . PHP_EOL + ); + } + } + + if (empty(GenerationErrorHandler::getInstance()->getAllErrors())) { + $output->writeln("Generate Tests Command Run" . PHP_EOL); + return 0; + } else { + GenerationErrorHandler::getInstance()->printErrorSummary(); + $output->writeln("Generate Tests Command Run (with errors)" . PHP_EOL); + return 1; + } } /** @@ -161,8 +283,8 @@ protected function execute(InputInterface $input, OutputInterface $output) * @param string $json * @param array $tests * @return array - * @throws \Magento\FunctionalTestingFramework\Exceptions\TestReferenceException - * @throws \Magento\FunctionalTestingFramework\Exceptions\XmlException + * @throws FastFailException + * @throws TestFrameworkException */ private function createTestConfiguration( $json, @@ -179,7 +301,20 @@ private function createTestConfiguration( $testObjects = []; foreach ($testConfiguration['tests'] as $test) { - $testObjects[$test] = TestObjectHandler::getInstance()->getObject($test); + try { + $testObjects[$test] = TestObjectHandler::getInstance()->getObject($test); + } catch (FastFailException $e) { + throw $e; + } catch (\Exception $e) { + $message = "Unable to create test object {$test} from test configuration. " . $e->getMessage(); + LoggingUtil::getInstance()->getLogger(self::class)->error($message); + if (MftfApplicationConfig::getConfig()->verboseEnabled() + && MftfApplicationConfig::getConfig()->getPhase() === MftfApplicationConfig::GENERATION_PHASE + ) { + print($message); + } + GenerationErrorHandler::getInstance()->addError('test', $test, $message); + } } $testConfiguration['tests'] = $testObjects; @@ -210,4 +345,277 @@ private function parseTestsConfigJson($json, array $testConfiguration) $jsonTestConfiguration['suites'] = $testConfigArray['suites'] ?? null; return $jsonTestConfiguration; } + + /** + * Parse console command options --time and/or --groups and return config type and config number in an array + * + * @param mixed $time + * @param mixed $groups + * @return array + * @throws FastFailException + */ + private function parseConfigParallelOptions($time, $groups) + { + $config = null; + $configNumber = null; + if ($time !== null && $groups !== null) { + throw new FastFailException( + "'time' and 'groups' options are mutually exclusive. " + . "Only one can be specified for 'config parallel'" + ); + } elseif ($time === null && $groups === null) { + $config = 'parallelByTime'; + $configNumber = self::PARALLEL_DEFAULT_TIME * 60 * 1000; // convert from minutes to milliseconds + } elseif ($time !== null && is_numeric($time)) { + $time = $time * 60 * 1000; // convert from minutes to milliseconds + if (is_int($time) && $time > 0) { + $config = 'parallelByTime'; + $configNumber = $time; + } + } elseif ($groups !== null && is_numeric($groups)) { + $groups = $groups * 1; + if (is_int($groups) && $groups > 0) { + $config = 'parallelByGroup'; + $configNumber = $groups; + } + } + + if ($config && $configNumber) { + return [$config, $configNumber]; + } elseif ($time !== null) { + throw new FastFailException("'time' option must be an integer and greater than 0"); + } else { + throw new FastFailException("'groups' option must be an integer and greater than 0"); + } + } + + /** + * console command options --log and create test dependencies in json file + * @return void + * @throws TestFrameworkException + * @throws XmlException|FastFailException + */ + private function getTestEntityJson(array $filterList, array $tests = []) + { + $testDependencies = $this->getTestDependencies($filterList, $tests); + $this->array2Json($testDependencies); + } + + /** + * Function responsible for getting test dependencies in array + * @param array $filterList + * @param array $tests + * @return array + * @throws FastFailException + * @throws TestFrameworkException + * @throws XmlException + */ + public function getTestDependencies(array $filterList, array $tests = []): array + { + $this->scriptUtil = new ScriptUtil(); + $this->testDependencyUtil = new TestDependencyUtil(); + $allModules = $this->scriptUtil->getAllModulePaths(); + + if (!class_exists('\Magento\Framework\Component\ComponentRegistrar')) { + throw new TestFrameworkException( + "TEST DEPENDENCY CHECK ABORTED: MFTF must be attached or pointing to Magento codebase." + ); + } + $registrar = new \Magento\Framework\Component\ComponentRegistrar(); + $this->moduleNameToPath = $registrar->getPaths(\Magento\Framework\Component\ComponentRegistrar::MODULE); + $this->moduleNameToComposerName = $this->testDependencyUtil->buildModuleNameToComposerName( + $this->moduleNameToPath + ); + $this->flattenedDependencies = $this->testDependencyUtil->buildComposerDependencyList( + $this->moduleNameToPath, + $this->moduleNameToComposerName + ); + + if (!empty($tests)) { + # specific test dependencies will be generate. + $testXmlFiles = $this->scriptUtil->getModuleXmlFilesByTestNames($tests); + } else { + $filePaths = [ + DIRECTORY_SEPARATOR . 'Test' . DIRECTORY_SEPARATOR + ]; + // These files can contain references to other modules. + $testXmlFiles = $this->scriptUtil->getModuleXmlFilesByScope($allModules, $filePaths[0]); + } + + list($testDependencies, $extendedTestMapping) = $this->findTestDependentModule($testXmlFiles); + return $this->testDependencyUtil->mergeDependenciesForExtendingTests( + $testDependencies, + $filterList, + $extendedTestMapping + ); + } + + /** + * Finds all test dependencies in given set of files + * @param Finder $files + * @return array + * @throws FastFailException + * @throws XmlException + */ + private function findTestDependentModule(Finder $files): array + { + $testDependencies = []; + $extendedTests = []; + $extendedTestMapping = []; + foreach ($files as $filePath) { + $allEntities = []; + $filePath = $filePath->getPathname(); + $moduleName = $this->testDependencyUtil->getModuleName($filePath, $this->moduleNameToPath); + // Not a module, is either dev/tests/acceptance or loose folder with test materials + if ($moduleName == null) { + continue; + } + + $contents = file_get_contents($filePath); + preg_match_all(ActionObject::ACTION_ATTRIBUTE_VARIABLE_REGEX_PATTERN, $contents, $braceReferences); + preg_match_all(self::ACTIONGROUP_REGEX_PATTERN, $contents, $actionGroupReferences); + preg_match_all(self::EXTENDS_REGEX_PATTERN, $contents, $extendReferences); + + // Remove Duplicates + $braceReferences[0] = array_unique($braceReferences[0]); + $actionGroupReferences[1] = array_unique($actionGroupReferences[1]); + $braceReferences[1] = array_unique($braceReferences[1]); + $braceReferences[2] = array_filter(array_unique($braceReferences[2])); + + // resolve entity references + $allEntities = array_merge( + $allEntities, + $this->scriptUtil->resolveEntityReferences($braceReferences[0], $contents) + ); + + // resolve parameterized references + $allEntities = array_merge( + $allEntities, + $this->scriptUtil->resolveParametrizedReferences($braceReferences[2], $contents) + ); + + // resolve entity by names + $allEntities = array_merge( + $allEntities, + $this->scriptUtil->resolveEntityByNames($actionGroupReferences[1]) + ); + + // resolve entity by names + $allEntities = array_merge( + $allEntities, + $this->scriptUtil->resolveEntityByNames($extendReferences[1]) + ); + $modulesReferencedInTest = $this->testDependencyUtil->getModuleDependenciesFromReferences( + $allEntities, + $this->moduleNameToComposerName, + $this->moduleNameToPath + ); + if (! empty($modulesReferencedInTest)) { + $document = new \DOMDocument(); + $document->loadXML($contents); + $test_file = $document->getElementsByTagName('test')->item(0); + $test_name = $test_file->getAttribute('name'); + + # check any test extends on with this test. + $extended_test = $test_file->getAttribute('extends') ?? ""; + if (!empty($extended_test)) { + $extendedTests[] = $extended_test; + $extendedTestMapping[] = ["child_test_name" =>$test_name, "parent_test_name" =>$extended_test]; + } + + $flattenedDependencyMap = array_values( + array_unique(call_user_func_array('array_merge', array_values($modulesReferencedInTest))) + ); + $suite_name = $this->getSuiteName($test_name); + $full_name = "Magento\AcceptanceTest\_". $suite_name. "\Backend\\".$test_name."Cest.".$test_name; + $dependencyMap = [ + "file_path" => $filePath, + "full_name" => $full_name, + "test_name" => $test_name, + "test_modules" => $flattenedDependencyMap, + ]; + $testDependencies[] = $dependencyMap; + } + } + + if (!empty($extendedTests)) { + list($extendedDependencies, $tempExtendedTestMapping) = $this->getExtendedTestDependencies($extendedTests); + $testDependencies = array_merge($testDependencies, $extendedDependencies); + $extendedTestMapping = array_merge($extendedTestMapping, $tempExtendedTestMapping); + } + + return [$testDependencies, $extendedTestMapping]; + } + + /** + * Finds all extended test dependencies in given set of files + * @param array $extendedTests + * @return array + * @throws FastFailException + * @throws XmlException + */ + private function getExtendedTestDependencies(array $extendedTests): array + { + $testXmlFiles = $this->scriptUtil->getModuleXmlFilesByTestNames($extendedTests); + return $this->findTestDependentModule($testXmlFiles); + } + + /** + * Create json file of test dependencies + * @param array $array + * @return void + */ + private function array2Json(array $array) + { + $testDependencyFileLocation = self::TEST_DEPENDENCY_FILE_LOCATION_EMBEDDED; + if (isset($_ENV['MAGENTO_BP'])) { + $testDependencyFileLocation = self::TEST_DEPENDENCY_FILE_LOCATION_STANDALONE; + } + $testDependencyFileLocationDir = dirname($testDependencyFileLocation); + if (!is_dir($testDependencyFileLocationDir)) { + mkdir($testDependencyFileLocationDir, 0777, true); + } + $file = fopen($testDependencyFileLocation, 'w'); + $json = json_encode($array, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); + fwrite($file, $json); + fclose($file); + } + + /** + * Get suite name. + * @param string $test_name + * @return integer|mixed|string + * @throws FastFailException + */ + private function getSuiteName(string $test_name) + { + $suite_name = json_decode($this->getTestAndSuiteConfiguration([$test_name]), true)["suites"] ?? "default"; + if (is_array($suite_name)) { + $suite_name = array_keys($suite_name)[0]; + } + return $suite_name; + } + + /** + * @param string $path + * @return array + * @throws TestFrameworkException + */ + private function generateTestFileFromPath(string $path): array + { + if (!file_exists($path)) { + throw new TestFrameworkException("Could not find file $path. Check the path and try again."); + } + + $test_names = file($path, FILE_IGNORE_NEW_LINES); + $tests = []; + foreach ($test_names as $test_name) { + if (empty(trim($test_name))) { + continue; + } + $test_name_array = explode(' ', trim($test_name)); + $tests = array_merge($tests, $test_name_array); + } + return $tests; + } } diff --git a/src/Magento/FunctionalTestingFramework/Console/RunManifestCommand.php b/src/Magento/FunctionalTestingFramework/Console/RunManifestCommand.php index 37a90bb77..3b75cbb87 100644 --- a/src/Magento/FunctionalTestingFramework/Console/RunManifestCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/RunManifestCommand.php @@ -11,6 +11,7 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; use Magento\FunctionalTestingFramework\Util\Path\FilePathFormatter; @@ -80,12 +81,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Delete the Codeception failed file just in case it exists from any previous test runs $this->deleteFailedFile(); - foreach ($manifestFile as $manifestLine) { - if (empty($manifestLine)) { + for ($line = 0; $line < count($manifestFile); $line++) { + if (empty($manifestFile[$line])) { continue; } - $this->runManifestLine($manifestLine, $output); + if ($line === count($manifestFile) - 1) { + $this->runManifestLine($manifestFile[$line], $output, true); + } else { + $this->runManifestLine($manifestFile[$line], $output); + } + $this->aggregateFailed(); } @@ -102,24 +108,38 @@ protected function execute(InputInterface $input, OutputInterface $output): int * * @param string $manifestLine * @param OutputInterface $output + * @param boolean $exit * @return void + * @throws \Exception * - * @SuppressWarnings(PHPMD.UnusedLocalVariable) Need this because of the unused $type variable in the closure + * @SuppressWarnings(PHPMD.UnusedFormalParameter) Need this because of the unused $type variable in the closure */ - private function runManifestLine(string $manifestLine, OutputInterface $output) + private function runManifestLine($manifestLine, $output, $exit = false) { - $codeceptionCommand = realpath(PROJECT_ROOT . "/vendor/bin/codecept") - . " run functional --verbose --steps " - . $manifestLine; - - // run the codecept command in a sub process - $process = new Process($codeceptionCommand); - $process->setWorkingDirectory(TESTS_BP); - $process->setIdleTimeout(600); - $process->setTimeout(0); - $subReturnCode = $process->run(function ($type, $buffer) use ($output) { - $output->write($buffer); - }); + if (getenv('ENABLE_PAUSE') === 'true') { + $codeceptionCommand = BaseGenerateCommand::CODECEPT_RUN_FUNCTIONAL + . '--verbose --steps --debug '; + if (!$exit) { + $codeceptionCommand .= BaseGenerateCommand::CODECEPT_RUN_OPTION_NO_EXIT; + } + $codeceptionCommand .= $manifestLine; + $input = new StringInput($codeceptionCommand); + $command = $this->getApplication()->find(BaseGenerateCommand::CODECEPT_RUN); + $subReturnCode = $command->run($input, $output); + } else { + $codeceptionCommand = realpath(PROJECT_ROOT . "/vendor/bin/codecept") + . " run functional --verbose --steps " . $manifestLine; + + // run the codecept command in a sub process + $process = Process::fromShellCommandline($codeceptionCommand); + $process->setWorkingDirectory(TESTS_BP); + $process->setIdleTimeout(600); + $process->setTimeout(0); + $subReturnCode = $process->run(function ($type, $buffer) use ($output) { + $output->write($buffer); + }); + } + $this->returnCode = max($this->returnCode, $subReturnCode); } diff --git a/src/Magento/FunctionalTestingFramework/Console/RunTestCommand.php b/src/Magento/FunctionalTestingFramework/Console/RunTestCommand.php index 373256cfc..31ca307d5 100644 --- a/src/Magento/FunctionalTestingFramework/Console/RunTestCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/RunTestCommand.php @@ -8,6 +8,8 @@ namespace Magento\FunctionalTestingFramework\Console; use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; +use Magento\FunctionalTestingFramework\Util\GenerationErrorHandler; use Magento\FunctionalTestingFramework\Util\Path\FilePathFormatter; use Magento\FunctionalTestingFramework\Util\TestGenerator; use Symfony\Component\Console\Input\ArrayInput; @@ -18,6 +20,9 @@ use Symfony\Component\Process\Process; use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +/** + * @SuppressWarnings(PHPMD) + */ class RunTestCommand extends BaseGenerateCommand { /** @@ -36,17 +41,26 @@ protected function configure() { $this->setName("run:test") ->setDescription("generation and execution of test(s) defined in xml") - ->addArgument( + ->addOption( + 'xml', + 'xml', + InputOption::VALUE_NONE, + "creates xml report for executed test" + )->addArgument( 'name', - InputArgument::REQUIRED | InputArgument::IS_ARRAY, + InputArgument::OPTIONAL | InputArgument::IS_ARRAY, "name of tests to generate and execute" )->addOption( 'skip-generate', 'k', InputOption::VALUE_NONE, "skip generation and execute existing test" + )->addOption( + 'tests', + 't', + InputOption::VALUE_REQUIRED, + 'A parameter accepting a JSON string or JSON file path used to determine the test configuration' ); - parent::configure(); } @@ -61,6 +75,7 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output): int { $tests = $input->getArgument('name'); + $json = $input->getOption('tests'); // for backward compatibility $skipGeneration = $input->getOption('skip-generate'); $force = $input->getOption('force'); $remove = $input->getOption('remove'); @@ -84,7 +99,24 @@ protected function execute(InputInterface $input, OutputInterface $output): int $allowSkipped ); - $testConfiguration = $this->getTestAndSuiteConfiguration($tests); + if ($json !== null) { + if (is_file($json)) { + $testConfiguration = file_get_contents($json); + } else { + $testConfiguration = $json; + } + } + + if (!empty($tests)) { + $testConfiguration = $this->getTestAndSuiteConfiguration($tests); + } + + if ($testConfiguration !== null && !json_decode($testConfiguration)) { + // stop execution if we have failed to properly parse any json passed in by the user + throw new TestFrameworkException("JSON could not be parsed: " . json_last_error_msg()); + } + + $generationErrorCode = 0; if (!$skipGeneration) { $command = $this->getApplication()->find('generate:tests'); @@ -94,22 +126,30 @@ protected function execute(InputInterface $input, OutputInterface $output): int '--remove' => $remove, '--debug' => $debug, '--allow-skipped' => $allowSkipped, - '-v' => $verbose + '-v' => $verbose, + '' ]; $command->run(new ArrayInput($args), $output); + + if (!empty(GenerationErrorHandler::getInstance()->getAllErrors())) { + $generationErrorCode = 1; + } } $testConfigArray = json_decode($testConfiguration, true); if (isset($testConfigArray['tests'])) { - $this->runTests($testConfigArray['tests'], $output); + $this->runTests($testConfigArray['tests'], $output, $input); } if (isset($testConfigArray['suites'])) { - $this->runTestsInSuite($testConfigArray['suites'], $output); + $this->runTestsInSuite($testConfigArray['suites'], $output, $input); } - return $this->returnCode; + // Add all failed tests in 'failed' file + $this->applyAllFailed(); + + return max($this->returnCode, $generationErrorCode); } /** @@ -117,27 +157,50 @@ protected function execute(InputInterface $input, OutputInterface $output): int * * @param array $tests * @param OutputInterface $output + * @param InputInterface $input * @return void * @throws TestFrameworkException + * @throws \Exception */ - private function runTests(array $tests, OutputInterface $output) + private function runTests(array $tests, OutputInterface $output, InputInterface $input) { - $codeceptionCommand = realpath(PROJECT_ROOT . '/vendor/bin/codecept') . ' run functional '; + $xml = ($input->getOption('xml')) ? '--xml' : ""; + $noAnsi = ($input->getOption('no-ansi')) ? '--no-ansi' : ""; + if ($this->pauseEnabled()) { + $codeceptionCommand = self::CODECEPT_RUN_FUNCTIONAL; + } else { + $codeceptionCommand = realpath(PROJECT_ROOT . '/vendor/bin/codecept') . ' run functional '; + } + $testsDirectory = FilePathFormatter::format(TESTS_MODULE_PATH) . TestGenerator::GENERATED_DIR . DIRECTORY_SEPARATOR . TestGenerator::DEFAULT_DIR . DIRECTORY_SEPARATOR ; - foreach ($tests as $test) { - $testName = $test . 'Cest.php'; + for ($i = 0; $i < count($tests); $i++) { + $testName = $tests[$i] . 'Cest.php'; if (!realpath($testsDirectory . $testName)) { throw new TestFrameworkException( $testName . " is not available under " . $testsDirectory ); } - $fullCommand = $codeceptionCommand . $testsDirectory . $testName . ' --verbose --steps'; - $this->returnCode = max($this->returnCode, $this->executeTestCommand($fullCommand, $output)); + + if ($this->pauseEnabled()) { + $fullCommand = $codeceptionCommand . $testsDirectory . $testName . ' --verbose --steps --debug '.$xml; + if ($i !== count($tests) - 1) { + $fullCommand .= self::CODECEPT_RUN_OPTION_NO_EXIT; + } + $this->returnCode = max($this->returnCode, $this->codeceptRunTest($fullCommand, $output)); + } else { + $fullCommand = $codeceptionCommand . $testsDirectory . $testName . ' --verbose --steps '.$xml; + $this->returnCode = max($this->returnCode, $this->executeTestCommand($fullCommand, $output, $noAnsi)); + } + if (!empty($xml)) { + $this->movingXMLFileFromSourceToDestination($xml, $testName, $output); + } + // Save failed tests + $this->appendRunFailed(); } } @@ -146,15 +209,41 @@ private function runTests(array $tests, OutputInterface $output) * * @param array $suitesConfig * @param OutputInterface $output + * @param InputInterface $input * @return void + * @throws \Exception */ - private function runTestsInSuite(array $suitesConfig, OutputInterface $output) + private function runTestsInSuite(array $suitesConfig, OutputInterface $output, InputInterface $input) { - $codeceptionCommand = realpath(PROJECT_ROOT . '/vendor/bin/codecept') . ' run functional --verbose --steps '; + $xml = ($input->getOption('xml')) ? '--xml' : ""; + $noAnsi = ($input->getOption('no-ansi')) ? '--no-ansi' : ""; + if ($this->pauseEnabled()) { + $codeceptionCommand = self::CODECEPT_RUN_FUNCTIONAL . '--verbose --steps --debug '.$xml; + } else { + $codeceptionCommand = realpath(PROJECT_ROOT . '/vendor/bin/codecept') + . ' run functional --verbose --steps '.$xml; + } + + $count = count($suitesConfig); + $index = 0; //for tests in suites, run them as a group to run before and after block foreach (array_keys($suitesConfig) as $suite) { $fullCommand = $codeceptionCommand . " -g {$suite}"; - $this->returnCode = max($this->returnCode, $this->executeTestCommand($fullCommand, $output)); + + $index += 1; + if ($this->pauseEnabled()) { + if ($index !== $count) { + $fullCommand .= self::CODECEPT_RUN_OPTION_NO_EXIT; + } + $this->returnCode = max($this->returnCode, $this->codeceptRunTest($fullCommand, $output)); + } else { + $this->returnCode = max($this->returnCode, $this->executeTestCommand($fullCommand, $output, $noAnsi)); + } + if (!empty($xml)) { + $this->movingXMLFileFromSourceToDestination($xml, $suite, $output); + } + // Save failed tests + $this->appendRunFailed(); } } @@ -165,16 +254,33 @@ private function runTestsInSuite(array $suitesConfig, OutputInterface $output) * @param OutputInterface $output * @return integer * - * @SuppressWarnings(PHPMD.UnusedLocalVariable) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - private function executeTestCommand(string $command, OutputInterface $output) + private function executeTestCommand(string $command, OutputInterface $output, $noAnsi) { - $process = new Process($command); + $process = Process::fromShellCommandline($command); $process->setWorkingDirectory(TESTS_BP); $process->setIdleTimeout(600); $process->setTimeout(0); - return $process->run(function ($type, $buffer) use ($output) { + + return $process->run(function ($type, $buffer) use ($output, $noAnsi) { + $buffer = $this->disableAnsiColorCodes($buffer, $noAnsi); $output->write($buffer); }); } + + /** + * @param string $buffer + * @param string $noAnsi + * @return string + */ + private function disableAnsiColorCodes($buffer, $noAnsi) :string + { + if (empty($noAnsi)) { + return $buffer; + } + $pattern = "/\x1B\[([0-9]{1,2}(;[0-9]{1,2})*)?[m|K]/"; + // Use preg_replace to remove ANSI escape codes from the string + return preg_replace($pattern, '', $buffer); + } } diff --git a/src/Magento/FunctionalTestingFramework/Console/RunTestFailedCommand.php b/src/Magento/FunctionalTestingFramework/Console/RunTestFailedCommand.php index 5f0596a7f..55834ad9b 100644 --- a/src/Magento/FunctionalTestingFramework/Console/RunTestFailedCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/RunTestFailedCommand.php @@ -7,36 +7,18 @@ namespace Magento\FunctionalTestingFramework\Console; -use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; -use Magento\FunctionalTestingFramework\Util\Path\FilePathFormatter; -use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; -use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; -use Symfony\Component\Console\Input\InputOption; class RunTestFailedCommand extends BaseGenerateCommand { - /** - * Default Test group to signify not in suite - */ const DEFAULT_TEST_GROUP = 'default'; /** * @var string */ - private $testsFailedFile; - - /** - * @var string - */ - private $testsReRunFile; - - /** - * @var string - */ - private $testsManifestFile; + private $testsReRunFile = "rerun_tests"; /** * @var array @@ -69,66 +51,41 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output): int { - $testsOutputDir = FilePathFormatter::format(TESTS_BP) . - "tests" . - DIRECTORY_SEPARATOR . - "_output" . - DIRECTORY_SEPARATOR; - - $this->testsFailedFile = $testsOutputDir . "failed"; - $this->testsReRunFile = $testsOutputDir . "rerun_tests"; - $this->testsManifestFile= FilePathFormatter::format(TESTS_MODULE_PATH) . - "_generated" . - DIRECTORY_SEPARATOR . - "testManifest.txt"; - - $force = $input->getOption('force'); - $debug = $input->getOption('debug') ?? MftfApplicationConfig::LEVEL_DEVELOPER; // for backward compatibility - $allowSkipped = $input->getOption('allow-skipped'); - $verbose = $output->isVerbose(); - - // Create Mftf Configuration - MftfApplicationConfig::create( - $force, - MftfApplicationConfig::EXECUTION_PHASE, - $verbose, - $debug, - $allowSkipped - ); - - $testConfiguration = $this->getFailedTestList(); - - if ($testConfiguration === null) { - // no failed tests found, run is a success + $this->testsFailedFile = $this->getTestsOutputDir() . self::FAILED_FILE; + $this->testsReRunFile = $this->getTestsOutputDir() . "rerun_tests"; + + $failedTests = $this->readFailedTestFile($this->testsFailedFile); + $testManifestList = $this->filterTestsForExecution($failedTests); + + if (empty($testManifestList)) { + // If there is no tests in manifest then we have nothing to execute. return 0; } - - $command = $this->getApplication()->find('generate:tests'); - $args = [ - '--tests' => $testConfiguration, - '--force' => $force, - '--remove' => true, - '--debug' => $debug, - '--allow-skipped' => $allowSkipped, - '-v' => $verbose - ]; - $command->run(new ArrayInput($args), $output); - - $testManifestList = $this->readTestManifestFile(); $returnCode = 0; - foreach ($testManifestList as $testCommand) { - $codeceptionCommand = realpath(PROJECT_ROOT . '/vendor/bin/codecept') . ' run functional '; - $codeceptionCommand .= $testCommand; - - $process = new Process($codeceptionCommand); - $process->setWorkingDirectory(TESTS_BP); - $process->setIdleTimeout(600); - $process->setTimeout(0); - $returnCode = max($returnCode, $process->run( - function ($type, $buffer) use ($output) { - $output->write($buffer); + for ($i = 0; $i < count($testManifestList); $i++) { + if ($this->pauseEnabled()) { + $codeceptionCommand = self::CODECEPT_RUN_FUNCTIONAL . $testManifestList[$i] . ' --debug '; + if ($i !== count($testManifestList) - 1) { + $codeceptionCommand .= self::CODECEPT_RUN_OPTION_NO_EXIT; } - )); + $returnCode = $this->codeceptRunTest($codeceptionCommand, $output); + } else { + $codeceptionCommand = realpath(PROJECT_ROOT . '/vendor/bin/codecept') . ' run functional '; + $codeceptionCommand .= $testManifestList[$i]; + + $process = Process::fromShellCommandline($codeceptionCommand); + $process->setWorkingDirectory(TESTS_BP); + $process->setIdleTimeout(600); + $process->setTimeout(0); + $returnCode = max($returnCode, $process->run( + function ($type, $buffer) use ($output) { + $output->write($buffer); + } + )); + $process->__destruct(); + unset($process); + } + if (file_exists($this->testsFailedFile)) { $this->failedList = array_merge( $this->failedList, @@ -136,6 +93,7 @@ function ($type, $buffer) use ($output) { ); } } + foreach ($this->failedList as $test) { $this->writeFailedTestToFile($test, $this->testsFailedFile); } @@ -144,72 +102,50 @@ function ($type, $buffer) use ($output) { } /** - * Returns a json string of tests that failed on the last run + * Returns a list of tests/suites which should have an additional run. * - * @return string + * @param array $failedTests + * @return array */ - private function getFailedTestList() + private function filterTestsForExecution(array $failedTests): array { - $failedTestDetails = ['tests' => [], 'suites' => []]; - - if (realpath($this->testsFailedFile)) { - $testList = $this->readFailedTestFile($this->testsFailedFile); - - foreach ($testList as $test) { - if (!empty($test)) { - $this->writeFailedTestToFile($test, $this->testsReRunFile); - $testInfo = explode(DIRECTORY_SEPARATOR, $test); - $testName = explode(":", $testInfo[count($testInfo) - 1])[1]; - $suiteName = $testInfo[count($testInfo) - 2]; - - if ($suiteName == self::DEFAULT_TEST_GROUP) { - array_push($failedTestDetails['tests'], $testName); - } else { - // Trim potential suite_parallel_0 to suite_parallel - $suiteNameArray = explode("_", $suiteName); - if (is_numeric(array_pop($suiteNameArray))) { - $suiteName = implode("_", $suiteNameArray); - } - $failedTestDetails['suites'] = array_merge_recursive( - $failedTestDetails['suites'], - [$suiteName => [$testName]] - ); + $testsOrGroupsToRerun = []; + + foreach ($failedTests as $test) { + if (!empty($test)) { + $this->writeFailedTestToFile($test, $this->testsReRunFile); + $testInfo = explode(DIRECTORY_SEPARATOR, $test); + $suiteName = $testInfo[count($testInfo) - 2]; + list($testPath) = explode(":", $test); + + if ($suiteName === self::DEFAULT_TEST_GROUP) { + $testsOrGroupsToRerun[] = $testPath; + } else { + $group = "-g " . $suiteName; + if (!in_array($group, $testsOrGroupsToRerun)) { + $testsOrGroupsToRerun[] = $group; } } } } - if (empty($failedTestDetails['tests']) & empty($failedTestDetails['suites'])) { - return null; - } - if (empty($failedTestDetails['tests'])) { - $failedTestDetails['tests'] = null; - } - if (empty($failedTestDetails['suites'])) { - $failedTestDetails['suites'] = null; - } - $testConfigurationJson = json_encode($failedTestDetails); - return $testConfigurationJson; - } - /** - * Returns an array of run commands read from the manifest file created post generation - * - * @return array|boolean - */ - private function readTestManifestFile() - { - return file($this->testsManifestFile, FILE_IGNORE_NEW_LINES); + return $testsOrGroupsToRerun; } /** * Returns an array of tests read from the failed test file in _output * * @param string $filePath - * @return array|boolean + * @return array */ - private function readFailedTestFile($filePath) + private function readFailedTestFile(string $filePath): array { - return file($filePath, FILE_IGNORE_NEW_LINES); + $data = []; + if (file_exists($filePath)) { + $file = file($filePath, FILE_IGNORE_NEW_LINES); + $data = $file === false ? [] : $file; + } + return $data; } /** diff --git a/src/Magento/FunctionalTestingFramework/Console/RunTestGroupCommand.php b/src/Magento/FunctionalTestingFramework/Console/RunTestGroupCommand.php index 6ea37785d..fefbe08f8 100644 --- a/src/Magento/FunctionalTestingFramework/Console/RunTestGroupCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/RunTestGroupCommand.php @@ -7,16 +7,15 @@ namespace Magento\FunctionalTestingFramework\Console; -use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; -use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Util\GenerationErrorHandler; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; -use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; class RunTestGroupCommand extends BaseGenerateCommand { @@ -28,7 +27,15 @@ class RunTestGroupCommand extends BaseGenerateCommand protected function configure() { $this->setName('run:group') - ->setDescription('Execute a set of tests referenced via group annotations') + ->setDescription( + 'Execute a set of tests referenced via group annotations' + ) + ->addOption( + 'xml', + 'xml', + InputOption::VALUE_NONE, + "creates xml report for executed group" + ) ->addOption( 'skip-generate', 'k', @@ -51,10 +58,15 @@ protected function configure() * @return integer * @throws \Exception * - * @SuppressWarnings(PHPMD.UnusedLocalVariable) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ protected function execute(InputInterface $input, OutputInterface $output): int { + $xml = ($input->getOption('xml')) + ? '--xml' + : ""; $skipGeneration = $input->getOption('skip-generate'); $force = $input->getOption('force'); $groups = $input->getArgument('groups'); @@ -79,6 +91,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $allowSkipped ); + $generationErrorCode = 0; + if (!$skipGeneration) { $testConfiguration = $this->getGroupAndSuiteConfiguration($groups); $command = $this->getApplication()->find('generate:tests'); @@ -92,33 +106,57 @@ protected function execute(InputInterface $input, OutputInterface $output): int ]; $command->run(new ArrayInput($args), $output); + + if (!empty(GenerationErrorHandler::getInstance()->getAllErrors())) { + $generationErrorCode = 1; + } } - $commandString = realpath(PROJECT_ROOT . '/vendor/bin/codecept') . ' run functional --verbose --steps'; + if ($this->pauseEnabled()) { + $commandString = self::CODECEPT_RUN_FUNCTIONAL . '--verbose --steps --debug '.$xml; + } else { + $commandString = realpath( + PROJECT_ROOT . '/vendor/bin/codecept' + ) . ' run functional --verbose --steps '.$xml; + } $exitCode = -1; $returnCodes = []; - foreach ($groups as $group) { - $codeceptionCommandString = $commandString . " -g {$group}"; - - $process = new Process($codeceptionCommandString); - $process->setWorkingDirectory(TESTS_BP); - $process->setIdleTimeout(600); - $process->setTimeout(0); + for ($i = 0; $i < count($groups); $i++) { + $codeceptionCommandString = $commandString . ' -g ' . $groups[$i]; - $returnCodes[] = $process->run( - function ($type, $buffer) use ($output) { - $output->write($buffer); + if ($this->pauseEnabled()) { + if ($i !== count($groups) - 1) { + $codeceptionCommandString .= self::CODECEPT_RUN_OPTION_NO_EXIT; } - ); + $returnCodes[] = $this->codeceptRunTest($codeceptionCommandString, $output); + } else { + $process = Process::fromShellCommandline($codeceptionCommandString); + $process->setWorkingDirectory(TESTS_BP); + $process->setIdleTimeout(600); + $process->setTimeout(0); + $returnCodes[] = $process->run( + function ($type, $buffer) use ($output) { + $output->write($buffer); + } + ); + } + if (!empty($xml)) { + $this->movingXMLFileFromSourceToDestination($xml, $groups[$i].'_'.'group', $output); + } + // Save failed tests + $this->appendRunFailed(); } + // Add all failed tests in 'failed' file + $this->applyAllFailed(); + foreach ($returnCodes as $returnCode) { - if ($returnCode != 0) { + if ($returnCode !== 0) { return $returnCode; } $exitCode = 0; } - return $exitCode; + return max($exitCode, $generationErrorCode); } } diff --git a/src/Magento/FunctionalTestingFramework/Console/SetupEnvCommand.php b/src/Magento/FunctionalTestingFramework/Console/SetupEnvCommand.php index 65e8c61e7..258e905d5 100644 --- a/src/Magento/FunctionalTestingFramework/Console/SetupEnvCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/SetupEnvCommand.php @@ -18,6 +18,8 @@ class SetupEnvCommand extends Command { + private const SUCCESS_EXIT_CODE = 0; + /** * Env processor manages .env files. * @@ -47,10 +49,10 @@ protected function configure() * * @param InputInterface $input * @param OutputInterface $output - * @return void + * @return integer * @throws \Symfony\Component\Console\Exception\LogicException */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $config = $this->envProcessor->getEnv(); $userEnv = []; @@ -62,5 +64,7 @@ protected function execute(InputInterface $input, OutputInterface $output) } $this->envProcessor->putEnvFile($userEnv); $output->writeln(".env configuration successfully applied."); + + return self::SUCCESS_EXIT_CODE; } } diff --git a/src/Magento/FunctionalTestingFramework/Console/StaticChecksCommand.php b/src/Magento/FunctionalTestingFramework/Console/StaticChecksCommand.php index 30c9cd600..e8d67e04b 100644 --- a/src/Magento/FunctionalTestingFramework/Console/StaticChecksCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/StaticChecksCommand.php @@ -15,11 +15,20 @@ use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Exception; +use Symfony\Component\Console\Style\SymfonyStyle; class StaticChecksCommand extends Command { + /** + * Associative array containing static ruleset properties. + * + * @var array + */ + private $ruleSet; + /** * Pool of all existing static check objects * @@ -34,6 +43,13 @@ class StaticChecksCommand extends Command */ private $staticCheckObjects; + /** + * Console output style + * + * @var SymfonyStyle + */ + protected $ioStyle; + /** * Configures the current command. * @@ -44,14 +60,20 @@ protected function configure() $list = new StaticChecksList(); $this->allStaticCheckObjects = $list->getStaticChecks(); $staticCheckNames = implode(', ', array_keys($this->allStaticCheckObjects)); - $description = "This command will run all static checks on xml test materials. " - . "Available static check scripts are:\n{$staticCheckNames}"; + $description = 'This command will run all static checks on xml test materials. ' + . 'Available static check scripts are:' . PHP_EOL . $staticCheckNames; $this->setName('static-checks') ->setDescription($description) ->addArgument( 'names', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'name(s) of specific static check script(s) to run' + )->addOption( + 'path', + 'p', + InputOption::VALUE_OPTIONAL, + 'Path to a MFTF test module to run "deprecatedEntityUsage" static check script. ' . PHP_EOL + . 'Option is ignored by other static check scripts.' . PHP_EOL ); } @@ -65,32 +87,41 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output) { + $this->ioStyle = new SymfonyStyle($input, $output); try { - $this->validateInputArguments($input, $output); + $this->validateInput($input); } catch (InvalidArgumentException $e) { LoggingUtil::getInstance()->getLogger(StaticChecksCommand::class)->error($e->getMessage()); - $output->writeln($e->getMessage() . " Please fix input arguments and rerun."); + $this->ioStyle->error($e->getMessage() . ' Please fix input argument(s) or option(s) and rerun.'); return 1; } + $cmdFailed = false; $errors = []; foreach ($this->staticCheckObjects as $name => $staticCheck) { LoggingUtil::getInstance()->getLogger(get_class($staticCheck))->info( - "\nRunning static check script for: " . $name - ); - $output->writeln( - "\nRunning static check script for: " . $name + 'Running static check script for: ' . $name . PHP_EOL ); - $staticCheck->execute($input); + $this->ioStyle->text(PHP_EOL . 'Running static check script for: ' . $name . PHP_EOL); + $start = microtime(true); + try { + $staticCheck->execute($input); + } catch (Exception $e) { + $cmdFailed = true; + LoggingUtil::getInstance()->getLogger(get_class($staticCheck))->error($e->getMessage() . PHP_EOL); + $this->ioStyle->error($e->getMessage()); + } + $end = microtime(true); + $errors += $staticCheck->getErrors(); $staticOutput = $staticCheck->getOutput(); LoggingUtil::getInstance()->getLogger(get_class($staticCheck))->info($staticOutput); - $output->writeln($staticOutput); - $errors += $staticCheck->getErrors(); - } + $this->ioStyle->text($staticOutput??""); - if (empty($errors)) { + $this->ioStyle->text('Total execution time is ' . (string)($end - $start) . ' seconds.' . PHP_EOL); + } + if (!$cmdFailed && empty($errors)) { return 0; } else { return 1; @@ -104,30 +135,78 @@ protected function execute(InputInterface $input, OutputInterface $output) * @return void * @throws InvalidArgumentException */ - private function validateInputArguments(InputInterface $input) + private function validateInput(InputInterface $input) { $this->staticCheckObjects = []; $requiredChecksNames = $input->getArgument('names'); - $invalidCheckNames = []; - // Found user required static check script(s) to run, - // If no static check name is supplied, run all static check scripts + // Build list of static check names to run. + if (empty($requiredChecksNames)) { + $this->parseRulesetJson(); + $requiredChecksNames = $this->ruleSet['tests'] ?? null; + } if (empty($requiredChecksNames)) { $this->staticCheckObjects = $this->allStaticCheckObjects; } else { - for ($index = 0; $index < count($requiredChecksNames); $index++) { - if (in_array($requiredChecksNames[$index], array_keys($this->allStaticCheckObjects))) { - $this->staticCheckObjects[$requiredChecksNames[$index]] = - $this->allStaticCheckObjects[$requiredChecksNames[$index]]; - } else { - $invalidCheckNames[] = $requiredChecksNames[$index]; - } + $this->validateTestNames($requiredChecksNames); + } + + if ($input->getOption('path')) { + if ((count($this->staticCheckObjects) !== 1) + || !in_array( + array_keys($this->staticCheckObjects)[0], + [ + StaticChecksList::DEPRECATED_ENTITY_USAGE_CHECK_NAME, + StaticChecksList::PAUSE_ACTION_USAGE_CHECK_NAME + ] + ) + ) + throw new InvalidArgumentException( + '--path option is not supported for the command."' + ); + } + } + + /** + * Validates that all passed in static-check names match an existing static check + * @param string[] $requiredChecksNames + * @return void + */ + private function validateTestNames($requiredChecksNames) + { + $invalidCheckNames = []; + for ($index = 0; $index < count($requiredChecksNames); $index++) { + if (in_array($requiredChecksNames[$index], array_keys($this->allStaticCheckObjects))) { + $this->staticCheckObjects[$requiredChecksNames[$index]] = + $this->allStaticCheckObjects[$requiredChecksNames[$index]]; + } else { + $invalidCheckNames[] = $requiredChecksNames[$index]; } } if (!empty($invalidCheckNames)) { throw new InvalidArgumentException( - "Invalid static check script(s): " . implode(', ', $invalidCheckNames) . "." + 'Invalid static check script(s): ' . implode(', ', $invalidCheckNames) . '.' ); } } + + /** + * Parses and sets local ruleSet. If not found, simply returns and lets script continue. + * @return void; + */ + private function parseRulesetJson() + { + $pathAddition = "/dev/tests/acceptance/"; + // MFTF is both NOT attached and no MAGENTO_BP defined in .env + if (MAGENTO_BP === FW_BP) { + $pathAddition = "/dev/"; + } + $pathToRuleset = MAGENTO_BP . $pathAddition . "staticRuleset.json"; + if (!file_exists($pathToRuleset)) { + $this->ioStyle->text("No ruleset under $pathToRuleset" . PHP_EOL); + return; + } + $this->ioStyle->text("Using ruleset under $pathToRuleset" . PHP_EOL); + $this->ruleSet = json_decode(file_get_contents($pathToRuleset), true); + } } diff --git a/src/Magento/FunctionalTestingFramework/Console/UpgradeTestsCommand.php b/src/Magento/FunctionalTestingFramework/Console/UpgradeTestsCommand.php index 526ab4903..06284605d 100644 --- a/src/Magento/FunctionalTestingFramework/Console/UpgradeTestsCommand.php +++ b/src/Magento/FunctionalTestingFramework/Console/UpgradeTestsCommand.php @@ -17,6 +17,8 @@ class UpgradeTestsCommand extends Command { + private const SUCCESS_EXIT_CODE = 0; + /** * Pool of upgrade scripts to run * @@ -46,10 +48,10 @@ protected function configure() * * @param InputInterface $input * @param OutputInterface $output - * @return int|null|void + * @return int * @throws \Exception */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { /** @var \Magento\FunctionalTestingFramework\Upgrade\UpgradeInterface[] $upgradeScriptObjects */ $upgradeScriptObjects = $this->upgradeScriptsList->getUpgradeScripts(); @@ -59,5 +61,7 @@ protected function execute(InputInterface $input, OutputInterface $output) LoggingUtil::getInstance()->getLogger(get_class($upgradeScriptObject))->info($upgradeOutput); $output->writeln($upgradeOutput . PHP_EOL); } + + return self::SUCCESS_EXIT_CODE; } } diff --git a/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/ArrayType.php b/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/ArrayType.php index 566c09912..b292236c6 100644 --- a/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/ArrayType.php +++ b/src/Magento/FunctionalTestingFramework/Data/Argument/Interpreter/ArrayType.php @@ -98,7 +98,7 @@ private function compareItems($firstItemKey, $secondItemKey, $indexedItems) $secondValue = intval($secondItem['sortOrder']); } - if ($firstValue == $secondValue) { + if ($firstValue === $secondValue) { // These keys reflect initial relative position of items. // Allows stable sort for items with equal 'sortOrder' return $firstItemKey < $secondItemKey ? -1 : 1; diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Config/Dom.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Config/Dom.php index d4b8f7c8f..830abe6c9 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Config/Dom.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Config/Dom.php @@ -81,6 +81,13 @@ public function initDom($xml, $filename = null) } } + $itemNodes = $dom->getElementsByTagName('item'); + /** @var \DOMElement $itemNode */ + foreach ($itemNodes as $itemKey => $itemNode) { + if ($itemNode->hasAttribute("name") === false) { + $itemNode->setAttribute("name", (string)$itemKey); + } + } return $dom; } } diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Config/Reader/Filesystem.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Config/Reader/Filesystem.php deleted file mode 100644 index 4f21fabea..000000000 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Config/Reader/Filesystem.php +++ /dev/null @@ -1,81 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\FunctionalTestingFramework\DataGenerator\Config\Reader; - -/** - * Filesystem configuration loader. Loads configuration from XML files, split by scopes. - */ -class Filesystem extends \Magento\FunctionalTestingFramework\Config\Reader\Filesystem -{ - /** - * An array of paths which do have a key for merging but instead are value based nodes which can only be appended - * - * @var array - */ - private $mergeablePaths; - - /** - * Constructor - * - * @param \Magento\FunctionalTestingFramework\Config\FileResolverInterface $fileResolver - * @param \Magento\FunctionalTestingFramework\Config\ConverterInterface $converter - * @param \Magento\FunctionalTestingFramework\Config\SchemaLocatorInterface $schemaLocator - * @param \Magento\FunctionalTestingFramework\Config\ValidationStateInterface $validationState - * @param string $fileName - * @param array $idAttributes - * @param array $mergeablePaths - * @param string $domDocumentClass - * @param string $defaultScope - */ - public function __construct( - \Magento\FunctionalTestingFramework\Config\FileResolverInterface $fileResolver, - \Magento\FunctionalTestingFramework\Config\ConverterInterface $converter, - \Magento\FunctionalTestingFramework\Config\SchemaLocatorInterface $schemaLocator, - \Magento\FunctionalTestingFramework\Config\ValidationStateInterface $validationState, - $fileName, - $idAttributes = [], - $mergeablePaths = [], - $domDocumentClass = \Magento\FunctionalTestingFramework\Config\Dom::class, - $defaultScope = 'global' - ) { - $this->fileResolver = $fileResolver; - $this->converter = $converter; - $this->fileName = $fileName; - $this->idAttributes = array_replace($this->idAttributes, $idAttributes); - $this->mergeablePaths = $mergeablePaths; - $this->validationState = $validationState; - $this->schemaFile = $schemaLocator->getSchema(); - $this->perFileSchema = $schemaLocator->getPerFileSchema() && $validationState->isValidationRequired() - ? $schemaLocator->getPerFileSchema() : null; - $this->domDocumentClass = $domDocumentClass; - $this->defaultScope = $defaultScope; - } - - /** - * Return newly created instance of a config merger. Overridden to include new arg in mergerClass. - * - * @param string $mergerClass - * @param string $initialContents - * @return \Magento\FunctionalTestingFramework\Config\Dom - * @throws \UnexpectedValueException - */ - protected function createConfigMerger($mergerClass, $initialContents) - { - $result = new $mergerClass( - $initialContents, - $this->idAttributes, - $this->mergeablePaths, - null, - $this->perFileSchema - ); - if (!$result instanceof \Magento\FunctionalTestingFramework\Config\Dom) { - throw new \UnexpectedValueException( - "Instance of the DOM config merger is expected, got {$mergerClass} instead." - ); - } - return $result; - } -} diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/CredentialStore.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/CredentialStore.php index d6e6b9a69..0347ddaa8 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/CredentialStore.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/CredentialStore.php @@ -58,7 +58,7 @@ class CredentialStore */ public static function getInstance() { - if (self::$INSTANCE == null) { + if (self::$INSTANCE === null) { self::$INSTANCE = new CredentialStore(); } @@ -162,13 +162,13 @@ private function getExceptionContexts() $exceptionMessage = "\n"; foreach ($this->exceptionContexts->getErrors() as $type => $exceptions) { $exceptionMessage .= "\nException from "; - if ($type == self::ARRAY_KEY_FOR_FILE) { + if ($type === self::ARRAY_KEY_FOR_FILE) { $exceptionMessage .= "File Storage: \n"; } - if ($type == self::ARRAY_KEY_FOR_VAULT) { + if ($type === self::ARRAY_KEY_FOR_VAULT) { $exceptionMessage .= "Vault Storage: \n"; } - if ($type == self::ARRAY_KEY_FOR_AWS_SECRETS_MANAGER) { + if ($type === self::ARRAY_KEY_FOR_AWS_SECRETS_MANAGER) { $exceptionMessage .= "AWS Secrets Manager Storage: \n"; } @@ -221,11 +221,13 @@ private function initializeCredentialStorage() * * @return void */ - private function initializeFileStorage() + private function initializeFileStorage(): void { // Initialize file storage try { - $this->credStorage[self::ARRAY_KEY_FOR_FILE] = new FileStorage(); + $fileStorage = new FileStorage(); + $fileStorage->initialize(); + $this->credStorage[self::ARRAY_KEY_FOR_FILE] = $fileStorage; } catch (TestFrameworkException $e) { // Print error message in console print_r($e->getMessage()); diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/DataObjectHandler.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/DataObjectHandler.php index 26b603915..da7888388 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/DataObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/DataObjectHandler.php @@ -191,8 +191,8 @@ private function processParserOutput($parserOutput) if (array_key_exists(self::OBJ_DEPRECATED, $rawEntity)) { $deprecated = $rawEntity[self::OBJ_DEPRECATED]; LoggingUtil::getInstance()->getLogger(self::class)->deprecation( - $deprecated, - ["dataName" => $filename, "deprecatedEntity" => $deprecated] + "The data entity '{$name}' is deprecated.", + ["fileName" => $filename, "deprecatedMessage" => $deprecated] ); } @@ -226,8 +226,8 @@ private function processParserOutput($parserOutput) private function processArray($arrayItems, $data, $key) { $items = []; - foreach ($arrayItems as $item) { - $items[] = $item[self::_VALUE]; + foreach ($arrayItems as $key => $item) { + $items[$key] = $item[self::_VALUE]; } return array_merge($items, $data[$key] ?? []); @@ -319,8 +319,8 @@ private function processVarElements($entityData) */ private function extendDataObject($dataObject) { - if ($dataObject->getParentName() != null) { - if ($dataObject->getParentName() == $dataObject->getName()) { + if ($dataObject->getParentName() !== null) { + if ($dataObject->getParentName() === $dataObject->getName()) { throw new TestFrameworkException("Mftf Data can not extend from itself: " . $dataObject->getName()); } return $this->extendUtil->extendEntity($dataObject); diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/OperationDefinitionObjectHandler.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/OperationDefinitionObjectHandler.php index 2c8ab7530..eb4651b3e 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/OperationDefinitionObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/OperationDefinitionObjectHandler.php @@ -215,8 +215,8 @@ private function initialize() if ($deprecated !== null) { LoggingUtil::getInstance()->getLogger(self::class)->deprecation( - $deprecated, - ["operationName" => $dataDefName, "deprecatedOperation" => $deprecated] + $message = "The operation {$dataDefName} is deprecated.", + ["operationType" => $operation, "deprecatedMessage" => $deprecated] ); } diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/PersistedObjectHandler.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/PersistedObjectHandler.php index 927c0ab4e..e9f905a6b 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/PersistedObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/PersistedObjectHandler.php @@ -86,17 +86,6 @@ public function createEntity( foreach ($dependentObjectKeys as $objectKey) { $retrievedDependentObjects[] = $this->retrieveEntity($objectKey, $scope); } - - foreach ($overrideFields as $index => $field) { - try { - $decrptedField = CredentialStore::getInstance()->decryptAllSecretsInString($field); - if ($decrptedField !== false) { - $overrideFields[$index] = $decrptedField; - } - } catch (TestFrameworkException $e) { - //catch exception if Credentials are not defined - } - } $retrievedEntity = DataObjectHandler::getInstance()->getObject($entity); @@ -107,6 +96,8 @@ public function createEntity( ); } + $overrideFields = $this->resolveOverrideFields($overrideFields); + $persistedObject = new DataPersistenceHandler( $retrievedEntity, $retrievedDependentObjects, @@ -115,9 +106,9 @@ public function createEntity( $persistedObject->createEntity($storeCode); - if ($scope == self::TEST_SCOPE) { + if ($scope === self::TEST_SCOPE) { $this->testObjects[$key] = $persistedObject; - } elseif ($scope == self::HOOK_SCOPE) { + } elseif ($scope === self::HOOK_SCOPE) { $this->hookObjects[$key] = $persistedObject; } else { $this->suiteObjects[$key] = $persistedObject; @@ -179,9 +170,9 @@ public function getEntity($key, $scope, $entity, $dependentObjectKeys = [], $sto ); $persistedObject->getEntity($index, $storeCode); - if ($scope == self::TEST_SCOPE) { + if ($scope === self::TEST_SCOPE) { $this->testObjects[$key] = $persistedObject; - } elseif ($scope == self::HOOK_SCOPE) { + } elseif ($scope === self::HOOK_SCOPE) { $this->hookObjects[$key] = $persistedObject; } else { $this->suiteObjects[$key] = $persistedObject; @@ -202,8 +193,9 @@ public function retrieveEntityField($stepKey, $field, $scope) $warnMsg = "Undefined field {$field} in entity object with a stepKey of {$stepKey}\n"; $warnMsg .= "Please fix the invalid reference. This will result in fatal error in next major release."; //TODO: change this to throw an exception in next major release - LoggingUtil::getInstance()->getLogger(PersistedObjectHandler::class)->warn($warnMsg); - if (MftfApplicationConfig::getConfig()->getPhase() !== MftfApplicationConfig::UNIT_TEST_PHASE) { + LoggingUtil::getInstance()->getLogger(PersistedObjectHandler::class)->warning($warnMsg); + if (MftfApplicationConfig::getConfig()->verboseEnabled() + && MftfApplicationConfig::getConfig()->getPhase() !== MftfApplicationConfig::UNIT_TEST_PHASE) { print("\n$warnMsg\n"); } } @@ -222,7 +214,7 @@ private function retrieveEntity($stepKey, $scope) // Assume TEST_SCOPE is default $entityArrays = [$this->testObjects, $this->hookObjects, $this->suiteObjects]; - if ($scope == self::HOOK_SCOPE) { + if ($scope === self::HOOK_SCOPE) { $entityArrays[0] = $this->hookObjects; $entityArrays[1] = $this->testObjects; } @@ -262,4 +254,29 @@ public function clearSuiteObjects() { $this->suiteObjects = []; } + + /** + * Resolve secret values in $overrideFields + * + * @param array $overrideFields + * @return array + */ + private function resolveOverrideFields($overrideFields) + { + foreach ($overrideFields as $index => $field) { + if (is_array($field)) { + $overrideFields[$index] = $this->resolveOverrideFields($field); + } elseif (is_string($field)) { + try { + $decrptedField = CredentialStore::getInstance()->decryptAllSecretsInString($field); + if ($decrptedField !== false) { + $overrideFields[$index] = $decrptedField; + } + } catch (TestFrameworkException $e) { + //catch exception if Credentials are not defined + } + } + } + return $overrideFields; + } } diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/SecretStorage/FileStorage.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/SecretStorage/FileStorage.php index be77a6de2..48087e890 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/SecretStorage/FileStorage.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/SecretStorage/FileStorage.php @@ -6,8 +6,8 @@ namespace Magento\FunctionalTestingFramework\DataGenerator\Handlers\SecretStorage; -use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; use Magento\FunctionalTestingFramework\Util\Path\FilePathFormatter; @@ -21,14 +21,17 @@ class FileStorage extends BaseStorage private $secretData = []; /** - * FileStorage constructor + * Initialize secret data value which represents encrypted credentials + * + * @return void * @throws TestFrameworkException */ - public function __construct() + public function initialize(): void { - parent::__construct(); - $creds = $this->readInCredentialsFile(); - $this->secretData = $this->encryptCredFileContents($creds); + if (!$this->secretData) { + $creds = $this->readInCredentialsFile(); + $this->secretData = $this->encryptCredFileContents($creds); + } } /** @@ -36,10 +39,12 @@ public function __construct() * * @param string $key * @return string|null + * @throws TestFrameworkException */ - public function getEncryptedValue($key) + public function getEncryptedValue($key): ?string { - $value = null; + $this->initialize(); + // Check if secret is in cached array if (null !== ($value = parent::getEncryptedValue($key))) { return $value; @@ -88,6 +93,7 @@ private function readInCredentialsFile() * * @param array $credContents * @return array + * @throws TestFrameworkException */ private function encryptCredFileContents($credContents) { @@ -95,6 +101,10 @@ private function encryptCredFileContents($credContents) foreach ($credContents as $credValue) { if (substr($credValue, 0, 1) === '#' || empty($credValue)) { continue; + } elseif (strpos($credValue, "=") === false) { + throw new TestFrameworkException( + $credValue . " not configured correctly in .credentials file" + ); } list($key, $value) = explode("=", $credValue, 2); diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/SecretStorage/VaultStorage.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/SecretStorage/VaultStorage.php index 39839e8f9..7e307b30c 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/SecretStorage/VaultStorage.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/SecretStorage/VaultStorage.php @@ -6,12 +6,15 @@ namespace Magento\FunctionalTestingFramework\DataGenerator\Handlers\SecretStorage; +use Laminas\Diactoros\RequestFactory; +use Laminas\Diactoros\StreamFactory; +use Laminas\Diactoros\Uri; +use GuzzleHttp\Client as GuzzleClient; use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; use Vault\Client; -use VaultTransports\Guzzle6Transport; class VaultStorage extends BaseStorage { @@ -78,8 +81,17 @@ public function __construct($baseUrl, $secretBasePath) { parent::__construct(); if (null === $this->client) { - // Creating the client using Guzzle6 Transport and passing a custom url - $this->client = new Client(new Guzzle6Transport(['base_uri' => $baseUrl])); + // client configuration and override http errors settings + $this->client = new Client( + new Uri($baseUrl), + new GuzzleClient([ + 'timeout' => 15, + 'base_uri' => $baseUrl, + 'http_errors' => false + ]), + new RequestFactory(), + new StreamFactory() + ); $this->secretBasePath = $secretBasePath; } $this->readVaultTokenFromFileSystem(); diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/SecretStorage/VaultTokenAuthStrategy.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/SecretStorage/VaultTokenAuthStrategy.php index 716344ca1..0f071216d 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/SecretStorage/VaultTokenAuthStrategy.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Handlers/SecretStorage/VaultTokenAuthStrategy.php @@ -36,7 +36,7 @@ public function __construct($token) * @return Auth * @throws TestFrameworkException */ - public function authenticate() + public function authenticate(): ?Auth { try { return new Auth(['clientToken' => $this->token]); diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/EntityDataObject.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/EntityDataObject.php index 2204364f5..817fb6ec4 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/EntityDataObject.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/EntityDataObject.php @@ -233,7 +233,7 @@ private function resolveDataReferences($name, $uniquenessFormat) $this->data[$name_lower], $this->name . '.' . $name ); - if (null === $uniquenessData || $uniquenessFormat == self::NO_UNIQUE_PROCESS) { + if (null === $uniquenessData || $uniquenessFormat === self::NO_UNIQUE_PROCESS) { return $this->data[$name_lower]; } return $this->formatUniqueData($name_lower, $uniquenessData, $uniquenessFormat); @@ -276,7 +276,7 @@ private function formatUniqueData($name, $uniqueData, $uniqueDataFormat) switch ($uniqueDataFormat) { case self::SUITE_UNIQUE_VALUE: $this->checkUniquenessFunctionExists(self::SUITE_UNIQUE_FUNCTION, $uniqueDataFormat); - if ($uniqueData == 'prefix') { + if ($uniqueData === 'prefix') { return msqs($this->getName()) . $this->data[$name]; } else { // $uniData == 'suffix' return $this->data[$name] . msqs($this->getName()); @@ -284,21 +284,21 @@ private function formatUniqueData($name, $uniqueData, $uniqueDataFormat) break; case self::CEST_UNIQUE_VALUE: $this->checkUniquenessFunctionExists(self::CEST_UNIQUE_FUNCTION, $uniqueDataFormat); - if ($uniqueData == 'prefix') { + if ($uniqueData === 'prefix') { return msq($this->getName()) . $this->data[$name]; } else { // $uniqueData == 'suffix' return $this->data[$name] . msq($this->getName()); } break; case self::SUITE_UNIQUE_NOTATION: - if ($uniqueData == 'prefix') { + if ($uniqueData === 'prefix') { return self::SUITE_UNIQUE_FUNCTION . '("' . $this->getName() . '")' . $this->data[$name]; } else { // $uniqueData == 'suffix' return $this->data[$name] . self::SUITE_UNIQUE_FUNCTION . '("' . $this->getName() . '")'; } break; case self::CEST_UNIQUE_NOTATION: - if ($uniqueData == 'prefix') { + if ($uniqueData === 'prefix') { return self::CEST_UNIQUE_FUNCTION . '("' . $this->getName() . '")' . $this->data[$name]; } else { // $uniqueData == 'suffix' return $this->data[$name] . self::CEST_UNIQUE_FUNCTION . '("' . $this->getName() . '")'; @@ -358,7 +358,7 @@ public function getLinkedEntitiesOfType($type) $groupedArray = []; foreach ($this->linkedEntities as $entityName => $entityType) { - if ($entityType == $type) { + if ($entityType === $type) { $groupedArray[] = $entityName; } } diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/OperationDefinitionObject.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/OperationDefinitionObject.php index 67fe48300..ab5879301 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/OperationDefinitionObject.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Objects/OperationDefinitionObject.php @@ -6,6 +6,8 @@ namespace Magento\FunctionalTestingFramework\DataGenerator\Objects; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; + /** * Class OperationDefinitionObject * @SuppressWarnings(PHPMD) @@ -165,7 +167,7 @@ public function __construct( $this->operation = $operation; $this->dataType = $dataType; $this->apiMethod = $apiMethod; - $this->apiUri = trim($apiUri, '/'); + $this->apiUri = trim($apiUri ?? '', '/'); $this->auth = $auth; $this->headers = $headers; $this->params = $params; @@ -341,4 +343,20 @@ public function addQueryParams() $this->apiUrl = $this->apiUrl . $paramName . "=" . $paramValue; } } + + /** + * Function to log a referenced deprecated operation at runtime. + * + * @return void + */ + public function logDeprecated() + { + if ($this->deprecated !== null) { + LoggingUtil::getInstance()->getLogger(self::class)->deprecation( + $message = "The operation {$this->name} is deprecated.", + ["operationType" => $this->operation, "deprecatedMessage" => $this->deprecated], + true + ); + } + } } diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/AbstractExecutor.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/AbstractExecutor.php deleted file mode 100644 index e27da3718..000000000 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/AbstractExecutor.php +++ /dev/null @@ -1,34 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\FunctionalTestingFramework\DataGenerator\Persist\Curl; - -use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; -use Magento\FunctionalTestingFramework\Util\Path\UrlFormatter; -use Magento\FunctionalTestingFramework\Util\Protocol\CurlInterface; - -/** - * Abstract Curl executor. - */ -abstract class AbstractExecutor implements CurlInterface -{ - /** - * Returns Magento base URL. Used as a fallback for other services (eg. WebApi, Backend) - * - * @var string - */ - protected static $baseUrl = null; - - /** - * Returns base URL for Magento instance - * @return string - * @throws TestFrameworkException - */ - public function getBaseUrl(): string - { - return UrlFormatter::format(getenv('MAGENTO_BASE_URL')); - } -} diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/WebapiExecutor.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/WebapiExecutor.php deleted file mode 100644 index 494ce8f97..000000000 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/WebapiExecutor.php +++ /dev/null @@ -1,209 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\FunctionalTestingFramework\DataGenerator\Persist\Curl; - -use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; -use Magento\FunctionalTestingFramework\Util\Path\UrlFormatter; -use Magento\FunctionalTestingFramework\Util\Protocol\CurlInterface; -use Magento\FunctionalTestingFramework\Util\Protocol\CurlTransport; - -/** - * Curl executor for Magento Web Api requests. - */ -class WebapiExecutor extends AbstractExecutor implements CurlInterface -{ - /** - * Curl transport protocol. - * - * @var CurlTransport - */ - private $transport; - - /** - * Api headers. - * - * @var array - */ - private $headers = [ - 'Accept: application/json', - 'Content-Type: application/json', - ]; - - /** - * Response data. - * - * @var string - */ - private $response; - - /** - * Admin authentication url. - */ - const ADMIN_AUTH_URL = '/V1/integration/admin/token'; - - /** - * Store code in api request. - * - * @var string - */ - private $storeCode; - - /** - * Admin user auth token. - * - * @var string - */ - private $authToken; - - /** - * WebapiExecutor Constructor. - * - * @param string $storeCode - * @throws TestFrameworkException - */ - public function __construct($storeCode = null) - { - $this->storeCode = $storeCode; - $this->authToken = null; - $this->transport = new CurlTransport(); - $this->authorize(); - } - - /** - * Returns base URL for Magento Web API instance - * @return string - * @throws TestFrameworkException - */ - public function getBaseUrl(): string - { - $webapiHost = getenv('MAGENTO_RESTAPI_SERVER_HOST'); - $webapiPort = getenv("MAGENTO_RESTAPI_SERVER_PORT"); - $webapiProtocol = getenv("MAGENTO_RESTAPI_SERVER_PROTOCOL"); - - if ($webapiHost && $webapiProtocol) { - $baseUrl = UrlFormatter::format( - sprintf('%s://%s', $webapiProtocol, $webapiHost), - false - ); - } elseif ($webapiHost) { - $baseUrl = UrlFormatter::format(sprintf('%s', $webapiProtocol, $webapiHost), false); - } - - if (!isset($baseUrl)) { - $baseUrl = rtrim(parent::getBaseUrl(), '/'); - } - - if ($webapiPort) { - $baseUrl .= ':' . $webapiPort; - } - - return $baseUrl . '/'; - } - - /** - * Acquire and store the authorization token needed for REST requests. - * - * @return void - * @throws TestFrameworkException - */ - protected function authorize() - { - $authUrl = $this->getFormattedUrl(self::ADMIN_AUTH_URL); - $authCreds = [ - 'username' => getenv('MAGENTO_ADMIN_USERNAME'), - 'password' => getenv('MAGENTO_ADMIN_PASSWORD') - ]; - - $this->transport->write($authUrl, json_encode($authCreds), CurlInterface::POST, $this->headers); - $this->authToken = str_replace('"', "", $this->read()); - $this->headers = array_merge(['Authorization: Bearer ' . $this->authToken], $this->headers); - } - - /** - * Send request to the remote server. - * - * @param string $url - * @param array $data - * @param string $method - * @param array $headers - * @return void - * @throws TestFrameworkException - */ - public function write($url, $data = [], $method = CurlInterface::POST, $headers = []) - { - $this->transport->write( - $this->getFormattedUrl($url), - json_encode($data, JSON_PRETTY_PRINT), - $method, - array_unique(array_merge($headers, $this->headers)) - ); - } - - /** - * Read response from server. - * - * @param string $successRegex - * @param string $returnRegex - * @param string|null $returnIndex - * @return string - * @throws TestFrameworkException - */ - public function read($successRegex = null, $returnRegex = null, $returnIndex = null) - { - $this->response = $this->transport->read(); - return $this->response; - } - - /** - * Add additional option to cURL. - * - * @param integer $option CURLOPT_* constants. - * @param integer|string|boolean|array $value - * @return void - */ - public function addOption($option, $value) - { - $this->transport->addOption($option, $value); - } - - /** - * Close the connection to the server. - * - * @return void - */ - public function close() - { - $this->transport->close(); - } - - /** - * Builds and returns URL for request, appending storeCode if needed. - * @param string $resource - * @return string - * @throws TestFrameworkException - */ - public function getFormattedUrl($resource) - { - $urlResult = $this->getBaseUrl() . 'rest/'; - if ($this->storeCode != null) { - $urlResult .= $this->storeCode . '/'; - } - $urlResult .= trim($resource, '/'); - return $urlResult; - } - - /** - * Return admin auth token. - * - * @return string - * @throws TestFrameworkException - */ - public function getAuthToken() - { - return $this->authToken; - } -} diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/CurlHandler.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/CurlHandler.php index 05178c915..89055b83f 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/CurlHandler.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/CurlHandler.php @@ -6,15 +6,15 @@ namespace Magento\FunctionalTestingFramework\DataGenerator\Persist; use Magento\FunctionalTestingFramework\Allure\AllureHelper; -use Magento\FunctionalTestingFramework\DataGenerator\Persist\Curl\AdminExecutor; -use Magento\FunctionalTestingFramework\DataGenerator\Persist\Curl\FrontendExecutor; -use Magento\FunctionalTestingFramework\DataGenerator\Persist\Curl\WebapiExecutor; -use Magento\FunctionalTestingFramework\DataGenerator\Persist\Curl\WebapiNoAuthExecutor; +use Magento\FunctionalTestingFramework\DataTransport\AdminFormExecutor; +use Magento\FunctionalTestingFramework\DataTransport\FrontendFormExecutor; +use Magento\FunctionalTestingFramework\DataTransport\WebApiExecutor; +use Magento\FunctionalTestingFramework\DataTransport\WebApiNoAuthExecutor; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\OperationDefinitionObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; use Magento\FunctionalTestingFramework\DataGenerator\Objects\OperationDefinitionObject; use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; -use Magento\FunctionalTestingFramework\Util\Protocol\CurlInterface; +use Magento\FunctionalTestingFramework\DataTransport\Protocol\CurlInterface; /** * Class CurlHandler @@ -123,6 +123,7 @@ public function executeRequest($dependentEntities) $returnRegex = $this->operationDefinition->getReturnRegex(); $returnIndex = $this->operationDefinition->getReturnIndex(); $method = $this->operationDefinition->getApiMethod(); + $this->operationDefinition->logDeprecated(); AllureHelper::addAttachmentToCurrentStep($apiUrl, 'API Endpoint'); AllureHelper::addAttachmentToCurrentStep(json_encode($headers, JSON_PRETTY_PRINT), 'Request Headers'); @@ -134,19 +135,21 @@ public function executeRequest($dependentEntities) false ); + AllureHelper::addAttachmentToCurrentStep(json_encode($this->requestData, JSON_PRETTY_PRINT), 'Request Body'); + if (($contentType === 'application/json') && ($authorization === 'adminOauth')) { $this->isJson = true; - $executor = new WebapiExecutor($this->storeCode); + $executor = new WebApiExecutor($this->storeCode); } elseif ($authorization === 'adminFormKey') { - $executor = new AdminExecutor($this->operationDefinition->removeUrlBackend()); + $executor = new AdminFormExecutor($this->operationDefinition->removeUrlBackend()); } elseif ($authorization === 'customerFormKey') { - $executor = new FrontendExecutor( + $executor = new FrontendFormExecutor( $this->requestData['customer_email'], $this->requestData['customer_password'] ); } elseif ($authorization === 'anonymous') { $this->isJson = true; - $executor = new WebapiNoAuthExecutor($this->storeCode); + $executor = new WebApiNoAuthExecutor($this->storeCode); } if (!$executor) { @@ -169,7 +172,6 @@ public function executeRequest($dependentEntities) $response = $executor->read($successRegex, $returnRegex, $returnIndex); $executor->close(); - AllureHelper::addAttachmentToCurrentStep(json_encode($this->requestData, JSON_PRETTY_PRINT), 'Request Body'); AllureHelper::addAttachmentToCurrentStep( json_encode(json_decode($response, true), JSON_PRETTY_PRINT+JSON_UNESCAPED_UNICODE+JSON_UNESCAPED_SLASHES), 'Response Data' @@ -229,7 +231,7 @@ private function resolveUrlReference($urlIn, $entityObjects) foreach ($entityObjects as $entityObject) { $param = null; - if ($paramEntityParent === "" || $entityObject->getType() == $paramEntityParent) { + if ($paramEntityParent === "" || $entityObject->getType() === $paramEntityParent) { $param = $entityObject->getDataByName( $dataItem, EntityDataObject::CEST_UNIQUE_VALUE diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/DataPersistenceHandler.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/DataPersistenceHandler.php index 73ee2be19..55197a211 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/DataPersistenceHandler.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/DataPersistenceHandler.php @@ -9,6 +9,7 @@ use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\ObjectManagerFactory; /** * Class DataPersistenceHandler @@ -81,12 +82,15 @@ public function __construct($entityObject, $dependentObjects = [], $customFields * @return void * @throws TestFrameworkException */ - public function createEntity($storeCode = null) + public function createEntity(?string $storeCode = null) { if (!empty($storeCode)) { $this->storeCode = $storeCode; } - $curlHandler = new CurlHandler('create', $this->entityObject, $this->storeCode); + $curlHandler = ObjectManagerFactory::getObjectManager()->create( + CurlHandler::class, + ['operation' => 'create', 'entityObject' => $this->entityObject, 'storeCode' => $this->storeCode] + ); $result = $curlHandler->executeRequest($this->dependentObjects); $this->setCreatedObject( $result, @@ -111,7 +115,10 @@ public function updateEntity($updateDataName, $updateDependentObjects = []) $this->dependentObjects[] = $dependentObject->getCreatedObject(); } $updateEntityObject = DataObjectHandler::getInstance()->getObject($updateDataName); - $curlHandler = new CurlHandler('update', $updateEntityObject, $this->storeCode); + $curlHandler = ObjectManagerFactory::getObjectManager()->create( + CurlHandler::class, + ['operation' => 'update', 'entityObject' => $updateEntityObject, 'storeCode' => $this->storeCode] + ); $result = $curlHandler->executeRequest(array_merge($this->dependentObjects, [$this->createdObject])); $this->setCreatedObject( $result, @@ -129,12 +136,15 @@ public function updateEntity($updateDataName, $updateDependentObjects = []) * @return void * @throws TestFrameworkException */ - public function getEntity($index = null, $storeCode = null) + public function getEntity(?string $index = null, ?string $storeCode = null) { if (!empty($storeCode)) { $this->storeCode = $storeCode; } - $curlHandler = new CurlHandler('get', $this->entityObject, $this->storeCode); + $curlHandler = ObjectManagerFactory::getObjectManager()->create( + CurlHandler::class, + ['operation' => 'get', 'entityObject' => $this->entityObject, 'storeCode' => $this->storeCode] + ); $result = $curlHandler->executeRequest($this->dependentObjects); $this->setCreatedObject( $result, @@ -152,7 +162,11 @@ public function getEntity($index = null, $storeCode = null) */ public function deleteEntity() { - $curlHandler = new CurlHandler('delete', $this->createdObject, $this->storeCode); + $curlHandler = ObjectManagerFactory::getObjectManager()->create( + CurlHandler::class, + ['operation' => 'delete', 'entityObject' => $this->createdObject, 'storeCode' => $this->storeCode] + ); + $curlHandler->executeRequest($this->dependentObjects); } diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/OperationDataArrayResolver.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/OperationDataArrayResolver.php index 60b6e73c2..8ff1f72c9 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/OperationDataArrayResolver.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/OperationDataArrayResolver.php @@ -46,7 +46,7 @@ class OperationDataArrayResolver * * @param array $dependentEntities */ - public function __construct($dependentEntities = null) + public function __construct(?array $dependentEntities = null) { if ($dependentEntities !== null) { foreach ($dependentEntities as $entity) { @@ -78,7 +78,7 @@ public function resolveOperationDataArray($entityObject, $operationMetadata, $op self::incrementSequence($entityObject->getName()); foreach ($operationMetadata as $operationElement) { - if ($operationElement->getType() == OperationElementExtractor::OPERATION_OBJECT_OBJ_NAME) { + if ($operationElement->getType() === OperationElementExtractor::OPERATION_OBJECT_OBJ_NAME) { $entityObj = $this->resolveOperationObjectAndEntityData($entityObject, $operationElement->getValue()); if (null === $entityObj && $operationElement->isRequired()) { throw new \Exception(sprintf( @@ -189,13 +189,13 @@ private function resolvePrimitiveReference($entityObject, $operationKey, $operat EntityDataObject::CEST_UNIQUE_VALUE ); - if ($elementData == null && $entityObject->getVarReference($operationKey) != null) { + if ($elementData === null && $entityObject->getVarReference($operationKey) !== null) { list($type, $field) = explode( DataObjectHandler::_SEPARATOR, $entityObject->getVarReference($operationKey) ); - if ($operationElementType == OperationDefinitionObjectHandler::ENTITY_OPERATION_ARRAY) { + if ($operationElementType === OperationDefinitionObjectHandler::ENTITY_OPERATION_ARRAY) { $elementDatas = []; $entities = $this->getDependentEntitiesOfType($type); foreach ($entities as $entity) { @@ -224,7 +224,7 @@ private function getDependentEntitiesOfType($type) $entitiesOfType = []; foreach ($this->dependentEntities as $dependentEntity) { - if ($dependentEntity->getType() == $type) { + if ($dependentEntity->getType() === $type) { $entitiesOfType[] = $dependentEntity; } } @@ -244,11 +244,12 @@ private function getDependentEntitiesOfType($type) */ private function resolveOperationObjectAndEntityData($entityObject, $operationElementValue) { - if ($operationElementValue != $entityObject->getType()) { - // if we have a mismatch attempt to retrieve linked data and return just the first linkage + if ($operationElementValue !== $entityObject->getType()) { + // if we have a mismatch attempt to retrieve linked data and return just the last linkage + // this enables overwriting of required entity fields $linkName = $entityObject->getLinkedEntitiesOfType($operationElementValue); if (!empty($linkName)) { - $linkName = $linkName[0]; + $linkName = array_pop($linkName); return DataObjectHandler::getInstance()->getObject($linkName); } return null; @@ -274,7 +275,7 @@ private function resolveNonPrimitiveElement($entityName, $operationElement, $ope // in array case if (!is_array($operationElement->getValue()) && !empty($operationElement->getNestedOperationElement($operationElement->getValue())) - && $operationElement->getType() == OperationDefinitionObjectHandler::ENTITY_OPERATION_ARRAY + && $operationElement->getType() === OperationDefinitionObjectHandler::ENTITY_OPERATION_ARRAY ) { $operationSubArray = $this->resolveOperationDataArray( $linkedEntityObj, @@ -402,7 +403,7 @@ private function resolvePrimitiveReferenceElement($entityObject, $operationEleme // If data was defined at all, attempt to put it into operation data array // If data was not defined, and element is required, throw exception // If no data is defined, don't input defaults per primitive into operation data array - if ($elementData != null) { + if ($elementData !== null && $elementData !== '') { if (array_key_exists($operationElement->getKey(), $entityObject->getUniquenessData())) { $uniqueData = $entityObject->getUniquenessDataByName($operationElement->getKey()); if ($uniqueData === 'suffix') { @@ -448,7 +449,7 @@ private function resolveNonPrimitiveReferenceElement($entityObject, $operation, $entityNamesOfType = $entityObject->getLinkedEntitiesOfType($operationElementType); // If an element is required by metadata, but was not provided in the entity, throw an exception - if ($operationElement->isRequired() && $entityNamesOfType == null) { + if ($operationElement->isRequired() && $entityNamesOfType === null) { throw new \Exception(sprintf( self::EXCEPTION_REQUIRED_DATA, $operationElement->getType(), @@ -475,7 +476,7 @@ private function resolveNonPrimitiveReferenceElement($entityObject, $operation, } } - if ($operationElement->getType() == OperationDefinitionObjectHandler::ENTITY_OPERATION_ARRAY) { + if ($operationElement->getType() === OperationDefinitionObjectHandler::ENTITY_OPERATION_ARRAY) { $operationDataArray[$operationElement->getKey()][] = $operationDataSubArray; } else { $operationDataArray[$operationElement->getKey()] = $operationDataSubArray; diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Util/DataExtensionUtil.php b/src/Magento/FunctionalTestingFramework/DataGenerator/Util/DataExtensionUtil.php index 3a12bdce1..d803be9a2 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Util/DataExtensionUtil.php +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/Util/DataExtensionUtil.php @@ -32,7 +32,7 @@ public function extendEntity($entityObject) { // Check to see if the parent entity exists $parentEntity = DataObjectHandler::getInstance()->getObject($entityObject->getParentName()); - if ($parentEntity == null) { + if ($parentEntity === null) { throw new XmlException( "Parent Entity " . $entityObject->getParentName() . @@ -52,8 +52,8 @@ public function extendEntity($entityObject) PHP_EOL ); } - if (MftfApplicationConfig::getConfig()->verboseEnabled() && - MftfApplicationConfig::getConfig()->getPhase() !== MftfApplicationConfig::UNIT_TEST_PHASE) { + if (MftfApplicationConfig::getConfig()->verboseEnabled() + && MftfApplicationConfig::getConfig()->getPhase() !== MftfApplicationConfig::UNIT_TEST_PHASE) { print("Extending Data: " . $parentEntity->getName() . " => " . $entityObject->getName() . PHP_EOL); } @@ -62,7 +62,7 @@ public function extendEntity($entityObject) // Get all data for both parent and child and merge $referencedData = $parentEntity->getAllData(); - $newData = array_merge($referencedData, $entityObject->getAllData()); + $newData = array_extend($referencedData, $entityObject->getAllData()); // Get all linked references for both parent and child and merge $referencedLinks = $parentEntity->getLinkedEntities(); diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd b/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd index 9d0d8bb35..923f7c48f 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd +++ b/src/Magento/FunctionalTestingFramework/DataGenerator/etc/dataProfileSchema.xsd @@ -118,17 +118,32 @@ <xs:complexType name="arrayType"> <xs:sequence> - <xs:element name="item" type="xs:string" maxOccurs="unbounded" minOccurs="0"> - <xs:annotation> - <xs:documentation> - Individual piece of data to be passed in as part of the parrent array type. - </xs:documentation> - </xs:annotation> + <xs:element name="item" type="itemType" maxOccurs="unbounded" minOccurs="0"> + <xs:annotation> + <xs:documentation> + Individual piece of data to be passed in as part of the parrent array type. + </xs:documentation> + </xs:annotation> </xs:element> </xs:sequence> <xs:attribute type="xs:string" name="key" use="required"/> </xs:complexType> + + <xs:complexType name="itemType"> + <xs:simpleContent> + <xs:extension base="xs:string"> + <xs:attribute type="xs:string" name="name" use="optional"> + <xs:annotation> + <xs:documentation> + Key attribute of item pair. + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:extension> + </xs:simpleContent> + </xs:complexType> + <xs:complexType name="requiredEntityType"> <xs:simpleContent> <xs:extension base="xs:string"> diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/AdminExecutor.php b/src/Magento/FunctionalTestingFramework/DataTransport/AdminFormExecutor.php similarity index 70% rename from src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/AdminExecutor.php rename to src/Magento/FunctionalTestingFramework/DataTransport/AdminFormExecutor.php index 86b3b742f..105cce7e7 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/AdminExecutor.php +++ b/src/Magento/FunctionalTestingFramework/DataTransport/AdminFormExecutor.php @@ -4,17 +4,20 @@ * See COPYING.txt for license details. */ -namespace Magento\FunctionalTestingFramework\DataGenerator\Persist\Curl; +namespace Magento\FunctionalTestingFramework\DataTransport; -use Magento\FunctionalTestingFramework\Util\Path\UrlFormatter; -use Magento\FunctionalTestingFramework\Util\Protocol\CurlInterface; -use Magento\FunctionalTestingFramework\Util\Protocol\CurlTransport; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; +use Magento\FunctionalTestingFramework\Util\MftfGlobals; +use Magento\FunctionalTestingFramework\DataTransport\Protocol\CurlInterface; +use Magento\FunctionalTestingFramework\DataTransport\Protocol\CurlTransport; use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\DataTransport\Auth\Tfa\OTP; +use Magento\FunctionalTestingFramework\DataTransport\Auth\Tfa; /** * Curl executor for requests to Admin. */ -class AdminExecutor extends AbstractExecutor implements CurlInterface +class AdminFormExecutor implements CurlInterface { /** * Curl transport protocol. @@ -57,25 +60,6 @@ public function __construct($removeBackend) $this->authorize(); } - /** - * Returns base URL for Magento backend instance - * @return string - * @throws TestFrameworkException - */ - public function getBaseUrl(): string - { - $backendHost = getenv('MAGENTO_BACKEND_BASE_URL') - ? - UrlFormatter::format(getenv('MAGENTO_BACKEND_BASE_URL')) - : - parent::getBaseUrl(); - return empty(getenv('MAGENTO_BACKEND_NAME')) - ? - $backendHost - : - $backendHost . getenv('MAGENTO_BACKEND_NAME') . '/'; - } - /** * Authorize admin on backend. * @@ -85,21 +69,39 @@ public function getBaseUrl(): string private function authorize() { // Perform GET to backend url so form_key is set - $this->transport->write($this->getBaseUrl(), [], CurlInterface::GET); + $this->transport->write(MftfGlobals::getBackendBaseUrl(), [], CurlInterface::GET); $this->read(); // Authenticate admin user - $authUrl = $this->getBaseUrl() . 'admin/auth/login/'; + $authUrl = MftfGlobals::getBackendBaseUrl() . 'admin/auth/login/'; + $encryptedSecret = CredentialStore::getInstance()->getSecret('magento/MAGENTO_ADMIN_PASSWORD'); + $secret = CredentialStore::getInstance()->decryptSecretValue($encryptedSecret); $data = [ 'login[username]' => getenv('MAGENTO_ADMIN_USERNAME'), - 'login[password]' => getenv('MAGENTO_ADMIN_PASSWORD'), + 'login[password]' => $secret, 'form_key' => $this->formKey, ]; $this->transport->write($authUrl, $data, CurlInterface::POST); $response = $this->read(); + if (strpos($response, 'login-form')) { throw new TestFrameworkException('Admin user authentication failed!'); } + + // Get OTP + if (Tfa::isEnabled()) { + $authUrl = MftfGlobals::getBackendBaseUrl() . Tfa::getProviderAdminFormEndpoint('google'); + $data = [ + 'tfa_code' => OTP::getOTP(), + 'form_key' => $this->formKey, + ]; + $this->transport->write($authUrl, $data, CurlInterface::POST); + $response = json_decode($this->read()); + + if (!$response->success) { + throw new TestFrameworkException('Admin user 2FA authentication failed!'); + } + } } /** @@ -128,10 +130,11 @@ private function setFormKey() public function write($url, $data = [], $method = CurlInterface::POST, $headers = []) { $url = ltrim($url, "/"); - $apiUrl = $this->getBaseUrl() . $url; + $apiUrl = MftfGlobals::getBackendBaseUrl() . $url; if ($this->removeBackend) { - $apiUrl = parent::getBaseUrl() . $url; + //TODO + //Cannot find usage. Do we need this? } if ($this->formKey) { @@ -154,7 +157,7 @@ public function write($url, $data = [], $method = CurlInterface::POST, $headers * @return string|array * @throws TestFrameworkException */ - public function read($successRegex = null, $returnRegex = null, $returnIndex = null) + public function read(?string $successRegex = null, ?string $returnRegex = null, ?string $returnIndex = null) { $this->response = $this->transport->read(); $this->setFormKey(); diff --git a/src/Magento/FunctionalTestingFramework/DataTransport/Auth/Tfa.php b/src/Magento/FunctionalTestingFramework/DataTransport/Auth/Tfa.php new file mode 100644 index 000000000..04f1971b7 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/DataTransport/Auth/Tfa.php @@ -0,0 +1,108 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\DataTransport\Auth; + +use Magento\FunctionalTestingFramework\Util\MftfGlobals; +use Magento\FunctionalTestingFramework\DataTransport\Protocol\CurlInterface; +use Magento\FunctionalTestingFramework\DataTransport\Protocol\CurlTransport; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; + +/** + * Class Tfa (i.e. 2FA) + */ +class Tfa +{ + const WEB_API_AUTH_GOOGLE = 'V1/tfa/provider/google/authenticate'; + const ADMIN_FORM_AUTH_GOOGLE = 'tfa/google/authpost/?isAjax=true'; + const TFA_SCHEMA = 'schema?services=twoFactorAuthAdminTokenServiceV1'; + + /** + * If 2FA is enabled + * + * @var boolean|null + */ + private static $tfaEnabled = null; + + /** Rest request headers + * + * @var string[] + */ + private static $headers = [ + 'Accept: application/json', + 'Content-Type: application/json', + ]; + + /** + * 2FA provider web API authentication endpoints + * + * @var string[] + */ + private static $providerWebApiAuthEndpoints = [ + 'google' => self::WEB_API_AUTH_GOOGLE, + ]; + + /** + * 2FA provider admin form authentication endpoints + * + * @var string[] + */ + private static $providerAdminFormAuthEndpoints = [ + 'google' => self::ADMIN_FORM_AUTH_GOOGLE, + ]; + + /** + * Check if 2FA is enabled for Magento instance under test + * + * @return boolean + * @throws TestFrameworkException + */ + public static function isEnabled() + { + if (self::$tfaEnabled !== null) { + return self::$tfaEnabled; + } + + $schemaUrl = MftfGlobals::getWebApiBaseUrl() . self::TFA_SCHEMA; + $transport = new CurlTransport(); + try { + $transport->write($schemaUrl, [], CurlInterface::GET, self::$headers); + $response = $transport->read(); + $transport->close(); + $schema = json_decode($response, true); + if (isset($schema['definitions'], $schema['paths'])) { + return true; + } + } catch (TestFrameworkException $e) { + $transport->close(); + } + return false; + } + + /** + * Return provider's 2FA web API authentication endpoint + * + * @param string $name + * @return string|null + */ + public static function getProviderWebApiAuthEndpoint($name) + { + // Currently only support Google Authenticator + return self::$providerWebApiAuthEndpoints[$name] ?? null; + } + + /** + * Return 2FA provider's admin form authentication endpoint + * + * @param string $name + * @return string|null + */ + public static function getProviderAdminFormEndpoint($name) + { + // Currently only support Google Authenticator + return self::$providerAdminFormAuthEndpoints[$name] ?? null; + } +} diff --git a/src/Magento/FunctionalTestingFramework/DataTransport/Auth/Tfa/OTP.php b/src/Magento/FunctionalTestingFramework/DataTransport/Auth/Tfa/OTP.php new file mode 100644 index 000000000..b12733b33 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/DataTransport/Auth/Tfa/OTP.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\DataTransport\Auth\Tfa; + +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use OTPHP\TOTP; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; + +/** + * Class OTP + */ +class OTP +{ + const OTP_SHARED_SECRET_PATH = 'magento/tfa/OTP_SHARED_SECRET'; + + /** + * TOTP object + * + * @var TOTP[] + */ + private static $totps = []; + + /** + * Return OTP for custom secret stored in `magento/tfa/OTP_SHARED_SECRET` + * + * @param string|null $path + * @return string + * @throws TestFrameworkException + */ + public static function getOTP(?string $path = null) + { + if ($path === null) { + $path = self::OTP_SHARED_SECRET_PATH; + } + return self::create($path)->now(); + } + + /** + * Create TOTP object + * + * @param string $path + * @return TOTP + * @throws TestFrameworkException + */ + private static function create($path) + { + if (!isset(self::$totps[$path])) { + try { + // Get shared secret from Credential storage + $encryptedSecret = CredentialStore::getInstance()->getSecret($path); + $secret = CredentialStore::getInstance()->decryptSecretValue($encryptedSecret); + } catch (TestFrameworkException $e) { + throw new TestFrameworkException('Unable to get OTP' . PHP_EOL . $e->getMessage()); + } + + self::$totps[$path] = TOTP::create($secret); + self::$totps[$path]->setIssuer('MFTF'); + self::$totps[$path]->setLabel('MFTF Testing'); + } + return self::$totps[$path]; + } +} diff --git a/src/Magento/FunctionalTestingFramework/DataTransport/Auth/WebApiAuth.php b/src/Magento/FunctionalTestingFramework/DataTransport/Auth/WebApiAuth.php new file mode 100644 index 000000000..ac2d3ae99 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/DataTransport/Auth/WebApiAuth.php @@ -0,0 +1,154 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\DataTransport\Auth; + +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; +use Magento\FunctionalTestingFramework\Exceptions\FastFailException; +use Magento\FunctionalTestingFramework\Util\MftfGlobals; +use Magento\FunctionalTestingFramework\DataTransport\Protocol\CurlInterface; +use Magento\FunctionalTestingFramework\DataTransport\Protocol\CurlTransport; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\DataTransport\Auth\Tfa\OTP; + +/** + * Class WebApiAuth + */ +class WebApiAuth +{ + const PATH_ADMIN_AUTH = 'V1/integration/admin/token'; + + /** Rest request headers + * + * @var string[] + */ + private static $headers = [ + 'Accept: application/json', + 'Content-Type: application/json', + ]; + + /** + * Tokens for admin users + * + * @var string[] + */ + private static $adminAuthTokens = []; + + /** + * Timestamps of when admin user tokens were created. They need to be refreshed every ~4 hours + * + * @var int[] + */ + private static $adminAuthTokenTimestamps = []; + + /** + * Return the API token for an admin user + * Use MAGENTO_ADMIN_USERNAME and MAGENTO_ADMIN_PASSWORD when $username and/or $password is/are omitted + * + * @param string $username + * @param string $password + * @return string + * @throws FastFailException + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + public static function getAdminToken(?string $username = null, ?string $password = null) + { + $login = $username ?? getenv('MAGENTO_ADMIN_USERNAME'); + try { + $encryptedSecret = CredentialStore::getInstance()->getSecret('magento/MAGENTO_ADMIN_PASSWORD'); + $secret = CredentialStore::getInstance()->decryptSecretValue($encryptedSecret); + $password = $password ?? $secret; + } catch (TestFrameworkException $e) { + $message = "Password not found in credentials file"; + throw new FastFailException($message . $e->getMessage(), $e->getContext()); + } + if (!$login || !$password) { + $message = 'Cannot retrieve API token without credentials. Please fill out .env.'; + $context = [ + 'MAGENTO_BASE_URL' => getenv('MAGENTO_BASE_URL'), + 'MAGENTO_BACKEND_BASE_URL' => getenv('MAGENTO_BACKEND_BASE_URL'), + 'MAGENTO_ADMIN_USERNAME' => getenv('MAGENTO_ADMIN_USERNAME'), + 'MAGENTO_ADMIN_PASSWORD' => $secret, + ]; + throw new FastFailException($message, $context); + } + + if (self::hasExistingToken($login)) { + return self::$adminAuthTokens[$login]; + } + + try { + $authUrl = MftfGlobals::getWebApiBaseUrl() . self::PATH_ADMIN_AUTH; + + $data = [ + 'username' => $login, + 'password' => $password + ]; + + if (Tfa::isEnabled()) { + $authUrl = MftfGlobals::getWebApiBaseUrl() . Tfa::getProviderWebApiAuthEndpoint('google'); + $data['otp'] = OTP::getOTP(); + } + + $transport = new CurlTransport(); + $transport->write( + $authUrl, + json_encode($data, JSON_PRETTY_PRINT), + CurlInterface::POST, + self::$headers + ); + } catch (TestFrameworkException $e) { + $message = "Cannot retrieve API token with credentials. Please check configurations in .env.\n"; + throw new FastFailException($message . $e->getMessage(), $e->getContext()); + } + + try { + $response = $transport->read(); + $transport->close(); + $token = json_decode($response); + if ($token !== null) { + self::$adminAuthTokens[$login] = $token; + self::$adminAuthTokenTimestamps[$login] = time(); + return $token; + } + $errMessage = "Invalid response: {$response}"; + } catch (TestFrameworkException $e) { + $transport->close(); + $errMessage = $e->getMessage(); + } + + $message = 'Cannot retrieve API token with credentials.'; + try { + // No exception will ever throw from here + $message .= Tfa::isEnabled() ? ' and 2FA settings:' : ':' . PHP_EOL; + } catch (TestFrameworkException $e) { + } + $message .= $errMessage; + $context = ['url' => $authUrl]; + throw new FastFailException($message, $context); + } + + /** + * Is there an existing WebAPI admin token for this login? + * + * @param string $login + * @return boolean + */ + private static function hasExistingToken(string $login) + { + if (!isset(self::$adminAuthTokens[$login])) { + return false; + } + + $tokenLifetime = getenv('MAGENTO_ADMIN_WEBAPI_TOKEN_LIFETIME'); + + $isTokenExpired = $tokenLifetime && time() - self::$adminAuthTokenTimestamps[$login] > $tokenLifetime; + + return !$isTokenExpired; + } +} diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/FrontendExecutor.php b/src/Magento/FunctionalTestingFramework/DataTransport/FrontendFormExecutor.php similarity index 88% rename from src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/FrontendExecutor.php rename to src/Magento/FunctionalTestingFramework/DataTransport/FrontendFormExecutor.php index 799cd6d5c..c167c79b6 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/FrontendExecutor.php +++ b/src/Magento/FunctionalTestingFramework/DataTransport/FrontendFormExecutor.php @@ -4,16 +4,17 @@ * See COPYING.txt for license details. */ -namespace Magento\FunctionalTestingFramework\DataGenerator\Persist\Curl; +namespace Magento\FunctionalTestingFramework\DataTransport; -use Magento\FunctionalTestingFramework\Util\Protocol\CurlInterface; -use Magento\FunctionalTestingFramework\Util\Protocol\CurlTransport; +use Magento\FunctionalTestingFramework\Util\MftfGlobals; +use Magento\FunctionalTestingFramework\DataTransport\Protocol\CurlInterface; +use Magento\FunctionalTestingFramework\DataTransport\Protocol\CurlTransport; use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; /** * Curl executor for requests to Frontend. */ -class FrontendExecutor extends AbstractExecutor implements CurlInterface +class FrontendFormExecutor implements CurlInterface { /** * Curl transport protocol. @@ -58,7 +59,7 @@ class FrontendExecutor extends AbstractExecutor implements CurlInterface private $customerPassword; /** - * FrontendExecutor constructor. + * FrontendFormExecutor constructor. * * @param string $customerEmail * @param string $customerPassWord @@ -81,11 +82,11 @@ public function __construct($customerEmail, $customerPassWord) */ private function authorize() { - $url = $this->getBaseUrl() . 'customer/account/login/'; + $url = MftfGlobals::getBaseUrl() . 'customer/account/login/'; $this->transport->write($url, [], CurlInterface::GET); $this->read(); - $url = $this->getBaseUrl() . 'customer/account/loginPost/'; + $url = MftfGlobals::getBaseUrl() . 'customer/account/loginPost/'; $data = [ 'login[username]' => $this->customerEmail, 'login[password]' => $this->customerPassword, @@ -143,7 +144,7 @@ public function write($url, $data = [], $method = CurlInterface::POST, $headers if (isset($data['customer_password'])) { unset($data['customer_password']); } - $apiUrl = $this->getBaseUrl() . $url; + $apiUrl = MftfGlobals::getBaseUrl() . $url; if ($this->formKey) { $data['form_key'] = $this->formKey; } else { @@ -164,7 +165,7 @@ public function write($url, $data = [], $method = CurlInterface::POST, $headers * @return string|array * @throws TestFrameworkException */ - public function read($successRegex = null, $returnRegex = null, $returnIndex = null) + public function read(?string $successRegex = null, ?string $returnRegex = null, ?string $returnIndex = null) { $this->response = $this->transport->read(); $this->setCookies(); diff --git a/src/Magento/FunctionalTestingFramework/Util/Protocol/CurlInterface.php b/src/Magento/FunctionalTestingFramework/DataTransport/Protocol/CurlInterface.php similarity index 86% rename from src/Magento/FunctionalTestingFramework/Util/Protocol/CurlInterface.php rename to src/Magento/FunctionalTestingFramework/DataTransport/Protocol/CurlInterface.php index a2d6bc344..31d931819 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Protocol/CurlInterface.php +++ b/src/Magento/FunctionalTestingFramework/DataTransport/Protocol/CurlInterface.php @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ -namespace Magento\FunctionalTestingFramework\Util\Protocol; +namespace Magento\FunctionalTestingFramework\DataTransport\Protocol; /** * Curl protocol interface. @@ -47,7 +47,7 @@ public function write($url, $body = [], $method = CurlInterface::POST, $headers * @param string|null $returnIndex * @return string|array */ - public function read($successRegex = null, $returnRegex = null, $returnIndex = null); + public function read(?string $successRegex = null, ?string $returnRegex = null, ?string $returnIndex = null); /** * Close the connection to the server. diff --git a/src/Magento/FunctionalTestingFramework/Util/Protocol/CurlTransport.php b/src/Magento/FunctionalTestingFramework/DataTransport/Protocol/CurlTransport.php similarity index 89% rename from src/Magento/FunctionalTestingFramework/Util/Protocol/CurlTransport.php rename to src/Magento/FunctionalTestingFramework/DataTransport/Protocol/CurlTransport.php index 16c716e22..fc35da08e 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Protocol/CurlTransport.php +++ b/src/Magento/FunctionalTestingFramework/DataTransport/Protocol/CurlTransport.php @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ -namespace Magento\FunctionalTestingFramework\Util\Protocol; +namespace Magento\FunctionalTestingFramework\DataTransport\Protocol; use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; @@ -132,18 +132,18 @@ public function write($url, $body = [], $method = CurlInterface::POST, $headers CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_COOKIEFILE => '', - CURLOPT_HTTPHEADER => $headers, + CURLOPT_HTTPHEADER => is_object($headers) ? (array) $headers : $headers, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, ]; switch ($method) { case CurlInterface::POST: $options[CURLOPT_POST] = true; - $options[CURLOPT_POSTFIELDS] = $body; + $options[CURLOPT_POSTFIELDS] = is_object($body) ? (array) $body : $body; break; case CurlInterface::PUT: $options[CURLOPT_CUSTOMREQUEST] = self::PUT; - $options[CURLOPT_POSTFIELDS] = $body; + $options[CURLOPT_POSTFIELDS] = is_object($body) ? (array) $body : $body; break; case CurlInterface::DELETE: $options[CURLOPT_CUSTOMREQUEST] = self::DELETE; @@ -167,7 +167,7 @@ public function write($url, $body = [], $method = CurlInterface::POST, $headers * @return string * @throws TestFrameworkException */ - public function read($successRegex = null, $returnRegex = null, $returnIndex = null) + public function read(?string $successRegex = null, ?string $returnRegex = null, ?string $returnIndex = null) { $response = curl_exec($this->getResource()); @@ -189,7 +189,10 @@ public function read($successRegex = null, $returnRegex = null, $returnIndex = n */ public function close() { - curl_close($this->getResource()); + if (version_compare(PHP_VERSION, '8.0') < 0) { + // this function no longer has an effect in PHP 8.0, but it's required in earlier versions + curl_close($this->getResource()); + } $this->resource = null; } @@ -271,7 +274,11 @@ public function multiRequest(array $urls, array $options = []) $result[$key] = curl_multi_getcontent($handle); curl_multi_remove_handle($multiHandle, $handle); } - curl_multi_close($multiHandle); + if (version_compare(PHP_VERSION, '8.0') < 0) { + // this function no longer has an effect in PHP 8.0, but it's required in earlier versions + curl_multi_close($multiHandle); + } + return $result; } diff --git a/src/Magento/FunctionalTestingFramework/DataTransport/WebApiExecutor.php b/src/Magento/FunctionalTestingFramework/DataTransport/WebApiExecutor.php new file mode 100644 index 000000000..52b7cdb65 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/DataTransport/WebApiExecutor.php @@ -0,0 +1,144 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\DataTransport; + +use Magento\FunctionalTestingFramework\Exceptions\FastFailException; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Util\MftfGlobals; +use Magento\FunctionalTestingFramework\DataTransport\Protocol\CurlInterface; +use Magento\FunctionalTestingFramework\DataTransport\Protocol\CurlTransport; +use Magento\FunctionalTestingFramework\DataTransport\Auth\WebApiAuth; + +/** + * Curl executor for Magento Web Api requests. + */ +class WebApiExecutor implements CurlInterface +{ + /** + * Curl transport protocol + * + * @var CurlTransport + */ + private $transport; + + /** + * Rest request headers + * + * @var string[] + */ + private $headers = [ + 'Accept: application/json', + 'Content-Type: application/json', + ]; + + /** + * Store code in API request + * + * @var string + */ + private $storeCode; + + /** + * WebApiExecutor Constructor + * + * @param string $storeCode + * @throws FastFailException + */ + public function __construct(?string $storeCode = null) + { + $this->storeCode = $storeCode; + $this->transport = new CurlTransport(); + $this->authorize(); + } + + /** + * Acquire and store the authorization token needed for REST requests + * + * @return void + * @throws FastFailException + */ + protected function authorize() + { + $this->headers = array_merge( + ['Authorization: Bearer ' . WebApiAuth::getAdminToken()], + $this->headers + ); + } + + /** + * Send request to the remote server. + * + * @param string $url + * @param array $data + * @param string $method + * @param array $headers + * @return void + * @throws TestFrameworkException + */ + public function write($url, $data = [], $method = CurlInterface::POST, $headers = []) + { + $this->transport->write( + $this->getFormattedUrl($url), + json_encode($data, JSON_PRETTY_PRINT), + $method, + array_unique(array_merge($headers, $this->headers)) + ); + } + + /** + * Read response from server. + * + * @param string $successRegex + * @param string $returnRegex + * @param string|null $returnIndex + * @return string + * @throws TestFrameworkException + */ + public function read(?string $successRegex = null, ?string $returnRegex = null, ?string $returnIndex = null) + { + return $this->transport->read(); + } + + /** + * Add additional option to cURL. + * + * @param integer $option CURLOPT_* constants. + * @param integer|string|boolean|array $value + * @return void + */ + public function addOption($option, $value) + { + $this->transport->addOption($option, $value); + } + + /** + * Close the connection to the server. + * + * @return void + */ + public function close() + { + $this->transport->close(); + } + + /** + * Builds and returns URL for request, appending storeCode if needed + * + * @param string $resource + * @return string + * @throws TestFrameworkException + */ + protected function getFormattedUrl($resource) + { + $urlResult = MftfGlobals::getWebApiBaseUrl(); + if ($this->storeCode !== null) { + $urlResult .= $this->storeCode . '/'; + } + $urlResult .= trim($resource, '/'); + return $urlResult; + } +} diff --git a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/WebapiNoAuthExecutor.php b/src/Magento/FunctionalTestingFramework/DataTransport/WebApiNoAuthExecutor.php similarity index 74% rename from src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/WebapiNoAuthExecutor.php rename to src/Magento/FunctionalTestingFramework/DataTransport/WebApiNoAuthExecutor.php index c54bc60b9..066c676f2 100644 --- a/src/Magento/FunctionalTestingFramework/DataGenerator/Persist/Curl/WebapiNoAuthExecutor.php +++ b/src/Magento/FunctionalTestingFramework/DataTransport/WebApiNoAuthExecutor.php @@ -4,12 +4,12 @@ * See COPYING.txt for license details. */ -namespace Magento\FunctionalTestingFramework\DataGenerator\Persist\Curl; +namespace Magento\FunctionalTestingFramework\DataTransport; /** * Curl executor for Magento Web Api requests that do not require authorization. */ -class WebapiNoAuthExecutor extends WebapiExecutor +class WebApiNoAuthExecutor extends WebApiExecutor { /** * No authorization is needed and just return. diff --git a/src/Magento/FunctionalTestingFramework/Exceptions/FastFailException.php b/src/Magento/FunctionalTestingFramework/Exceptions/FastFailException.php new file mode 100644 index 000000000..70d84dff2 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Exceptions/FastFailException.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Exceptions; + +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; + +/** + * Class FastFailException + * + * This exception type should not be caught and should allow fast fail of current execution + */ +class FastFailException extends \Exception +{ + /** + * Exception context + * + * @var array + */ + protected $context; + + /** + * FastFailException constructor + * + * @param string $message + * @param array $context + * + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + */ + public function __construct($message, $context = []) + { + list($childClass, $callingClass) = debug_backtrace(false, 2); + LoggingUtil::getInstance()->getLogger($callingClass['class'])->error( + $message, + $context + ); + + $this->context = $context; + parent::__construct($message); + } + + /** + * Return exception context + * + * @return array + */ + public function getContext() + { + return $this->context; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Extension/TestContextExtension.php b/src/Magento/FunctionalTestingFramework/Extension/TestContextExtension.php index c50156f08..5f850a403 100644 --- a/src/Magento/FunctionalTestingFramework/Extension/TestContextExtension.php +++ b/src/Magento/FunctionalTestingFramework/Extension/TestContextExtension.php @@ -7,8 +7,21 @@ namespace Magento\FunctionalTestingFramework\Extension; use Codeception\Events; +use Codeception\Step; +use Codeception\Test\Test; use Magento\FunctionalTestingFramework\Allure\AllureHelper; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\PersistedObjectHandler; +use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; +use Qameta\Allure\Allure; +use Qameta\Allure\AllureLifecycleInterface; +use Qameta\Allure\Model\StepResult; +use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; +use Magento\FunctionalTestingFramework\Test\Objects\ActionGroupObject; +use Magento\FunctionalTestingFramework\Util\TestGenerator; +use Qameta\Allure\Model\TestResult; +use Qameta\Allure\Model\Status; +use Magento\FunctionalTestingFramework\Codeception\Subscriber\Console; /** * Class TestContextExtension @@ -17,6 +30,28 @@ */ class TestContextExtension extends BaseExtension { + private const STEP_PASSED = "passed"; + + /** + * Test files cache. + * + * @var array + */ + private $testFiles = []; + + /** + * Action group step key. + * + * @var null|string + */ + private $actionGroupStepKey = null; + + /** + * Boolean value to indicate if steps are invisible steps + * + * @var boolean + */ + private $atInvisibleSteps = false; const TEST_PHASE_AFTER = "_after"; const TEST_PHASE_BEFORE = "_before"; @@ -32,13 +67,19 @@ class TestContextExtension extends BaseExtension */ public static $events; + /** + * The name of the currently running test + * @var string + */ + public $currentTest; + /** * Initialize local vars * * @return void * @throws \Exception */ - public function _initialize() + public function _initialize(): void { $events = [ Events::TEST_START => 'testStart', @@ -55,8 +96,23 @@ public function _initialize() * @throws \Exception * @return void */ - public function testStart() + public function testStart(\Codeception\Event\TestEvent $e) { + if (getenv('ENABLE_CODE_COVERAGE') === 'true') { + // Curl against test.php and pass in the test name. Used when gathering code coverage. + $this->currentTest = $e->getTest()->getMetadata()->getName(); + $cURLConnection = curl_init(); + curl_setopt_array($cURLConnection, [ + CURLOPT_RETURNTRANSFER => 1, + CURLOPT_URL => getenv('MAGENTO_BASE_URL') . "/test.php?test=" . $this->currentTest, + ]); + curl_exec($cURLConnection); + if (version_compare(PHP_VERSION, '8.0') < 0) { + // this function no longer has an effect in PHP 8.0, but it's required in earlier versions + curl_close($cURLConnection); + } + } + PersistedObjectHandler::getInstance()->clearHookObjects(); PersistedObjectHandler::getInstance()->clearTestObjects(); } @@ -74,7 +130,7 @@ public function testEnd(\Codeception\Event\TestEvent $e) //Access private TestResultObject to find stack and if there are any errors/failures $testResultObject = call_user_func(\Closure::bind( function () use ($cest) { - return $cest->getTestResultObject(); + return $cest->getResultAggregator(); }, $cest )); @@ -82,8 +138,8 @@ function () use ($cest) { // check for errors in all test hooks and attach in allure if (!empty($testResultObject->errors())) { foreach ($testResultObject->errors() as $error) { - if ($error->failedTest()->getTestMethod() == $cest->getTestMethod()) { - $this->attachExceptionToAllure($error->thrownException(), $cest->getTestMethod()); + if ($error->getTest()->getTestMethod() === $cest->getTestMethod()) { + $this->attachExceptionToAllure($error->getFail(), $cest->getTestMethod()); } } } @@ -91,13 +147,148 @@ function () use ($cest) { // check for failures in all test hooks and attach in allure if (!empty($testResultObject->failures())) { foreach ($testResultObject->failures() as $failure) { - if ($failure->failedTest()->getTestMethod() == $cest->getTestMethod()) { - $this->attachExceptionToAllure($failure->thrownException(), $cest->getTestMethod()); + if ($failure->getTest()->getTestMethod() === $cest->getTestMethod()) { + $this->attachExceptionToAllure($failure->getFail(), $cest->getTestMethod()); } } } // Reset Session and Cookies after all Test Runs, workaround due to functional.suite.yml restart: true $this->getDriver()->_runAfter($e->getTest()); + + $lifecycle = Allure::getLifecycle(); + $lifecycle->updateTest( + function (TestResult $testResult) { + $this->getFormattedSteps($testResult); + } + ); + + $this->addTestsInSuites($lifecycle, $cest); + } + + /** + * Function to add test under the suites. + * + * @param object $lifecycle + * @param object $cest + * + * @return void + */ + private function addTestsInSuites($lifecycle, $cest): void + { + $groupName = null; + if ($this->options['groups'] !== null) { + $group = $this->options['groups'][0]; + $groupName = $this->sanitizeGroupName($group); + } + $lifecycle->updateTest( + function (TestResult $testResult) use ($groupName, $cest) { + $labels = $testResult->getLabels(); + foreach ($labels as $label) { + if ($groupName !== null && $label->getName() === "parentSuite") { + $label->setValue(sprintf('%s\%s', $label->getValue(), $groupName)); + } + if ($label->getName() === "package") { + $className = $cest->getReportFields()['class']; + $className = preg_replace('{_[0-9]*_G}', '', $className); + $label->setValue($className); + } + } + } + ); + } + + /** + * Function which santizes any group names changed by the framework for execution in order to consolidate reporting. + * + * @param string $group + * @return string + */ + private function sanitizeGroupName($group): string + { + $suiteNames = array_keys(SuiteObjectHandler::getInstance()->getAllObjects()); + $exactMatch = in_array($group, $suiteNames); + + // if this is an existing suite name we dont' need to worry about changing it + if ($exactMatch || strpos($group, "_") === false) { + return $group; + } + + // if we can't find this group in the generated suites we have to assume that the group was split for generation + $groupNameSplit = explode("_", $group); + array_pop($groupNameSplit); + array_pop($groupNameSplit); + $originalName = implode("_", $groupNameSplit); + + // confirm our original name is one of the existing suite names otherwise just return the original group name + $originalName = in_array($originalName, $suiteNames) ? $originalName : $group; + + return $originalName; + } + + /** + * @param TestResult $testResult + * @return void + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * Revisited to reduce cyclomatic complexity, left unrefactored for readability + */ + private function getFormattedSteps(TestResult $testResult): void + { + $steps = $testResult->getSteps(); + $formattedSteps = []; + $actionGroupKey = null; + foreach ($steps as $key => $step) { + if (str_contains($step->getName(), 'start before hook') + || str_contains($step->getName(), 'end before hook') + || str_contains($step->getName(), 'start after hook') + || str_contains($step->getName(), 'end after hook') + ) { + $step->setName(strtoupper($step->getName())); + } + // Remove all parameters from step because parameters already added in formatted step + call_user_func(\Closure::bind( + function () use ($step) { + $step->parameters = []; + }, + null, + $step + )); + if (strpos($step->getName(), ActionGroupObject::ACTION_GROUP_CONTEXT_START) !== false) { + $step->setName(str_replace(ActionGroupObject::ACTION_GROUP_CONTEXT_START, '', $step->getName())); + $actionGroupKey = $key; + $formattedSteps[$actionGroupKey] = $step; + continue; + } + if (stripos($step->getName(), ActionGroupObject::ACTION_GROUP_CONTEXT_END) !== false) { + $actionGroupKey = null; + continue; + } + if ($actionGroupKey !== null) { + if ($step->getName() !== null) { + $formattedSteps[$actionGroupKey]->addSteps($step); + if ($step->getStatus()->jsonSerialize() !== self::STEP_PASSED) { + $formattedSteps[$actionGroupKey]->setStatus($step->getStatus()); + $actionGroupKey = null; + } + } + } else { + if ($step->getName() !== null) { + $formattedSteps[$key] = $step; + } + } + } + /** @var StepResult[] $formattedSteps*/ + $formattedSteps = array_values($formattedSteps); + + // No public function for setting the testResult steps + call_user_func(\Closure::bind( + function () use ($testResult, $formattedSteps) { + $testResult->steps = $formattedSteps; + }, + null, + $testResult + )); } /** @@ -110,7 +301,7 @@ public function extractContext($trace, $class) { foreach ($trace as $entry) { $traceClass = $entry["class"] ?? null; - if (strpos($traceClass, $class) != 0) { + if (strpos($traceClass, $class) !== 0) { return $entry["function"]; } } @@ -141,16 +332,12 @@ public function attachExceptionToAllure($exception, $testMethod) AllureHelper::addAttachmentToCurrentStep($exception, $context . 'Exception'); - //pop suppressed exceptions and attach to allure - $change = function () { - if ($this instanceof \PHPUnit\Framework\ExceptionWrapper) { - return $this->previous; - } else { - return $this->getPrevious(); - } - }; - - $previousException = $change->call($exception); + $previousException = null; + if ($exception instanceof \PHPUnit\Framework\ExceptionWrapper) { + $previousException = $exception->getPreviousWrapped(); + } elseif ($exception instanceof \Throwable) { + $previousException = $exception->getPrevious(); + } if ($previousException !== null) { $this->attachExceptionToAllure($previousException, $testMethod); @@ -167,11 +354,87 @@ public function attachExceptionToAllure($exception, $testMethod) */ public function beforeStep(\Codeception\Event\StepEvent $e) { + if ($this->pageChanged($e->getStep())) { $this->getDriver()->cleanJsError(); } } + /** + * @param \Codeception\Event\StepEvent $e + * @return string|void + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * Revisited to reduce cyclomatic complexity, left unrefactored for readability + */ + public function stepName(\Codeception\Event\StepEvent $e) + { + $stepAction = $e->getStep()->getAction(); + if (in_array($stepAction, ActionObject::INVISIBLE_STEP_ACTIONS)) { + $this->atInvisibleSteps = true; + return; + } + // Set back atInvisibleSteps flag + if ($this->atInvisibleSteps && !in_array($stepAction, ActionObject::INVISIBLE_STEP_ACTIONS)) { + $this->atInvisibleSteps = false; + } + + //Hard set to 200; we don't expose this config in MFTF + $argumentsLength = 200; + $stepKey = null; + + if (!($e->getStep() instanceof Comment)) { + $stepKey = $this->retrieveStepKeyForAllure($e->getStep(), $e->getTest()->getMetadata()->getFilename()); + $isActionGroup = ( + strpos( + $e->getStep()->__toString(), + ActionGroupObject::ACTION_GROUP_CONTEXT_START + ) !== false + ); + if ($isActionGroup) { + preg_match(TestGenerator::ACTION_GROUP_STEP_KEY_REGEX, $e->getStep()->__toString(), $matches); + if (!empty($matches['actionGroupStepKey'])) { + $this->actionGroupStepKey = ucfirst($matches['actionGroupStepKey']); + } + } + } + // DO NOT alter action if actionGroup is starting, need the exact actionGroup name for good logging + if (strpos($stepAction, ActionGroupObject::ACTION_GROUP_CONTEXT_START) === false + && !($e->getStep() instanceof Comment) + ) { + $stepAction = $e->getStep()->getHumanizedActionWithoutArguments(); + } + $stepArgs = $e->getStep()->getArgumentsAsString($argumentsLength); + if (!trim($stepAction)) { + $stepAction = $e->getStep()->getMetaStep()->getHumanizedActionWithoutArguments(); + $stepArgs = $e->getStep()->getMetaStep()->getArgumentsAsString($argumentsLength); + } + $stepName = ''; + + if (isset($stepName)) { + $stepName .= '[' . $stepKey . '] '; + if (empty($stepKey)) { + $stepName = ""; + } + } + $stepName .= $stepAction . ' ' . $stepArgs; + // Strip control characters so that report generation does not fail + $stepName = preg_replace('/[[:cntrl:]]/', '', $stepName); + if (stripos($stepName, "\mftf\helper")) { + preg_match("/\[(.*?)\]/", $stepName, $matches); + $stepKeyData = preg_split('/\s+/', ucwords($matches[1])); + if (count($stepKeyData) > 0) { + $this->actionGroupStepKey = (isset($this->actionGroupStepKey)) + ?$this->actionGroupStepKey + : ""; + $stepKeyHelper = str_replace($this->actionGroupStepKey, '', lcfirst(implode("", $stepKeyData))); + $stepName= '['.$stepKeyHelper.'] '.preg_replace('#\[.*\]#', '', $stepName); + } + } + return ucfirst($stepName); + } + /** * Codeception event listener function, triggered after step. * Calls ErrorLogger to log JS errors encountered. @@ -181,9 +444,20 @@ public function beforeStep(\Codeception\Event\StepEvent $e) */ public function afterStep(\Codeception\Event\StepEvent $e) { - $browserLog = $this->getDriver()->webDriver->manage()->getLog("browser"); + $lifecycle = Allure::getLifecycle(); + $stepName = $this->stepName($e); + $lifecycle->updateStep( + function (StepResult $step) use ($stepName) { + $step->setName($stepName); + } + ); + $browserLog = []; + try { + $browserLog = $this->getDriver()->webDriver->manage()->getLog("browser"); + } catch (\Exception $exception) { + } if (getenv('ENABLE_BROWSER_LOG') === 'true') { - foreach (explode(',', getenv('BROWSER_LOG_BLACKLIST')) as $source) { + foreach (explode(',', getenv('BROWSER_LOG_BLOCKLIST')) as $source) { $browserLog = BrowserLogUtil::filterLogsOfType($browserLog, $source); } if (!empty($browserLog)) { @@ -211,13 +485,13 @@ public function saveFailed(\Codeception\Event\PrintResultEvent $e) } foreach ($result->failures() as $fail) { - $output[] = $this->localizePath(\Codeception\Test\Descriptor::getTestFullName($fail->failedTest())); + $output[] = $this->localizePath(\Codeception\Test\Descriptor::getTestFullName($fail->getTest())); } foreach ($result->errors() as $fail) { - $output[] = $this->localizePath(\Codeception\Test\Descriptor::getTestFullName($fail->failedTest())); + $output[] = $this->localizePath(\Codeception\Test\Descriptor::getTestFullName($fail->getTest())); } - foreach ($result->notImplemented() as $fail) { - $output[] = $this->localizePath(\Codeception\Test\Descriptor::getTestFullName($fail->failedTest())); + foreach ($result->incomplete() as $fail) { + $output[] = $this->localizePath(\Codeception\Test\Descriptor::getTestFullName($fail->getTest())); } if (empty($output)) { @@ -235,9 +509,43 @@ public function saveFailed(\Codeception\Event\PrintResultEvent $e) protected function localizePath($path) { $root = realpath($this->getRootDir()) . DIRECTORY_SEPARATOR; - if (substr($path, 0, strlen($root)) == $root) { + if (substr($path, 0, strlen($root)) === $root) { return substr($path, strlen($root)); } return $path; } + + /** + * Reading stepKey from file. + * + * @param Step $step + * @param string $filePath + * @return string|null + */ + private function retrieveStepKeyForAllure(Step $step, string $filePath) + { + $stepKey = null; + $stepLine = $step->getLineNumber(); + $stepLine = $stepLine - 1; + + //If the step's filepath is different from the test, it's a comment action. + if ($this->getRootDir() . $step->getFilePath() != $filePath) { + return ""; + } + + if (!array_key_exists($filePath, $this->testFiles)) { + $this->testFiles[$filePath] = explode(PHP_EOL, file_get_contents($filePath)); + } + + preg_match(TestGenerator::ACTION_STEP_KEY_REGEX, $this->testFiles[$filePath][$stepLine], $matches); + if (!empty($matches['stepKey'])) { + $stepKey = $matches['stepKey']; + } + if ($this->actionGroupStepKey !== null) { + $stepKey = str_replace($this->actionGroupStepKey, '', $stepKey); + } + + $stepKey = $stepKey === '[]' ? null : $stepKey; + return $stepKey; + } } diff --git a/src/Magento/FunctionalTestingFramework/Filter/Test/ExcludeGroup.php b/src/Magento/FunctionalTestingFramework/Filter/Test/ExcludeGroup.php new file mode 100644 index 000000000..38a3bf8f2 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Filter/Test/ExcludeGroup.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\FunctionalTestingFramework\Filter\Test; + +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Filter\FilterInterface; +use Magento\FunctionalTestingFramework\Test\Objects\TestObject; + +/** + * Class ExcludeGroup + */ +class ExcludeGroup implements FilterInterface +{ + const ANNOTATION_TAG = 'group'; + + /** + * @var array + */ + private $filterValues = []; + + /** + * Group constructor. + * + * @param array $filterValues + * @throws TestFrameworkException + */ + public function __construct(array $filterValues = []) + { + $this->filterValues = $filterValues; + } + + /** + * Filter tests by group. + * + * @param TestObject[] $tests + * @return void + */ + public function filter(array &$tests) + { + if ($this->filterValues === []) { + return; + } + /** @var TestObject $test */ + foreach ($tests as $testName => $test) { + $groups = $test->getAnnotationByName(self::ANNOTATION_TAG); + $testExcludeGroup = !empty(array_intersect($groups, $this->filterValues)); + if ($testExcludeGroup) { + unset($tests[$testName]); + } + } + } +} diff --git a/src/Magento/FunctionalTestingFramework/Filter/Test/IncludeGroup.php b/src/Magento/FunctionalTestingFramework/Filter/Test/IncludeGroup.php new file mode 100644 index 000000000..b84a48e55 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Filter/Test/IncludeGroup.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\FunctionalTestingFramework\Filter\Test; + +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Filter\FilterInterface; +use Magento\FunctionalTestingFramework\Test\Objects\TestObject; + +/** + * Class IncludeGroup + */ +class IncludeGroup implements FilterInterface +{ + const ANNOTATION_TAG = 'group'; + + /** + * @var array + */ + private $filterValues = []; + + /** + * Group constructor. + * + * @param array $filterValues + * @throws TestFrameworkException + */ + public function __construct(array $filterValues = []) + { + $this->filterValues = $filterValues; + } + + /** + * Filter tests by group. + * + * @param TestObject[] $tests + * @return void + */ + public function filter(array &$tests) + { + if ($this->filterValues === []) { + return; + } + /** @var TestObject $test */ + foreach ($tests as $testName => $test) { + $groups = $test->getAnnotationByName(self::ANNOTATION_TAG); + $testIncludeGroup = empty(array_intersect($groups, $this->filterValues)); + if ($testIncludeGroup) { + unset($tests[$testName]); + } + } + } +} diff --git a/src/Magento/FunctionalTestingFramework/Helper/Acceptance.php b/src/Magento/FunctionalTestingFramework/Helper/Acceptance.php deleted file mode 100644 index fafe167af..000000000 --- a/src/Magento/FunctionalTestingFramework/Helper/Acceptance.php +++ /dev/null @@ -1,42 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\FunctionalTestingFramework\Helper; - -use Magento\FunctionalTestingFramework\Module\MagentoWebDriver; -use Codeception\Module; - -/** - * Class Acceptance - * - * Define global actions - * All public methods declared in helper class will be available in $I - */ -class Acceptance extends Module -{ - /** - * Reconfig WebDriver. - * - * @param string $config - * @param string $value - * @return void - */ - public function changeConfiguration($config, $value) - { - $this->getModule(MagentoWebDriver::class)->_reconfigure([$config => $value]); - } - - /** - * Get WebDriver configuration. - * - * @param string $config - * @return string - */ - public function getConfiguration($config) - { - return $this->getModule(MagentoWebDriver::class)->_getConfig($config); - } -} diff --git a/src/Magento/FunctionalTestingFramework/Helper/HelperContainer.php b/src/Magento/FunctionalTestingFramework/Helper/HelperContainer.php index 83a71fbc0..f3b5852b8 100644 --- a/src/Magento/FunctionalTestingFramework/Helper/HelperContainer.php +++ b/src/Magento/FunctionalTestingFramework/Helper/HelperContainer.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types = 1); namespace Magento\FunctionalTestingFramework\Helper; @@ -11,7 +12,7 @@ /** * Class HelperContainer */ -class HelperContainer +class HelperContainer extends \Codeception\Module { /** * @var Helper[] @@ -19,12 +20,22 @@ class HelperContainer private $helpers = []; /** - * HelperContainer constructor. - * @param array $helpers + * Create custom helper class. + * + * @param string $helperClass + * @return Helper + * @throws \Exception */ - public function __construct(array $helpers = []) + public function create(string $helperClass): Helper { - $this->helpers = $helpers; + if (get_parent_class($helperClass) !== Helper::class) { + throw new \Exception("Helper class must extend " . Helper::class); + } + if (!isset($this->helpers[$helperClass])) { + $this->helpers[$helperClass] = $this->moduleContainer->create($helperClass); + } + + return $this->helpers[$helperClass]; } /** @@ -34,7 +45,7 @@ public function __construct(array $helpers = []) * @return Helper * @throws TestFrameworkException */ - public function get(string $className) + public function get(string $className): Helper { if ($this->has($className)) { return $this->helpers[$className]; @@ -48,7 +59,7 @@ public function get(string $className) * @param string $className * @return boolean */ - public function has(string $className) + public function has(string $className): bool { return array_key_exists($className, $this->helpers); } diff --git a/src/Magento/FunctionalTestingFramework/Helper/MagentoFakerData.php b/src/Magento/FunctionalTestingFramework/Helper/MagentoFakerData.php deleted file mode 100644 index a5b19b4c6..000000000 --- a/src/Magento/FunctionalTestingFramework/Helper/MagentoFakerData.php +++ /dev/null @@ -1,54 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\FunctionalTestingFramework\Helper; - -/** - * Class MagentoFakerData - */ -class MagentoFakerData extends \Codeception\Module -{ - /** - * Get Customer data. - * - * @param array $additional - * @return array - */ - public function getCustomerData(array $additional = []) - { - return [$additional]; - } - - /** - * Get category data. - * - * @return array - */ - public function getCategoryData() - { - return []; - } - - /** - * Get simple product data. - * - * @return array - */ - public function getProductData() - { - return []; - } - - /** - * Get Content Page Data. - * - * @return array - */ - public function getContentPage() - { - return []; - } -} diff --git a/src/Magento/FunctionalTestingFramework/Helper/views/TestInjectMethod.mustache b/src/Magento/FunctionalTestingFramework/Helper/views/TestInjectMethod.mustache index d921120cf..36f2f8bf6 100644 --- a/src/Magento/FunctionalTestingFramework/Helper/views/TestInjectMethod.mustache +++ b/src/Magento/FunctionalTestingFramework/Helper/views/TestInjectMethod.mustache @@ -6,14 +6,10 @@ /** * Special method which automatically creates the respective objects. */ - public function _inject( - {{argumentsWithTypes}} - ) { - $this->helperContainer = new \Magento\FunctionalTestingFramework\Helper\HelperContainer( - [ - {{#arguments}} - '{{type}}' => {{var}}, - {{/arguments}} - ] - ); + public function _inject(\Magento\FunctionalTestingFramework\Helper\HelperContainer $helperContainer) + { + $this->helperContainer = $helperContainer; + {{#arguments}} + $this->helperContainer->create("{{type}}"); + {{/arguments}} } diff --git a/src/Magento/FunctionalTestingFramework/Module/MagentoActionProxies.php b/src/Magento/FunctionalTestingFramework/Module/MagentoActionProxies.php index 5a3b2360b..55b19a7c1 100644 --- a/src/Magento/FunctionalTestingFramework/Module/MagentoActionProxies.php +++ b/src/Magento/FunctionalTestingFramework/Module/MagentoActionProxies.php @@ -20,5 +20,122 @@ */ class MagentoActionProxies extends CodeceptionModule { - // TODO: placeholder for proxy functions currently in MagentoWebDriver (MQE-1904) + /** + * Create an entity + * + * @param string $key StepKey of the createData action. + * @param string $scope + * @param string $entity Name of xml entity to create. + * @param array $dependentObjectKeys StepKeys of other createData actions that are required. + * @param array $overrideFields Array of FieldName => Value of override fields. + * @param string $storeCode + * @return void + */ + public function createEntity( + $key, + $scope, + $entity, + $dependentObjectKeys = [], + $overrideFields = [], + $storeCode = '' + ) { + PersistedObjectHandler::getInstance()->createEntity( + $key, + $scope, + $entity, + $dependentObjectKeys, + $overrideFields, + $storeCode + ); + } + + /** + * Retrieves and updates a previously created entity + * + * @param string $key StepKey of the createData action. + * @param string $scope + * @param string $updateEntity Name of the static XML data to update the entity with. + * @param array $dependentObjectKeys StepKeys of other createData actions that are required. + * @return void + */ + public function updateEntity($key, $scope, $updateEntity, $dependentObjectKeys = []) + { + PersistedObjectHandler::getInstance()->updateEntity( + $key, + $scope, + $updateEntity, + $dependentObjectKeys + ); + } + + /** + * Performs GET on given entity and stores entity for use + * + * @param string $key StepKey of getData action. + * @param string $scope + * @param string $entity Name of XML static data to use. + * @param array $dependentObjectKeys StepKeys of other createData actions that are required. + * @param string $storeCode + * @param integer $index + * @return void + */ + public function getEntity($key, $scope, $entity, $dependentObjectKeys = [], $storeCode = '', $index = null) + { + PersistedObjectHandler::getInstance()->getEntity( + $key, + $scope, + $entity, + $dependentObjectKeys, + $storeCode, + $index + ); + } + + /** + * Retrieves and deletes a previously created entity + * + * @param string $key StepKey of the createData action. + * @param string $scope + * @return void + */ + public function deleteEntity($key, $scope) + { + PersistedObjectHandler::getInstance()->deleteEntity($key, $scope); + } + + /** + * Retrieves a field from an entity, according to key and scope given + * + * @param string $stepKey + * @param string $field + * @param string $scope + * @return string + */ + public function retrieveEntityField($stepKey, $field, $scope) + { + return PersistedObjectHandler::getInstance()->retrieveEntityField($stepKey, $field, $scope); + } + + /** + * Get encrypted value by key + * + * @param string $key + * @return string|null + * @throws TestFrameworkException + */ + public function getSecret($key) + { + return CredentialStore::getInstance()->getSecret($key); + } + + /** + * Returns a value to origin of the action + * + * @param mixed $value + * @return mixed + */ + public function return($value) + { + return $value; + } } diff --git a/src/Magento/FunctionalTestingFramework/Module/MagentoAssert.php b/src/Magento/FunctionalTestingFramework/Module/MagentoAssert.php index 3c6c08173..78301ad6f 100644 --- a/src/Magento/FunctionalTestingFramework/Module/MagentoAssert.php +++ b/src/Magento/FunctionalTestingFramework/Module/MagentoAssert.php @@ -36,7 +36,7 @@ public function assertArrayIsSorted(array $data, $sortOrder = "asc") $data = array_map('strtolower', $data); } - if ($sortOrder == "asc") { + if ($sortOrder === 'asc') { for ($i = 1; $i < $elementTotal; $i++) { // $i >= $i-1 $this->assertLessThanOrEqual($data[$i], $data[$i-1], $message); diff --git a/src/Magento/FunctionalTestingFramework/Module/MagentoPwaWebDriver.php b/src/Magento/FunctionalTestingFramework/Module/MagentoPwaWebDriver.php deleted file mode 100644 index 9b75cb10d..000000000 --- a/src/Magento/FunctionalTestingFramework/Module/MagentoPwaWebDriver.php +++ /dev/null @@ -1,74 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\FunctionalTestingFramework\Module; - -use Codeception\Module\WebDriver; - -/** - * Class MagentoPwaActions - * - * Contains all custom PWA action functions to be used in PWA tests. - * - * @package Magento\FunctionalTestingFramework\Module - */ -class MagentoPwaWebDriver extends MagentoWebDriver -{ - /** - * Go to the page. - * - * Overriding the MagentoWebDriver version because it contains 'waitForPageLoad'. - * The AJAX check in 'waitForPageLoad' does NOT work with a PWA. - * - * @param string $page - * @throws \Exception - * @return void - */ - public function amOnPage($page) - { - WebDriver::amOnPage($page); - } - - /** - * Wait for a PWA Element to NOT be visible using JavaScript. - * Add the WAIT_TIMEOUT variable to your .env file for this action. - * - * @param null $selector - * @param null $timeout - * @throws \Exception - * @return void - */ - public function waitForPwaElementNotVisible($selector, $timeout = null) - { - // Determine what type of Selector is used. - // Then use the correct JavaScript to locate the Element. - if (\Codeception\Util\Locator::isXPath($selector)) { - $this->waitForJS("return !document.evaluate(`$selector`, document);", $timeout); - } else { - $this->waitForJS("return !document.querySelector(`$selector`);", $timeout); - } - } - - /** - * Wait for a PWA Element to be visible using JavaScript. - * Add the WAIT_TIMEOUT variable to your .env file for this action. - * - * @param null $selector - * @param null $timeout - * @throws \Exception - * @return void - */ - public function waitForPwaElementVisible($selector, $timeout = null) - { - // Determine what type of Selector is used. - // Then use the correct JavaScript to locate the Element. - if (\Codeception\Util\Locator::isXPath($selector)) { - $this->waitForJS("return !!document && !!document.evaluate(`$selector`, document);", $timeout); - } else { - $this->waitForJS("return !!document && !!document.querySelector(`$selector`);", $timeout); - } - } -} diff --git a/src/Magento/FunctionalTestingFramework/Module/MagentoSequence.php b/src/Magento/FunctionalTestingFramework/Module/MagentoSequence.php index 6273965dd..02f60c736 100644 --- a/src/Magento/FunctionalTestingFramework/Module/MagentoSequence.php +++ b/src/Magento/FunctionalTestingFramework/Module/MagentoSequence.php @@ -7,7 +7,7 @@ // @codingStandardsIgnoreFile namespace Magento\FunctionalTestingFramework\Module; -use Codeception\Module\Sequence; +use Magento\FunctionalTestingFramework\Codeception\Module\Sequence; use Codeception\Exception\ModuleException; /** @@ -16,7 +16,7 @@ */ class MagentoSequence extends Sequence { - protected $config = ['prefix' => '']; + protected array $config = ['prefix' => '']; } if (!function_exists('msq') && !function_exists('msqs')) { require_once __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'Util' . DIRECTORY_SEPARATOR . 'msq.php'; diff --git a/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php b/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php index b02675163..057876659 100644 --- a/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php +++ b/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriver.php @@ -6,6 +6,7 @@ namespace Magento\FunctionalTestingFramework\Module; +use Codeception\Lib\Actor\Shared\Pause; use Codeception\Module\WebDriver; use Codeception\Test\Descriptor; use Codeception\TestInterface; @@ -14,19 +15,19 @@ use Codeception\Exception\ModuleConfigException; use Codeception\Exception\ModuleException; use Codeception\Util\Uri; +use Codeception\Lib\ModuleContainer; +use Magento\FunctionalTestingFramework\DataTransport\WebApiExecutor; +use Magento\FunctionalTestingFramework\DataTransport\Auth\WebApiAuth; +use Magento\FunctionalTestingFramework\DataTransport\Auth\Tfa\OTP; +use Magento\FunctionalTestingFramework\DataTransport\Protocol\CurlInterface; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; -use Magento\FunctionalTestingFramework\DataGenerator\Persist\Curl\WebapiExecutor; +use Magento\FunctionalTestingFramework\Module\Util\ModuleUtils; use Magento\FunctionalTestingFramework\Util\Path\UrlFormatter; -use Magento\FunctionalTestingFramework\Util\Protocol\CurlInterface; use Magento\FunctionalTestingFramework\Util\ConfigSanitizerUtil; -use Yandex\Allure\Adapter\AllureException; -use Magento\FunctionalTestingFramework\Util\Protocol\CurlTransport; -use Yandex\Allure\Adapter\Support\AttachmentSupport; +use Qameta\Allure\Allure; +use Magento\FunctionalTestingFramework\DataTransport\Protocol\CurlTransport; +use Qameta\Allure\Io\DataSourceFactory; use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; -use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; -use Facebook\WebDriver\Remote\RemoteWebDriver; -use Facebook\WebDriver\Exception\WebDriverCurlException; -use Magento\FunctionalTestingFramework\DataGenerator\Handlers\PersistedObjectHandler; /** * MagentoWebDriver module provides common Magento web actions through Selenium WebDriver. @@ -48,10 +49,13 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.ExcessivePublicCount) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ class MagentoWebDriver extends WebDriver { - use AttachmentSupport; + use Pause { + pause as codeceptPause; + } const MAGENTO_CRON_INTERVAL = 60; const MAGENTO_CRON_COMMAND = 'cron:run'; @@ -61,25 +65,15 @@ class MagentoWebDriver extends WebDriver * * @var array */ - public static $loadingMasksLocators = [ + protected $loadingMasksLocators = [ '//div[contains(@class, "loading-mask")]', '//div[contains(@class, "admin_data-grid-loading-mask")]', '//div[contains(@class, "admin__data-grid-loading-mask")]', '//div[contains(@class, "admin__form-loading-mask")]', '//div[@data-role="spinner"]', - ]; - - /** - * The module required fields, to be set in the suite .yml configuration file. - * - * @var array - */ - protected $requiredFields = [ - 'url', - 'backend_name', - 'username', - 'password', - 'browser', + '//div[contains(@class,"file-uploader-spinner")]', + '//div[contains(@class,"image-uploader-spinner")]', + '//div[contains(@class,"uploader")]//div[@class="file-row"]', ]; /** @@ -139,6 +133,7 @@ class MagentoWebDriver extends WebDriver public function _initialize() { $this->config = ConfigSanitizerUtil::sanitizeWebDriverConfig($this->config); + parent::_initialize(); $this->cleanJsError(); } @@ -148,7 +143,7 @@ public function _initialize() * * @return void */ - public function _resetConfig() + public function _resetConfig():void { parent::_resetConfig(); $this->config = ConfigSanitizerUtil::sanitizeWebDriverConfig($this->config); @@ -178,6 +173,16 @@ public function _after(TestInterface $test) // DO NOT RESET SESSIONS } + /** + * Return ModuleContainer + * + * @return ModuleContainer + */ + public function getModuleContainer() + { + return $this->moduleContainer; + } + /** * Returns URL of a host. * @@ -204,14 +209,14 @@ public function _getUrl() * @throws ModuleException * @api */ - public function _getCurrentUri() + public function _getCurrentUri():string { $url = $this->webDriver->getCurrentURL(); - if ($url == 'about:blank') { + if ($url === 'about:blank') { throw new ModuleException($this, 'Current url is blank, no page was opened'); } - return Uri::retrieveUri($url); + return Uri::retrieveUri((string)$url); } /** @@ -219,9 +224,8 @@ public function _getCurrentUri() * * @param string $url * @return void - * @throws AllureException */ - public function dontSeeCurrentUrlEquals($url) + public function dontSeeCurrentUrlEquals($url):void { $actualUrl = $this->webDriver->getCurrentURL(); $comparison = "Expected: $url\nActual: $actualUrl"; @@ -234,9 +238,8 @@ public function dontSeeCurrentUrlEquals($url) * * @param string $regex * @return void - * @throws AllureException */ - public function dontSeeCurrentUrlMatches($regex) + public function dontSeeCurrentUrlMatches($regex):void { $actualUrl = $this->webDriver->getCurrentURL(); $comparison = "Expected: $regex\nActual: $actualUrl"; @@ -249,14 +252,13 @@ public function dontSeeCurrentUrlMatches($regex) * * @param string $needle * @return void - * @throws AllureException */ - public function dontSeeInCurrentUrl($needle) + public function dontSeeInCurrentUrl($needle):void { $actualUrl = $this->webDriver->getCurrentURL(); $comparison = "Expected: $needle\nActual: $actualUrl"; AllureHelper::addAttachmentToCurrentStep($comparison, 'Comparison'); - $this->assertNotContains($needle, $actualUrl); + $this->assertStringNotContainsString($needle, $actualUrl); } /** @@ -265,7 +267,7 @@ public function dontSeeInCurrentUrl($needle) * @param string|null $regex * @return string */ - public function grabFromCurrentUrl($regex = null) + public function grabFromCurrentUrl($regex = null):string { $fullUrl = $this->webDriver->getCurrentURL(); if (!$regex) { @@ -288,9 +290,8 @@ public function grabFromCurrentUrl($regex = null) * * @param string $url * @return void - * @throws AllureException */ - public function seeCurrentUrlEquals($url) + public function seeCurrentUrlEquals($url):void { $actualUrl = $this->webDriver->getCurrentURL(); $comparison = "Expected: $url\nActual: $actualUrl"; @@ -303,9 +304,8 @@ public function seeCurrentUrlEquals($url) * * @param string $regex * @return void - * @throws AllureException */ - public function seeCurrentUrlMatches($regex) + public function seeCurrentUrlMatches($regex):void { $actualUrl = $this->webDriver->getCurrentURL(); $comparison = "Expected: $regex\nActual: $actualUrl"; @@ -318,14 +318,13 @@ public function seeCurrentUrlMatches($regex) * * @param string $needle * @return void - * @throws AllureException */ - public function seeInCurrentUrl($needle) + public function seeInCurrentUrl($needle):void { $actualUrl = $this->webDriver->getCurrentURL(); $comparison = "Expected: $needle\nActual: $actualUrl"; AllureHelper::addAttachmentToCurrentStep($comparison, 'Comparison'); - $this->assertContains($needle, $actualUrl); + $this->assertStringContainsString(urldecode($needle), urldecode($actualUrl)); } /** @@ -437,7 +436,9 @@ public function waitForPageLoad($timeout = null) */ public function waitForLoadingMaskToDisappear($timeout = null) { - foreach (self::$loadingMasksLocators as $maskLocator) { + $timeout = $timeout ?? $this->_getConfig()['pageload_timeout']; + + foreach ($this->loadingMasksLocators as $maskLocator) { // Get count of elements found for looping. // Elements are NOT useful for interaction, as they cannot be fed to codeception actions. $loadingMaskElements = $this->_findElements($maskLocator); @@ -450,19 +451,26 @@ public function waitForLoadingMaskToDisappear($timeout = null) } /** - * @param float $money + * Format input to specified currency in locale specified + * @link https://php.net/manual/en/numberformatter.formatcurrency.php + * + * @param float $value * @param string $locale - * @return array + * @param string $currency + * @return string + * @throws TestFrameworkException */ - public function formatMoney(float $money, $locale = 'en_US.UTF-8') + public function formatCurrency(float $value, $locale, $currency) { - $this->mSetLocale(LC_MONETARY, $locale); - $money = money_format('%.2n', $money); - $this->mResetLocale(); - $prefix = substr($money, 0, 1); - $number = substr($money, 1); + $formatter = \NumberFormatter::create($locale, \NumberFormatter::CURRENCY); + if ($formatter && !empty($formatter)) { + $result = $formatter->formatCurrency($value, $currency); + if ($result) { + return $result; + } + } - return ['prefix' => $prefix, 'number' => $number]; + throw new TestFrameworkException('Invalid attributes used in formatCurrency.'); } /** @@ -485,7 +493,7 @@ public function parseFloat($floatString) */ public function mSetLocale(int $category, $locale) { - if (self::$localeAll[$category] == $locale) { + if (self::$localeAll[$category] === $locale) { return; } foreach (self::$localeAll as $c => $l) { @@ -522,9 +530,9 @@ public function scrollToTopOfPage() /** * Takes given $command and executes it against bin/magento or custom exposed entrypoint. Returns command output. * - * @param string $command - * @param integer $timeout - * @param string $arguments + * @param string $command + * @param integer|null $timeout + * @param string|null $arguments * @return string * * @throws TestFrameworkException @@ -542,13 +550,12 @@ public function magentoCLI($command, $timeout = null, $arguments = null) false ); - $restExecutor = new WebapiExecutor(); $executor = new CurlTransport(); $executor->write( $apiURL, [ - 'token' => $restExecutor->getAuthToken(), - getenv('MAGENTO_CLI_COMMAND_PARAMETER') => $command, + 'token' => WebApiAuth::getAdminToken(), + getenv('MAGENTO_CLI_COMMAND_PARAMETER') => urlencode($command), 'arguments' => $arguments, 'timeout' => $timeout, ], @@ -556,10 +563,11 @@ public function magentoCLI($command, $timeout = null, $arguments = null) [] ); $response = $executor->read(); - $restExecutor->close(); $executor->close(); - return $response; + $util = new ModuleUtils(); + $response = trim($util->utf8SafeControlCharacterTrim($response)); + return $response != "" ? $response : "CLI did not return output."; } /** @@ -644,7 +652,7 @@ private function getCronWait(array $cronGroups = [], int $cronInterval = self::M */ public function deleteEntityByUrl($url) { - $executor = new WebapiExecutor(null); + $executor = new WebApiExecutor(null); $executor->write($url, [], CurlInterface::DELETE, []); $response = $executor->read(); $executor->close(); @@ -686,7 +694,7 @@ public function conditionalClick($selector, $dependentSelector, $visible) * @param string $selector * @return void */ - public function clearField($selector) + public function clearField($selector):void { $this->fillField($selector, ""); } @@ -708,7 +716,7 @@ public function assertElementContainsAttribute($selector, $attribute, $value) // When an "attribute" is blank or null it returns "true" so we assert that "true" is present. $this->assertEquals($attributes, 'true'); } else { - $this->assertContains($value, $attributes); + $this->assertStringContainsString($value, $attributes); } } @@ -736,28 +744,83 @@ public function _before(TestInterface $test) * @param integer $yOffset * @return void */ - public function dragAndDrop($source, $target, $xOffset = null, $yOffset = null) + public function dragAndDrop($source, $target, $xOffset = null, $yOffset = null):void { + $snodes = $this->matchFirstOrFail($this->baseElement, $source); + $tnodes = $this->matchFirstOrFail($this->baseElement, $target); + $action = new WebDriverActions($this->webDriver); if ($xOffset !== null || $yOffset !== null) { - $snodes = $this->matchFirstOrFail($this->baseElement, $source); - $tnodes = $this->matchFirstOrFail($this->baseElement, $target); - $targetX = intval($tnodes->getLocation()->getX() + $xOffset); $targetY = intval($tnodes->getLocation()->getY() + $yOffset); - $travelX = intval($targetX - $snodes->getLocation()->getX()); $travelY = intval($targetY - $snodes->getLocation()->getY()); - - $action = new WebDriverActions($this->webDriver); - $action->moveToElement($snodes)->perform(); - $action->clickAndHold($snodes)->perform(); - $action->moveByOffset($travelX, $travelY)->perform(); + $action->moveToElement($snodes); + $action->clickAndHold($snodes); + // Fix Start + $action->moveByOffset(-1, -1); + $action->moveByOffset(1, 1); + // Fix End + $action->moveByOffset($travelX, $travelY); $action->release()->perform(); } else { - parent::dragAndDrop($source, $target); + $action->clickAndHold($snodes); + // Fix Start + $action->moveByOffset(-1, -1); + $action->moveByOffset(1, 1); + // Fix End + $action->moveToElement($tnodes); + $action->release($tnodes)->perform(); } } + /** + * Simple rapid click as per given count number. + * + * @param string $selector + * @param string $count + * @return void + * @throws \Exception + */ + public function rapidClick($selector, $count) + { + for ($i = 0; $i < $count; $i++) { + $this->click($selector); + } + } + + /** + * Grabs a cookie attributes value. + * You can set additional cookie params like `domain`, `path` in array passed as last argument. + * If the cookie is set by an ajax request (XMLHttpRequest), + * there might be some delay caused by the browser, so try `$I->wait(0.1)`. + * @param string $cookie + * @param array $params + * @return mixed + */ + public function grabCookieAttributes(string $cookie, array $params = []): array + { + $params['name'] = $cookie; + $cookieArrays = $this->filterCookies($this->webDriver->manage()->getCookies(), $params); + $cookieAttributes = []; + if (is_array($cookieArrays)) { // Microsoft Edge returns null if there are no cookies... + foreach ($cookieArrays as $cookieArray) { + if ($cookieArray->getName() === $cookie) { + $cookieAttributes['name'] = $cookieArray->getValue(); + $cookieAttributes['path'] = $cookieArray->getPath(); + $cookieAttributes['domain'] = $cookieArray->getDomain(); + $cookieAttributes['secure'] = $cookieArray->isSecure(); + $cookieAttributes['httpOnly'] = $cookieArray->isHttpOnly(); + $cookieAttributes['sameSite'] = $cookieArray->getSameSite(); + $cookieAttributes['expiry'] = date('d/m/Y', $cookieArray->getExpiry()); + + return $cookieAttributes; + } + } + } + + return $cookieAttributes; + } + /** * Function used to fill sensitive credentials with user data, data is decrypted immediately prior to fill to avoid * exposure in console or log. @@ -783,9 +846,9 @@ public function fillSecretField($field, $value) * Function used to create data that contains sensitive credentials in a <createData> <field> override. * The data is decrypted immediately prior to data creation to avoid exposure in console or log. * - * @param string $command - * @param null $timeout - * @param null $arguments + * @param string $command + * @param integer|null $timeout + * @param string|null $arguments * @throws TestFrameworkException * @return string */ @@ -815,15 +878,26 @@ public function _failed(TestInterface $test, $fail) if ($this->pngReport === null && $this->htmlReport === null) { $this->saveScreenshot(); + if (getenv('ENABLE_PAUSE') === 'true') { + $this->pause(true); + } } - if ($this->current_test == null) { - throw new \RuntimeException("Suite condition failure: \n" . $fail->getMessage()); + if ($this->current_test === null) { + throw new \RuntimeException("Suite condition failure: \n" + . " Something went wrong with selenium server/chrome driver : \n . + {$fail->getMessage()}\n{$fail->getTraceAsString()}"); } - - $this->addAttachment($this->pngReport, $test->getMetadata()->getName() . '.png', 'image/png'); - $this->addAttachment($this->htmlReport, $test->getMetadata()->getName() . '.html', 'text/html'); - + AllureHelper::doAddAttachment( + DataSourceFactory::fromFile($this->pngReport), + $test->getMetadata()->getName() . '.png', + 'image/png' + ); + AllureHelper::doAddAttachment( + DataSourceFactory::fromFile($this->htmlReport), + $test->getMetadata()->getName() . '.html', + 'text/html' + ); $this->debug("Failure due to : {$fail->getMessage()}"); $this->debug("Screenshot saved to {$this->pngReport}"); $this->debug("Html saved to {$this->htmlReport}"); @@ -837,7 +911,7 @@ public function _failed(TestInterface $test, $fail) public function saveScreenshot() { $testDescription = "unknown." . uniqid(); - if ($this->current_test != null) { + if ($this->current_test !== null) { $testDescription = Descriptor::getTestSignature($this->current_test); } @@ -854,9 +928,9 @@ public function saveScreenshot() * @return void * @throws \Exception */ - public function amOnPage($page) + public function amOnPage($page):void { - parent::amOnPage($page); + (0 === strpos($page, 'http')) ? parent::amOnUrl($page) : parent::amOnPage($page); $this->waitForPageLoad(); } @@ -918,9 +992,8 @@ public function dontSeeJsError() * * @param string $name * @return void - * @throws AllureException */ - public function makeScreenshot($name = null) + public function makeScreenshot($name = null):void { if (empty($name)) { $name = uniqid(date("Y-m-d_H-i-s_")); @@ -936,148 +1009,158 @@ public function makeScreenshot($name = null) } /** - * Create an entity - * TODO: move this function to MagentoActionProxies after MQE-1904 + * Return OTP based on a shared secret * - * @param string $key StepKey of the createData action. - * @param string $scope - * @param string $entity Name of xml entity to create. - * @param array $dependentObjectKeys StepKeys of other createData actions that are required. - * @param array $overrideFields Array of FieldName => Value of override fields. - * @param string $storeCode - * @return void + * @param string|null $secretsPath + * @return string + * @throws TestFrameworkException */ - public function createEntity( - $key, - $scope, - $entity, - $dependentObjectKeys = [], - $overrideFields = [], - $storeCode = '' - ) { - PersistedObjectHandler::getInstance()->createEntity( - $key, - $scope, - $entity, - $dependentObjectKeys, - $overrideFields, - $storeCode - ); + public function getOTP($secretsPath = null) + { + return OTP::getOTP($secretsPath); } /** - * Retrieves and updates a previously created entity - * TODO: move this function to MagentoActionProxies after MQE-1904 + * Waits proper amount of time to perform Cron execution * - * @param string $key StepKey of the createData action. - * @param string $scope - * @param string $updateEntity Name of the static XML data to update the entity with. - * @param array $dependentObjectKeys StepKeys of other createData actions that are required. - * @return void + * @param array $cronGroups + * @param integer $timeout + * @param string $arguments + * @return string + * @throws TestFrameworkException */ - public function updateEntity($key, $scope, $updateEntity, $dependentObjectKeys = []) + private function executeCronjobs($cronGroups, $timeout, $arguments): string { - PersistedObjectHandler::getInstance()->updateEntity( - $key, - $scope, - $updateEntity, - $dependentObjectKeys - ); + $cronGroups = array_filter($cronGroups); + + if (isset($cronGroups[0]) && !isset($cronGroups[1])) { + $arguments .= ' --bootstrap=standaloneProcessStarted=1'; + } + + $waitFor = $this->getCronWait($cronGroups); + + if ($waitFor) { + $this->wait($waitFor); + } + + $command = array_reduce($cronGroups, function ($command, $cronGroup) { + $command .= ' --group=' . $cronGroup; + return $command; + }, self::MAGENTO_CRON_COMMAND); + $timeStart = microtime(true); + $cronResult = $this->magentoCLI($command, $timeout, $arguments); + $timeEnd = microtime(true); + + $this->notifyCronFinished($cronGroups); + + return sprintf('%s (wait: %ss, execution: %ss)', $cronResult, $waitFor, round($timeEnd - $timeStart, 2)); } /** - * Performs GET on given entity and stores entity for use - * TODO: move this function to MagentoActionProxies after MQE-1904 + * Switch to another frame on the page by name, ID, CSS or XPath. * - * @param string $key StepKey of getData action. - * @param string $scope - * @param string $entity Name of XML static data to use. - * @param array $dependentObjectKeys StepKeys of other createData actions that are required. - * @param string $storeCode - * @param integer $index + * @param string|null $locator * @return void + * @throws \Exception */ - public function getEntity($key, $scope, $entity, $dependentObjectKeys = [], $storeCode = '', $index = null) + public function switchToIFrame($locator = null):void { - PersistedObjectHandler::getInstance()->getEntity( - $key, - $scope, - $entity, - $dependentObjectKeys, - $storeCode, - $index - ); + try { + parent::switchToIFrame($locator); + } catch (\Exception $e) { + $els = $this->_findElements("#$locator"); + if (!count($els)) { + $this->debug('Failed to find locator by ID: ' . $e->getMessage()); + throw new \Exception("IFrame with $locator was not found."); + } + $this->webDriver->switchTo()->frame($els[0]); + } } /** - * Retrieves and deletes a previously created entity - * TODO: move this function to MagentoActionProxies after MQE-1904 + * Invoke Codeption pause() * - * @param string $key StepKey of the createData action. - * @param string $scope + * @param boolean $pauseOnFail * @return void */ - public function deleteEntity($key, $scope) + public function pause($pauseOnFail = false):void { - PersistedObjectHandler::getInstance()->deleteEntity($key, $scope); + if (\Composer\InstalledVersions::isInstalled('hoa/console') === false) { + $message = "<pause /> action is unavailable." . PHP_EOL; + $message .= "Please install `hoa/console` via \"composer require hoa/console\"" . PHP_EOL; + print($message); + return; + } + if (!\Codeception\Util\Debug::isEnabled()) { + return; + } + + if ($pauseOnFail) { + print(PHP_EOL . "Failure encountered. Pausing execution..." . PHP_EOL . PHP_EOL); + } + + $this->codeceptPause(); } /** - * Retrieves a field from an entity, according to key and scope given - * TODO: move this function to MagentoActionProxies after MQE-1904 - * - * @param string $stepKey - * @param string $field - * @param string $scope - * @return string + * @param string $selector + * @param string $expected + * @return void */ - public function retrieveEntityField($stepKey, $field, $scope) + public function seeNumberOfElements($selector, $expected): void { - return PersistedObjectHandler::getInstance()->retrieveEntityField($stepKey, $field, $scope); + $counted = count($this->matchVisible($selector)); + if (is_array($expected)) { + [$floor, $ceil] = $expected; + $this->assertTrue( + $floor <= $counted && $ceil >= $counted, + 'Number of elements counted differs from expected range' + ); + } else { + $this->assertSame( + (int)$expected, + (int)$counted, + 'Number of elements counted differs from expected number' + ); + } } /** - * Get encrypted value by key - * TODO: move this function to MagentoActionProxies after MQE-1904 - * - * @param string $key - * @return string|null - * @throws TestFrameworkException + * @param string $text + * @param string $selector + * @return void */ - public function getSecret($key) + public function see($text, $selector = null): void { - return CredentialStore::getInstance()->getSecret($key); + $text = (isset($text)) + ? (string)$text + : ""; + if (!$selector) { + $this->assertPageContains($text); + return; + } + + $this->enableImplicitWait(); + $nodes = $this->matchVisible($selector); + $this->disableImplicitWait(); + $this->assertNodesContain($text, $nodes, $selector); } /** - * Waits proper amount of time to perform Cron execution - * - * @param array $cronGroups - * @param integer $timeout - * @param string $arguments - * @return string - * @throws TestFrameworkException + * @param string $text + * @param string $selector + * @return void */ - private function executeCronjobs($cronGroups, $timeout, $arguments): string + public function dontSee($text, $selector = null): void { - $cronGroups = array_filter($cronGroups); - - $waitFor = $this->getCronWait($cronGroups); - - if ($waitFor) { - $this->wait($waitFor); + $text = (isset($text)) + ? (string)$text + : ""; + if (!$selector) { + $this->assertPageNotContains($text); + } else { + $nodes = $this->matchVisible($selector); + $this->assertNodesNotContain($text, $nodes, $selector); } - - $command = array_reduce($cronGroups, function ($command, $cronGroup) { - $command .= ' --group=' . $cronGroup; - return $command; - }, self::MAGENTO_CRON_COMMAND); - $timeStart = microtime(true); - $cronResult = $this->magentoCLI($command, $timeout, $arguments); - $timeEnd = microtime(true); - - $this->notifyCronFinished($cronGroups); - - return sprintf('%s (wait: %ss, execution: %ss)', $cronResult, $waitFor, round($timeEnd - $timeStart, 2)); } } diff --git a/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriverDoctor.php b/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriverDoctor.php index 1407c957b..05332aac4 100644 --- a/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriverDoctor.php +++ b/src/Magento/FunctionalTestingFramework/Module/MagentoWebDriverDoctor.php @@ -91,8 +91,8 @@ private function connectToSeleniumServer() $this->capabilities, $this->connectionTimeoutInMs, $this->requestTimeoutInMs, - $this->httpProxy, - $this->httpProxyPort + $this->config['http_proxy'], + $this->config['http_proxy_port'] ); if (null !== $this->remoteWebDriver) { return; diff --git a/src/Magento/FunctionalTestingFramework/Module/Util/ModuleUtils.php b/src/Magento/FunctionalTestingFramework/Module/Util/ModuleUtils.php new file mode 100644 index 000000000..d9bc203c4 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Module/Util/ModuleUtils.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Module\Util; + +class ModuleUtils +{ + /** + * Module util function that returns UTF-8 encoding string with control/invisible characters removed, + * and it returns the original string when on error. + * + * @param string $input + * @return string + */ + public function utf8SafeControlCharacterTrim(string $input): string + { + // Convert $input string to UTF-8 encoding + $convInput = iconv("ISO-8859-1", "UTF-8//IGNORE", $input); + if ($convInput !== false) { + // Remove invisible control characters, unused code points and replacement character + // so that they don't break xml test results for Allure + $cleanInput = preg_replace('/[^\PC\s]|\x{FFFD}/u', '', $convInput); + if ($cleanInput !== null) { + return $cleanInput; + } else { + $err = preg_last_error_msg(); + print("MagentoCLI response preg_replace() with error $err.\n"); + } + } + + return $input; + } +} diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager.php b/src/Magento/FunctionalTestingFramework/ObjectManager.php index 8b0f34537..ed07a0d9f 100644 --- a/src/Magento/FunctionalTestingFramework/ObjectManager.php +++ b/src/Magento/FunctionalTestingFramework/ObjectManager.php @@ -39,8 +39,8 @@ class ObjectManager extends \Magento\FunctionalTestingFramework\ObjectManager\Ob * @param array $sharedInstances */ public function __construct( - \Magento\FunctionalTestingFramework\ObjectManager\Factory $factory = null, - \Magento\FunctionalTestingFramework\ObjectManager\ConfigInterface $config = null, + ?\Magento\FunctionalTestingFramework\ObjectManager\Factory $factory = null, + ?\Magento\FunctionalTestingFramework\ObjectManager\ConfigInterface $config = null, array $sharedInstances = [] ) { parent::__construct($factory, $config, $sharedInstances); diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Config.php b/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Config.php index 3b2c827d5..435b29818 100644 --- a/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Config.php +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Config.php @@ -76,7 +76,7 @@ class Config implements \Magento\FunctionalTestingFramework\ObjectManager\Config * @param RelationsInterface|null $relations * @param DefinitionInterface|null $definitions */ - public function __construct(RelationsInterface $relations = null, DefinitionInterface $definitions = null) + public function __construct(?RelationsInterface $relations = null, ?DefinitionInterface $definitions = null) { $this->relations = $relations ? : new RelationsRuntime(); $this->definitions = $definitions ? : new DefinitionRuntime(); diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Mapper/Dom.php b/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Mapper/Dom.php index ba7bc64cd..ee1ef6045 100644 --- a/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Mapper/Dom.php +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Mapper/Dom.php @@ -33,8 +33,8 @@ class Dom implements \Magento\FunctionalTestingFramework\Config\ConverterInterfa */ public function __construct( InterpreterInterface $argumentInterpreter, - BooleanUtils $booleanUtils = null, - ArgumentParser $argumentParser = null + ?BooleanUtils $booleanUtils = null, + ?ArgumentParser $argumentParser = null ) { $this->argumentInterpreter = $argumentInterpreter; $this->booleanUtils = $booleanUtils ?: new BooleanUtils(); @@ -54,7 +54,7 @@ public function convert($config) $output = []; /** @var \DOMNode $node */ foreach ($config->documentElement->childNodes as $node) { - if ($node->nodeType != XML_ELEMENT_NODE) { + if ($node->nodeType !== XML_ELEMENT_NODE) { continue; } switch ($node->nodeName) { @@ -73,7 +73,7 @@ public function convert($config) if ($typeNodeShared) { $typeData['shared'] = $this->booleanUtils->toBoolean($typeNodeShared->nodeValue); } - if ($node->nodeName == 'virtualType') { + if ($node->nodeName === 'virtualType') { $attributeType = $typeNodeAttributes->getNamedItem('type'); // attribute type is required for virtual type only in merged configuration if ($attributeType) { @@ -102,14 +102,14 @@ private function setTypeArguments($node) foreach ($node->childNodes as $typeChildNode) { /** @var \DOMNode $typeChildNode */ - if ($typeChildNode->nodeType != XML_ELEMENT_NODE) { + if ($typeChildNode->nodeType !== XML_ELEMENT_NODE) { continue; } switch ($typeChildNode->nodeName) { case 'arguments': /** @var \DOMNode $argumentNode */ foreach ($typeChildNode->childNodes as $argumentNode) { - if ($argumentNode->nodeType != XML_ELEMENT_NODE) { + if ($argumentNode->nodeType !== XML_ELEMENT_NODE) { continue; } $argumentName = $argumentNode->attributes->getNamedItem('name')->nodeValue; diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Reader/Dom.php b/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Reader/Dom.php index 876e949cb..bcc897b4b 100644 --- a/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Reader/Dom.php +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/Config/Reader/Dom.php @@ -65,6 +65,6 @@ public function __construct( */ protected function _createConfigMerger($mergerClass, $initialContents) { - return new $mergerClass($initialContents, $this->_idAttributes, self::TYPE_ATTRIBUTE, $this->_perFileSchema); + return new $mergerClass($initialContents, $this->idAttributes, self::TYPE_ATTRIBUTE, $this->perFileSchema); } } diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/Definition/Runtime.php b/src/Magento/FunctionalTestingFramework/ObjectManager/Definition/Runtime.php index f77e7d110..095bf17eb 100644 --- a/src/Magento/FunctionalTestingFramework/ObjectManager/Definition/Runtime.php +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/Definition/Runtime.php @@ -29,7 +29,7 @@ class Runtime implements \Magento\FunctionalTestingFramework\ObjectManager\Defin * Runtime constructor. * @param \Magento\FunctionalTestingFramework\Code\Reader\ClassReader|null $reader */ - public function __construct(\Magento\FunctionalTestingFramework\Code\Reader\ClassReader $reader = null) + public function __construct(?\Magento\FunctionalTestingFramework\Code\Reader\ClassReader $reader = null) { $this->reader = $reader ? : new \Magento\FunctionalTestingFramework\Code\Reader\ClassReader(); } diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/Factory.php b/src/Magento/FunctionalTestingFramework/ObjectManager/Factory.php index c5882d035..31b6c4f50 100644 --- a/src/Magento/FunctionalTestingFramework/ObjectManager/Factory.php +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/Factory.php @@ -31,8 +31,8 @@ class Factory extends \Magento\FunctionalTestingFramework\ObjectManager\Factory\ */ public function __construct( ConfigInterface $config, - \Magento\FunctionalTestingFramework\ObjectManagerInterface $objectManager = null, - DefinitionInterface $definitions = null, + ?\Magento\FunctionalTestingFramework\ObjectManagerInterface $objectManager = null, + ?DefinitionInterface $definitions = null, $globalArguments = [] ) { parent::__construct($config, $objectManager, $definitions, $globalArguments); @@ -84,7 +84,8 @@ public function prepareArguments($object, $method, array $arguments = []) { $type = get_class($object); $parameters = $this->classReader->getParameters($type, $method); - if ($parameters == null) { + + if ($parameters === null) { return []; } @@ -227,7 +228,8 @@ public function create($requestedType, array $arguments = []) { $instanceType = $this->config->getInstanceType($requestedType); $parameters = $this->definitions->getParameters($instanceType); - if ($parameters == null) { + + if ($parameters === null) { return new $instanceType(); } if (isset($this->creationStack[$requestedType])) { diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/Factory/Dynamic/Developer.php b/src/Magento/FunctionalTestingFramework/ObjectManager/Factory/Dynamic/Developer.php index 937be7da6..17c7fd972 100644 --- a/src/Magento/FunctionalTestingFramework/ObjectManager/Factory/Dynamic/Developer.php +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/Factory/Dynamic/Developer.php @@ -54,8 +54,8 @@ class Developer implements \Magento\FunctionalTestingFramework\ObjectManager\Fac */ public function __construct( \Magento\FunctionalTestingFramework\ObjectManager\ConfigInterface $config, - \Magento\FunctionalTestingFramework\ObjectManagerInterface $objectManager = null, - \Magento\FunctionalTestingFramework\ObjectManager\DefinitionInterface $definitions = null, + ?\Magento\FunctionalTestingFramework\ObjectManagerInterface $objectManager = null, + ?\Magento\FunctionalTestingFramework\ObjectManager\DefinitionInterface $definitions = null, $globalArguments = [] ) { $this->config = $config; @@ -178,7 +178,8 @@ public function create($requestedType, array $arguments = []) { $type = $this->config->getInstanceType($requestedType); $parameters = $this->definitions->getParameters($type); - if ($parameters == null) { + + if ($parameters === null) { return new $type(); } if (isset($this->creationStack[$requestedType])) { diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/ObjectHandlerInterface.php b/src/Magento/FunctionalTestingFramework/ObjectManager/ObjectHandlerInterface.php index 94147dc08..8bde3b96d 100644 --- a/src/Magento/FunctionalTestingFramework/ObjectManager/ObjectHandlerInterface.php +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/ObjectHandlerInterface.php @@ -24,7 +24,7 @@ public static function getInstance(); * Function to return a single object by name * * @param string $objectName - * @return object + * @return mixed */ public function getObject($objectName); diff --git a/src/Magento/FunctionalTestingFramework/ObjectManager/Relations/Runtime.php b/src/Magento/FunctionalTestingFramework/ObjectManager/Relations/Runtime.php index 192758220..1a49a120d 100644 --- a/src/Magento/FunctionalTestingFramework/ObjectManager/Relations/Runtime.php +++ b/src/Magento/FunctionalTestingFramework/ObjectManager/Relations/Runtime.php @@ -28,7 +28,7 @@ class Runtime implements \Magento\FunctionalTestingFramework\ObjectManager\Relat * Runtime constructor. * @param \Magento\FunctionalTestingFramework\Code\Reader\ClassReader|null $classReader */ - public function __construct(\Magento\FunctionalTestingFramework\Code\Reader\ClassReader $classReader = null) + public function __construct(?\Magento\FunctionalTestingFramework\Code\Reader\ClassReader $classReader = null) { $this->classReader = $classReader ? : new \Magento\FunctionalTestingFramework\Code\Reader\ClassReader(); } diff --git a/src/Magento/FunctionalTestingFramework/Page/Handlers/PageObjectHandler.php b/src/Magento/FunctionalTestingFramework/Page/Handlers/PageObjectHandler.php index 8dcf090f0..dd723fa1b 100644 --- a/src/Magento/FunctionalTestingFramework/Page/Handlers/PageObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/Page/Handlers/PageObjectHandler.php @@ -23,7 +23,7 @@ class PageObjectHandler implements ObjectHandlerInterface const PARAMETERIZED = 'parameterized'; const AREA = 'area'; const FILENAME = 'filename'; - const NAME_BLACKLIST_ERROR_MSG = "Page names cannot contain non alphanumeric characters.\tPage='%s'"; + const NAME_BLOCKLIST_ERROR_MSG = "Page names cannot contain non alphanumeric characters.\tPage='%s'"; /** * The singleton instance of this class @@ -58,7 +58,7 @@ private function __construct() $pageNameValidator = new NameValidationUtil(); foreach ($parserOutput as $pageName => $pageData) { if (preg_match('/[^a-zA-Z0-9_]/', $pageName)) { - throw new XmlException(sprintf(self::NAME_BLACKLIST_ERROR_MSG, $pageName)); + throw new XmlException(sprintf(self::NAME_BLOCKLIST_ERROR_MSG, $pageName)); } $filename = $pageData[self::FILENAME] ?? null; @@ -66,7 +66,7 @@ private function __construct() $area = $pageData[self::AREA] ?? null; $url = $pageData[self::URL] ?? null; - if ($area == 'admin') { + if ($area === 'admin') { $url = ltrim($url, "/"); } @@ -79,8 +79,8 @@ private function __construct() if ($deprecated !== null) { LoggingUtil::getInstance()->getLogger(self::class)->deprecation( - $deprecated, - ["pageName" => $filename, "deprecatedPage" => $deprecated] + "The page '{$pageName}' is deprecated.", + ["fileName" => $filename, "deprecatedMessage" => $deprecated] ); } diff --git a/src/Magento/FunctionalTestingFramework/Page/Handlers/SectionObjectHandler.php b/src/Magento/FunctionalTestingFramework/Page/Handlers/SectionObjectHandler.php index 5d658bac0..6ac3456fd 100644 --- a/src/Magento/FunctionalTestingFramework/Page/Handlers/SectionObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/Page/Handlers/SectionObjectHandler.php @@ -89,8 +89,8 @@ private function __construct() $elementDeprecated = $elementData[self::OBJ_DEPRECATED] ?? null; if ($elementDeprecated !== null) { LoggingUtil::getInstance()->getLogger(ElementObject::class)->deprecation( - $elementDeprecated, - ["elementName" => $elementName, "deprecatedElement" => $elementDeprecated] + "The element '{$elementName}' is deprecated.", + ["fileName" => $filename, "deprecatedMessage" => $elementDeprecated] ); } $elements[$elementName] = new ElementObject( diff --git a/src/Magento/FunctionalTestingFramework/Page/Objects/ElementObject.php b/src/Magento/FunctionalTestingFramework/Page/Objects/ElementObject.php index 567872fc7..262647b34 100644 --- a/src/Magento/FunctionalTestingFramework/Page/Objects/ElementObject.php +++ b/src/Magento/FunctionalTestingFramework/Page/Objects/ElementObject.php @@ -75,9 +75,9 @@ class ElementObject */ public function __construct($name, $type, $selector, $locatorFunction, $timeout, $parameterized, $deprecated = null) { - if ($selector != null && $locatorFunction != null) { + if ($selector !== null && $locatorFunction !== null) { throw new XmlException("Element '{$name}' cannot have both a selector and a locatorFunction."); - } elseif ($selector == null && $locatorFunction == null) { + } elseif ($selector === null && $locatorFunction === null) { throw new XmlException("Element '{$name}' must have either a selector or a locatorFunction.'"); } @@ -85,7 +85,7 @@ public function __construct($name, $type, $selector, $locatorFunction, $timeout, $this->type = $type; $this->selector = $selector; $this->locatorFunction = $locatorFunction; - if (strpos($locatorFunction, "Locator::") === false) { + if ($locatorFunction !== null && strpos($locatorFunction, "Locator::") === false) { $this->locatorFunction = "Locator::" . $locatorFunction; } $this->timeout = $timeout; @@ -160,11 +160,11 @@ public function getPrioritizedSelector() */ public function getTimeout() { - if ($this->timeout == ElementObject::DEFAULT_TIMEOUT_SYMBOL) { + if ($this->timeout === ElementObject::DEFAULT_TIMEOUT_SYMBOL) { return null; } - return (int)$this->timeout; + return (int) $this->timeout; } /** diff --git a/src/Magento/FunctionalTestingFramework/StaticCheck/ActionGroupArgumentsCheck.php b/src/Magento/FunctionalTestingFramework/StaticCheck/ActionGroupStandardsCheck.php similarity index 54% rename from src/Magento/FunctionalTestingFramework/StaticCheck/ActionGroupArgumentsCheck.php rename to src/Magento/FunctionalTestingFramework/StaticCheck/ActionGroupStandardsCheck.php index 556ad45be..fdc90b6d9 100644 --- a/src/Magento/FunctionalTestingFramework/StaticCheck/ActionGroupArgumentsCheck.php +++ b/src/Magento/FunctionalTestingFramework/StaticCheck/ActionGroupStandardsCheck.php @@ -6,29 +6,26 @@ namespace Magento\FunctionalTestingFramework\StaticCheck; -use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; -use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Test\Handlers\ActionGroupObjectHandler; use Magento\FunctionalTestingFramework\Test\Objects\ActionGroupObject; use Symfony\Component\Console\Input\InputInterface; -use Magento\FunctionalTestingFramework\Util\ModuleResolver; use Symfony\Component\Finder\Finder; use Exception; use Magento\FunctionalTestingFramework\Util\Script\ScriptUtil; +use Symfony\Component\Finder\SplFileInfo; +use DOMElement; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; /** * Class ActionGroupArgumentsCheck * @package Magento\FunctionalTestingFramework\StaticCheck */ -class ActionGroupArgumentsCheck implements StaticCheckInterface +class ActionGroupStandardsCheck implements StaticCheckInterface { - - const ACTIONGROUP_XML_REGEX_PATTERN = '/<actionGroup\sname=(?: (?!<\/actionGroup>).)*/mxs'; - const ACTIONGROUP_ARGUMENT_REGEX_PATTERN = '/<argument[^\/>]*name="([^"\']*)/mxs'; const ACTIONGROUP_NAME_REGEX_PATTERN = '/<actionGroup name=["\']([^\'"]*)/'; - - const ERROR_LOG_FILENAME = 'mftf-arguments-checks'; + const ERROR_LOG_FILENAME = 'mftf-standards-checks'; const ERROR_LOG_MESSAGE = 'MFTF Action Group Unused Arguments Check'; + const STEP_KEY_REGEX_PATTERN = '/stepKey=["\']([^\'"]*)/'; /** * Array containing all errors found after running the execute() function. @@ -42,6 +39,13 @@ class ActionGroupArgumentsCheck implements StaticCheckInterface */ private $output; + /** + * ScriptUtil instance + * + * @var ScriptUtil + */ + private $scriptUtil; + /** * Checks unused arguments in action groups and prints out error to file. * @@ -51,18 +55,19 @@ class ActionGroupArgumentsCheck implements StaticCheckInterface */ public function execute(InputInterface $input) { - $allModules = ScriptUtil::getAllModulePaths(); + $this->scriptUtil = new ScriptUtil(); + $allModules = $this->scriptUtil->getAllModulePaths(); - $actionGroupXmlFiles = ScriptUtil::getModuleXmlFilesByScope( + $actionGroupXmlFiles = $this->scriptUtil->getModuleXmlFilesByScope( $allModules, DIRECTORY_SEPARATOR . 'ActionGroup' . DIRECTORY_SEPARATOR ); $this->errors = $this->findErrorsInFileSet($actionGroupXmlFiles); - $this->output = ScriptUtil::printErrorsToFile( + $this->output = $this->scriptUtil->printErrorsToFile( $this->errors, - self::ERROR_LOG_FILENAME, + StaticChecksList::getErrorFilesPath() . DIRECTORY_SEPARATOR . self::ERROR_LOG_FILENAME . '.txt', self::ERROR_LOG_MESSAGE ); } @@ -93,61 +98,111 @@ public function getOutput() private function findErrorsInFileSet($files) { $actionGroupErrors = []; + /** @var SplFileInfo $filePath */ foreach ($files as $filePath) { - $contents = file_get_contents($filePath); - preg_match_all(self::ACTIONGROUP_XML_REGEX_PATTERN, $contents, $actionGroups); - $actionGroupToArguments = $this->buildUnusedArgumentList($actionGroups[0]); - $actionGroupErrors += $this->setErrorOutput($actionGroupToArguments, $filePath); + $actionGroupReferencesDataArray = []; + $actionGroupToArguments = []; + $contents = $filePath->getContents(); + preg_match_all( + self::STEP_KEY_REGEX_PATTERN, + preg_replace('/<!--(.|\s)*?-->/', '', $contents), + $actionGroupReferences + ); + foreach ($actionGroupReferences[0] as $actionGroupReferencesData) { + $actionGroupReferencesDataArray[] = trim( + str_replace(['stepKey', '='], [""], $actionGroupReferencesData) + ).'"'; + } + $duplicateStepKeys = array_unique( + array_diff_assoc( + $actionGroupReferencesDataArray, + array_unique( + $actionGroupReferencesDataArray + ) + ) + ); + unset($actionGroupReferencesDataArray); + if (isset($duplicateStepKeys) && count($duplicateStepKeys) > 0) { + throw new TestFrameworkException('Action group has duplicate step keys ' + .implode(",", array_unique($duplicateStepKeys))." File Path ".$filePath); + } + /** @var DOMElement $actionGroup */ + $actionGroup = $this->getActionGroupDomElement($contents); + $arguments = $this->extractActionGroupArguments($actionGroup); + $unusedArguments = $this->findUnusedArguments($arguments, $contents); + if (!empty($unusedArguments)) { + $actionGroupToArguments[$actionGroup->getAttribute('name')] = $unusedArguments; + $actionGroupErrors += $this->setErrorOutput($actionGroupToArguments, $filePath); + } } return $actionGroupErrors; } /** - * Builds array of action group => unused arguments - * @param array $actionGroups - * @return array $actionGroupToArguments + * Extract actionGroup DomElement from xml file + * @param string $contents + * @return \DOMElement */ - private function buildUnusedArgumentList($actionGroups) + public function getActionGroupDomElement($contents) { - $actionGroupToArguments = []; + $domDocument = new \DOMDocument(); + $domDocument->loadXML($contents); + return $domDocument->getElementsByTagName('actionGroup')[0]; + } - foreach ($actionGroups as $actionGroupXml) { - preg_match(self::ACTIONGROUP_NAME_REGEX_PATTERN, $actionGroupXml, $actionGroupName); - $unusedArguments = $this->findUnusedArguments($actionGroupXml); - if (!empty($unusedArguments)) { - $actionGroupToArguments[$actionGroupName[1]] = $unusedArguments; + /** + * Get list of action group arguments declared in an action group + * @param \DOMElement $actionGroup + * @return array $arguments + */ + public function extractActionGroupArguments($actionGroup) + { + $arguments = []; + $argumentsNodes = $actionGroup->getElementsByTagName('arguments'); + if ($argumentsNodes->length > 0) { + $argumentNodes = $argumentsNodes[0]->getElementsByTagName('argument'); + foreach ($argumentNodes as $argumentNode) { + $arguments[] = $argumentNode->getAttribute('name'); } } - return $actionGroupToArguments; + return $arguments; } /** * Returns unused arguments in an action group - * @param string $actionGroupXml + * @param array $arguments + * @param string $contents * @return array */ - private function findUnusedArguments($actionGroupXml) + public function findUnusedArguments($arguments, $contents) { $unusedArguments = []; - - preg_match_all(self::ACTIONGROUP_ARGUMENT_REGEX_PATTERN, $actionGroupXml, $arguments); - preg_match(self::ACTIONGROUP_NAME_REGEX_PATTERN, $actionGroupXml, $actionGroupName); + preg_match(self::ACTIONGROUP_NAME_REGEX_PATTERN, $contents, $actionGroupName); + $validActionGroup = false; try { $actionGroup = ActionGroupObjectHandler::getInstance()->getObject($actionGroupName[1]); - } catch (XmlException $e) { + if ($actionGroup) { + $validActionGroup = true; + } + } catch (Exception $e) { } - foreach ($arguments[1] as $argument) { + + if (!$validActionGroup) { + return $unusedArguments; + } + + foreach ($arguments as $argument) { //pattern to match all argument references $patterns = [ '(\{{2}' . $argument . '(\.[a-zA-Z0-9_\[\]\(\).,\'\/ ]+)?}{2})', '([(,\s\'$$]' . $argument . '(\.[a-zA-Z0-9_$\[\]]+)?[),\s\'])' ]; // matches entity references - if (preg_match($patterns[0], $actionGroupXml)) { + if (preg_match($patterns[0], $contents)) { continue; } //matches parametrized references - if (preg_match($patterns[1], $actionGroupXml)) { + if (preg_match($patterns[1], $contents)) { continue; } //for extending action groups, exclude arguments that are also defined in parent action group @@ -183,8 +238,8 @@ private function isParentActionGroupArgument($argument, $actionGroup) /** * Builds and returns error output for violating references * - * @param array $actionGroupToArguments - * @param string $path + * @param array $actionGroupToArguments + * @param SplFileInfo $path * @return mixed */ private function setErrorOutput($actionGroupToArguments, $path) diff --git a/src/Magento/FunctionalTestingFramework/StaticCheck/AnnotationsCheck.php b/src/Magento/FunctionalTestingFramework/StaticCheck/AnnotationsCheck.php new file mode 100644 index 000000000..0b3e6c40d --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/StaticCheck/AnnotationsCheck.php @@ -0,0 +1,262 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\StaticCheck; + +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; +use Magento\FunctionalTestingFramework\Test\Objects\TestObject; +use Magento\FunctionalTestingFramework\Util\Script\ScriptUtil; +use Symfony\Component\Console\Input\InputInterface; +use Exception; + +/** + * Class AnnotationsCheck + * @package Magento\FunctionalTestingFramework\StaticCheck + */ +class AnnotationsCheck implements StaticCheckInterface +{ + const ERROR_LOG_FILENAME = 'mftf-annotations-static-check'; + const ERROR_LOG_MESSAGE = 'MFTF Annotations Static Check'; + + /** + * Array containing all errors found after running the execute() function. + * @var array + */ + private $errors = []; + + /** + * String representing the output summary found after running the execute() function. + * @var string + */ + private $output; + + /** + * Array containing + * key = Story appended to Title + * value = test names that have that pair + * @var array + */ + private $storiesTitlePairs = []; + + /** + * Array containing + * key = testCaseId appended to Title + * value = test names that have that pair + * @var array + */ + private $testCaseIdTitlePairs = []; + + /** + * Validates test annotations + * + * @param InputInterface $input + * @return void + * @throws Exception + */ + public function execute(InputInterface $input) + { + // Set MFTF to the UNIT_TEST_PHASE to mute the default DEPRECATION warnings from the TestObjectHandler. + MftfApplicationConfig::create( + true, + MftfApplicationConfig::UNIT_TEST_PHASE, + false, + MftfApplicationConfig::LEVEL_DEFAULT, + true + ); + $allTests = TestObjectHandler::getInstance(false)->getAllObjects(); + + foreach ($allTests as $test) { + if ($this->validateSkipIssueId($test)) { + //if test is skipped ignore other checks + continue; + } + $this->validateRequiredAnnotations($test); + $this->aggregateStoriesTitlePairs($test); + $this->aggregateTestCaseIdTitlePairs($test); + } + + $this->validateStoriesTitlePairs(); + $this->validateTestCaseIdTitlePairs(); + + $scriptUtil = new ScriptUtil(); + $this->output = $scriptUtil->printErrorsToFile( + $this->errors, + StaticChecksList::getErrorFilesPath() . DIRECTORY_SEPARATOR . self::ERROR_LOG_FILENAME . '.txt', + self::ERROR_LOG_MESSAGE + ); + } + + /** + * Return array containing all errors found after running the execute() function. + * @return array + */ + public function getErrors() + { + return $this->errors; + } + + /** + * Return string of a short human readable result of the check. For example: "No Dependency errors found." + * @return string + */ + public function getOutput() + { + return $this->output; + } + + /** + * Validates that the test has the following annotations: + * stories + * title + * description + * severity + * + * @param TestObject $test + * @return void + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + private function validateRequiredAnnotations($test) + { + $annotations = $test->getAnnotations(); + $missing = []; + + $stories = $annotations['stories'] ?? null; + if ($stories === null || !isset($stories[0]) || empty(trim($stories[0]))) { + $missing[] = "stories"; + } + + $testCaseId = "[NO TESTCASEID]"; + if (isset($annotations['testCaseId'][0])) { + $testCaseId = trim($annotations['testCaseId'][0]); + } + + $title = $annotations['title'] ?? null; + if ($title === null + || !isset($title[0]) + || empty(trim($title[0])) + || empty(trim(substr(trim($title[0]), strlen($testCaseId . ': '))))) { + $missing[] = "title"; + } + + $description = $annotations['description']['main'] ?? null; + if ($description === null || empty(trim($description))) { + $missing[] = "description"; + } + + $severity = $annotations['severity'] ?? null; + if ($severity === null || !isset($severity[0]) || empty(trim($severity[0]))) { + $missing[] = "severity"; + } + + $allMissing = join(", ", $missing); + if (strlen($allMissing) > 0) { + $this->errors[][] = "Test {$test->getName()} is missing the required annotations: " . $allMissing; + } + } + + /** + * Validates that if the test is skipped, that it has an issueId value. + * + * @param TestObject $test + * @return boolean + */ + private function validateSkipIssueId($test) + { + $validateSkipped = false; + $annotations = $test->getAnnotations(); + + $skip = $annotations['skip'] ?? null; + if ($skip !== null) { + $validateSkipped = true; + if ((!isset($skip[0]) || strlen($skip[0]) === 0) + && (!isset($skip['issueId']) || strlen($skip['issueId']) === 0)) { + $this->errors[][] = "Test {$test->getName()} is skipped but the issueId is empty."; + } + } + return $validateSkipped; + } + + /** + * Add the key = "stories appended to title", value = test name, to the class variable. + * + * @param TestObject $test + * @return void + */ + private function aggregateStoriesTitlePairs($test) + { + $annotations = $test->getAnnotations(); + $stories = $annotations['stories'][0] ?? null; + $title = $this->getTestTitleWithoutPrefix($test); + if ($stories !== null && $title !== null) { + $this->storiesTitlePairs[$stories . $title][] = $test->getName(); + } + } + + /** + * Add the key = "testCaseId appended to title", value = test name, to the class variable. + * + * @param TestObject $test + * @return void + */ + private function aggregateTestCaseIdTitlePairs($test) + { + $annotations = $test->getAnnotations(); + $testCaseId = $annotations['testCaseId'][0] ?? null; + $title = $this->getTestTitleWithoutPrefix($test); + if ($testCaseId !== null && $title !== null) { + $this->testCaseIdTitlePairs[$testCaseId . $title][] = $test->getName(); + } + } + + /** + * Strip away the testCaseId prefix that was automatically added to the test title + * so that way we have just the raw title from the XML file. + * + * @param TestObject $test + * @return string|null + */ + private function getTestTitleWithoutPrefix($test) + { + $annotations = $test->getAnnotations(); + $title = $annotations['title'][0] ?? null; + if ($title === null) { + return null; + } else { + $testCaseId = $annotations['testCaseId'][0] ?? "[NO TESTCASEID]"; + return substr($title, strlen($testCaseId . ": ")); + } + } + + /** + * Adds an error if any story+title pairs are used by more than one test. + * + * @return void + */ + private function validateStoriesTitlePairs() + { + foreach ($this->storiesTitlePairs as $pair) { + if (sizeof($pair) > 1) { + $this->errors[][] = "Stories + title combination must be unique: " . join(", ", $pair); + } + } + } + + /** + * Adds an error if any testCaseId+title pairs are used by more than one test. + * + * @return void + */ + private function validateTestCaseIdTitlePairs() + { + foreach ($this->testCaseIdTitlePairs as $pair) { + if (sizeof($pair) > 1) { + $this->errors[][] = "testCaseId + title combination must be unique: " . join(", ", $pair); + } + } + } +} diff --git a/src/Magento/FunctionalTestingFramework/StaticCheck/ClassFileNamingCheck.php b/src/Magento/FunctionalTestingFramework/StaticCheck/ClassFileNamingCheck.php new file mode 100644 index 000000000..4ce3c83d3 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/StaticCheck/ClassFileNamingCheck.php @@ -0,0 +1,184 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\StaticCheck; + +use Symfony\Component\Console\Input\InputInterface; +use Exception; +use Magento\FunctionalTestingFramework\Util\Script\ScriptUtil; +use Symfony\Component\Finder\SplFileInfo; + +/** + * Class ClassFileNamingCheck + * @package Magento\FunctionalTestingFramework\StaticCheck + */ +class ClassFileNamingCheck implements StaticCheckInterface +{ + const ERROR_LOG_FILENAME = 'mftf-class-file-naming-check'; + const ERROR_LOG_MESSAGE = 'MFTF Class File Naming Check'; + const ALLOW_LIST_FILENAME = 'class-file-naming-allowlist'; + const WARNING_LOG_FILENAME = 'mftf-class-file-naming-warnings'; + + /** + * Array containing all warnings found after running the execute() function. + * @var array + */ + private $warnings = []; + /** + * Array containing all errors found after running the execute() function. + * @var array + */ + private $errors = []; + + /** + * String representing the output summary found after running the execute() function. + * @var string + */ + private $output; + + /** + * @var array $allowFailureEntities + */ + private $allowFailureEntities = []; + + /** + * ScriptUtil instance + * + * @var ScriptUtil + */ + private $scriptUtil; + /** + * Checks usage of pause action in action groups, tests and suites and prints out error to file. + * + * @param InputInterface $input + * @return void + * @throws Exception + */ + public function execute(InputInterface $input) + { + $this->scriptUtil = new ScriptUtil(); + $modulePaths = []; + $path = $input->getOption('path'); + if ($path) { + if (!realpath($path)) { + throw new \InvalidArgumentException('Invalid --path option: ' . $path); + } + $modulePaths[] = realpath($path); + } else { + $modulePaths = $this->scriptUtil->getAllModulePaths(); + } + foreach ($modulePaths as $modulePath) { + if (file_exists($modulePath . DIRECTORY_SEPARATOR . self::ALLOW_LIST_FILENAME)) { + $contents = file_get_contents($modulePath . DIRECTORY_SEPARATOR . self::ALLOW_LIST_FILENAME); + foreach (explode("\n", $contents) as $entity) { + $this->allowFailureEntities[$entity] = true; + } + } + } + $testXmlFiles = $this->scriptUtil->getModuleXmlFilesByScope($modulePaths, "Test"); + $actionGroupXmlFiles = $this->scriptUtil->getModuleXmlFilesByScope($modulePaths, "ActionGroup"); + $pageXmlFiles = $this->scriptUtil->getModuleXmlFilesByScope($modulePaths, "Page"); + $sectionXmlFiles = $this->scriptUtil->getModuleXmlFilesByScope($modulePaths, "Section"); + $suiteXmlFiles = $this->scriptUtil->getModuleXmlFilesByScope($modulePaths, 'Suite'); + $this->errors = []; + $this->errors += $this->findErrorsInFileSet($testXmlFiles, 'test'); + $this->errors += $this->findErrorsInFileSet($actionGroupXmlFiles, 'actionGroup'); + $this->errors += $this->findErrorsInFileSet($pageXmlFiles, 'page'); + $this->errors += $this->findErrorsInFileSet($sectionXmlFiles, 'section'); + $this->errors += $this->findErrorsInFileSet($suiteXmlFiles, 'suite'); + + // hold on to the output and print any errors to a file + $this->output = $this->scriptUtil->printErrorsToFile( + $this->errors, + StaticChecksList::getErrorFilesPath() . DIRECTORY_SEPARATOR . self::ERROR_LOG_FILENAME . '.txt', + self::ERROR_LOG_MESSAGE + ); + if (!empty($this->warnings) && !empty($this->errors)) { + $this->output .= "\n " . $this->scriptUtil->printWarningsToFile( + $this->warnings, + StaticChecksList::getErrorFilesPath() . DIRECTORY_SEPARATOR . self::WARNING_LOG_FILENAME . '.txt', + self::ERROR_LOG_MESSAGE + ); + } + } + + /** + * Return array containing all errors found after running the execute() function. + * @return array + */ + public function getErrors() + { + return $this->errors; + } + + /** + * Return string of a short human readable result of the check. For example: "No errors found." + * @return string + */ + public function getOutput() + { + return $this->output; + } + + /** + * Returns Violations if found + * @param SplFileInfo $files + * @param string $fileType + * @return array + */ + public function findErrorsInFileSet($files, $fileType) + { + $errors = []; + /** @var SplFileInfo $filePath */ + + foreach ($files as $filePath) { + $fileNameWithoutExtension = pathinfo($filePath->getFilename(), PATHINFO_FILENAME); + $domDocument = new \DOMDocument(); + $domDocument->load($filePath); + $testResult = $this->getAttributesFromDOMNodeList( + $domDocument->getElementsByTagName($fileType), + ["type" => 'name'] + ); + if ($fileNameWithoutExtension != array_values($testResult[0])[0]) { + $isInAllowList = array_key_exists(array_values($testResult[0])[0], $this->allowFailureEntities); + if ($isInAllowList) { + $errorOutput = ucfirst($fileType). " name does not match with file name + {$filePath->getRealPath()}. ".ucfirst($fileType)." ".array_values($testResult[0])[0]; + $this->warnings[$filePath->getFilename()][] = $errorOutput; + continue; + } + $errorOutput = ucfirst($fileType). " name does not match with file name + {$filePath->getRealPath()}. ".ucfirst($fileType)." ".array_values($testResult[0])[0]; + $errors[$filePath->getFilename()][] = $errorOutput; + } + } + return $errors; + } + + /** + * Return attribute value for each node in DOMNodeList as an array + * + * @param DOMNodeList $nodes + * @param string $attributeName + * @return array + */ + public function getAttributesFromDOMNodeList($nodes, $attributeName) + { + $attributes = []; + foreach ($nodes as $node) { + if (is_string($attributeName)) { + $attributeValue = $node->getAttribute($attributeName); + } else { + $attributeValue = [$node->getAttribute(key($attributeName)) => + $node->getAttribute($attributeName[key($attributeName)])]; + } + if (!empty($attributeValue)) { + $attributes[] = $attributeValue; + } + } + return $attributes; + } +} diff --git a/src/Magento/FunctionalTestingFramework/StaticCheck/CreatedDataFromOutsideActionGroupCheck.php b/src/Magento/FunctionalTestingFramework/StaticCheck/CreatedDataFromOutsideActionGroupCheck.php new file mode 100644 index 000000000..343e4dd8c --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/StaticCheck/CreatedDataFromOutsideActionGroupCheck.php @@ -0,0 +1,198 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\StaticCheck; + +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use InvalidArgumentException; +use Exception; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Exceptions\XmlException; +use Magento\FunctionalTestingFramework\Page\Objects\SectionObject; +use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; +use Magento\FunctionalTestingFramework\Page\Objects\ElementObject; +use Magento\FunctionalTestingFramework\Test\Objects\ActionGroupObject; +use Magento\FunctionalTestingFramework\Page\Objects\PageObject; +use Magento\FunctionalTestingFramework\Test\Objects\TestObject; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Finder\Finder; +use Magento\FunctionalTestingFramework\Util\Script\ScriptUtil; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\OperationDefinitionObjectHandler; +use Magento\FunctionalTestingFramework\DataGenerator\Objects\OperationDefinitionObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; +use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Symfony\Component\Finder\SplFileInfo; +use DOMNodeList; +use DOMElement; + +/** + * Class CreatedDataFromOutsideActionGroupCheck + * @package Magento\FunctionalTestingFramework\StaticCheck + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CreatedDataFromOutsideActionGroupCheck implements StaticCheckInterface +{ + const ACTIONGROUP_REGEX_PATTERN = '/\$(\$)*([\w.]+)(\$)*\$/'; + const ERROR_LOG_FILENAME = 'create-data-from-outside-action-group'; + const ERROR_MESSAGE = 'Created Data From Outside Action Group'; + + /** + * Array containing all errors found after running the execute() function + * + * @var array + */ + private $errors = []; + + /** + * String representing the output summary found after running the execute() function + * + * @var string + */ + private $output; + + /** + * ScriptUtil instance + * + * @var ScriptUtil + */ + private $scriptUtil; + + /** + * @var array + */ + private $actionGroupXmlFile = []; + + /** + * Checks test dependencies, determined by references in tests versus the dependencies listed in the Magento module + * + * @param InputInterface $input + * @return void + * @throws Exception + */ + public function execute(InputInterface $input) + { + $this->scriptUtil = new ScriptUtil(); + $this->loadAllXmlFiles($input); + $this->errors = []; + $this->errors += $this->findReferenceErrorsInActionFiles($this->actionGroupXmlFile); + // hold on to the output and print any errors to a file + $this->output = $this->scriptUtil->printErrorsToFile( + $this->errors, + StaticChecksList::getErrorFilesPath() . DIRECTORY_SEPARATOR . self::ERROR_LOG_FILENAME . '.txt', + self::ERROR_MESSAGE + ); + } + + /** + * Return array containing all errors found after running the execute() function + * + * @return array + */ + public function getErrors() + { + return $this->errors; + } + + /** + * Return string of a short human readable result of the check. For example: "No Dependency errors found." + * + * @return string + */ + public function getOutput() + { + return $this->output; + } + + /** + * Read all XML files for scanning + * + * @param InputInterface $input + * @return void + * @throws Exception + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + private function loadAllXmlFiles($input) + { + $modulePaths = []; + $path = $input->getOption('path'); + if ($path) { + if (!realpath($path)) { + throw new InvalidArgumentException('Invalid --path option: ' . $path); + } + MftfApplicationConfig::create( + true, + MftfApplicationConfig::UNIT_TEST_PHASE, + false, + MftfApplicationConfig::LEVEL_DEFAULT, + true + ); + $modulePaths[] = realpath($path); + } else { + $modulePaths = $this->scriptUtil->getAllModulePaths(); + } + + // These files can contain references to other entities + $this->actionGroupXmlFile = $this->scriptUtil->getModuleXmlFilesByScope($modulePaths, 'ActionGroup'); + + if (empty($this->actionGroupXmlFile)) { + if ($path) { + throw new InvalidArgumentException( + 'Invalid --path option: ' + . $path + . PHP_EOL + . 'Please make sure --path points to a valid MFTF Test Module.' + ); + } elseif (empty($this->rootSuiteXmlFiles)) { + throw new TestFrameworkException('No xml file to scan.'); + } + } + } + + /** + * Find reference errors in set of action files + * + * @param Finder $files + * @return array + * @throws XmlException + */ + private function findReferenceErrorsInActionFiles($files) + { + $testErrors = []; + /** @var SplFileInfo $filePath */ + foreach ($files as $filePath) { + $contents = file_get_contents($filePath); + preg_match_all(self::ACTIONGROUP_REGEX_PATTERN, $contents, $actionGroupReferences); + if (count($actionGroupReferences) > 0) { + $testErrors = array_merge($testErrors, $this->setErrorOutput($actionGroupReferences, $filePath)); + } + } + + return $testErrors; + } + + /** + * Build and return error output for violating references + * + * @param array $actionGroupReferences + * @param SplFileInfo $path + * @return mixed + */ + private function setErrorOutput($actionGroupReferences, $path) + { + $testErrors = []; + $errorOutput = ""; + $filePath = StaticChecksList::getFilePath($path->getRealPath()); + + foreach ($actionGroupReferences as $key => $actionGroupReferencesData) { + foreach ($actionGroupReferencesData as $actionGroupReferencesDataResult) { + $errorOutput .= "\nFile \"{$filePath}\" contains: ". "\n\t + {$actionGroupReferencesDataResult} in {$filePath}"; + $testErrors[$filePath][] = $errorOutput; + } + } + return $testErrors; + } +} diff --git a/src/Magento/FunctionalTestingFramework/StaticCheck/DeprecatedEntityUsageCheck.php b/src/Magento/FunctionalTestingFramework/StaticCheck/DeprecatedEntityUsageCheck.php new file mode 100644 index 000000000..547e6a633 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/StaticCheck/DeprecatedEntityUsageCheck.php @@ -0,0 +1,722 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\StaticCheck; + +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use InvalidArgumentException; +use Exception; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Exceptions\XmlException; +use Magento\FunctionalTestingFramework\Page\Objects\SectionObject; +use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; +use Magento\FunctionalTestingFramework\Page\Objects\ElementObject; +use Magento\FunctionalTestingFramework\Test\Objects\ActionGroupObject; +use Magento\FunctionalTestingFramework\Page\Objects\PageObject; +use Magento\FunctionalTestingFramework\Test\Objects\TestObject; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Finder\Finder; +use Magento\FunctionalTestingFramework\Util\Script\ScriptUtil; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\OperationDefinitionObjectHandler; +use Magento\FunctionalTestingFramework\DataGenerator\Objects\OperationDefinitionObject; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; +use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Symfony\Component\Finder\SplFileInfo; +use DOMNodeList; +use DOMElement; + +/** + * Class DeprecatedEntityUsageCheck + * @package Magento\FunctionalTestingFramework\StaticCheck + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class DeprecatedEntityUsageCheck implements StaticCheckInterface +{ + const EXTENDS_REGEX_PATTERN = '/extends=["\']([^\'"]*)/'; + const ACTIONGROUP_REGEX_PATTERN = '/ref=["\']([^\'"]*)/'; + const DEPRECATED_REGEX_PATTERN = '/deprecated=["\']([^\'"]*)/'; + + const ERROR_LOG_FILENAME = 'mftf-deprecated-entity-usage-checks'; + const ERROR_LOG_MESSAGE = 'MFTF Deprecated Entity Usage Check'; + + /** + * Array containing all errors found after running the execute() function + * + * @var array + */ + private $errors = []; + + /** + * String representing the output summary found after running the execute() function + * + * @var string + */ + private $output; + + /** + * ScriptUtil instance + * + * @var ScriptUtil + */ + private $scriptUtil; + + /** + * Data operations + * + * @var array + */ + private $dataOperations = ['create', 'update', 'get', 'delete']; + + /** + * Test xml files to scan + * + * @var Finder|array + */ + private $testXmlFiles = []; + + /** + * Action group xml files to scan + * + * @var Finder|array + */ + private $actionGroupXmlFiles = []; + + /** + * Suite xml files to scan + * + * @var Finder|array + */ + private $suiteXmlFiles = []; + + /** + * Root suite xml files to scan + * + * @var Finder|array + */ + private $rootSuiteXmlFiles = []; + + /** + * Data xml files to scan + * + * @var Finder|array + */ + private $dataXmlFiles = []; + + /** + * Checks test dependencies, determined by references in tests versus the dependencies listed in the Magento module + * + * @param InputInterface $input + * @return void + * @throws Exception + */ + public function execute(InputInterface $input) + { + $this->scriptUtil = new ScriptUtil(); + $this->loadAllXmlFiles($input); + + $this->errors = []; + $this->errors += $this->findReferenceErrorsInActionFiles($this->testXmlFiles); + $this->errors += $this->findReferenceErrorsInActionFiles($this->actionGroupXmlFiles); + $this->errors += $this->findReferenceErrorsInActionFiles($this->suiteXmlFiles, true); + if (!empty($this->rootSuiteXmlFiles)) { + $this->errors += $this->findReferenceErrorsInActionFiles($this->rootSuiteXmlFiles, true); + } + $this->errors += $this->findReferenceErrorsInDataFiles($this->dataXmlFiles); + + // Hold on to the output and print any errors to a file + $this->output = $this->scriptUtil->printErrorsToFile( + $this->errors, + StaticChecksList::getErrorFilesPath() . DIRECTORY_SEPARATOR . self::ERROR_LOG_FILENAME . '.txt', + self::ERROR_LOG_MESSAGE + ); + } + + /** + * Return array containing all errors found after running the execute() function + * + * @return array + */ + public function getErrors() + { + return $this->errors; + } + + /** + * Return string of a short human readable result of the check. For example: "No Dependency errors found." + * + * @return string + */ + public function getOutput() + { + return $this->output; + } + + /** + * Read all XML files for scanning + * + * @param InputInterface $input + * @return void + * @throws Exception + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + private function loadAllXmlFiles($input) + { + $modulePaths = []; + $includeRootPath = true; + $path = $input->getOption('path'); + if ($path) { + if (!realpath($path)) { + throw new InvalidArgumentException('Invalid --path option: ' . $path); + } + MftfApplicationConfig::create( + true, + MftfApplicationConfig::UNIT_TEST_PHASE, + false, + MftfApplicationConfig::LEVEL_DEFAULT, + true + ); + $modulePaths[] = realpath($path); + $includeRootPath = false; + } else { + $modulePaths = $this->scriptUtil->getAllModulePaths(); + } + + // These files can contain references to other entities + $this->testXmlFiles = $this->scriptUtil->getModuleXmlFilesByScope($modulePaths, 'Test'); + $this->actionGroupXmlFiles = $this->scriptUtil->getModuleXmlFilesByScope($modulePaths, 'ActionGroup'); + $this->suiteXmlFiles = $this->scriptUtil->getModuleXmlFilesByScope($modulePaths, 'Suite'); + if ($includeRootPath) { + $this->rootSuiteXmlFiles = $this->scriptUtil->getRootSuiteXmlFiles(); + } + $this->dataXmlFiles= $this->scriptUtil->getModuleXmlFilesByScope($modulePaths, 'Data'); + + if (empty($this->thistestXmlFiles) + && empty($this->actionGroupXmlFiles) + && empty($this->suiteXmlFiles) + && empty($this->dataXmlFiles) + ) { + if ($path) { + throw new InvalidArgumentException( + 'Invalid --path option: ' + . $path + . PHP_EOL + . 'Please make sure --path points to a valid MFTF Test Module.' + ); + } elseif (empty($this->rootSuiteXmlFiles)) { + throw new TestFrameworkException('No xml file to scan.'); + } + } + } + + /** + * Find reference errors in set of action files + * + * @param Finder $files + * @param boolean $checkTestRef + * @return array + * @throws XmlException + */ + private function findReferenceErrorsInActionFiles($files, $checkTestRef = false) + { + $testErrors = []; + /** @var SplFileInfo $filePath */ + foreach ($files as $filePath) { + $contents = file_get_contents($filePath); + if ($this->isDeprecated($contents)) { + continue; + } + preg_match_all(ActionObject::ACTION_ATTRIBUTE_VARIABLE_REGEX_PATTERN, $contents, $braceReferences); + preg_match_all(self::ACTIONGROUP_REGEX_PATTERN, $contents, $actionGroupReferences); + preg_match_all(self::EXTENDS_REGEX_PATTERN, $contents, $extendReferences); + + $domDocument = new \DOMDocument(); + $domDocument->load($filePath); + $createdDataReferences = $this->getAttributesFromDOMNodeList( + $domDocument->getElementsByTagName('createData'), + 'entity' + ); + $updatedDataReferences = $this->getAttributesFromDOMNodeList( + $domDocument->getElementsByTagName('updateData'), + 'entity' + ); + $getDataReferences = $this->getAttributesFromDOMNodeList( + $domDocument->getElementsByTagName('getData'), + 'entity' + ); + + // Remove Duplicates + $actionGroupReferences[1] = array_unique($actionGroupReferences[1]); + $extendReferences[1] = array_unique($extendReferences[1]); + $braceReferences[0] = array_unique($braceReferences[0]); + $braceReferences[2] = array_filter(array_unique($braceReferences[2])); + $createdDataReferences = array_unique($createdDataReferences); + $updatedDataReferences = array_unique($updatedDataReferences); + $getDataReferences = array_unique($getDataReferences); + + // Resolve entity references + $entityReferences = $this->scriptUtil->resolveEntityReferences($braceReferences[0], $contents, true); + // Resolve parameterized references + $entityReferences = array_merge( + $entityReferences, + $this->scriptUtil->resolveParametrizedReferences($braceReferences[2], $contents, true) + ); + // Resolve action group entity by names + $entityReferences = array_merge( + $entityReferences, + $this->scriptUtil->resolveEntityByNames($actionGroupReferences[1]) + ); + // Resolve extends entity by names + $entityReferences = array_merge( + $entityReferences, + $this->scriptUtil->resolveEntityByNames($extendReferences[1]) + ); + // Resolve create data entity by names + $entityReferences = array_merge( + $entityReferences, + $this->scriptUtil->resolveEntityByNames($createdDataReferences) + ); + // Resolve update data entity by names + $entityReferences = array_merge( + $entityReferences, + $this->scriptUtil->resolveEntityByNames($updatedDataReferences) + ); + // Resolve get data entity by names + $entityReferences = array_merge( + $entityReferences, + $this->scriptUtil->resolveEntityByNames($getDataReferences) + ); + // Find test references if needed + if ($checkTestRef) { + $entityReferences = array_merge($entityReferences, $this->resolveTestEntityInSuite($domDocument)); + } + + // Find violating references + $violatingReferences = $this->findViolatingReferences($entityReferences); + + // Find metadata references from persist data + $metadataReferences = $this->getMetadataFromData($createdDataReferences, 'create'); + $metadataReferences = array_merge_recursive( + $metadataReferences, + $this->getMetadataFromData($updatedDataReferences, 'update') + ); + $metadataReferences = array_merge_recursive( + $metadataReferences, + $this->getMetadataFromData($getDataReferences, 'get') + ); + + // Find violating references + $violatingReferences = array_merge( + $violatingReferences, + $this->findViolatingMetadataReferences($metadataReferences) + ); + + // Set error output + $testErrors = array_merge($testErrors, $this->setErrorOutput($violatingReferences, $filePath)); + } + return $testErrors; + } + + /** + * Checks if entity is deprecated in action files. + * @param string $contents + * @return boolean + */ + private function isDeprecated($contents) + { + preg_match_all(self::DEPRECATED_REGEX_PATTERN, $contents, $deprecatedEntity); + return (!empty($deprecatedEntity[1])); + } + + /** + * Find reference errors in a set of data files + * + * @param Finder $files + * @return array + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + private function findReferenceErrorsInDataFiles($files) + { + $testErrors = []; + /** @var SplFileInfo $filePath */ + foreach ($files as $filePath) { + $dataReferences = []; + $metadataReferences = []; + $domDocument = new \DOMDocument(); + $domDocument->load($filePath); + $entities = $domDocument->getElementsByTagName('entity'); + foreach ($entities as $entity) { + /** @var DOMElement $entity */ + $deprecated = $entity->getAttribute('deprecated'); + // skip check if entity is deprecated + if (!empty($deprecated)) { + continue; + } + $entityName = $entity->getAttribute('name'); + $metadataType = $entity->getAttribute('type'); + $parentEntityName = $entity->getAttribute('extends'); + if (!empty($metadataType && !isset($metadataReferences[$entityName][$metadataType]))) { + $metadataReferences[$entityName][$metadataType] = 'all'; + } + if (!empty($parentEntityName)) { + $dataReferences[$entityName][] = $parentEntityName; + } + // Find metadata reference in `var` elements + $varElements = $entity->getElementsByTagName('var'); + foreach ($varElements as $varElement) { + /** @var DOMElement $varElement */ + $metadataType = $varElement->getAttribute('entityType'); + if (!empty($metadataType) && !isset($metadataReferences[$entityName][$metadataType])) { + $metadataReferences[$entityName][$metadataType] = 'all'; + } + } + // Find metadata reference in `requiredEntity` elements, and + // Find data references in `requiredEntity` elements + $requiredElements = $entity->getElementsByTagName('requiredEntity'); + foreach ($requiredElements as $requiredElement) { + /** @var DOMElement $requiredElement */ + $metadataType = $requiredElement->getAttribute('type'); + if (!empty($metadataType) && !isset($metadataReferences[$entityName][$metadataType])) { + $metadataReferences[$entityName][$metadataType] = 'all'; + } + $dataReferences[$entityName][] = $requiredElement->nodeValue; + } + } + + // Find violating references + // Metadata references is unique + $violatingReferences = $this->findViolatingMetadataReferences($metadataReferences); + // Data references is not unique + $violatingReferences = array_merge_recursive( + $violatingReferences, + $this->findViolatingDataReferences($this->twoDimensionArrayUnique($dataReferences)) + ); + + // Set error output + $testErrors = array_merge($testErrors, $this->setErrorOutput($violatingReferences, $filePath)); + } + return $testErrors; + } + + /** + * Trim duplicate values from two-dimensional array. Dimension 1 array key is unique. + * + * @param array $inArray + * @return array + */ + private function twoDimensionArrayUnique($inArray) + { + $outArray = []; + foreach ($inArray as $key => $arr) { + $outArray[$key] = array_unique($arr); + } + return $outArray; + } + + /** + * Return attribute value for each node in DOMNodeList as an array + * + * @param DOMNodeList $nodes + * @param string $attributeName + * @return array + */ + private function getAttributesFromDOMNodeList($nodes, $attributeName) + { + $attributes = []; + /** @var DOMElement $node */ + foreach ($nodes as $node) { + $attributeValue = $node->getAttribute($attributeName); + if (!empty($attributeValue)) { + $attributes[] = $attributeValue; + } + } + return $attributes; + } + + /** + * Find metadata from data array + * + * @param array $references + * @param string $type + * @return array + */ + private function getMetadataFromData($references, $type) + { + $metaDataReferences = []; + try { + foreach ($references as $dataName) { + /** @var EntityDataObject $dataEntity */ + $dataEntity = $this->scriptUtil->findEntity($dataName); + if ($dataEntity) { + $metadata = $dataEntity->getType(); + if (!empty($metadata)) { + $metaDataReferences[$dataName][$metadata] = $type; + } + } + } + } catch (Exception $e) { + } + return $metaDataReferences; + } + + /** + * Find violating metadata references. Input array format is either + * + * [ + * $dataName1 => [ + * $metaDataName1 = [ + * 'create', + * 'update', + * ], + * ], + * $dataName2 => [ + * $metaDataName2 => 'create', + * ], + * $dataName3 => [ + * $metaDataName3 = [ + * 'get', + * 'create', + * 'update', + * ], + * ], + * ... + * ] + * + * or + * + * [ + * $dataName1 => [ + * $metaDataName1 => 'all', + * $metaDataName2 => 'all', + * ... + * ], + * $dataName2 => [ + * $metaDataName2 => 'all', + * ... + * ], + * $dataName5 => [ + * $metaDataName5 => 'all', + * $metaDataName4 => 'all', + * $metaDataName1 => 'all', + * ... + * ], + * ... + * ] + * + * @param array $references + * @return array + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + private function findViolatingMetadataReferences($references) + { + $allObjects = OperationDefinitionObjectHandler::getInstance()->getAllObjects(); + + // Find Violations + $violatingReferences = []; + foreach ($references as $dataName => $metadataArray) { + foreach ($metadataArray as $metadata => $types) { + $operations = []; + $strict = true; + if (is_array($types)) { + $operations = $types; + } elseif ($types === 'all') { + $operations = $this->dataOperations; + $strict = false; + } else { + $operations = [$types]; + } + + $deprecated = null; + $exists = false; + foreach ($operations as $operation) { + if (array_key_exists($operation . $metadata, $allObjects)) { + $exists = true; + /** @var OperationDefinitionObject $entity */ + $entity = $allObjects[$operation . $metadata]; + // When not strictly checking, it's not deprecated as long as we found one that's not deprecated + if (!$strict && empty($entity->getDeprecated())) { + $deprecated = false; + break; + } + // When strictly checking, it's deprecated as long as we found one that's deprecated + if ($strict && !empty($entity->getDeprecated())) { + $deprecated = true; + break; + } + } + } + if ($exists && !$strict && $deprecated !== false) { + $deprecated = true; + } + if ($strict && $deprecated !== true) { + $deprecated = false; + } + + if ($deprecated) { + $violatingReferences["\"{$dataName}\" references deprecated"][] = [ + 'name' => $metadata, + // TODO add filename in OperationDefinitionObject + 'file' => 'metadata xml file', + ]; + } + } + } + return $violatingReferences; + } + + /** + * Find violating data references. Input array format is + * + * [ + * $dataName1 => [ $requiredDataName1, $requiredDataName2, $requiredDataName3], + * $dataName2 => [ $requiredDataName2, $requiredDataName5, $requiredDataName7], + * ... + * ] + * + * @param array $references + * @return array + */ + private function findViolatingDataReferences($references) + { + // Find Violations + $violatingReferences = []; + foreach ($references as $dataName => $requiredDataNames) { + foreach ($requiredDataNames as $requiredDataName) { + try { + /** @var EntityDataObject $requiredData */ + $requiredData = DataObjectHandler::getInstance()->getObject($requiredDataName); + if ($requiredData && $requiredData->getDeprecated()) { + $violatingReferences["\"{$dataName}\" references deprecated"][] = [ + 'name' => $requiredDataName, + 'file' => $requiredData->getFilename(), + ]; + } + } catch (Exception $e) { + } + } + } + return $violatingReferences; + } + + /** + * Find violating references. Input array format is + * + * [ + * 'actionGroupName' => $actionGroupEntity, + * 'dataGroupName' => $dataEntity, + * 'testName' => $testEntity, + * 'pageName' => $pageEntity, + * 'section.field' => $fieldElementEntity, + * ... + * ] + * + * @param array $references + * @return array + */ + private function findViolatingReferences($references) + { + // Find Violations + $violatingReferences = []; + foreach ($references as $key => $entity) { + if ($entity->getDeprecated()) { + $classType = get_class($entity); + $name = $entity->getName(); + if ($classType === ElementObject::class) { + $name = $key; + list($section,) = explode('.', $key, 2); + /** @var SectionObject $references[$section] */ + $file = StaticChecksList::getFilePath($references[$section]->getFilename()); + } else { + $file = StaticChecksList::getFilePath($entity->getFilename()); + } + $violatingReferences[$this->getSubjectFromClassType($classType)][] = [ + 'name' => $name, + 'file' => $file, + ]; + } + } + return $violatingReferences; + } + + /** + * Build and return error output for violating references + * + * @param array $violatingReferences + * @param SplFileInfo $path + * @return mixed + */ + private function setErrorOutput($violatingReferences, $path) + { + $testErrors = []; + + $filePath = StaticChecksList::getFilePath($path->getRealPath()); + + if (!empty($violatingReferences)) { + // Build error output + $errorOutput = "\nFile \"{$filePath}\" contains:\n"; + foreach ($violatingReferences as $subject => $data) { + $errorOutput .= "\t- {$subject}:\n"; + foreach ($data as $item) { + $errorOutput .= "\t\t\"" . $item['name'] . "\" in " . $item['file'] . "\n"; + } + } + $testErrors[$filePath][] = $errorOutput; + } + + return $testErrors; + } + + /** + * Resolve test entity in suite + * + * @param \DOMDocument $domDocument + * @return array + */ + private function resolveTestEntityInSuite($domDocument) + { + $testReferences = $this->getAttributesFromDOMNodeList( + $domDocument->getElementsByTagName('test'), + 'name' + ); + + // Remove Duplicates + $testReferences = array_unique($testReferences); + + // Resolve test entity by names + try { + return $this->scriptUtil->resolveEntityByNames($testReferences); + } catch (XmlException $e) { + return []; + } + } + + /** + * Return subject string for a class name + * + * @param string $classname + * @return string|null + */ + private function getSubjectFromClassType($classname) + { + $subject = null; + if ($classname === ActionGroupObject::class) { + $subject = 'Deprecated ActionGroup(s)'; + } elseif ($classname === TestObject::class) { + $subject = 'Deprecated Test(s)'; + } elseif ($classname === SectionObject::class) { + $subject = 'Deprecated Section(s)'; + } elseif ($classname === PageObject::class) { + $subject = 'Deprecated Page(s)'; + } elseif ($classname === ElementObject::class) { + $subject = 'Deprecated Element(s)'; + } elseif ($classname === EntityDataObject::class) { + $subject = 'Deprecated Data(s)'; + } elseif ($classname === OperationDefinitionObject::class) { + $subject = 'Deprecated Metadata(s)'; + } + return $subject; + } +} diff --git a/src/Magento/FunctionalTestingFramework/StaticCheck/PauseActionUsageCheck.php b/src/Magento/FunctionalTestingFramework/StaticCheck/PauseActionUsageCheck.php new file mode 100644 index 000000000..e7d106bf0 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/StaticCheck/PauseActionUsageCheck.php @@ -0,0 +1,229 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\StaticCheck; + +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Finder\Finder; +use Exception; +use Magento\FunctionalTestingFramework\Util\Script\ScriptUtil; +use Symfony\Component\Finder\SplFileInfo; + +/** + * Class PauseActionUsageCheck + * @package Magento\FunctionalTestingFramework\StaticCheck + */ +class PauseActionUsageCheck implements StaticCheckInterface +{ + const ERROR_LOG_FILENAME = 'mftf-pause-action-usage-checks'; + const ERROR_LOG_MESSAGE = 'MFTF Pause Action Usage Check'; + + /** + * Array containing all errors found after running the execute() function. + * @var array + */ + private $errors = []; + + /** + * String representing the output summary found after running the execute() function. + * @var string + */ + private $output; + + /** + * ScriptUtil instance + * + * @var ScriptUtil + */ + private $scriptUtil; + + /** + * Test xml files to scan + * + * @var Finder|array + */ + private $testXmlFiles = []; + + /** + * Action group xml files to scan + * + * @var Finder|array + */ + private $actionGroupXmlFiles = []; + + /** + * Suite xml files to scan + * + * @var Finder|array + */ + private $suiteXmlFiles = []; + + /** + * Root suite xml files to scan + * + * @var Finder|array + */ + private $rootSuiteXmlFiles = []; + + /** + * Checks usage of pause action in action groups, tests and suites and prints out error to file. + * + * @param InputInterface $input + * @return void + * @throws Exception + */ + public function execute(InputInterface $input) + { + $this->scriptUtil = new ScriptUtil(); + $modulePaths = []; + $includeRootPath = true; + $path = $input->getOption('path'); + if ($path) { + if (!realpath($path)) { + throw new \InvalidArgumentException('Invalid --path option: ' . $path); + } + $modulePaths[] = realpath($path); + $includeRootPath = false; + } else { + $modulePaths = $this->scriptUtil->getAllModulePaths(); + } + + $this->testXmlFiles = $this->scriptUtil->getModuleXmlFilesByScope($modulePaths, 'Test'); + $this->actionGroupXmlFiles = $this->scriptUtil->getModuleXmlFilesByScope($modulePaths, 'ActionGroup'); + $this->suiteXmlFiles = $this->scriptUtil->getModuleXmlFilesByScope($modulePaths, 'Suite'); + if ($includeRootPath) { + $this->rootSuiteXmlFiles = $this->scriptUtil->getRootSuiteXmlFiles(); + } + $this->errors = []; + $this->errors += $this->validatePauseActionUsageInActionGroups($this->actionGroupXmlFiles); + $this->errors += $this->validatePauseActionUsageInTests($this->testXmlFiles); + $this->errors += $this->validatePauseActionUsageInSuites($this->suiteXmlFiles); + $this->errors += $this->validatePauseActionUsageInSuites($this->rootSuiteXmlFiles); + + $this->output = $this->scriptUtil->printErrorsToFile( + $this->errors, + StaticChecksList::getErrorFilesPath() . DIRECTORY_SEPARATOR . self::ERROR_LOG_FILENAME . '.txt', + self::ERROR_LOG_MESSAGE + ); + } + + /** + * Finds usages of pause action in action group files + * @param array $actionGroupXmlFiles + * @return array + */ + private function validatePauseActionUsageInActionGroups($actionGroupXmlFiles) + { + $actionGroupErrors = []; + foreach ($actionGroupXmlFiles as $filePath) { + $domDocument = new \DOMDocument(); + $domDocument->load($filePath); + $actionGroup = $domDocument->getElementsByTagName('actionGroup')->item(0); + $violatingStepKeys = $this->findViolatingPauseStepKeys($actionGroup); + $actionGroupErrors = array_merge($actionGroupErrors, $this->setErrorOutput($violatingStepKeys, $filePath)); + } + return $actionGroupErrors; + } + + /** + * Finds usages of pause action in test files + * @param array $testXmlFiles + * @return array + */ + private function validatePauseActionUsageInTests($testXmlFiles) + { + $testErrors = []; + foreach ($testXmlFiles as $filePath) { + $domDocument = new \DOMDocument(); + $domDocument->load($filePath); + $test = $domDocument->getElementsByTagName('test')->item(0); + $violatingStepKeys = $this->findViolatingPauseStepKeys($test); + $testErrors = array_merge($testErrors, $this->setErrorOutput($violatingStepKeys, $filePath)); + } + return $testErrors; + } + + /** + * Finds usages of pause action in suite files + * @param array $suiteXmlFiles + * @return array + */ + private function validatePauseActionUsageInSuites($suiteXmlFiles) + { + $suiteErrors = []; + foreach ($suiteXmlFiles as $filePath) { + $domDocument = new \DOMDocument(); + $domDocument->load($filePath); + $suite = $domDocument->getElementsByTagName('suite')->item(0); + $violatingStepKeys = $this->findViolatingPauseStepKeys($suite); + $suiteErrors = array_merge($suiteErrors, $this->setErrorOutput($violatingStepKeys, $filePath)); + } + return $suiteErrors; + } + + /** + * Finds violating pause action step keys + * @param \DomNode $entity + * @return array + */ + private function findViolatingPauseStepKeys($entity) + { + $violatingStepKeys = []; + $entityName = $entity->getAttribute('name'); + $references = $entity->getElementsByTagName('pause'); + + foreach ($references as $reference) { + $pauseStepKey = $reference->getAttribute('stepKey'); + $violatingStepKeys[$entityName][] = $pauseStepKey; + } + return $violatingStepKeys; + } + + /** + * Return array containing all errors found after running the execute() function. + * @return array + */ + public function getErrors() + { + return $this->errors; + } + + /** + * Return string of a short human readable result of the check. For example: "No errors found." + * @return string + */ + public function getOutput() + { + return $this->output; + } + + /** + * Build and return error output for pause action usages + * + * @param array $violatingReferences + * @param SplFileInfo $path + * @return mixed + */ + private function setErrorOutput($violatingReferences, $path) + { + $testErrors = []; + + $filePath = StaticChecksList::getFilePath($path->getRealPath()); + + if (!empty($violatingReferences)) { + // Build error output + $errorOutput = "\nFile \"{$filePath}\""; + $errorOutput .= "\ncontains pause action(s):\n\t\t"; + foreach ($violatingReferences as $entityName => $stepKey) { + $errorOutput .= "\n\t {$entityName} has pause action at stepKey(s): " . implode(", ", $stepKey); + } + $testErrors[$filePath][] = $errorOutput; + } + return $testErrors; + } +} diff --git a/src/Magento/FunctionalTestingFramework/StaticCheck/StaticChecksList.php b/src/Magento/FunctionalTestingFramework/StaticCheck/StaticChecksList.php index ffa63389d..0cb6b4a73 100644 --- a/src/Magento/FunctionalTestingFramework/StaticCheck/StaticChecksList.php +++ b/src/Magento/FunctionalTestingFramework/StaticCheck/StaticChecksList.php @@ -7,12 +7,24 @@ namespace Magento\FunctionalTestingFramework\StaticCheck; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Util\Path\FilePathFormatter; + /** * Class StaticChecksList has a list of static checks to run on test xml * @codingStandardsIgnoreFile */ class StaticChecksList implements StaticCheckListInterface { + const DEPRECATED_ENTITY_USAGE_CHECK_NAME = 'deprecatedEntityUsage'; + const PAUSE_ACTION_USAGE_CHECK_NAME = 'pauseActionUsage'; + const CREATED_DATA_FROM_OUTSIDE_ACTIONGROUP = 'createdDataFromOutsideActionGroup'; + const UNUSED_ENTITY_CHECK = 'unusedEntityCheck'; + + const CLASS_FILE_NAMING_CHECK = 'classFileNamingCheck'; + + const STATIC_RESULTS = 'tests' . DIRECTORY_SEPARATOR .'_output' . DIRECTORY_SEPARATOR . 'static-results'; + /** * Property contains all static check scripts. * @@ -20,17 +32,37 @@ class StaticChecksList implements StaticCheckListInterface */ private $checks; + /** + * Directory path for static checks error files + * + * @var string + */ + private static $errorFilesPath = null; + /** * Constructor * * @param array $checks + * @throws TestFrameworkException */ public function __construct(array $checks = []) { $this->checks = [ 'testDependencies' => new TestDependencyCheck(), - 'actionGroupArguments' => new ActionGroupArgumentsCheck(), - ] + $checks; + 'actionGroupArguments' => new ActionGroupStandardsCheck(), + self::DEPRECATED_ENTITY_USAGE_CHECK_NAME => new DeprecatedEntityUsageCheck(), + 'annotations' => new AnnotationsCheck(), + self::PAUSE_ACTION_USAGE_CHECK_NAME => new PauseActionUsageCheck(), + self::UNUSED_ENTITY_CHECK => new UnusedEntityCheck(), + self::CREATED_DATA_FROM_OUTSIDE_ACTIONGROUP => new CreatedDataFromOutsideActionGroupCheck(), + self::CLASS_FILE_NAMING_CHECK => new ClassFileNamingCheck(), + + ] + $checks; + + // Static checks error files directory + if (null === self::$errorFilesPath) { + self::$errorFilesPath = FilePathFormatter::format(TESTS_BP) . self::STATIC_RESULTS; + } } /** @@ -40,4 +72,30 @@ public function getStaticChecks() { return $this->checks; } + + /** + * Return the directory path for the static check error files + */ + public static function getErrorFilesPath() + { + return self::$errorFilesPath; + } + + /** + * Return relative path to files for unit testing purposes. + * @param string $fileNames + * @return string + */ + public static function getFilePath($fileNames) + { + if (!empty($fileNames)) { + $relativeFileNames = ltrim( + str_replace(MAGENTO_BP, '', $fileNames) + ); + if (!empty($relativeFileNames)) { + return $relativeFileNames; + } + } + return $fileNames; + } } diff --git a/src/Magento/FunctionalTestingFramework/StaticCheck/TestDependencyCheck.php b/src/Magento/FunctionalTestingFramework/StaticCheck/TestDependencyCheck.php index bc1b06cc5..01660c427 100644 --- a/src/Magento/FunctionalTestingFramework/StaticCheck/TestDependencyCheck.php +++ b/src/Magento/FunctionalTestingFramework/StaticCheck/TestDependencyCheck.php @@ -6,40 +6,29 @@ namespace Magento\FunctionalTestingFramework\StaticCheck; -use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; -use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; -use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; use Magento\FunctionalTestingFramework\Exceptions\XmlException; -use Magento\FunctionalTestingFramework\Page\Handlers\PageObjectHandler; -use Magento\FunctionalTestingFramework\Page\Handlers\SectionObjectHandler; -use Magento\FunctionalTestingFramework\Test\Handlers\ActionGroupObjectHandler; -use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; -use Magento\FunctionalTestingFramework\Util\TestGenerator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Finder\Finder; use Exception; use Magento\FunctionalTestingFramework\Util\Script\ScriptUtil; +use Magento\FunctionalTestingFramework\Util\Script\TestDependencyUtil; /** * Class TestDependencyCheck * @package Magento\FunctionalTestingFramework\StaticCheck - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class TestDependencyCheck implements StaticCheckInterface { const EXTENDS_REGEX_PATTERN = '/extends=["\']([^\'"]*)/'; const ACTIONGROUP_REGEX_PATTERN = '/ref=["\']([^\'"]*)/'; - const ACTIONGROUP_ARGUMENT_REGEX_PATTERN = '/<argument[^\/>]*name="([^"\']*)/'; - const ERROR_LOG_FILENAME = 'mftf-dependency-checks'; + const ERROR_LOG_FILENAME = 'mftf-dependency-checks-errors'; const ERROR_LOG_MESSAGE = 'MFTF File Dependency Check'; + const WARNING_LOG_FILENAME = 'mftf-dependency-checks-warnings'; - /** - * Array of FullModuleName => [dependencies] - * @var array - */ - private $allDependencies; + const ALLOW_LIST_FILENAME = 'test-dependency-allowlist'; /** * Array of FullModuleName => [dependencies], including flattened dependency tree @@ -60,16 +49,21 @@ class TestDependencyCheck implements StaticCheckInterface private $moduleNameToComposerName; /** - * Transactional Array to keep track of what dependencies have already been extracted. + * Array containing all errors found after running the execute() function. * @var array */ - private $alreadyExtractedDependencies; + private $errors = []; /** - * Array containing all errors found after running the execute() function. + * Array containing all warnings found after running the execute() function. * @var array */ - private $errors = []; + private $warnings = []; + /** + * Array containing warnings found while iterating through files + * @var array + */ + private $tempWarnings = []; /** * String representing the output summary found after running the execute() function. @@ -83,24 +77,62 @@ class TestDependencyCheck implements StaticCheckInterface */ private $allEntities = []; + /** + * ScriptUtil instance + * + * @var ScriptUtil + */ + private $scriptUtil; + + /** + * @var TestDependencyUtil + */ + private $testDependencyUtil; + + /** + * @var array $allowFailureEntities + */ + private $allowFailureEntities = []; + /** * Checks test dependencies, determined by references in tests versus the dependencies listed in the Magento module * * @param InputInterface $input - * @return string - * @throws Exception; + * @return void + * @throws Exception */ public function execute(InputInterface $input) { - $allModules = ScriptUtil::getAllModulePaths(); + $this->scriptUtil = new ScriptUtil(); + $this->testDependencyUtil = new TestDependencyUtil(); + $allModules = $this->scriptUtil->getAllModulePaths(); if (!class_exists('\Magento\Framework\Component\ComponentRegistrar')) { - return "TEST DEPENDENCY CHECK ABORTED: MFTF must be attached or pointing to Magento codebase."; + throw new TestFrameworkException( + "TEST DEPENDENCY CHECK ABORTED: MFTF must be attached or pointing to Magento codebase." + ); + } + + // Build array of entities found in allow-list files + // Expect one entity per file line, no commas or anything else + foreach ($allModules as $modulePath) { + if (file_exists($modulePath . DIRECTORY_SEPARATOR . self::ALLOW_LIST_FILENAME)) { + $contents = file_get_contents($modulePath . DIRECTORY_SEPARATOR . self::ALLOW_LIST_FILENAME); + foreach (explode("\n", $contents) as $entity) { + $this->allowFailureEntities[$entity] = true; + } + } } + $registrar = new \Magento\Framework\Component\ComponentRegistrar(); $this->moduleNameToPath = $registrar->getPaths(\Magento\Framework\Component\ComponentRegistrar::MODULE); - $this->moduleNameToComposerName = $this->buildModuleNameToComposerName($this->moduleNameToPath); - $this->flattenedDependencies = $this->buildComposerDependencyList(); + $this->moduleNameToComposerName = $this->testDependencyUtil->buildModuleNameToComposerName( + $this->moduleNameToPath + ); + $this->flattenedDependencies = $this->testDependencyUtil->buildComposerDependencyList( + $this->moduleNameToPath, + $this->moduleNameToComposerName + ); $filePaths = [ DIRECTORY_SEPARATOR . 'Test' . DIRECTORY_SEPARATOR, @@ -108,9 +140,9 @@ public function execute(InputInterface $input) DIRECTORY_SEPARATOR . 'Data' . DIRECTORY_SEPARATOR, ]; // These files can contain references to other modules. - $testXmlFiles = ScriptUtil::getModuleXmlFilesByScope($allModules, $filePaths[0]); - $actionGroupXmlFiles = ScriptUtil::getModuleXmlFilesByScope($allModules, $filePaths[1]); - $dataXmlFiles= ScriptUtil::getModuleXmlFilesByScope($allModules, $filePaths[2]); + $testXmlFiles = $this->scriptUtil->getModuleXmlFilesByScope($allModules, $filePaths[0]); + $actionGroupXmlFiles = $this->scriptUtil->getModuleXmlFilesByScope($allModules, $filePaths[1]); + $dataXmlFiles= $this->scriptUtil->getModuleXmlFilesByScope($allModules, $filePaths[2]); $this->errors = []; $this->errors += $this->findErrorsInFileSet($testXmlFiles); @@ -118,18 +150,25 @@ public function execute(InputInterface $input) $this->errors += $this->findErrorsInFileSet($dataXmlFiles); // hold on to the output and print any errors to a file - $this->output = ScriptUtil::printErrorsToFile( + $this->output = $this->scriptUtil->printErrorsToFile( $this->errors, - self::ERROR_LOG_FILENAME, + StaticChecksList::getErrorFilesPath() . DIRECTORY_SEPARATOR . self::ERROR_LOG_FILENAME . '.txt', self::ERROR_LOG_MESSAGE ); + if (!empty($this->warnings) && !empty($this->errors)) { + $this->output .= "\n " . $this->scriptUtil->printWarningsToFile( + $this->warnings, + StaticChecksList::getErrorFilesPath() . DIRECTORY_SEPARATOR . self::WARNING_LOG_FILENAME . '.txt', + self::ERROR_LOG_MESSAGE + ); + } } /** * Return array containing all errors found after running the execute() function. * @return array */ - public function getErrors() + public function getErrors(): array { return $this->errors; } @@ -138,26 +177,25 @@ public function getErrors() * Return string of a short human readable result of the check. For example: "No Dependency errors found." * @return string */ - public function getOutput() + public function getOutput(): string { - return $this->output; + return $this->output??""; } /** * Finds all reference errors in given set of files * @param Finder $files * @return array - * @throws TestReferenceException * @throws XmlException */ - private function findErrorsInFileSet($files) + private function findErrorsInFileSet(Finder $files): array { $testErrors = []; foreach ($files as $filePath) { - $modulePath = dirname(dirname(dirname(dirname($filePath)))); - $moduleFullName = array_search($modulePath, $this->moduleNameToPath) ?? null; + $this->allEntities = []; + $moduleName = $this->testDependencyUtil->getModuleName($filePath, $this->moduleNameToPath); // Not a module, is either dev/tests/acceptance or loose folder with test materials - if ($moduleFullName == null) { + if ($moduleName === null) { continue; } @@ -172,130 +210,69 @@ private function findErrorsInFileSet($files) $braceReferences[1] = array_unique($braceReferences[1]); $braceReferences[2] = array_filter(array_unique($braceReferences[2])); - // resolve data entity references - $this->resolveDataEntityReferences($braceReferences[0], $contents); + // resolve entity references + $this->allEntities = array_merge( + $this->allEntities, + $this->scriptUtil->resolveEntityReferences($braceReferences[0], $contents) + ); - //resolve entity references - $this->resolveParametrizedReferences($braceReferences[2], $contents); + // resolve parameterized references + $this->allEntities = array_merge( + $this->allEntities, + $this->scriptUtil->resolveParametrizedReferences($braceReferences[2], $contents) + ); - // Check actionGroup references - $this->resolveEntityReferences($actionGroupReferences[1]); + // resolve entity by names + $this->allEntities = array_merge( + $this->allEntities, + $this->scriptUtil->resolveEntityByNames($actionGroupReferences[1]) + ); - // Check extended objects - $this->resolveEntityReferences($extendReferences[1]); + // resolve entity by names + $this->allEntities = array_merge( + $this->allEntities, + $this->scriptUtil->resolveEntityByNames($extendReferences[1]) + ); // Find violating references and set error output - $violatingReferences = $this->findViolatingReferences($moduleFullName); - $testErrors = $this->setErrorOutput($violatingReferences, $filePath); + $violatingReferences = $this->findViolatingReferences($moduleName); + $testErrors = array_merge($testErrors, $this->setErrorOutput($violatingReferences, $filePath)); + $this->warnings = array_merge($this->warnings, $this->setErrorOutput($this->tempWarnings, $filePath)); } return $testErrors; } - /** - * Drill down into params in {{ref.params('string', $data.key$, entity.reference)}} - * and resolve references. - * - * @param array $braceReferences - * @param string $contents - * @return void - * @throws XmlException - */ - private function resolveParametrizedReferences($braceReferences, $contents) - { - foreach ($braceReferences as $parameterizedReference) { - preg_match( - ActionObject::ACTION_ATTRIBUTE_VARIABLE_REGEX_PARAMETER, - $parameterizedReference, - $arguments - ); - $splitArguments = explode(',', ltrim(rtrim($arguments[0], ")"), "(")); - foreach ($splitArguments as $argument) { - // Do nothing for 'string' or $persisted.data$ - if (preg_match(ActionObject::STRING_PARAMETER_REGEX, $argument)) { - continue; - } elseif (preg_match(TestGenerator::PERSISTED_OBJECT_NOTATION_REGEX, $argument)) { - continue; - } - // trim `data.field` to `data` - preg_match('/([^.]+)/', $argument, $entityName); - // Double check that {{data.field}} isn't an argument for an ActionGroup - $entity = $this->findEntity($entityName[1]); - preg_match_all(self::ACTIONGROUP_ARGUMENT_REGEX_PATTERN, $contents, $possibleArgument); - if (array_search($entityName[1], $possibleArgument[1]) !== false) { - continue; - } - if ($entity !== null) { - $this->allEntities[$entity->getName()] = $entity; - } - } - } - } - - /** - * Check `data` entities in {{data.field}} or {{data.field('param')}} and resolve references - * - * @param array $braceReferences - * @param string $contents - * @return void - * @throws XmlException - - */ - private function resolveDataEntityReferences($braceReferences, $contents) - { - foreach ($braceReferences as $reference) { - // trim `{{data.field}}` to `data` - preg_match('/{{([^.]+)/', $reference, $entityName); - // Double check that {{data.field}} isn't an argument for an ActionGroup - $entity = $this->findEntity($entityName[1]); - preg_match_all(self::ACTIONGROUP_ARGUMENT_REGEX_PATTERN, $contents, $possibleArgument); - if (array_search($entityName[1], $possibleArgument[1]) !== false) { - continue; - } - if ($entity !== null) { - $this->allEntities[$entity->getName()] = $entity; - } - } - } - - /** - * Resolve entity references - * - * @param array $references - * @return void - * @throws XmlException - */ - private function resolveEntityReferences($references) - { - foreach ($references as $reference) { - $entity = $this->findEntity($reference); - if ($entity !== null) { - $this->allEntities[$entity->getName()] = $entity; - } - } - } - /** * Find violating references * * @param string $moduleName * @return array */ - private function findViolatingReferences($moduleName) + private function findViolatingReferences(string $moduleName): array { // Find Violations $violatingReferences = []; $currentModule = $this->moduleNameToComposerName[$moduleName]; - $modulesReferencedInTest = $this->getModuleDependenciesFromReferences($this->allEntities); + $modulesReferencedInTest = $this->testDependencyUtil->getModuleDependenciesFromReferences( + $this->allEntities, + $this->moduleNameToComposerName, + $this->moduleNameToPath + ); $moduleDependencies = $this->flattenedDependencies[$moduleName]; foreach ($modulesReferencedInTest as $entityName => $files) { + $isInAllowList = array_key_exists($entityName, $this->allowFailureEntities); $valid = false; foreach ($files as $module) { - if (array_key_exists($module, $moduleDependencies) || $module == $currentModule) { + if (array_key_exists($module, $moduleDependencies) || $module === $currentModule) { $valid = true; break; } } if (!$valid) { + if ($isInAllowList) { + $this->tempWarnings[$entityName] = $files; + continue; + } $violatingReferences[$entityName] = $files; } } @@ -306,11 +283,10 @@ private function findViolatingReferences($moduleName) /** * Builds and returns error output for violating references * - * @param array $violatingReferences - * @param string $path - * @return mixed + * @param array $violatingReferences + * @return array */ - private function setErrorOutput($violatingReferences, $path) + private function setErrorOutput(array $violatingReferences, $path): array { $testErrors = []; @@ -326,120 +302,4 @@ private function setErrorOutput($violatingReferences, $path) return $testErrors; } - - /** - * Builds and returns array of FullModuleNae => composer name - * @param array $array - * @return array - */ - private function buildModuleNameToComposerName($array) - { - $returnList = []; - foreach ($array as $moduleName => $path) { - $composerData = json_decode(file_get_contents($path . DIRECTORY_SEPARATOR . "composer.json")); - $returnList[$moduleName] = $composerData->name; - } - return $returnList; - } - - /** - * Builds and returns flattened dependency list based on composer dependencies - * @return array - */ - private function buildComposerDependencyList() - { - $flattenedDependencies = []; - - foreach ($this->moduleNameToPath as $moduleName => $pathToModule) { - $composerData = json_decode(file_get_contents($pathToModule . DIRECTORY_SEPARATOR . "composer.json"), true); - $this->allDependencies[$moduleName] = $composerData['require']; - } - foreach ($this->allDependencies as $moduleName => $dependencies) { - $this->alreadyExtractedDependencies = []; - $flattenedDependencies[$moduleName] = $this->extractSubDependencies($moduleName); - } - return $flattenedDependencies; - } - - /** - * Recursive function to fetch dependencies of given dependency, and its child dependencies - * @param string $subDependencyName - * @return array - */ - private function extractSubDependencies($subDependencyName) - { - $flattenedArray = []; - - if (array_search($subDependencyName, $this->alreadyExtractedDependencies) !== false) { - return $flattenedArray; - } - - if (isset($this->allDependencies[$subDependencyName])) { - $subDependencyArray = $this->allDependencies[$subDependencyName]; - $flattenedArray = array_merge($flattenedArray, $this->allDependencies[$subDependencyName]); - - // Keep track of dependencies that have already been used, prevents circular dependency problems - $this->alreadyExtractedDependencies[] = $subDependencyName; - foreach ($subDependencyArray as $composerDependencyName => $version) { - $subDependencyFullName = array_search($composerDependencyName, $this->moduleNameToComposerName); - $flattenedArray = array_merge( - $flattenedArray, - $this->extractSubDependencies($subDependencyFullName) - ); - } - } - return $flattenedArray; - } - - /** - * Finds unique array ofcomposer dependencies of given testObjects - * @param array $array - * @return array - */ - private function getModuleDependenciesFromReferences($array) - { - $filenames = []; - foreach ($array as $item) { - // Should it append ALL filenames, including merges? - $allFiles = explode(",", $item->getFilename()); - foreach ($allFiles as $file) { - $modulePath = dirname(dirname(dirname(dirname($file)))); - $fullModuleName = array_search($modulePath, $this->moduleNameToPath); - $composerModuleName = $this->moduleNameToComposerName[$fullModuleName]; - $filenames[$item->getName()][] = $composerModuleName; - } - } - return $filenames; - } - - /** - * Attempts to find any MFTF entity by its name. Returns null if none are found. - * @param string $name - * @return mixed - * @throws XmlException - */ - private function findEntity($name) - { - if ($name == '_ENV' || $name == '_CREDS') { - return null; - } - - if (DataObjectHandler::getInstance()->getObject($name)) { - return DataObjectHandler::getInstance()->getObject($name); - } elseif (PageObjectHandler::getInstance()->getObject($name)) { - return PageObjectHandler::getInstance()->getObject($name); - } elseif (SectionObjectHandler::getInstance()->getObject($name)) { - return SectionObjectHandler::getInstance()->getObject($name); - } - - try { - return ActionGroupObjectHandler::getInstance()->getObject($name); - } catch (TestReferenceException $e) { - } - try { - return TestObjectHandler::getInstance()->getObject($name); - } catch (TestReferenceException $e) { - } - return null; - } } diff --git a/src/Magento/FunctionalTestingFramework/StaticCheck/UnusedEntityCheck.php b/src/Magento/FunctionalTestingFramework/StaticCheck/UnusedEntityCheck.php new file mode 100644 index 000000000..d8d6979d5 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/StaticCheck/UnusedEntityCheck.php @@ -0,0 +1,644 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\StaticCheck; + +use Exception; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Finder\Finder; +use Magento\FunctionalTestingFramework\Util\Script\ScriptUtil; +use Symfony\Component\Finder\SplFileInfo; +use DOMElement; + +/** + * Class UnusedEntityCheck + * + * @package Magento\FunctionalTestingFramework\StaticCheck + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class UnusedEntityCheck implements StaticCheckInterface +{ + const ERROR_LOG_FILENAME = "mftf-unused-entity-usage-checks"; + const ENTITY_REGEX_PATTERN = "/\{(\{)*([\w.]+)(\})*\}/"; + const SELECTOR_REGEX_PATTERN = '/selector=["\']([^\'"]*)/'; + const ERROR_LOG_MESSAGE = "MFTF Unused Entity Usage Check"; + const SECTION_REGEX_PATTERN = "/\w*Section\b/"; + const REQUIRED_ENTITY = '/<requiredEntity(.*?)>(.+?)<\/requiredEntity>/'; + const ENTITY_SEPERATED_BY_DOT_REFERENCE = '/([\w]+)(\.)+([\w]+)/'; + + /** + * ScriptUtil instance + * + * @var ScriptUtil $scriptUtil + */ + private $scriptUtil; + + /** + * @var array + */ + private $errors = []; + + /** + * @var string + */ + private $output = ''; + + /** + * Checks test dependencies, determined by references in tests versus the dependencies listed in the Magento module + * + * @param InputInterface $input + * @return void + * @throws Exception + */ + public function execute(InputInterface $input) + { + $this->errors = $this->unusedEntities(); + $this->output = $this->scriptUtil->printErrorsToFile( + $this->errors, + StaticChecksList::getErrorFilesPath() . DIRECTORY_SEPARATOR . self::ERROR_LOG_FILENAME . '.txt', + self::ERROR_LOG_MESSAGE + ); + } + + /** + * Centralized method to get unused Entities + * @return array + */ + public function unusedEntities() + { + $this->scriptUtil = new ScriptUtil(); + $domDocument = new \DOMDocument(); + $modulePaths = $this->scriptUtil->getAllModulePaths(); + $testXmlFiles = $this->scriptUtil->getModuleXmlFilesByScope($modulePaths, "Test"); + $actionGroupXmlFiles = $this->scriptUtil->getModuleXmlFilesByScope($modulePaths, "ActionGroup"); + $dataXmlFiles = $this->scriptUtil->getModuleXmlFilesByScope($modulePaths, "Data"); + $pageXmlFiles = $this->scriptUtil->getModuleXmlFilesByScope($modulePaths, "Page"); + $sectionXmlFiles = $this->scriptUtil->getModuleXmlFilesByScope($modulePaths, "Section"); + $suiteXmlFiles = $this->scriptUtil->getModuleXmlFilesByScope($modulePaths, 'Suite'); + foreach ($dataXmlFiles as $filePath) { + $domDocument->load($filePath); + $entityResult = $this->getAttributesFromDOMNodeList( + $domDocument->getElementsByTagName("entity"), + ["type" => "name"] + ); + foreach ($entityResult as $entitiesResultData) { + $dataNames[$entitiesResultData[key($entitiesResultData)]] = [ + "dataFilePath"=>$filePath->getRealPath() + ]; + } + } + foreach ($actionGroupXmlFiles as $filePath) { + $domDocument->load($filePath); + $actionGroupName = $domDocument->getElementsByTagName("actionGroup")->item(0)->getAttribute("name"); + if (!empty($domDocument->getElementsByTagName("actionGroup")->item(0)->getAttribute("deprecated"))) { + continue; + } + $allActionGroupFileNames[$actionGroupName ] = + $filePath->getRealPath(); + } + + foreach ($sectionXmlFiles as $filePath) { + $domDocument->load($filePath); + $sectionName = $domDocument->getElementsByTagName("section")->item(0)->getAttribute("name"); + $sectionFileNames[$sectionName] = $filePath->getRealPath(); + } + foreach ($pageXmlFiles as $filePath) { + $domDocument->load($filePath); + $pageName = $domDocument->getElementsByTagName("page")->item(0)->getAttribute("name"); + $pageFiles[$pageName] = $filePath->getRealPath(); + } + $actionGroupReferences = $this->unusedActionEntity( + $domDocument, + $actionGroupXmlFiles, + $testXmlFiles, + $allActionGroupFileNames, + $suiteXmlFiles + ); + $entityReferences = $this->unusedData( + $domDocument, + $actionGroupXmlFiles, + $testXmlFiles, + $dataNames, + $dataXmlFiles + ); + $pagesReference = $this->unusedPageEntity( + $domDocument, + $actionGroupXmlFiles, + $testXmlFiles, + $pageFiles, + $suiteXmlFiles + ); + $sectionReference = $this->unusedSectionEntity( + $domDocument, + $actionGroupXmlFiles, + $testXmlFiles, + $pageXmlFiles, + $sectionFileNames, + $suiteXmlFiles + ); + return $this->setErrorOutput( + array_merge( + array_values($actionGroupReferences), + array_values($entityReferences), + array_values($pagesReference), + array_values($sectionReference) + ) + ); + } + + /** + * Setting error message + * + * @return array + * @throws Exception + */ + private function setErrorOutput($unusedFilePath) + { + $testErrors = []; + foreach ($unusedFilePath as $files) { + $contents = file_get_contents($files); + $file = fopen($files, 'a'); + if (!str_contains($contents, '<!--@Ignore(Unused_Entity_Check)-->')) { + fwrite($file, '<!--@Ignore(Unused_Entity_Check)-->'); + } + } + return $testErrors; + } + + /** + * Retrieves Unused Action Group Entities + * + * @param DOMDocument $domDocument + * @param ScriptUtil $actionGroupXmlFiles + * @param ScriptUtil $testXmlFiles + * @param ScriptUtil $allActionGroupFileNames + * @param ScriptUtil $suiteXmlFiles + * @return array + * @throws Exception + */ + public function unusedActionEntity( + $domDocument, + $actionGroupXmlFiles, + $testXmlFiles, + $allActionGroupFileNames, + $suiteXmlFiles + ) { + foreach ($suiteXmlFiles as $filePath) { + $domDocument->load($filePath); + $referencesSuite= $this->getAttributesFromDOMNodeList( + $domDocument->getElementsByTagName("actionGroup"), + "ref" + ); + foreach ($referencesSuite as $referencesResultSuite) { + if (isset($allActionGroupFileNames[$referencesResultSuite])) { + unset($allActionGroupFileNames[$referencesResultSuite]); + } + } + } + + foreach ($actionGroupXmlFiles as $filePath) { + $domDocument->load($filePath); + $actionGroup = $domDocument->getElementsByTagName("actionGroup")->item(0); + $references = $actionGroup->getAttribute("extends"); + if (in_array($references, array_keys($allActionGroupFileNames))) { + unset($allActionGroupFileNames[$references]); + } + } + foreach ($testXmlFiles as $filePath) { + $domDocument->load($filePath); + $testReferences = $this->getAttributesFromDOMNodeList( + $domDocument->getElementsByTagName("actionGroup"), + "ref" + ); + foreach ($testReferences as $testReferencesResult) { + if (isset($allActionGroupFileNames[$testReferencesResult])) { + unset($allActionGroupFileNames[$testReferencesResult]); + } + } + } + return $allActionGroupFileNames; + } + + /** + * Retrieves Unused Page Entities + * + * @param DOMDocument $domDocument + * @return array + * @throws Exception + */ + public function unusedPageEntity($domDocument, $actionGroupXmlFiles, $testXmlFiles, $pageNames, $suiteXmlFiles) + { + + foreach ($suiteXmlFiles as $filePath) { + $domDocument->load($filePath); + $contents = file_get_contents($filePath); + $pagesReferencesInSuites = $this->getAttributesFromDOMNodeList( + $domDocument->getElementsByTagName("amOnPage"), + "url" + ); + foreach ($pagesReferencesInSuites as $pagesReferencesInSuitesResult) { + $explodepagesReferencesResult = explode( + ".", + trim($pagesReferencesInSuitesResult, "{}") + ); + unset($pageNames[$explodepagesReferencesResult[0]]); + } + $pageNames = $this->entityReferencePatternCheck($domDocument, $pageNames, $contents, false, []); + } + foreach ($actionGroupXmlFiles as $filePath) { + $domDocument->load($filePath); + $contents = file_get_contents($filePath); + $pagesReferences = $this->getAttributesFromDOMNodeList( + $domDocument->getElementsByTagName("amOnPage"), + "url" + ); + foreach ($pagesReferences as $pagesReferencesResult) { + $explodepagesReferencesResult = explode( + ".", + trim($pagesReferencesResult, "{}") + ); + unset($pageNames[$explodepagesReferencesResult[0]]); + } + $pageNames = $this->entityReferencePatternCheck($domDocument, $pageNames, $contents, false, []); + } + + foreach ($testXmlFiles as $filePath) { + $domDocument->load($filePath); + $contents = file_get_contents($filePath); + $testReferences = $this->getAttributesFromDOMNodeList( + $domDocument->getElementsByTagName("amOnPage"), + "url" + ); + foreach ($testReferences as $pagesReferencesResult) { + $explodepagesReferencesResult = explode( + ".", + trim($pagesReferencesResult, "{}") + ); + unset($pageNames[$explodepagesReferencesResult[0]]); + } + $pageNames = $this->entityReferencePatternCheck($domDocument, $pageNames, $contents, false, []); + } + return $pageNames; + } + + /** + * Common Pattern Check Method + * + * @param DOMDocument $domDocument + * @param array $fileNames + * @param string $contents + * @return array + * @throws Exception + */ + private function entityReferencePatternCheck($domDocument, $fileNames, $contents, $data, $removeDataFilePath) + { + $sectionArgumentValueReference = $this->getAttributesFromDOMNodeList( + $domDocument->getElementsByTagName("argument"), + "value" + ); + $sectionDefaultValueArgumentReference = $this->getAttributesFromDOMNodeList( + $domDocument->getElementsByTagName("argument"), + "defaultValue" + ); + $sectionArgumentValue = array_merge($sectionArgumentValueReference, $sectionDefaultValueArgumentReference); + foreach ($sectionArgumentValue as $sectionArgumentValueResult) { + $explodedReference = str_contains($sectionArgumentValueResult, '$') + ? explode(".", trim($sectionArgumentValueResult, '$')) + : explode(".", trim($sectionArgumentValueResult, "{}")); + if (in_array($explodedReference[0], array_keys($fileNames))) { + $removeDataFilePath[] = isset($fileNames[$explodedReference[0]]["dataFilePath"]) + ? $fileNames[$explodedReference[0]]["dataFilePath"] + : []; + unset($fileNames[$explodedReference[0]]); + } + } + preg_match_all(self::ENTITY_REGEX_PATTERN, $contents, $bracketReferencesData); + preg_match_all( + self::ENTITY_SEPERATED_BY_DOT_REFERENCE, + $contents, + $entitySeperatedByDotReferenceActionGroup + ); + $entityReferenceDataResultActionGroup = array_merge( + array_unique($bracketReferencesData[0]), + array_unique($entitySeperatedByDotReferenceActionGroup[0]) + ); + + foreach (array_unique($entityReferenceDataResultActionGroup) as $bracketReferencesResults) { + $bracketReferencesDataResultOutput = explode(".", trim($bracketReferencesResults, "{}")); + if (in_array($bracketReferencesDataResultOutput[0], array_keys($fileNames))) { + $removeDataFilePath[] = isset($fileNames[$bracketReferencesDataResultOutput[0]]["dataFilePath"]) + ? $fileNames[$bracketReferencesDataResultOutput[0]]["dataFilePath"] + : []; + unset($fileNames[$bracketReferencesDataResultOutput[0]]); + } + } + + return ($data === true) ? ['dataFilePath'=>$removeDataFilePath ,'fileNames'=> $fileNames ] : $fileNames ; + } + + /** + * Retrieves Unused Section Entities + * + * @param DOMDocument $domDocument + * @param ScriptUtil $actionGroupXmlFiles + * @param ScriptUtil $testXmlFiles + * @param ScriptUtil $pageXmlFiles + * @param array $sectionFileNames + * @param ScriptUtil $suiteXmlFiles + * @return array + * @throws Exception + */ + public function unusedSectionEntity( + $domDocument, + $actionGroupXmlFiles, + $testXmlFiles, + $pageXmlFiles, + $sectionFileNames, + $suiteXmlFiles + ) { + foreach ($suiteXmlFiles as $filePath) { + $contents = file_get_contents($filePath); + $domDocument->load($filePath); + preg_match_all( + self::SELECTOR_REGEX_PATTERN, + $contents, + $selectorReferences + ); + + if (isset($selectorReferences[1])) { + foreach (array_unique($selectorReferences[1]) as $selectorReferencesResult) { + $trimSelector = explode(".", trim($selectorReferencesResult, "{{}}")); + if (isset($sectionFileNames[$trimSelector[0]])) { + unset($sectionFileNames[$trimSelector[0]]); + } + } + } + $sectionFileNames = $this->entityReferencePatternCheck( + $domDocument, + $sectionFileNames, + $contents, + false, + [] + ); + } + foreach ($actionGroupXmlFiles as $filePath) { + $contents = file_get_contents($filePath); + $domDocument->load($filePath); + preg_match_all( + self::SELECTOR_REGEX_PATTERN, + $contents, + $selectorReferences + ); + + if (isset($selectorReferences[1])) { + foreach (array_unique($selectorReferences[1]) as $selectorReferencesResult) { + $trimSelector = explode(".", trim($selectorReferencesResult, "{{}}")); + if (isset($sectionFileNames[$trimSelector[0]])) { + unset($sectionFileNames[$trimSelector[0]]); + } + } + } + $sectionFileNames = $this->entityReferencePatternCheck( + $domDocument, + $sectionFileNames, + $contents, + false, + [] + ); + } + $sectionFileNames = $this->getUnusedSectionEntitiesReferenceInActionGroupAndTestFiles( + $testXmlFiles, + $pageXmlFiles, + $domDocument, + $sectionFileNames + ); + return $sectionFileNames; + } + + /** + * Get unused section entities reference in Action group and Test files + * @param ScriptUtil $testXmlFiles + * @param ScriptUtil $pageXmlFiles + * @param DOMDocument $domDocument + * @param array $sectionFileNames + * @return array + * @throws Exception + */ + private function getUnusedSectionEntitiesReferenceInActionGroupAndTestFiles( + $testXmlFiles, + $pageXmlFiles, + $domDocument, + $sectionFileNames + ) { + foreach ($testXmlFiles as $filePath) { + $contents = file_get_contents($filePath); + $domDocument->load($filePath); + preg_match_all( + self::SELECTOR_REGEX_PATTERN, + $contents, + $selectorReferences + ); + if (isset($selectorReferences[1])) { + foreach (array_unique($selectorReferences[1]) as $selectorReferencesResult) { + $trimSelector = explode(".", trim($selectorReferencesResult, "{{}}")); + if (isset($sectionFileNames[$trimSelector[0]])) { + unset($sectionFileNames[$trimSelector[0]]); + } + } + } + $sectionFileNames = $this->entityReferencePatternCheck( + $domDocument, + $sectionFileNames, + $contents, + false, + [] + ); + } + + foreach ($pageXmlFiles as $filePath) { + $contents = file_get_contents($filePath); + preg_match_all( + self::SELECTOR_REGEX_PATTERN, + $contents, + $selectorReferences + ); + if (isset($selectorReferences[1])) { + foreach (array_unique($selectorReferences[1]) as $selectorReferencesResult) { + $trimSelector = explode(".", trim($selectorReferencesResult, "{{}}")); + if (isset($sectionFileNames[$trimSelector[0]])) { + unset($sectionFileNames[$trimSelector[0]]); + } + } + } + $sectionFileNames = $this->entityReferencePatternCheck( + $domDocument, + $sectionFileNames, + $contents, + false, + [] + ); + } + return $sectionFileNames; + } + /** + * Return Unused Data entities + * + * @param DOMDocument $domDocument + * @return array + */ + public function unusedData( + $domDocument, + $actionGroupXmlFiles, + $testXmlFiles, + $dataNames, + $dataXmlFiles + ) { + $removeDataFilePath = []; + foreach ($dataXmlFiles as $filePath) { + $domDocument->load($filePath); + $contents = file_get_contents($filePath); + preg_match_all(self::REQUIRED_ENTITY, $contents, $requiredEntityReference); + foreach ($requiredEntityReference[2] as $requiredEntityReferenceResult) { + if (isset($dataNames[$requiredEntityReferenceResult])) { + $removeDataFilePath[] = + $dataNames[$requiredEntityReferenceResult]["dataFilePath"]; + unset($dataNames[$requiredEntityReferenceResult]); + } + } + } + foreach ($actionGroupXmlFiles as $filePath) { + $domDocument->load($filePath); + $contents = file_get_contents($filePath); + $getUnusedFilePath = $this->entityReferencePatternCheck( + $domDocument, + $dataNames, + $contents, + true, + $removeDataFilePath + ); + $dataNames = $getUnusedFilePath['fileNames']; + $removeDataFilePath = $getUnusedFilePath['dataFilePath']; + } + foreach ($testXmlFiles as $filePath) { + $domDocument->load($filePath); + $contents = file_get_contents($filePath); + $getUnusedFilePath = $this->entityReferencePatternCheck( + $domDocument, + $dataNames, + $contents, + true, + $removeDataFilePath + ); + $dataNames = $getUnusedFilePath['fileNames']; + $removeDataFilePath = $getUnusedFilePath['dataFilePath']; + $createdDataReferences = $this->getAttributesFromDOMNodeList( + $domDocument->getElementsByTagName("createData"), + "entity" + ); + $updatedDataReferences = $this->getAttributesFromDOMNodeList( + $domDocument->getElementsByTagName("updateData"), + "entity" + ); + $getDataReferences = $this->getAttributesFromDOMNodeList( + $domDocument->getElementsByTagName("getData"), + "entity" + ); + $dataReferences = array_unique( + array_merge( + $createdDataReferences, + $updatedDataReferences, + $getDataReferences + ) + ); + if (count($dataReferences) > 0) { + foreach ($dataReferences as $dataReferencesResult) { + if (isset($dataNames[$dataReferencesResult])) { + $removeDataFilePath[] = $dataNames[$dataReferencesResult]["dataFilePath"]; + unset($dataNames[$dataReferencesResult]); + } + } + } + } + $dataFilePathResult = $this->unsetFilePath($dataNames, $removeDataFilePath); + return array_unique($dataFilePathResult); + } + + /** + * Remove used entities file path from unused entities array + * + * @return array + */ + private function unsetFilePath($dataNames, $removeDataFilePath) + { + $dataFilePathResult = []; + foreach ($dataNames as $key => $dataNamesResult) { + if (in_array($dataNamesResult["dataFilePath"], $removeDataFilePath)) { + unset($dataNames[$key]); + continue; + } + $dataFilePathResult[] = $dataNames[$key]['dataFilePath']; + } + return array_unique($dataFilePathResult); + } + + /** + * Return attribute value for each node in DOMNodeList as an array + * + * @param DOMNodeList $nodes + * @param string $attributeName + * @return array + */ + private function getAttributesFromDOMNodeList($nodes, $attributeName) + { + $attributes = []; + foreach ($nodes as $node) { + if (is_string($attributeName)) { + $attributeValue = $node->getAttribute($attributeName); + } else { + $attributeValue = [$node->getAttribute(key($attributeName)) => + $node->getAttribute($attributeName[key($attributeName)])]; + } + if (!empty($attributeValue)) { + $attributes[] = $attributeValue; + } + } + return $attributes; + } + + /** + * Extract actionGroup DomElement from xml file + * + * @param string $contents + * @return \DOMElement + */ + public function getActionGroupDomElement(string $contents): DOMElement + { + $domDocument = new \DOMDocument(); + $domDocument->loadXML($contents); + return $domDocument->getElementsByTagName("actionGroup")[0]; + } + + /** + * Return array containing all errors found after running the execute() function + * + * @return array + */ + public function getErrors() + { + return $this->errors; + } + + /** + * Return output + * + * @return string + */ + public function getOutput() + { + return $this->output; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Step/Backend/AdminStep.php b/src/Magento/FunctionalTestingFramework/Step/Backend/AdminStep.php deleted file mode 100644 index 5ae74a076..000000000 --- a/src/Magento/FunctionalTestingFramework/Step/Backend/AdminStep.php +++ /dev/null @@ -1,2316 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -namespace Magento\FunctionalTestingFramework\Step\Backend; - -require_once __DIR__ . '/../../Helper/AdminUrlList.php'; - -/** - * Class AdminStep - * @SuppressWarnings(PHPMD) - * @codingStandardsIgnoreFile - */ -class AdminStep extends \Magento\FunctionalTestingFramework\AcceptanceTester -{ - public static $adminPageTitle = '.page-title'; - - public function openNewTabGoToVerify($url) - { - $I = $this; - $I->openNewTab(); - $I->amOnPage($url); - $I->waitForPageLoad(); - $I->seeInCurrentUrl($url); - } - - public function closeNewTab() - { - $I = $this; - $I->closeTab(); - } - - // Key Admin Pages - public function goToRandomAdminPage() - { - $I = $this; - - $admin_url_list = array( - "/admin/admin/dashboard/", - "/admin/sales/order/", - "/admin/sales/invoice/", - "/admin/sales/shipment/", - "/admin/sales/creditmemo/", - "/admin/paypal/billing_agreement/", - "/admin/sales/transactions/", - "/admin/catalog/product/", - "/admin/catalog/category/", - "/admin/customer/index/", - "/admin/customer/online/", - "/admin/catalog_rule/promo_catalog/", - "/admin/sales_rule/promo_quote/", - "/admin/admin/email_template/", - "/admin/newsletter/template/", - "/admin/newsletter/queue/", - "/admin/newsletter/subscriber/", - "/admin/admin/url_rewrite/index/", - "/admin/search/term/index/", - "/admin/search/synonyms/index/", - "/admin/admin/sitemap/", - "/admin/review/product/index/", - "/admin/cms/page/", - "/admin/cms/block/", - "/admin/admin/widget_instance/", - "/admin/theme/design_config/", - "/admin/admin/system_design_theme/", - "/admin/admin/system_design/", - "/admin/reports/report_shopcart/product/", - "/admin/search/term/report/", - "/admin/reports/report_shopcart/abandoned/", - "/admin/newsletter/problem/", - "/admin/reports/report_review/customer/", - "/admin/reports/report_review/product/", - "/admin/reports/report_sales/sales/", - "/admin/reports/report_sales/tax/", - "/admin/reports/report_sales/invoiced/", - "/admin/reports/report_sales/shipping/", - "/admin/reports/report_sales/refunded/", - "/admin/reports/report_sales/coupons/", - "/admin/paypal/paypal_reports/", - "/admin/braintree/report/", - "/admin/reports/report_customer/totals/", - "/admin/reports/report_customer/orders/", - "/admin/reports/report_customer/accounts/", - "/admin/reports/report_product/viewed/", - "/admin/reports/report_sales/bestsellers/", - "/admin/reports/report_product/lowstock/", - "/admin/reports/report_product/sold/", - "/admin/reports/report_product/downloads/", - "/admin/reports/report_statistics/", - "/admin/admin/system_store/", - "/admin/admin/system_config/", - "/admin/checkout/agreement/", - "/admin/sales/order_status/", - "/admin/tax/rule/", - "/admin/tax/rate/", - "/admin/admin/system_currency/", - "/admin/admin/system_currencysymbol/", - "/admin/catalog/product_attribute/", - "/admin/catalog/product_set/", - "/admin/review/rating/", - "/admin/customer/group/", - "/admin/admin/import/", - "/admin/admin/export/", - "/admin/tax/rate/importExport/", - "/admin/admin/history/", - "/admin/admin/integration/", - "/admin/admin/cache/", - "/admin/backup/index/", - "/admin/indexer/indexer/list/", - "/admin/admin/user/", - "/admin/admin/locks/", - "/admin/admin/user_role/", - "/admin/admin/notification/", - "/admin/admin/system_variable/", - "/admin/admin/crypt_key/" - ); - - $random_admin_url = array_rand($admin_url_list, 1); - - $I->amOnPage($admin_url_list[$random_admin_url]); - $I->waitForPageLoad(); - - return $admin_url_list[$random_admin_url]; - } - - public function goToTheAdminLoginPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminLoginPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminLogoutPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminLogoutPage); - } - - // Sales - public function goToTheAdminOrdersGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrdersGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminOrderForIdPage($orderId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrderByIdPage . $orderId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddOrderPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddOrderPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddOrderForCustomerIdPage($customerId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddOrderForCustomerIdPage . $customerId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminInvoicesGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminInvoicesGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddInvoiceForOrderIdPage($orderId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddInvoiceForOrderIdPage . $orderId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminShipmentsGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminShipmentsGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminShipmentForIdPage($shipmentId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminShipmentForIdPage . $shipmentId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminCreditMemosGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCreditMemosGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminCreditMemoForIdPage($creditMemoId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCreditMemoForIdPage . $creditMemoId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminBillingAgreementsGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminBillingAgreementsGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminTransactionsGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTransactionsGrid); - $I->waitForPageLoad(); - } - - // Products - public function goToTheAdminCatalogPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCatalogGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminProductForIdPage($productId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductForIdPage . $productId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddSimpleProductPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddSimpleProductPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddConfigurableProductPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddConfigurableProductPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddGroupedProductPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddGroupedProductPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddVirtualProductPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddVirtualProductPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddBundledProductPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddBundleProductPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddDownloadableProductPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddDownloadableProductPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminCategoriesPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCategoriesPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminCategoryForIdPage($categoryId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCategoryForIdPage . $categoryId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddRootCategoryForStoreIdPage($storeId) - { - $I = $this; - $I->amOnPage(('/admin/catalog/category/add/store/' . $storeId . '/parent/1')); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddSubCategoryForStoreIdPage($storeId) - { - $I = $this; - $I->amOnPage(('/admin/catalog/category/add/store/' . $storeId . '/parent/2')); - $I->waitForPageLoad(); - } - - // Customers - public function goToTheAdminAllCustomersGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAllCustomersGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminCustomersNowOnlineGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomersNowOnlineGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminCustomerForIdPage($customerId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomerForCustomerIdPage . $customerId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddCustomerPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddCustomerPage); - $I->waitForPageLoad(); - } - - // Marketing - public function goToTheAdminCatalogPriceRuleGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCatalogPriceRuleGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminCatalogPriceRuleForIdPage($catalogPriceRuleId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCatalogPriceRuleForIdPage . $catalogPriceRuleId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddCatalogPriceRulePage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddCatalogPriceRulePage); - $I->waitForPageLoad(); - } - - public function goToTheAdminCartPriceRulesGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCartPriceRulesGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminCartPriceRuleForIdPage($cartPriceRuleId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCartPriceRuleForIdPage . $cartPriceRuleId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddCartPriceRulePage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddCartPriceRulePage); - $I->waitForPageLoad(); - } - - public function goToTheAdminEmailTemplatesGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminEmailTemplatesGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminEmailTemplateForIdPage($emailTemplateId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminEmailTemplateForIdPage . $emailTemplateId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddEmailTemplatePage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddEmailTemplatePage); - $I->waitForPageLoad(); - } - - public function goToTheAdminNewsletterTemplateGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNewsletterTemplateGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminNewsletterTemplateByIdPage($newsletterTemplateId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNewsletterTemplateForIdPage . $newsletterTemplateId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddNewsletterTemplatePage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddNewsletterTemplatePage); - $I->waitForPageLoad(); - } - - public function goToTheAdminNewsletterQueueGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNewsletterQueueGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminNewsletterSubscribersGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNewsletterSubscribersGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminURLRewritesGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminURLRewritesGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminURLRewriteForId($urlRewriteId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminURLRewriteForIdPage . $urlRewriteId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddURLRewritePage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddURLRewritePage); - $I->waitForPageLoad(); - } - - public function goToTheAdminSearchTermsGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSearchTermsGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminSearchTermForIdPage($searchTermId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSearchTermForIdPage . $searchTermId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddSearchTermPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddSearchTermPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminSearchSynonymsGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSearchSynonymsGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminSearchSynonymGroupByIdPage($searchSynonymId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSearchSynonymGroupForIdPage . $searchSynonymId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddSearchSynonymGroupPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddSearchSynonymGroupPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminSiteMapGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSiteMapGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminSiteMapForIdPage($siteMapId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSiteMapForIdPage . $siteMapId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddSiteMapPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddSiteMapPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminReviewsGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminReviewsGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminReviewForIdPage($reviewId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminReviewByIdPage . $reviewId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddReviewPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddReviewPage); - $I->waitForPageLoad(); - } - - // Content - public function goToTheAdminPagesGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminPagesGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminPageForIdPage($pageId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminPageForIdPage . $pageId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddPagePage() - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddPagePage)); - $I->waitForPageLoad(); - } - - public function goToTheAdminBlocksGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminBlocksGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminBlockForIdPage($blockId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminBlockForIdPage . $blockId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddBlockPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddBlockPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminWidgetsGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminWidgetsGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddWidgetPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddWidgetPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminDesignConfigurationGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminDesignConfigurationGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminThemesGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminThemesGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminThemeByIdPage($themeId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminThemeByIdPage . $themeId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminStoreContentScheduleGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminStoreContentScheduleGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminStoreContentScheduleForIdPage($storeContentScheduleId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminStoreContentScheduleForIdPage . $storeContentScheduleId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddStoreDesignChangePage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddStoreDesignChangePage); - $I->waitForPageLoad(); - } - - // Reports - public function goToTheAdminProductsInCartGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductsInCartGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminSearchTermsReportGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSearchTermsReportGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminAbandonedCartsGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAbandonedCartsGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminNewsletterProblemsReportGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNewsletterProblemsReportGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminCustomerReviewsReportGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomerReviewsReportGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminProductReviewsReportGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductReviewsReportGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminProductReviewsForProductIdPage($productId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductReviewsForProductIdPage . $productId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminProductReviewIdForProductIdPage($productReviewId, $productId) - { - $I = $this; - $I->amOnPage(('/admin/review/product/edit/id/' . $productReviewId . '/productId/' . $productId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminOrdersReportGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrdersReportGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminTaxReportGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTaxReportGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminInvoiceReportGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminInvoiceReportGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminShippingReportGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminShippingReportGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminRefundsReportGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminRefundsReportGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminCouponsReportGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCouponsReportGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminPayPalSettlementReportsGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminPayPalSettlementReportsGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminBraintreeSettlementReportGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminBraintreeSettlementReportGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminOrderTotalReportGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrderTotalReportGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminOrderCountReportGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrderCountReportGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminNewAccountsReportGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNewAccountsReportGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminProductViewsReportGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductViewsReportGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminBestsellersReportGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminBestsellersReportGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminLowStockReportGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminLowStockReportGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminOrderedProductsReportGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrderedProductsReportGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminDownloadsReportGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminDownloadsReportGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminRefreshStatisticsGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminRefreshStatisticsGrid); - $I->waitForPageLoad(); - } - - // Stores - public function goToTheAdminAllStoresGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAllStoresGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminCreateStoreViewPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCreateStoreViewPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminCreateStorePage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCreateStorePage); - $I->waitForPageLoad(); - } - - public function goToTheAdminCreateWebsitePage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCreateWebsitePage); - $I->waitForPageLoad(); - } - - public function goToTheAdminWebsiteForIdPage($websiteId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminWebsiteByIdPage . $websiteId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminStoreViewForIdPage($storeViewId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminStoreViewByIdPage . $storeViewId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminStoreForIdPage($storeId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminStoreByIdPage . $storeId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminConfigurationGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminConfigurationGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminTermsAndConditionsGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTermsAndConditionsGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminTermsAndConditionForIdPage($termsAndConditionsId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTermsAndConditionByIdPage . $termsAndConditionsId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddNewTermsAndConditionsPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddNewTermsAndConditionPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminOrderStatusGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrderStatusGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddOrderStatusPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddOrderStatusPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminTaxRulesGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTaxRulesGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminTaxRuleForIdPage($taxRuleId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTaxRuleByIdPage . $taxRuleId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddTaxRulePage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddTaxRulePage); - $I->waitForPageLoad(); - } - - public function goToTheAdminTaxZonesAndRatesGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTaxZonesAndRatesGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminTaxZoneAndRateForIdPage($taxZoneAndRateId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTaxZoneAndRateByIdPage . $taxZoneAndRateId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddTaxZoneAndRatePage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddTaxZoneAndRatePage); - $I->waitForPageLoad(); - } - - public function goToTheAdminCurrencyRatesPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCurrencyRatesPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminCurrencySymbolsPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCurrencySymbolsPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminProductAttributesGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductAttributesGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminProductAttributeForIdPage($productAttributeId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductAttributeForIdPage . $productAttributeId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddProductAttributePage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddProductAttributePage); - $I->waitForPageLoad(); - } - - public function goToTheAdminAttributeSetGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAttributeSetsGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminAttributeSetByIdPage($attributeSetId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAttributeSetByIdPage . $attributeSetId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddAttributeSetPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddAttributeSetPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminRatingGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminRatingsGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminRatingForIdPage($ratingId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminRatingForIdPage . $ratingId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddRatingPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddRatingPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminCustomerGroupsGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomerGroupsGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminCustomerGroupForIdPage($customerGroupId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomerGroupByIdPage . $customerGroupId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddCustomerGroupPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddCustomerGroupPage); - $I->waitForPageLoad(); - } - - // System - public function goToTheAdminImportPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminImportPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminExportPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminExportPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminImportAndExportTaxRatesPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminImportAndExportTaxRatesPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminImportHistoryGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminImportHistoryGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminIntegrationsGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminIntegrationsGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminIntegrationForIdPage($integrationId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminIntegrationByIdPage . $integrationId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddIntegrationPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddIntegrationPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminCacheManagementGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCacheManagementGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminBackupsGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminBackupsGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminIndexManagementGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminIndexManagementGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminWebSetupWizardPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminWebSetupWizardPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminAllUsersGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAllUsersGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminUserForIdPage($userId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminUserByIdPage . $userId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddUserPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddNewUserPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminLockedUsersGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminLockedUsersGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminUserRolesGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminUserRolesGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminUserRoleForIdPage($userRoleId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminUserRoleByIdPage . $userRoleId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddUserRolePage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddUserRolePage); - $I->waitForPageLoad(); - } - - public function goToTheAdminNotificationsGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNotificationsGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminCustomVariablesGrid() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomVariablesGrid); - $I->waitForPageLoad(); - } - - public function goToTheAdminCustomVariableForId($customVariableId) - { - $I = $this; - $I->amOnPage((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomVariableByIdPage . $customVariableId)); - $I->waitForPageLoad(); - } - - public function goToTheAdminAddCustomVariablePage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddCustomVariablePage); - $I->waitForPageLoad(); - } - - public function goToTheAdminEncryptionKeyPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminEncryptionKeyPage); - $I->waitForPageLoad(); - } - - public function goToTheAdminFindPartnersAndExtensionsPage() - { - $I = $this; - $I->amOnPage(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminFindPartnersAndExtensions); - $I->waitForPageLoad(); - } - - // Key Admin Pages - public function shouldBeOnTheAdminLoginPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminLoginPage); - } - - public function shouldBeOnTheAdminDashboardPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminDashboardPage); - $I->see('Dashboard', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminForgotYourPasswordPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminForgotYourPasswordPage); - } - - // Sales - public function shouldBeOnTheAdminOrdersGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrdersGrid); - $I->see('Orders', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminOrderForIdPage($orderId) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrderByIdPage . $orderId)); - $I->see($orderId, self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddOrderPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddOrderPage); - $I->see('Create New Order', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddOrderForCustomerIdPage($customerId) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddOrderForCustomerIdPage . $customerId)); - $I->see('Create New Order', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminInvoicesGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminInvoicesGrid); - $I->see('Invoices', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddInvoiceForOrderIdPage($orderId) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddInvoiceForOrderIdPage . $orderId)); - $I->see('New Invoice', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminShipmentsGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminShipmentsGrid); - $I->see('Shipments', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminShipmentForIdPage($shipmentId) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminShipmentForIdPage . $shipmentId)); - $I->see('New Shipment'); - } - - public function shouldBeOnTheAdminCreditMemosGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCreditMemosGrid); - $I->see('Credit Memos', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminCreditMemoForIdPage($creditMemoId) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCreditMemoForIdPage . $creditMemoId)); - $I->see('View Memo', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminBillingAgreementsGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminBillingAgreementsGrid); - $I->see('Billing Agreements', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminTransactionsGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTransactionsGrid); - $I->see('Transactions', self::$adminPageTitle); - } - - // Products - public function shouldBeOnTheAdminCatalogGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCatalogGrid); - $I->see('Catalog', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminProductForIdPage($productId, $productName) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductForIdPage . $productId)); - $I->see($productName, self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddSimpleProductPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddSimpleProductPage); - $I->see('New Product', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddConfigurableProductPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddConfigurableProductPage); - $I->see('New Product', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddGroupedProductPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddGroupedProductPage); - $I->see('New Product', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddVirtualProductPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddVirtualProductPage); - $I->see('New Product', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddBundledProductPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddBundleProductPage); - $I->see('New Product', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddDownloadableProductPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddDownloadableProductPage); - $I->see('New Product', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminCategoriesPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCategoriesPage); - $I->see('Default Category', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminCategoryForIdPage($categoryId, $categoryName) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCategoryForIdPage . $categoryId)); - $I->see($categoryName, self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddRootCategoryForStoreIdPage($storeId) - { - $I = $this; - $I->seeInCurrentUrl(('/admin/catalog/category/add/store/' . $storeId . '/parent/1')); - $I->see('New Category', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddSubCategoryForStoreIdPage($storeId) - { - $I = $this; - $I->seeInCurrentUrl(('/admin/catalog/category/add/store/' . $storeId . '/parent/2')); - $I->see('New Category', self::$adminPageTitle); - } - - // Customers - public function shouldBeOnTheAdminAllCustomersGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAllCustomersGrid); - $I->see('Customers', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminCustomersNowOnlineGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomersNowOnlineGrid); - $I->see('Customers Now Online', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminCustomerForIdPage($customerId, $customerName) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomerForCustomerIdPage . $customerId)); - $I->see($customerName, self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddCustomerPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddCustomerPage); - $I->see('New Customer', self::$adminPageTitle); - } - - // Marketing - public function shouldBeOnTheAdminCatalogPriceRuleGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCatalogPriceRuleGrid); - $I->see('Catalog Price Rule', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminCatalogPriceRuleForIdPage($catalogPriceRuleId, $catalogPriceRuleName) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCatalogPriceRuleForIdPage . $catalogPriceRuleId)); - $I->see($catalogPriceRuleName, self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddCatalogPriceRulePage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddCatalogPriceRulePage); - $I->see('New Catalog Price Rule', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminCartPriceRulesGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCartPriceRulesGrid); - $I->see('Cart Price Rules', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminCartPriceRuleForIdPage($cartPriceRuleId, $cartPriceRuleName) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCartPriceRuleForIdPage . $cartPriceRuleId)); - $I->see($cartPriceRuleName, self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddCartPriceRulePage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddCartPriceRulePage); - $I->see('New Cart Price Rule', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminEmailTemplatesGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminEmailTemplatesGrid); - $I->see('Email Templates', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminEmailTemplateForIdPage($emailTemplateId, $templateName) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminEmailTemplateForIdPage . $emailTemplateId)); - $I->see($templateName, self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddEmailTemplatePage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddEmailTemplatePage); - $I->see('New Template', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminNewsletterTemplateGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNewsletterTemplateGrid); - $I->see('Newsletter Templates', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminNewsletterTemplateByIdPage($newsletterTemplateId, $templateName) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNewsletterTemplateForIdPage . $newsletterTemplateId)); - $I->see($templateName, self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddNewsletterTemplatePage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddNewsletterTemplatePage); - $I->see('New Template', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminNewsletterQueueGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNewsletterQueueGrid); - $I->see('Newsletter Queue', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminNewsletterSubscribersGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNewsletterSubscribersGrid); - $I->see('Newsletter Subscribers', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminURLRewritesGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminURLRewritesGrid); - $I->see('URL Rewrites', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminURLRewriteForId($urlRewriteId) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminURLRewriteForIdPage . $urlRewriteId)); - $I->see('Edit URL Rewrite for a', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddURLRewritePage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddURLRewritePage); - $I->see('Add New URL Rewrite', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminSearchTermsGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSearchTermsGrid); - $I->see('Search Terms', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminSearchTermForIdPage($searchTermId, $searchQuery) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSearchTermForIdPage . $searchTermId)); - $I->see($searchQuery, self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddSearchTermPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddSearchTermPage); - $I->see('New Search', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminSearchSynonymsGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSearchSynonymsGrid); - $I->see('Search Synonyms', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminSearchSynonymGroupByIdPage($searchSynonymId, $synonyms) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSearchSynonymGroupForIdPage . $searchSynonymId)); - $I->see($synonyms, self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddSearchSynonymGroupPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddSearchSynonymGroupPage); - $I->see('New Synonym Group', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminSiteMapGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSiteMapGrid); - $I->see('Site Map', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminSiteMapForIdPage($siteMapId, $fileName) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSiteMapForIdPage . $siteMapId)); - $I->see($fileName, self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddSiteMapPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddSiteMapPage); - $I->see('New Site Map', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminReviewsGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminReviewsGrid); - $I->see('Reviews', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminReviewForIdPage($reviewId) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminReviewByIdPage . $reviewId)); - $I->see('Edit Review', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddReviewPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddReviewPage); - $I->see('New Review', self::$adminPageTitle); - } - - // Content - public function shouldBeOnTheAdminPagesGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminPagesGrid); - $I->see('Pages', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminPageForIdPage($pageId, $pageTitle) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminPageForIdPage . $pageId)); - $I->see($pageTitle, self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddPagePage() - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddPagePage)); - $I->see('New Page', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminBlocksGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminBlocksGrid); - $I->see('Blocks', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminBlockForIdPage($blockId, $blockTitle) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminBlockForIdPage . $blockId)); - $I->see($blockTitle, self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddBlockPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddBlockPage); - $I->see('New Block', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminWidgetsGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminWidgetsGrid); - $I->see('Widgets', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddWidgetPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddWidgetPage); - $I->see('Widgets', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminDesignConfigurationGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminDesignConfigurationGrid); - $I->see('Design Configuration', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminThemesGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminThemesGrid); - $I->see('Themes', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminThemeByIdPage($themeId, $themeTitle) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminThemeByIdPage . $themeId)); - $I->see($themeTitle); - } - - public function shouldBeOnTheAdminStoreContentScheduleGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminStoreContentScheduleGrid); - $I->see('Store Design Schedule', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminStoreContentScheduleForIdPage($storeContentScheduleId) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminStoreContentScheduleForIdPage . $storeContentScheduleId)); - $I->see('Edit Store Design Change', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddStoreDesignChangePage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddStoreDesignChangePage); - $I->see('New Store Design Change'); - } - - // Reports - public function shouldBeOnTheAdminProductsInCartGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductsInCartGrid); - $I->see('Products in Carts', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminSearchTermsReportGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminSearchTermsReportGrid); - $I->see('Search Terms Report', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAbandonedCartsGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAbandonedCartsGrid); - $I->see('Abandoned Carts', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminNewsletterProblemsReportGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNewsletterProblemsReportGrid); - $I->see('Newsletter Problems Report', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminCustomerReviewsReportGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomerReviewsReportGrid); - $I->see('Customer Reviews Report', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminProductReviewsReportGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductReviewsReportGrid); - $I->see('Product Reviews Report', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminProductReviewsForProductIdPage($productId) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductReviewsForProductIdPage . $productId)); - $I->see('Reviews', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminProductReviewIdForProductIdPage($productReviewId, $productId) - { - $I = $this; - $I->seeInCurrentUrl(('/admin/review/product/edit/id/' . $productReviewId . '/productId/' . $productId)); - $I->see('Edit Review', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminOrdersReportGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrdersReportGrid); - $I->see('Orders Report', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminTaxReportGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTaxReportGrid); - $I->see('Tax Report', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminInvoiceReportGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminInvoiceReportGrid); - $I->see('Invoice Report', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminShippingReportGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminShippingReportGrid); - $I->see('Shipping Report', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminRefundsReportGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminRefundsReportGrid); - $I->see('Refunds Report', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminCouponsReportGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCouponsReportGrid); - $I->see('Coupons Report', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminPayPalSettlementReportsGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminPayPalSettlementReportsGrid); - $I->see('PayPal Settlement Reports', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminBraintreeSettlementReportGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminBraintreeSettlementReportGrid); - $I->see('Braintree Settlement Report', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminOrderTotalReportGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrderTotalReportGrid); - $I->see('Order Total Report', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminOrderCountReportGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrderCountReportGrid); - $I->see('Order Count Report', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminNewAccountsReportGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNewAccountsReportGrid); - $I->see('New Accounts Report', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminProductViewsReportGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductViewsReportGrid); - $I->see('Product Views Report', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminBestsellersReportGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminBestsellersReportGrid); - $I->see('Bestsellers Report', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminLowStockReportGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminLowStockReportGrid); - $I->see('Low Stock Report', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminOrderedProductsGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrderedProductsReportGrid); - $I->see('Ordered Products Report', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminDownloadsReportGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminDownloadsReportGrid); - $I->see('Downloads Report', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminRefreshStatisticsGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminRefreshStatisticsGrid); - $I->see('Refresh Statistics', self::$adminPageTitle); - } - - // Stores - public function shouldBeOnTheAdminAllStoresGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAllStoresGrid); - $I->see('Stores', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminCreateStoreViewPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCreateStoreViewPage); - $I->see('Stores', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminCreateStorePage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCreateStorePage); - $I->see('Stores', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminCreateWebsitePage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCreateWebsitePage); - $I->see('Stores'); - } - - public function shouldBeOnTheAdminWebsiteForIdPage($websiteId) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminWebsiteByIdPage . $websiteId)); - } - - public function shouldBeOnTheAdminStoreViewForIdPage($storeViewId) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminStoreViewByIdPage . $storeViewId)); - } - - public function shouldBeOnTheAdminStoreForIdPage($storeId) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminStoreByIdPage . $storeId)); - } - - public function shouldBeOnTheAdminConfigurationGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminConfigurationGrid); - $I->see('Configuration', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminTermsAndConditionsGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTermsAndConditionsGrid); - $I->see('Terms and Conditions', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminTermsAndConditionForIdPage($termsAndConditionsId, $conditionName) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTermsAndConditionByIdPage . $termsAndConditionsId)); - $I->see($conditionName, self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddNewTermsAndConditionsPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddNewTermsAndConditionPage); - $I->see('New Condition', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminOrderStatusGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminOrderStatusGrid); - $I->see('Order Status', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddOrderStatusPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddOrderStatusPage); - $I->see('Create New Order Status', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminTaxRulesGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTaxRulesGrid); - $I->see('Tax Rules', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminTaxRuleForIdPage($taxRuleId, $taxRuleName) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTaxRuleByIdPage . $taxRuleId)); - $I->see($taxRuleName, self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddTaxRulePage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddTaxRulePage); - $I->see('New Tax Rule', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminTaxZonesAndRatesGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTaxZonesAndRatesGrid); - $I->see('Tax Zones and Rates', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminTaxZoneAndRateForIdPage($taxZoneAndRateId, $taxIdentifier) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminTaxZoneAndRateByIdPage . $taxZoneAndRateId)); - $I->see($taxIdentifier, self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddTaxZoneAndRatePage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddTaxZoneAndRatePage); - $I->see('New Tax Rate', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminCurrencyRatesPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCurrencyRatesPage); - $I->see('Currency Rates', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminCurrencySymbolsPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCurrencySymbolsPage); - $I->see('Currency Symbols', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminProductAttributesGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductAttributesGrid); - $I->see('Product Attributes', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminProductAttributeForIdPage($productAttributeId, $productAttributeDefaultLabel) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminProductAttributeForIdPage . $productAttributeId)); - $I->see($productAttributeDefaultLabel, self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddProductAttributePage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddProductAttributePage); - $I->see('New Product Attribute', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAttributeSetsGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAttributeSetsGrid); - $I->see('Attribute Sets', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAttributeSetByIdPage($attributeSetId, $attributeSetName) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAttributeSetByIdPage . $attributeSetId)); - $I->see($attributeSetName, self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddAttributeSetPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddAttributeSetPage); - $I->see('New Attribute Set', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminRatingsGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminRatingsGrid); - $I->see('Ratings', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminRatingForIdPage($ratingId, $ratingDefaultValue) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminRatingForIdPage . $ratingId)); - $I->see($ratingDefaultValue, self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddRatingPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddRatingPage); - $I->see('New Rating', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminCustomerGroupsGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomerGroupsGrid); - $I->see('Customer Groups', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminCustomerGroupForIdPage($customerGroupId, $customerGroupName) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomerGroupByIdPage . $customerGroupId)); - $I->see($customerGroupName, self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddCustomerGroupPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddCustomerGroupPage); - $I->see('New Customer Group', self::$adminPageTitle); - } - - // System - public function shouldBeOnTheAdminImportPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminImportPage); - $I->see('Import', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminExportPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminExportPage); - $I->see('Export', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminImportAndExportTaxRatesPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminImportAndExportTaxRatesPage); - $I->see('Import and Export Tax Rates', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminImportHistoryGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminImportHistoryGrid); - $I->see('Import History', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminIntegrationsGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminIntegrationsGrid); - $I->see('Integrations', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminIntegrationForIdPage($integrationId, $integrationName) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminIntegrationByIdPage . $integrationId)); - $I->see('Edit', self::$adminPageTitle); - $I->see($integrationName, self::$adminPageTitle); - $I->see('Integration', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddIntegrationPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddIntegrationPage); - $I->see('New Integration', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminCacheManagementGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCacheManagementGrid); - $I->see('Cache Management', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminBackupsGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminBackupsGrid); - $I->see('Backups', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminIndexManagementGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminIndexManagementGrid); - $I->see('Index Management', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminWebSetupWizardPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminWebSetupWizardPage); - $I->see('Setup Wizard', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAllUsersGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAllUsersGrid); - $I->see('Users', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminUserForIdPage($userId, $userFirstAndLastName) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminUserByIdPage . $userId)); - $I->see($userFirstAndLastName, self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddUserPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddNewUserPage); - $I->see('New User', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminLockedUsersGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminLockedUsersGrid); - $I->see('Locked Users', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminUserRolesGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminUserRolesGrid); - $I->see('Roles', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminUserRoleForIdPage($userRoleId, $userRoleName) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminUserRoleByIdPage . $userRoleId)); - $I->see($userRoleName, self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddUserRolePage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddUserRolePage); - $I->see('New Role', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminNotificationsGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminNotificationsGrid); - $I->see('Notifications', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminCustomVariablesGrid() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomVariablesGrid); - $I->see('Custom Variables', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminCustomVariableForId($customVariableId, $customVariableCode) - { - $I = $this; - $I->seeInCurrentUrl((\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminCustomVariableByIdPage . $customVariableId)); - $I->see($customVariableCode, self::$adminPageTitle); - } - - public function shouldBeOnTheAdminAddCustomVariablePage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminAddCustomVariablePage); - $I->see('New Custom Variable', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminEncryptionKeyPage() - { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminEncryptionKeyPage); - $I->see('Encryption Key', self::$adminPageTitle); - } - - public function shouldBeOnTheAdminFindPartnersAndExtensionsPage() { - $I = $this; - $I->seeInCurrentUrl(\Magento\FunctionalTestingFramework\Helper\AdminUrlList::$adminFindPartnersAndExtensions); - $I->see('Magento Marketplace', self::$adminPageTitle); - } -} diff --git a/src/Magento/FunctionalTestingFramework/Suite/Generators/GroupClassGenerator.php b/src/Magento/FunctionalTestingFramework/Suite/Generators/GroupClassGenerator.php index 415d884b6..e160c4875 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/Generators/GroupClassGenerator.php +++ b/src/Magento/FunctionalTestingFramework/Suite/Generators/GroupClassGenerator.php @@ -6,6 +6,7 @@ namespace Magento\FunctionalTestingFramework\Suite\Generators; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; use Magento\FunctionalTestingFramework\Suite\Objects\SuiteObject; use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; @@ -32,6 +33,10 @@ class GroupClassGenerator 'comment' => 'print' ]; const GROUP_DIR_NAME = 'Group'; + const FUNCTION_PLACEHOLDER = 'PLACEHOLDER'; + const FUNCTION_START = 'this->getModuleForAction("'; + const FUNCTION_END = '")'; + const FUNCTION_REPLACE_REGEX = '/(PLACEHOLDER->([^\(]+))\(/'; /** * Mustache_Engine instance for template loading @@ -131,19 +136,23 @@ private function buildHookMustacheArray($hookObj) { $actions = []; $mustacheHookArray['actions'][] = ['webDriverInit' => true]; + $mustacheHookArray['helpers'] = []; foreach ($hookObj->getActions() as $action) { /** @var ActionObject $action */ $index = count($actions); + if ($action->getType() === ActionObject::ACTION_TYPE_HELPER) { + $mustacheHookArray['helpers'][] = $action->getCustomActionAttributes()['class']; + } //deleteData contains either url or createDataKey, if it contains the former it needs special formatting if ($action->getType() !== "createData" && !array_key_exists(TestGenerator::REQUIRED_ENTITY_REFERENCE, $action->getCustomActionAttributes())) { - $actions = $this->buildWebDriverActionsMustacheArray($action, $actions, $index); + $actions = $this->buildModuleActionsMustacheArray($action, $actions); continue; } // add these as vars to be created a class level in the template - if ($action->getType() == 'createData') { + if ($action->getType() === 'createData') { $mustacheHookArray[self::MUSTACHE_VAR_TAG][] = [self::ENTITY_MERGE_KEY => $action->getStepKey()]; } @@ -161,7 +170,7 @@ private function buildHookMustacheArray($hookObj) } /** - * Takes an action object and array of generated action steps. Converst the action object into generated php and + * Takes an action object and array of generated action steps. Convert the action object into generated php and * appends the entry to the given array. The result is returned by the function. * * @param ActionObject $action @@ -169,14 +178,22 @@ private function buildHookMustacheArray($hookObj) * @return array * @throws TestReferenceException */ - private function buildWebDriverActionsMustacheArray($action, $actionEntries) + private function buildModuleActionsMustacheArray($action, $actionEntries) { - $step = TestGenerator::getInstance()->generateStepsPhp([$action], TestGenerator::SUITE_SCOPE, 'webDriver'); + $step = TestGenerator::getInstance()->generateStepsPhp( + [$action], + TestGenerator::SUITE_SCOPE, + self::FUNCTION_PLACEHOLDER + ); $rawPhp = str_replace(["\t"], "", $step); $multipleCommands = explode(PHP_EOL, $rawPhp, -1); $multipleCommands = array_filter($multipleCommands); foreach ($multipleCommands as $command) { - $actionEntries = $this->replaceReservedTesterFunctions($command . PHP_EOL, $actionEntries, 'webDriver'); + $actionEntries = $this->replaceReservedTesterFunctions( + $command . PHP_EOL, + $actionEntries, + self::FUNCTION_PLACEHOLDER + ); } return $actionEntries; @@ -196,11 +213,21 @@ private function replaceReservedTesterFunctions($formattedStep, $actionEntries, $formattedStep = rtrim($formattedStep); foreach (self::REPLACEMENT_ACTIONS as $testAction => $replacement) { $testActionCall = "\${$actor}->{$testAction}"; - if (substr($formattedStep, 0, strlen($testActionCall)) == $testActionCall) { + if (substr($formattedStep, 0, strlen($testActionCall)) === $testActionCall) { $resultingStep = str_replace($testActionCall, $replacement, $formattedStep); $actionEntries[] = ['action' => $resultingStep]; } else { - $actionEntries[] = ['action' => $formattedStep]; + $placeholder = self::FUNCTION_PLACEHOLDER; + $begin = self::FUNCTION_START; + $end = self::FUNCTION_END; + $resultingStep = preg_replace_callback( + self::FUNCTION_REPLACE_REGEX, + function ($matches) use ($placeholder, $begin, $end) { + return str_replace($placeholder, $begin . $matches[2] . $end, $matches[1]) . '('; + }, + $formattedStep + ); + $actionEntries[] = ['action' => $resultingStep]; } } @@ -221,16 +248,16 @@ private function buildPersistenceMustacheArray($action, $entityArray) $action->getCustomActionAttributes()[TestGenerator::REQUIRED_ENTITY_REFERENCE]; // append entries for any required entities to this entry - if (array_key_exists('requiredEntities', $action->getCustomActionAttributes())) { - $entityArray[self::REQUIRED_ENTITY_KEY] = - $this->buildReqEntitiesMustacheArray($action->getCustomActionAttributes()); + $requiredEntities = $this->buildReqEntitiesMustacheArray($action->getCustomActionAttributes()); + if (!array_key_exists(-1, $requiredEntities)) { + $entityArray[self::REQUIRED_ENTITY_KEY] = $requiredEntities; } // append entries for customFields if specified by the user. if (array_key_exists('customFields', $action->getCustomActionAttributes())) { $entityArray['customFields'] = $action->getStepKey() . 'Fields'; } - + return $entityArray; } @@ -251,7 +278,7 @@ private function buildReqEntitiesMustacheArray($customAttributes) continue; } - if ($attribute[ActionObjectExtractor::NODE_NAME] == 'requiredEntity') { + if ($attribute[ActionObjectExtractor::NODE_NAME] === 'requiredEntity') { $requiredEntities[] = [self::ENTITY_NAME_TAG => $attribute[TestGenerator::REQUIRED_ENTITY_REFERENCE]]; } } diff --git a/src/Magento/FunctionalTestingFramework/Suite/Handlers/SuiteObjectHandler.php b/src/Magento/FunctionalTestingFramework/Suite/Handlers/SuiteObjectHandler.php index bbaaf4379..75e86b3a8 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/Handlers/SuiteObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/Suite/Handlers/SuiteObjectHandler.php @@ -5,6 +5,9 @@ */ namespace Magento\FunctionalTestingFramework\Suite\Handlers; +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Exceptions\FastFailException; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\ObjectManager\ObjectHandlerInterface; @@ -13,6 +16,8 @@ use Magento\FunctionalTestingFramework\Suite\Parsers\SuiteDataParser; use Magento\FunctionalTestingFramework\Suite\Util\SuiteObjectExtractor; use Magento\FunctionalTestingFramework\Test\Util\ObjectExtractor; +use Magento\FunctionalTestingFramework\Util\GenerationErrorHandler; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; /** * Class SuiteObjectHandler @@ -53,11 +58,11 @@ private function __clone() * Function to enforce singleton design pattern * * @return ObjectHandlerInterface - * @throws XmlException + * @throws FastFailException */ public static function getInstance(): ObjectHandlerInterface { - if (self::$instance == null) { + if (self::$instance === null) { self::$instance = new SuiteObjectHandler(); self::$instance->initSuiteData(); } @@ -74,7 +79,9 @@ public static function getInstance(): ObjectHandlerInterface public function getObject($objectName): SuiteObject { if (!array_key_exists($objectName, $this->suiteObjects)) { - throw new TestReferenceException("Suite ${objectName} is not defined in xml."); + throw new TestReferenceException( + "Suite {$objectName} is not defined in xml or is invalid." + ); } return $this->suiteObjects[$objectName]; } @@ -93,6 +100,7 @@ public function getAllObjects(): array * Function which return all tests referenced by suites. * * @return array + * @throws TestFrameworkException */ public function getAllTestReferences(): array { @@ -114,11 +122,16 @@ public function getAllTestReferences(): array * * @return void * @SuppressWarnings(PHPMD.UnusedPrivateMethod) - * @throws XmlException + * @throws FastFailException */ private function initSuiteData() { - $suiteDataParser = ObjectManagerFactory::getObjectManager()->create(SuiteDataParser::class); + try { + $suiteDataParser = ObjectManagerFactory::getObjectManager()->create(SuiteDataParser::class); + } catch (\Exception $e) { + throw new FastFailException("Suite Data Parser Error: " . $e->getMessage()); + } + $suiteObjectExtractor = new SuiteObjectExtractor(); $this->suiteObjects = $suiteObjectExtractor->parseSuiteDataIntoObjects($suiteDataParser->readSuiteData()); } diff --git a/src/Magento/FunctionalTestingFramework/Suite/Objects/SuiteObject.php b/src/Magento/FunctionalTestingFramework/Suite/Objects/SuiteObject.php index 64820cf09..57d5c11f0 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/Objects/SuiteObject.php +++ b/src/Magento/FunctionalTestingFramework/Suite/Objects/SuiteObject.php @@ -6,6 +6,7 @@ namespace Magento\FunctionalTestingFramework\Suite\Objects; use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; use Magento\FunctionalTestingFramework\Filter\FilterInterface; use Magento\FunctionalTestingFramework\Test\Objects\TestHookObject; use Magento\FunctionalTestingFramework\Test\Objects\TestObject; @@ -82,6 +83,7 @@ public function getName() * Returns an array of Test Objects based on specifications in exclude and include arrays. * * @return array + * @throws TestFrameworkException */ public function getTests() { @@ -96,7 +98,7 @@ public function getTests() * @param TestObject[] $includeTests * @param TestObject[] $excludeTests * @return TestObject[] - * @throws \Exception + * @throws TestFrameworkException */ private function resolveTests($includeTests, $excludeTests) { diff --git a/src/Magento/FunctionalTestingFramework/Suite/Service/SuiteGeneratorService.php b/src/Magento/FunctionalTestingFramework/Suite/Service/SuiteGeneratorService.php new file mode 100644 index 000000000..58823e195 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Suite/Service/SuiteGeneratorService.php @@ -0,0 +1,160 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\FunctionalTestingFramework\Suite\Service; + +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; +use Magento\FunctionalTestingFramework\Suite\SuiteGenerator; +use Magento\FunctionalTestingFramework\Util\Path\FilePathFormatter; +use Magento\FunctionalTestingFramework\Util\TestGenerator; +use Symfony\Component\Yaml\Yaml; + +/** + * Class SuiteGeneratorService + */ +class SuiteGeneratorService +{ + /** + * Singleton SuiteGeneratorService Instance. + * + * @var SuiteGeneratorService + */ + private static $INSTANCE; + + /** + * SuiteGeneratorService constructor. + */ + private function __construct() + { + } + + /** + * Get CestFileCreatorUtil instance. + * + * @return SuiteGeneratorService + */ + public static function getInstance(): SuiteGeneratorService + { + if (!self::$INSTANCE) { + self::$INSTANCE = new SuiteGeneratorService(); + } + + return self::$INSTANCE; + } + + /** + * Function which takes the current config.yml array and clears any previous configuration for suite group object + * files. + * + * @return void + * @throws TestFrameworkException + */ + public function clearPreviousSessionConfigEntries(): void + { + $ymlArray = self::getYamlFileContents(); + $newYmlArray = $ymlArray; + // if the yaml entries haven't already been cleared + if (array_key_exists(SuiteGenerator::YAML_EXTENSIONS_TAG, $ymlArray)) { + $ymlEntries = $ymlArray[SuiteGenerator::YAML_EXTENSIONS_TAG][SuiteGenerator::YAML_ENABLED_TAG]; + + foreach ($ymlEntries as $key => $entry) { + if (preg_match('/(Group\\\\.*)/', $entry)) { + unset($newYmlArray[SuiteGenerator::YAML_EXTENSIONS_TAG][SuiteGenerator::YAML_ENABLED_TAG][$key]); + } + } + // needed for proper yml file generation based on indices + $newYmlArray[SuiteGenerator::YAML_EXTENSIONS_TAG][SuiteGenerator::YAML_ENABLED_TAG] = + array_values($newYmlArray[SuiteGenerator::YAML_EXTENSIONS_TAG][SuiteGenerator::YAML_ENABLED_TAG]); + } + + if (array_key_exists(SuiteGenerator::YAML_GROUPS_TAG, $newYmlArray)) { + unset($newYmlArray[SuiteGenerator::YAML_GROUPS_TAG]); + } + $ymlText = SuiteGenerator::YAML_COPYRIGHT_TEXT . Yaml::dump($newYmlArray, 10); + file_put_contents(self::getYamlConfigFilePath() . SuiteGenerator::YAML_CODECEPTION_CONFIG_FILENAME, $ymlText); + } + + /** + * Function which accepts a suite name and suite path and appends a new group entry to the codeception.yml.dist + * file in order to register the set of tests as a new group. Also appends group object location if required + * by suite. + * + * @param string $suiteName + * @param string $suitePath + * @param string|null $groupNamespace + * + * @return void + * @throws TestFrameworkException + */ + public function appendEntriesToConfig(string $suiteName, string $suitePath, ?string $groupNamespace): void + { + $relativeSuitePath = substr($suitePath, strlen(TESTS_BP)); + $relativeSuitePath = ltrim($relativeSuitePath, DIRECTORY_SEPARATOR); + $ymlArray = self::getYamlFileContents(); + + if (!array_key_exists(SuiteGenerator::YAML_GROUPS_TAG, $ymlArray)) { + $ymlArray[SuiteGenerator::YAML_GROUPS_TAG] = []; + } + + if ($groupNamespace) { + $ymlArray[SuiteGenerator::YAML_EXTENSIONS_TAG][SuiteGenerator::YAML_ENABLED_TAG][] = $groupNamespace; + } + + $ymlArray[SuiteGenerator::YAML_GROUPS_TAG][$suiteName] = [$relativeSuitePath]; + $ymlText = SuiteGenerator::YAML_COPYRIGHT_TEXT . Yaml::dump($ymlArray, 10); + file_put_contents(self::getYamlConfigFilePath() . SuiteGenerator::YAML_CODECEPTION_CONFIG_FILENAME, $ymlText); + } + + /** + * Function which takes a string which is the desired output directory (under _generated) and an array of tests + * relevant to the suite to be generated. The function takes this information and creates a new instance of the + * test generator which is then called to create all the test files for the suite. + * + * @param string $path + * @param array $tests + * + * @return void + * @throws TestReferenceException + */ + public function generateRelevantGroupTests(string $path, array $tests): void + { + $testGenerator = TestGenerator::getInstance($path, $tests); + $testGenerator->createAllTestFiles(null, []); + } + + /** + * Function to return contents of codeception.yml file for config changes. + * + * @return array + * @throws TestFrameworkException + */ + private static function getYamlFileContents(): array + { + $configYmlFile = self::getYamlConfigFilePath() . SuiteGenerator::YAML_CODECEPTION_CONFIG_FILENAME; + $defaultConfigYmlFile = self::getYamlConfigFilePath() . SuiteGenerator::YAML_CODECEPTION_DIST_FILENAME; + + if (file_exists($configYmlFile)) { + $ymlContents = file_get_contents($configYmlFile); + } else { + $ymlContents = file_get_contents($defaultConfigYmlFile); + } + + return Yaml::parse($ymlContents) ?? []; + } + + /** + * Static getter for the Config yml filepath (as path cannot be stored in a const). + * + * @return string + * @throws TestFrameworkException + */ + private static function getYamlConfigFilePath(): string + { + return FilePathFormatter::format(TESTS_BP); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Suite/SuiteGenerator.php b/src/Magento/FunctionalTestingFramework/Suite/SuiteGenerator.php index b40b48ae7..4c007ed95 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/SuiteGenerator.php +++ b/src/Magento/FunctionalTestingFramework/Suite/SuiteGenerator.php @@ -6,19 +6,22 @@ namespace Magento\FunctionalTestingFramework\Suite; -use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Exceptions\Collector\ExceptionCollector; +use Magento\FunctionalTestingFramework\Exceptions\FastFailException; use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Suite\Generators\GroupClassGenerator; use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; use Magento\FunctionalTestingFramework\Suite\Objects\SuiteObject; +use Magento\FunctionalTestingFramework\Suite\Service\SuiteGeneratorService; use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; use Magento\FunctionalTestingFramework\Util\Filesystem\DirSetupUtil; +use Magento\FunctionalTestingFramework\Util\GenerationErrorHandler; use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; use Magento\FunctionalTestingFramework\Util\Manifest\BaseTestManifest; use Magento\FunctionalTestingFramework\Util\Path\FilePathFormatter; use Magento\FunctionalTestingFramework\Util\TestGenerator; -use Symfony\Component\Yaml\Yaml; +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; /** * Class SuiteGenerator @@ -88,31 +91,36 @@ public static function getInstance(): SuiteGenerator * * @param BaseTestManifest $testManifest * @return void - * @throws \Exception + * @throws FastFailException */ public function generateAllSuites($testManifest) { $suites = $testManifest->getSuiteConfig(); foreach ($suites as $suiteName => $suiteContent) { - if (empty($suiteContent)) { - LoggingUtil::getInstance()->getLogger(self::class)->notification( - "Suite '" . $suiteName . "' contains no tests and won't be generated." . PHP_EOL, - [], - true - ); - continue; - } - $firstElement = array_values($suiteContent)[0]; + try { + if (empty($suiteContent)) { + LoggingUtil::getInstance()->getLogger(self::class)->notification( + "Suite '" . $suiteName . "' contains no tests and won't be generated.", + [], + true + ); + continue; + } + $firstElement = array_values($suiteContent)[0]; - // if the first element is a string we know that we simply have an array of tests - if (is_string($firstElement)) { - $this->generateSuiteFromTest($suiteName, $suiteContent); - } + // if the first element is a string we know that we simply have an array of tests + if (is_string($firstElement)) { + $this->generateSuiteFromTest($suiteName, $suiteContent); + } - // if our first element is an array we know that we have split the suites - if (is_array($firstElement)) { - $this->generateSplitSuiteFromTest($suiteName, $suiteContent); + // if our first element is an array we know that we have split the suites + if (is_array($firstElement)) { + $this->generateSplitSuiteFromTest($suiteName, $suiteContent); + } + } catch (FastFailException $e) { + throw $e; + } catch (\Exception $e) { } } } @@ -131,6 +139,108 @@ public function generateSuite($suiteName) $this->generateSuiteFromTest($suiteName, []); } + /** + * Function which generate Testgroupmembership file. + * + * @param object $testManifest + * @return void + * @throws \Exception + */ + public function generateTestgroupmembership($testManifest): void + { + $suites = $this->getSuitesDetails($testManifest); + + // Path to groups folder + $baseDir = FilePathFormatter::format(TESTS_MODULE_PATH); + $path = $baseDir .'_generated/groups'; + + $allGroupsContent = $this->readAllGroupFiles($path); + + // Output file path + $memberShipFilePath = $baseDir.'_generated/testgroupmembership.txt'; + $testCaseNumber = 0; + + if (!empty($allGroupsContent)) { + foreach ($allGroupsContent as $groupId => $groupInfo) { + foreach ($groupInfo as $testName) { + // If file has -g then it is test suite + if (str_contains($testName, '-g')) { + $suitename = explode(" ", $testName); + $suitename[1] = trim($suitename[1]); + + if (!empty($suites[$suitename[1]])) { + foreach ($suites[$suitename[1]] as $key => $test) { + $suiteTest = sprintf('%s:%s:%s:%s', $groupId, $key, $suitename[1], $test); + file_put_contents($memberShipFilePath, $suiteTest . PHP_EOL, FILE_APPEND); + } + } + } else { + $defaultSuiteTest = sprintf('%s:%s:%s', $groupId, $testCaseNumber, $testName); + file_put_contents($memberShipFilePath, $defaultSuiteTest, FILE_APPEND); + } + $testCaseNumber++; + } + $testCaseNumber = 0; + } + } + } + + /** + * Function to format suites details + * + * @param object $testManifest + * @return array $suites + */ + private function getSuitesDetails($testManifest): array + { + // Get suits and subsuites data array + $suites = $testManifest->getSuiteConfig(); + + // Add subsuites array[2nd dimension] to main array[1st dimension] to access it directly later + if (!empty($suites)) { + foreach ($suites as $subSuites) { + if (!empty($subSuites)) { + foreach ($subSuites as $subSuiteName => $suiteTestNames) { + if (!is_numeric($subSuiteName)) { + $suites[$subSuiteName] = $suiteTestNames; + } else { + continue; + } + } + } + } + } + return $suites; + } + + /** + * Function to read all group* text files inside /groups folder + * + * @param object $path + * @return array $allGroupsContent + */ + private function readAllGroupFiles($path): array + { + // Read all group files + if (is_dir($path)) { + $groupFiles = glob("$path/group*.txt"); + if ($groupFiles === false) { + throw new RuntimeException("glob(): error with '$path'"); + } + sort($groupFiles, SORT_NATURAL); + } + + // Read each file in the reverse order and form an array with groupId as key + $groupNumber = 0; + $allGroupsContent = []; + while (!empty($groupFiles)) { + $group = array_pop($groupFiles); + $allGroupsContent[$groupNumber] = file($group); + $groupNumber++; + } + return $allGroupsContent; + } + /** * Function which takes a suite name and a set of test names. The function then generates all relevant supporting * files and classes for the suite. The function takes an optional argument for suites which are split by a parallel @@ -140,9 +250,9 @@ public function generateSuite($suiteName) * @param array $tests * @param string $originalSuiteName * @return void - * @throws TestReferenceException - * @throws XmlException - * @throws TestFrameworkException + * @throws \Exception + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ private function generateSuiteFromTest($suiteName, $tests = [], $originalSuiteName = null) { @@ -150,25 +260,72 @@ private function generateSuiteFromTest($suiteName, $tests = [], $originalSuiteNa $fullPath = FilePathFormatter::format(TESTS_MODULE_PATH) . $relativePath . DIRECTORY_SEPARATOR; DirSetupUtil::createGroupDir($fullPath); + $exceptionCollector = new ExceptionCollector(); + try { + $relevantTests = []; + if (!empty($tests)) { + $this->validateTestsReferencedInSuite($suiteName, $tests, $originalSuiteName); + foreach ($tests as $testName) { + try { + $relevantTests[$testName] = TestObjectHandler::getInstance()->getObject($testName); + } catch (FastFailException $e) { + throw $e; + } catch (\Exception $e) { + $exceptionCollector->addError( + self::class, + "Unable to find relevant test \"{$testName}\" for suite \"{$suiteName}\"" + ); + } + } + } else { + $relevantTests = SuiteObjectHandler::getInstance()->getObject($suiteName)->getTests(); + } - $relevantTests = []; - if (!empty($tests)) { - $this->validateTestsReferencedInSuite($suiteName, $tests, $originalSuiteName); - foreach ($tests as $testName) { - $relevantTests[$testName] = TestObjectHandler::getInstance()->getObject($testName); + if (empty($relevantTests)) { + $exceptionCollector->reset(); + // There are suites that include no test on purpose for certain Magento edition. + // To keep backward compatibility, we will return with no error. + // This might inevitably hide some suite errors that are resulted by real broken tests. + if (file_exists($fullPath)) { + DirSetupUtil::rmdirRecursive($fullPath); + } + return; } - } else { - $relevantTests = SuiteObjectHandler::getInstance()->getObject($suiteName)->getTests(); - } - $this->generateRelevantGroupTests($suiteName, $relevantTests); - $groupNamespace = $this->generateGroupFile($suiteName, $relevantTests, $originalSuiteName); + try { + $this->generateRelevantGroupTests($suiteName, $relevantTests); + } catch (FastFailException $e) { + throw $e; + } catch (\Exception $e) { + $exceptionCollector->addError( + self::class, + "Failed to generate tests for suite \"{$suiteName}\"" + ); + } + + $groupNamespace = $this->generateGroupFile($suiteName, $relevantTests, $originalSuiteName); + + $this->appendEntriesToConfig($suiteName, $fullPath, $groupNamespace); + + if (MftfApplicationConfig::getConfig()->verboseEnabled() + && MftfApplicationConfig::getConfig()->getPhase() === MftfApplicationConfig::GENERATION_PHASE) { + print("suite {$suiteName} generated\n"); + } + LoggingUtil::getInstance()->getLogger(self::class)->info( + "suite generated", + ['suite' => $suiteName, 'relative_path' => $relativePath] + ); + } catch (FastFailException $e) { + throw $e; + } catch (\Exception $e) { + if (file_exists($fullPath)) { + DirSetupUtil::rmdirRecursive($fullPath); + } + $exceptionCollector->addError(self::class, $e->getMessage()); + GenerationErrorHandler::getInstance()->addError('suite', $suiteName, self::class . ': ' . $e->getMessage()); + } - $this->appendEntriesToConfig($suiteName, $fullPath, $groupNamespace); - LoggingUtil::getInstance()->getLogger(SuiteGenerator::class)->info( - "suite generated", - ['suite' => $suiteName, 'relative_path' => $relativePath] - ); + $this->throwCollectedExceptions($exceptionCollector); } /** @@ -209,7 +366,16 @@ private function validateTestsReferencedInSuite($suiteName, $testsReferenced, $o private function generateSplitSuiteFromTest($suiteName, $suiteContent) { foreach ($suiteContent as $suiteSplitName => $tests) { - $this->generateSuiteFromTest($suiteSplitName, $tests, $suiteName); + try { + $this->generateSuiteFromTest($suiteSplitName, $tests, $suiteName); + } catch (FastFailException $e) { + throw $e; + } catch (\Exception $e) { + // There are suites that include tests that reference tests from other Magento editions + // To keep backward compatibility, we will catch such exceptions with no error. + // This might inevitably hide some suite errors that are resulted by tests with broken references + //TODO MQE-2484 + } } } @@ -240,7 +406,7 @@ private function generateGroupFile($suiteName, $tests, $originalSuiteName) } else { $suiteObject = SuiteObjectHandler::getInstance()->getObject($suiteName); // we have to handle the case when there is a custom configuration for an existing suite. - if (count($suiteObject->getTests()) != count($tests)) { + if (count($suiteObject->getTests()) !== count($tests)) { return $this->generateGroupFile($suiteName, $tests, $suiteName); } } @@ -266,21 +432,7 @@ private function generateGroupFile($suiteName, $tests, $originalSuiteName) */ private function appendEntriesToConfig($suiteName, $suitePath, $groupNamespace) { - $relativeSuitePath = substr($suitePath, strlen(TESTS_BP)); - $relativeSuitePath = ltrim($relativeSuitePath, DIRECTORY_SEPARATOR); - - $ymlArray = self::getYamlFileContents(); - if (!array_key_exists(self::YAML_GROUPS_TAG, $ymlArray)) { - $ymlArray[self::YAML_GROUPS_TAG]= []; - } - - if ($groupNamespace) { - $ymlArray[self::YAML_EXTENSIONS_TAG][self::YAML_ENABLED_TAG][] = $groupNamespace; - } - $ymlArray[self::YAML_GROUPS_TAG][$suiteName] = [$relativeSuitePath]; - - $ymlText = self::YAML_COPYRIGHT_TEXT . Yaml::dump($ymlArray, 10); - file_put_contents(self::getYamlConfigFilePath() . self::YAML_CODECEPTION_CONFIG_FILENAME, $ymlText); + SuiteGeneratorService::getInstance()->appendEntriesToConfig($suiteName, $suitePath, $groupNamespace); } /** @@ -291,43 +443,23 @@ private function appendEntriesToConfig($suiteName, $suitePath, $groupNamespace) */ private static function clearPreviousSessionConfigEntries() { - $ymlArray = self::getYamlFileContents(); - $newYmlArray = $ymlArray; - // if the yaml entries haven't already been cleared - if (array_key_exists(self::YAML_EXTENSIONS_TAG, $ymlArray)) { - foreach ($ymlArray[self::YAML_EXTENSIONS_TAG][self::YAML_ENABLED_TAG] as $key => $entry) { - if (preg_match('/(Group\\\\.*)/', $entry)) { - unset($newYmlArray[self::YAML_EXTENSIONS_TAG][self::YAML_ENABLED_TAG][$key]); - } - } - - // needed for proper yml file generation based on indices - $newYmlArray[self::YAML_EXTENSIONS_TAG][self::YAML_ENABLED_TAG] = - array_values($newYmlArray[self::YAML_EXTENSIONS_TAG][self::YAML_ENABLED_TAG]); - } - - if (array_key_exists(self::YAML_GROUPS_TAG, $newYmlArray)) { - unset($newYmlArray[self::YAML_GROUPS_TAG]); - } - - $ymlText = self::YAML_COPYRIGHT_TEXT . Yaml::dump($newYmlArray, 10); - file_put_contents(self::getYamlConfigFilePath() . self::YAML_CODECEPTION_CONFIG_FILENAME, $ymlText); + SuiteGeneratorService::getInstance()->clearPreviousSessionConfigEntries(); } /** * Function which takes a string which is the desired output directory (under _generated) and an array of tests - * relevant to the suite to be generated. The function takes this information and creates a new instance of the test - * generator which is then called to create all the test files for the suite. + * relevant to the suite to be generated. The function takes this information and creates a new instance of the + * test generator which is then called to create all the test files for the suite. * * @param string $path * @param array $tests + * * @return void * @throws TestReferenceException */ private function generateRelevantGroupTests($path, $tests) { - $testGenerator = TestGenerator::getInstance($path, $tests); - $testGenerator->createAllTestFiles(null, []); + SuiteGeneratorService::getInstance()->generateRelevantGroupTests($path, $tests); } /** @@ -342,33 +474,25 @@ private static function clearPreviousGroupPreconditions() } /** - * Function to return contents of codeception.yml file for config changes. + * Log error and throw collected exceptions * - * @return array + * @param ExceptionCollector $exceptionCollector + * @return void + * @throws \Exception */ - private static function getYamlFileContents() + private function throwCollectedExceptions($exceptionCollector) { - $configYmlFile = self::getYamlConfigFilePath() . self::YAML_CODECEPTION_CONFIG_FILENAME; - $defaultConfigYmlFile = self::getYamlConfigFilePath() . self::YAML_CODECEPTION_DIST_FILENAME; - - $ymlContents = null; - if (file_exists($configYmlFile)) { - $ymlContents = file_get_contents($configYmlFile); - } else { - $ymlContents = file_get_contents($defaultConfigYmlFile); + if (!empty($exceptionCollector->getErrors())) { + foreach ($exceptionCollector->getErrors() as $file => $errorMessage) { + if (is_array($errorMessage)) { + foreach (array_unique($errorMessage) as $message) { + LoggingUtil::getInstance()->getLogger(self::class)->error(trim($message)); + } + } else { + LoggingUtil::getInstance()->getLogger(self::class)->error(trim($errorMessage)); + } + } + $exceptionCollector->throwException(); } - - return Yaml::parse($ymlContents) ?? []; - } - - /** - * Static getter for the Config yml filepath (as path cannot be stored in a const) - * - * @return string - * @throws TestFrameworkException - */ - private static function getYamlConfigFilePath() - { - return FilePathFormatter::format(TESTS_BP); } } diff --git a/src/Magento/FunctionalTestingFramework/Suite/Util/SuiteObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Suite/Util/SuiteObjectExtractor.php index 90fbddd6e..6af9070ba 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/Util/SuiteObjectExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Suite/Util/SuiteObjectExtractor.php @@ -6,6 +6,8 @@ namespace Magento\FunctionalTestingFramework\Suite\Util; use Exception; +use Magento\FunctionalTestingFramework\Exceptions\FastFailException; +use Magento\FunctionalTestingFramework\Exceptions\GenerationErrorCollector; use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Suite\Objects\SuiteObject; use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; @@ -13,12 +15,19 @@ use Magento\FunctionalTestingFramework\Test\Util\BaseObjectExtractor; use Magento\FunctionalTestingFramework\Test\Util\TestHookObjectExtractor; use Magento\FunctionalTestingFramework\Test\Util\TestObjectExtractor; -use Magento\FunctionalTestingFramework\Util\ModuleResolver; -use Magento\FunctionalTestingFramework\Util\Path\FilePathFormatter; +use Magento\FunctionalTestingFramework\Util\GenerationErrorHandler; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; use Magento\FunctionalTestingFramework\Util\Validation\NameValidationUtil; -use Symfony\Component\Finder\Finder; use Magento\FunctionalTestingFramework\Util\ModulePathExtractor; +use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +/** + * Class SuiteObjectExtractor + * @package Magento\FunctionalTestingFramework\Suite\Util + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class SuiteObjectExtractor extends BaseObjectExtractor { const SUITE_ROOT_TAG = 'suites'; @@ -49,10 +58,11 @@ public function __construct() * * @param array $parsedSuiteData * @return array - * @throws XmlException - * @throws \Exception + * @throws FastFailException * * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function parseSuiteDataIntoObjects($parsedSuiteData) { @@ -71,28 +81,82 @@ public function parseSuiteDataIntoObjects($parsedSuiteData) $this->validateSuiteName($parsedSuite); - //extract include and exclude references - $groupTestsToInclude = $parsedSuite[self::INCLUDE_TAG_NAME] ?? []; - $groupTestsToExclude = $parsedSuite[self::EXCLUDE_TAG_NAME] ?? []; + try { + // extract include and exclude references + $groupTestsToInclude = $parsedSuite[self::INCLUDE_TAG_NAME] ?? []; + $groupTestsToExclude = $parsedSuite[self::EXCLUDE_TAG_NAME] ?? []; + + // resolve references as test objects + // continue if failed in include + $include = $this->extractTestObjectsFromSuiteRef($groupTestsToInclude); + $includeTests = $include['objects'] ?? []; + $stepError = $include['status'] ?? 0; + $includeMessage = ''; + if ($stepError !== 0) { + $includeMessage = "ERROR: " . strval($stepError) . " test(s) not included for suite " + . $parsedSuite[self::NAME]; + } + + // it's ok if failed in exclude + $exclude = $this->extractTestObjectsFromSuiteRef($groupTestsToExclude); + $excludeTests = $exclude['objects'] ?? []; - //resolve references as test objects - $includeTests = $this->extractTestObjectsFromSuiteRef($groupTestsToInclude); - $excludeTests = $this->extractTestObjectsFromSuiteRef($groupTestsToExclude); + // parse any object hooks + $suiteHooks = $this->parseObjectHooks($parsedSuite); + + // log error if suite is empty + if ($this->isSuiteEmpty($suiteHooks, $includeTests, $excludeTests)) { + LoggingUtil::getInstance()->getLogger(self::class)->error( + "Unable to parse suite " . $parsedSuite[self::NAME] . ". Suite must not be empty." + ); + + GenerationErrorHandler::getInstance()->addError( + 'suite', + $parsedSuite[self::NAME], + self::class . ': ' . 'Suite must not be empty.' + ); - // parse any object hooks - $suiteHooks = $this->parseObjectHooks($parsedSuite); + continue; + }; - //throw an exception if suite is empty - if ($this->isSuiteEmpty($suiteHooks, $includeTests, $excludeTests)) { - throw new XmlException(sprintf( - "Suites must not be empty. Suite: \"%s\"", - $parsedSuite[self::NAME] - )); - }; + // add all test if include tests is completely empty + if (empty($includeTests)) { + $includeTests = TestObjectHandler::getInstance()->getAllObjects(); + } - // add all test if include tests is completely empty - if (empty($includeTests)) { - $includeTests = TestObjectHandler::getInstance()->getAllObjects(); + if (!empty($includeMessage)) { + LoggingUtil::getInstance()->getLogger(self::class)->error($includeMessage); + if (MftfApplicationConfig::getConfig()->verboseEnabled() + && MftfApplicationConfig::getConfig()->getPhase() === MftfApplicationConfig::GENERATION_PHASE + ) { + print($includeMessage); + } + + GenerationErrorHandler::getInstance()->addError( + 'suite', + $parsedSuite[self::NAME], + self::class . ': ' . $includeMessage, + true + ); + } + } catch (FastFailException $e) { + throw $e; + } catch (\Exception $e) { + LoggingUtil::getInstance()->getLogger(self::class)->error( + "Unable to parse suite " . $parsedSuite[self::NAME] . "\n" . $e->getMessage() + ); + if (MftfApplicationConfig::getConfig()->verboseEnabled() + && MftfApplicationConfig::getConfig()->getPhase() === MftfApplicationConfig::GENERATION_PHASE) { + print("ERROR: Unable to parse suite " . $parsedSuite[self::NAME] . "\n"); + } + + GenerationErrorHandler::getInstance()->addError( + 'suite', + $parsedSuite[self::NAME], + self::class . ': Unable to parse suite ' . $e->getMessage() + ); + + continue; } // create the new suite object @@ -114,14 +178,14 @@ public function parseSuiteDataIntoObjects($parsedSuiteData) * * @param array $parsedSuite * @return void - * @throws XmlException + * @throws FastFailException */ private function validateSuiteName($parsedSuite) { //check if name used is using special char or the "default" reserved name NameValidationUtil::validateName($parsedSuite[self::NAME], 'Suite'); - if ($parsedSuite[self::NAME] == 'default') { - throw new XmlException("A Suite can not have the name \"default\""); + if ($parsedSuite[self::NAME] === 'default') { + throw new FastFailException("A Suite can not have the name \"default\""); } $suiteName = $parsedSuite[self::NAME]; @@ -134,7 +198,7 @@ private function validateSuiteName($parsedSuite) } $exceptionmessage = "\"Suite names and Group names can not have the same value. \t\n" . "Suite: \"{$suiteName}\" also exists as a group annotation in: \n{$testGroupConflictsFileNames}"; - throw new XmlException($exceptionmessage); + throw new FastFailException($exceptionmessage); } } @@ -144,27 +208,35 @@ private function validateSuiteName($parsedSuite) * @param array $parsedSuite * @return array * @throws XmlException + * @throws TestReferenceException */ private function parseObjectHooks($parsedSuite) { $suiteHooks = []; if (array_key_exists(TestObjectExtractor::TEST_BEFORE_HOOK, $parsedSuite)) { - $suiteHooks[TestObjectExtractor::TEST_BEFORE_HOOK] = $this->testHookObjectExtractor->extractHook( + $hookObject = $this->testHookObjectExtractor->extractHook( $parsedSuite[self::NAME], TestObjectExtractor::TEST_BEFORE_HOOK, $parsedSuite[TestObjectExtractor::TEST_BEFORE_HOOK] ); + // Validate hook actions + $hookObject->getActions(); + $suiteHooks[TestObjectExtractor::TEST_BEFORE_HOOK] = $hookObject; } + if (array_key_exists(TestObjectExtractor::TEST_AFTER_HOOK, $parsedSuite)) { - $suiteHooks[TestObjectExtractor::TEST_AFTER_HOOK] = $this->testHookObjectExtractor->extractHook( + $hookObject = $this->testHookObjectExtractor->extractHook( $parsedSuite[self::NAME], TestObjectExtractor::TEST_AFTER_HOOK, $parsedSuite[TestObjectExtractor::TEST_AFTER_HOOK] ); + // Validate hook actions + $hookObject->getActions(); + $suiteHooks[TestObjectExtractor::TEST_AFTER_HOOK] = $hookObject; } - if (count($suiteHooks) == 1) { + if (count($suiteHooks) === 1) { throw new XmlException(sprintf( "Suites that contain hooks must contain both a 'before' and an 'after' hook. Suite: \"%s\"", $parsedSuite[self::NAME] @@ -183,7 +255,7 @@ private function parseObjectHooks($parsedSuite) */ private function isSuiteEmpty($suiteHooks, $includeTests, $excludeTests) { - $noHooks = count($suiteHooks) == 0 || + $noHooks = count($suiteHooks) === 0 || ( empty($suiteHooks['before']->getActions()) && empty($suiteHooks['after']->getActions()) @@ -201,35 +273,53 @@ private function isSuiteEmpty($suiteHooks, $includeTests, $excludeTests) * * @param array $suiteReferences * @return array - * @throws \Exception + * @throws FastFailException */ private function extractTestObjectsFromSuiteRef($suiteReferences) { $testObjectList = []; + $errCount = 0; foreach ($suiteReferences as $suiteRefName => $suiteRefData) { if (!is_array($suiteRefData)) { continue; } - switch ($suiteRefData[self::NODE_NAME]) { - case self::TEST_TAG_NAME: - $testObject = TestObjectHandler::getInstance()->getObject($suiteRefData[self::NAME]); - $testObjectList[$testObject->getName()] = $testObject; - break; - case self::GROUP_TAG_NAME: - $testObjectList = $testObjectList + - TestObjectHandler::getInstance()->getTestsByGroup($suiteRefData[self::NAME]); - break; - case self::MODULE_TAG_NAME: - $testObjectList = array_merge( - $testObjectList, - $this->getTestsByModuleName($suiteRefData[self::NAME]) - ); - break; + try { + switch ($suiteRefData[self::NODE_NAME]) { + case self::TEST_TAG_NAME: + $testObject = TestObjectHandler::getInstance()->getObject($suiteRefData[self::NAME]); + $testObjectList[$testObject->getName()] = $testObject; + break; + case self::GROUP_TAG_NAME: + $testObjectList = $testObjectList + + TestObjectHandler::getInstance()->getTestsByGroup($suiteRefData[self::NAME]); + break; + case self::MODULE_TAG_NAME: + $testObjectList = array_merge( + $testObjectList, + $this->getTestsByModuleName($suiteRefData[self::NAME]) + ); + break; + } + } catch (FastFailException $e) { + throw $e; + } catch (\Exception $e) { + $errCount++; + LoggingUtil::getInstance()->getLogger(self::class)->error( + "Unable to find <" + . $suiteRefData[self::NODE_NAME] + . "> reference " + . $suiteRefData[self::NAME] + . " for suite " + . $suiteRefData[self::NAME] + ); } } - return $testObjectList; + return [ + 'status' => $errCount, + 'objects' => $testObjectList + ]; } /** diff --git a/src/Magento/FunctionalTestingFramework/Suite/views/SuiteClass.mustache b/src/Magento/FunctionalTestingFramework/Suite/views/SuiteClass.mustache index b98405c03..599b5791e 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/views/SuiteClass.mustache +++ b/src/Magento/FunctionalTestingFramework/Suite/views/SuiteClass.mustache @@ -2,8 +2,17 @@ namespace Group; +use Facebook\WebDriver\Remote\RemoteWebDriver; +use Facebook\WebDriver\Remote\DesiredCapabilities; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\PersistedObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; +use Magento\FunctionalTestingFramework\Module\MagentoWebDriver; +use Magento\FunctionalTestingFramework\Module\MagentoAssert; +use Magento\FunctionalTestingFramework\Module\MagentoActionProxies; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Codeception\Lib\ModuleContainer; +use Codeception\Module; +use Facebook\WebDriver\Chrome\ChromeOptions; /** * Group class is Codeception Extension which is allowed to handle to all internal events. @@ -20,12 +29,31 @@ class {{suiteName}} extends \Codeception\GroupObject private $testCount = {{testCount}}; private $preconditionFailure = null; private $currentTestRun = 0; + {{#helpers}} + /** + * @var \Magento\FunctionalTestingFramework\Helper\HelperContainer $helperContainer + */ + private $helperContainer; + {{/helpers}} private static $HOOK_EXECUTION_INIT = "\n/******** Beginning execution of {{suiteName}} suite %s block ********/\n"; private static $HOOK_EXECUTION_END = "\n/******** Execution of {{suiteName}} suite %s block complete ********/\n"; + /** @var MagentoWebDriver */ + private $webDriver; + /** @var ModuleContainer */ + private $moduleContainer; {{#before}} public function _before(\Codeception\Event\TestEvent $e) { + $this->webDriver = $this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver'); + $this->moduleContainer = $this->webDriver->getModuleContainer(); + {{#helpers}} + /** @var \Magento\FunctionalTestingFramework\Helper\HelperContainer $helperContainer */ + $this->helperContainer = $this->getModule('\Magento\FunctionalTestingFramework\Helper\HelperContainer'); + {{/helpers}} + {{#helpers}} + $this->helperContainer->create("{{ . }}"); + {{/helpers}} // increment test count per execution $this->currentTestRun++; $this->executePreConditions(); @@ -36,24 +64,24 @@ class {{suiteName}} extends \Codeception\GroupObject } } - private function executePreConditions() { if ($this->currentTestRun == 1) { + $this->testCount = $this->getTestCount(); + print sprintf(self::$HOOK_EXECUTION_INIT, "before"); try { {{> testActions}} - - // reset configuration and close session - $this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver')->_resetConfig(); - $webDriver->webDriver->close(); - $webDriver->webDriver = null; - } catch (\Exception $exception) { $this->preconditionFailure = $exception->getMessage(); } + // reset configuration and close session + $this->webDriver->_resetConfig(); + $this->webDriver->webDriver->close(); + $this->webDriver->webDriver = null; + print sprintf(self::$HOOK_EXECUTION_END, "before"); } } @@ -65,7 +93,6 @@ class {{suiteName}} extends \Codeception\GroupObject $this->executePostConditions($e); } - private function executePostConditions(\Codeception\Event\TestEvent $e) { if ($this->currentTestRun == $this->testCount) { @@ -78,7 +105,7 @@ class {{suiteName}} extends \Codeception\GroupObject //Access private TestResultObject to find stack and if there are any errors (as opposed to failures) $testResultObject = call_user_func(\Closure::bind( function () use ($cest) { - return $cest->getTestResultObject(); + return $cest->getResultAggregator(); }, $cest )); @@ -86,7 +113,7 @@ class {{suiteName}} extends \Codeception\GroupObject if (!empty($errors)) { foreach ($errors as $error) { - if ($error->failedTest()->getTestMethod() == $cest->getName()) { + if ($error->getTest()->getTestMethod() == $cest->getName()) { // Do not attempt to run _after if failure was in the _after block // Try to run _after but catch exceptions to prevent them from overwriting original failure. print("LAST TEST IN SUITE FAILED, TEST AFTER MAY NOT BE SUCCESSFUL\n"); @@ -99,8 +126,85 @@ class {{suiteName}} extends \Codeception\GroupObject } PersistedObjectHandler::getInstance()->clearSuiteObjects(); + + $this->closeSession($this->webDriver); + print sprintf(self::$HOOK_EXECUTION_END, "after"); } } {{/after}} -} + + /** + * Close session method closes current session. + * If config 'close_all_sessions' is set to 'true' all sessions will be closed. + * + * return void + */ + private function closeSession(): void + { + $webDriverConfig = $this->webDriver->_getConfig(); + $this->webDriver->_closeSession(); + if (isset($webDriverConfig['close_all_sessions']) && $webDriverConfig['close_all_sessions'] === "true") { + $wdHost = sprintf( + '%s://%s:%s%s', + $webDriverConfig['protocol'], + $webDriverConfig['host'], + $webDriverConfig['port'], + $webDriverConfig['path'] + ); + $availableSessions = RemoteWebDriver::getAllSessions($wdHost); + foreach ($availableSessions as $session) { + try { + $chromeOptions = new ChromeOptions(); + $capabilities = DesiredCapabilities::chrome(); + $remoteWebDriver = RemoteWebDriver::createBySessionID($session['id'], $wdHost, + $webDriverConfig['connection_timeout'], $webDriverConfig['request_timeout'], true, $capabilities); + $remoteWebDriver->quit(); + } catch (\Exception $exception) { + print("Failed trying to quit WebDriver session. Exception message: " . $exception->getMessage() . " Test execution will continue." . PHP_EOL); + // Session already closed so nothing to do + } + } + } + } + + /** + * Return the module for an action. + * + * @param string $action + * @return Module + * @throws \Exception + */ + private function getModuleForAction($action) + { + $module = $this->moduleContainer->moduleForAction($action); + if ($module === null) { + throw new TestFrameworkException('Invalid action "' . $action . '"' . PHP_EOL); + } + return $module; + } + + /** + * Counts how many tests in group. + * + * @return integer + */ + private function getTestCount() + { + $config = $this->getGlobalConfig(); + if (empty($config['groups']) || empty($config['groups'][self::$group])) { + return $this->testCount; + } + $pathToGroupDir = TESTS_BP . DIRECTORY_SEPARATOR . array_first($config['groups'][self::$group]); + $pathToGroupCests = $pathToGroupDir . "*Cest.php"; + + $files = glob($pathToGroupCests); + if (is_array($files)) { + $qty = count($files); + print('In a group "' . self::$group . '" suite executor found ' . $qty . ' tests.' . PHP_EOL); + return $qty; + } + + return $this->testCount; + } +} \ No newline at end of file diff --git a/src/Magento/FunctionalTestingFramework/Suite/views/partials/testActions.mustache b/src/Magento/FunctionalTestingFramework/Suite/views/partials/testActions.mustache index 2ed5e8d5e..c07958778 100644 --- a/src/Magento/FunctionalTestingFramework/Suite/views/partials/testActions.mustache +++ b/src/Magento/FunctionalTestingFramework/Suite/views/partials/testActions.mustache @@ -1,15 +1,10 @@ {{#actions}} {{#webDriverInit}} -$webDriver = $this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver'); - -// close any open sessions -if ($webDriver->webDriver != null) { - $webDriver->webDriver->close(); - $webDriver->webDriver = null; +if ($this->webDriver->webDriver != null) { + $this->webDriver->_restart(); +} else { + $this->webDriver->_initializeSession(); } - -// initialize the webdriver session -$webDriver->_initializeSession(); {{/webDriverInit}} {{#action}} {{{action}}} @@ -18,9 +13,9 @@ $webDriver->_initializeSession(); PersistedObjectHandler::getInstance()->createEntity( "{{stepKey}}", "suite", - "{{entityName}}"{{#requiredEntities}}, - [$this->{{entityName}}{{^last}}, {{/last}}]{{/requiredEntities}}{{#customFields}}, - ${{customFields}}{{/customFields}} + "{{entityName}}", + [{{#requiredEntities}}"{{entityName}}"{{^last}}, {{/last}}{{/requiredEntities}}]{{#customFields}}, + ${{customFields}}{{/customFields}} ); {{/createData}} {{#deleteData}} diff --git a/src/Magento/FunctionalTestingFramework/System/Code/ClassReader.php b/src/Magento/FunctionalTestingFramework/System/Code/ClassReader.php index 5f44ea104..8408f361b 100644 --- a/src/Magento/FunctionalTestingFramework/System/Code/ClassReader.php +++ b/src/Magento/FunctionalTestingFramework/System/Code/ClassReader.php @@ -20,6 +20,7 @@ class ClassReader * @param string $method * @return array|null * @throws \ReflectionException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function getParameters($className, $method) { @@ -31,9 +32,13 @@ public function getParameters($className, $method) /** @var $parameter \ReflectionParameter */ foreach ($method->getParameters() as $parameter) { try { + $paramType = $parameter->getType(); + $name = ($paramType && method_exists($paramType, 'isBuiltin') && !$paramType->isBuiltin()) + ? new \ReflectionClass($paramType->getName()) + : null; $result[$parameter->getName()] = [ $parameter->getName(), - ($parameter->getClass() !== null) ? $parameter->getClass()->getName() : null, + $name !== null ? $name->getName() : null, !$parameter->isOptional(), $parameter->isOptional() ? $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null : diff --git a/src/Magento/FunctionalTestingFramework/Test/Config/Converter/Dom/Flat.php b/src/Magento/FunctionalTestingFramework/Test/Config/Converter/Dom/Flat.php index 0769f5ebf..23d8d5331 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Config/Converter/Dom/Flat.php +++ b/src/Magento/FunctionalTestingFramework/Test/Config/Converter/Dom/Flat.php @@ -77,7 +77,7 @@ public function convertXml(\DOMNode $source, $basePath = '') $value = []; /** @var \DOMNode $node */ foreach ($source->childNodes as $node) { - if ($node->nodeType == XML_ELEMENT_NODE) { + if ($node->nodeType === XML_ELEMENT_NODE) { $nodeName = $node->nodeName; $nodePath = $basePath . '/' . $nodeName; $arrayKeyAttribute = $this->arrayNodeConfig->getAssocArrayKeyAttribute($nodePath); @@ -90,7 +90,7 @@ public function convertXml(\DOMNode $source, $basePath = '') ); } - if ($nodeName == self::REMOVE_ACTION) { + if ($nodeName === self::REMOVE_ACTION) { // Check to see if the test extends for this remove action $parentHookExtends = in_array($node->parentNode->nodeName, self::TEST_HOOKS) && !empty($node->parentNode->parentNode->getAttribute('extends')); @@ -121,12 +121,12 @@ public function convertXml(\DOMNode $source, $basePath = '') } else { $value[$nodeName] = $nodeData; } - } elseif ($node->nodeType == XML_CDATA_SECTION_NODE - || ($node->nodeType == XML_TEXT_NODE && trim($node->nodeValue) != '') + } elseif ($node->nodeType === XML_CDATA_SECTION_NODE + || ($node->nodeType === XML_TEXT_NODE && trim($node->nodeValue) !== '') ) { $value = $node->nodeValue; break; - } elseif ($node->nodeType == XML_COMMENT_NODE && + } elseif ($node->nodeType === XML_COMMENT_NODE && in_array($node->parentNode->nodeName, self::VALID_COMMENT_PARENT)) { $uniqid = uniqid($node->nodeName); $value[$uniqid] = [ @@ -163,7 +163,7 @@ protected function getNodeAttributes(\DOMNode $node) $attributes = $node->attributes ?: []; /** @var \DOMNode $attribute */ foreach ($attributes as $attribute) { - if ($attribute->nodeType == XML_ATTRIBUTE_NODE) { + if ($attribute->nodeType === XML_ATTRIBUTE_NODE) { $result[$attribute->nodeName] = $attribute->nodeValue; } } diff --git a/src/Magento/FunctionalTestingFramework/Test/Config/Dom.php b/src/Magento/FunctionalTestingFramework/Test/Config/Dom.php index 7ded754be..89f779968 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Config/Dom.php +++ b/src/Magento/FunctionalTestingFramework/Test/Config/Dom.php @@ -173,7 +173,7 @@ protected function appendMergePointerToActions($testNode, $insertType, $insertKe $childNodes = $testNode->childNodes; $previousStepKey = $insertKey; $actionInsertType = ActionObject::MERGE_ACTION_ORDER_AFTER; - if ($insertType == self::TEST_MERGE_POINTER_BEFORE) { + if ($insertType === self::TEST_MERGE_POINTER_BEFORE) { $actionInsertType = ActionObject::MERGE_ACTION_ORDER_BEFORE; } for ($i = 0; $i < $childNodes->length; $i++) { diff --git a/src/Magento/FunctionalTestingFramework/Test/Handlers/ActionGroupObjectHandler.php b/src/Magento/FunctionalTestingFramework/Test/Handlers/ActionGroupObjectHandler.php index 7f1d49d5c..fb4dac688 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Handlers/ActionGroupObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/Test/Handlers/ActionGroupObjectHandler.php @@ -6,6 +6,7 @@ namespace Magento\FunctionalTestingFramework\Test\Handlers; use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\ObjectManager\ObjectHandlerInterface; use Magento\FunctionalTestingFramework\ObjectManagerFactory; use Magento\FunctionalTestingFramework\Test\Objects\ActionGroupObject; @@ -49,6 +50,7 @@ class ActionGroupObjectHandler implements ObjectHandlerInterface * Singleton getter for instance of ActionGroupObjectHandler * * @return ActionGroupObjectHandler + * @throws XmlException */ public static function getInstance(): ActionGroupObjectHandler { @@ -61,6 +63,7 @@ public static function getInstance(): ActionGroupObjectHandler /** * ActionGroupObjectHandler constructor. + * @throws XmlException */ private function __construct() { @@ -73,6 +76,8 @@ private function __construct() * * @param string $actionGroupName * @return ActionGroupObject + * @throws TestFrameworkException + * @throws XmlException */ public function getObject($actionGroupName) { @@ -88,6 +93,8 @@ public function getObject($actionGroupName) * Function to return all objects for which the handler is responsible * * @return array + * @throws TestFrameworkException + * @throws XmlException */ public function getAllObjects(): array { @@ -101,6 +108,7 @@ public function getAllObjects(): array * Method which populates field array with objects from parsed action_group.xml * * @return void + * @throws XmlException * @SuppressWarnings(PHPMD.UnusedPrivateMethod) */ private function initActionGroups() @@ -137,14 +145,15 @@ private function initActionGroups() * * @param ActionGroupObject $actionGroupObject * @return ActionGroupObject + * @throws XmlException * @throws TestFrameworkException */ private function extendActionGroup($actionGroupObject): ActionGroupObject { if ($actionGroupObject->getParentName() !== null) { - if ($actionGroupObject->getParentName() == $actionGroupObject->getName()) { + if ($actionGroupObject->getParentName() === $actionGroupObject->getName()) { throw new TestFrameworkException( - "Mftf Action Group can not extend from itself: " . $actionGroupObject->getName() + 'Mftf Action Group can not extend from itself: ' . $actionGroupObject->getName() ); } return $this->extendUtil->extendActionGroup($actionGroupObject); diff --git a/src/Magento/FunctionalTestingFramework/Test/Handlers/TestObjectHandler.php b/src/Magento/FunctionalTestingFramework/Test/Handlers/TestObjectHandler.php index 109237a77..2361c5938 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Handlers/TestObjectHandler.php +++ b/src/Magento/FunctionalTestingFramework/Test/Handlers/TestObjectHandler.php @@ -5,7 +5,8 @@ */ namespace Magento\FunctionalTestingFramework\Test\Handlers; -use Magento\FunctionalTestingFramework\Exceptions\Collector\ExceptionCollector; +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Exceptions\FastFailException; use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; use Magento\FunctionalTestingFramework\Exceptions\XmlException; @@ -15,11 +16,14 @@ use Magento\FunctionalTestingFramework\Test\Parsers\TestDataParser; use Magento\FunctionalTestingFramework\Test\Util\ObjectExtensionUtil; use Magento\FunctionalTestingFramework\Test\Util\TestObjectExtractor; +use Magento\FunctionalTestingFramework\Util\GenerationErrorHandler; use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; use Magento\FunctionalTestingFramework\Util\Validation\NameValidationUtil; /** * Class TestObjectHandler + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class TestObjectHandler implements ObjectHandlerInterface { @@ -51,13 +55,14 @@ class TestObjectHandler implements ObjectHandlerInterface * Singleton method to return TestObjectHandler. * * @return TestObjectHandler - * @throws XmlException + * @throws FastFailException + * @throws TestFrameworkException */ - public static function getInstance() + public static function getInstance($validateAnnotations = true) { if (!self::$testObjectHandler) { self::$testObjectHandler = new TestObjectHandler(); - self::$testObjectHandler->initTestData(); + self::$testObjectHandler->initTestData($validateAnnotations); } return self::$testObjectHandler; @@ -81,7 +86,7 @@ private function __construct() public function getObject($testName) { if (!array_key_exists($testName, $this->tests)) { - throw new TestReferenceException("Test ${testName} not defined in xml."); + throw new TestReferenceException("Test {$testName} not defined in xml."); } $testObject = $this->tests[$testName]; @@ -92,13 +97,41 @@ public function getObject($testName) * Returns all tests parsed from xml indexed by testName. * * @return array + * @throws FastFailException + * @throws TestFrameworkException */ public function getAllObjects() { + $errCount = 0; $testObjects = []; foreach ($this->tests as $testName => $test) { - $testObjects[$testName] = $this->extendTest($test); + try { + $testObjects[$testName] = $this->extendTest($test); + } catch (FastFailException $exception) { + throw $exception; + } catch (\Exception $exception) { + $errCount++; + LoggingUtil::getInstance()->getLogger(self::class)->error( + "Unable to extend test " . $testName . "\n" . $exception->getMessage() + ); + GenerationErrorHandler::getInstance()->addError( + 'test', + $testName, + self::class . ': Unable to extend test ' . $exception->getMessage() + ); + } + } + + if ($errCount > 0 + && MftfApplicationConfig::getConfig()->verboseEnabled() + && MftfApplicationConfig::getConfig()->getPhase() === MftfApplicationConfig::GENERATION_PHASE) { + print( + "ERROR: " + . strval($errCount) + . " Test(s) cannot be extended in TestObjectHandler::getAllObjects(). See mftf.log for details." + ); } + return $testObjects; } @@ -107,58 +140,129 @@ public function getAllObjects() * * @param string $groupName * @return TestObject[] + * @throws FastFailException + * @throws TestFrameworkException */ public function getTestsByGroup($groupName) { + $errCount = 0; $relevantTests = []; foreach ($this->tests as $test) { - /** @var TestObject $test */ - if (in_array($groupName, $test->getAnnotationByName('group'))) { - $relevantTests[$test->getName()] = $this->extendTest($test); - continue; + try { + /** @var TestObject $test */ + if (in_array($groupName, $test->getAnnotationByName('group'))) { + $relevantTests[$test->getName()] = $this->extendTest($test); + } + } catch (FastFailException $exception) { + throw $exception; + } catch (\Exception $exception) { + $errCount++; + $message = "Unable to reference test " + . $test->getName() + . " for group {$groupName}\n" + . $exception->getMessage(); + LoggingUtil::getInstance()->getLogger(self::class)->error($message); + + GenerationErrorHandler::getInstance()->addError( + 'test', + $test->getName(), + self::class . ': ' . $message + ); } } + if ($errCount > 0 + && MftfApplicationConfig::getConfig()->verboseEnabled() + && MftfApplicationConfig::getConfig()->getPhase() === MftfApplicationConfig::GENERATION_PHASE) { + print( + "ERROR: " + . strval($errCount) + . " Test(s) cannot be referenced for group {$groupName} in TestObjectHandler::getTestsByGroup()." + . " See mftf.log for details." + ); + } + return $relevantTests; } + /** + * Sanitize test objects + * + * @param array $testsToRemove + * @return void + */ + public function sanitizeTests($testsToRemove) + { + foreach ($testsToRemove as $name) { + unset($this->tests[$name]); + LoggingUtil::getInstance()->getLogger(self::class)->error( + "Removed invalid test object {$name}" + ); + } + } + /** * This method reads all Test.xml files into objects and stores them in an array for future access. * + * @param boolean $validateAnnotations * @return void + * @throws FastFailException + * @throws TestFrameworkException + * * @SuppressWarnings(PHPMD.UnusedPrivateMethod) - * @throws XmlException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - private function initTestData() + private function initTestData($validateAnnotations = true) { - $testDataParser = ObjectManagerFactory::getObjectManager()->create(TestDataParser::class); - $parsedTestArray = $testDataParser->readTestData(); + $parserErrorMessage = null; + try { + $testDataParser = ObjectManagerFactory::getObjectManager()->create(TestDataParser::class); + $parsedTestArray = $testDataParser->readTestData(); - $testObjectExtractor = new TestObjectExtractor(); + if (!$parsedTestArray) { + $parserErrorMessage = "Could not parse any test in xml."; + } + } catch (\Exception $e) { + $parserErrorMessage = $e->getMessage(); + } - if (!$parsedTestArray) { - trigger_error("Could not parse any test in xml.", E_USER_NOTICE); - return; + if ($parserErrorMessage) { + throw new FastFailException("Test Data Parser Error: " . $parserErrorMessage); } - $exceptionCollector = new ExceptionCollector(); + $testObjectExtractor = new TestObjectExtractor(); + $testNameValidator = new NameValidationUtil(); foreach ($parsedTestArray as $testName => $testData) { - $filename = $testData[TestObjectHandler::TEST_FILENAME_ATTRIBUTE]; - $testNameValidator->validatePascalCase($testName, NameValidationUtil::TEST_NAME, $filename); - if (!is_array($testData)) { - continue; - } try { - $this->tests[$testName] = $testObjectExtractor->extractTestData($testData); - } catch (XmlException $exception) { - $exceptionCollector->addError(self::class, $exception->getMessage()); + $filename = $testData[TestObjectHandler::TEST_FILENAME_ATTRIBUTE]; + $testNameValidator->validatePascalCase($testName, NameValidationUtil::TEST_NAME, $filename); + if (!is_array($testData)) { + continue; + } + $this->tests[$testName] = $testObjectExtractor->extractTestData($testData, $validateAnnotations); + } catch (FastFailException $exception) { + throw $exception; + } catch (\Exception $exception) { + LoggingUtil::getInstance()->getLogger(self::class)->error( + "Unable to parse test " . $testName . "\n" . $exception->getMessage() + ); + if (MftfApplicationConfig::getConfig()->verboseEnabled() + && MftfApplicationConfig::getConfig()->getPhase() === MftfApplicationConfig::GENERATION_PHASE) { + print("ERROR: Unable to parse test " . $testName . "\n"); + } + GenerationErrorHandler::getInstance()->addError( + 'test', + $testName, + self::class . ': Unable to parse test ' . $exception->getMessage() + ); } } - $exceptionCollector->throwException(); $testNameValidator->summarize(NameValidationUtil::TEST_NAME); - $testObjectExtractor->getAnnotationExtractor()->validateStoryTitleUniqueness(); - $testObjectExtractor->getAnnotationExtractor()->validateTestCaseIdTitleUniqueness(); + if ($validateAnnotations) { + $testObjectExtractor->getAnnotationExtractor()->validateStoryTitleUniqueness(); + $testObjectExtractor->getAnnotationExtractor()->validateTestCaseIdTitleUniqueness(); + } } /** @@ -167,12 +271,15 @@ private function initTestData() * @param TestObject $testObject * @return TestObject * @throws TestFrameworkException + * @throws XmlException */ private function extendTest($testObject) { if ($testObject->getParentName() !== null) { - if ($testObject->getParentName() == $testObject->getName()) { - throw new TestFrameworkException("Mftf Test can not extend from itself: " . $testObject->getName()); + if ($testObject->getParentName() === $testObject->getName()) { + throw new TestFrameworkException( + "Mftf Test can not extend from itself: " . $testObject->getName() + ); } return $this->extendUtil->extendTest($testObject); } diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php index 3e1afae31..593320d44 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php +++ b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionGroupObject.php @@ -24,18 +24,20 @@ class ActionGroupObject "executeJS", "magentoCLI", "generateDate", - "formatMoney", + "formatCurrency", "deleteData", "getData", "updateData", "createData", "grabAttributeFrom", "grabCookie", + "grabCookieAttributes", "grabFromCurrentUrl", "grabMultiple", "grabPageSource", "grabTextFrom", - "grabValueFrom" + "grabValueFrom", + "getOTP" ]; /** @@ -210,6 +212,9 @@ private function getResolvedActionsWithArgs($arguments, $actionReferenceKey) foreach ($this->parsedActions as $action) { $replacementStepKeys[$action->getStepKey()] = $action->getStepKey() . ucfirst($actionReferenceKey); $varAttributes = array_intersect($this->varAttributes, array_keys($action->getCustomActionAttributes())); + if ($action->getType() === ActionObject::ACTION_TYPE_HELPER) { + $varAttributes = array_keys($action->getCustomActionAttributes()); + } // replace createDataKey attributes inside the action group $resolvedActionAttributes = $this->replaceCreateDataKeys($action, $replacementStepKeys); @@ -235,7 +240,7 @@ private function getResolvedActionsWithArgs($arguments, $actionReferenceKey) $action->getStepKey() . ucfirst($actionReferenceKey), $action->getType(), array_replace_recursive($resolvedActionAttributes, $newActionAttributes), - $action->getLinkedAction() == null ? null : $action->getLinkedAction() . ucfirst($actionReferenceKey), + $action->getLinkedAction() === null ? null : $action->getLinkedAction() . ucfirst($actionReferenceKey), $orderOffset, [self::ACTION_GROUP_ORIGIN_NAME => $this->name, self::ACTION_GROUP_ORIGIN_TEST_REF => $actionReferenceKey], @@ -527,7 +532,7 @@ private function findArgumentByName($name, $argumentList) $matchedArgument = array_filter( $argumentList, function ($e) use ($name) { - return $e->getName() == $name; + return $e->getName() === $name; } ); if (isset(array_values($matchedArgument)[0])) { diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php index 648471c7a..d8ed23098 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php +++ b/src/Magento/FunctionalTestingFramework/Test/Objects/ActionObject.php @@ -77,6 +77,7 @@ class ActionObject const ACTION_TYPE_COMMENT = 'comment'; const ACTION_TYPE_HELPER = 'helper'; const INVISIBLE_STEP_ACTIONS = ['retrieveEntityField', 'getSecret']; + const PAUSE_ACTION_INTERNAL_ATTRIBUTE = 'pauseOnFail'; /** * The unique identifier for the action @@ -183,6 +184,17 @@ public static function getDefaultWaitTimeout() return getenv('WAIT_TIMEOUT'); } + /** + * Retrieve default timeout for 'magentoCLI' or 'magentoCLISecret' in seconds + * + * @return integer + */ + public static function getDefaultMagentoCLIWaitTimeout() + { + $timeout = getenv('MAGENTO_CLI_WAIT_TIMEOUT'); + return !empty($timeout) ? $timeout : self::DEFAULT_COMMAND_WAIT_TIMEOUT; + } + /** * This function returns the string property stepKey. * @@ -288,8 +300,9 @@ public function resolveReferences() $this->resolveSelectorReferenceAndTimeout(); $this->resolveUrlReference(); $this->resolveDataInputReferences(); + $this->detectCredentials(); $this->validateTimezoneAttribute(); - if ($this->getType() == "deleteData") { + if ($this->getType() === 'deleteData') { $this->validateMutuallyExclusiveAttributes(self::DELETE_DATA_MUTUAL_EXCLUSIVE_ATTRIBUTES); } } @@ -416,6 +429,22 @@ private function resolveSelectorReferenceAndTimeout() } } } + /** + * Sets requiredCredentials property + * + * @return void + * @throws TestReferenceException + */ + public function detectCredentials() + { + $requiredCredentials = ""; + $attributes = $this->getCustomActionAttributes(); + if (isset($attributes['userInput']) && stristr($attributes['userInput'], '_CREDS') == true) { + $credentials = explode(".", trim($attributes['userInput'], '{}')); + $requiredCredentials = $credentials[1]; + } + $this->resolvedCustomAttributes['requiredCredentials'] = $requiredCredentials; + } /** * Look up the url for SomePageName and set it, with MAGENTO_BASE_URL prepended, as the url attribute in the @@ -560,24 +589,24 @@ private function findAndReplaceReferences($objectHandler, $inputString) continue; } - if ($obj == null) { + if ($obj === null) { // keep initial values for subsequent logic $replacement = null; $parameterized = false; - } elseif (get_class($obj) == PageObject::class) { + } elseif (get_class($obj) === PageObject::class) { if ($obj->getDeprecated() !== null) { $this->deprecatedUsage[] = "DEPRECATED PAGE in Test: " . $match . ' ' . $obj->getDeprecated(); } $this->validateUrlAreaAgainstActionType($obj); $replacement = $obj->getUrl(); $parameterized = $obj->isParameterized(); - } elseif (get_class($obj) == SectionObject::class) { + } elseif (get_class($obj) === SectionObject::class) { if ($obj->getDeprecated() !== null) { $this->deprecatedUsage[] = "DEPRECATED SECTION in Test: " . $match . ' ' . $obj->getDeprecated(); } list(,$objField) = $this->stripAndSplitReference($match); - if ($obj->getElement($objField) == null) { + if ($obj->getElement($objField) === null) { throw new TestReferenceException( "Could not resolve entity reference \"{$inputString}\" " . "in Action with stepKey \"{$this->getStepKey()}\"", @@ -591,7 +620,7 @@ private function findAndReplaceReferences($objectHandler, $inputString) $this->deprecatedUsage[] = "DEPRECATED ELEMENT in Test: " . $match . ' ' . $obj->getElement($objField)->getDeprecated(); } - } elseif (get_class($obj) == EntityDataObject::class) { + } elseif (get_class($obj) === EntityDataObject::class) { if ($obj->getDeprecated() !== null) { $this->deprecatedUsage[] = "DEPRECATED DATA ENTITY in Test: " . $match . ' ' . $obj->getDeprecated(); @@ -637,7 +666,7 @@ private function validateMutuallyExclusiveAttributes(array $attributes) . implode("', '", $attributes) . "'", ["type" => $this->getType(), "attributes" => $attributes] ); - } elseif (count($matches) == 0) { + } elseif (count($matches) === 0) { throw new TestReferenceException( "Actions of type '{$this->getType()}' must contain at least one attribute of types '" . implode("', '", $attributes) . "'", @@ -655,7 +684,7 @@ private function validateMutuallyExclusiveAttributes(array $attributes) */ private function validateUrlAreaAgainstActionType($obj) { - if ($obj->getArea() == 'external' && + if ($obj->getArea() === 'external' && in_array($this->getType(), self::EXTERNAL_URL_AREA_INVALID_ACTIONS)) { throw new TestReferenceException( "Page of type 'external' is not compatible with action type '{$this->getType()}'", @@ -696,7 +725,7 @@ private function resolveEntityDataObjectReference($obj, $match) { list(,$objField) = $this->stripAndSplitReference($match); - if (strpos($objField, '[') == true) { + if (strpos($objField, '[') !== false) { // Access <array>...</array> $parts = explode('[', $objField); $name = $parts[0]; @@ -725,8 +754,13 @@ private function resolveParameterization($isParameterized, $replacement, $match, } else { $resolvedReplacement = $replacement; } - if (get_class($object) == PageObject::class && $object->getArea() == PageObject::ADMIN_AREA) { - $resolvedReplacement = "/{{_ENV.MAGENTO_BACKEND_NAME}}/" . $resolvedReplacement; + if (get_class($object) === PageObject::class && $object->getArea() === PageObject::ADMIN_AREA) { + $urlSegments = [ + '{{_ENV.MAGENTO_BACKEND_BASE_URL}}', + '{{_ENV.MAGENTO_BACKEND_NAME}}', + $resolvedReplacement + ]; + $resolvedReplacement = implode('/', $urlSegments); } return $resolvedReplacement; } @@ -788,7 +822,7 @@ private function checkParameterCount($matches, $parameters, $reference) if (count($matches) > count($parameters)) { if (is_array($parameters)) { $parametersGiven = implode(",", $parameters); - } elseif ($parameters == null) { + } elseif ($parameters === null) { $parametersGiven = "NONE"; } else { $parametersGiven = $parameters; @@ -804,7 +838,7 @@ private function checkParameterCount($matches, $parameters, $reference) $reference . ". Parameters Given: " . implode(", ", $parameters), ["reference" => $reference, "parametersGiven" => $parameters] ); - } elseif (count($matches) == 0) { + } elseif (count($matches) === 0) { throw new TestReferenceException( "Parameter Resolution Failed: No parameter matches found in parameterized element with selector " . $reference, diff --git a/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php b/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php index 504d207b4..c92bff588 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php +++ b/src/Magento/FunctionalTestingFramework/Test/Objects/TestObject.php @@ -21,14 +21,36 @@ class TestObject const TEST_ACTION_WEIGHT = [ 'waitForPageLoad' => 1500, - 'amOnPage' => 1000, + 'amOnPage' => 1500, 'waitForLoadingMaskToDisappear' => 500, 'wait' => self::WAIT_TIME_ATTRIBUTE, + 'waitForAjaxLoad' => 500, + 'waitForElementNotVisible' => 500, + 'waitForElementVisible' => 500, + 'waitForText' => 500, + 'waitForElement' => 500, + 'waitForJS' => 500, 'comment' => 5, 'assertCount' => 5, - 'closeAdminNotification' => 10 + 'closeAdminNotification' => 10, + 'magentoCLI' => 1000, + 'magentoCron' => 3000, + 'createData' => 500, + 'deleteData' => 200, + 'updateData' => 200, + 'getOTP' => 1000, + 'startMessageQueue' => 700, ]; + const WEBAPI_AUTH_TEST_ACTIONS = [ + 'createData', + 'deleteData', + 'updateData', + 'getData', + ]; + + const WEBAPI_AUTH_TEST_ACTION_WEIGHT = 6000; + /** * Name of the test * @@ -78,6 +100,20 @@ class TestObject */ private $cachedOrderedActions = null; + /** + * Deprecation message. + * + * @var string|null + */ + private $deprecated; + + /** + * Indicates if a test contains an action that requires Web API authentication. + * + * @var boolean + */ + private $hasWebApiAuthAction; + /** * TestObject constructor. * @@ -87,15 +123,25 @@ class TestObject * @param TestHookObject[] $hooks * @param string $filename * @param string $parentTest + * @param string|null $deprecated */ - public function __construct($name, $parsedSteps, $annotations, $hooks, $filename = null, $parentTest = null) - { + public function __construct( + $name, + $parsedSteps, + $annotations, + $hooks, + $filename = null, + $parentTest = null, + $deprecated = null + ) { $this->name = $name; $this->parsedSteps = $parsedSteps; $this->annotations = $annotations; $this->hooks = $hooks; $this->filename = $filename; $this->parentTest = $parentTest; + $this->deprecated = $deprecated; + $this->hasWebApiAuthAction = false; } /** @@ -128,6 +174,16 @@ public function getParentName() return $this->parentTest; } + /** + * Returns deprecated messages. + * + * @return string|null + */ + public function getDeprecated() + { + return $this->deprecated; + } + /** * Getter for the skip_test boolean * @@ -135,11 +191,8 @@ public function getParentName() */ public function isSkipped() { - // TODO deprecation|deprecate MFTF 3.0.0 remove elseif when group skip is no longer allowed if (array_key_exists('skip', $this->annotations)) { return true; - } elseif (array_key_exists('group', $this->annotations) && (in_array("skip", $this->annotations['group']))) { - return true; } return false; } @@ -151,7 +204,7 @@ public function isSkipped() */ public function getCodeceptionName() { - if (strpos($this->name, 'Cest') && substr($this->name, -4) == 'Cest') { + if (strpos($this->name, 'Cest') && substr($this->name, -4) === 'Cest') { return $this->name; } @@ -199,9 +252,36 @@ public function getEstimatedDuration() $testTime = $this->calculateWeightedActionTimes($this->getOrderedActions()); - return $hookTime + $testTime; + if ($this->hasWebApiAuthAction) { + return $hookTime + $testTime + self::WEBAPI_AUTH_TEST_ACTION_WEIGHT; + } else { + return $hookTime + $testTime; + } } + /** + * Function to return credentials + * @return array + */ + public function getCredentials() + { + $requiredCredentials = []; + foreach ($this->hooks as $hookObject) { + foreach ($hookObject->getActions() as $action) { + if (isset($action->getCustomActionAttributes()['requiredCredentials']) + && !empty($action->getCustomActionAttributes()['requiredCredentials'])) { + $requiredCredentials[] = $action->getCustomActionAttributes()['requiredCredentials']; + } + } + } + foreach ($this->getOrderedActions() as $action) { + if (isset($action->getCustomActionAttributes()['requiredCredentials']) + && !empty($action->getCustomActionAttributes()['requiredCredentials'])) { + $requiredCredentials[] = $action->getCustomActionAttributes()['requiredCredentials']; + } + } + return array_unique($requiredCredentials); + } /** * Function which takes a set of actions and estimates time for completion based on action type. * @@ -214,6 +294,11 @@ private function calculateWeightedActionTimes($actions) // search for any actions of special type foreach ($actions as $action) { /** @var ActionObject $action */ + + if (!$this->hasWebApiAuthAction && in_array($action->getType(), self::WEBAPI_AUTH_TEST_ACTIONS)) { + $this->hasWebApiAuthAction = true; + } + if (array_key_exists($action->getType(), self::TEST_ACTION_WEIGHT)) { $weight = self::TEST_ACTION_WEIGHT[$action->getType()]; if ($weight === self::WAIT_TIME_ATTRIBUTE) { @@ -252,7 +337,7 @@ public function getAnnotationByName($name) */ public function getCustomData() { - return $this->customData; + return null; } /** diff --git a/src/Magento/FunctionalTestingFramework/Test/Parsers/ActionGroupDataParser.php b/src/Magento/FunctionalTestingFramework/Test/Parsers/ActionGroupDataParser.php index 4709d3aa8..57e90dd0e 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Parsers/ActionGroupDataParser.php +++ b/src/Magento/FunctionalTestingFramework/Test/Parsers/ActionGroupDataParser.php @@ -11,8 +11,14 @@ /** * Class ActionGroupDataParser */ + class ActionGroupDataParser { + /** + * @var DataInterface + */ + private $actionGroupData; + /** * ActionGroupDataParser constructor. * diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupAnnotationExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupAnnotationExtractor.php index 65f0f41b0..7f35ee302 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupAnnotationExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupAnnotationExtractor.php @@ -17,12 +17,13 @@ class ActionGroupAnnotationExtractor extends AnnotationExtractor * This method trims away irrelevant tags and returns annotations used in the array passed. The annotations * can be found in both Tests and their child element tests. * - * @param array $testAnnotations - * @param string $filename + * @param array $testAnnotations + * @param string $filename + * @param boolean $validateAnnotations * @return array * @throws \Exception */ - public function extractAnnotations($testAnnotations, $filename) + public function extractAnnotations($testAnnotations, $filename, $validateAnnotations = true) { $annotationObjects = []; $annotations = $this->stripDescriptorTags($testAnnotations, parent::NODE_NAME); diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupObjectExtractor.php index 618cddc09..84b960f1b 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupObjectExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/ActionGroupObjectExtractor.php @@ -65,8 +65,8 @@ public function extractActionGroup($actionGroupData) if (array_key_exists(self::OBJ_DEPRECATED, $actionGroupData)) { $deprecated = $actionGroupData[self::OBJ_DEPRECATED]; LoggingUtil::getInstance()->getLogger(ActionGroupObject::class)->deprecation( - $deprecated, - ["actionGroupName" => $actionGroupData[self::FILENAME], "deprecatedActionGroup" => $deprecated] + "The action group '{$actionGroupData[self::NAME]}' is deprecated.", + ["fileName" => $actionGroupData[self::FILENAME], "deprecatedMessage" => $deprecated] ); } $actionGroupReference = $actionGroupData[self::EXTENDS_ACTION_GROUP] ?? null; diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php b/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php index 28a7d405d..a601deb24 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/ActionMergeUtil.php @@ -170,10 +170,10 @@ private function resolveActionGroups($mergedSteps) foreach ($mergedSteps as $key => $mergedStep) { /**@var ActionObject $mergedStep**/ - if ($mergedStep->getType() == ActionObjectExtractor::ACTION_GROUP_TAG) { + if ($mergedStep->getType() === ActionObjectExtractor::ACTION_GROUP_TAG) { $actionGroupRef = $mergedStep->getCustomActionAttributes()[ActionObjectExtractor::ACTION_GROUP_REF]; $actionGroup = ActionGroupObjectHandler::getInstance()->getObject($actionGroupRef); - if ($actionGroup == null) { + if ($actionGroup === null) { throw new TestReferenceException("Could not find ActionGroup by ref \"{$actionGroupRef}\""); } $args = $mergedStep->getCustomActionAttributes()[ActionObjectExtractor::ACTION_GROUP_ARGUMENTS] ?? null; diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/ActionObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/ActionObjectExtractor.php index d9b8d32fb..a9fb0817e 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/ActionObjectExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/ActionObjectExtractor.php @@ -27,7 +27,7 @@ class ActionObjectExtractor extends BaseObjectExtractor const ACTION_GROUP_ARGUMENTS = 'arguments'; const ACTION_GROUP_ARG_VALUE = 'value'; const BEFORE_AFTER_ERROR_MSG = "Merge Error - Steps cannot have both before and after attributes.\tStepKey='%s'"; - const STEP_KEY_BLACKLIST_ERROR_MSG = "StepKeys cannot contain non alphanumeric characters.\tStepKey='%s'"; + const STEP_KEY_BLOCKLIST_ERROR_MSG = "StepKeys cannot contain non alphanumeric characters.\tStepKey='%s'"; const STEP_KEY_EMPTY_ERROR_MSG = "StepKeys cannot be empty.\tAction='%s'"; const DATA_PERSISTENCE_CUSTOM_FIELD = 'field'; const DATA_PERSISTENCE_CUSTOM_FIELD_KEY = 'key'; @@ -70,7 +70,7 @@ public function extractActions($testActions, $testName = null) } if (preg_match('/[^a-zA-Z0-9_]/', $stepKey)) { - throw new XmlException(sprintf(self::STEP_KEY_BLACKLIST_ERROR_MSG, $actionName)); + throw new XmlException(sprintf(self::STEP_KEY_BLOCKLIST_ERROR_MSG, $actionName)); } $actionAttributes = $this->stripDescriptorTags( @@ -90,7 +90,7 @@ public function extractActions($testActions, $testName = null) $actions = $this->extractFieldActions($actionData, $actions); $actionAttributes = $this->extractFieldReferences($actionData, $actionAttributes); - if ($linkedAction['stepKey'] != null) { + if ($linkedAction['stepKey'] !== null) { $stepKeyRefs[$linkedAction['stepKey']][] = $stepKey; } @@ -157,7 +157,7 @@ private function processActionGroupArgs($actionType, $actionAttributeData) $actionAttributeArgData = []; foreach ($actionAttributeData as $attributeDataKey => $attributeDataValues) { - if ($attributeDataKey == self::ACTION_GROUP_REF) { + if ($attributeDataKey === self::ACTION_GROUP_REF) { $actionAttributeArgData[self::ACTION_GROUP_REF] = $attributeDataValues; continue; } @@ -187,7 +187,7 @@ private function processHelperArgs($actionType, $actionAttributeData) $actionAttributeArgData = []; foreach ($actionAttributeData as $attributeDataKey => $attributeDataValues) { - if (isset($attributeDataValues['nodeName']) && $attributeDataValues['nodeName'] == 'argument') { + if (isset($attributeDataValues['nodeName']) && $attributeDataValues['nodeName'] === 'argument') { if (isset($attributeDataValues['name']) && in_array($attributeDataValues['name'], $reservedHelperVariableNames)) { $message = 'Helper argument names ' . implode(',', $reservedHelperVariableNames); @@ -225,7 +225,7 @@ private function extractFieldActions($actionData, $actions) $fieldActions = []; foreach ($actionData as $type => $data) { // determine if field type is entity passed in - if (!is_array($data) || $data[self::NODE_NAME] != self::DATA_PERSISTENCE_CUSTOM_FIELD) { + if (!is_array($data) || $data[self::NODE_NAME] !== self::DATA_PERSISTENCE_CUSTOM_FIELD) { continue; } @@ -258,7 +258,8 @@ private function extractFieldReferences($actionData, $actionAttributes) $attributes = []; foreach ($actionAttributes as $attributeName => $attributeValue) { - if (!is_array($attributeValue) || $attributeValue[self::NODE_NAME] != self::DATA_PERSISTENCE_CUSTOM_FIELD) { + if (!is_array($attributeValue) || + $attributeValue[self::NODE_NAME] !== self::DATA_PERSISTENCE_CUSTOM_FIELD) { $attributes[$attributeName] = $attributeValue; continue; } @@ -305,7 +306,7 @@ private function auditMergeSteps($stepKeyRefs, $testName) }); foreach ($atRiskStepRef as $stepKey => $stepRefs) { - LoggingUtil::getInstance()->getLogger(ActionObjectExtractor::class)->warn( + LoggingUtil::getInstance()->getLogger(ActionObjectExtractor::class)->warning( 'multiple actions referencing step key', ['test' => $testName, 'stepKey' => $stepKey, 'ref' => $stepRefs] ); diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/AnnotationExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/AnnotationExtractor.php index 1859f3195..8710fbb22 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/AnnotationExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/AnnotationExtractor.php @@ -7,8 +7,10 @@ namespace Magento\FunctionalTestingFramework\Test\Util; use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; +use Magento\FunctionalTestingFramework\Util\GenerationErrorHandler; use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; /** @@ -55,12 +57,14 @@ public function __construct() * This method trims away irrelevant tags and returns annotations used in the array passed. The annotations * can be found in both Tests and their child element tests. * - * @param array $testAnnotations - * @param string $filename + * @param array $testAnnotations + * @param string $filename + * @param boolean $validateAnnotations * @return array * @throws XmlException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - public function extractAnnotations($testAnnotations, $filename) + public function extractAnnotations($testAnnotations, $filename, $validateAnnotations = true) { $annotationObjects = []; $annotations = $this->stripDescriptorTags($testAnnotations, self::NODE_NAME); @@ -73,34 +77,32 @@ public function extractAnnotations($testAnnotations, $filename) } $annotationValues = []; // Only transform severity annotation - if ($annotationKey == "severity") { + if ($annotationKey === "severity") { $annotationObjects[$annotationKey] = $this->transformAllureSeverityToMagento( - $annotationData[0]['value'] + trim($annotationData[0][self::ANNOTATION_VALUE]) ); continue; } - if ($annotationKey == "skip") { + if ($annotationKey === "skip") { $annotationData = $annotationData['issueId']; - $this->validateSkippedIssues($annotationData, $filename); + if ($validateAnnotations) { + $this->validateSkippedIssues($annotationData, $filename); + } } foreach ($annotationData as $annotationValue) { - $annotationValues[] = $annotationValue[self::ANNOTATION_VALUE]; - } - // TODO deprecation|deprecate MFTF 3.0.0 - if ($annotationKey == "group" && in_array("skip", $annotationValues)) { - LoggingUtil::getInstance()->getLogger(AnnotationExtractor::class)->warning( - "Use of group skip will be deprecated in MFTF 3.0.0. Please update tests to use skip tags.", - ["test" => $filename] - ); + $annotationValues[] = trim($annotationValue[self::ANNOTATION_VALUE]); } $annotationObjects[$annotationKey] = $annotationValues; } + if ($validateAnnotations) { + $this->validateMissingAnnotations($annotationObjects, $filename); + } + $this->addTestCaseIdToTitle($annotationObjects, $filename); - $this->validateMissingAnnotations($annotationObjects, $filename); $this->addStoryTitleToMap($annotationObjects, $filename); return $annotationObjects; @@ -155,7 +157,9 @@ private function validateMissingAnnotations($annotationObjects, $filename) $missingAnnotations = []; foreach (self::REQUIRED_ANNOTATIONS as $REQUIRED_ANNOTATION) { - if (!array_key_exists($REQUIRED_ANNOTATION, $annotationObjects)) { + if (!array_key_exists($REQUIRED_ANNOTATION, $annotationObjects) + || !isset($annotationObjects[$REQUIRED_ANNOTATION][0]) + || empty($annotationObjects[$REQUIRED_ANNOTATION][0])) { $missingAnnotations[] = $REQUIRED_ANNOTATION; } } @@ -172,38 +176,54 @@ private function validateMissingAnnotations($annotationObjects, $filename) /** * Validates that all Story/Title combinations are unique, builds list of violators if found. - * @throws XmlException + * * @return void + * @throws TestFrameworkException */ public function validateStoryTitleUniqueness() { - $dupes = []; + if (MftfApplicationConfig::getConfig()->getPhase() === MftfApplicationConfig::EXECUTION_PHASE) { + return; + } + $dupes = []; foreach ($this->storyToTitleMappings as $storyTitle => $files) { if (count($files) > 1) { $dupes[$storyTitle] = "'" . implode("', '", $files) . "'"; } } if (!empty($dupes)) { - $message = "Story and Title annotation pairs must be unique:\n\n"; foreach ($dupes as $storyTitle => $tests) { - $storyTitleArray = explode("/", $storyTitle); - $story = $storyTitleArray[0]; - $title = $storyTitleArray[1]; - $message .= "Story: '{$story}' Title: '{$title}' in Tests {$tests}\n\n"; + $message = "Story and Title annotation pairs is not unique in Tests {$tests}\n"; + LoggingUtil::getInstance()->getLogger(self::class)->error($message); + if (MftfApplicationConfig::getConfig()->verboseEnabled() + && MftfApplicationConfig::getConfig()->getPhase() === MftfApplicationConfig::GENERATION_PHASE) { + print('ERROR: ' . $message); + } + $testArray = explode(',', $tests); + foreach ($testArray as $test) { + GenerationErrorHandler::getInstance()->addError( + 'test', + trim($test, "' \t"), + $message, + true + ); + } } - throw new XmlException($message); } } /** * Validates uniqueness between Test Case ID and Titles globally - * @returns void - * @throws XmlException * @return void + * @throws TestFrameworkException */ public function validateTestCaseIdTitleUniqueness() { + if (MftfApplicationConfig::getConfig()->getPhase() === MftfApplicationConfig::EXECUTION_PHASE) { + return; + } + $dupes = []; foreach ($this->testCaseToTitleMappings as $newTitle => $files) { if (count($files) > 1) { @@ -211,14 +231,23 @@ public function validateTestCaseIdTitleUniqueness() } } if (!empty($dupes)) { - $message = "TestCaseId and Title pairs must be unique:\n\n"; foreach ($dupes as $newTitle => $tests) { - $testCaseTitleArray = explode(": ", $newTitle); - $testCaseId = $testCaseTitleArray[0]; - $title = $testCaseTitleArray[1]; - $message .= "TestCaseId: '{$testCaseId}' Title: '{$title}' in Tests {$tests}\n\n"; + $message = "TestCaseId and Title pairs is not unique in Tests {$tests}\n"; + LoggingUtil::getInstance()->getLogger(self::class)->error($message); + if (MftfApplicationConfig::getConfig()->verboseEnabled() + && MftfApplicationConfig::getConfig()->getPhase() === MftfApplicationConfig::GENERATION_PHASE) { + print('ERROR: ' . $message); + } + $testArray = explode(',', $tests); + foreach ($testArray as $test) { + GenerationErrorHandler::getInstance()->addError( + 'test', + trim($test, "' \t"), + $message, + true + ); + } } - throw new XmlException($message); } } @@ -232,7 +261,7 @@ public function validateTestCaseIdTitleUniqueness() public function validateSkippedIssues($issues, $filename) { foreach ($issues as $issueId) { - if (empty($issueId['value'])) { + if (empty(trim($issueId['value']))) { $message = "issueId for skipped tests cannot be empty. Test: $filename"; throw new XmlException($message); } diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/ObjectExtensionUtil.php b/src/Magento/FunctionalTestingFramework/Test/Util/ObjectExtensionUtil.php index 91617b1e2..7f6d050fb 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/ObjectExtensionUtil.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/ObjectExtensionUtil.php @@ -7,6 +7,7 @@ namespace Magento\FunctionalTestingFramework\Test\Util; use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Test\Handlers\ActionGroupObjectHandler; @@ -32,7 +33,8 @@ public function __construct() * * @param TestObject $testObject * @return TestObject - * @throws TestReferenceException|XmlException + * @throws TestFrameworkException + * @throws XmlException */ public function extendTest($testObject) { @@ -40,13 +42,13 @@ public function extendTest($testObject) try { $parentTest = TestObjectHandler::getInstance()->getObject($testObject->getParentName()); } catch (TestReferenceException $error) { + $skippedTest = $this->skipTest($testObject, 'ParentTestDoesNotExist'); if (MftfApplicationConfig::getConfig()->verboseEnabled()) { LoggingUtil::getInstance()->getLogger(ObjectExtensionUtil::class)->debug( "parent test not defined. test will be skipped", ["parent" => $testObject->getParentName(), "test" => $testObject->getName()] ); } - $skippedTest = $this->skipTest($testObject); return $skippedTest; } @@ -62,6 +64,11 @@ public function extendTest($testObject) ->debug("extending test", ["parent" => $parentTest->getName(), "test" => $testObject->getName()]); } + // Skip test if parent is skipped + if ($parentTest->isSkipped()) { + return $this->skipTest($testObject); + } + // Get steps for both the parent and the child tests $referencedTestSteps = $parentTest->getUnresolvedSteps(); $newSteps = $this->processRemoveActions(array_merge($referencedTestSteps, $testObject->getUnresolvedSteps())); @@ -91,7 +98,7 @@ public function extendActionGroup($actionGroupObject) { // Check to see if the parent action group exists $parentActionGroup = ActionGroupObjectHandler::getInstance()->getObject($actionGroupObject->getParentName()); - if ($parentActionGroup == null) { + if ($parentActionGroup === null) { throw new XmlException( "Parent Action Group " . $actionGroupObject->getParentName() . @@ -193,7 +200,7 @@ private function processRemoveActions($actions) // remove actions merged that are of type 'remove' foreach ($actions as $actionName => $actionData) { - if ($actionData->getType() != "remove") { + if ($actionData->getType() !== 'remove') { $cleanedActions[$actionName] = $actionData; } } @@ -205,17 +212,19 @@ private function processRemoveActions($actions) * This method returns a skipped form of the Test Object * * @param TestObject $testObject + * @param string $skipReason * @return TestObject */ - public function skipTest($testObject) + public function skipTest($testObject, $skipReason = null) { $annotations = $testObject->getAnnotations(); + $skipReason = $skipReason ?? 'ParentTestIsSkipped'; // Add skip to the group array if it doesn't already exist if (array_key_exists('skip', $annotations)) { return $testObject; } elseif (!array_key_exists('skip', $annotations)) { - $annotations['skip'] = ['issueId' => "ParentTestDoesNotExist"]; + $annotations['skip'] = ['issueId' => $skipReason]; } $skippedTest = new TestObject( @@ -227,6 +236,12 @@ public function skipTest($testObject) $testObject->getParentName() ); + if (MftfApplicationConfig::getConfig()->verboseEnabled()) { + LoggingUtil::getInstance()->getLogger(ObjectExtensionUtil::class)->debug( + "{$testObject->getName()} is skipped due to {$skipReason}" + ); + } + return $skippedTest; } } diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/TestHookObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/TestHookObjectExtractor.php index c11a6e512..523f1278b 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/TestHookObjectExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/TestHookObjectExtractor.php @@ -6,6 +6,8 @@ namespace Magento\FunctionalTestingFramework\Test\Util; +use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; +use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; use Magento\FunctionalTestingFramework\Test\Objects\TestHookObject; @@ -37,7 +39,8 @@ public function __construct() * @param string $hookType * @param array $testHook * @return TestHookObject - * @throws \Exception + * @throws XmlException + * @throws TestReferenceException */ public function extractHook($parentName, $hookType, $testHook) { @@ -57,19 +60,26 @@ public function extractHook($parentName, $hookType, $testHook) /** * Creates the default failed hook object with a single saveScreenshot action. + * And a pause action when ENABLE_PAUSE is set to true. * * @param string $parentName * @return TestHookObject */ public function createDefaultFailedHook($parentName) { - - $saveScreenshotStep = [new ActionObject("saveScreenshot", "saveScreenshot", [])]; + $defaultSteps['saveScreenshot'] = new ActionObject("saveScreenshot", "saveScreenshot", []); + if (getenv('ENABLE_PAUSE') === 'true') { + $defaultSteps['pauseWhenFailed'] = new ActionObject( + 'pauseWhenFailed', + 'pause', + [ActionObject::PAUSE_ACTION_INTERNAL_ATTRIBUTE => true] + ); + } $hook = new TestHookObject( TestObjectExtractor::TEST_FAILED_HOOK, $parentName, - $saveScreenshotStep + $defaultSteps ); return $hook; diff --git a/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php b/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php index a25db0935..510898c6c 100644 --- a/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Test/Util/TestObjectExtractor.php @@ -7,6 +7,7 @@ namespace Magento\FunctionalTestingFramework\Test\Util; use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Page\Objects\ElementObject; use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; @@ -83,19 +84,22 @@ public function getAnnotationExtractor() * This method takes and array of test data and strips away irrelevant tags. The data is converted into an array of * TestObjects. * - * @param array $testData + * @param array $testData + * @param boolean $validateAnnotations * @return TestObject - * @throws \Exception + * @throws TestReferenceException + * @throws XmlException + * @throws TestFrameworkException */ - public function extractTestData($testData) + public function extractTestData($testData, $validateAnnotations = true) { - // validate the test name for blacklisted char (will cause allure report issues) MQE-483 + // validate the test name for blocklisted char (will cause allure report issues) MQE-483 NameValidationUtil::validateName($testData[self::NAME], "Test"); $testAnnotations = []; $testHooks = []; $filename = $testData['filename'] ?? null; - $fileNames = explode(",", $filename); + $fileNames = explode(",", $filename ?? ''); $baseFileName = $fileNames[0]; $module = $this->modulePathExtractor->extractModuleName($baseFileName); $testReference = $testData['extends'] ?? null; @@ -117,7 +121,8 @@ public function extractTestData($testData) $testAnnotations = $this->annotationExtractor->extractAnnotations( $testData[self::TEST_ANNOTATIONS] ?? [], - $testData[self::NAME] + $testData[self::NAME], + $validateAnnotations ); //Override features with module name if present, populates it otherwise @@ -177,7 +182,8 @@ public function extractTestData($testData) $testAnnotations, $testHooks, $filename, - $testReference + $testReference, + $deprecated ); } catch (XmlException $exception) { throw new XmlException($exception->getMessage() . ' in Test ' . $filename); diff --git a/src/Magento/FunctionalTestingFramework/Test/etc/Actions/assertActions.xsd b/src/Magento/FunctionalTestingFramework/Test/etc/Actions/assertActions.xsd index 22f306382..52794f578 100644 --- a/src/Magento/FunctionalTestingFramework/Test/etc/Actions/assertActions.xsd +++ b/src/Magento/FunctionalTestingFramework/Test/etc/Actions/assertActions.xsd @@ -14,11 +14,15 @@ <xs:element type="assertElementContainsAttributeType" name="assertElementContainsAttribute" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="assertArrayHasKeyType" name="assertArrayHasKey" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="assertArrayNotHasKeyType" name="assertArrayNotHasKey" minOccurs="0" maxOccurs="unbounded"/> - <xs:element type="assertArraySubsetType" name="assertArraySubset" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="assertContainsType" name="assertContains" minOccurs="0" maxOccurs="unbounded"/> + <xs:element type="assertStringContainsStringType" name="assertStringContainsString" minOccurs="0" maxOccurs="unbounded"/> + <xs:element type="assertStringContainsStringIgnoringCaseType" name="assertStringContainsStringIgnoringCase" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="assertCountType" name="assertCount" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="assertEmptyType" name="assertEmpty" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="assertEqualsType" name="assertEquals" minOccurs="0" maxOccurs="unbounded"/> + <xs:element type="assertEqualsWithDeltaType" name="assertEqualsWithDelta" minOccurs="0" maxOccurs="unbounded"/> + <xs:element type="assertEqualsCanonicalizingType" name="assertEqualsCanonicalizing" minOccurs="0" maxOccurs="unbounded"/> + <xs:element type="assertEqualsIgnoringCaseType" name="assertEqualsIgnoringCase" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="assertFalseType" name="assertFalse" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="assertFileExistsType" name="assertFileExists" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="assertFileNotExistsType" name="assertFileNotExists" minOccurs="0" maxOccurs="unbounded"/> @@ -26,14 +30,18 @@ <xs:element type="assertGreaterThanType" name="assertGreaterThan" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="assertGreaterThanOrEqualType" name="assertGreaterThanOrEqual" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="assertInstanceOfType" name="assertInstanceOf" minOccurs="0" maxOccurs="unbounded"/> - <xs:element type="assertInternalTypeType" name="assertInternalType" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="assertIsEmptyType" name="assertIsEmpty" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="assertLessOrEqualsType" name="assertLessOrEquals" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="assertLessThanType" name="assertLessThan" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="assertLessThanOrEqualType" name="assertLessThanOrEqual" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="assertNotContainsType" name="assertNotContains" minOccurs="0" maxOccurs="unbounded"/> + <xs:element type="assertStringNotContainsStringType" name="assertStringNotContainsString" minOccurs="0" maxOccurs="unbounded"/> + <xs:element type="assertStringNotContainsStringIgnoringCaseType" name="assertStringNotContainsStringIgnoringCase" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="assertNotEmptyType" name="assertNotEmpty" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="assertNotEqualsType" name="assertNotEquals" minOccurs="0" maxOccurs="unbounded"/> + <xs:element type="assertNotEqualsWithDeltaType" name="assertNotEqualsWithDelta" minOccurs="0" maxOccurs="unbounded"/> + <xs:element type="assertNotEqualsCanonicalizingType" name="assertNotEqualsCanonicalizing" minOccurs="0" maxOccurs="unbounded"/> + <xs:element type="assertNotEqualsIgnoringCaseType" name="assertNotEqualsIgnoringCase" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="assertNotInstanceOfType" name="assertNotInstanceOf" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="assertNotNullType" name="assertNotNull" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="assertNotRegExpType" name="assertNotRegExp" minOccurs="0" maxOccurs="unbounded"/> @@ -156,6 +164,34 @@ <xs:attributeGroup ref="commonActionAttributes"/> </xs:complexType> + <xs:complexType name="assertStringContainsStringType"> + <xs:annotation> + <xs:documentation> + Asserts that given string contains a value. + </xs:documentation> + </xs:annotation> + <xs:choice maxOccurs="unbounded"> + <xs:element name="expectedResult" type="expectedResultType" minOccurs="0"/> + <xs:element name="actualResult" type="actualResultType" minOccurs="0"/> + </xs:choice> + <xs:attribute ref="message"/> + <xs:attributeGroup ref="commonActionAttributes"/> + </xs:complexType> + + <xs:complexType name="assertStringContainsStringIgnoringCaseType"> + <xs:annotation> + <xs:documentation> + Asserts that given string contains a value ignoring case. + </xs:documentation> + </xs:annotation> + <xs:choice maxOccurs="unbounded"> + <xs:element name="expectedResult" type="expectedResultType" minOccurs="0"/> + <xs:element name="actualResult" type="actualResultType" minOccurs="0"/> + </xs:choice> + <xs:attribute ref="message"/> + <xs:attributeGroup ref="commonActionAttributes"/> + </xs:complexType> + <xs:complexType name="assertCountType"> <xs:annotation> <xs:documentation> @@ -186,7 +222,21 @@ <xs:complexType name="assertEqualsType"> <xs:annotation> <xs:documentation> - Asserts that two given variables are equal. Can be given a "delta" to allow precision tolerance in floating point comparison. + Asserts that two given variables are equal. + </xs:documentation> + </xs:annotation> + <xs:choice maxOccurs="unbounded"> + <xs:element name="expectedResult" type="expectedResultType" minOccurs="0"/> + <xs:element name="actualResult" type="actualResultType" minOccurs="0"/> + </xs:choice> + <xs:attribute ref="message"/> + <xs:attributeGroup ref="commonActionAttributes"/> + </xs:complexType> + + <xs:complexType name="assertEqualsWithDeltaType"> + <xs:annotation> + <xs:documentation> + Asserts that two given variables are equal. Accepts a delta. </xs:documentation> </xs:annotation> <xs:choice maxOccurs="unbounded"> @@ -198,6 +248,36 @@ <xs:attributeGroup ref="commonActionAttributes"/> </xs:complexType> + <xs:complexType name="assertEqualsIgnoringCaseType"> + <xs:annotation> + <xs:documentation> + Asserts that two given variables are equal. + </xs:documentation> + </xs:annotation> + <xs:choice maxOccurs="unbounded"> + <xs:element name="expectedResult" type="expectedResultType" minOccurs="0"/> + <xs:element name="actualResult" type="actualResultType" minOccurs="0"/> + </xs:choice> + <xs:attribute ref="message"/> + <xs:attributeGroup ref="commonActionAttributes"/> + </xs:complexType> + + <xs:complexType name="assertEqualsCanonicalizingType"> + <xs:annotation> + <xs:documentation> + Asserts that two given variables are equal. The contents are canonicalized before they are compared. + For instance, when the two variables $expected and $actual are arrays, then these arrays are + sorted before they are compared. + </xs:documentation> + </xs:annotation> + <xs:choice maxOccurs="unbounded"> + <xs:element name="expectedResult" type="expectedResultType" minOccurs="0"/> + <xs:element name="actualResult" type="actualResultType" minOccurs="0"/> + </xs:choice> + <xs:attribute ref="message"/> + <xs:attributeGroup ref="commonActionAttributes"/> + </xs:complexType> + <xs:complexType name="assertFalseType"> <xs:annotation> <xs:documentation> @@ -376,6 +456,34 @@ <xs:attributeGroup ref="commonActionAttributes"/> </xs:complexType> + <xs:complexType name="assertStringNotContainsStringType"> + <xs:annotation> + <xs:documentation> + Asserts that given string does not contain a value. + </xs:documentation> + </xs:annotation> + <xs:choice maxOccurs="unbounded"> + <xs:element name="expectedResult" type="expectedResultType" minOccurs="0"/> + <xs:element name="actualResult" type="actualResultType" minOccurs="0"/> + </xs:choice> + <xs:attribute ref="message"/> + <xs:attributeGroup ref="commonActionAttributes"/> + </xs:complexType> + + <xs:complexType name="assertStringNotContainsStringIgnoringCaseType"> + <xs:annotation> + <xs:documentation> + Asserts that given string does not contain a value ignoring case. + </xs:documentation> + </xs:annotation> + <xs:choice maxOccurs="unbounded"> + <xs:element name="expectedResult" type="expectedResultType" minOccurs="0"/> + <xs:element name="actualResult" type="actualResultType" minOccurs="0"/> + </xs:choice> + <xs:attribute ref="message"/> + <xs:attributeGroup ref="commonActionAttributes"/> + </xs:complexType> + <xs:complexType name="assertNotEmptyType"> <xs:annotation> <xs:documentation> @@ -392,7 +500,7 @@ <xs:complexType name="assertNotEqualsType"> <xs:annotation> <xs:documentation> - Asserts that actual and expected are not equal. Can be given a "delta" to allow precision tolerance in floating point comparison. + Asserts that actual and expected are not equal. </xs:documentation> </xs:annotation> <xs:choice maxOccurs="unbounded"> @@ -400,7 +508,51 @@ <xs:element name="actualResult" type="actualResultType" minOccurs="0"/> </xs:choice> <xs:attribute ref="message"/> + <xs:attributeGroup ref="commonActionAttributes"/> + </xs:complexType> + + <xs:complexType name="assertNotEqualsWithDeltaType"> + <xs:annotation> + <xs:documentation> + Asserts that two given variables are not equal. Accepts a delta. + </xs:documentation> + </xs:annotation> + <xs:choice maxOccurs="unbounded"> + <xs:element name="expectedResult" type="expectedResultType" minOccurs="0"/> + <xs:element name="actualResult" type="actualResultType" minOccurs="0"/> + </xs:choice> <xs:attribute ref="delta"/> + <xs:attribute ref="message"/> + <xs:attributeGroup ref="commonActionAttributes"/> + </xs:complexType> + + <xs:complexType name="assertNotEqualsIgnoringCaseType"> + <xs:annotation> + <xs:documentation> + Asserts that actual and expected are not equal. + </xs:documentation> + </xs:annotation> + <xs:choice maxOccurs="unbounded"> + <xs:element name="expectedResult" type="expectedResultType" minOccurs="0"/> + <xs:element name="actualResult" type="actualResultType" minOccurs="0"/> + </xs:choice> + <xs:attribute ref="message"/> + <xs:attributeGroup ref="commonActionAttributes"/> + </xs:complexType> + + <xs:complexType name="assertNotEqualsCanonicalizingType"> + <xs:annotation> + <xs:documentation> + Asserts that two given variables are equal. The contents are canonicalized before they are compared. + For instance, when the two variables $expected and $actual are arrays, then these arrays are + sorted before they are compared. + </xs:documentation> + </xs:annotation> + <xs:choice maxOccurs="unbounded"> + <xs:element name="expectedResult" type="expectedResultType" minOccurs="0"/> + <xs:element name="actualResult" type="actualResultType" minOccurs="0"/> + </xs:choice> + <xs:attribute ref="message"/> <xs:attributeGroup ref="commonActionAttributes"/> </xs:complexType> @@ -609,4 +761,4 @@ <xs:enumeration value="const"/> </xs:restriction> </xs:simpleType> -</xs:schema> \ No newline at end of file +</xs:schema> diff --git a/src/Magento/FunctionalTestingFramework/Test/etc/Actions/customActions.xsd b/src/Magento/FunctionalTestingFramework/Test/etc/Actions/customActions.xsd index fbb21456f..2ea291cd8 100644 --- a/src/Magento/FunctionalTestingFramework/Test/etc/Actions/customActions.xsd +++ b/src/Magento/FunctionalTestingFramework/Test/etc/Actions/customActions.xsd @@ -17,7 +17,7 @@ <xs:element type="closeAdminNotificationType" name="closeAdminNotification" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="searchAndMultiSelectOptionType" name="searchAndMultiSelectOption" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="selectMultipleOptionsType" name="selectMultipleOptions" minOccurs="0" maxOccurs="unbounded"/> - <xs:element type="formatMoneyType" name="formatMoney" minOccurs="0" maxOccurs="unbounded"/> + <xs:element type="formatCurrencyType" name="formatCurrency" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="parseFloatType" name="parseFloat" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="mSetLocaleType" name="mSetLocale" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="mResetLocaleType" name="mResetLocale" minOccurs="0" maxOccurs="unbounded"/> @@ -25,6 +25,13 @@ <xs:element type="clearFieldType" name="clearField" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="assertArrayIsSortedType" name="assertArrayIsSorted" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="generateDateType" name="generateDate" minOccurs="0" maxOccurs="unbounded"/> + <xs:element type="getOTPType" name="getOTP" minOccurs="0" maxOccurs="unbounded"/> + </xs:choice> + </xs:group> + + <xs:group name="returnTags"> + <xs:choice> + <xs:element type="returnType" name="return" minOccurs="0" maxOccurs="1"/> </xs:choice> </xs:group> @@ -178,25 +185,29 @@ <xs:attributeGroup ref="commonActionAttributes"/> </xs:complexType> - <xs:complexType name="formatMoneyType"> + <xs:complexType name="formatCurrencyType"> <xs:annotation> <xs:documentation> - Formats given input to given locale. Returns formatted string for test use. + Format input to specified currency according to the locale specified. Returns formatted string for test use. + Use NumberFormatter::formatCurrency(), see https://www.php.net/manual/en/numberformatter.formatcurrency.php </xs:documentation> </xs:annotation> - <xs:simpleContent> - <xs:extension base="xs:string"> - <xs:attribute ref="userInput"/> - <xs:attribute type="xs:string" name="locale"> - <xs:annotation> - <xs:documentation> - Locale to format given input. Defaults to 'en_US.UTF-8' if nothing is given. - </xs:documentation> - </xs:annotation> - </xs:attribute> - <xs:attributeGroup ref="commonActionAttributes"/> - </xs:extension> - </xs:simpleContent> + <xs:attribute type="xs:string" name="userInput" use="required"/> + <xs:attribute type="xs:string" name="locale" use="required"> + <xs:annotation> + <xs:documentation> + Locale in which the input would be formatted (e.g. en_US). + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute type="xs:string" name="currency" use="required"> + <xs:annotation> + <xs:documentation> + The 3-letter ISO 4217 currency code indicating the currency to use. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attributeGroup ref="commonActionAttributes"/> </xs:complexType> <xs:complexType name="parseFloatType"> @@ -316,6 +327,19 @@ </xs:simpleContent> </xs:complexType> + <xs:complexType name="getOTPType"> + <xs:annotation> + <xs:documentation> + Generates TOTP by a shared secret. Use https://github.com/Spomky-Labs/otphp + </xs:documentation> + </xs:annotation> + <xs:simpleContent> + <xs:extension base="xs:string"> + <xs:attribute ref="userInput"/> + <xs:attributeGroup ref="commonActionAttributes"/> + </xs:extension> + </xs:simpleContent> + </xs:complexType> <xs:complexType name="arrayType"> <xs:simpleContent> @@ -329,6 +353,26 @@ </xs:simpleContent> </xs:complexType> + <xs:complexType name="returnType"> + <xs:annotation> + <xs:documentation> + Used in an action group to return a value. Must be used only once in action group. Do not use in tests or suites. + </xs:documentation> + </xs:annotation> + <xs:simpleContent> + <xs:extension base="xs:string"> + <xs:attribute name="value" use="required" type="xs:string"> + <xs:annotation> + <xs:documentation> + Value or variable to be returned. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attributeGroup ref="commonActionAttributes"/> + </xs:extension> + </xs:simpleContent> + </xs:complexType> + <xs:simpleType name="sortEnum" final="restriction"> <xs:annotation> <xs:documentation> diff --git a/src/Magento/FunctionalTestingFramework/Test/etc/Actions/dataActions.xsd b/src/Magento/FunctionalTestingFramework/Test/etc/Actions/dataActions.xsd index 2cd614266..003760bfc 100644 --- a/src/Magento/FunctionalTestingFramework/Test/etc/Actions/dataActions.xsd +++ b/src/Magento/FunctionalTestingFramework/Test/etc/Actions/dataActions.xsd @@ -125,14 +125,32 @@ </xs:simpleContent> </xs:complexType> + <xs:complexType name="additionalFieldType"> - <xs:annotation> - <xs:documentation>field used to override defined fields from metadata or existing data definitions, during operation.</xs:documentation> - </xs:annotation> <xs:simpleContent> <xs:extension base="xs:string"> - <xs:attribute name="key" use="required"/> + <xs:attribute type="xs:string" name="key" use="required"> + <xs:annotation> + <xs:documentation>xp + Key attribute of data/value pair. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute type="uniquenessEnumType" name="unique" use="optional"> + <xs:annotation> + <xs:documentation> + Add suite or test wide unique sequence as "prefix" or "suffix" to the data value if specified. + </xs:documentation> + </xs:annotation> + </xs:attribute> </xs:extension> </xs:simpleContent> </xs:complexType> -</xs:schema> \ No newline at end of file + + <xs:simpleType name="uniquenessEnumType"> + <xs:restriction base="xs:string"> + <xs:enumeration value="prefix" /> + <xs:enumeration value="suffix" /> + </xs:restriction> + </xs:simpleType> +</xs:schema> diff --git a/src/Magento/FunctionalTestingFramework/Test/etc/Actions/grabActions.xsd b/src/Magento/FunctionalTestingFramework/Test/etc/Actions/grabActions.xsd index 9109489aa..993657a86 100644 --- a/src/Magento/FunctionalTestingFramework/Test/etc/Actions/grabActions.xsd +++ b/src/Magento/FunctionalTestingFramework/Test/etc/Actions/grabActions.xsd @@ -13,6 +13,7 @@ <xs:choice> <xs:element type="grabAttributeFromType" name="grabAttributeFrom" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="grabCookieType" name="grabCookie" minOccurs="0" maxOccurs="unbounded"/> + <xs:element type="grabCookieAttributesType" name="grabCookieAttributes" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="grabFromCurrentUrlType" name="grabFromCurrentUrl" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="grabMultipleType" name="grabMultiple" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="grabPageSourceType" name="grabPageSource" minOccurs="0" maxOccurs="unbounded"/> @@ -53,6 +54,21 @@ </xs:simpleContent> </xs:complexType> + <xs:complexType name="grabCookieAttributesType"> + <xs:annotation> + <xs:documentation> + Grabs a cookie attributes value. + </xs:documentation> + </xs:annotation> + <xs:simpleContent> + <xs:extension base="xs:string"> + <xs:attribute ref="userInput"/> + <xs:attribute ref="parameterArray"/> + <xs:attributeGroup ref="commonActionAttributes"/> + </xs:extension> + </xs:simpleContent> + </xs:complexType> + <xs:complexType name="grabFromCurrentUrlType"> <xs:annotation> <xs:documentation> @@ -129,4 +145,4 @@ </xs:extension> </xs:simpleContent> </xs:complexType> -</xs:schema> \ No newline at end of file +</xs:schema> diff --git a/src/Magento/FunctionalTestingFramework/Test/etc/Actions/waitActions.xsd b/src/Magento/FunctionalTestingFramework/Test/etc/Actions/waitActions.xsd index 70b8201a1..18214ba9a 100644 --- a/src/Magento/FunctionalTestingFramework/Test/etc/Actions/waitActions.xsd +++ b/src/Magento/FunctionalTestingFramework/Test/etc/Actions/waitActions.xsd @@ -23,6 +23,8 @@ <xs:element type="waitForPwaElementNotVisibleType" name="waitForPwaElementNotVisible" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="waitForPwaElementVisibleType" name="waitForPwaElementVisible" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="waitForTextType" name="waitForText" minOccurs="0" maxOccurs="unbounded"/> + <xs:element type="waitForElementClickableType" name="waitForElementClickable" minOccurs="0" maxOccurs="unbounded"/> + </xs:choice> </xs:group> @@ -224,4 +226,19 @@ </xs:extension> </xs:simpleContent> </xs:complexType> + <xs:complexType name="waitForElementClickableType"> + <xs:annotation> + <xs:documentation> + Waits up to $timeout seconds for the given element to be clickable. + If element doesn’t become clickable, a timeout exception is thrown. + </xs:documentation> + </xs:annotation> + <xs:simpleContent> + <xs:extension base="xs:string"> + <xs:attribute ref="selector" use="required"/> + <xs:attribute ref="time"/> + <xs:attributeGroup ref="commonActionAttributes"/> + </xs:extension> + </xs:simpleContent> + </xs:complexType> </xs:schema> \ No newline at end of file diff --git a/src/Magento/FunctionalTestingFramework/Test/etc/actionTypeTags.xsd b/src/Magento/FunctionalTestingFramework/Test/etc/actionTypeTags.xsd index 7bcdbdf50..1ce58001d 100644 --- a/src/Magento/FunctionalTestingFramework/Test/etc/actionTypeTags.xsd +++ b/src/Magento/FunctionalTestingFramework/Test/etc/actionTypeTags.xsd @@ -34,6 +34,7 @@ <xs:element type="closeTabType" name="closeTab" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="commentType" name="comment" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="dragAndDropType" name="dragAndDrop" minOccurs="0" maxOccurs="unbounded"/> + <xs:element type="rapidClickType" name="rapidClick" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="executeJSType" name="executeJS" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="fillFieldType" name="fillField" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="loadSessionSnapshotType" name="loadSessionSnapshot" minOccurs="0" maxOccurs="unbounded"/> @@ -43,7 +44,7 @@ <xs:element type="moveForwardType" name="moveForward" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="moveMouseOverType" name="moveMouseOver" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="openNewTabType" name="openNewTab" minOccurs="0" maxOccurs="unbounded"/> - <xs:element type="pauseExecutionType" name="pauseExecution" minOccurs="0" maxOccurs="unbounded"/> + <xs:element type="pauseType" name="pause" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="pressKeyType" name="pressKey" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="reloadPageType" name="reloadPage" minOccurs="0" maxOccurs="unbounded"/> <xs:element type="resetCookieType" name="resetCookie" minOccurs="0" maxOccurs="unbounded"/> @@ -68,6 +69,12 @@ </xs:choice> </xs:group> + <xs:group name="returnTypeTags"> + <xs:choice> + <xs:group ref="returnTags"/> + </xs:choice> + </xs:group> + <!-- Complex Types --> <xs:complexType name="failType"> @@ -247,6 +254,33 @@ </xs:simpleContent> </xs:complexType> + <xs:complexType name="rapidClickType"> + <xs:annotation> + <xs:documentation> + Performs simple mouse rapid click as per given count. + </xs:documentation> + </xs:annotation> + <xs:simpleContent> + <xs:extension base="xs:string"> + <xs:attribute type="xs:string" name="selector" use="required"> + <xs:annotation> + <xs:documentation> + Selector for rapid click. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute type="xs:string" name="count" use="required"> + <xs:annotation> + <xs:documentation> + Click count. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attributeGroup ref="commonActionAttributes"/> + </xs:extension> + </xs:simpleContent> + </xs:complexType> + <xs:complexType name="dragAndDropType"> <xs:annotation> <xs:documentation> @@ -415,7 +449,7 @@ </xs:simpleContent> </xs:complexType> - <xs:complexType name="pauseExecutionType"> + <xs:complexType name="pauseType"> <xs:annotation> <xs:documentation> Pauses test execution in debug mode. To proceed test, press "ENTER" in console. Useful for test writing. diff --git a/src/Magento/FunctionalTestingFramework/Test/etc/mergedActionGroupSchema.xsd b/src/Magento/FunctionalTestingFramework/Test/etc/mergedActionGroupSchema.xsd index bd32ba879..f45c33acd 100644 --- a/src/Magento/FunctionalTestingFramework/Test/etc/mergedActionGroupSchema.xsd +++ b/src/Magento/FunctionalTestingFramework/Test/etc/mergedActionGroupSchema.xsd @@ -15,29 +15,21 @@ </xs:choice> </xs:complexType> <xs:complexType name="actionsRefType"> - <xs:choice minOccurs="0" maxOccurs="unbounded"> - <xs:group ref="actionTypeTags"/> - <xs:element name="arguments"> - <xs:complexType> - <xs:sequence> - <xs:element name="argument" maxOccurs="unbounded" minOccurs="0"> - <xs:complexType> - <xs:attribute type="xs:string" name="name" use="required"/> - <xs:attribute type="xs:string" name="defaultValue"/> - <xs:attribute type="dataTypeEnum" name="type" default="entity"/> - </xs:complexType> - </xs:element> - </xs:sequence> - </xs:complexType> - </xs:element> - <xs:element name="annotations"> - <xs:complexType> - <xs:sequence> - <xs:element name="description"/> - </xs:sequence> - </xs:complexType> - </xs:element> - </xs:choice> + <xs:sequence> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:group ref="actionTypeTags"/> + <xs:element ref="arguments"/> + <xs:element ref="annotations"/> + </xs:choice> + <xs:sequence minOccurs="0"> + <xs:group ref="returnTags"/> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:group ref="actionTypeTags"/> + <xs:element ref="arguments"/> + <xs:element ref="annotations"/> + </xs:choice> + </xs:sequence> + </xs:sequence> <xs:attribute type="xs:string" name="name" use="required"/> <xs:attribute type="xs:string" name="filename"/> <xs:attribute type="xs:string" name="insertBefore"/> @@ -57,4 +49,28 @@ <xs:enumeration value="entity"/> </xs:restriction> </xs:simpleType> + + <!-- elements --> + + <xs:element name="arguments"> + <xs:complexType> + <xs:sequence> + <xs:element name="argument" maxOccurs="unbounded" minOccurs="0"> + <xs:complexType> + <xs:attribute type="xs:string" name="name" use="required"/> + <xs:attribute type="xs:string" name="defaultValue"/> + <xs:attribute type="dataTypeEnum" name="type" default="entity"/> + </xs:complexType> + </xs:element> + </xs:sequence> + </xs:complexType> + </xs:element> + + <xs:element name="annotations"> + <xs:complexType> + <xs:sequence> + <xs:element name="description"/> + </xs:sequence> + </xs:complexType> + </xs:element> </xs:schema> diff --git a/src/Magento/FunctionalTestingFramework/Upgrade/RemoveModuleFileInSuiteFiles.php b/src/Magento/FunctionalTestingFramework/Upgrade/RemoveModuleFileInSuiteFiles.php index ac36742f1..a42fbbf31 100644 --- a/src/Magento/FunctionalTestingFramework/Upgrade/RemoveModuleFileInSuiteFiles.php +++ b/src/Magento/FunctionalTestingFramework/Upgrade/RemoveModuleFileInSuiteFiles.php @@ -64,15 +64,16 @@ class RemoveModuleFileInSuiteFiles implements UpgradeInterface */ public function execute(InputInterface $input, OutputInterface $output) { + $scriptUtil = new ScriptUtil(); $this->setOutputStyle($input, $output); $this->output = $output; $testPaths[] = $input->getArgument('path'); if (empty($testPaths[0])) { - $testPaths = ScriptUtil::getAllModulePaths(); + $testPaths = $scriptUtil->getAllModulePaths(); } // Get module suite xml files - $xmlFiles = ScriptUtil::getModuleXmlFilesByScope($testPaths, 'Suite'); + $xmlFiles = $scriptUtil->getModuleXmlFilesByScope($testPaths, 'Suite'); $this->processXmlFiles($xmlFiles); return ("Removed module file reference in {$this->testsUpdated} suite file(s)."); diff --git a/src/Magento/FunctionalTestingFramework/Upgrade/RemoveUnusedArguments.php b/src/Magento/FunctionalTestingFramework/Upgrade/RemoveUnusedArguments.php new file mode 100644 index 000000000..87586c181 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Upgrade/RemoveUnusedArguments.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Upgrade; + +use Magento\FunctionalTestingFramework\StaticCheck\ActionGroupStandardsCheck; +use Magento\FunctionalTestingFramework\Util\Script\ScriptUtil; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Filesystem\Filesystem; +use DOMElement; + +/** + * Class RenameMetadataFiles + * @package Magento\FunctionalTestingFramework\Upgrade + */ +class RemoveUnusedArguments implements UpgradeInterface +{ + const ARGUMENTS_BLOCK_REGEX_PATTERN = "/\s*<arguments.*\/arguments>/s"; + + /** + * Updates all actionGroup xml files + * + * @param InputInterface $input + * @param OutputInterface $output + * @return string + */ + public function execute(InputInterface $input, OutputInterface $output) + { + $scriptUtil = new ScriptUtil(); + $testPaths[] = $input->getArgument('path'); + if (empty($testPaths[0])) { + $testPaths = $scriptUtil->getAllModulePaths(); + } + $xmlFiles = $scriptUtil->getModuleXmlFilesByScope($testPaths, 'ActionGroup'); + $actionGroupsUpdated = 0; + $fileSystem = new Filesystem(); + foreach ($xmlFiles as $file) { + $contents = $file->getContents(); + $argumentsCheck = new ActionGroupStandardsCheck(); + /** @var DOMElement $actionGroup */ + $actionGroup = $argumentsCheck->getActionGroupDomElement($contents); + $allArguments = $argumentsCheck->extractActionGroupArguments($actionGroup); + $unusedArguments = $argumentsCheck->findUnusedArguments($allArguments, $contents); + if (empty($unusedArguments)) { + continue; + } + //Remove <arguments> block if all arguments are unused + if (empty(array_diff($allArguments, $unusedArguments))) { + $contents = preg_replace(self::ARGUMENTS_BLOCK_REGEX_PATTERN, '', $contents); + } else { + foreach ($unusedArguments as $argument) { + $argumentRegexPattern = "/\s*<argument.*name\s*=\s*\"".$argument."\".*\/>/"; + $contents = preg_replace($argumentRegexPattern, '', $contents); + } + } + $fileSystem->dumpFile($file->getRealPath(), $contents); + $actionGroupsUpdated++; + } + return "Removed unused action group arguments from {$actionGroupsUpdated} file(s)."; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Upgrade/RenameMetadataFiles.php b/src/Magento/FunctionalTestingFramework/Upgrade/RenameMetadataFiles.php index 960456010..5be881a62 100644 --- a/src/Magento/FunctionalTestingFramework/Upgrade/RenameMetadataFiles.php +++ b/src/Magento/FunctionalTestingFramework/Upgrade/RenameMetadataFiles.php @@ -26,9 +26,10 @@ class RenameMetadataFiles implements UpgradeInterface */ public function execute(InputInterface $input, OutputInterface $output) { + $scriptUtil = new ScriptUtil(); $testPaths[] = $input->getArgument('path'); if (empty($testPaths[0])) { - $testPaths = ScriptUtil::getAllModulePaths(); + $testPaths = $scriptUtil->getAllModulePaths(); } foreach ($testPaths as $testsPath) { diff --git a/src/Magento/FunctionalTestingFramework/Upgrade/SplitMultipleEntitiesFiles.php b/src/Magento/FunctionalTestingFramework/Upgrade/SplitMultipleEntitiesFiles.php index fd7957bda..b0190d959 100644 --- a/src/Magento/FunctionalTestingFramework/Upgrade/SplitMultipleEntitiesFiles.php +++ b/src/Magento/FunctionalTestingFramework/Upgrade/SplitMultipleEntitiesFiles.php @@ -69,16 +69,17 @@ class SplitMultipleEntitiesFiles implements UpgradeInterface */ public function execute(InputInterface $input, OutputInterface $output) { + $scriptUtil = new ScriptUtil(); $this->output = $output; $this->testsUpdated = 0; $testPaths[] = $input->getArgument('path'); if (empty($testPaths[0])) { - $testPaths = ScriptUtil::getAllModulePaths(); + $testPaths = $scriptUtil->getAllModulePaths(); } // Process module xml files foreach ($this->entityCategories as $type => $urn) { - $xmlFiles = ScriptUtil::getModuleXmlFilesByScope($testPaths, $type); + $xmlFiles = $scriptUtil->getModuleXmlFilesByScope($testPaths, $type); $this->processXmlFiles($xmlFiles, $type, $urn); } diff --git a/src/Magento/FunctionalTestingFramework/Upgrade/UpdateAssertionSchema.php b/src/Magento/FunctionalTestingFramework/Upgrade/UpdateAssertionSchema.php index 95f7c9750..3c6825092 100644 --- a/src/Magento/FunctionalTestingFramework/Upgrade/UpdateAssertionSchema.php +++ b/src/Magento/FunctionalTestingFramework/Upgrade/UpdateAssertionSchema.php @@ -28,9 +28,10 @@ class UpdateAssertionSchema implements UpgradeInterface */ public function execute(InputInterface $input, OutputInterface $output) { + $scriptUtil = new ScriptUtil(); $testPaths[] = $input->getArgument('path'); if (empty($testPaths[0])) { - $testPaths = ScriptUtil::getAllModulePaths(); + $testPaths = $scriptUtil->getAllModulePaths(); } $testsUpdated = 0; @@ -103,9 +104,9 @@ private function convertOldAssertionToNew($assertion) $value = rtrim(ltrim($value, "'"), "'"); } // If value is empty string (" " or ' '), trim again to become empty - if (str_replace(" ", "", $value) == "''") { + if (str_replace(" ", "", $value) === "''") { $value = ""; - } elseif (str_replace(" ", "", $value) == '""') { + } elseif (str_replace(" ", "", $value) === '""') { $value = ""; } @@ -118,20 +119,20 @@ private function convertOldAssertionToNew($assertion) } // Store in subtype for child element creation - if ($type == "actual") { + if ($type === "actual") { $subElements["actual"]["value"] = $value; - } elseif ($type == "actualType") { + } elseif ($type === "actualType") { $subElements["actual"]["type"] = $value; - } elseif ($type == "expected" or $type == "expectedValue") { + } elseif ($type === "expected" or $type === "expectedValue") { $subElements["expected"]["value"] = $value; - } elseif ($type == "expectedType") { + } elseif ($type === "expectedType") { $subElements["expected"]["type"] = $value; } } $newString .= ">\n"; // Assert type is very edge-cased, completely different schema - if ($assertType == 'assertElementContainsAttribute') { + if ($assertType === 'assertElementContainsAttribute') { // assertElementContainsAttribute type defaulted to string if not present if (!isset($subElements["expected"]['type'])) { $subElements["expected"]['type'] = "string"; diff --git a/src/Magento/FunctionalTestingFramework/Upgrade/UpdateTestSchemaPaths.php b/src/Magento/FunctionalTestingFramework/Upgrade/UpdateTestSchemaPaths.php index 09140d1cd..e6d3358d6 100644 --- a/src/Magento/FunctionalTestingFramework/Upgrade/UpdateTestSchemaPaths.php +++ b/src/Magento/FunctionalTestingFramework/Upgrade/UpdateTestSchemaPaths.php @@ -57,16 +57,17 @@ class UpdateTestSchemaPaths implements UpgradeInterface */ public function execute(InputInterface $input, OutputInterface $output) { + $scriptUtil = new ScriptUtil(); $this->output = $output; $this->testsUpdated = 0; $testPaths[] = $input->getArgument('path'); if (empty($testPaths[0])) { - $testPaths = ScriptUtil::getAllModulePaths(); + $testPaths = $scriptUtil->getAllModulePaths(); } // Process module xml files foreach ($this->typeToUrns as $type => $urn) { - $xmlFiles = ScriptUtil::getModuleXmlFilesByScope($testPaths, $type); + $xmlFiles = $scriptUtil->getModuleXmlFilesByScope($testPaths, $type); $this->processXmlFiles($xmlFiles, $urn); } diff --git a/src/Magento/FunctionalTestingFramework/Upgrade/UpgradeScriptList.php b/src/Magento/FunctionalTestingFramework/Upgrade/UpgradeScriptList.php index 6293b589b..cf2aabe2d 100644 --- a/src/Magento/FunctionalTestingFramework/Upgrade/UpgradeScriptList.php +++ b/src/Magento/FunctionalTestingFramework/Upgrade/UpgradeScriptList.php @@ -28,6 +28,7 @@ class UpgradeScriptList implements UpgradeScriptListInterface public function __construct(array $scripts = []) { $this->scripts = [ + 'removeUnusedArguments' => new RemoveUnusedArguments(), 'upgradeTestSchema' => new UpdateTestSchemaPaths(), 'upgradeAssertionSchema' => new UpdateAssertionSchema(), 'renameMetadataFiles' => new RenameMetadataFiles(), diff --git a/src/Magento/FunctionalTestingFramework/Util/ComposerModuleResolver.php b/src/Magento/FunctionalTestingFramework/Util/ComposerModuleResolver.php index 210c10fdd..92d5dc121 100644 --- a/src/Magento/FunctionalTestingFramework/Util/ComposerModuleResolver.php +++ b/src/Magento/FunctionalTestingFramework/Util/ComposerModuleResolver.php @@ -48,7 +48,7 @@ public function getComposerInstalledTestModules($rootComposerFile) return $this->installedTestModules; } - if (!file_exists($rootComposerFile) || basename($rootComposerFile, '.json') != 'composer') { + if (!file_exists($rootComposerFile) || basename($rootComposerFile, '.json') !== 'composer') { throw new TestFrameworkException("Invalid root composer json file: {$rootComposerFile}"); } @@ -168,7 +168,7 @@ private function findComposerJsonFilesAtDepth($directory, $depth) self::findComposerJsonFilesAtDepth($dir, $depth-1) ); } - } elseif ($depth == 0) { + } elseif ($depth === 0) { $jsonFileList = glob($directory . $jsonPattern); if ($jsonFileList === false) { $jsonFileList = []; diff --git a/src/Magento/FunctionalTestingFramework/Util/ConfigSanitizerUtil.php b/src/Magento/FunctionalTestingFramework/Util/ConfigSanitizerUtil.php index 3b92b48e4..ee4c85018 100644 --- a/src/Magento/FunctionalTestingFramework/Util/ConfigSanitizerUtil.php +++ b/src/Magento/FunctionalTestingFramework/Util/ConfigSanitizerUtil.php @@ -43,16 +43,16 @@ public static function sanitizeWebDriverConfig($config, $params = ['url', 'selen */ private static function sanitizeSeleniumEnvs($config) { - if ($config['protocol'] == '%SELENIUM_PROTOCOL%') { + if ($config['protocol'] === '%SELENIUM_PROTOCOL%') { $config['protocol'] = "http"; } - if ($config['host'] == '%SELENIUM_HOST%') { + if ($config['host'] === '%SELENIUM_HOST%') { $config['host'] = "127.0.0.1"; } - if ($config['port'] == '%SELENIUM_PORT%') { + if ($config['port'] === '%SELENIUM_PORT%') { $config['port'] = "4444"; } - if ($config['path'] == '%SELENIUM_PATH%') { + if ($config['path'] === '%SELENIUM_PATH%') { $config['path'] = "/wd/hub"; } return $config; diff --git a/src/Magento/FunctionalTestingFramework/Util/Filesystem/CestFileCreatorUtil.php b/src/Magento/FunctionalTestingFramework/Util/Filesystem/CestFileCreatorUtil.php new file mode 100644 index 000000000..9fe205151 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Util/Filesystem/CestFileCreatorUtil.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\FunctionalTestingFramework\Util\Filesystem; + +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; + +class CestFileCreatorUtil +{ + /** + * Singleton CestFileCreatorUtil Instance. + * + * @var CestFileCreatorUtil + */ + private static $INSTANCE; + + /** + * CestFileCreatorUtil constructor. + */ + private function __construct() + { + } + + /** + * Get CestFileCreatorUtil instance. + * + * @return CestFileCreatorUtil + */ + public static function getInstance(): CestFileCreatorUtil + { + if (!self::$INSTANCE) { + self::$INSTANCE = new CestFileCreatorUtil(); + } + + return self::$INSTANCE; + } + + /** + * Create a single PHP file containing the $cestPhp using the $filename. + * If the _generated directory doesn't exist it will be created. + * + * @param string $filename + * @param string $exportDirectory + * @param string $testPhp + * + * @return void + * @throws TestFrameworkException + */ + public function create(string $filename, string $exportDirectory, string $testPhp): void + { + DirSetupUtil::createGroupDir($exportDirectory); + $exportFilePath = $exportDirectory . DIRECTORY_SEPARATOR . $filename . '.php'; + $file = fopen($exportFilePath, 'w'); + + if (!$file) { + throw new TestFrameworkException( + sprintf('Could not open test file: "%s"', $exportFilePath) + ); + } + + fwrite($file, $testPhp); + fclose($file); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Util/Filesystem/DirSetupUtil.php b/src/Magento/FunctionalTestingFramework/Util/Filesystem/DirSetupUtil.php index 9f0036629..120b74ac3 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Filesystem/DirSetupUtil.php +++ b/src/Magento/FunctionalTestingFramework/Util/Filesystem/DirSetupUtil.php @@ -49,6 +49,10 @@ public static function createGroupDir($fullPath) */ public static function rmdirRecursive($directory) { + if (!is_dir($directory)) { + return; + } + $it = new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS); while ($it->valid()) { diff --git a/src/Magento/FunctionalTestingFramework/Util/GenerationErrorHandler.php b/src/Magento/FunctionalTestingFramework/Util/GenerationErrorHandler.php new file mode 100644 index 000000000..a86cd459d --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Util/GenerationErrorHandler.php @@ -0,0 +1,176 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\FunctionalTestingFramework\Util; + +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use PHP_CodeSniffer\Exceptions\RuntimeException; + +class GenerationErrorHandler +{ + /** + * Generation Error Handler Instance + * + * @var GenerationErrorHandler + */ + private static $instance; + + /** + * Collected errors + * + * @var array + */ + private $errors = []; + + /** + * Singleton method to return GenerationErrorHandler + * + * @return GenerationErrorHandler + */ + public static function getInstance() + { + if (!self::$instance) { + self::$instance = new GenerationErrorHandler(); + } + + return self::$instance; + } + + /** + * GenerationErrorHandler constructor + */ + private function __construct() + { + } + + /** + * Add a generation error into error handler + * + * @param string $type + * @param string $entityName + * @param string $message + * @param boolean $generated + * @return void + * @throws \Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException + */ + public function addError($type, $entityName, $message, $generated = false) + { + $error[$entityName] = [ + 'message' => $message, + 'generated' => $generated, + ]; + if (isset($this->errors[$type])) { + $this->errors[$type] = array_merge_recursive($this->errors[$type], $error); + } else { + $this->errors[$type] = $error; + } + } + + /** + * Return all errors + * + * @return array + */ + public function getAllErrors() + { + return $this->errors; + } + + /** + * Return all error message in a string + * + * @return string + */ + public function getAllErrorMessages() + { + $errMessages = ''; + + foreach ($this->errors as $type => $errors) { + foreach ($errors as $error) { + if (is_array($error['message'])) { + $errMessages .= (!empty($errMessages) ? PHP_EOL : '') . implode(PHP_EOL, $error['message']); + } else { + $errMessages .= (!empty($errMessages) ? PHP_EOL : '') . $error['message']; + } + } + } + + return $errMessages; + } + + /** + * Return errors for given type + * + * @param string $type + * @return array + */ + public function getErrorsByType($type) + { + return $this->errors[$type] ?? []; + } + + /** + * Reset error to empty array + * + * @return void + */ + public function reset() + { + $this->errors = []; + } + + /** + * Print error summary in console + * + * @return void + */ + public function printErrorSummary() + { + foreach (array_keys($this->errors) as $type) { + $totalErrors = count($this->getErrorsByType($type)); + $totalAnnotationErrors = 0; + foreach ($this->getErrorsByType($type) as $entity => $error) { + if ((is_array($error['generated']) && $error['generated'][0] === true) + || ($error['generated'] === true)) { + $totalAnnotationErrors++; + } + } + $totalNotGenErrors = $totalErrors - $totalAnnotationErrors; + if ($totalNotGenErrors > 0) { + print( + 'ERROR: ' + . strval($totalNotGenErrors) + . ' ' + . ucfirst($type) + . "(s) failed to generate. See mftf.log for details." + . PHP_EOL + ); + } + if ($totalAnnotationErrors > 0) { + if ($type !== 'suite') { + print( + 'ERROR: ' + . strval($totalAnnotationErrors) + . ' ' + . ucfirst($type) + . "(s) generated with annotation errors. See mftf.log for details." + . PHP_EOL + ); + } else { + print( + 'ERROR: ' + . strval($totalAnnotationErrors) + . ' ' + . ucfirst($type) + . '(s) has(have) tests with annotation errors or some included tests missing.' + . ' See mftf.log for details.' + . PHP_EOL + ); + } + } + } + print(PHP_EOL); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Util/Iterator/AbstractIterator.php b/src/Magento/FunctionalTestingFramework/Util/Iterator/AbstractIterator.php index 69e2527aa..85b5dd411 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Iterator/AbstractIterator.php +++ b/src/Magento/FunctionalTestingFramework/Util/Iterator/AbstractIterator.php @@ -33,6 +33,7 @@ abstract class AbstractIterator implements \Iterator, \Countable * * @return mixed */ + #[\ReturnTypeWillChange] abstract public function current(); // @codingStandardsIgnoreEnd @@ -48,14 +49,14 @@ abstract public function current(); * * @return boolean */ - abstract protected function isValid(); + abstract protected function isValid() : bool; /** * Initialize Data Array * * @return void */ - public function rewind() + public function rewind() : void { reset($this->data); if (!$this->isValid()) { @@ -68,7 +69,7 @@ public function rewind() * * @return void */ - public function next() + public function next() : void { $this->current = next($this->data); @@ -86,7 +87,7 @@ public function next() * * @return boolean */ - public function valid() + public function valid() : bool { $current = current($this->data); if ($current === false || $current === null) { @@ -96,22 +97,25 @@ public function valid() } } + // @codingStandardsIgnoreStart /** * Get data key of the current data element * * @return integer|string */ + #[\ReturnTypeWillChange] public function key() { return key($this->data); } + // @codingStandardsIgnoreEnd /** * To make iterator countable * * @return integer */ - public function count() + public function count() : int { return count($this->data); } diff --git a/src/Magento/FunctionalTestingFramework/Util/Iterator/File.php b/src/Magento/FunctionalTestingFramework/Util/Iterator/File.php index 0186acd11..aafc4e288 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Iterator/File.php +++ b/src/Magento/FunctionalTestingFramework/Util/Iterator/File.php @@ -58,7 +58,7 @@ public function current() * * @return boolean */ - protected function isValid() + protected function isValid() : bool { return true; } diff --git a/src/Magento/FunctionalTestingFramework/Util/Logger/LoggingUtil.php b/src/Magento/FunctionalTestingFramework/Util/Logger/LoggingUtil.php index 97fdada21..51d707305 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Logger/LoggingUtil.php +++ b/src/Magento/FunctionalTestingFramework/Util/Logger/LoggingUtil.php @@ -9,7 +9,6 @@ use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; use Magento\FunctionalTestingFramework\Util\Path\FilePathFormatter; use Monolog\Handler\StreamHandler; -use Monolog\Logger; class LoggingUtil { @@ -37,7 +36,6 @@ public static function getInstance(): LoggingUtil if (self::$instance === null) { self::$instance = new LoggingUtil(); } - return self::$instance; } @@ -63,12 +61,12 @@ private function __clone() * * @param string $className * @return MftfLogger - * @throws \Exception + * @throws TestFrameworkException */ public function getLogger($className): MftfLogger { - if ($className == null) { - throw new \Exception("You must pass a class name to receive a logger"); + if ($className === null) { + throw new TestFrameworkException("You must pass a class name to receive a logger"); } if (!array_key_exists($className, $this->loggers)) { diff --git a/src/Magento/FunctionalTestingFramework/Util/Logger/MftfLogger.php b/src/Magento/FunctionalTestingFramework/Util/Logger/MftfLogger.php index 0e8c1e17f..880dd9736 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Logger/MftfLogger.php +++ b/src/Magento/FunctionalTestingFramework/Util/Logger/MftfLogger.php @@ -7,28 +7,55 @@ namespace Magento\FunctionalTestingFramework\Util\Logger; use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; -use Monolog\Handler\StreamHandler; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Monolog\Handler\HandlerInterface; use Monolog\Logger; class MftfLogger extends Logger { + /** + * MFTF execution phase + * + * @var string + */ + private $phase; + + /** + * MftfLogger constructor. + * + * @param string $name + * @param HandlerInterface[] $handlers + * @param callable[] $processors + * @throws TestFrameworkException + */ + public function __construct($name, array $handlers = [], array $processors = []) + { + parent::__construct($name, $handlers, $processors); + $this->phase = MftfApplicationConfig::getConfig()->getPhase(); + } + /** * Prints a deprecation warning, as well as adds a log at the WARNING level. + * Suppresses logging during execution phase. * * @param string $message The log message. * @param array $context The log context. * @param boolean $verbose * @return void - * @throws \Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException */ public function deprecation($message, array $context = [], $verbose = false) { $message = "DEPRECATION: " . $message; - // Suppress print during unit testing - if (MftfApplicationConfig::getConfig()->getPhase() !== MftfApplicationConfig::UNIT_TEST_PHASE && $verbose) { + // print during test generation including metadata + if ((array_key_exists('operationType', $context) || + $this->phase === MftfApplicationConfig::GENERATION_PHASE) && $verbose) { print ($message . json_encode($context) . "\n"); } - parent::warning($message, $context); + // suppress logging during test execution except metadata + if (array_key_exists('operationType', $context) || + $this->phase !== MftfApplicationConfig::EXECUTION_PHASE) { + parent::warning($message, $context); + } } /** @@ -38,13 +65,12 @@ public function deprecation($message, array $context = [], $verbose = false) * @param array $context The log context. * @param boolean $verbose * @return void - * @throws \Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException */ public function criticalFailure($message, array $context = [], $verbose = false) { $message = "FAILURE: " . $message; // Suppress print during unit testing - if (MftfApplicationConfig::getConfig()->getPhase() !== MftfApplicationConfig::UNIT_TEST_PHASE && $verbose) { + if ($this->phase !== MftfApplicationConfig::UNIT_TEST_PHASE && $verbose) { print ($message . implode("\n", $context) . "\n"); } parent::critical($message, $context); @@ -52,20 +78,23 @@ public function criticalFailure($message, array $context = [], $verbose = false) /** * Adds a log record at the NOTICE level. + * Suppresses logging during execution phase. * * @param string $message * @param array $context * @param boolean $verbose * @return void - * @throws \Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException */ public function notification($message, array $context = [], $verbose = false) { $message = "NOTICE: " . $message; - // Suppress print during unit testing - if (MftfApplicationConfig::getConfig()->getPhase() !== MftfApplicationConfig::UNIT_TEST_PHASE && $verbose) { - print ($message . implode("\n", $context) . "\n"); + // print during test generation + if ($this->phase === MftfApplicationConfig::GENERATION_PHASE && $verbose) { + print ($message . json_encode($context) . "\n"); + } + // suppress logging during test execution + if ($this->phase !== MftfApplicationConfig::EXECUTION_PHASE) { + parent::notice($message, $context); } - parent::notice($message, $context); } } diff --git a/src/Magento/FunctionalTestingFramework/Util/Manifest/ParallelTestManifest.php b/src/Magento/FunctionalTestingFramework/Util/Manifest/BaseParallelTestManifest.php similarity index 71% rename from src/Magento/FunctionalTestingFramework/Util/Manifest/ParallelTestManifest.php rename to src/Magento/FunctionalTestingFramework/Util/Manifest/BaseParallelTestManifest.php index 201e002a2..1e9e1d109 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Manifest/ParallelTestManifest.php +++ b/src/Magento/FunctionalTestingFramework/Util/Manifest/BaseParallelTestManifest.php @@ -6,60 +6,59 @@ namespace Magento\FunctionalTestingFramework\Util\Manifest; -use Codeception\Suite; use Magento\Framework\Exception\RuntimeException; -use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; -use Magento\FunctionalTestingFramework\Suite\Objects\SuiteObject; -use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; use Magento\FunctionalTestingFramework\Test\Objects\TestObject; use Magento\FunctionalTestingFramework\Util\Filesystem\DirSetupUtil; use Magento\FunctionalTestingFramework\Util\Sorter\ParallelGroupSorter; -use RecursiveArrayIterator; -use RecursiveIteratorIterator; -class ParallelTestManifest extends BaseTestManifest +abstract class BaseParallelTestManifest extends BaseTestManifest { - const PARALLEL_CONFIG = 'parallel'; - /** * An associate array of test name to size of test. * * @var string[] */ - private $testNameToSize = []; + protected $testNameToSize = []; /** * Class variable to store resulting group config. * * @var array */ - private $testGroups; + protected $testGroups; /** * An instance of the group sorter which will take suites and tests organizing them to be run together. * * @var ParallelGroupSorter */ - private $parallelGroupSorter; + protected $parallelGroupSorter; /** * Path to the directory that will contain all test group files * * @var string */ - private $dirPath; + protected $dirPath; + + /** + * An array of test name count in a single group + * @var array + */ + protected $testCountsToGroup = []; /** - * TestManifest constructor. + * BaseParallelTestManifest constructor. * * @param array $suiteConfiguration + * @param string $runConfig * @param string $testPath */ - public function __construct($suiteConfiguration, $testPath) + public function __construct($suiteConfiguration, $runConfig, $testPath) { $this->dirPath = dirname($testPath) . DIRECTORY_SEPARATOR . 'groups'; $this->parallelGroupSorter = new ParallelGroupSorter(); - parent::__construct($testPath, self::PARALLEL_CONFIG, $suiteConfiguration); + parent::__construct($testPath, $runConfig, $suiteConfiguration); } /** @@ -74,22 +73,12 @@ public function addTest($testObject) } /** - * Function which generates test groups based on arg passed. The function builds groups using the args as an upper - * limit. + * Function which generates test groups based on arg passed. * - * @param integer $time + * @param integer $number * @return void */ - public function createTestGroups($time) - { - $this->testGroups = $this->parallelGroupSorter->getTestsGroupedBySize( - $this->getSuiteConfig(), - $this->testNameToSize, - $time - ); - - $this->suiteConfiguration = $this->parallelGroupSorter->getResultingSuiteConfig(); - } + abstract public function createTestGroups($number); /** * Function which generates the actual manifest once the relevant tests have been added to the array. @@ -104,6 +93,8 @@ public function generate() foreach ($this->testGroups as $groupNumber => $groupContents) { $this->generateGroupFile($groupContents, $groupNumber, $suites); } + + $this->generateGroupSummaryFile($this->testCountsToGroup); } /** @@ -126,22 +117,40 @@ public function getSorter() * @param array $suites * @return void */ - private function generateGroupFile($testGroup, $nodeNumber, $suites) + protected function generateGroupFile($testGroup, $nodeNumber, $suites) { foreach ($testGroup as $entryName => $testValue) { $fileResource = fopen($this->dirPath . DIRECTORY_SEPARATOR . "group{$nodeNumber}.txt", 'a'); - $line = null; + $this->testCountsToGroup["group{$nodeNumber}"] = $this->testCountsToGroup["group{$nodeNumber}"] ?? 0; + if (!empty($suites[$entryName])) { $line = "-g {$entryName}"; + $this->testCountsToGroup["group{$nodeNumber}"] += count($suites[$entryName]); } else { $line = $this->relativeDirPath . DIRECTORY_SEPARATOR . $entryName . '.php'; + $this->testCountsToGroup["group{$nodeNumber}"]++; } fwrite($fileResource, $line . PHP_EOL); fclose($fileResource); } } + /** + * @param array $groups + * @return void + */ + protected function generateGroupSummaryFile(array $groups) + { + $fileResource = fopen($this->dirPath . DIRECTORY_SEPARATOR . "mftf_group_summary.txt", 'w'); + $contents = "Total Number of Groups: " . count($groups) . PHP_EOL; + foreach ($groups as $key => $value) { + $contents .= $key . " - ". $value . " tests" .PHP_EOL; + } + fwrite($fileResource, $contents); + fclose($fileResource); + } + /** * Function which recusrively parses a given potentially multidimensional array of suites containing their split * groups. The result is a flattened array of suite names to relevant tests for generation of the manifest. @@ -149,7 +158,7 @@ private function generateGroupFile($testGroup, $nodeNumber, $suites) * @param array $multiDimensionalSuites * @return array */ - private function getFlattenedSuiteConfiguration($multiDimensionalSuites) + protected function getFlattenedSuiteConfiguration($multiDimensionalSuites) { $suites = []; foreach ($multiDimensionalSuites as $suiteName => $suiteContent) { diff --git a/src/Magento/FunctionalTestingFramework/Util/Manifest/ParallelByGroupTestManifest.php b/src/Magento/FunctionalTestingFramework/Util/Manifest/ParallelByGroupTestManifest.php new file mode 100644 index 000000000..1baced519 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Util/Manifest/ParallelByGroupTestManifest.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Util\Manifest; + +use Magento\Framework\Exception\RuntimeException; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Test\Objects\TestObject; +use Magento\FunctionalTestingFramework\Util\Filesystem\DirSetupUtil; +use Magento\FunctionalTestingFramework\Util\Sorter\ParallelGroupSorter; + +class ParallelByGroupTestManifest extends BaseParallelTestManifest +{ + const PARALLEL_CONFIG = 'parallelByGroup'; + + /** + * ParallelByGroupTestManifest constructor. + * + * @param array $suiteConfiguration + * @param string $testPath + */ + public function __construct($suiteConfiguration, $testPath) + { + parent::__construct($suiteConfiguration, self::PARALLEL_CONFIG, $testPath); + } + + /** + * Function which generates test groups based on arg passed. + * + * @param integer $totalGroups + * @return void + * @throws TestFrameworkException + */ + public function createTestGroups($totalGroups) + { + $this->testGroups = $this->parallelGroupSorter->getTestsGroupedByFixedGroupCount( + $this->getSuiteConfig(), + $this->testNameToSize, + $totalGroups + ); + + $this->suiteConfiguration = $this->parallelGroupSorter->getResultingSuiteConfig(); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Util/Manifest/ParallelByTimeTestManifest.php b/src/Magento/FunctionalTestingFramework/Util/Manifest/ParallelByTimeTestManifest.php new file mode 100644 index 000000000..2a0984280 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Util/Manifest/ParallelByTimeTestManifest.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Util\Manifest; + +use Codeception\Suite; +use Magento\Framework\Exception\RuntimeException; +use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; +use Magento\FunctionalTestingFramework\Suite\Objects\SuiteObject; +use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; +use Magento\FunctionalTestingFramework\Test\Objects\TestObject; +use Magento\FunctionalTestingFramework\Util\Filesystem\DirSetupUtil; +use Magento\FunctionalTestingFramework\Util\Sorter\ParallelGroupSorter; +use RecursiveArrayIterator; +use RecursiveIteratorIterator; + +class ParallelByTimeTestManifest extends BaseParallelTestManifest +{ + const PARALLEL_CONFIG = 'parallelByTime'; + + /** + * GroupBasedParallelTestManifest constructor. + * + * @param array $suiteConfiguration + * @param string $testPath + */ + public function __construct($suiteConfiguration, $testPath) + { + parent::__construct($suiteConfiguration, self::PARALLEL_CONFIG, $testPath); + } + + /** + * Function which generates test groups based on arg passed. The function builds groups using the args as an upper + * limit. + * + * @param integer $time + * @return void + */ + public function createTestGroups($time) + { + $this->testGroups = $this->parallelGroupSorter->getTestsGroupedBySize( + $this->getSuiteConfig(), + $this->testNameToSize, + $time + ); + + $this->suiteConfiguration = $this->parallelGroupSorter->getResultingSuiteConfig(); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Util/Manifest/TestManifestFactory.php b/src/Magento/FunctionalTestingFramework/Util/Manifest/TestManifestFactory.php index fd7d885bf..e07007264 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Manifest/TestManifestFactory.php +++ b/src/Magento/FunctionalTestingFramework/Util/Manifest/TestManifestFactory.php @@ -41,8 +41,11 @@ public static function makeManifest($runConfig, $suiteConfiguration, $testPath = case 'singleRun': return new SingleRunTestManifest($suiteConfiguration, $testDirFullPath); - case 'parallel': - return new ParallelTestManifest($suiteConfiguration, $testDirFullPath); + case 'parallelByTime': + return new ParallelByTimeTestManifest($suiteConfiguration, $testDirFullPath); + + case 'parallelByGroup': + return new ParallelByGroupTestManifest($suiteConfiguration, $testDirFullPath); default: return new DefaultTestManifest($suiteConfiguration, $testDirFullPath); diff --git a/src/Magento/FunctionalTestingFramework/Util/MftfGlobals.php b/src/Magento/FunctionalTestingFramework/Util/MftfGlobals.php new file mode 100644 index 000000000..cb0f4f5ae --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Util/MftfGlobals.php @@ -0,0 +1,163 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\FunctionalTestingFramework\Util; + +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Util\Path\UrlFormatter; + +/** + * MFTF Globals + */ +class MftfGlobals +{ + /** + * Magento Base URL + * + * @var string null + */ + private static $baseUrl = null; + + /** + * Magento Backend Base URL + * + * @var string null + */ + private static $backendBaseUrl = null; + + /** + * Magento Web API Base URL + * + * @var string null + */ + private static $webApiBaseUrl = null; + + /** + * Returns Magento Base URL + * + * @param boolean $withTrailingSeparator + * @return string + * @throws TestFrameworkException + */ + public static function getBaseUrl($withTrailingSeparator = true) + { + if (!self::$baseUrl) { + try { + $url = getenv('MAGENTO_BASE_URL'); + if ($url) { + self::$baseUrl = UrlFormatter::format($url, false); + } + } catch (TestFrameworkException $e) { + } + } + + if (self::$baseUrl) { + return UrlFormatter::format(self::$baseUrl, $withTrailingSeparator); + } + + throw new TestFrameworkException( + 'Unable to retrieve Magento Base URL. Please check .env and set:' + . PHP_EOL + . '"MAGENTO_BASE_URL"' + ); + } + + /** + * Return Magento Backend Base URL + * + * @param boolean $withTrailingSeparator + * @return string + * @throws TestFrameworkException + */ + public static function getBackendBaseUrl($withTrailingSeparator = true) + { + if (!self::$backendBaseUrl) { + try { + $backendName = getenv('MAGENTO_BACKEND_NAME'); + $bUrl = getenv('MAGENTO_BACKEND_BASE_URL'); + if ($bUrl && $backendName) { + self::$backendBaseUrl = UrlFormatter::format( + UrlFormatter::format($bUrl) . $backendName, + false + ); + } else { + $baseUrl = getenv('MAGENTO_BASE_URL'); + if ($baseUrl && $backendName) { + self::$backendBaseUrl = UrlFormatter::format( + UrlFormatter::format($baseUrl) . $backendName, + false + ); + } + } + } catch (TestFrameworkException $e) { + } + } + + if (self::$backendBaseUrl) { + return UrlFormatter::format(self::$backendBaseUrl, $withTrailingSeparator); + } + + throw new TestFrameworkException( + 'Unable to retrieve Magento Backend Base URL. Please check .env and set either:' + . PHP_EOL + . '"MAGENTO_BASE_URL" and "MAGENTO_BACKEND_NAME"' + . PHP_EOL + . 'or' + . PHP_EOL + . '"MAGENTO_BACKEND_BASE_URL"' + ); + } + + /** + * Return Web API Base URL + * + * @param boolean $withTrailingSeparator + * @return string + * @throws TestFrameworkException + */ + public static function getWebApiBaseUrl($withTrailingSeparator = true) + { + if (!self::$webApiBaseUrl) { + try { + $webapiHost = getenv('MAGENTO_RESTAPI_SERVER_HOST'); + $webapiPort = getenv("MAGENTO_RESTAPI_SERVER_PORT"); + $webapiProtocol = getenv("MAGENTO_RESTAPI_SERVER_PROTOCOL"); + + if ($webapiHost && $webapiProtocol) { + $baseUrl = UrlFormatter::format( + sprintf('%s://%s', $webapiProtocol, $webapiHost), + false + ); + } elseif ($webapiHost) { + $baseUrl = UrlFormatter::format($webapiHost, false); + } + + if (!isset($baseUrl)) { + $baseUrl = MftfGlobals::getBaseUrl(false); + } + + if ($webapiPort) { + $baseUrl .= ':' . $webapiPort; + } + + self::$webApiBaseUrl = $baseUrl . '/rest'; + } catch (TestFrameworkException $e) { + } + } + if (self::$webApiBaseUrl) { + return UrlFormatter::format(self::$webApiBaseUrl, $withTrailingSeparator); + } + throw new TestFrameworkException( + 'Unable to retrieve Magento Web API Base URL. Please check .env and set either:' + . PHP_EOL + . '"MAGENTO_BASE_URL"' + . PHP_EOL + . 'or' + . PHP_EOL + . '"MAGENTO_RESTAPI_SERVER_HOST"' + ); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Util/ModulePathExtractor.php b/src/Magento/FunctionalTestingFramework/Util/ModulePathExtractor.php index b7386a586..a9f51d226 100644 --- a/src/Magento/FunctionalTestingFramework/Util/ModulePathExtractor.php +++ b/src/Magento/FunctionalTestingFramework/Util/ModulePathExtractor.php @@ -72,7 +72,7 @@ public function getExtensionPath($path) private function splitKeyForParts($key) { $parts = explode(self::SPLIT_DELIMITER, $key); - return count($parts) == 2 ? $parts : []; + return count($parts) === 2 ? $parts : []; } /** @@ -90,7 +90,7 @@ private function extractKeyByPath($path) } foreach ($this->testModulePaths as $key => $value) { - if (substr($path, 0, strlen($value)) == $value) { + if (substr($path, 0, strlen($value)) === $value) { return $key; } } diff --git a/src/Magento/FunctionalTestingFramework/Util/ModuleResolver.php b/src/Magento/FunctionalTestingFramework/Util/ModuleResolver.php index b24e77d9c..2cbed00fc 100644 --- a/src/Magento/FunctionalTestingFramework/Util/ModuleResolver.php +++ b/src/Magento/FunctionalTestingFramework/Util/ModuleResolver.php @@ -7,11 +7,13 @@ namespace Magento\FunctionalTestingFramework\Util; use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\CredentialStore; +use Magento\FunctionalTestingFramework\Exceptions\FastFailException; use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Util\ModuleResolver\ModuleResolverService; use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; use Magento\FunctionalTestingFramework\Util\Path\FilePathFormatter; use Magento\FunctionalTestingFramework\Util\Path\UrlFormatter; -use Symfony\Component\HttpFoundation\Response; use \Magento\FunctionalTestingFramework\Util\ModuleResolver\AlphabeticSequenceSorter; use \Magento\FunctionalTestingFramework\Util\ModuleResolver\SequenceSorterInterface; @@ -25,9 +27,9 @@ class ModuleResolver { /** - * Environment field name for module whitelist. + * Environment field name for module allowlist. */ - const MODULE_WHITELIST = 'MODULE_WHITELIST'; + const MODULE_ALLOWLIST = 'MODULE_ALLOWLIST'; /** * Environment field name for custom module paths. @@ -132,31 +134,10 @@ class ModuleResolver * * @var array */ - protected $moduleBlacklist = [ + protected $moduleBlocklist = [ 'SampleTests', 'SampleTemplates' ]; - /** - * Registered module list in magento system under test - * - * @var array - */ - private $registeredModuleList = []; - - /** - * Composer json based test module paths - * - * @var array - */ - private $composerJsonModulePaths = null; - - /** - * Composer installed test module paths - * - * @var array - */ - private $composerInstalledModulePaths = null; - /** * Get ModuleResolver instance. * @@ -177,7 +158,7 @@ private function __construct() { $objectManager = \Magento\FunctionalTestingFramework\ObjectManagerFactory::getObjectManager(); - if (MftfApplicationConfig::getConfig()->getPhase() == MftfApplicationConfig::UNIT_TEST_PHASE) { + if (MftfApplicationConfig::getConfig()->getPhase() === MftfApplicationConfig::UNIT_TEST_PHASE) { $this->sequenceSorter = $objectManager->get(AlphabeticSequenceSorter::class); } else { $this->sequenceSorter = $objectManager->get(SequenceSorterInterface::class); @@ -189,6 +170,7 @@ private function __construct() * * @return array * @throws TestFrameworkException + * @throws FastFailException */ public function getEnabledModules() { @@ -196,11 +178,11 @@ public function getEnabledModules() return $this->enabledModules; } - if (MftfApplicationConfig::getConfig()->getPhase() == MftfApplicationConfig::GENERATION_PHASE) { + if (MftfApplicationConfig::getConfig()->getPhase() === MftfApplicationConfig::GENERATION_PHASE) { $this->printMagentoVersionInfo(); } - $token = $this->getAdminToken(); + $token = ModuleResolverService::getInstance()->getAdminToken(); $url = UrlFormatter::format(getenv('MAGENTO_BASE_URL')) . $this->moduleUrl; @@ -217,12 +199,14 @@ public function getEnabledModules() if (!$response) { $message = "Could not retrieve Modules from Magento Instance."; + $encryptedSecret = CredentialStore::getInstance()->getSecret('magento/MAGENTO_ADMIN_PASSWORD'); + $secret = CredentialStore::getInstance()->decryptSecretValue($encryptedSecret); $context = [ "Admin Module List Url" => $url, "MAGENTO_ADMIN_USERNAME" => getenv("MAGENTO_ADMIN_USERNAME"), - "MAGENTO_ADMIN_PASSWORD" => getenv("MAGENTO_ADMIN_PASSWORD"), + "MAGENTO_ADMIN_PASSWORD" => $secret, ]; - throw new TestFrameworkException($message, $context); + throw new FastFailException($message, $context); } $this->enabledModules = json_decode($response); @@ -235,6 +219,8 @@ public function getEnabledModules() * * @param boolean $verbosePath * @return array + * @throws TestFrameworkException + * @throws FastFailException */ public function getModulesPath($verbosePath = false) { @@ -247,7 +233,7 @@ public function getModulesPath($verbosePath = false) } // Find test modules paths by searching patterns (Test/Mftf, etc) - $allModulePaths = $this->aggregateTestModulePaths(); + $allModulePaths = ModuleResolverService::getInstance()->aggregateTestModulePaths(); // Find test modules paths by searching test composer.json files $composerBasedModulePaths = $this->aggregateTestModulePathsFromComposerJson(); @@ -270,7 +256,7 @@ public function getModulesPath($verbosePath = false) return $this->enabledModulePaths; } - $enabledModules = array_merge($this->getEnabledModules(), $this->getModuleWhitelist()); + $enabledModules = array_merge($this->getEnabledModules(), $this->getModuleAllowlist()); $enabledDirectoryPaths = $this->flipAndFilterModulePathsArray($allModulePaths, $enabledModules); $this->enabledModulePaths = $this->applyCustomModuleMethods($enabledDirectoryPaths); @@ -289,112 +275,18 @@ public function sortFilesByModuleSequence(array $files) } /** - * Return an array of module whitelist that not exist in target Magento instance. + * Return an array of module allowlist that not exist in target Magento instance. * * @return array */ - protected function getModuleWhitelist() + protected function getModuleAllowlist() { - $moduleWhitelist = getenv(self::MODULE_WHITELIST); + $moduleAllowlist = getenv(self::MODULE_ALLOWLIST); - if (empty($moduleWhitelist)) { + if (empty($moduleAllowlist)) { return []; } - return array_map('trim', explode(',', $moduleWhitelist)); - } - - /** - * Retrieves all module directories which might contain pertinent test code. - * - * @return array - * @throws TestFrameworkException - */ - private function aggregateTestModulePaths() - { - $allModulePaths = []; - - // Define the Module paths from magento bp - $magentoBaseCodePath = FilePathFormatter::format(MAGENTO_BP, false); - - // Define the Module paths from default TESTS_MODULE_PATH - $modulePath = defined('TESTS_MODULE_PATH') ? TESTS_MODULE_PATH : TESTS_BP; - $modulePath = FilePathFormatter::format($modulePath, false); - - // If $modulePath is DEV_TESTS path, we don't need to search by pattern - if (strpos($modulePath, self::DEV_TESTS) === false) { - $codePathsToPattern[$modulePath] = ''; - } - - $vendorCodePath = DIRECTORY_SEPARATOR . self::VENDOR; - $codePathsToPattern[$magentoBaseCodePath . $vendorCodePath] = self::TEST_MFTF_PATTERN; - - $appCodePath = DIRECTORY_SEPARATOR . self::APP_CODE; - $codePathsToPattern[$magentoBaseCodePath . $appCodePath] = self::TEST_MFTF_PATTERN; - - foreach ($codePathsToPattern as $codePath => $pattern) { - $allModulePaths = array_merge_recursive($allModulePaths, $this->globRelevantPaths($codePath, $pattern)); - } - - return $allModulePaths; - } - - /** - * Function which takes a code path and a pattern and determines if there are any matching subdir paths. Matches - * are returned as an associative array keyed by basename (the last dir excluding pattern) to an array containing - * the matching path. - * - * @param string $testPath - * @param string $pattern - * @return array - */ - private function globRelevantPaths($testPath, $pattern) - { - $modulePaths = []; - $relevantPaths = []; - - if (file_exists($testPath)) { - $relevantPaths = $this->globRelevantWrapper($testPath, $pattern); - } - - foreach ($relevantPaths as $codePath) { - // Reduce magento/app/code/Magento/AdminGws/<pattern> to magento/app/code/Magento/AdminGws to read symlink - // Symlinks must be resolved otherwise they will not match Magento's filepath to the module - $potentialSymlink = str_replace(DIRECTORY_SEPARATOR . $pattern, "", $codePath); - if (is_link($potentialSymlink)) { - $codePath = realpath($potentialSymlink) . DIRECTORY_SEPARATOR . $pattern; - } - $mainModName = basename(str_replace($pattern, '', $codePath)); - $modulePaths[$codePath] = [$mainModName]; - - if (MftfApplicationConfig::getConfig()->verboseEnabled()) { - LoggingUtil::getInstance()->getLogger(ModuleResolver::class)->debug( - "including module", - ['module' => $mainModName, 'path' => $codePath] - ); - } - } - - return $modulePaths; - } - - /** - * Glob wrapper for globRelevantPaths function - * - * @param string $testPath - * @param string $pattern - * @return array - */ - private static function globRelevantWrapper($testPath, $pattern) - { - if ($pattern == "") { - return glob($testPath . '*' . DIRECTORY_SEPARATOR . '*' . $pattern); - } - $subDirectory = "*" . DIRECTORY_SEPARATOR; - $directories = glob($testPath . $subDirectory . $pattern, GLOB_ONLYDIR); - foreach (glob($testPath . $subDirectory, GLOB_ONLYDIR) as $dir) { - $directories = array_merge_recursive($directories, self::globRelevantWrapper($dir, $pattern)); - } - return $directories; + return array_map('trim', explode(',', $moduleAllowlist)); } /** @@ -421,30 +313,9 @@ private function aggregateTestModulePathsFromComposerJson() $searchCodePaths[] = $modulePath; } - return $this->getComposerJsonTestModulePaths($searchCodePaths); + return ModuleResolverService::getInstance()->getComposerJsonTestModulePaths($searchCodePaths); } - /** - * Retrieve all module code paths that have test module composer json files - * - * @param array $codePaths - * @return array - */ - private function getComposerJsonTestModulePaths($codePaths) - { - if (null !== $this->composerJsonModulePaths) { - return $this->composerJsonModulePaths; - } - try { - $this->composerJsonModulePaths = []; - $resolver = new ComposerModuleResolver(); - $this->composerJsonModulePaths = $resolver->getTestModulesFromPaths($codePaths); - } catch (TestFrameworkException $e) { - } - - return $this->composerJsonModulePaths; - } - /** * Aggregate all code paths with composer installed test modules * @@ -456,28 +327,7 @@ private function aggregateTestModulePathsFromComposerInstaller() $magentoBaseCodePath = MAGENTO_BP; $composerFile = $magentoBaseCodePath . DIRECTORY_SEPARATOR . 'composer.json'; - return $this->getComposerInstalledTestModulePaths($composerFile); - } - - /** - * Retrieve composer installed test module code paths - * - * @params string $composerFile - * @return array - */ - private function getComposerInstalledTestModulePaths($composerFile) - { - if (null !== $this->composerInstalledModulePaths) { - return $this->composerInstalledModulePaths; - } - try { - $this->composerInstalledModulePaths = []; - $resolver = new ComposerModuleResolver(); - $this->composerInstalledModulePaths = $resolver->getComposerInstalledTestModules($composerFile); - } catch (TestFrameworkException $e) { - } - - return $this->composerInstalledModulePaths; + return ModuleResolverService::getInstance()->getComposerInstalledTestModulePaths($composerFile); } /** @@ -494,8 +344,8 @@ private function flipAndFilterModulePathsArray($objectArray, $filterArray) // Filter array by enabled modules foreach ($objectArray as $path => $modules) { if (!array_diff($modules, $filterArray) - || (count($modules) == 1 && isset($this->knownDirectories[$modules[0]]))) { - if (count($modules) == 1) { + || (count($modules) === 1 && isset($this->knownDirectories[$modules[0]]))) { + if (count($modules) === 1) { $oneToOneArray[$path] = $modules[0]; } else { $oneToManyArray[$path] = $modules; @@ -585,7 +435,7 @@ private function setArrayValueWithLogging($inArray, $index, $value) } else { $warnMsg = 'Path: ' . $value . ' is ignored by ModuleResolver. ' . PHP_EOL . 'Path: '; $warnMsg .= $inArray[$index] . ' is set for Module: ' . $index . PHP_EOL; - LoggingUtil::getInstance()->getLogger(ModuleResolver::class)->warn($warnMsg); + LoggingUtil::getInstance()->getLogger(ModuleResolver::class)->warning($warnMsg); } return $outArray; } @@ -618,7 +468,7 @@ private function mergeModulePaths($oneToOneArray, $oneToManyArray) */ private function normalizeModuleNames($codePaths) { - $allComponents = $this->getRegisteredModuleList(); + $allComponents = ModuleResolverService::getInstance()->getRegisteredModuleList(); if (empty($allComponents)) { return $codePaths; } @@ -685,66 +535,6 @@ private function printMagentoVersionInfo() ); } - /** - * Get the API token for admin. - * - * @return string|boolean - */ - public function getAdminToken() - { - $login = $_ENV['MAGENTO_ADMIN_USERNAME'] ?? null; - $password = $_ENV['MAGENTO_ADMIN_PASSWORD'] ?? null; - if (!$login || !$password || !$this->getBackendUrl()) { - $message = "Cannot retrieve API token without credentials and base url, please fill out .env."; - $context = [ - "MAGENTO_BASE_URL" => getenv("MAGENTO_BASE_URL"), - "MAGENTO_BACKEND_BASE_URL" => getenv("MAGENTO_BACKEND_BASE_URL"), - "MAGENTO_ADMIN_USERNAME" => getenv("MAGENTO_ADMIN_USERNAME"), - "MAGENTO_ADMIN_PASSWORD" => getenv("MAGENTO_ADMIN_PASSWORD"), - ]; - throw new TestFrameworkException($message, $context); - } - - $url = $this->getBackendUrl() . $this->adminTokenUrl; - $data = [ - 'username' => $login, - 'password' => $password - ]; - $headers = [ - 'Content-Type: application/json', - ]; - - $ch = curl_init($url); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - - $response = curl_exec($ch); - $responseCode = curl_getinfo($ch)['http_code']; - - if ($responseCode !== 200) { - if ($responseCode == 0) { - $details = "Could not find Magento Backend Instance at MAGENTO_BACKEND_BASE_URL or MAGENTO_BASE_URL"; - } else { - $details = $responseCode . " " . Response::$statusTexts[$responseCode]; - } - - $message = "Could not retrieve API token from Magento Instance ({$details})"; - $context = [ - "tokenUrl" => $url, - "responseCode" => $responseCode, - "MAGENTO_ADMIN_USERNAME" => getenv("MAGENTO_ADMIN_USERNAME"), - "MAGENTO_ADMIN_PASSWORD" => getenv("MAGENTO_ADMIN_PASSWORD"), - ]; - throw new TestFrameworkException($message, $context); - } - - return json_decode($response); - } - /** * A wrapping method for any custom logic which needs to be applied to the module list * @@ -753,8 +543,8 @@ public function getAdminToken() */ protected function applyCustomModuleMethods($modulesPath) { - $modulePathsResult = $this->removeBlacklistModules($modulesPath); - $customModulePaths = $this->getCustomModulePaths(); + $modulePathsResult = $this->removeBlocklistModules($modulesPath); + $customModulePaths = ModuleResolverService::getInstance()->getCustomModulePaths(); array_map(function ($key, $value) { LoggingUtil::getInstance()->getLogger(ModuleResolver::class)->info( @@ -770,17 +560,17 @@ protected function applyCustomModuleMethods($modulesPath) } /** - * Remove blacklist modules from input module paths. + * Remove blocklist modules from input module paths. * * @param array $modulePaths * @return string[] */ - private function removeBlacklistModules($modulePaths) + private function removeBlocklistModules($modulePaths) { $modulePathsResult = $modulePaths; foreach ($modulePathsResult as $moduleName => $modulePath) { - // Remove module if it is in blacklist - if (in_array($moduleName, $this->getModuleBlacklist())) { + // Remove module if it is in blocklist + if (in_array($moduleName, $this->getModuleBlocklist())) { unset($modulePathsResult[$moduleName]); LoggingUtil::getInstance()->getLogger(ModuleResolver::class)->info( "excluding module", @@ -793,95 +583,13 @@ private function removeBlacklistModules($modulePaths) } /** - * Returns an array of custom module paths defined by the user + * Getter for moduleBlocklist. * * @return string[] */ - private function getCustomModulePaths() + private function getModuleBlocklist() { - $customModulePaths = []; - $paths = getenv(self::CUSTOM_MODULE_PATHS); - - if (!$paths) { - return $customModulePaths; - } - - foreach (explode(',', $paths) as $path) { - $customModulePaths = [$this->findVendorAndModuleNameFromPath(trim($path)) => $path]; - } - - return $customModulePaths; - } - - /** - * Getter for moduleBlacklist. - * - * @return string[] - */ - private function getModuleBlacklist() - { - return $this->moduleBlacklist; - } - - /** - * Calls Magento method for determining registered modules. - * - * @return string[] - */ - private function getRegisteredModuleList() - { - if (!empty($this->registeredModuleList)) { - return $this->registeredModuleList; - } - - if (array_key_exists('MAGENTO_BP', $_ENV)) { - $autoloadPath = realpath(MAGENTO_BP . "/app/autoload.php"); - if ($autoloadPath) { - require_once($autoloadPath); - } else { - throw new TestFrameworkException("Magento app/autoload.php not found with given MAGENTO_BP:" - . MAGENTO_BP); - } - } - - try { - $allComponents = []; - if (!class_exists(self::REGISTRAR_CLASS)) { - throw new TestFrameworkException("Magento Installation not found when loading registered modules.\n"); - } - $components = new \Magento\Framework\Component\ComponentRegistrar(); - foreach (self::PATHS as $componentType) { - $allComponents = array_merge($allComponents, $components->getPaths($componentType)); - } - array_walk($allComponents, function (&$value) { - // Magento stores component paths with unix DIRECTORY_SEPARATOR, need to stay uniform and convert - $value = realpath($value); - $value .= DIRECTORY_SEPARATOR . self::TEST_MFTF_PATTERN; - }); - return $allComponents; - } catch (TestFrameworkException $e) { - LoggingUtil::getInstance()->getLogger(ModuleResolver::class)->warning( - "$e" - ); - } - return []; - } - - /** - * Returns custom Backend URL if set, fallback to Magento Base URL - * @return string|null - */ - private function getBackendUrl() - { - try { - if (getenv('MAGENTO_BACKEND_BASE_URL')) { - return UrlFormatter::format(getenv('MAGENTO_BACKEND_BASE_URL')); - } else { - return UrlFormatter::format(getenv('MAGENTO_BASE_URL')); - } - } catch (TestFrameworkException $e) { - return null; - } + return $this->moduleBlocklist; } /** diff --git a/src/Magento/FunctionalTestingFramework/Util/ModuleResolver/ModuleResolverService.php b/src/Magento/FunctionalTestingFramework/Util/ModuleResolver/ModuleResolverService.php new file mode 100644 index 000000000..16d3e7d2b --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Util/ModuleResolver/ModuleResolverService.php @@ -0,0 +1,335 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\FunctionalTestingFramework\Util\ModuleResolver; + +use Magento\Framework\Component\ComponentRegistrar; +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; +use Magento\FunctionalTestingFramework\DataTransport\Auth\WebApiAuth; +use Magento\FunctionalTestingFramework\Exceptions\FastFailException; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Util\ComposerModuleResolver; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; +use Magento\FunctionalTestingFramework\Util\ModuleResolver; +use Magento\FunctionalTestingFramework\Util\Path\FilePathFormatter; + +class ModuleResolverService +{ + /** + * Singleton ModuleResolverCreator Instance. + * + * @var ModuleResolverService + */ + private static $INSTANCE; + + /** + * Composer json based test module paths. + * + * @var array + */ + private $composerJsonModulePaths = null; + + /** + * Composer installed test module paths. + * + * @var array + */ + private $composerInstalledModulePaths = null; + + /** + * ModuleResolverService constructor. + */ + private function __construct() + { + } + + /** + * Get ModuleResolverCreator instance. + * + * @return ModuleResolverService + */ + public static function getInstance() + { + if (self::$INSTANCE === null) { + self::$INSTANCE = new ModuleResolverService(); + } + + return self::$INSTANCE; + } + + /** + * Calls Magento method for determining registered modules. + * + * @return string[] + * @throws TestFrameworkException + */ + public function getRegisteredModuleList(): array + { + if (!empty($this->registeredModuleList)) { + return $this->registeredModuleList; + } + + if (array_key_exists('MAGENTO_BP', $_ENV)) { + $autoloadPath = realpath(MAGENTO_BP . "/app/autoload.php"); + + if ($autoloadPath) { + require_once($autoloadPath); + } else { + throw new TestFrameworkException( + "Magento app/autoload.php not found with given MAGENTO_BP:" . MAGENTO_BP + ); + } + } + + try { + $allComponents = []; + + if (!class_exists(ModuleResolver::REGISTRAR_CLASS)) { + throw new TestFrameworkException("Magento Installation not found when loading registered modules.\n"); + } + + $components = new ComponentRegistrar(); + + foreach (ModuleResolver::PATHS as $componentType) { + $allComponents = array_merge($allComponents, $components->getPaths($componentType)); + } + + array_walk($allComponents, function (&$value) { + // Magento stores component paths with unix DIRECTORY_SEPARATOR, need to stay uniform and convert + $value = realpath($value); + $value .= DIRECTORY_SEPARATOR . ModuleResolver::TEST_MFTF_PATTERN; + }); + + return $allComponents; + } catch (TestFrameworkException $exception) { + LoggingUtil::getInstance()->getLogger(ModuleResolver::class)->warning("$exception"); + } + return []; + } + + /** + * Function which takes a code path and a pattern and determines if there are any matching subdir paths. Matches + * are returned as an associative array keyed by basename (the last dir excluding pattern) to an array containing + * the matching path. + * + * @param string $testPath + * @param string $pattern + * + * @return array + * @throws TestFrameworkException + */ + public function globRelevantPaths(string $testPath, string $pattern): array + { + $modulePaths = []; + $relevantPaths = []; + + if (file_exists($testPath)) { + $relevantPaths = $this->globRelevantWrapper($testPath, $pattern); + } + + foreach ($relevantPaths as $codePath) { + $potentialSymlink = str_replace(DIRECTORY_SEPARATOR . $pattern, "", $codePath); + + if (is_link($potentialSymlink)) { + $codePath = realpath($potentialSymlink) . DIRECTORY_SEPARATOR . $pattern; + } + + $mainModName = basename(str_replace($pattern, '', $codePath)); + $modulePaths[$codePath] = [$mainModName]; + + if (MftfApplicationConfig::getConfig()->verboseEnabled()) { + LoggingUtil::getInstance()->getLogger(ModuleResolver::class)->debug( + "including module", + ['module' => $mainModName, 'path' => $codePath] + ); + } + } + + return $modulePaths; + } + + /** + * Glob wrapper for globRelevantPaths function. + * + * @param string $testPath + * @param string $pattern + * + * @return array + */ + private static function globRelevantWrapper(string $testPath, string $pattern): array + { + if ($pattern === '') { + return glob($testPath . '*' . DIRECTORY_SEPARATOR . '*' . $pattern); + } + + $subDirectory = '*' . DIRECTORY_SEPARATOR; + $directories = glob($testPath . $subDirectory . $pattern, GLOB_ONLYDIR); + + foreach (glob($testPath . $subDirectory, GLOB_ONLYDIR) as $dir) { + $directories = array_merge_recursive($directories, self::globRelevantWrapper($dir, $pattern)); + } + + return $directories; + } + + /** + * Retrieve all module code paths that have test module composer json files. + * + * @param array $codePaths + * + * @return array + */ + public function getComposerJsonTestModulePaths(array $codePaths): array + { + if (null !== $this->composerJsonModulePaths) { + return $this->composerJsonModulePaths; + } + + try { + $this->composerJsonModulePaths = []; + $resolver = new ComposerModuleResolver(); + $this->composerJsonModulePaths = $resolver->getTestModulesFromPaths($codePaths); + } catch (TestFrameworkException $e) { + } + + return $this->composerJsonModulePaths; + } + + /** + * Retrieve composer installed test module code paths. + * + * @param string $composerFile + * + * @return array + */ + public function getComposerInstalledTestModulePaths(string $composerFile): array + { + if (null !== $this->composerInstalledModulePaths) { + return $this->composerInstalledModulePaths; + } + + try { + $this->composerInstalledModulePaths = []; + $resolver = new ComposerModuleResolver(); + $this->composerInstalledModulePaths = $resolver->getComposerInstalledTestModules($composerFile); + } catch (TestFrameworkException $e) { + } + + return $this->composerInstalledModulePaths; + } + + /** + * Retrieves all module directories which might contain pertinent test code. + * + * @return array + * @throws TestFrameworkException + */ + public function aggregateTestModulePaths(): array + { + $allModulePaths = []; + + // Define the Module paths from magento bp + $magentoBaseCodePath = FilePathFormatter::format(MAGENTO_BP, false); + + // Define the Module paths from default TESTS_MODULE_PATH + $modulePath = defined('TESTS_MODULE_PATH') ? TESTS_MODULE_PATH : TESTS_BP; + $modulePath = FilePathFormatter::format($modulePath, false); + + // If $modulePath is DEV_TESTS path, we don't need to search by pattern + if (strpos($modulePath, ModuleResolver::DEV_TESTS) === false) { + $codePathsToPattern[$modulePath] = ''; + } + + $vendorCodePath = DIRECTORY_SEPARATOR . ModuleResolver::VENDOR; + $codePathsToPattern[$magentoBaseCodePath . $vendorCodePath] = ModuleResolver::TEST_MFTF_PATTERN; + + $appCodePath = DIRECTORY_SEPARATOR . ModuleResolver::APP_CODE; + $codePathsToPattern[$magentoBaseCodePath . $appCodePath] = ModuleResolver::TEST_MFTF_PATTERN; + + foreach ($codePathsToPattern as $codePath => $pattern) { + $allModulePaths = array_merge_recursive($allModulePaths, $this->globRelevantPaths($codePath, $pattern)); + } + + return $allModulePaths; + } + + /** + * Returns an array of custom module paths defined by the user. + * + * @return string[] + */ + public function getCustomModulePaths(): array + { + $customModulePaths = []; + $paths = getenv(ModuleResolver::CUSTOM_MODULE_PATHS); + + if (!$paths) { + return $customModulePaths; + } + + foreach (explode(',', $paths) as $path) { + $customModulePaths[$this->findVendorAndModuleNameFromPath(trim($path))] = $path; + } + + return $customModulePaths; + } + + /** + * Find vendor and module name from path. + * + * @param string $path + * + * @return string + */ + private function findVendorAndModuleNameFromPath(string $path): string + { + $path = str_replace(DIRECTORY_SEPARATOR . ModuleResolver::TEST_MFTF_PATTERN, '', $path); + + return $this->findVendorNameFromPath($path) . '_' . basename($path); + } + + /** + * Find vendor name from path. + * + * @param string $path + * + * @return string + */ + private function findVendorNameFromPath(string $path): string + { + $possibleVendorName = 'UnknownVendor'; + $dirPaths = [ + ModuleResolver::VENDOR, + ModuleResolver::APP_CODE, + ModuleResolver::DEV_TESTS + ]; + + foreach ($dirPaths as $dirPath) { + $regex = "~.+\\/" . $dirPath . "\/(?<" . ModuleResolver::VENDOR . ">[^\/]+)\/.+~"; + $match = []; + preg_match($regex, $path, $match); + + if (isset($match[ModuleResolver::VENDOR])) { + $possibleVendorName = ucfirst($match[ModuleResolver::VENDOR]); + return $possibleVendorName; + } + } + + return $possibleVendorName; + } + + /** + * Get admin token. + * + * @return string + * @throws FastFailException + */ + public function getAdminToken(): string + { + return WebApiAuth::getAdminToken(); + } +} diff --git a/src/Magento/FunctionalTestingFramework/Util/Path/FilePathFormatter.php b/src/Magento/FunctionalTestingFramework/Util/Path/FilePathFormatter.php index 8b496e739..092e060b5 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Path/FilePathFormatter.php +++ b/src/Magento/FunctionalTestingFramework/Util/Path/FilePathFormatter.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\FunctionalTestingFramework\Util\Path; @@ -11,14 +12,15 @@ class FilePathFormatter implements FormatterInterface { /** - * Return formatted full file path from input string, or false on error + * Return formatted full file path from input string, or false on error. * * @param string $path * @param boolean $withTrailingSeparator + * * @return string * @throws TestFrameworkException */ - public static function format($path, $withTrailingSeparator = true) + public static function format(string $path, bool $withTrailingSeparator = true): string { $validPath = realpath($path); diff --git a/src/Magento/FunctionalTestingFramework/Util/Path/FormatterInterface.php b/src/Magento/FunctionalTestingFramework/Util/Path/FormatterInterface.php index 11de71204..0da9fe6d8 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Path/FormatterInterface.php +++ b/src/Magento/FunctionalTestingFramework/Util/Path/FormatterInterface.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\FunctionalTestingFramework\Util\Path; @@ -11,12 +12,13 @@ interface FormatterInterface { /** - * Return formatted path (file path, url, etc) from input string, or false on error + * Return formatted path (file path, url, etc) from input string, or false on error. * * @param string $input * @param boolean $withTrailingSeparator + * * @return string * @throws TestFrameworkException */ - public static function format($input, $withTrailingSeparator = true); + public static function format(string $input, bool $withTrailingSeparator = true): string; } diff --git a/src/Magento/FunctionalTestingFramework/Util/Path/UrlFormatter.php b/src/Magento/FunctionalTestingFramework/Util/Path/UrlFormatter.php index 2ff1e430b..49f5e0e18 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Path/UrlFormatter.php +++ b/src/Magento/FunctionalTestingFramework/Util/Path/UrlFormatter.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\FunctionalTestingFramework\Util\Path; @@ -11,14 +12,15 @@ class UrlFormatter implements FormatterInterface { /** - * Return formatted url path from input string, or false on error + * Return formatted url path from input string. * * @param string $url * @param boolean $withTrailingSeparator + * * @return string * @throws TestFrameworkException */ - public static function format($url, $withTrailingSeparator = true) + public static function format(string $url, bool $withTrailingSeparator = true): string { $sanitizedUrl = rtrim($url, '/'); @@ -47,12 +49,13 @@ public static function format($url, $withTrailingSeparator = true) } /** - * Try to build missing url scheme and host + * Try to build missing url scheme and host. * * @param string $url + * * @return string */ - private static function buildUrl($url) + private static function buildUrl(string $url): string { $urlParts = parse_url($url); @@ -76,32 +79,42 @@ private static function buildUrl($url) /** * Returns url from $parts given, used with parse_url output for convenience. * This only exists because of deprecation of http_build_url, which does the exact same thing as the code below. + * * @param array $parts + * * @return string */ - private static function merge(array $parts) + private static function merge(array $parts): string { $get = function ($key) use ($parts) { - return isset($parts[$key]) ? $parts[$key] : null; + return $parts[$key] ?? ''; }; - $pass = $get('pass'); - $user = $get('user'); - $userinfo = $pass !== null ? "$user:$pass" : $user; - $port = $get('port'); - $scheme = $get('scheme'); - $query = $get('query'); - $fragment = $get('fragment'); - $authority = - ($userinfo !== null ? "$userinfo@" : '') . - $get('host') . - ($port ? ":$port" : ''); - - return - (strlen($scheme) ? "$scheme:" : '') . - (strlen($authority) ? "//$authority" : '') . - $get('path') . - (strlen($query) ? "?$query" : '') . - (strlen($fragment) ? "#$fragment" : ''); + $pass = $get('pass'); + $user = $get('user'); + $userinfo = $pass !== '' ? "$user:$pass" : $user; + $port = $get('port'); + $scheme = $get('scheme'); + $query = $get('query'); + $fragment = $get('fragment'); + $authority = ($userinfo !== '' ? "$userinfo@" : '') . $get('host') . ($port ? ":$port" : ''); + + return str_replace( + [ + '%scheme', + '%authority', + '%path', + '%query', + '%fragment' + ], + [ + strlen($scheme) ? "$scheme:" : '', + strlen($authority) ? "//$authority" : '', + $get('path'), + strlen($query) ? "?$query" : '', + strlen($fragment) ? "#$fragment" : '' + ], + '%scheme%authority%path%query%fragment' + ); } } diff --git a/src/Magento/FunctionalTestingFramework/Util/Script/ScriptUtil.php b/src/Magento/FunctionalTestingFramework/Util/Script/ScriptUtil.php index e1534d655..9cd02a076 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Script/ScriptUtil.php +++ b/src/Magento/FunctionalTestingFramework/Util/Script/ScriptUtil.php @@ -5,25 +5,43 @@ */ namespace Magento\FunctionalTestingFramework\Util\Script; +use Exception; use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; +use Magento\FunctionalTestingFramework\Exceptions\XmlException; +use Magento\FunctionalTestingFramework\Page\Objects\ElementObject; +use Magento\FunctionalTestingFramework\Page\Objects\SectionObject; +use Magento\FunctionalTestingFramework\Util\Path\FilePathFormatter; use Symfony\Component\Finder\Finder; use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; use Magento\FunctionalTestingFramework\Util\ModuleResolver; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; +use Magento\FunctionalTestingFramework\Page\Handlers\PageObjectHandler; +use Magento\FunctionalTestingFramework\Page\Handlers\SectionObjectHandler; +use Magento\FunctionalTestingFramework\Test\Handlers\ActionGroupObjectHandler; +use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; +use Magento\FunctionalTestingFramework\Test\Objects\ActionObject; +use Magento\FunctionalTestingFramework\Util\TestGenerator; /** * ScriptUtil class that contains helper functions for static and upgrade scripts * * @package Magento\FunctionalTestingFramework\Util\Script + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ScriptUtil { + const ACTIONGROUP_ARGUMENT_REGEX_PATTERN = '/<argument[^\/>]*name="([^"\']*)/'; + const ROOT_SUITE_DIR = 'tests/_suite'; + const DEV_TESTS_DIR = 'dev/tests/acceptance/'; + /** * Return all installed Magento module paths * * @return array * @throws TestFrameworkException */ - public static function getAllModulePaths() + public function getAllModulePaths(): array { MftfApplicationConfig::create( true, @@ -39,28 +57,61 @@ public static function getAllModulePaths() /** * Prints out given errors to file, and returns summary result string * @param array $errors - * @param string $filename + * @param string $filePath * @param string $message * @return string */ - public static function printErrorsToFile($errors, $filename, $message) + public function printErrorsToFile(array $errors, string $filePath, string $message): string { if (empty($errors)) { return $message . ": No errors found."; } - $outputPath = getcwd() . DIRECTORY_SEPARATOR . $filename . ".txt"; - $fileResource = fopen($outputPath, 'w'); + $this->printTofile($errors, $filePath); + + $errorCount = count($errors); + + return $message . ": Errors found across {$errorCount} file(s). Error details output to {$filePath}"; + } + + /** + * Prints out given warnings to file, and returns summary result string + * @param array $warnings + * @param string $filePath + * @param string $message + * @return string + */ + public function printWarningsToFile(array $warnings, string $filePath, string $message): string + { + if (empty($warnings)) { + return $message . ": No warnings found."; + } + $this->printTofile($warnings, $filePath); + $errorCount = count($warnings); + + return $message . ": Warnings found across {$errorCount} file(s). Warning details output to {$filePath}"; + } + + /** + * Writes contents to filePath + * @param array $contents + * @param string $filePath + * @return void + */ + private function printTofile(array $contents, string $filePath) + { + $dirname = dirname($filePath); + if (!file_exists($dirname)) { + mkdir($dirname, 0777, true); + } + + $fileResource = fopen($filePath, 'w'); - foreach ($errors as $test => $error) { + foreach ($contents as $test => $error) { fwrite($fileResource, $error[0] . PHP_EOL); } fclose($fileResource); - $errorCount = count($errors); - $output = $message . ": Errors found across {$errorCount} file(s). Error details output to {$outputPath}"; - - return $output; } /** @@ -70,7 +121,7 @@ public static function printErrorsToFile($errors, $filename, $message) * @param string $scope * @return Finder|array */ - public static function getModuleXmlFilesByScope($modulePaths, $scope) + public function getModuleXmlFilesByScope(array $modulePaths, string $scope) { $found = false; $scopePath = DIRECTORY_SEPARATOR . ucfirst($scope) . DIRECTORY_SEPARATOR; @@ -80,9 +131,211 @@ public static function getModuleXmlFilesByScope($modulePaths, $scope) if (!realpath($modulePath . $scopePath)) { continue; } - $finder->files()->followLinks()->in($modulePath . $scopePath)->name("*.xml"); + $finder->files()->followLinks()->in($modulePath . $scopePath)->name("*.xml")->sortByName(); $found = true; } return $found ? $finder->files() : []; } + + /** + * Return suite XML files in TESTS_BP/ROOT_SUITE_DIR directory + * + * @return Finder|array + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function getRootSuiteXmlFiles() + { + $rootSuitePaths = []; + $defaultTestPath = null; + $devTestsPath = null; + + try { + $defaultTestPath = FilePathFormatter::format(TESTS_BP); + } catch (TestFrameworkException $e) { + } + + try { + $devTestsPath = FilePathFormatter::format(MAGENTO_BP) . self::DEV_TESTS_DIR; + } catch (TestFrameworkException $e) { + } + + if ($defaultTestPath) { + $rootSuitePaths[] = $defaultTestPath . self::ROOT_SUITE_DIR; + } + + if ($devTestsPath && realpath($devTestsPath) && $devTestsPath !== $defaultTestPath) { + $rootSuitePaths[] = $devTestsPath . self::ROOT_SUITE_DIR; + } + + $found = false; + $finder = new Finder(); + foreach ($rootSuitePaths as $rootSuitePath) { + if (!realpath($rootSuitePath)) { + continue; + } + $finder->files()->followLinks()->in($rootSuitePath)->name("*.xml"); + $found = true; + } + + return $found ? $finder->files() : []; + } + + /** + * Resolve entity reference in {{entity.field}} or {{entity.field('param')}} + * + * @param array $braceReferences + * @param string $contents + * @param boolean $resolveSectionElement + * @return array + * @throws XmlException + */ + public function resolveEntityReferences($braceReferences, $contents, $resolveSectionElement = false) + { + $entities = []; + foreach ($braceReferences as $reference) { + // trim `{{data.field}}` to `data` + preg_match('/{{([^.]+)/', $reference, $entityName); + // Double check that {{data.field}} isn't an argument for an ActionGroup + $entity = $this->findEntity($entityName[1]); + preg_match_all(self::ACTIONGROUP_ARGUMENT_REGEX_PATTERN, $contents, $possibleArgument); + if (array_search($entityName[1], $possibleArgument[1]) !== false) { + continue; + } + if ($entity !== null) { + $entities[$entity->getName()] = $entity; + if ($resolveSectionElement) { + if (get_class($entity) === SectionObject::class) { + // trim `{{data.field}}` to `field` + preg_match('/.([^.]+)}}/', $reference, $elementName); + /** @var ElementObject $element */ + $element = $entity->getElement($elementName[1]); + if ($element) { + $entities[$entity->getName() . '.' . $elementName[1]] = $element; + } + } + } + } + } + return $entities; + } + + /** + * Drill down into params in {{ref.params('string', $data.key$, entity.reference)}} to resolve entity reference + * + * @param array $braceReferences + * @param string $contents + * @param boolean $resolveSectionElement + * @return array + * @throws XmlException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function resolveParametrizedReferences($braceReferences, $contents, $resolveSectionElement = false): array + { + $entities = []; + foreach ($braceReferences as $parameterizedReference) { + preg_match( + ActionObject::ACTION_ATTRIBUTE_VARIABLE_REGEX_PARAMETER, + $parameterizedReference, + $arguments + ); + $splitArguments = explode(',', ltrim(rtrim($arguments[0], ")"), "(")); + foreach ($splitArguments as $argument) { + // Do nothing for 'string' or $persisted.data$ + if (preg_match(ActionObject::STRING_PARAMETER_REGEX, $argument)) { + continue; + } elseif (preg_match(TestGenerator::PERSISTED_OBJECT_NOTATION_REGEX, $argument)) { + continue; + } + // trim `data.field` to `data` + preg_match('/([^.]+)/', $argument, $entityName); + // Double check that {{data.field}} isn't an argument for an ActionGroup + $entity = $this->findEntity($entityName[1]); + preg_match_all(self::ACTIONGROUP_ARGUMENT_REGEX_PATTERN, $contents, $possibleArgument); + if (array_search($entityName[1], $possibleArgument[1]) !== false) { + continue; + } + if ($entity !== null) { + $entities[$entity->getName()] = $entity; + if ($resolveSectionElement) { + if (get_class($entity) === SectionObject::class) { + // trim `data.field` to `field` + preg_match('/.([^.]+)/', $argument, $elementName); + /** @var ElementObject $element */ + $element = $entity->getElement($elementName[1]); + if ($element) { + $entities[$entity->getName() . '.' . $elementName[1]] = $element; + } + } + } + } + } + } + return $entities; + } + + /** + * Resolve entity by names + * + * @param array $references + * @return array + * @throws XmlException + */ + public function resolveEntityByNames(array $references): array + { + $entities = []; + foreach ($references as $reference) { + $entity = $this->findEntity($reference); + if ($entity !== null) { + $entities[$entity->getName()] = $entity; + } + } + return $entities; + } + + /** + * Attempts to find any MFTF entity by its name. Returns null if none are found + * + * @param string $name + * @return mixed + * @throws XmlException + * @throws Exception + */ + public function findEntity(string $name) + { + if ($name === '_ENV' || $name === '_CREDS') { + return null; + } + + if (DataObjectHandler::getInstance()->getObject($name)) { + return DataObjectHandler::getInstance()->getObject($name); + } elseif (PageObjectHandler::getInstance()->getObject($name)) { + return PageObjectHandler::getInstance()->getObject($name); + } elseif (SectionObjectHandler::getInstance()->getObject($name)) { + return SectionObjectHandler::getInstance()->getObject($name); + } elseif (ActionGroupObjectHandler::getInstance()->getObject($name)) { + return ActionGroupObjectHandler::getInstance()->getObject($name); + } + + try { + return TestObjectHandler::getInstance()->getObject($name); + } catch (TestReferenceException $e) { + } + return null; + } + + /** + * Return all XML files in given test name, empty array if no path is valid + * @param array $testNames + * @return array|Finder + */ + public function getModuleXmlFilesByTestNames(array $testNames) + { + $finder = new Finder(); + array_walk($testNames, function (&$value) { + $value = $value . ".xml"; + }); + $finder->files()->followLinks()->in(MAGENTO_BP)->name($testNames)->sortByName(); + + return $finder->files() ?? []; + } } diff --git a/src/Magento/FunctionalTestingFramework/Util/Script/TestDependencyUtil.php b/src/Magento/FunctionalTestingFramework/Util/Script/TestDependencyUtil.php new file mode 100644 index 000000000..bef8725e9 --- /dev/null +++ b/src/Magento/FunctionalTestingFramework/Util/Script/TestDependencyUtil.php @@ -0,0 +1,217 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\FunctionalTestingFramework\Util\Script; + +use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; +use Magento\FunctionalTestingFramework\Filter\FilterList; +use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; + +/** + * TestDependencyUtil class that contains helper functions for static and upgrade scripts + * + * @package Magento\FunctionalTestingFramework\Util\Script + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class TestDependencyUtil +{ + /** + * Array of FullModuleName => [dependencies] + * @var array + */ + private $allDependencies; + + /** + * Transactional Array to keep track of what dependencies have already been extracted. + * @var array + */ + private $alreadyExtractedDependencies; + + /** + * Builds and returns array of FullModuleNae => composer name + * @param array $moduleNameToPath + * @return array + */ + public function buildModuleNameToComposerName(array $moduleNameToPath): array + { + $moduleNameToComposerName = []; + foreach ($moduleNameToPath as $moduleName => $path) { + $composerData = json_decode(file_get_contents($path . DIRECTORY_SEPARATOR . "composer.json")); + $moduleNameToComposerName[$moduleName] = $composerData->name; + } + return $moduleNameToComposerName; + } + + /** + * Builds and returns flattened dependency list based on composer dependencies + * @param array $moduleNameToPath + * @param array $moduleNameToComposerName + * @return array + */ + public function buildComposerDependencyList(array $moduleNameToPath, array $moduleNameToComposerName): array + { + $flattenedDependencies = []; + + foreach ($moduleNameToPath as $moduleName => $pathToModule) { + $composerData = json_decode( + file_get_contents($pathToModule . DIRECTORY_SEPARATOR . "composer.json"), + true + ); + $this->allDependencies[$moduleName] = $composerData['require']; + } + foreach ($this->allDependencies as $moduleName => $dependencies) { + $this->alreadyExtractedDependencies = []; + $flattenedDependencies[$moduleName] = $this->extractSubDependencies($moduleName, $moduleNameToComposerName); + } + return $flattenedDependencies; + } + + /** + * Recursive function to fetch dependencies of given dependency, and its child dependencies + * @param string $subDependencyName + * @param array $moduleNameToComposerName + * @return array + */ + private function extractSubDependencies(string $subDependencyName, array $moduleNameToComposerName): array + { + $flattenedArray = []; + + if (in_array($subDependencyName, $this->alreadyExtractedDependencies)) { + return $flattenedArray; + } + + if (isset($this->allDependencies[$subDependencyName])) { + $subDependencyArray = $this->allDependencies[$subDependencyName]; + $flattenedArray = array_merge($flattenedArray, $this->allDependencies[$subDependencyName]); + + // Keep track of dependencies that have already been used, prevents circular dependency problems + $this->alreadyExtractedDependencies[] = $subDependencyName; + foreach ($subDependencyArray as $composerDependencyName => $version) { + $subDependencyFullName = array_search($composerDependencyName, $moduleNameToComposerName); + $flattenedArray = array_merge( + $flattenedArray, + $this->extractSubDependencies($subDependencyFullName, $moduleNameToComposerName) + ); + } + } + return $flattenedArray; + } + + /** + * Finds unique array composer dependencies of given testObjects + * @param array $allEntities + * @param array $moduleComposerName + * @param array $moduleNameToPath + * @return array + */ + public function getModuleDependenciesFromReferences( + array $allEntities, + array $moduleComposerName, + array $moduleNameToPath + ): array { + $filenames = []; + foreach ($allEntities as $item) { + // Should it append ALL filenames, including merges? + $allFiles = explode(",", $item->getFilename()); + foreach ($allFiles as $file) { + $moduleName = $this->getModuleName($file, $moduleNameToPath); + if (isset($moduleComposerName[$moduleName])) { + $composerModuleName = $moduleComposerName[$moduleName]; + $filenames[$item->getName()][] = $composerModuleName; + } + } + } + return $filenames; + } + + /** + * Return module name for a file path + * + * @param string $filePath + * @param array $moduleNameToPath + * @return string|null + */ + public function getModuleName(string $filePath, array $moduleNameToPath): ?string + { + $moduleName = null; + foreach ($moduleNameToPath as $name => $path) { + if (strpos($filePath, $path. "/") !== false) { + $moduleName = $name; + break; + } + } + return $moduleName; + } + + /** + * Return array of merge test modules and file path with same test name. + * @param array $testDependencies + * @param array $filterList + * @param array $extendedTestMapping + * @return array + */ + public function mergeDependenciesForExtendingTests( + array $testDependencies, + array $filterList, + array $extendedTestMapping = [] + ): array { + $testObjects = TestObjectHandler::getInstance()->getAllObjects(); + $filters = MftfApplicationConfig::getConfig()->getFilterList()->getFilters(); + $filteredTestNames = (count($filterList)>0)?$this->getFilteredTestNames($testObjects, $filters):[]; + $temp_array = array_reverse(array_column($testDependencies, "test_name"), true); + if (!empty($extendedTestMapping)) { + foreach ($extendedTestMapping as $value) { + $key = array_search($value["parent_test_name"], $temp_array); + if ($key !== false) { + #if parent test found merge this to child, for doing so just replace test name with child. + $testDependencies[$key]["test_name"] = $value["child_test_name"]; + } + } + } + $temp_array = []; + foreach ($testDependencies as $testDependency) { + $temp_array[$testDependency["test_name"]][] = $testDependency; + } + $testDependencies = []; + foreach ($temp_array as $testDependencyArray) { + if (( + empty($filterList)) || + isset($filteredTestNames[$testDependencyArray[0]["test_name"]]) + ) { + $testDependencies[] = [ + "file_path" => array_column($testDependencyArray, 'file_path'), + "full_name" => $testDependencyArray[0]["full_name"], + "test_name" => $testDependencyArray[0]["test_name"], + "test_modules" => array_values( + array_unique( + call_user_func_array( + 'array_merge', + array_column($testDependencyArray, 'test_modules') + ) + ) + ), + ]; + } + } + return $testDependencies; + } + + /** + * Return array of merge test modules and file path with same test name. + * @param array $testObjects + * @param array $filters + * @return array + */ + public function getFilteredTestNames(array $testObjects, array $filters) : array + { + foreach ($filters as $filter) { + $filter->filter($testObjects); + } + $testValues = array_map(function ($testObjects) { + return $testObjects->getName(); + }, $testObjects); + return $testValues; + } +} diff --git a/src/Magento/FunctionalTestingFramework/Util/Sorter/ParallelGroupSorter.php b/src/Magento/FunctionalTestingFramework/Util/Sorter/ParallelGroupSorter.php index 8f4cac8a2..479471f96 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Sorter/ParallelGroupSorter.php +++ b/src/Magento/FunctionalTestingFramework/Util/Sorter/ParallelGroupSorter.php @@ -5,7 +5,7 @@ */ namespace Magento\FunctionalTestingFramework\Util\Sorter; -use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; +use Magento\FunctionalTestingFramework\Exceptions\FastFailException; use Magento\FunctionalTestingFramework\Test\Handlers\TestObjectHandler; class ParallelGroupSorter @@ -26,19 +26,19 @@ public function __construct() } /** - * Function which returns tests and suites split according to desired number of lines divded into groups. + * Function which returns tests and suites split according to desired number of lines divided into groups. * * @param array $suiteConfiguration * @param array $testNameToSize * @param integer $time * @return array - * @throws TestFrameworkException + * @throws FastFailException */ public function getTestsGroupedBySize($suiteConfiguration, $testNameToSize, $time) { // we must have the lines argument in order to create the test groups if ($time == 0) { - throw new TestFrameworkException( + throw new FastFailException( "Please provide the argument '--time' to the robo command in order to". " generate grouped tests manifests for a parallel execution" ); @@ -75,6 +75,241 @@ public function getTestsGroupedBySize($suiteConfiguration, $testNameToSize, $tim return $testGroups; } + /** + * Function which returns tests and suites split according to desired number of groups. + * + * @param array $suiteConfiguration + * @param array $testNameToSize + * @param integer $groupTotal + * @return array + * @throws FastFailException + */ + public function getTestsGroupedByFixedGroupCount($suiteConfiguration, $testNameToSize, $groupTotal) + { + if (empty($suiteConfiguration)) { + return $this->convertArrayIndexStartingAtOne($this->splitTestsIntoGroups($testNameToSize, $groupTotal)); + } + + $suiteNameToTestSize = $this->getSuiteNameToTestSize($suiteConfiguration); + + // Calculate suite group totals + $suiteNameToGroupCount = $this->getSuiteGroupCounts($suiteNameToTestSize, $testNameToSize, $groupTotal); + + $suitesGroupTotal = array_sum($suiteNameToGroupCount); + + // Calculate minimum required groups + $minSuiteGroupTotal = count($suiteNameToTestSize); + $minTestGroupTotal = empty($testNameToSize) ? 0 : 1; + $minRequiredGroupTotal = $minSuiteGroupTotal + $minTestGroupTotal; + + if ($groupTotal < $minRequiredGroupTotal) { + throw new FastFailException( + "Invalid parameter 'groupTotal': must be equal or greater than {$minRequiredGroupTotal}" + ); + } elseif ($groupTotal < $suitesGroupTotal + $minTestGroupTotal) { + // Split in savvy mode when $groupTotal requested is very small + $testGroupTotal = $minTestGroupTotal; + // Reduce suite group total + $suiteNameToGroupCount = $this->reduceSuiteGroupTotal( + $suiteNameToGroupCount, + $groupTotal - $minTestGroupTotal + ); + } else { + // Calculate test group total + $testGroupTotal = $groupTotal - $suitesGroupTotal; + } + + // Split tests and suites + $testGroups = $this->splitTestsIntoGroups($testNameToSize, $testGroupTotal); + $testGroups = array_merge( + $testGroups, + $this->splitSuitesIntoGroups($suiteNameToTestSize, $suiteNameToGroupCount) + ); + + return $this->convertArrayIndexStartingAtOne($testGroups); + } + + /** + * Return suite's group counts from a group total + * + * @param array $suiteNameToTestSize + * @param array $testNameToSize + * @param integer $groupTotal + * @return array + */ + private function getSuiteGroupCounts($suiteNameToTestSize, $testNameToSize, $groupTotal) + { + if (empty($suiteNameToTestSize)) { + return []; + } + + // Calculate the minimum possible group time + $suiteNameToSize = $this->getSuiteToSize($suiteNameToTestSize); + $minGroupTime = ceil((array_sum($testNameToSize) + array_sum($suiteNameToSize)) / $groupTotal); + + // Find maximum suite time + $maxSuiteTime = max($suiteNameToSize); + + // Calculate 2 possible suite group times + $ceilSuiteGroupNumber = (int)ceil($maxSuiteTime / $minGroupTime); + $ceilSuiteGroupTime = max(ceil($maxSuiteTime / $ceilSuiteGroupNumber), $minGroupTime); + $floorSuiteGroupNumber = (int)floor($maxSuiteTime / $minGroupTime); + if ($floorSuiteGroupNumber != 0) { + $floorSuiteGroupTime = max(ceil($maxSuiteTime / $floorSuiteGroupNumber), $minGroupTime); + } + + // Calculate test group time for ceiling + $ceilSuiteNameToGroupCount = $this->getSuiteGroupCountFromGroupTime($suiteNameToTestSize, $ceilSuiteGroupTime); + $ceilSuiteGroupTotal = array_sum($ceilSuiteNameToGroupCount); + $ceilTestGroupTotal = $groupTotal - $ceilSuiteGroupTotal; + + if ($ceilTestGroupTotal == 0) { + $ceilTestGroupTime = 0; + } else { + $ceilTestGroupTime = ceil(array_sum($testNameToSize) / $ceilTestGroupTotal); + } + + // Set suite group total to ceiling + $suiteNameToGroupCount = $ceilSuiteNameToGroupCount; + + if (isset($floorSuiteGroupTime) && $ceilSuiteGroupTime != $floorSuiteGroupTime) { + // Calculate test group time for floor + $floorSuiteNameToGroupCount = $this->getSuiteGroupCountFromGroupTime( + $suiteNameToTestSize, + $floorSuiteGroupTime + ); + $floorSuiteGroupTotal = array_sum($floorSuiteNameToGroupCount); + $floorTestGroupTotal = $groupTotal - $floorSuiteGroupTotal; + if ($floorTestGroupTotal == 0) { + $floorTestGroupTime = 0; + } else { + $floorTestGroupTime = ceil(array_sum($testNameToSize) / $floorTestGroupTotal); + } + + // Choose the closer value between test group time and suite group time + $ceilDiff = abs($ceilTestGroupTime - $ceilSuiteGroupTime); + $floorDiff = abs($floorTestGroupTime - $floorSuiteGroupTime); + if ($ceilDiff > $floorDiff) { + // Adjust suite group total to floor + $suiteNameToGroupCount = $floorSuiteNameToGroupCount; + } + } + + return $suiteNameToGroupCount; + } + + /** + * Reduce total suite groups to a given $total. + * This method will reduce 1 from a suite that's greater than 1 repeatedly until sum of all groups reaches $total. + * + * @param array $suiteNameToGroupCount + * @param integer $total + * @return array + * @throws FastFailException + */ + private function reduceSuiteGroupTotal($suiteNameToGroupCount, $total) + { + if (count($suiteNameToGroupCount) > $total) { + throw new FastFailException( + "Invalid parameter 'total': must be equal or greater than {count($suiteNameToGroupCount)}" + ); + } + + $done = false; + while (!$done) { + foreach ($suiteNameToGroupCount as $suite => $count) { + if (array_sum($suiteNameToGroupCount) == $total) { + $done = true; + break; + } + if ($count > 1) { + $suiteNameToGroupCount[$suite] -= 1; + } + } + } + + return $suiteNameToGroupCount; + } + + /** + * Return array contains suitename to number of groups to be split based on time. + * + * @param array $suiteNameToTestSize + * @param integer $time + * @return array + */ + private function getSuiteGroupCountFromGroupTime($suiteNameToTestSize, $time) + { + $suiteNameToGroupCount = []; + foreach ($suiteNameToTestSize as $suiteName => $tests) { + $maxCount = count($tests); + $suiteTime = array_sum($tests); + if ($suiteTime <= $time) { + $suiteNameToGroupCount[$suiteName] = 1; + } else { + $suiteNameToGroupCount[$suiteName] = min((int)ceil($suiteTime/$time), $maxCount); + } + } + return $suiteNameToGroupCount; + } + + /** + * Split tests into given number of groups. + * + * @param array $tests + * @param integer $groupCnt + * @return array + */ + private function splitTestsIntoGroups($tests, $groupCnt) + { + if (empty($tests)) { + return []; + } + + // Reverse sort the test array by size + uasort($tests, function ($a, $b) { + return $a >= $b ? -1 : 1; + }); + $groups = array_fill(0, $groupCnt, []); + $sums = array_fill(0, $groupCnt, 0); + + foreach ($tests as $test => $size) { + // Always add the next test to the group with the smallest sum + $key = array_search(min($sums), $sums); + $groups[$key][$test] = $size; + $sums[$key] += $size; + } + // Filter empty array + return array_filter($groups); + } + + /** + * Split suites into given number of groups. + * + * @param array $suiteNameToTestSize + * @param array $suiteNameToGroupCount + * @return array + */ + private function splitSuitesIntoGroups($suiteNameToTestSize, $suiteNameToGroupCount) + { + $groups = []; + foreach ($suiteNameToTestSize as $suiteName => $suiteTests) { + $suiteCnt = $suiteNameToGroupCount[$suiteName]; + if ($suiteCnt == 1) { + $groups[][$suiteName] = array_sum($suiteTests); + $this->addSuiteToConfig($suiteName, null, $suiteTests); + } elseif ($suiteCnt > 1) { + $suiteGroups = $this->splitTestsIntoGroups($suiteTests, $suiteCnt); + foreach ($suiteGroups as $index => $tests) { + $newSuiteName = $suiteName . '_' . strval($index) . '_G'; + $groups[][$newSuiteName] = array_sum($tests); + $this->addSuiteToConfig($suiteName, $newSuiteName, $tests); + } + } + } + return $groups; + } + /** * Function which returns the newly formed suite objects created as a part of the sort * @@ -232,7 +467,7 @@ private function getSuiteToSize($suiteNamesToTests) * * E.g. * Input {suitename = 'sample', tests = ['test1' => 100,'test2' => 150, 'test3' => 300], linelimit = 275} - * Result { ['sample_01' => ['test3' => 300], 'sample_02' => ['test2' => 150, 'test1' => 100]] } + * Result { ['sample_01_G' => ['test3' => 300], 'sample_02_G' => ['test2' => 150, 'test1' => 100]] } * * @param string $suiteName * @param array $tests @@ -252,8 +487,8 @@ private function splitTestSuite($suiteName, $tests, $maxTime) } $group = $this->createTestGroup($maxTime, $test, $size, $availableTests); - $splitSuites["{$suiteName}_${splitCount}"] = $group; - $this->addSuiteToConfig($suiteName, "{$suiteName}_${splitCount}", $group); + $splitSuites["{$suiteName}_{$splitCount}_G"] = $group; + $this->addSuiteToConfig($suiteName, "{$suiteName}_{$splitCount}_G", $group); $availableTests = array_diff_key($availableTests, $group); $splitCount++; @@ -274,11 +509,28 @@ private function splitTestSuite($suiteName, $tests, $maxTime) */ private function addSuiteToConfig($originalSuiteName, $newSuiteName, $tests) { - if ($newSuiteName == null) { + if ($newSuiteName === null) { $this->suiteConfig[$originalSuiteName] = array_keys($tests); return; } $this->suiteConfig[$originalSuiteName][$newSuiteName] = array_keys($tests); } + + /** + * Convert array index starting at 1 + * + * @param array $inArray + * @return array + */ + private function convertArrayIndexStartingAtOne($inArray) + { + $outArray = []; + $index = 1; + foreach ($inArray as $value) { + $outArray[$index] = $value; + $index += 1; + } + return $outArray; + } } diff --git a/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php b/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php index 19319d0d7..01eab2996 100644 --- a/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php +++ b/src/Magento/FunctionalTestingFramework/Util/TestGenerator.php @@ -9,8 +9,10 @@ use Magento\FunctionalTestingFramework\Config\MftfApplicationConfig; use Magento\FunctionalTestingFramework\DataGenerator\Handlers\PersistedObjectHandler; use Magento\FunctionalTestingFramework\DataGenerator\Objects\EntityDataObject; +use Magento\FunctionalTestingFramework\Exceptions\FastFailException; use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; use Magento\FunctionalTestingFramework\Exceptions\TestReferenceException; +use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Filter\FilterInterface; use Magento\FunctionalTestingFramework\Suite\Handlers\SuiteObjectHandler; use Magento\FunctionalTestingFramework\Test\Handlers\ActionGroupObjectHandler; @@ -20,6 +22,8 @@ use Magento\FunctionalTestingFramework\Test\Objects\TestHookObject; use Magento\FunctionalTestingFramework\Test\Objects\TestObject; use Magento\FunctionalTestingFramework\Test\Util\BaseObjectExtractor; +use Magento\FunctionalTestingFramework\Util\Filesystem\CestFileCreatorUtil; +use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; use Magento\FunctionalTestingFramework\Util\Manifest\BaseTestManifest; use Magento\FunctionalTestingFramework\Test\Util\ActionObjectExtractor; use Magento\FunctionalTestingFramework\Util\Filesystem\DirSetupUtil; @@ -27,6 +31,7 @@ use Magento\FunctionalTestingFramework\Util\Path\FilePathFormatter; use Mustache_Engine; use Mustache_Loader_FilesystemLoader; +use Magento\FunctionalTestingFramework\DataGenerator\Handlers\DataObjectHandler; /** * Class TestGenerator @@ -153,7 +158,7 @@ private function __construct($exportDir, $tests, $debug = false) * @param boolean $debug * @return TestGenerator */ - public static function getInstance($dir = null, $tests = [], $debug = false) + public static function getInstance(?string $dir = null, $tests = [], $debug = false) { return new TestGenerator($dir, $tests, $debug); } @@ -174,6 +179,9 @@ public function getExportDir() * * @param array $testsToIgnore * @return array + * @throws TestReferenceException + * @throws TestFrameworkException + * @throws FastFailException */ private function loadAllTestObjects($testsToIgnore) { @@ -201,20 +209,13 @@ private function loadAllTestObjects($testsToIgnore) * * @param string $testPhp * @param string $filename + * * @return void - * @throws \Exception + * @throws TestFrameworkException */ private function createCestFile(string $testPhp, string $filename) { - $exportFilePath = $this->exportDirectory . DIRECTORY_SEPARATOR . $filename . ".php"; - $file = fopen($exportFilePath, 'w'); - - if (!$file) { - throw new \Exception(sprintf('Could not open test file: "%s"', $exportFilePath)); - } - - fwrite($file, $testPhp); - fclose($file); + CestFileCreatorUtil::getInstance()->create($filename, $this->exportDirectory, $testPhp); } /** @@ -224,10 +225,12 @@ private function createCestFile(string $testPhp, string $filename) * @param BaseTestManifest $testManifest * @param array $testsToIgnore * @return void + * @throws TestFrameworkException + * @throws XmlException + * @throws FastFailException * @throws TestReferenceException - * @throws \Exception */ - public function createAllTestFiles($testManifest = null, $testsToIgnore = null) + public function createAllTestFiles(?BaseTestManifest $testManifest = null, ?array $testsToIgnore = null) { if ($this->tests === null) { // no-op if the test configuration is null @@ -244,6 +247,73 @@ public function createAllTestFiles($testManifest = null, $testsToIgnore = null) $this->createCestFile($testPhpFile[1], $testPhpFile[0]); } } + + /** + * Throw exception if duplicate arguments found + * @param TestObject $testObject + * @return void + * @throws TestFrameworkException + */ + public function throwExceptionIfDuplicateArgumentsFound($testObject): void + { + if (!($testObject instanceof TestObject)) { + return; + } + $fileName = $testObject->getFilename(); + if (!empty($fileName) && file_exists($fileName)) { + return; + } + $fileContents = file_get_contents($fileName); + $parsedSteps = $testObject->getUnresolvedSteps(); + foreach ($parsedSteps as $parsedStep) { + if ($parsedStep->getType() !== 'actionGroup' && $parsedStep->getType() !== 'helper') { + continue; + } + $attributesActions = $parsedStep->getCustomActionAttributes(); + if (!key_exists('arguments', $attributesActions)) { + continue; + } + $arguments = $attributesActions['arguments']; + $stepKey = $parsedStep->getStepKey(); + + $fileToArr = explode("\n", $fileContents); + $actionGroupStart = false; + $argumentArray = []; + foreach ($fileToArr as $fileVal) { + $fileVal = trim($fileVal); + if ((str_contains($fileVal, '<actionGroup') || str_contains($fileVal, '<helper')) && + str_contains($fileVal, $stepKey)) { + $actionGroupStart = true; + continue; + } + if (str_contains($fileVal, '</actionGroup') || str_contains($fileVal, '</helper')) { + foreach ($arguments as $argumentName => $argumentValue) { + $argumentCounter = 0; + foreach ($argumentArray as $rawArgument) { + if (str_contains($rawArgument, '<argument') && + str_contains($rawArgument, 'name="'.$argumentName.'"')) { + $argumentCounter++; + } + if ($argumentCounter > 1) { + $err[] = sprintf( + 'Duplicate argument(%s) for stepKey: %s in test file: %s', + $argumentName, + $stepKey, + $testObject->getFileName() + ); + throw new TestFrameworkException(implode(PHP_EOL, $err)); + } + } + $actionGroupStart = false; + $argumentArray = []; + } + } + if ($actionGroupStart) { + $argumentArray[] = $fileVal; + } + } + } + } /** * Assemble the entire PHP string for a single Test based on a Test Object. @@ -256,6 +326,11 @@ public function createAllTestFiles($testManifest = null, $testsToIgnore = null) */ public function assembleTestPhp($testObject) { + if (!empty($testObject->getFilename()) && file_exists($testObject->getFilename())) { + $fileContents = file_get_contents($testObject->getFilename()); + $this->throwExceptionIfDuplicateArgumentsFound($fileContents, $testObject->getFilename()); + } + $this->customHelpers = []; $usePhp = $this->generateUseStatementsPhp(); $className = $testObject->getCodeceptionName(); @@ -269,7 +344,7 @@ public function assembleTestPhp($testObject) } catch (TestReferenceException $e) { throw new TestReferenceException($e->getMessage() . "\n" . $testObject->getFilename()); } - $classAnnotationsPhp = $this->generateAnnotationsPhp($testObject->getAnnotations()); + $classAnnotationsPhp = $this->generateAnnotationsPhp($testObject); $cestPhp = "<?php\n"; $cestPhp .= "namespace Magento\AcceptanceTest\\_" . $this->exportDirName . "\Backend;\n\n"; @@ -277,6 +352,10 @@ public function assembleTestPhp($testObject) $cestPhp .= $classAnnotationsPhp; $cestPhp .= sprintf("class %s\n", $className); $cestPhp .= "{\n"; + $cestPhp .= "\t/**\n"; + $cestPhp .= "\t * @var bool\n"; + $cestPhp .= "\t */\n"; + $cestPhp .= "\tprivate \$isSuccess = false;\n\n"; $cestPhp .= $this->generateInjectMethod(); $cestPhp .= $hookPhp; $cestPhp .= $testsPhp; @@ -320,6 +399,9 @@ private function generateInjectMethod() * @param BaseTestManifest $testManifest * @param array $testsToIgnore * @return array + * @throws TestFrameworkException + * @throws TestReferenceException + * @throws FastFailException */ private function assembleAllTestPhp($testManifest, array $testsToIgnore) { @@ -331,29 +413,58 @@ private function assembleAllTestPhp($testManifest, array $testsToIgnore) foreach ($filters as $filter) { $filter->filter($testObjects); } - foreach ($testObjects as $test) { - // Do not generate test if it is an extended test and parent does not exist - if ($test->isSkipped() && !empty($test->getParentName())) { - try { - TestObjectHandler::getInstance()->getObject($test->getParentName()); - } catch (TestReferenceException $e) { - print("{$test->getName()} will not be generated. Parent {$e->getMessage()} \n"); - continue; + try { + // Reset flag for new test + $removeLastTest = false; + + // Do not generate test if it is an extended test and parent does not exist + if ($test->isSkipped() && !empty($test->getParentName())) { + try { + TestObjectHandler::getInstance()->getObject($test->getParentName()); + } catch (TestReferenceException $e) { + TestObjectHandler::getInstance()->sanitizeTests([$test->getName()]); + $errMessage = "{$test->getName()} will not be generated. " + . "Parent test {$test->getParentName()} not defined in xml."; + // There are tests extend from non-existing parent on purpose on certain Magento editions. + // To keep backward compatibility, we will skip the test and continue + if (MftfApplicationConfig::getConfig()->verboseEnabled()) { + print("NOTICE: {$errMessage}"); + } + LoggingUtil::getInstance()->getLogger(self::class)->warning($errMessage); + continue; + } } - } - $this->debug("<comment>Start creating test: " . $test->getCodeceptionName() . "</comment>"); - $php = $this->assembleTestPhp($test); - $cestPhpArray[] = [$test->getCodeceptionName(), $php]; + $this->debug("<comment>Start creating test: " . $test->getCodeceptionName() . "</comment>"); + $php = $this->assembleTestPhp($test); + $cestPhpArray[] = [$test->getCodeceptionName(), $php]; + // Set flag in case something goes wrong + $removeLastTest = true; - $debugInformation = $test->getDebugInformation(); - $this->debug($debugInformation); - $this->debug("<comment>Finish creating test: " . $test->getCodeceptionName() . "</comment>" . PHP_EOL); + $debugInformation = $test->getDebugInformation(); + $this->debug($debugInformation); + $this->debug("<comment>Finish creating test: " . $test->getCodeceptionName() . "</comment>" . PHP_EOL); - //write to manifest here if manifest is not null - if ($testManifest != null) { - $testManifest->addTest($test); + // Write to manifest here if manifest is not null + if ($testManifest !== null) { + $testManifest->addTest($test); + } + } catch (FastFailException $e) { + throw $e; + } catch (\Exception $e) { + GenerationErrorHandler::getInstance()->addError( + 'test', + $test->getName(), + self::class . ': ' . $e->getMessage() + ); + LoggingUtil::getInstance()->getLogger(self::class)->error( + "Failed to generate {$test->getName()}" + ); + if ($removeLastTest) { + array_pop($cestPhpArray); + } + TestObjectHandler::getInstance()->sanitizeTests([$test->getName()]); } } @@ -408,12 +519,13 @@ private function generateUseStatementsPhp() /** * Generates Annotations PHP for given object, using given scope to determine indentation and additional output. * - * @param array $annotationsObject + * @param array $testObject * @param boolean $isMethod * @return string */ - private function generateAnnotationsPhp($annotationsObject, $isMethod = false) + private function generateAnnotationsPhp($testObject, $isMethod = false) { + $annotationsObject = $testObject->getAnnotations(); //TODO: Refactor to deal with PHPMD.CyclomaticComplexity if ($isMethod) { $indent = "\t"; @@ -425,11 +537,11 @@ private function generateAnnotationsPhp($annotationsObject, $isMethod = false) foreach ($annotationsObject as $annotationType => $annotationName) { //Remove conditional and output useCaseId upon completion of MQE-588 - if ($annotationType == "useCaseId") { + if ($annotationType === 'useCaseId') { continue; } if (!$isMethod) { - $annotationsPhp .= $this->generateClassAnnotations($annotationType, $annotationName); + $annotationsPhp .= $this->generateClassAnnotations($annotationType, $annotationName, $testObject); } else { $annotationsPhp .= $this->generateMethodAnnotations($annotationType, $annotationName); } @@ -451,7 +563,7 @@ private function generateAnnotationsPhp($annotationsObject, $isMethod = false) * @param string|null $annotationName * @return null|string */ - private function generateMethodAnnotations($annotationType = null, $annotationName = null) + private function generateMethodAnnotations(?string $annotationType = null, mixed $annotationName = null) { $annotationToAppend = null; $indent = "\t"; @@ -486,11 +598,7 @@ private function generateMethodAnnotations($annotationType = null, $annotationNa break; case null: - $annotationToAppend = sprintf( - "{$indent} * @Parameter(name = \"%s\", value=\"$%s\")\n", - "AcceptanceTester", - "I" - ); + $annotationToAppend = ""; $annotationToAppend .= sprintf("{$indent} * @param %s $%s\n", "AcceptanceTester", "I"); $annotationToAppend .= "{$indent} * @return void\n"; $annotationToAppend .= "{$indent} * @throws \Exception\n"; @@ -499,19 +607,41 @@ private function generateMethodAnnotations($annotationType = null, $annotationNa return $annotationToAppend; } + /** + * Returs required credentials to configure + * + * @param TestObject $testObject + * @return string + */ + public function requiredCredentials($testObject) + { + $requiredCredentials = (!empty($testObject->getCredentials())) + ? implode(",", $testObject->getCredentials()) + : ""; + + return $requiredCredentials; + } /** * Method which return formatted class level annotations based on type and name(s). * * @param string $annotationType * @param array $annotationName + * @param array $testObject * @return null|string * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - private function generateClassAnnotations($annotationType, $annotationName) + private function generateClassAnnotations($annotationType, $annotationName, $testObject) { $annotationToAppend = null; - + if (!$testObject->isSkipped() && !empty($annotationName['main'])) { + $requiredCredentialsMessage = $this->requiredCredentials($testObject); + $credMsg = "\n\n"."This test uses the following credentials:"."\n"; + $annotationName = (!empty($requiredCredentialsMessage)) ? + ['main'=>$annotationName['main'].', '.$credMsg.''.$requiredCredentialsMessage, + 'test_files'=> "\n".$annotationName['test_files'], 'deprecated'=>$annotationName['deprecated']] + : $annotationName; + } switch ($annotationType) { case "title": $annotationToAppend = sprintf(" * @Title(\"%s\")\n", $annotationName[0]); @@ -610,6 +740,7 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato $function = null; $time = null; $locale = null; + $currency = null; $username = null; $password = null; $width = null; @@ -654,7 +785,11 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato $sortOrder = $customActionAttributes['sortOrder']; } - if (isset($customActionAttributes['userInput']) && isset($customActionAttributes['url'])) { + if (isset($customActionAttributes['userInput']) + && isset($customActionAttributes['locale']) + && isset($customActionAttributes['currency'])) { + $input = $this->parseUserInput($customActionAttributes['userInput']); + } elseif (isset($customActionAttributes['userInput']) && isset($customActionAttributes['url'])) { $input = $this->addUniquenessFunctionCall($customActionAttributes['userInput']); $url = $this->addUniquenessFunctionCall($customActionAttributes['url']); } elseif (isset($customActionAttributes['userInput'])) { @@ -707,12 +842,12 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato } if (in_array($actionObject->getType(), ActionObject::COMMAND_ACTION_ATTRIBUTES)) { - $time = $time ?? ActionObject::DEFAULT_COMMAND_WAIT_TIMEOUT; + $time = $time ?? ActionObject::getDefaultMagentoCLIWaitTimeout(); } else { $time = $time ?? ActionObject::getDefaultWaitTimeout(); } - if (isset($customActionAttributes['parameterArray']) && $actionObject->getType() != 'pressKey') { + if (isset($customActionAttributes['parameterArray']) && $actionObject->getType() !== 'pressKey') { // validate the param array is in the correct format $this->validateParameterArray($customActionAttributes['parameterArray']); @@ -731,6 +866,11 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato $selector = $this->addUniquenessFunctionCall($customActionAttributes['selector']); $selector = $this->resolveLocatorFunctionInAttribute($selector); } + if (isset($customActionAttributes['count'])) { + $countClickValue = $customActionAttributes['count']; + $countValue = $this->addUniquenessFunctionCall($countClickValue); + $countValue = $this->resolveLocatorFunctionInAttribute($countValue); + } if (isset($customActionAttributes['selector1']) || isset($customActionAttributes['filterSelector'])) { $selectorOneValue = $customActionAttributes['selector1'] ?? $customActionAttributes['filterSelector']; @@ -759,7 +899,7 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato $function = trim($function, '"'); } // turn $javaVariable => \$javaVariable but not {$mftfVariable} - if ($actionObject->getType() == "executeJS") { + if ($actionObject->getType() === "executeJS") { $function = preg_replace('/(?<!{)(\$[A-Za-z._]+)(?![A-z.]*+\$)/', '\\\\$1', $function); } } @@ -772,6 +912,10 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato $locale = $this->wrapWithDoubleQuotes($customActionAttributes['locale']); } + if (isset($customActionAttributes['currency'])) { + $currency = $this->wrapWithDoubleQuotes($customActionAttributes['currency']); + } + if (isset($customActionAttributes['username'])) { $username = $this->wrapWithDoubleQuotes($customActionAttributes['username']); } @@ -850,18 +994,23 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato $stepKey, $customActionAttributes['class'] . '::' . $customActionAttributes['method'] ); - $testSteps .= $this->wrapFunctionCall($actor, $actionObject, $arguments); + $testSteps .= $this->wrapFunctionCallWithReturnValue( + $stepKey, + $actor, + $actionObject, + $arguments + ); break; case "createData": $entity = $customActionAttributes['entity']; - + $this->entityExistsCheck($entity, $stepKey); //TODO refactor entity field override to not be individual actionObjects $customEntityFields = $customActionAttributes[ActionObjectExtractor::ACTION_OBJECT_PERSISTENCE_FIELDS] ?? []; $requiredEntityKeys = []; foreach ($actionObject->getCustomActionAttributes() as $actionAttribute) { - if (is_array($actionAttribute) && $actionAttribute['nodeName'] == 'requiredEntity') { + if (is_array($actionAttribute) && $actionAttribute['nodeName'] === 'requiredEntity') { //append ActionGroup if provided $requiredEntityActionGroup = $actionAttribute['actionGroup'] ?? null; $requiredEntityKeys[] = $actionAttribute['createDataKey'] . $requiredEntityActionGroup; @@ -872,7 +1021,6 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato if (!empty($requiredEntityKeys)) { $requiredEntityKeysArray = '"' . implode('", "', $requiredEntityKeys) . '"'; } - $scope = $this->getObjectScope($generationScope); $createEntityFunctionCall = "\t\t\${$actor}->createEntity("; @@ -933,7 +1081,7 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato // Build array of requiredEntities $requiredEntityKeys = []; foreach ($actionObject->getCustomActionAttributes() as $actionAttribute) { - if (is_array($actionAttribute) && $actionAttribute['nodeName'] == 'requiredEntity') { + if (is_array($actionAttribute) && $actionAttribute['nodeName'] === 'requiredEntity') { //append ActionGroup if provided $requiredEntityActionGroup = $actionAttribute['actionGroup'] ?? null; $requiredEntityKeys[] = $actionAttribute['createDataKey'] . $requiredEntityActionGroup; @@ -968,7 +1116,7 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato // Build array of requiredEntities $requiredEntityKeys = []; foreach ($actionObject->getCustomActionAttributes() as $actionAttribute) { - if (is_array($actionAttribute) && $actionAttribute['nodeName'] == 'requiredEntity') { + if (is_array($actionAttribute) && $actionAttribute['nodeName'] === 'requiredEntity') { $requiredEntityActionGroup = $actionAttribute['actionGroup'] ?? null; $requiredEntityKeys[] = $actionAttribute['createDataKey'] . $requiredEntityActionGroup; } @@ -1044,6 +1192,7 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato $parameterArray ); break; + case "grabCookieAttributes": case "grabCookie": $testSteps .= $this->wrapFunctionCallWithReturnValue( $stepKey, @@ -1081,6 +1230,7 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato break; case "selectOption": case "unselectOption": + case "seeNumberOfElements": $testSteps .= $this->wrapFunctionCall( $actor, $actionObject, @@ -1108,6 +1258,14 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato $y ); break; + case "rapidClick": + $testSteps .= $this->wrapFunctionCall( + $actor, + $actionObject, + $selector, + $countValue + ); + break; case "selectMultipleOptions": $testSteps .= $this->wrapFunctionCall( $actor, @@ -1162,13 +1320,24 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato $selector ); break; - case "formatMoney": + case "return": + $actionOrigin = $actionObject->getActionOrigin(); + $actionOriginStepKey = $actionOrigin[ActionGroupObject::ACTION_GROUP_ORIGIN_TEST_REF]; + $testSteps .= $this->wrapFunctionCallWithReturnValue( + $actionOriginStepKey, + $actor, + $actionObject, + $value + ); + break; + case "formatCurrency": $testSteps .= $this->wrapFunctionCallWithReturnValue( $stepKey, $actor, $actionObject, $input, - $locale + $locale, + $currency ); break; case "mSetLocale": @@ -1195,10 +1364,12 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato ); break; case "grabPageSource": + case "getOTP": $testSteps .= $this->wrapFunctionCallWithReturnValue( $stepKey, $actor, - $actionObject + $actionObject, + $input ); break; case "resizeWindow": @@ -1244,15 +1415,6 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato case "seeOptionIsSelected": $testSteps .= $this->wrapFunctionCall($actor, $actionObject, $selector, $input); break; - case "seeNumberOfElements": - $testSteps .= $this->wrapFunctionCall( - $actor, - $actionObject, - $selector, - $input, - $parameterArray - ); - break; case "seeInPageSource": case "dontSeeInPageSource": case "seeInSource": @@ -1272,15 +1434,12 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato $visible ); break; - case "assertEquals": case "assertGreaterOrEquals": case "assertGreaterThan": case "assertGreaterThanOrEqual": - case "assertInternalType": case "assertLessOrEquals": case "assertLessThan": case "assertLessThanOrEqual": - case "assertNotEquals": case "assertInstanceOf": case "assertNotInstanceOf": case "assertNotRegExp": @@ -1294,6 +1453,10 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato case "assertCount": case "assertContains": case "assertNotContains": + case "assertStringContainsString": + case "assertStringContainsStringIgnoringCase": + case "assertStringNotContainsString": + case "assertStringNotContainsStringIgnoringCase": case "expectException": $testSteps .= $this->wrapFunctionCall( $actor, @@ -1304,6 +1467,31 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato $assertDelta ); break; + case "assertEquals": + case "assertNotEquals": + case "assertEqualsIgnoringCase": + case "assertNotEqualsIgnoringCase": + case "assertEqualsCanonicalizing": + case "assertNotEqualsCanonicalizing": + $testSteps .= $this->wrapFunctionCall( + $actor, + $actionObject, + $assertExpected, + $assertActual, + $assertMessage + ); + break; + case "assertEqualsWithDelta": + case "assertNotEqualsWithDelta": + $testSteps .= $this->wrapFunctionCall( + $actor, + $actionObject, + $assertExpected, + $assertActual, + $assertDelta, + $assertMessage + ); + break; case "assertElementContainsAttribute": // If a blank string or null is passed in we need to pass a blank string to the function. if (empty($assertExpected)) { @@ -1334,16 +1522,6 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato $assertMessage ); break; - case "assertArraySubset": - $testSteps .= $this->wrapFunctionCall( - $actor, - $actionObject, - $assertExpected, - $assertActual, - $assertIsStrict, - $assertMessage - ); - break; case "fail": $testSteps .= $this->wrapFunctionCall( $actor, @@ -1392,11 +1570,12 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato $actionObject->getActionOrigin() )[0]; $argRef = "\t\t\$"; - $input = $this->resolveAllRuntimeReferences([$input])[0]; + $input = (isset($actionObject->getCustomActionAttributes()['unique'])) ? + $this->getUniqueIdForInput($actionObject->getCustomActionAttributes()['unique'], $input) + : $input; $argRef .= str_replace(ucfirst($fieldKey), "", $stepKey) . - "Fields['{$fieldKey}'] = ${input};"; - + "Fields['{$fieldKey}'] = {$input};"; $testSteps .= $argRef; break; case "generateDate": @@ -1412,6 +1591,16 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato $testSteps .= $dateGenerateCode; break; + case "pause": + $pauseAttr = $actionObject->getCustomActionAttributes( + ActionObject::PAUSE_ACTION_INTERNAL_ATTRIBUTE + ); + if ($pauseAttr) { + $testSteps .= sprintf("\t\t$%s->%s(%s);", $actor, $actionObject->getType(), 'true'); + } else { + $testSteps .= sprintf("\t\t$%s->%s();", $actor, $actionObject->getType()); + } + break; case "comment": $input = $input === null ? strtr($value, ['$' => '\$', '{' => '\{', '}' => '\}']) : $input; // Combining userInput from native XML comment and <comment/> action to fall-through 'default' case @@ -1429,10 +1618,24 @@ public function generateStepsPhp($actionObjects, $generationScope = TestGenerato } $testSteps .= PHP_EOL; } - return $testSteps; } + /** + * Get unique value appended to input string + * + * @param string $uniqueValue + * @param string $input + * @return string + */ + public function getUniqueIdForInput($uniqueValue, $input) + { + $input = ($uniqueValue == 'prefix') + ? '"'.uniqid().str_replace('"', '', $input).'"' + : '"'.str_replace('"', '', $input).uniqid().'"'; + return $input; + } + /** * Resolves Locator:: in given $attribute if it is found. * @@ -1512,7 +1715,7 @@ private function replaceMatchesIntoArg($matches, &$outputArg) $replacement = null; $delimiter = '$'; $variable = $this->stripAndSplitReference($match, $delimiter); - if (count($variable) != 2) { + if (count($variable) !== 2) { throw new \Exception( "Invalid Persisted Entity Reference: {$match}. Test persisted entity references must follow {$delimiter}entityStepKey.field{$delimiter} format." @@ -1564,7 +1767,7 @@ private function processQuoteBreaks($match, $argument, $replacement) */ private function resolveStepKeyReferences($input, $actionGroupOrigin, $matchAll = false) { - if ($actionGroupOrigin == null) { + if ($actionGroupOrigin === null) { return $input; } $output = $input; @@ -1626,7 +1829,7 @@ private function wrapFunctionArgsWithQuotes($functionRegex, $input) foreach ($allArguments as $argument) { $argument = trim($argument); - if ($argument[0] == self::ARRAY_WRAP_OPEN) { + if ($argument[0] === self::ARRAY_WRAP_OPEN) { $replacement = $this->wrapParameterArray($this->addUniquenessToParamArray($argument)); } elseif (is_numeric($argument)) { $replacement = $argument; @@ -1668,6 +1871,10 @@ private function generateHooksPhp($hookObjects) { $hooks = ""; + if (!isset($hookObjects['after'])) { + $hookObjects['after'] = new TestHookObject('after', '', []); + } + foreach ($hookObjects as $hookObject) { $type = $hookObject->getType(); $dependencies = 'AcceptanceTester $I'; @@ -1686,9 +1893,40 @@ private function generateHooksPhp($hookObjects) throw new TestReferenceException($e->getMessage() . " in Element \"" . $type . "\""); } + if ($type === 'before' && $steps) { + $steps = sprintf( + "\t\t$%s->comment('[%s]');" . PHP_EOL, + 'I', + 'START BEFORE HOOK' + ) . $steps; + $steps = $steps . sprintf( + "\t\t$%s->comment('[%s]');" . PHP_EOL, + 'I', + 'END BEFORE HOOK' + ); + } + + if ($type === 'after' && $steps) { + $steps = sprintf( + "\t\t$%s->comment('[%s]');" . PHP_EOL, + 'I', + 'START AFTER HOOK' + ) . $steps; + $steps = $steps . sprintf( + "\t\t$%s->comment('[%s]');" . PHP_EOL, + 'I', + 'END AFTER HOOK' + ); + } + $hooks .= sprintf("\tpublic function _{$type}(%s)\n", $dependencies); $hooks .= "\t{\n"; $hooks .= $steps; + if ($type === 'after') { + $hooks .= "\t\t" . 'if ($this->isSuccess) {' . "\n"; + $hooks .= "\t\t\t" . 'unlink(__FILE__);' . "\n"; + $hooks .= "\t\t" . '}' . "\n"; + } $hooks .= "\t}\n\n"; } @@ -1710,7 +1948,7 @@ private function generateTestPhp($test) $testName = $test->getName(); $testName = str_replace(' ', '', $testName); - $testAnnotations = $this->generateAnnotationsPhp($test->getAnnotations(), true); + $testAnnotations = $this->generateAnnotationsPhp($test, true); $dependencies = 'AcceptanceTester $I'; if (!$test->isSkipped() || MftfApplicationConfig::getConfig()->allowSkipped()) { try { @@ -1726,7 +1964,8 @@ private function generateTestPhp($test) } else { $skipString .= "No issues have been specified."; } - $steps = "\t\t" . '$scenario->skip("' . $skipString . '");' . "\n"; + $steps = "\t\t" . 'unlink(__FILE__);' . "\n"; + $steps .= "\t\t" . '$scenario->skip("' . $skipString . '");' . "\n"; $dependencies .= ', \Codeception\Scenario $scenario'; } @@ -1736,6 +1975,14 @@ private function generateTestPhp($test) $testPhp .= $steps; $testPhp .= "\t}\n"; + if (!isset($skipString)) { + $testPhp .= PHP_EOL; + $testPhp .= sprintf("\tpublic function _passed(%s)\n", $dependencies); + $testPhp .= "\t{\n"; + $testPhp .= "\t\t// Test passed successfully." . PHP_EOL; + $testPhp .= "\t\t\$this->isSuccess = true;" . PHP_EOL; + $testPhp .= "\t}\n"; + } return $testPhp; } @@ -1873,7 +2120,7 @@ private function addUniquenessFunctionCall($input, $wrapWithDoubleQuotes = true) */ private function wrapWithDoubleQuotes($input) { - if ($input == null) { + if ($input === null || $input === '') { return ''; } //Only replace " with \" so that it doesn't break outer string. @@ -1907,6 +2154,24 @@ private function addDollarSign($input) return sprintf("$%s", ltrim($this->stripQuotes($input), '$')); } + /** + * Check if the entity exists + * + * @param string $entity + * @param string $stepKey + * @return void + * @throws TestReferenceException + */ + public function entityExistsCheck($entity, $stepKey) + { + $retrievedEntity = DataObjectHandler::getInstance()->getObject($entity); + if ($retrievedEntity === null) { + throw new TestReferenceException( + "Test generation failed as entity \"" . $entity . "\" does not exist. at stepkey ".$stepKey + ); + } + } + /** * Wrap parameters into a function call. * @@ -1918,16 +2183,7 @@ private function addDollarSign($input) */ private function wrapFunctionCall($actor, $action, ...$args) { - $isFirst = true; - $isActionHelper = $action->getType() === 'helper'; - $actionType = $action->getType(); - if ($isActionHelper) { - $actor = "this->helperContainer->get('" . $action->getCustomActionAttributes()['class'] . "')"; - $args = $args[0]; - $actionType = $action->getCustomActionAttributes()['method']; - } - - $output = sprintf("\t\t$%s->%s(", $actor, $actionType); + $output = sprintf("\t\t$%s->%s(", $actor, $action->getType()); for ($i = 0; $i < count($args); $i++) { if (null === $args[$i]) { continue; @@ -1948,17 +2204,22 @@ private function wrapFunctionCall($actor, $action, ...$args) /** * Wrap parameters into a function call with a return value. * - * @param string $returnVariable - * @param string $actor - * @param string $action - * @param array ...$args + * @param string $returnVariable + * @param string $actor + * @param actionObject $action + * @param array ...$args * @return string * @throws \Exception */ private function wrapFunctionCallWithReturnValue($returnVariable, $actor, $action, ...$args) { - $isFirst = true; - $output = sprintf("\t\t$%s = $%s->%s(", $returnVariable, $actor, $action->getType()); + $actionType = $action->getType(); + if ($actionType === 'helper') { + $actor = "this->helperContainer->get('" . $action->getCustomActionAttributes()['class'] . "')"; + $args = $args[0]; + $actionType = $action->getCustomActionAttributes()['method']; + } + $output = sprintf("\t\t$%s = $%s->%s(", $returnVariable, $actor, $actionType); for ($i = 0; $i < count($args); $i++) { if (null === $args[$i]) { continue; @@ -2001,18 +2262,20 @@ private function resolveRuntimeReference($args, $regex, $func) $newArgs = []; foreach ($args as $key => $arg) { - preg_match_all($regex, $arg, $matches); - if (!empty($matches[0])) { - $fullMatch = $matches[0][0]; - $refVariable = $matches[1][0]; - unset($matches); - $replacement = "{$func}(\"{$refVariable}\")"; - - $outputArg = $this->processQuoteBreaks($fullMatch, $arg, $replacement); - $newArgs[$key] = $outputArg; - continue; - } $newArgs[$key] = $arg; + if ($arg !== null) { + preg_match_all($regex, $arg, $matches); + if (!empty($matches[0])) { + foreach ($matches[0] as $matchKey => $fullMatch) { + $refVariable = $matches[1][$matchKey]; + $replacement = $this->getReplacement($func, $refVariable); + $outputArg = $this->processQuoteBreaks($fullMatch, $newArgs[$key], $replacement); + $newArgs[$key] = $outputArg; + } + unset($matches); + continue; + } + } } // override passed in args for use later. @@ -2079,7 +2342,7 @@ private function isWrappedArray(string $paramArray) * @return string|null * @throws TestReferenceException */ - private function resolveValueByType($value = null, $type = null) + private function resolveValueByType(?string $value = null, ?string $type = null) { if (null === $value) { return null; @@ -2201,6 +2464,7 @@ private function validateXmlAttributesMutuallyExclusive($key, $tagName, $attribu 'excludes' => [ 'dontSeeCookie', 'grabCookie', + 'grabCookieAttributes', 'resetCookie', 'seeCookie', 'setCookie', @@ -2261,4 +2525,43 @@ private function hasDecimalPoint(string $outStr) { return strpos($outStr, localeconv()['decimal_point']) !== false; } + + /** + * Parse action attribute `userInput` + * + * @param string $userInput + * @return string + */ + private function parseUserInput($userInput) + { + $floatPattern = '/^\s*([+-]?[0-9]*\.?[0-9]+)\s*$/'; + preg_match($floatPattern, $userInput, $float); + if (isset($float[1])) { + return $float[1]; + } + + $intPattern = '/^\s*([+-]?[0-9]+)\s*$/'; + preg_match($intPattern, $userInput, $int); + if (isset($int[1])) { + return $int[1]; + } + + return $this->addUniquenessFunctionCall($userInput); + } + + /** + * Supports fallback for BACKEND URL + * + * @param string $func + * @param string $refVariable + * @return string + */ + private function getReplacement($func, $refVariable): string + { + if ($refVariable === 'MAGENTO_BACKEND_BASE_URL') { + return "({$func}(\"{$refVariable}\") ? rtrim({$func}(\"{$refVariable}\"), \"/\") : \"\")"; + } + + return "{$func}(\"{$refVariable}\")"; + } } diff --git a/src/Magento/FunctionalTestingFramework/Util/Validation/DuplicateNodeValidationUtil.php b/src/Magento/FunctionalTestingFramework/Util/Validation/DuplicateNodeValidationUtil.php index 0c323a2dd..2eba2c493 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Validation/DuplicateNodeValidationUtil.php +++ b/src/Magento/FunctionalTestingFramework/Util/Validation/DuplicateNodeValidationUtil.php @@ -65,10 +65,10 @@ public function validateChildUniqueness(\DOMElement $parentNode, $filename, $par $withoutDuplicates = array_unique($keyValues); - if (count($withoutDuplicates) != count($keyValues)) { + if (count($withoutDuplicates) !== count($keyValues)) { $duplicates = array_diff_assoc($keyValues, $withoutDuplicates); $keyError = ""; - foreach ($duplicates as $duplicateKey => $duplicateValue) { + foreach ($duplicates as $duplicateValue) { $keyError .= "\t{$this->uniqueKey}: {$duplicateValue} is used more than once."; if ($parentKey !== null) { $keyError .=" (Parent: {$parentKey})"; diff --git a/src/Magento/FunctionalTestingFramework/Util/Validation/NameValidationUtil.php b/src/Magento/FunctionalTestingFramework/Util/Validation/NameValidationUtil.php index 0112af14e..bef552da3 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Validation/NameValidationUtil.php +++ b/src/Magento/FunctionalTestingFramework/Util/Validation/NameValidationUtil.php @@ -6,7 +6,7 @@ namespace Magento\FunctionalTestingFramework\Util\Validation; -use Exception; +use Magento\FunctionalTestingFramework\Exceptions\TestFrameworkException; use Magento\FunctionalTestingFramework\Exceptions\XmlException; use Magento\FunctionalTestingFramework\Util\Logger\LoggingUtil; @@ -40,7 +40,7 @@ public function __construct() } /** - * Function which runs a validation against the blacklisted char defined in this class. Validation occurs to insure + * Function which runs a validation against the blocklisted char defined in this class. Validation occurs to insure * allure report does not error/future devOps builds do not error against illegal char. * * @param string $name @@ -68,7 +68,7 @@ public static function validateName($name, $type) } if (!empty($illegalCharArray)) { - $errorMessage = "{$type} name \"${name}\" contains illegal characters, please fix and re-run."; + $errorMessage = "{$type} name \"{$name}\" contains illegal characters, please fix and re-run."; foreach ($illegalCharArray as $diffChar) { $errorMessage .= "\nTest names cannot contain '{$diffChar}'"; @@ -84,12 +84,12 @@ public static function validateName($name, $type) * @param string $str * @param string $type * @param string $filename - * @throws Exception + * @throws TestFrameworkException * @return void */ public function validatePascalCase($str, $type, $filename = null) { - if (!ctype_upper($str[0])) { + if (!is_string($str) || !ctype_upper($str[0])) { $message = "The {$type} {$str} should be PascalCase with an uppercase first letter."; if ($filename !== null) { @@ -112,12 +112,12 @@ public function validatePascalCase($str, $type, $filename = null) * @param string $str * @param string $type * @param string $filename - * @throws Exception + * @throws TestFrameworkException * @return void */ public function validateCamelCase($str, $type, $filename = null) { - if (!ctype_lower($str[0])) { + if (!is_string($str) || !ctype_lower($str[0])) { $message = "The {$type} {$str} should be camelCase with a lowercase first letter."; if ($filename !== null) { @@ -140,7 +140,7 @@ public function validateCamelCase($str, $type, $filename = null) * @param string $str * @param string $type * @param string $filename - * @throws Exception + * @throws TestFrameworkException * @return void */ public function validateAffixes($str, $type, $filename = null) @@ -170,7 +170,7 @@ public function validateAffixes($str, $type, $filename = null) * Outputs the number of validations detected by this instance. * * @param string $type - * @throws Exception + * @throws TestFrameworkException * @return void */ public function summarize($type) diff --git a/src/Magento/FunctionalTestingFramework/Util/Validation/SingleNodePerFileValidationUtil.php b/src/Magento/FunctionalTestingFramework/Util/Validation/SingleNodePerFileValidationUtil.php index c560349be..0fd440888 100644 --- a/src/Magento/FunctionalTestingFramework/Util/Validation/SingleNodePerFileValidationUtil.php +++ b/src/Magento/FunctionalTestingFramework/Util/Validation/SingleNodePerFileValidationUtil.php @@ -43,7 +43,7 @@ public function validateSingleNodeForTag($dom, $tag, $filename = '') { $tagNodes = $dom->getElementsByTagName($tag); $count = $tagNodes->length; - if ($count == 1) { + if ($count === 1) { return; } diff --git a/src/Magento/FunctionalTestingFramework/Util/msq.php b/src/Magento/FunctionalTestingFramework/Util/msq.php index 3968e3c82..b6f03ae6c 100644 --- a/src/Magento/FunctionalTestingFramework/Util/msq.php +++ b/src/Magento/FunctionalTestingFramework/Util/msq.php @@ -13,7 +13,7 @@ * @param null $id * @return string */ - function msq($id = null) + function msq(?string $id = null) { if ($id and isset(MagentoSequence::$hash[$id])) { return MagentoSequence::$hash[$id]; @@ -34,7 +34,7 @@ function msq($id = null) * @param null $id * @return string */ - function msqs($id = null) + function msqs(?string $id = null) { if ($id and isset(MagentoSequence::$suiteHash[$id])) { return MagentoSequence::$suiteHash[$id]; diff --git a/src/Magento/FunctionalTestingFramework/_bootstrap.php b/src/Magento/FunctionalTestingFramework/_bootstrap.php index e35b20bad..9b7f7a821 100644 --- a/src/Magento/FunctionalTestingFramework/_bootstrap.php +++ b/src/Magento/FunctionalTestingFramework/_bootstrap.php @@ -6,6 +6,8 @@ */ // define framework basepath for schema pathing +use Symfony\Component\Dotenv\Exception\PathException; + defined('FW_BP') || define('FW_BP', realpath(__DIR__ . '/../../../')); // get the root path of the project $projectRootPath = substr(FW_BP, 0, strpos(FW_BP, DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR)); @@ -21,8 +23,11 @@ //Load constants from .env file if (file_exists(ENV_FILE_PATH . '.env')) { - $env = new \Dotenv\Loader(ENV_FILE_PATH . '.env'); - $env->load(); + $env = new \Symfony\Component\Dotenv\Dotenv(); + if (function_exists('putenv')) { + $env->usePutenv(); + } + $env->populate($env->parse(file_get_contents(ENV_FILE_PATH . '.env'), ENV_FILE_PATH . '.env'), true); if (array_key_exists('TESTS_MODULE_PATH', $_ENV) xor array_key_exists('TESTS_BP', $_ENV)) { throw new Exception( @@ -42,16 +47,20 @@ 'MAGENTO_CLI_COMMAND_PATH', 'dev/tests/acceptance/utils/command.php' ); - $env->setEnvironmentVariable('MAGENTO_CLI_COMMAND_PATH', MAGENTO_CLI_COMMAND_PATH); - defined('MAGENTO_CLI_COMMAND_PARAMETER') || define('MAGENTO_CLI_COMMAND_PARAMETER', 'command'); - $env->setEnvironmentVariable('MAGENTO_CLI_COMMAND_PARAMETER', MAGENTO_CLI_COMMAND_PARAMETER); - defined('DEFAULT_TIMEZONE') || define('DEFAULT_TIMEZONE', 'America/Los_Angeles'); - $env->setEnvironmentVariable('DEFAULT_TIMEZONE', DEFAULT_TIMEZONE); - defined('WAIT_TIMEOUT') || define('WAIT_TIMEOUT', 30); - $env->setEnvironmentVariable('WAIT_TIMEOUT', WAIT_TIMEOUT); + defined('VERBOSE_ARTIFACTS') || define('VERBOSE_ARTIFACTS', false); + $env->populate( + [ + 'MAGENTO_CLI_COMMAND_PATH' => MAGENTO_CLI_COMMAND_PATH, + 'MAGENTO_CLI_COMMAND_PARAMETER' => MAGENTO_CLI_COMMAND_PARAMETER, + 'DEFAULT_TIMEZONE' => DEFAULT_TIMEZONE, + 'WAIT_TIMEOUT' => WAIT_TIMEOUT, + 'VERBOSE_ARTIFACTS' => VERBOSE_ARTIFACTS, + ], + true + ); try { new DateTimeZone(DEFAULT_TIMEZONE); @@ -71,9 +80,3 @@ 'TESTS_MODULE_PATH', realpath(TESTS_BP . $RELATIVE_TESTS_MODULE_PATH) ); - -// add the debug flag here -$debugMode = $_ENV['MFTF_DEBUG'] ?? false; -if (!(bool)$debugMode && extension_loaded('xdebug')) { - xdebug_disable(); -}